Event Loop 我感觉我又行了(浏览器篇)
我们先看看事件循环是怎么回事:
名词解释:
- 执行栈:所有代码都会放到这里执行;
- 微任务:语言标准(ECMA262)提供的API运行,Promise、MutationObserve、process.nextTick、setImmediate;
- GUI渲染:渲染DOM;
- 宏任务:宿主提供的异步方法和任务,setTimeout、setInterval、script、UI渲染、ajax、DOM的事件;
举个粟子
我们直接通过分析各种例子,来了解浏览器的 Event Loop 的过程。
例子1
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Loop</title>
</head>
<body>
<div id="J_wrapper" style="width: 200px; height: 30px; background: orange"></div>
<script src="./1.js"></script>
</body>
</html>
1.js
js
const oWrapper = document.querySelector('#J_wrapper')
console.log('start')
oWrapper.style.background = 'blue'
Promise.resolve().then(() => {
console.log(1)
for (let i = 1; i < 10000000000; i++) {
let a = 1
}
})
setTimeout(() => {
console.log(2)
}, 0)
console.log('end')
我们先不运行代码,结合事件循环图来分析一下流程:
-
因为 script 是在后面引入,DOM 肯定会先渲染,所以页面上应该呈现一个橙色的 div;
-
script 执行;
<执行栈>:
<微任务>:
<GUI渲染>:
<宏任务>:
-
const oWrapper = document.querySelector('#J_wrapper')
语句执行; -
console.log('start')
语句执行,输出 start;<执行栈>:
start
<微任务>:
<GUI渲染>:
<宏任务>:
-
oWrapper.style.background = 'blue'
语句被放入 GUI 渲染线程;<执行栈>:
start
<微任务>:
<GUI渲染>:
oWrapper.style.background = ‘blue’
<宏任务>:
-
Promise.resolve.then
语句执行,回调被放入微任务队列;<执行栈>:
start
<微任务>:
Promise.resolve.then 的回调
<GUI渲染>:
oWrapper.style.background = ‘blue’
<宏任务>:
-
setTimeout
语句执行,回调被放入宏任务队列;<执行栈>:
start
<微任务>:
Promise.resolve.then 的回调
<GUI渲染>:
oWrapper.style.background = ‘blue’
<宏任务>:
setTimeout 的回调
-
console.log(end)
语句执行,输出 end;<执行栈>:
start
end
<微任务>:
Promise.resolve.then 的回调
<GUI渲染>:
oWrapper.style.background = ‘blue’
<宏任务>:
setTimeout 的回调
-
清空微任务(本轮所有的微任务回调被放入执行栈执行);
<执行栈>:
start
end
1
<微任务>:
Promise.resolve.then 的回调<GUI渲染>:
oWrapper.style.background = ‘blue’
<宏任务>:
setTimeout 的回调
-
GUI渲染,页面显示 div 背景色变成 blue;
<执行栈>:
start
end
1
<微任务>:
Promise.resolve.then 的回调<GUI渲染>:
oWrapper.style.background = ‘blue’<宏任务>:
setTimeout 的回调
-
取一个宏任务回调放入执行栈执行(前提是等待时间已到);
<执行栈>:
start
end
1
2
<微任务>:
Promise.resolve.then 的回调<GUI渲染>:
oWrapper.style.background = ‘blue’<宏任务>:
setTimeout 的回调 -
完成一轮循环,已无其它任务,完事。
最终我们可以看到浏览器中输出的结果:
markdown
start
end
1
2
并且可以看到在输出1之后,会有一段等待 for 循环执行过程,之后才会把 div 的颜色变成蓝色。
例子2
js
console.log(1);
// setTimeout1
setTimeout(() => {
console.log(2)
// Promise1.then
Promise.resolve().then(() => {
console.log(3)
})
}, 0);
// Promise2
new Promise((resolve, reject) => {
console.log(4)
// setTimeout2
setTimeout(() => {
console.log(5)
resolve(6);
}, 0);
// Promise2.then1
}).then((res) => {
console.log(7);
// setTimeout3
setTimeout(() => {
console.log(res)
}, 0);
// Promise2.then2
}).then(() => {
console.log(8);
// Promise2.then3
}).then(() => {
console.log(9);
});
同样,我们还是先不运行代码,为了方便区分,上面代码加上了一些注释标识,对照着 Event Loop 图示来分析一下:
-
script 执行
<执行栈>:
<微任务>:
<GUI渲染>:
<宏任务>:
-
console.log(1)
放入执行栈,输出 1;<执行栈>:
1
<微任务>:
<GUI渲染>:
<宏任务>:
-
setTimeout1
放入执行栈,回调被放入宏任务队列;<执行栈>:
1
<微任务>:
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
-
Promise2
放入执行栈,Executor 中的代码是同步执行的,所以console.log(4)
会执行,输出4,之后setTimeout2
执行,回调被放入宏任务队列;<执行栈>:
1
4
<微任务>:
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
因为
Promise2
中的状态没变成 Fulfilled,所以 then 还不会被执行; -
执行栈空闲,微任务为空,GUI渲染为空,取一个宏任务
setTimeout1
的回调放入执行栈,输出2,之后Promise1.then
回调被放入微任务队列;<执行栈>:
1
4
2
<微任务>:
Promise1.then 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调
-
继续下一轮,取所有微任务回调放入执行栈,
console.log(3)
执行,输出3;<执行栈>:
1
4
2
3
<微任务>:
Promise1.then 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调
-
GUI渲染为空,取宏任务
setTimeout2
的回调执行,console.log(5)
执行,输出5,之后 Promise2 的状态变成 Fulfilled,Promise2.then1 的回调被放入微任务队列;<执行栈>:
1
4
2
3
5
<微任务>:
Promise1.then 的回调Promise2.then1 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调 -
继续下一轮,清空当前所有微任务;
Promise2.then1
中的console.log(7)
执行,输出7;之后setTimeout3
中的回调被放入宏任务队列;Promise2.then2
的回调放入微任务队列;<执行栈>:
1
4
2
3
5
7
<微任务>:
Promise1.then 的回调Promise2.then1 的回调Promise2.then2 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调setTimeout3 的回调
-
继续执行
Promise2.then2
的回调console.log(8)
,输出8;Promise2.then3
的回调被放入微任务队列;<执行栈>:
1
4
2
3
5
7
8
<微任务>:
Promise1.then 的回调Promise2.then1 的回调Promise2.then2 的回调Promise2.then3 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调setTimeout3 的回调
-
继续执行
Promise2.then3
的回调console.log(9)
, 输出9;至此,本轮所有微任务被清空完毕;<执行栈>:
1
4
2
3
5
7
8
9
<微任务>:
Promise1.then 的回调Promise2.then1 的回调Promise2.then2 的回调Promise2.then3 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调setTimeout3 的回调
-
没有 GUI渲染,取宏任务
setTimeout3
的回调,console.log(res)
执行,输出6;<执行栈>:
1
4
2
3
5
7
8
9
6
<微任务>:
Promise1.then 的回调Promise2.then1 的回调Promise2.then2 的回调Promise2.then3 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调setTimeout3 的回调 -
完事。
最终我们可以看到控制台输出如下:
例子3
js
let p = new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => console.log(2));
console.log(4);
}).then(t => console.log(t));
console.log(3);
这个题比较有意思。我们需要知道 Promise
中的 Executor
里面的代码是同步执行的,并且在 resolve 之后的代码也还是会继续执行的,并且只有 then 被调用的时候,回调才会被放入微任务队列。所以我们可以简单地分析一下代码的运行。
-
Promise 的 Executor 执行,
resolve(1)
;<执行栈>:
<微任务>:
<GUI渲染>:
<宏任务>:
-
Promise.resolve().then
执行,回调被放入微任务队列;之后console.log(4)
执行,输出4;<执行栈>:
4
<微任务>:
Promise.resolve().then 的回调
<GUI渲染>:
<宏任务>:
-
Promise.then 执行,回调
t => console.log(t)
被放入微任务队列;<执行栈>:
4
<微任务>:
Promise.resolve().then 的回调
Promise.then 的回调
<GUI渲染>:
<宏任务>:
-
console.log(3)
执行,输出3;<执行栈>:
4
3
<微任务>:
Promise.resolve().then 的回调
Promise.then 的回调
<GUI渲染>:
<宏任务>:
-
取当前轮所有微任务回调执行,
Promise.resolve().then
先被执行console.log(2)
,输出2;之后Promise.then
执行console.log(t)
,输出1;<执行栈>:
4
3
2
1
<微任务>:
Promise.resolve().then 的回调Promise.then 的回调<GUI渲染>:
<宏任务>:
-
完事
这里需要注意到,在 Promise 中,当且仅当状态变更, then 被调用的时候,它的回调才会被放入微任务队列。
例子4
js
// setTimeout1
setTimeout(() => {
console.log('A');
}, 0);
var obj = {
func: function() {
// setTimeout2
setTimeout(function() {
console.log('B');
}, 0);
return new Promise(function(resolve) {
console.log('C');
resolve();
});
},
};
obj.func().then(function() {
console.log('D');
});
console.log('E');
同样还是先分析一下代码。
-
script 执行。
<执行栈>:
<微任务>:
<GUI渲染>:
<宏任务>:
-
setTimeout1
执行,回调被放入宏任务队列;<执行栈>:
<微任务>:
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
-
obj.func()
执行,setTimeout2
执行,回调被放入宏任务队列;Promise
的 Executor 执行console.log('C')
,输出 C;<执行栈>:
C
<微任务>:
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
obj.func()
执行完后,调用 then, 回调被放入微任务队列;<执行栈>:
C
<微任务>:
obj.func().then 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
console.log('E')
执行,输出 E;<执行栈>:
C
E
<微任务>:
obj.func().then 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
清空本轮所有微任务,
obj.func().then
的回调执行console.log('D')
,输出 D;<执行栈>:
C
E
D
<微任务>:
obj.func().then 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
没有 GUI渲染,取一个宏任务
setTimeout1
的回调出来执行console.log('A')
,输出 A;<执行栈>:
C
E
D
A
<微任务>:
obj.func().then 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调
-
下一轮,没有微任务,没有 GUI 渲染,取宏任务
setTimeout2
的回调执行console.log('B')
,输出 B;<执行栈>:
C
E
D
A
B
<微任务>:
obj.func().then 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调 -
完事。
例子5
js
console.log("script start");
// setTimeout1
setTimeout(function() {
console.log("setTimeout---0");
}, 0);
// setTimeout2
setTimeout(function() {
console.log("setTimeout---200");
// setTimeout3
setTimeout(function() {
console.log("inner-setTimeout---0");
});
// Promise1.then
Promise.resolve().then(function() {
console.log("promise5");
});
}, 0);
// Promise2
Promise.resolve()
// Promise2.then1
.then(function() {
console.log("promise1");
})
// Promise2.then2
.then(function() {
console.log("promise2");
});
// Promise3.then
Promise.resolve().then(function() {
console.log("promise3");
});
console.log("script end");
-
script 执行;
<执行栈>:
<微任务>:
<GUI渲染>:
<宏任务>:
-
console.log("script start")
执行,输出 script start;<执行栈>:
script start
<微任务>:
<GUI渲染>:
<宏任务>:
-
setTimeout1
执行,回调放入宏任务队列;<执行栈>:
script start
<微任务>:
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
-
setTimeout2
执行,回调放入宏任务队列;<执行栈>:
script start
<微任务>:
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
Promise2
执行,Promise2.then1
回调放入微任务队列;<执行栈>:
script start
<微任务>:
Promise2.then1 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
Promise3
执行,回调放入微任务队列;<执行栈>:
script start
<微任务>:
Promise2.then1 的回调
Promise3.then 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
console.log("script end")
执行,输出 script end;<执行栈>:
script start
script end
<微任务>:
Promise2.then1 的回调
Promise3.then 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
清空当前所有微任务:
Promise2.then1
回调执行console.log("promise1")
,输出 promise1,此时Promise2.then1
执行完毕后返回一个新的 Promise,后面 then 执行,Promise2.then2
回调放入微任务队列;<执行栈>:
script start
script end
promise1
<微任务>:
Promise2.then1 的回调Promise3.then 的回调
Promise2.then2 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
Promise3.then
回调执行console.log("promise3")
,输出 promise3;Promise2.then2
回调执行console.log("promise2")
,输出 promise2;<执行栈>:
script start
script end
promise1
promise3
promise2
<微任务>:
Promise2.then1 的回调Promise3.then 的回调Promise2.then2 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调
setTimeout2 的回调
-
没有 GUI渲染,取一个宏任务
setTimeout1
的回调执行console.log("setTimeout---0")
,输出 setTimeout—0;<执行栈>:
script start
script end
promise1
promise3
promise2
setTimeout—0
<微任务>:
Promise2.then1 的回调~~Promise3.then 的回调~~Promise2.then2 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调
-
下一轮,没有微任务,没有 GUI渲染,取
setTimeout2
的回调执行:console.log("setTimeout---200")
,输出 setTimeout—200;setTimeout3
执行,回调被放入宏任务队列;Promise1.then
执行,回调放入微任务队列;<执行栈>:
script start
script end
promise1
promise3
promise2
setTimeout—0
setTimeout—200
<微任务>:
Promise2.then1 的回调~~Promise3.then 的回调~~Promise2.then2 的回调
Promise1.then 的回调
<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调setTimeout3 的回调
-
下一轮,清空微任务:
Promise1.then
回调执行console.log("promise5")
,输出 promise5;<执行栈>:
script start
script end
promise1
promise3
promise2
setTimeout—0
setTimeout—200
promise5
<微任务>:
Promise2.then1 的回调~~Promise3.then 的回调~~Promise2.then2 的回调
Promise1.then 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调setTimeout3 的回调
-
没有 GUI 渲染,取一个宏任务
setTimeout3
的回调执行console.log("inner-setTimeout---0")
,输出 inner-setTimeout—0;<执行栈>:
script start
script end
promise1
promise3
promise2
setTimeout—0
setTimeout—200
promise5
inner-setTimeout—0
<微任务>:
Promise2.then1 的回调~~Promise3.then 的回调~~Promise2.then2 的回调
Promise1.then 的回调<GUI渲染>:
<宏任务>:
setTimeout1 的回调setTimeout2 的回调setTimeout3 的回调 -
完事。
这里可能会对8、9两个步骤有疑问,但这也并不难理解,我们前面说过,当且仅当 Promise 状态变更,且调用 then 的时候,回调才会被放入微任务队列。
那么怎么理解呢?看下面的代码:
js
Promise.resolve()
.then(() => {
console.log(1)
})
.then(() => {
console.log(3)
})
我们可以将前面这一块看成一个整体:
then 返回一个新的 Promise,在 MDN 中有这样一段话:
The behavior of the handler function follows a specific set of rules. If a handler function:
- returns a value, the promise returned by
then
gets resolved with the returned value as its value.- doesn’t return anything, the promise returned by
then
gets resolved with anundefined
value.- throws an error, the promise returned by
then
gets rejected with the thrown error as its value.- returns an already fulfilled promise, the promise returned by
then
gets fulfilled with that promise’s value as its value.- returns an already rejected promise, the promise returned by
then
gets rejected with that promise’s value as its value.- returns another pending promise object, the resolution/rejection of the promise returned by
then
will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned bythen
will be the same as the resolved value of the promise returned by the handler.
我们可以看到,返回的 Promise 的状态会根据 then 中的回调函数的执行结果不同而不同,也就是说,会在回调执行了之后才会变更状态。
所以上面的代码我们可以这样看:
js
new Promise((resolve, reject) => {
Promise.resolve().then(() => {
console.log(1)
resolve()
})
}).then(() => {
console.log(3)
})
我们分析一下上面的代码:
-
Promise 的 Executor 执行,
Promise.resolve().then
执行,回调被放入微任务队列;<执行栈>:
<微任务>:
Promise.resolve().then 的回调
<GUI渲染>:
<宏任务>:
-
清空本轮微任务,
console.log(1)
被执行,输出1;之后 Promise 的状态变成 resolve, 所以Promise.then
被放入微任务队列。<执行栈>:
1
<微任务>:
Promise.resolve().then 的回调Promise.then 的回调
<GUI渲染>:
<宏任务>:
-
还是在清空本轮的微任务,
console.log(3)
被执行,输出3。<执行栈>:
1
3
<微任务>:
Promise.resolve().then 的回调Promise.then 的回调<GUI渲染>:
<宏任务>:
-
没有 GUI渲染,没有宏任务,完事。
我们再看下,他到底是不是在本轮执行后续的微任务?还是上面的例子,我们做一些变化,给它加一个宏任务:
js
new Promise((resolve, reject) => {
Promise.resolve().then(() => {
console.log(1)
resolve()
})
}).then(() => {
console.log(3)
})
setTimeout(() => {
console.log('setTimeout')
}, 0)
直接看执行结果:
显然,是在本轮执行后续的微任务。
趁热打铁,我们再做一个小粟子。
例子5-1
js
console.log('start')
// Promise1
Promise.resolve()
// Promise1.then1
.then(() => {
console.log(1)
// setTimeout1
setTimeout(() => {
console.log(2)
})
})
// Promise1.then2
.then(() => {
console.log(3)
// setTimeout2
setTimeout(() => {
console.log(4)
})
})
// Promise1.then3
.then(() => {
console.log(5)
// setTimeout3
setTimeout(() => {
console.log(6)
})
})
// Promise2
Promise.resolve()
// Promise2.then1
.then(() => {
console.log(7)
// setTimeout4
setTimeout(() => {
console.log(8)
})
})
// Promise2.then2
.then(() => {
console.log(9)
// setTimeout5
setTimeout(() => {
console.log(10)
})
})
// Promise2.then3
.then(() => {
console.log(11)
// setTimeout6
setTimeout(() => {
console.log(12)
})
})
console.log('end')
题看起来有点复杂,不过不要慌,我们一步一步地分析它(因为没有渲染操作,所以忽略它,不再分析):
-
script 执行;
<执行栈>:
<微任务>:
<宏任务>:
-
console.log('start')
执行,输出 start;<执行栈>:
start
<微任务>:
<宏任务>:
-
Promise1
执行,Promise1.then1
被放入微任务队列;<执行栈>:
start
<微任务>:
Promise1.then1 的回调
<宏任务>:
-
Promise2
执行,Promise2.then1
被放入微任务队列;<执行栈>:
start
<微任务>:
Promise1.then1 的回调
Promise2.then1 的回调
<宏任务>:
-
console.log('end')
执行,输出 end;<执行栈>:
start
end
<微任务>:
Promise1.then1 的回调
Promise2.then1 的回调
<宏任务>:
-
清空微任务,
Promise1.then1
的回调执行console.log(1)
,输出 1;setTimeout1
执行,回调放入宏任务队列;之后,Promise1.then2
的回调被放入微任务队列;<执行栈>:
start
end
1
<微任务>:
Promise1.then1 的回调Promise2.then1 的回调
Promise1.then2 的回调
<宏任务>:
setTimeout1 的回调
-
Promise2.then1
的回调执行console.log(7)
,输出 7;setTimeout 4
执行,回调放入宏任务队列;之后,Promise2.then2
的回调被放入微任务队列;<执行栈>:
start
end
1
7
<微任务>:
Promise1.then1 的回调Promise2.then1 的回调Promise1.then2 的回调
Promise2.then2 的回调
<宏任务>:
setTimeout1 的回调
setTimeout4 的回调
-
继续清空本轮微任务,
Promise1.then2
的回调执行console.log(3)
,输出3;setTimeout2
执行,回调放入宏任务队列;之后,Promise1.then3
的回调被放入微任务队列;<执行栈>:
start
end
1
7
3
<微任务>:
Promise1.then1 的回调Promise2.then1 的回调Promise1.then2 的回调Promise2.then2 的回调
Promise1.then3 的回调
<宏任务>:
setTimeout1 的回调
setTimeout4 的回调
setTimeout2 的回调
-
Promise2.then2
的回调执行console.log(9)
, 输出 9;setTimeout5
执行,回调被放入宏任务队列;之后Promise2.then3
的回调被放入微任务队列;<执行栈>:
start
end
1
7
3
9
<微任务>:
Promise1.then1 的回调Promise2.then1 的回调Promise1.then2 的回调Promise2.then2 的回调Promise1.then3 的回调
Promise2.then3 的回调
<宏任务>:
setTimeout1 的回调
setTimeout4 的回调
setTimeout2 的回调
setTimeout5 的回调
-
Promise1.then3
的回调执行console.log(5)
,输出 5;setTimeout3
执行,回调放入宏任务队列;<执行栈>:
start
end
1
7
3
9
5
<微任务>:
Promise1.then1 的回调Promise2.then1 的回调Promise1.then2 的回调Promise2.then2 的回调Promise1.then3 的回调Promise2.then3 的回调
<宏任务>:
setTimeout1 的回调
setTimeout4 的回调
setTimeout2 的回调
setTimeout5 的回调
setTimeout3 的回调
-
Promise2.then3
的回调执行console.log(11)
,输出 11;setTimeout6
执行,回调放入宏任务队列;<执行栈>:
start
end
1
7
3
9
5
11
<微任务>:
Promise1.then1 的回调Promise2.then1 的回调Promise1.then2 的回调Promise2.then2 的回调Promise1.then3 的回调Promise2.then3 的回调<宏任务>:
setTimeout1 的回调
setTimeout4 的回调
setTimeout2 的回调
setTimeout5 的回调
setTimeout3 的回调
setTimeout6 的回调
-
至此,本轮所有微任务被清空;GUI 渲染没有,取一个宏任务执行(从代码上看,已经不会再有微任务和GUI渲染),所以之后是依次取宏任务执行,这里就不一一分析了;
<执行栈>:
start
end
1
7
3
9
5
11
2
8
4
10
6
12
<微任务>:
Promise1.then1 的回调Promise2.then1 的回调Promise1.then2 的回调Promise2.then2 的回调Promise1.then3 的回调Promise2.then3 的回调<宏任务>:
setTimeout1 的回调setTimeout4 的回调setTimeout2 的回调setTimeout5 的回调setTimeout3 的回调setTimeout6 的回调 -
完事。
例子6
js
// setTimeout1
setTimeout(function () {
console.log('timeout1');
}, 1000);
console.log('start');
// Promise1.then
Promise.resolve().then(function () {
console.log('promise1');
// Promise2.then
Promise.resolve().then(function () {
console.log('promise2');
});
// setTimeout2
setTimeout(function () {
// Promise3.then
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('timeout2')
}, 0);
});
console.log('done');
-
script 执行;
<执行栈>:
<微任务>:
<宏任务>:
-
setTimeout1
执行,回调放入宏任务队列;<执行栈>:
<微任务>:
<宏任务>:
setTimeout1 的回调(延时1000)
-
console.log('start')
输出 start;<执行栈>:
start
<微任务>:
<宏任务>:
setTimeout1 的回调(延时1000)
-
Promise1.then
执行,回调放入微任务队列;<执行栈>:
start
<微任务>:
Promise1.then 的回调
<宏任务>:
setTimeout1 的回调(延时1000)
-
console.log('done')
输出 done;<执行栈>:
start
done
<微任务>:
Promise1.then 的回调
<宏任务>:
setTimeout1 的回调(延时1000)
-
清空微任务,
Promise1.then
的回调执行console.log('promise1')
,输出 promise1;Promise2.then
执行,回调放入微任务队列;setTimeout2
执行,回调放入宏任务队列;<执行栈>:
start
done
promise1
<微任务>:
Promise1.then 的回调Promise2.then 的回调
<宏任务>:
setTimeout1 的回调(延时1000)
setTimeout2 的回调(延时0)
-
继续本轮微任务,
Promise2.then
的回调执行console.log('promise2')
,输出 promise2;<执行栈>:
start
done
promise1
promise2
<微任务>:
Promise1.then 的回调Promise2.then 的回调<宏任务>:
setTimeout1 的回调(延时1000)
setTimeout2 的回调(延时0)
-
取一个宏任务回调执行,注意是延时已到了的执行,所以是
setTimeout2
的回调执行,Promise3.then
执行,回调放入微任务队列;console.log('timeout2')
执行,输出 timeout2;<执行栈>:
start
done
promise1
promise2
timeout2
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise3.then 的回调
<宏任务>:
setTimeout1 的回调(延时1000)
setTimeout2 的回调(延时0) -
下一轮,清空微任务,
Promise3.then
的回调执行console.log('promise3')
,输出 promise3;<执行栈>:
start
done
promise1
promise2
timeout2
promise3
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise3.then 的回调<宏任务>:
setTimeout1 的回调(延时1000)
setTimeout2 的回调(延时0) -
等待 1000ms 延迟过后,取
setTimeout1
的回调执行console.log('timeout1')
, 输出 timeout1;<执行栈>:
start
done
promise1
promise2
timeout2
promise3
timeout1
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise3.then 的回调<宏任务>:
setTimeout1 的回调(延时1000)setTimeout2 的回调(延时0) -
完事。
例子7
据说是一道头条的面试题。
js
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
我们在分析这道题之前,先了解一下 async/await 是怎么回事。
async/await
async/await 是一个 generator
+ co
的语法糖;一个函数,加上了 async 关键字,那么它的返回值必定是一个 Promise。await 后面的语句,它也必定是个 Promise,如果不是,将会被 Promise.resolve()
包裹。
js
async function test () {
console.log(1)
await 2
console.log(3)
}
// => 相当于
function test () {
console.log(1)
Promise.resolve(2)
.then(() => {
console.log(3)
})
}
async function test1 () {
console.log(1)
await test2()
console.log(2)
}
async function test2 () {
console.log(3)
}
// => 相当于
function test1 () {
console.log(1)
new Promise((resolve, reject) => {
console.log(3)
resolve()
})
.then(() => {
console.log(2)
})
}
有了上面的知识,我们继续分析一下这道题,我们先把 async 函数作一些调整:
js
function async1 () {
console.log('async1 start')
// Promise1
new Promise((resolve, reject) => {
async2()
resolve()
})
// Promise1.then
.then(() => {
console.log('async1 end')
})
}
function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
// Promise2
new Promise((resolve) => {
console.log('promise1')
resolve()
})
// Promise2.then
.then(function () {
console.log('promise2')
})
console.log('script end')
-
script 执行;
<执行栈>:
<微任务>:
<宏任务>:
-
async1
、async2
函数声明,console.log('script start')
执行,输出 script start;<执行栈>:
script start
<微任务>:
<宏任务>:
-
setTimeout
执行,回调放入宏任务队列;<执行栈>:
script start
<微任务>:
<宏任务>:
setTimeout 的回调
-
async1()
执行,console.log('async1 start')
执行,输出 async1 start;Promise1 的 Executor 内部代码执行,async2()
执行console.log('async2')
,输出 async2;Promise1.then
的回调放入微任务队列;<执行栈>:
script start
async1 start
async2
<微任务>:
Promise1.then 的回调
<宏任务>:
setTimeout 的回调
-
Promise2 的 Executor 内部代码执行
console.log('promise1')
,输出 promise1 ;Promise2.then
的回调放入微任务队列;<执行栈>:
script start
async1 start
async2
promise1
<微任务>:
Promise1.then 的回调
Promise2.then 的回调
<宏任务>:
setTimeout 的回调
-
console.log('script end')
执行,输出 script end;<执行栈>:
script start
async1 start
async2
promise1
script end
<微任务>:
Promise1.then 的回调
Promise2.then 的回调
<宏任务>:
setTimeout 的回调
-
清空微任务;
Promise1.then
的回调执行console.log('async1 end')
,输出 async1 end;Promise2.then
的回调执行console.log('promise2')
,输出 promise2;<执行栈>:
script start
async1 start
async2
promise1
script end
async1 end
promise2
<微任务>:
Promise1.then 的回调Promise2.then 的回调<宏任务>:
setTimeout 的回调
-
取一个宏任务
setTimeout1
的回调执行,输出 setTimeout;<执行栈>:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
<微任务>:
Promise1.then 的回调Promise2.then 的回调<宏任务>:
setTimeout 的回调 -
完事。
例子8
js
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 2000);
Promise.resolve()
.then(function() {
console.log('promise1');
})
.then(function() {
console.log('promise2');
});
async function foo() {
await bar()
console.log('async1 end')
}
foo()
async function errorFunc () {
try {
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res))
function bar() {
console.log('async2 end')
}
console.log('script end');
在分析这个例子之前,我们先看看 try...catch
是怎么回事:
js
console.log(a) // 报错:Uncaught ReferenceError: a is not defined
如果我们使用 try...catch
包裹上面的代码:
js
try {
console.log(a)
} catch (e) {
console.log(e) // 控制台输出:'Syntax Error: `a` is not defined'
}
我们看下 Promise 的情况:
js
async function test () {
await Promise.reject('Error') // 报错:Uncaught (in promise) Error
}
test()
使用 try...catch
包裹 Promise 的时候,如果 Promise 的状态变成 Rejected,那么会执行 catch 里面的代码:
js
async function test () {
try {
await Promise.reject('Error')
} catch (e) {
console.log(e) // 控制台输出:'Error'
}
}
test()
如果 Promise 的状态变成 Fulfilled,那么 catch 里面的代码不会被执行:
js
async function test () {
try {
await Promise.resolve(1)
} catch (e) {
console.log(e)
}
}
test()
注意:try...catch
并不会捕获 Promise.prototype.reject
出来的错误:
js
try {
Promise.reject('Error') // 控制台报错:Uncaught (in promise) Error
} catch (e) {
console.log(e)
}
但是加上了 await 之后,后续的代码执行完毕,会由 Generator.prototype.throw
来抛出一个错误,从而被 catch 捕捉到。
并且,try...catch
不会阻止后面的代码执行。
有了这些前置知识,我们可以将上面的代码转换成这样来分析一下:
js
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 2000);
// Promise1
Promise.resolve()
// Promise1.then1
.then(function() {
console.log('promise1');
})
// Promise1.then2
.then(function() {
console.log('promise2');
});
function foo() {
// Promise2
new Promise((resolve, reject) => {
bar()
resolve()
})
// Promise2.then
.then(() => {
console.log('async1 end')
})
}
foo()
function errorFunc () {
// Promise3
return new Promise((_, reject) => {
reject('error!!!')
})
// Promise3.then
.then(
() => {
console.log('async1')
return Promise.resolve('async1 success')
},
e => {
console.log(e)
console.log('async1')
return Promise.resolve('async1 success')
})
}
errorFunc().then(res => console.log(res))
function bar() {
console.log('async2 end')
}
console.log('script end');
-
script 执行;
<执行栈>:
<微任务>:
<宏任务>:
-
console.log('script start')
执行,输出 script start;<执行栈>:
script start
<微任务>:
<宏任务>:
-
setTimeout
执行,回调放入宏任务队列;<执行栈>:
script start
<微任务>:
<宏任务>:
setTimeout 的回调(延时2000)
-
Promise1
执行,Promise1.then1
的回调放入微任务队列;<执行栈>:
script start
<微任务>:
Promise1.then1 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
foo()
执行,Promise2 的 Executor 执行,bar()
执行console.log('async2 end')
,输出 async2 end;Promise2.then
的回调放入微任务队列;
<执行栈>:
script start
async2 end
<微任务>:
Promise1.then1 的回调
Promise2.then 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
errorFunc()
执行,Promise3 的 Executor 执行,Promise3.then
的回调放入微任务队列;<执行栈>:
script start
async2 end
<微任务>:
Promise1.then1 的回调
Promise2.then 的回调
Promise3.then 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
console.log('script end')
执行,输出 script end;<执行栈>:
script start
async2 end
script end
<微任务>:
Promise1.then1 的回调
Promise2.then 的回调
Promise3.then 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
清空本轮微任务,
Promise1.then1
的回调执行,输出 promise1;Promise1.then2
的回调放入微任务队列;<执行栈>:
script start
async2 end
script end
promise1
<微任务>:
Promise1.then1 的回调Promise2.then 的回调
Promise3.then 的回调
Promise1.then2 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
Promise2.then
的回调执行,输出 async1 end;<执行栈>:
script start
async2 end
script end
promise1
async1 end
<微任务>:
Promise1.then1 的回调Promise2.then 的回调Promise3.then 的回调
Promise1.then2 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
Promise3.then
的回调执行,因为它是一个 Rejected 的状态,所以会执行 then 中的第二个参数回调,输出 error!!!;console.log('async1')
执行,输出 async1; 同时返回一个 Fulfilled 状态的新 Promise;errorFunc().then
放入微任务队列;<执行栈>:
script start
async2 end
script end
promise1
async1 end
error!!!
async1;
<微任务>:
Promise1.then1 的回调Promise2.then 的回调Promise3.then 的回调Promise1.then2 的回调
errorFunc().then 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
Promise1.then2
的回调执行,输出 promise2;<执行栈>:
script start
async2 end
script end
promise1
async1 end
error!!!
async1;
promise2
<微任务>:
Promise1.then1 的回调Promise2.then 的回调Promise3.then 的回调Promise1.then2 的回调errorFunc().then 的回调
<宏任务>:
setTimeout 的回调(延时2000)
-
errorFunc().then
的回调被执行,输出 async1 success;至此,本轮微任务被清空完毕;<执行栈>:
script start
async2 end
script end
promise1
async1 end
error!!!
async1;
promise2
async1 success
<微任务>:
Promise1.then1 的回调Promise2.then 的回调Promise3.then 的回调Promise1.then2 的回调errorFunc().then 的回调<宏任务>:
setTimeout 的回调(延时2000)
-
延时 2000ms 之后,取
setTimeout
的回调执行,输出 setTimeout;<执行栈>:
script start
async2 end
script end
promise1
async1 end
error!!!
async1;
promise2
async1 success
setTimeout
<微任务>:
Promise1.then1 的回调Promise2.then 的回调Promise3.then 的回调Promise1.then2 的回调errorFunc().then 的回调<宏任务>:
setTimeout 的回调(延时2000) -
完事
例子9
js
console.log('start')
let i = 0
let t = setInterval(() => {
if (i > 2) {
clearInterval(t)
t = null
return
}
// Promise1.then
Promise.resolve().then(() => {
console.log('promise')
})
setTimeout(() => {
// Promise2.then
Promise.resolve().then(() => {
console.log('setTimeout Promise')
})
console.log('setTimeout')
})
console.log('setInterval')
i++
}, 50)
console.log('end')
这题是我自己瞎写的,在分析之前,我们先了解一下 setInterval
是怎么工作的:
js
let i = 1
let t = setInterval(() => {
if (i > 3) {
clearInterval(t)
t = null
}
Promise.resolve().then(console.log('promise'))
setTimeout(() => {
console.log('setTimeout')
})
console.log(i++)
for (let j = 0; j < 1000000000; j++) {
let a = 1
}
}, 10)
我们运行上面这段代码可以发现,它每一次输出的结果都有可能不一样。因为 setInterval
当延时时间到了,就会把回调推到宏任务队列,受到 for 循环的影响,每次执行回调所需的时间都不一致,导致后续的某些 setInterval
的回调会比 setTimeout
的回调更早执行。
例子上的代码,在 setInterval
的回调中的代码执行用时不多,它应该比设置的延时时间要更短,所以我们可以认为它的下一次回调会在执行完这次回调之后再推入宏任务队列。
-
script 执行;
<执行栈>:
<微任务>:
<宏任务>:
-
console.log('start')
执行,输出 start;<执行栈>:
start
<微任务>:
<宏任务>:
-
setInterval
执行,setInterval
的回调被放入宏任务队列;<执行栈>:
start
<微任务>:
<宏任务>:
setInterval 的回调
-
console.log('end')
执行,输出 end;<执行栈>:
start
end
<微任务>:
<宏任务>:
setInterval 的回调
-
本轮没有微任务,没有 GUI 渲染,取一个宏任务
setInterval
的回调执行;Promise1.then
的回调放入微任务队列;setTimeout
的回调放入宏任务队列;console.log('setInterval')
执行,输出setInterval
;i++
执行,此时 i 为 1;本次回调执行完毕,setInterval
的回调再次加入宏任务队列;<执行栈>:
start
end
setInterval
<微任务>:
Promise1.then 的回调
<宏任务>:
setInterval 的回调setTimeout 的回调
setInterval 的回调
-
下一轮,清空所有微任务;
Promise1.then
的回调执行,输出 promise;<执行栈>:
start
end
setInterval
promise
<微任务>:
Promise1.then 的回调<宏任务>:
setInterval 的回调setTimeout 的回调
setInterval 的回调
-
取一个宏任务
setTimeout
的回调执行,Promise2.then
的回调被放入微任务队列;console.log('setTimeout')
执行,输出 setTimeout;<执行栈>:
start
end
setInterval
promise
setTimeout
<微任务>:
Promise1.then 的回调Promise2.then 的回调
<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调
-
下一轮,清空微任务
Promise2.then
的回调执行,输出 setTimeout Promise;<执行栈>:
start
end
setInterval
promise
setTimeout
setTimeout Promise
<微任务>:
Promise1.then 的回调Promise2.then 的回调<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调
-
取一个宏任务
setInterval
的回调执行;Promise1.then
的回调放入微任务队列;setTimeout
的回调放入宏任务队列;console.log('setInterval')
执行,输出setInterval
;i++
执行,此时 i 为 2;本次回调执行完毕,setInterval
的回调再次加入宏任务队列;<执行栈>:
start
end
setInterval
promise
setTimeout
setTimeout Promise
setInterval
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise1.then 的回调
<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调setTimeout 的回调
setInterval 的回调
-
同 6;清空所有微任务;
Promise1.then
的回调执行,输出 promise;<执行栈>:
start
end
setInterval
promise
setTimeout
setTimeout Promise
setInterval
promise
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise1.then 的回调<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调setTimeout 的回调
setInterval 的回调
-
同 7;取一个宏任务
setTimeout
的回调执行,Promise2.then
的回调被放入微任务队列;console.log('setTimeout')
执行,输出 setTimeout;<执行栈>:
start
end
setInterval
promise
setTimeout
setTimeout Promise
setInterval
promise
setTimeout
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise1.then 的回调Promise2.then 的回调
<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调setTimeout 的回调setInterval 的回调
-
同 8;清空微任务
Promise2.then
的回调执行,输出 setTimeout Promise;<执行栈>:
start
end
setInterval
promise
setTimeout
setTimeout Promise
setInterval
promise
setTimeout
setTimeout Promise
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise1.then 的回调Promise2.then 的回调<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调setTimeout 的回调setInterval 的回调
-
同 9;取一个宏任务
setInterval
的回调执行;Promise1.then
的回调放入微任务队列;setTimeout
的回调放入宏任务队列;console.log('setInterval')
执行,输出setInterval
;i++
执行,此时 i 为 3;本次回调执行完毕,setInterval
的回调再次加入宏任务队列; -
同10
-
同11
-
同12
<执行栈>:
start
end
setInterval
promise
setTimeout
setTimeout Promise
setInterval
promise
setTimeout
setTimeout Promise
setInterval
promise
setTimeout
setTimeout Promise
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise1.then 的回调Promise2.then 的回调~~Promise1.then 的回调Promise2.then 的回调<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调setTimeout 的回调~~setInterval 的回调setTimeout 的回调setInterval 的回调
-
取一个宏任务
setInterval
的回调执行;此时 t 为 3,满足i > 2
的条件,clearInterval(t)
执行,return
中止后续代码执行;<执行栈>:
start
end
setInterval
promise
setTimeout
setTimeout Promise
setInterval
promise
setTimeout
setTimeout Promise
setInterval
promise
setTimeout
setTimeout Promise
<微任务>:
Promise1.then 的回调Promise2.then 的回调Promise1.then 的回调Promise2.then 的回调~~Promise1.then 的回调Promise2.then 的回调<宏任务>:
setInterval 的回调setTimeout 的回调setInterval 的回调setTimeout 的回调~~setInterval 的回调setTimeout 的回调setInterval 的回调 -
完事
例子10
我们看下这个例子,DOM 中有一个按钮
html
<button id="J_button">Button</button>
然后在 js 中对该按钮加两个点击事件处理函数
js
const oBtn = document.querySelector('#J_button')
function handler1 () {
// setTimeout1
setTimeout(() => {
console.log('setTimeout1')
}, 0)
// promise1.then
Promise.resolve().then(() => {
console.log('promise1')
})
console.log('handler1')
}
function handler2 () {
// setTimeout2
setTimeout(() => {
console.log('setTimeout2')
}, 0)
// promise2.then
Promise.resolve().then(() => {
console.log('promise2')
})
console.log('handler2')
}
oBtn.addEventListener('click', handler1, false)
oBtn.addEventListener('click', handler2, false)
// oBtn.click()
在我们通过手动点击按钮的时候,结果如下:
如果我们把 oBtn.click()
的注释解开,也就是说让程序自动执行点击操作,重新刷新页面后,结果如下:
为什么两种结果不一样呢?
oBtn.click()
我们先从程序执行点击操作来分析,在 oBtn.click()
执行的时候,实际上是直接执行了 handler1
和 handler2
两个事件处理函数,相当于以下代码:
js
function handler1 () {
// setTimeout1
setTimeout(() => {
console.log('setTimeout1')
}, 0)
// promise1.then
Promise.resolve().then(() => {
console.log('promise1')
})
console.log('handler1')
}
function handler2 () {
// setTimeout2
setTimeout(() => {
console.log('setTimeout2')
}, 0)
// promise2.then
Promise.resolve().then(() => {
console.log('promise2')
})
console.log('handler2')
}
handler1()
handler2()
这样子看,我们就很轻易地分析出如图上的结果。
用户点击触发
首先,我们需要知道 addEventListener
是一个宏任务,当它被用户触发时,会把事件处理函数放入宏任务队列。只有这样,我们才能分析在用户点击时,代码是怎样运行的。
-
script 执行;获取DOM;函数声明;事件注册;
<执行栈>:
<微任务>:
<宏任务>:
-
当用户点击时,
handler1
和hanlder2
依次放入宏任务队列;<执行栈>:
<微任务>:
<宏任务>:
handler1
handler2
-
取一个宏任务
handler1
执行;setTimeout1
的回调被放入宏任务队列;promise1.then
的回调被放入微任务队列;console.log('handler1')
执行,输出 handler1;<执行栈>:
hanlder1
<微任务>:
promise1.then 的回调
<宏任务>:
handler1handler2
setTimeout1 的回调
-
下一轮,清空微任务
promise1.then
的回调执行,输出promise1
;<执行栈>:
hanlder1
promise1
<微任务>:
promise1.then 的回调<宏任务>:
handler1handler2
setTimeout1 的回调
-
取一个宏任务
handle2
执行;setTimeout2
的回调被放入宏任务队列;promise2.then
的回调被放入微任务队列;console.log('handler2')
执行,输出 handler2;<执行栈>:
hanlder1
promise1
handler2
<微任务>:
promise1.then 的回调promise2.then 的回调
<宏任务>:
handler1handler2setTimeout1 的回调
setTimeout2 的回调
-
下一轮,清空微任务
promise2.then
的回调执行,输出promise2
;<执行栈>:
hanlder1
promise1
handler2
promise2
<微任务>:
promise1.then 的回调promise2.then 的回调<宏任务>:
handler1handler2setTimeout1 的回调
setTimeout2 的回调
-
取一个宏任务
setTimeout1
的回调执行,输出 setTimeout1;<执行栈>:
hanlder1
promise1
handler2
promise2
setTimeout1
<微任务>:
promise1.then 的回调promise2.then 的回调<宏任务>:
handler1handler2setTimeout1 的回调setTimeout2 的回调
-
下一轮,没有微任务,没有 GUI 渲染,取一个宏任务
setTimeout2
的回调执行,输出 setTimeout2;<执行栈>:
hanlder1
promise1
handler2
promise2
setTimeout1
setTimtoue2
<微任务>:
promise1.then 的回调promise2.then 的回调<宏任务>:
handler1handler2setTimeout1 的回调setTimeout2 的回调 -
完事。
宏任务
宏任务(Macro Task)是宿主提供的异步方法和任务,如 script
、setTimeout
、setInterval
、setImmediate
、messageChannel
、requestAnimationFrame
、用户交互事件、ajax
。
setTimeout
setTimeout
用于设定一个定时器,该定时器到期后,执行一个函数,或一段代码;
plain-text
setTimeout(callback[, delay, arg1, arg2, ...])
setTimeout(callback[, delay])
setTimeout(code[, delay])
参数说明
callback
:到期后想要执行的回调函数;code
:到期后想要执行的代码,字符串形式,不推荐这种写法;delay
:延时,单位为毫秒;如果省略该参数,默认值为 0;延时不代表它的回调会在到期时立即执行,实际的执行时间会比期待的delay
长。arg1 ... argN
:附加参数,它们作为参数传递给callback
。
返回值
返回 timeoutID
,它是一个正整数,表示定时器的编号,用于取消该定时器。
示例
js
const t1 = setTimeout(() => {
console.log(1)
}, 1000)
const t2 = setTimeout(() => {
console.log(2)
clearTimeout(t1)
}, 500)
setInterval
setInterval
用于重复执行一个函数或一段代码,每次调用都有相同的时间间隔;具体参数和用法和 setTimeout
一致。
setImmediate
setImmediate
用于把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成其它语句后,就立即执行这个回调函数。
注意:这个方法只有最新版本的 IE/Edge 和 Node.js 0.10+ 实现了。
plain-text
setImmediate(callback[, arg1, arg2, ...])
setImmediate(callback)
参数说明
callback
:回调函数arg1 ... argN
,传入回调的参数
返回值
类似 setTimeout
我们可以通过 setTimeout(callback, 0)
来模拟 setImmediate
。
MessageChannel
MessageChannel
接口允许我们创建一个新的消息通道,并通过两个 MessagePort
属性发送数据。
示例
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MessageChannel</title>
<link rel="stylesheet" href="message-channel.css" />
</head>
<body>
<div class="container">
<!-- PORT1 -->
<section class="port port1">
<header>
<h2>与 PORT2 的对话</h2>
</header>
<div class="content-wrapper" id="J_port1-content"></div>
<div class="input-wrapper">
<textarea type="text" id="J_port1-input"></textarea>
<button type="button" id="J_port1-button">SEND</button>
</div>
</section>
<!-- PORT2 -->
<section class="port port2">
<header>
<h2>与 PORT1 的对话</h2>
</header>
<div class="content-wrapper" id="J_port2-content"></div>
<div class="input-wrapper">
<textarea type="text" id="J_port2-input"></textarea>
<button type="button" id="J_port2-button">SEND</button>
</div>
</section>
</div>
<script type="tpl" id="J_tpl"></script>
<script>
const zeroFill = input => ('' + input).padStart(2, '0')
function formatDate (date, format = 'YYYY-MM-DD HH:mm:ss') {
const Y = date.getFullYear()
const M = date.getMonth() + 1
const D = date.getDate()
const H = date.getHours()
const m = date.getMinutes()
const s = date.getSeconds()
return format.replace(/YYYY/, Y)
.replace(/MM/, zeroFill(M))
.replace(/DD/, zeroFill(D))
.replace(/HH/, zeroFill(H))
.replace(/mm/, zeroFill(m))
.replace(/ss/, zeroFill(s))
}
</script>
<script type="text/javascript">
const { port1, port2 } = new MessageChannel()
const oPort1Input = document.querySelector('#J_port1-input')
const oPort1Button = document.querySelector('#J_port1-button')
const oPort1Content = document.querySelector('#J_port1-content')
const oPort2Input = document.querySelector('#J_port2-input')
const oPort2Button = document.querySelector('#J_port2-button')
const oPort2Content = document.querySelector('#J_port2-content')
const tpl = document.querySelector('#J_tpl').innerHTML
const reg = /{{(.+?)}}/g;
const data = new Proxy([], {
set (arr, index, value) {
Reflect.set(arr, index, value)
render()
return true
}
})
function render () {
let innerHTML = ''
data.forEach((item) => {
innerHTML += tpl.replace(reg, ($, $1) => {
return item[$1.trim()]
})
})
oPort1Content.innerHTML = innerHTML
oPort2Content.innerHTML = innerHTML
scrollToBottom()
}
function scrollToBottom () {
oPort1Content.scrollBy(0, 100000000)
oPort2Content.scrollBy(0, 100000000)
}
function handleSendMessage (port) {
let value = ''
switch (port) {
case 'port1':
value = oPort1Input.value
if (!value) {
return
}
port1.postMessage(value.replace(/[\r\n]/g, '<br />'))
oPort1Input.value = ''
oPort1Input.focus()
break
case 'port2':
value = oPort2Input.value
if (!value) {
return
}
port2.postMessage(value.replace(/[\r\n]/g, '<br />'))
oPort2Input.value = ''
oPort2Input.focus()
break
default:
break
}
}
port1.onmessage = (e) => {
data.push({
from: 'port2',
to: 'port1',
message: e.data,
date: formatDate(new Date())
})
}
port2.onmessage = (e) => {
data.push({
from: 'port1',
to: 'port2',
message: e.data,
date: formatDate(new Date())
})
}
oPort1Input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if ((e.metaKey || e.ctrlKey)) {
e.target.value += '\n'
return
}
e.preventDefault()
handleSendMessage('port1')
}
})
oPort2Input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if ((e.metaKey || e.ctrlKey)) {
e.target.value += '\n'
return
}
e.preventDefault()
handleSendMessage('port2')
}
})
oPort1Button.addEventListener('click', () => {
handleSendMessage('port1')
})
oPort2Button.addEventListener('click', () => {
handleSendMessage('port2')
})
</script>
</body>
</html>
message-channel.css
css
* { margin: 0; padding: 0; box-sizing: border-box; }
body { color: #333; font-size: 16px; }
.container {
display: flex;
width: 1000px;
margin: 50px auto;
justify-content: space-between;
}
.port {
width: 49%;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.port header {
padding: 8px 16px;
border-bottom: 1px solid #ddd;
background-color: deepskyblue;
}
.port h2 {
color: #fff;
font-size: 20px;
line-height: 24px;
}
.content-wrapper {
padding: 16px;
height: 320px;
overflow-y: auto;
background-color: #f4f4f8;
}
.content-wrapper .msg::after {
content: '';
display: table;
clear: both;
}
.content-wrapper .header {
display: flex;
align-items: center;
}
.content-wrapper .msg:not(:first-child) {
margin-top: 24px;
}
.content-wrapper .message {
position: relative;
max-width: 80%;
margin-top: 10px;
padding: 6px 16px;
border-radius: 4px;
word-break: break-all;
}
.content-wrapper .header .name {
font-size: 18px;
font-weight: 700;
}
.content-wrapper .header .date {
margin: 0 8px;
font-size: 14px;
color: #8c8c8c;
}
.input-wrapper {
border-top: 1px solid #ddd;
background-color: #f4f4f8;
}
.input-wrapper::after {
content: '';
display: table;
clear: both;
}
.input-wrapper > textarea {
display: block;
width: 100%;
height: 80px;
padding: 16px;
border: 0;
outline: 0;
line-height: 24px;
font-size: 16px;
background-color: #fff;
resize: none;
}
.input-wrapper > button {
float: right;
padding: 8px 16px;
outline: 0;
color: #fff;
border: 1px solid deepskyblue;
background-color: deepskyblue;
}
:is(
.port1 .content-wrapper .msg-from-port1,
.port2 .content-wrapper .msg-from-port2
) .header {
justify-content: flex-end;
}
:is(
.port1 .content-wrapper .msg-from-port1,
.port2 .content-wrapper .msg-from-port2
) .header .name {
order: 2;
}
:is(
.port1 .content-wrapper .msg-from-port1,
.port2 .content-wrapper .msg-from-port2
) .header .date {
order: 1;
}
:is(
.port1 .content-wrapper .msg-from-port1,
.port2 .content-wrapper .msg-from-port2
) .message {
float: right;
color: #fff;
background-color: deepskyblue;
}
:is(
.port1 .content-wrapper .msg-from-port1,
.port2 .content-wrapper .msg-from-port2
) .message::before {
content: '';
position: absolute;
right: 10px;
top: -5px;
width: 0;
height: 0;
border-bottom: 5px solid deepskyblue;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
:is(
.port1 .content-wrapper .msg-from-port2,
.port2 .content-wrapper .msg-from-port1
) .message {
float: left;
color: #333;
background-color: #fff;
}
:is(
.port1 .content-wrapper .msg-from-port2,
.port2 .content-wrapper .msg-from-port1
) .message::before {
content: '';
position: absolute;
left: 10px;
top: -5px;
width: 0;
height: 0;
border-bottom: 5px solid #fff;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
:is(
.port1 .content-wrapper .msg-from-port2,
.port2 .content-wrapper .msg-from-port1
) .message {
float: left;
color: #333;
background-color: #fff;
}
requestAnimationFrame
requestAnimationFrame()
方法需要传递一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
回调函数通常每秒执行60次,但在大多数浏览器中,回调函数的执行次数与屏幕的刷新率相匹配。
回调函数会被传入一个 DOMHighResTimeStamp
参数,指示当前回调函数被触发的时间。
示例
js
const element = document.getElementById('some-element-you-want-to-animate');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
//这里使用`Math.min()`确保元素刚好停在200px的位置。
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // 在两秒后停止动画
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
我们分析一下下面这段代码的执行顺序:
html
<div id="J_wrapper"></div>
<script type="text/javascript">
const oWrapper = document.querySelector('#J_wrapper')
function callback (mutationList, observer) {
console.log(4)
// setTimeout2
setTimeout(() => {
console.log(5)
})
// promise2.then
Promise.resolve().then(() => {
console.log(6)
})
}
const observer = new MutationObserver(callback)
// childList、attributes 和 characterData 三个属性必须有一个为 true
observer.observe(oWrapper, {
childList: true, // 观察目标子节点的变化,是否有添加或者删除,无默认值
attributes: true, // 观察属性变动,无默认值
characterData: true // 观察指定目标节点或子节点树中节点所包含的字符数据的变化,无默认值
})
oWrapper.textContent = 'Hello world!'
console.log(1)
// setTimeout1
setTimeout(() => {
console.log(2)
})
// promise1.then
Promise.resolve().then(() => {
console.log(3)
})
</script>
- script 执行;
- 代码依次执行,直接
observer.observe()
执行,MutationObserver
的回调放入宏任务
微任务
微任务(Micro Task)指的是语言标准(ECMA-262)提供的API:Promise.prototype.then
、mutationObserver
、process.nextTick
MutationObserver
MutationObserver
提供了监听 DOM 树变化的能力。
它是一个构造函数,实例化之后返回一个新的观察器,它会在触发指定 DOM 事件时,调用指定的回调函数。
MutationObserver
对 DOM 的观察不会立即启动,必须先调用 observe()
方法来开启,要监听哪一部分 DOM 以及要响应哪些更改。
参数
callback
:一个回调函数,当指定的节点或子树以及配置项有 DOM 变化时调用。
回调函数有两个参数:一个是描述所有被触发改动的 MutationRecrod
对象数组;另一个是调用该函数的 MutationObserver
对象。
示例
js
const oWrapper = document.querySelector('#J_wrapper')
function callback (mutationList, observer) {
console.log(mutationList, observer)
}
const observer = new MutationObserver(callback)
// childList、attributes 和 characterData 三个属性必须有一个为 true
observer.observe(oWrapper, {
childList: true, // 观察目标子节点的变化,是否有添加或者删除,无默认值
attributes: true, // 观察属性变动,无默认值
characterData: true // 观察指定目标节点或子节点树中节点所包含的字符数据的变化,无默认值
})
oWrapper.textContent = 'Hello world!'
process.nextTick
这个将放在 Node.js 的 Event Loop 里面说明