技术交流群的朋友们提起了一个关于nextTick的问题: 引发了大家的热烈讨论
q: 问个问题,浏览器渲染时机是,当前宏任务完成,然后再进行微任务队列,然后完成后才渲染DOM,但是Vue的nextTick是优先微任务,那不就在渲染前执行?这个时候还没渲染吧, 这个时候能为什么能拿到更新之后的DOM?
a: dom是js描述UI的对象
a: 渲染UI是渲染UI不一样哦
a: js中的dom对象更新了
a: UI没更新
b: 确实在当前宏任务完成后,浏览器会先执行微任务队列,然后再渲染DOM。而Vue的nextTick确实是优先微任务,但它只是在下一个微任务队列中执行回调函数,而不是在渲染前执行。因此,Vue的nextTick不会影响浏览器的渲染时机。
a : 每一帧都会判断dom有没有改变然后去更新页面这样
a : vue改dom对象的操作在nextTick之前
a: 所以就可以拿到修改的属性,然后页面上要等渲染UI后才能看到
c: 这个地方用框架来理解会有点奇怪
c: 用原生dom来理解好一点
c: 比如你创建一个元素 然后append到body 再获取高度 这个时候肯定是能查到元素高度的
c: 这个过程中 所有代码不都属于同一个任务吗
d: 针对nextTick这个问题,不知道我理解的对不对。nextTick本质是promise.then,除了里面的回调函数,其他所有的宏任务和微任务都会按顺序执行,等这些宏任务微任务都执行完毕之后,vue会相应地更新虚拟dom,这个时候才会执行nextTick即promise.then中的回调函数,所以里面能拿到最新的虚拟dom
e: 我把其他所有的宏任务微任务按顺序执行的整个过程理解为promise里面的构造函数,等构造函数里面的所有东西都执行完,再执行then,即对应nextTick中的回调函数
f: Vue更新数据时,dom并不会立即更新,因为你可能同时更新很多数据,如果dom更新是同步的,会导致你一个数据更新了100次,dom也更新100次,所以vue把dom更新放在了微任务队列中,当你同步更新了一百次数据,vue会在下一个微任务中更新dom
g: 是主线程去拿微任务或者宏任务(不纠结现在其他的任务队列,统称为宏任务),当主线程空闲时,会去拿微任务,如果微任务执行完了,再去拿宏任务,如果执行完一个宏任务,会再去微任务队列查看有没有微任务,如果没有,则再去拿宏任务,这种循环执行任务队列的方式,叫事件循环 ,也叫消息循环
各路大神各显神通, 八仙过海, 都说出了自己不同的理解
在理解这个问题之前, 我们有必要需要先了解一下什么是宏任务
setTimeout
: 用于在指定的时间后执行一个回调函数。它创建的宏任务将在指定的延时时间到达后加入宏任务队列。setInterval
: 用于以固定的时间间隔重复执行一个回调函数。它创建的宏任务将在每个时间间隔到达时加入宏任务队列。setImmediate
: 仅在 Node.js 环境中可用,用于在当前事件循环迭代完成后立即执行一个回调函数。它创建的宏任务将在当前迭代周期的末尾加入宏任务队列。- I/O 操作:网络请求、文件读写等异步 I/O 操作在完成时会创建宏任务并加入宏任务队列。
- 用户交互事件:如鼠标点击、键盘输入等。当用户产生交互事件时,浏览器会将相关的事件处理函数作为宏任务加入队列。
- UI 渲染:浏览器可能会将 UI 渲染任务放入宏任务队列,以确保在其他宏任务执行完毕后进行页面渲染。
MessageChannel
和MessagePort.postMessage
: 用于在 Web Workers 之间或跨窗口通信时创建宏任务。- 其他 API 产生的宏任务:例如,
requestAnimationFrame
(在下一次重绘之前执行回调函数)和requestIdleCallback
(在浏览器空闲时段执行回调函数)等。
这就说明, 键盘事件、鼠标事件、网络事件以及HTML解析在某种程度上可以看作宏任务。具体来说,这些事件或操作会触发相应的回调函数或事件处理函数。当浏览器准备执行这些回调或事件处理函数时,它们会被作为宏任务添加到事件循环的宏任务队列中。
- 键盘事件和鼠标事件:当用户与页面进行交互,例如点击鼠标或按下键盘时,浏览器会将与这些事件相关的回调函数或事件处理函数作为宏任务加入宏任务队列。
- 网络事件:当网络请求(如Ajax、Fetch等)完成时,成功或失败的回调函数会作为宏任务添加到宏任务队列中。
- HTML解析:HTML解析过程中,浏览器会遇到例如
<script>
标签等需要执行的代码。这些同步代码会直接在主线程上运行,而不是作为宏任务。然而,在解析过程中可能会遇到异步操作,如动态加载脚本等,这些操作会触发回调函数,这些回调函数会作为宏任务加入到宏任务队列中。
其实针对这个问题, 如果要想完美的解决。 得了解到一些概念, 同步代码, 宏任务, 微任务, 还有vue中的nextTick方法,
- 同步代码:同步代码是指在执行时按照顺序依次执行的代码。在同步代码中,一个任务的执行必须等待前一个任务完成。
- 宏任务(macro-task):宏任务是指一些异步操作的任务,如
setTimeout
、setInterval
和requestAnimationFrame
等。宏任务会被添加到宏任务队列中,浏览器会在适当的时机执行这些任务。 - 微任务(micro-task):微任务是一种比宏任务更轻量级的异步任务,如
Promise.then
、MutationObserver
等。浏览器在执行宏任务之间以及执行完所有同步代码之后,会先执行微任务队列中的所有任务。 - Vue中的
nextTick
方法:nextTick
是Vue中的一个方法,它的作用是在DOM更新后执行指定的回调函数。nextTick
的实现是基于微任务(micro-task),因此它的执行时机在当前宏任务完成、微任务队列执行之后,但在下一个宏任务开始之前。
话不多说, 我们来举个例子来帮助理解这些个概念:
1 | console.log('1. 同步代码开始'); |
在这个例子中,代码的执行顺序如下:
- 首先,浏览器执行同步代码,输出
1. 同步代码开始
和1. 同步代码结束
。 - 接下来,浏览器检查微任务队列,发现有一个
Promise.then
的任务,执行它,输出2. 微任务 Promise.then
。 - 此时,Vue的数据已经发生了变化,因此Vue会在微任务队列中添加一个
nextTick
的任务。浏览器发现微任务队列还有任务,执行nextTick
回调,输出3. Vue.nextTick 回调
。 - 最后,浏览器执行宏任务队列中的
setTimeout
任务,输出4. 宏任务 setTimeout
。
通过这个例子,你可以看到nextTick
是在同步代码执行完毕、微任务队列执行之后、下一个宏任务开始之前执行的。因此,nextTick
可以确保我们的回调函数在DOM更新之后执行,同时不影响浏览器的渲染性能。
我们还需要注意一点, 在事件循环(event loop)中,宏任务(macro-task)和微任务(micro-task)本身与渲染并没有直接关系。事件循环是浏览器中用于处理异步任务的一种机制,而渲染是浏览器将DOM更新反映在屏幕上的过程。
什么时候事件循环会进入下一个迭代周期?
我们来用代码举一个例子:
1 | console.log("同步代码 1"); |
这段代码的执行顺序:
同步代码1 ——-> 同步代码2———> 微任务1———> 微任务2———–>宏任务1(这个时候执行完了一个宏任务, 就代表着进入了下一个迭代周期)
在第二个迭代周期里, 只有一个宏任务, 所以直接打印宏任务2
我们来看看进入迭代周期的标志:
- 当前的同步代码执行完毕。
- 当前的微任务队列中的所有任务都已执行完毕。
在每个迭代周期中,事件循环会按照以下顺序执行:
- 执行同步代码。
- 检查并执行微任务队列中的所有任务。
- 检查并执行宏任务队列中的一个任务。
所以综上所述:
Vue 的 nextTick
方法在内部使用了微任务队列。当数据发生变化时,Vue 会将 nextTick
的回调函数添加到微任务队列中。在浏览器的事件循环中,微任务队列中的任务会在宏任务之前执行。因此,可以理解为 nextTick
的回调函数会在当前宏任务完成后、DOM 渲染之前执行。
然而,需要注意的是,Vue 的数据变化和 DOM 更新是异步的。即使 nextTick
的回调函数在 DOM 渲染之前执行,但它仍然可以访问到已经更新的 DOM。这是因为 Vue 会在内部使用一个队列来批量处理数据更新,然后在一个适当的时机(通常是在当前宏任务完成后的微任务队列中)将这些更新应用到 DOM 上。这样,当 nextTick
的回调函数执行时,DOM 已经更新,但浏览器尚未进行最终渲染。
值得注意的是:
当代码刚开始执行时,浏览器首先会执行同步代码。这些同步代码并不属于宏任务。然而,在同步代码执行过程中,可能会创建并添加宏任务到宏任务队列中。例如,当代码中包含setTimeout
或setInterval
这类异步函数时,它们会创建并添加宏任务到队列。
简而言之,在代码刚开始执行时,是没有正在执行的宏任务的。同步代码执行完毕后,事件循环将检查宏任务队列,并按顺序执行队列中的任务。如果同步代码中创建了宏任务,那么这些任务将在同步代码执行完毕后被执行。