众所周知,js是单线程,负责页面绘制和用户事件等,即我们通常理解的UI线程,对于页面的绘制,只能在一个线程上更新,这是共识,不然同时有多个线程更新UI,这是不可理解的。虽然android的UI线程也是如此,但是android是支持多线程的,而js就只有单线程,这意味着我们同一时间只能做一件事情,虽说有异步任务,但那也只是把执行时间延后罢了。
当然后面为了适应时代发展,js通过web worker支持了多线程能力,这有一个例子
下面是一些概念:
同步任务
JS 主线程里面立即被推入执行栈且可以被执行的函数。在主线程上排队执行的任务,只有前一个执行完毕,才能执行后一个,代码是阻塞的,顺序执行。要注意的是,click,dispatchEvent等人工合成事件是同步任务,同步调用事件处理程序。可以参考以下链接:
// 下面输出为
// on click
// end
const App = () => {
return (
<div ref={(ref) => {
if(ref) {
ref.click()
console.log('end')
}
}} onClick={() =>{
console.log('on click')
}}>
</div>
);
}
复制代码
异步任务
如果一个函数在调用之后 不能马上得到预期结果 那么就是异步任务,任务是非阻塞的。
每次执行异步任务,就会将任务放进对应的任务队列。
- setTimeout setInterval
- promise
- dom事件
- 网络请求
- …
事件循环
js的主线程通过等待任务队列,执行任务源源不断地处理用户事件和页面绘制,每次事件循环称为一次tick,包括从任务队列取出任务执行,清空微任务队列,页面重绘(不一定执行)。
宏任务(浏览器发起的)
执行宏任务进入宏任务队列
- I/O
- dom事件
- setTimeout setInterval(web api) 浏览器有个定时器模块,定时器到了执行时间才会把异步任务放到异步队列,setTimeOut setInterval的延时就是指多少时间后回调函数会放入任务队列
微任务(js引擎发起的)
执行微任务会进入当前任务的微任务队列,在下一个事件到来之前会被清空执行。微任务的好处就是优先级高, 但是如果反复执行微任务,会造成下一个事件的处理延后。
- Promise.then .catch
- MutationObserver
- queueMicrotask 把函数当成微任务入队
requestAnimationFrame
requestAnimationFrame也属于异步任务,但是它比较特殊,既不属于宏也不属于微,它是在event下次重绘之前调用,也就是晚于微任务,早于下一次事件。具体可以看这个。但是每个eventloop不一定会进行重绘,所以在不同浏览器中,requestAnimationFrame的执行时机不太一样,这是重绘时机的规范。
任务队列
有一个演示效果的demo latentflip.com/loupe
(图片来源zhuanlan.zhihu.com/p/105903652)
例子
代码在这
模拟事件机制
console.log("--- task start ---");
setTimeout(() => {
console.log("macro task 1");
}, 0);
console.log("wait countdown");
new Promise((resolved, reject) => {
let time = Date.now();
console.log("countdown 500ms start");
while (Date.now() - time < 500) {}
console.log("countdown end");
resolved('success')
})
.then((e) => {
console.log("micro task 1");
})
console.log("wait end");
queueMicrotask(() => {
console.log("micro task 2");
});
queueMicrotask(() => {
console.log("micro task 3");
console.log("---micro task end---");
console.log("---macro task start---");
});
setTimeout(() => {
console.log("macro task 2");
console.log("---macro task end---");
}, 0);
console.log("---task end---");
console.log("---micro task start---");
复制代码
输出结果如下:
--- task start ---
wait countdown
countdown 500ms start
countdown end
wait end
---task end---
---micro task start---
micro task 1
micro task 2
micro task 3
---micro task end---
---macro task start---
macro task 1
macro task 2
---macro task end---
// 可以看出promise.then是微任务 微任务执行顺序和入队顺序一致
// promise里是同步任务 进入执行栈执行
// setTimeout是宏任务 执行顺序和入队顺序一致 晚于微任务执行
复制代码
模拟dom事件
import "./styles.css";
const onClick = (type:string) => {
console.log(`${type} on click`)
Promise.resolve().then(e => {
console.log(`${type} micro task`)
})
setTimeout(() => {
console.log(`${type} macro task`)
}, 0);
}
export default function App() {
return (
<div className="App">
<h2>查看点击事件的过程</h2>
<div className="Parent" onClick={()=>{
onClick('parent')
}}>
<div className="Child" ref={(ref) => {
if(ref) {
console.log('---auto click---')
ref.click()
}
}}onClick={()=>{
onClick('child')
}}>点我</div>
</div>
</div>
);
}
复制代码
页面加载成功结果如下:
---auto click---
child on click
parent on click
child micro task
parent micro task
child macro task
parent macro task
// 主动进行事件的合成和分发,这时候两个onClick是作为同步事件进入执行栈,
// 两个onClick在同一个事件中,所以会先输出两个on click
点击按钮之后输出结果如下:
child on click
parent on click
child micro task
parent micro task
child macro task
parent macro task
// 而手动点击则不一样,这时候两个onClick都是作为异步任务进入宏队列
// 两个onClick不在同一个事件中,所以第一个onClick的promise会先于第二个onClick执行
复制代码
总结
在每次事件循环中,从宏任务队列取出任务t执行,然后把任务t里的同步任务按顺序执行, 异步任务则进入任务队列,如果是宏任务,则进入宏任务队列,如果是微任务,则进入当前微任务队列。接着,等执行栈清空之后,会执行当前微任务队列的所有任务,接着浏览器根据是否重绘页面调用requestAnimationFrame。