撤销某一个具体的节点

1
2

git revert a7d8dcaa4e25667c9aaaf2a0809aca7fe1a27b6d

但是当这个节点是一个 merge 节点的时候, 你根本无法去 revert 这个节点,因为你不知道 merge 的哪一条分支应该作为主线,哪个分支作为他撤销后的主分支,因此git 会输出错误信息:

1
error: commit cae5381823aad7c285d017e5cf7e8bc4b7b12240 is a merge but no -m option was given.

让我们来看看这个 m 参数代表什么

Usually you cannot revert a merge because you do not know which side of the merge should be considered the mainline. This option specifies the parent number (starting from 1) of the mainline and allows revert to reverse the change relative to the specified parent.

Reverting a merge commit declares that you will never want the tree changes brought in by the merge. As a result, later merges will only bring in tree changes introduced by commits that are not ancestors of the previously reverted merge. This may or may not be what you want

翻译过来就是

通常情况下,你无法 revert 一个 merge,因为你不知道 merge 的哪一条线应该被视为主线。这个选项(-m)指定了主线的 parent 的代号(从1开始),并允许以相对于指定的 parent 的进行 revert。

revert 一个 merge commit 意味着你将完全不想要来自 merge commit 带来的 tree change。 因此,之后的 merge 只会引入那些不是之前被 revert 的那个 merge 的祖先引入的 tree change,这可能是也可能不是你想要的。

如同下图所示,revert 到 ‘thumbup-feature’ 的时候不知道 到底是 revert 到绿色的线还是紫色的线。

也可以使用 git show 命令查看相关信息

1
2
3
4
5
6
7
8
9
git show

输出内容:

commit cae5381823aad7c285d017e5cf7e8bc4b7b12240
Merge: edf99ca 125cfdd
Date: Thu Apr 12 18:27:21 2018 +0800

Merge tag 'thumbup-feature'

如下图标示的 parent1 对应

执行 git revert cae5381 -m 1

1
2
3
4
5
6
7
8
9
10
11
12
13
git revert cae5381 -m 1

输出:

Revert "Merge tag 'thumbup-feature'"

This reverts commit cae5381823aad7c285d017e5cf7e8bc4b7b12240, reversing
changes made to edf99ca31755a27b0a43b290263ed810833a95c4.

[master f0aac26] Revert "Merge tag 'thumbup-feature'"
2 files changed, 2 deletions(-)
delete mode 100644 file3
delete mode 100644 file4

复制代码file3 和 file4 是 feature branch 上的 commit 引入的文件,被正确地删掉了

执行 git revert cae5381 -m 2

1
2
3
4
5
6
7
8
9
git reset --hard d208cba 
git revert cae5381 -m 2

输出

[master 2c5a0ee] Revert "Merge tag 'thumbup-feature'"
2 files changed, 2 deletions(-)
delete mode 100644 file5
delete mode 100644 file6

这种 revert 把 master 在 feature branch 期间进行的 commit 都给干掉了。

本文绝大部分的内容和数据都引用自 ULIVZ 的文章:

要回答这个问题,需要知道什么是编程范式。

编程范式

函数式编程范式是一种 编程范式
我们通常见到的编程范式有 命令式编程(Imperative programming)函数式编程逻辑式编程,面向对象编程也属于命令式编程的一种。

命令式编程

命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元)、赋值语句(获取指令和存储指令)表达式(内存引用)和控制语句(跳转指令)。总而言之,命令式编程就是一个冯诺伊曼机的指令序列**。

函数式编程的本质

而函数式编程是面向数学的抽象,将计算描述为一种表达式值,可以这么说:一个函数就是一个表达式值

函数式编程中的函数这个术语不是说计算机中的函数(实际上为 Subroutine,而是指数学中的函数,也就是自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如 sqrt(x)函数计算x的平方根,只要 x 不变,无论何时调用,调用几次值都是不变的。

在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。

纯函数式编程中的变量也不是命令式编程中的变量,即存储状态状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中的那样多次给一个变量赋值。 例如 在命令式编程语言中我们写的“x = x + 1”,这依赖可变状态的事实,拿给程序看是正确的,但是拿给数学家看,却被认为这个等式为假。

函数式语言的如条件语句,循环语句也不是命令式编程语言中的控制语句,而是函数式编程中的语法糖。

严格意思上的函数式编程意味着不使用可变的变量,赋值,循环和其他命令式结构进行编程。

从理论上说,函数式语言也不是通过冯诺伊曼体系结构的机器上运行的,而是通过λ演算来运行的,就是通过变量替换的方式进行,变量替换为其值或表达式,函数也替换为其表达式,并根据运算符进行计算。λ演算是图灵完全(Turing completeness)的,但是大多数情况,函数式程序还是被编译成(冯诺依曼机的)机器语言的指令执行的。

函数式编程的好处

由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。

一个好处是,函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。

由于函数是引用透明的,以及函数式编程不像命令式编程那样关注执行步骤,这个系统提供了优化函数式程序的空间,包括惰性求值和并性处理。

还有一个好处是,由于函数式语言是面向数学的抽象,更接近人的语言,而不是机器语言,代码会比较简洁,也更容易被理解。

函数式编程的特性

由于变量值是不可变的,对于值的操作并不是修改原来的值,而是修改新产生的值,原来的值保持不便。例如一个Point类,其moveBy方法不是改变已有Point实例的x和y坐标值,而是返回一个新的Point实例。

1
2
3
4
5
6
7
class Point(x: Int, y: Int){
override def toString() = "Point (" + x + ", " + y + ")"

def moveBy(deltaX: Int, deltaY: Int) = {
new Point(x + deltaX, y + deltaY)
}
}

(示例来源:Anders Hejlsberg在echDays 2010上的演讲)

同样由于变量不可变,纯函数编程语言无法实现循环,这是因为for循环使用可变的状态作为计数器,而While循环或DoWhile循环需要可变的状态作为跳出循环的条件。因此在函数式语言里就只能使用递归来解决迭代问题,这使得函数式编程严重依赖递归。

通常来说,算法都有 递推 (iterative)递归(recursive) 两种定义,以阶乘为例,阶乘的递推定义为:

阶乘的定义为

递推定义的计算时需要使用一个累积器保存每个迭代的中间计算结果,Java代码如下:

1
2
3
4
5
6
7
public static int fact(int n){
int acc = 1;
for(int k = 1; k <= n; k++){
acc = acc * k;
}
return acc;
}

而递归定义的计算的Scala代码如下:

1
2
3
4
def fact(n: Int):Int= {
if(n == 0) return 1
n * fact(n-1)
}

我们可以看到,没有使用循环,没有使用可变的状态,函数更短小,不需要显示地使用累积器保存中间计算结果,而是使用参数n(在栈上分配)来保存中间计算结果。

当然,这样的递归调用有更高的开销和局限(调用栈深度),那么尽量把递归写成尾递归的方式,编译器会自动优化为循环。

一般来说,递归这种方式于循环相比被认为是更符合人的思维的,即告诉机器做什么,而不是告诉机器怎么做。递归还是有很强大的表现力的。

函数式语言中的特性

  • 高阶函数(High-order function)
  • 偏函数(Particial Applied function)
  • Currying
  • 闭包 (closure)
  • 惰性求值 (Lazy evaluation)、(call by need)
  1. 高阶函数

1
2
3
function sum(funcA, funcB, value) {
return funcA(value) + funcB(value)
}

  1. 偏函数

偏函数则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数

偏函数和 currying 有一定的联系

引用 functional-programming-jargon 的话来说就是:

Curried functions are automatically partially applied.

1
2
3
4
5
6
7
8
9
10
11
function add(a, b) {
return a + b
}

// 计算 a、b 的和
add(a, b)

// 使用偏函数
var addOne = partial(add, 1)

addOne(2)

  1. Currying

1
2
3
4
5
6
7
8
9
10
function add(a, b) {
return a + b;
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3

4.惰性求值

1
2
3
4
var a = function (b) {
return b
}
a(3)

总结

函数式编程是给软件开发者提供的另一套工具箱,为我们提供了另外一种抽象和思考的方式。 函数式编程也有不太擅长的场合,比如处理可变状态和处理IO,要么引入可变变量,要么通过Monad来进行封装(如State Monad和IO Monad)

参考资料

其他等待理解的

  • 图灵机完备
  • lamda 演算
  • SICP
  • Haskell
  • Monald

我所做的一切,不过是在取悦一个影子

什么是 Promise ?

  1. 定义:Promise 是一个对象,他会在未来的某个时刻生成一个值,已完成 (resolved)的值或者一个没有完成的理由。
  2. 特性:Promise 存在三个状态 FULLFILLED (已完成)、REJECTED (已拒绝)、PENDING (等待中)
  3. 回调:Promise 的使用者可以附上回调函数来处理已完成的值或者拒绝的原因

Promise 是热切的,一旦 promise 的构造函数被调用,它就会开始执行你交给它的任务。如果你需要一个懒惰的,请看 observables 或者 task

Promise 用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

const promise = new Promise(function(resolve, reject) {
// Your code
if (/* 异步操作成功 */) {
resolve(value)
} else {
reject(value)
}
})

promise.then(function(value) {
// success
}, function (error) {
// error
})

Promise Polifil

Promise API

Promise 规范有很多,Promise A、 Promise B、 Promise A+、Promise D

目前 ES6 的 Promise 是基于 Promise A+ 实现的

我们知道 Promise 是一个构造函数,需要用 new 调用,并有以下几个 api:( 从哪里知道? 怎么知道?)

API:

1
2
3
4
5
6
7
8
9
10

function Promise(resolver) {}

Promise.prototype.then = function(onResolved) {}
Promise.prototype.catch = function(onRejected) {}

Promise.resolve = function() {}
Promise.reject = function() {}
Promise.race = function() {}
Promise.all = function() {}

同步任务(存放在执行栈)

异步任务 (存放在异步队列) 异步任务又分为宏任务和微任务

Chrome:

宏任务 Task: setTimeout setInterval setImmediate(IE专用) messageChannel

等待执行栈和微任务队列都执行完毕才会执行,并且在执行完每一个宏任务之后,会去看看微任务队列有没有新添加的任务,如果有,会先将微任务队列中的任务清空,才会继续执行下一个宏任务

微任务 microTask:promise MutationObserver…

当执行栈中的代码执行完毕,会在执行宏任务队列之前先看看微任务队列中有没有任务,如果有会先将微任务队列中的任务清空才会去执行宏任务队列

Node:

宏任务 Task: setTimeout setInterval…

微任务 microTask: process.nextTric promise setImmediate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var isFulfilled = false;

var d = new Promise(function (resolve, reject) {

setTimeout(function () {

resolve(2);

isFulfilled = true;

}, 50);

});

d.then(function onFulfilled() {

console.log(isFulfilled == true)

});

Promise 的零碎实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
const PENDING = "PENDING"
const RESOLVED = "RESOLVED"
const REJECTED = "REJECTED"

// 值穿透

// 构造函数
class MyPromise {

constructor(exexutor) {

this.status = PENDING
this.value = undefined

// 回调函数
this.onResolvedCallback = []
this.onRejectedCallback = []

// inner resolve
let _resolved = (value) => {
/**
* 这里增加 setTimeout 是为了避免这种情况的出现
* promise2 = new MyPromise(function(resolve, reject) {
* promise2().then(() => {
* console.log(1111)
* })
* })
*
*/
setTimeout(() => {
if (this.status === PENDING) {
this.status = RESOLVED
this.value = value
// 针对非链式调用
/**
* P = new Promise()
*
* P.then()
*
* P.then()
*/
this.onResolvedCallback.forEach(cb => cb(value))
}
})
}

// inner reject
let _rejected = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.value = reason
// 针对非链式调用
this.onRejectedCallback.forEach(cb => cb(reason))
}
}

try {
exexutor(_resolved, _rejected)
} catch (err) {
_rejected(err)
}
}

// 原型链表
then(onResolved, onRejected) {
let promise2
let self = this

// 根据标准规定, 如果 then 的参数不是一个函数那么, 就抛出
onResolved = typeof onResolved === 'function' ? onResolved : function (value) { return value }
onRejected = typeof onRejected === 'function' ? onRejected : function (reason) { return reason }

if (this.status === PENDING) {
// 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,
// 只能等到Promise的状态确定后,才能确实如何处理。
// 所以我们需要把我们的**两种情况**的处理逻辑做为callback放入promise1(此处即this/self)的回调数组里
// 逻辑本身跟第一个if块内的几乎一致,此处不做过多解释
return promise2 = new MyPromise(function (resolve, reject) {
self.onResolvedCallback.push(function (value) {
try {
var x = onResolved(self.value)
if (x instanceof MyPromise) {
x.then(resolve, reject)
}
resolve(x)
} catch (e) {
reject(e)
}
})

self.onRejectedCallback.push(function (reason) {
try {
var x = onRejected(self.value)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
})
}

if (this.status === RESOLVED) {
return promise2 = new MyPromise(function (resolve, rejected) {
try {
x = onResolved(self.value)
if (x instanceof MyPromise) { // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
x.then(resolve, rejected)
}
resolve(x) // 否则,以它的返回值做为promise2的结果
} catch (err) {
rejected(err)
}
})
}

if (this.status === REJECTED) {
return promise2 = new MyPromise(function (resolve, rejected) {
try {
x = onRejected(self.value)
if (x instanceof MyPromise) { // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
x.then(resolve, rejected)
}
} catch (err) {
rejected(err)
}
})
}

}
}

// then 方法


var isFulfilled = false;

var d = new MyPromise(function (resolve, reject) {

setTimeout(function () {

resolve(2);

isFulfilled = true;

}, 50);

});

d.then(function onFulfilled() {

console.log(isFulfilled == true)

});


// 测试

let promise = new MyPromise(function (resolve, rejected) {
setTimeout(() => {
console.log('pending')
resolve()
}, 1000);
}).then(() => {
console.log(2)
})


let promise2 = new Promise(function (resolve, rejected) {
setTimeout(() => {
console.log('pending')
resolve()
}, 1000);
}).then(() => {
console.log(2)
})

参考

后续

拓展

今天在将 node 从 8.6.0 升级到 node 12.0.4 时候遇到这么一个问题:

这是因为 gnulib glib 和 glibc 的版本过低导致的,那么问题来了 gnulib glib 和 glibc 分别指的什么呢 ?

答: glibc 是一个 C 语言 运行时库, 例如 printf(3) fopen(3)

答: glib 是一个基于 C 语言的工具库,同时也是一个面向对象的事件循环

答: gnulib 是一个适配 POSIX API 和 native API 的库。

那么问题又来了,到底什么是 runtime library 呢?

答: 运行时库就是程序运行的时候所需要依赖的库。运行时库就是程序运行的时候所需要依赖的库。
运行的时候指的是指令加载到内存并由CPU执行的时候。
C代码编译成可执行文件的时候,指令没有被CPU执行,这个时候算是编译时,就是编译的时候。

参考:

为什么 JavaScript 是单线程

众所周知 JavaScript 最开始是被设计运行在浏览器中的脚本语言,从浏览器的功能和特性角度来讲,单线程是最合适的。多个线程操作 DOM 的时候容易出现此类问题:这个 DOM 应该被哪个线程控制的问题? 因此为了避免问题的复杂性,从诞生开始 JavaScript 就是单线程,这已经成为这门语言的核心特征。

随着硬件资源(具体指的是 CPU 核数)不断提升和业务场景的不断的扩大(密集型计算、游戏),需要充分利用多核能力。 HTML 5 提出 Web Worker 标准, 允许 JavaScript 脚本创建多个线程,但主线程完全控制子线程且子线程没有操作 DOM 的权限, 所以说并没有改变 JavaScript 单线程的本质。

执行栈与任务队列

单线程意味着排队,必须按照书写代码的顺序执行下去 A -> B -> C -> D,然而一旦哪一步执行的时间非常长就不得不进行长时间的等待,那么这个时候任务就不得不等着前面的执行完。
如果是计算量过大 CPU 忙不过来了倒也好说, 然而这时候 CPU 是闲着的,因为许多任务是比较缓慢的,例如: IO、网络,比如等到有结果才能继续执行。

那么可以把任务分为两种:同步任务(synchronous)、异步任务(asynchronous)。同步任务是指:一个任务执行完后再进行下一个任务,异步任务是说:不进入主线程而进入“任务队列(task queue)“的任务,只有“任务队列“通知主线程某个异步任务可以执行了,该任务才可以执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

1
2
3
4
5
6
7
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。这个过程会不断重复。

事件和回调函数

“任务队列”是事件的队列,IO 设备每完成一项任务,就在“任务队列”中添加一个事件,表示相关的异步任务可以进入“执行栈”了。主线程读取“任务队列”,就是读取里面有哪些事件。

“任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

Event Loop

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。

定时器

除了放置异步任务的事件,”任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做”定时器”(timer)功能,也就是定时执行的代码。

总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在”任务队列”的尾部添加一个事件,因此要等到同步任务和”任务队列”现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了”任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

定时器运行机制

setTimeout和setInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。

这意味着,setTimeout和setInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。

特殊的定时器语句

我们经常会看到这么一些语句 setTimeout(fn, 0),那么他的意思真的是等待 0 秒后立即执行么? 当然不会,正如上文所说需要等到上文全部执行完, 总之,setTimeout(f, 0)这种写法的目的是,尽可能早地执行f,但是并不能保证立刻就执行f。

应用

Node.js 中的 Event Loop

根据上图, Node.js 的运行机制

1
2
3
4
5
6
7
(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

问题遗留

process.nextTick process.setImmediate setTimeout 的区别

引用:

浏览器的组成结构

  1. 用户界面

包括地址栏、前进、后退、书签栏。

  1. 浏览器引擎

在用户界面和浏览器引擎之间传送指令。

  1. 呈现引擎

负责呈现浏览器内容,负责解析 HTML 和 CSS 内容,并将解析后的内容现实在屏幕上。

  1. 网络

用于网络调用。

  1. 用户界面后台

用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。

  1. JavaScript 解释器

解析执行 JavaScript 代码。

  1. 数据存储

持久层,浏览器需要在硬盘上保存各种数据,例如 Cookie,新的 HTML 5 规范定义了网络数据库,这是一个完整轻便的网络数据库

和大多浏览器不同,Chrome 的每个标签页分别对应一个呈现引擎实例,每个标签页都是一个独立的进程。

呈现引擎

呈现引擎,又称为渲染引擎也成为浏览器内核,在这方面又称为 UI 线程,这是由各大浏览器厂商根据 W3C 标准自行研发的, 常见的内核有四种 (Webkit (chrome, safari) Gecko(firefox))。

呈现主流程

呈现引擎最大的作用是用于呈现,也就是在浏览器中显示请求的内容。一开始从网络层获取文档内容,内容大小限制一般在 8000 个块以内, 然后进行以下流程:

使用 HTML 构建 DOM 结构,并将各个标记转换为 “内容树” 上的 DOM 节点。同时也会解析外部 CSS 文件以及样式文件中的样式数据。 HTML 中这些带有视觉指令的样式信息将用于创建另一个树结构: 呈现树。

呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。

呈现树构建完毕之后,进入“布局”处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。下一个阶段是绘制 - 呈现引擎会遍历呈现树,由用户界面后端层将每个节点绘制出来。

示例:webkit 主流程示例图

示例:Mozila 的 Gecko 呈现主流程引擎

JavaScript 解释器

什么是 JavaScript 解释器?简单地说,JavaScript 解释器就是能够“读懂” JavaScript 代码,并准确地给出代码运行结果的一段程序。

所以 JavaScript 解释器,又称为 JavaScript 解析引擎,又称为 JavaScript 引擎,也可以成为 JavaScript 内核,在线程方面又称为 JavaScript 引擎线程。比较有名的有 Chrome 的 V8 引擎(用 C/C++ 编写),除此外还有 IE9 的 Chakra、Firefox 的 TraceMonkey。它是基于事件驱动单线程执行的,JavaScript 引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个 JavaScript 线程在运行 JavaScript 程序。

学过编译原理的人都知道,对于静态语言来说(如Java、C++、C),处理上述这些事情的叫编译器(Compiler),相应地对于 JavaScript 这样的动态语言则叫解释器(Interpreter)。这两者的区别用一句话来概括就是:编译器是将源代码编译为另外一种代码(比如机器码,或者字节码),而解释器是直接解析并将代码运行结果输出。 比方说,firebug 的 console 就是一个 JavaScript 解释器。但我们无需过多在这些点上纠结。因为比如像 V8,它其实是为了提高 JavaScript 的运行性能,会在运行之前将 JavaScript 编译为本地的机器码然后再去执行,这样速度就快很多,相信大家对 JIT(Just In Time Compilation)一定不陌生吧。

JavaScript 解释器和我们平时讨论的 ECMAScript 有很大关系,标准的 JavaScript 解释器会根据 ECMAScript 标准去实现文档中对语言规定的方方面面,但由于这不是一个强制措施,所以也有不按照标准来实现的解释器,比如 IE6,这也是一直困扰前端开发的一个来由——兼容问题。有关 JavaScript 解释器的部分不做过于深入的介绍,但是由于我们对它有了部分的了解,接下来可以介绍一个新的部分——线程。

JavaScript 与浏览器的线程机制

作为浏览器脚本语言,JavaScript 主要用于处理页面中用户交互,以及操作 DOM 树、CSS 样式树(当然也包括服务器逻辑的交互处理)。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征。

我们可以回顾一下最开始所提的一个问题:Web Worker 真的让 JavaScript 拥有了多线程的能力吗?

为了利用多核 CPU 的计算能力,在 HTML5 中引入的工作线程使得浏览器端的 JavaScript 引擎可以并发地执行 JavaScript 代码,从而实现了对浏览器端多线程编程的良好支持。Web Worker 允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM 。所以,这个新标准并没有改变 JavaScript 单线程的本质。

为什么页面会卡顿?

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。为了防止渲染出现不可预期的结果,浏览器设置 UI 渲染线程与 JavaScript 引擎线程为互斥的关系,当 JavaScript 引擎线程执行时 UI 渲染线程会被挂起,UI 更新会被保存在一个队列中等到 JavaScript 引擎线程空闲时立即被执行。

于是,我们便明白了:假设一个 JavaScript 代码执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染出现“加载阻塞”的现象。当然,针对 DOM 的大量操作也会造成页面出现卡顿现象,毕竟我们经常说:DOM 天生就很慢。

所以,当你需要考虑性能优化时就可以从如上的原因出发,大致有以下几个努力的方面:

  • 减少 JavaScript 加载对 DOM 渲染的影响(将 JavaScript 代码的加载逻辑放在 HTML 文件的尾部,减少对渲染引擎呈现工作的影响);
  • 避免重排,减少重绘(避免白屏,或者交互过程中的卡顿);
  • 减少 DOM 的层级(可以减少渲染引擎工作过程中的计算量);
  • 使用 requestAnimationFrame 来实现视觉变化(一般来说我们会使用 setTimeout 或 setInterval 来执行动画之类的视觉变化,但这种做法的问题是,回调将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿);

优化方面可以看 优化 JavaScript 的执行

浏览器中的那些线程

事实上我们在使用浏览器的时候都会涉及到网络工具、浏览器事件、定时器触发线程。事实上这些线程如果出现在主线程上的话工作效率会非常的低下(这里的工作效率指的是人能看到的渲染引擎渲染出的页面)所以浏览器为这些功能独立设计了其他的线程

  • 浏览器事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JavaScript 的单线程关系所有这些事件都得排队等待 JavaScript 引擎处理

  • 定时器触发线程:浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案

  • 异步 HTTP 请求线程:在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理;

总结

这篇大部分基本摘抄 聊聊 JavaScript 与浏览器的那些事 - 引擎与线程浏览器的工作原理:新式网络浏览器幕后揭秘主要是自己对浏览器结构的一个简单梳理、也是为下一篇 Event Loop 的铺垫。

引用:

how to build a building a building

很长一段时间都在和 SRE、Infra 合作主要做的是一些比较基础的服务。 这篇文章主要记录工作中做过的和自己见过的重要的东西,想象如果自己有一天到了一家没有技术的公司自己都得做什么。

私服

在一个公司的前端开发中,理想情况是不同项目应当都使用公司提供的脚手架,并且由 infra 做统一维护。
但是做过开发的人都知道,做开发时不会每个组件都自己写,我们通常会引用一些外部文件(JavaScript -> node_modules, Java -> Maven)。
这个时候我们希望能把公司开发者使用的组件、包都放在自己的服务器上,因为主要有以下几点好处:

  1. 节省自己的外网带宽

  2. 加速Maven、npm的构建,提升构建速度。

  3. 部署自己的第三方组件(公司的公共 node、 开发框架)

node_modules 依赖

使用工具:

Nexus Repository Manager OSS 3.x

构建平台

计划:主要包含 CI CD, 关于 jenkins 进行自动化共建的内容

通用脚手架

计划:

  • web pc
  • H5
  • 小程序
  • 快应用
  • chrome extension

通用 CLI 工具

  • 作为通用脚手架的承载

文档体系

主要用于知识分享

  • docsify
  • vuepress

日志收集

对线上页面进行日志收集以便快速定位问题

性能监控

对线上问题进行性能监控

一直以来受到技术论坛的“荼毒”,要阅读源码。我也就按照很多论坛中说的那样,git clone 一份代码就开始看,结局都不怎么好。主要还是因为以下几种原因:

技术方面说:

  1. 语法特性掌握还不够

比如说看到 Reflect.defineProperty、Proxy 等一些日常业务开发中少见的用法时不能很好的理解其中的细节,频繁的中断会降低个人学习体验。

  1. 对 JavaScript 这门语言的设计哲学认识还不够

原型链几乎每个前端都知道,要真的问也都能插上几句话。这个东西长时间不看确实会忘, 正如 王福朋这篇经典的文章中提到 的他每次面试前都要看看。但个人认为原型链是 web 前端开发中不多的值得深刻记忆的东西。

  1. 构建工具相关的体系

前端开发在最近几年发展迅速,工程化工具也伴随着剧烈变化通常都是 break changes,比如说第一份工作使用 webpack 2.0 进行打包 React,现在我的电脑里还留着一坨关于 webpack 2.0 配置的宝贝。但是第二份工作却是使用 vue 进行开发,有了 vue.config.js 这么好使的玩意儿对 webpack 4.0 生疏也是难免, 毕竟在业务开发中很够用。

再比如说 babel6.0 和 babel 7.0 的区别,按照官网来配置有时候也跑不通。

源代码中有许多工程性质的配置和针对运行时的特殊优化,对构建工具不清不楚看起来自然会很懵逼。

  1. 数据结构与算法的缺失

比如 vue keep-alive 的实现方式,如果你不了解 LRU 的话,我想很难去看明白他在做什么。

学习方法和心理来说:

过去相当长的一段时间总有这么一种观点:沉浸在技术论坛有助于我的极速成长,我的技术视野得到了扩展但是针对现阶段的我造成的弊端也足够明显。

  1. 持续性

看到新的技术或名词总想去试试,但浅尝辄止的尝试并不能给现阶段的自己带来过多的帮助甚至会带来迷茫,如果真的想把现在所做的事当作一辈子的事业来做的话这种做法显然有些浮躁了。

  1. 自信心

看到各种名词、术语、理念、思想自己都不明白经常会给我带来一种深厚的挫败感。从自己走过来的两年再回头看:这些人出于扩展自己的技术影响力也好、分享也好、还是宣传自己也好,很多都是人为的造词或者新瓶装旧酒。

说到这,想起最近发生的一件事: 某司的人力资源领导被优化了,他曾在去年的年底在自媒上发过一篇文章 《行业寒冬之下,为什么被裁的总是你》。

总之作为一个互联网从业者应当具备很强的信息甄别能力。

  1. roadMap

当你迷茫的时候不知道学什么的时候,第一种方法是去看计算机相关书籍,数据结构与算法也好操作系统也好计算机网络也好。

如果你觉得这种方式太枯燥太理论派你更直接一点的话,可以去找相关机构、课程的目录,但是我觉得没有必要去为此付费,按图索骥的按照目录去学习就好。

第一阶段

首先从比较小的代码区块看起,比如:

30 seconds of code

ramdaJS

之前看ramada的时候遇到一个问题,在 stackoverflow 上提问还收到了作者的回答,

第二阶段

第二步:找到了看源码的感觉后,再去看某个类库的源码,比如说redux、moment这种,功能专一同时也兼顾深度。

完全理解 redux

带着问题看React-Redux源码(一万四千字长文预警

第三阶段

这时候自己有了一定的基础和感觉了,可以再去看现在mvvm框架实现的原理,也是从单一的功能开始,拆解mvvm框架的通用实现模式,如双向绑定、虚拟dom等,最后实现自己的mini mvvm。

50行代码的MVVM,感受闭包的艺术

不好意思!耽误你的十分钟,让MVVM原理还给你

第四阶段

这时候你具备了看react、vue这种框架的能力了,最好还是带着问题去看,比如看react fiber的原理、如何渲染的,setState怎么操作等等。

[React技术内幕] setState的秘密

怎样学习React?当然是自己动手实现一个React啦

但是实际开发中没有这种造轮子的机会,大家的关注点更多还是放在业务上

多端秒开方案、性能优化相关

移动 H5 首屏秒开优化方案探讨

工程化套件(脚手架、开发调试工具、发布管理)

15分钟搭一个企业级脚手架

前端自动化发布实战总结

数据埋点/监控

前端埋点的设计方案

多端融合

多端统一方案 Taro

Awesome Flutter:带你从入门到进阶的 Flutter 指南

组件库

  1. pre code
  2. low code

互动平台、直播

直播原理与web直播实战

SSR 微前端

一文吃透 React SSR 服务端渲染和同构原理

BFF、Serverless、BFF in Serverless

BFF、Serverless、以及BFF in Serverless

灰度平台/ABtest等等

https://juejin.im/post/5da88d795188252f051e2b47

软技能

软技能-代码之外的生存指南1(职业篇)

工程师如何在工作中提升自己?

参考:

路线图大部分参考了知乎中的这篇回答一年经验的前端开发工程师看不懂源码怎么办,

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×