众所周知,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 mockEventLoop = () => {
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---");
};
const onClick = (type: string, event: string) => {
console.log(`${type}`, `on ${event} click `);
Promise.resolve().then((e) => {
console.log(`${type}`, `${event} click micro task `);
});
setTimeout(() => {
console.log(`${type}`, `${event} click macro task `);
}, 0);
};
export default function App() {
return (
<div className="App">
<h2 onClick={mockEventLoop}>点击模拟事件机制</h2>
<h2>查看点击事件的过程</h2>
<div
ref={(ref) => {
if (ref) {
ref.addEventListener("click", (e) => {
onClick("parent", "native");
});
}
}}
className="Parent"
onClick={(e) => {
onClick("parent", "synthetic");
}}
>
<div
className="Child"
ref={(ref) => {
if (ref) {
ref.addEventListener("click", (e) => {
onClick("child ", "native");
});
setTimeout(() => {
console.log("--auto click --");
ref.click();
}, 1000);
}
}}
onClick={(e) => {
onClick("child ", "synthetic");
}}
>
点我
</div>
</div>
</div>
);
}
页面加载成功结果如下:
--auto click --
child native click
parent native click
child synthetic click
parent synthetic click
child native click micro task
parent native click micro task
child synthetic click micro task
parent synthetic click micro task
child native click macro task
parent native click macro task
child synthetic click macro task
parent synthetic click macro task
// onClick分两类 原生事件和react的合成事件
// 主动进行事件的合成和分发,这时候原生事件和合成事件的onClick是作为同步事件进入执行栈,
// onClick在同一个事件中,所以会先输出click 再输出微任务 宏任务
点击按钮之后输出结果如下:
child native click
child native click micro task
parent native click
parent native click micro task
child synthetic click
parent synthetic click
child synthetic click micro task
parent synthetic click micro task
child native click macro task
parent native click macro task
child synthetic click macro task
parent synthetic click macro task
// onClick分两类 原生事件和react的合成事件
// 手动点击则不一样
// 原生事件的onClick作为异步任务进入宏队列
// 两个onClick不在同一个事件中,所以child的onClick的promise会先于parent的onClick执行
// 合成事件的onClick作为同步任务进入执行栈
// 两个onClick在同一个事件中,所以child的onClick的promise会晚于parent的onClick执行
总结
在每次事件循环中,从宏任务队列取出任务t执行,然后把任务t里的同步任务按顺序执行, 异步任务则进入任务队列,如果是宏任务,则进入宏任务队列,如果是微任务,则进入当前微任务队列。接着,等执行栈清空之后,会执行当前微任务队列的所有任务,接着浏览器根据是否重绘页面调用requestAnimationFrame。