Node-JS专精10_02在浏览器支持nextTick

代码仓库

我们使用了 process.nextTick 代替 setTimeout

  • 浏览器不支持 nextTick
  • 如果你用过vue, 你知道vue提供了一个 nextTick,那么 vue是如何做到即支持 node又支持 浏览器呢?

MutationObserver

它本来是监听 DOM树 的更新的,它有一个特别厉害的地方就是 当DOM树更新就会调用一个异步的回调,而这个回调的优先级比 setTimeout 要高

示例代码

  • 你可以拷贝代码在 jsbin里运行
  • 通过实例可以看到无论 setTimeout 在 span改变之前还是之后都没 MutationObserver 的优先级高
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<span id="aaa">1</span>
<br>
<button id="btn">+1</button>
<script>

var span = document.getElementById('aaa');

var config = { attributes: true, childList: true, subtree: true };

var callback = function() {
console.log("observer")
};

var observer = new MutationObserver(callback);
observer.observe(span, config);


btn.onclick = function(){
setTimeout(()=>{console.log("timeout1")},0)
span.innerText += "s";
setTimeout(()=>{console.log("timeout2")},0)
}
/*
每次点击button 始终打印
observer
timeout1
timeout2
*/
</script>
</body>
</html>

我们自己实现一个 nextTick

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button id="btn">+1</button>
<script>

btn.onclick = function(){
setTimeout(()=>{console.log("timeout1")},0)
nextTick(()=>{console.log("nextTick")})
setTimeout(()=>{console.log("timeout2")},0)
}
/*
每次点击button 始终打印
nextTick
timeout1
timeout2
*/

function nextTick(fn){
var counter = 1
var observer = new MutationObserver(fn)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {characterData: true})

// 更改文本内容触发 dom树更新 触发回调
counter = counter + 1
textNode.data = String(counter)
}
</script>
</body>
</html>

兼容node和浏览器的nextTick

1
2
3
4
5
6
7
8
9
10
11
12
13
function nextTick(fn){
if( process !== undefined && typeof process.nextTick === 'function'){
return process.nextTick(fn);
}else{
var counter = 1
var observer = new MutationObserver(fn)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {characterData: true})
// 更改文本内容触发 dom树更新 触发回调
counter = counter + 1
textNode.data = String(counter)
}
}

代码仓库

重构代码

  • 注意每次优化都跑一下测试用例保证代码正确性
  • 注意每次优化都跑一下测试用例保证代码正确性
  • 注意每次优化都跑一下测试用例保证代码正确性

src/promise.ts

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
class Promise2{
state = "pending";
callbacks = [];
private resolveOrReject(state, data, i){
if(this.state !== "pending") return;
this.state = state;
nextTick(() => {
this.callbacks.forEach(handle=>{
if(typeof handle[i] === "function"){
let x;
try {
x = handle[i].call(undefined,data);
} catch (e) {
return handle[2].reject(e);
}
handle[2].resolveWith(x);
}
})
});
}
resolve(result){
this.resolveOrReject("fulfilled",result,0);
}
reject(reason){
this.resolveOrReject("rejected",reason,1);
}
constructor(fn){
if(typeof fn !== 'function'){
throw new Error("我只接受函数")
}

fn(this.resolve.bind(this), this.reject.bind(this));
}
then(succeed?,fail?){
// handle除了记录成功和失败 还要记录成功和失败的后续
const handle = [];
if(typeof succeed === 'function'){
handle[0] = succeed;
}
if(typeof fail === 'function'){
handle[1] = fail;
}

// 记录then之后的后续
handle[2] = new Promise2(()=>{});

this.callbacks.push(handle);
// 把函数推进 callBacks 里面

return handle[2];
}
resolveWithSelf(){
this.reject(new TypeError())
}
resolveWithPromise(x){
x.then(
(result)=>{
this.resolve(result)
},
(reason)=>{
this.reject(reason)
}
)
}
private getThen(x){
let then;
try{
then = x.then;
}catch(e){
return this.reject(e);
}
return then;
}
resolveWithThenable(x){
try {
x.then(
y => {
this.resolveWith(y)
},
r =>{
this.reject(r);
}
);
} catch (error) {
this.reject(error)
}
}
resolveWithObject(x){
let then = this.getThen(x);
if(then instanceof Function){
this.resolveWithThenable(x);
} else {
this.resolve(x);
}
}
resolveWith(x){
if( this === x){
this.resolveWithSelf();
} else if ( x instanceof Promise2){
this.resolveWithPromise(x);
} else if ( x instanceof Object){
this.resolveWithObject(x);
} else {
this.resolve(x);
}
}
}

export default Promise2

function nextTick(fn){
if( process !== undefined && typeof process.nextTick === 'function'){
return process.nextTick(fn);
}else{
var counter = 1
var observer = new MutationObserver(fn)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {characterData: true})
// 更改文本内容触发 dom树更新 触发回调
counter = counter + 1
textNode.data = String(counter)
}
}

区分宏任务和微任务

MacroTask

  • setTimeout

MicroTask(优先级高)

  • process.nextTick(nodeJS里用) / MutationObserver(浏览器里用)
  • setImmediate(只能在IE浏览器里用,nodeJS虽然也有这个API但是兼容性不好)

整体完成后的代码