JavaScript 引擎是建立在一个事件循环系统之上的,它实时监控事件队列,如果有事件就执行,如果没有事件就等待。事件系统是一个典型的生产消费模式,生产者发出事件,接收者监听事件,在UI 开发中是常见的一个设计模式。在 JavaScript中,事件被定义为任务,有微任务和宏任务两种任务。
宏任务
对于浏览器,宏任务是 JavaScript要执行的代码,例如运行外部引入的脚本,页面上需要处理的用户点击事件,定时任务等等。每个宏任务执行完毕之后,才进行渲染,如果宏任务运行事件过长,会导致浏览器卡死,没有响应。创建宏任务最典型的方式就是 setTimeOut。
微任务
在每次宏任务运行完成之后,都会将本次宏任务中的待处理的微任务处理完成,所以微任务执行过程中,页面是不会进行渲染的。创建微任务,可以通过queueMicrotask 或者 Promise。如下图所示。
可以通过如下代码熟悉微任务和宏任务的处理、执行顺序。
console.log(1);setTimeout(() => console.log(2));Promise.resolve().then(() => console.log(3));Promise.resolve().then(() => setTimeout(() => console.log(4)));Promise.resolve().then(() => console.log(5));setTimeout(() => console.log(6));console.log(7);#输出为
1 7 3 5 2 6 4
微任务和宏任务,对于队列来说,没有什么特别的,监听队列并执行任务,微任务放到当前宏任务完成之后再执行。根据宏任务的特性,当前任务完成之前是不更新页面的,如果在页面逻辑中,在同一个宏任务、微任务中更新了多次 Dom,那么实际上只有最后的更新才是有效的。JavaScript中最特别是 UI 处理方式,它不像 Android 和 iOS,它们都有专门做渲染的线程,要理解任务和 UI 渲染之间的关系。
在 React中,State 变化之后会进行组件的重新渲染,为了提高性能,setState 是通过异步的方式进行调用的,State 修改完成之后并不是立刻进行渲染。如果我们做这样一个效果,为用户提示错误信息 “请输入用户名!”,如果这个字段一直不填写,这个错误可能会提示多次。但是,由于 State 没有变化,第二次不会在提示,所以要先清一下错误信息,然后再更新,这里就需要在不同的宏任务中进行处理,代码如下:
function App() {const [error, setError] = useState(true);const [name, setName] = useState("test");const handleClick = () => {setName("")setTimeout(() => {setName(name)})};return (<div className="App"><AutoHideInput name={name}/><input type="text" onChange={(e) => setName(e.target.value)}/><button onClick={handleClick}>Click</button></div>);
微任务和宏任务是前端的基础,在很多框架中都能看到他们的身影,需要理解透彻,一些渲染中的奇怪问题都可能与宏任务有关。