你真的了解NextTick吗?带你剖析NextTick原理

5,287 阅读6分钟

自我介绍

看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。

前言

我们或许都知道nickTick的回调是异步的,并且Vue视图渲染也是异步的,但你知道它们之间有什么关系吗?知晓它们之间的关系,更有利于你顺利搬砖。

引出

因为Vue的异步渲染关系,当我们想要获取渲染完成后的dom时,或者在视图更新之后去做一些事情时,就需要在nextTick的回调当中去做一些逻辑。

比如这样一个demo:

点击按钮 --> 修改响应式数据count --> 通过nextTick去获得dom更新之后的innerHTML

通常是这样写

image.png

image.png

原理就是,先更新count触发了视图更新,调用nixtTick在此轮视图渲染之后,就可以获取到最新的dom,此时再取值即可。

当然nextTick也可以这么用,它是一个可以重载的函数,有3种用法。这也是一种

image.png

将nextTick理解为异步任务,结合事件循环。

通过微任务

image.png

通过宏任务

image.png

都是可以达到目的

一切的一切都是如此的顺利。。。

但如果是要在 修改响应式数据前后拿到 视图更新前后的dom呢?

这样可以吗? image.png

结论是可以

image.png 那用微任务呢? image.png 结论也是可以 image.png 如果用setTimeOut 呢, image.png 结论是不行 image.png

思考一下,因为vue更新视图也是通过异步任务来的,渲染视图也是一次异步任务。

而异步任务(可以分为宏任务、微任务 各自有自己的队列,这里涉及到事件循环的知识)是有一个队列的,nextTick的回调理解为一个异步任务,视图更新也是一个异步任务,它是这么一个触发过程:

修改响应式数据-->通过依赖关系触发视图更新任务-->将视图更新任务放入异步队列

因此在 队列中 视图渲染这个异步任务的前后是可以获取到渲染前后的DOM。想象中的异步队列:[其他任务,视图更新,其他任务,. . .]

而宏任务是在下一轮事件循环,必然是修改视图之后(目前猜测视图更新是一个微任务)了。因此获取到的dom必然是最新的。

既然如此,尝试修改两次呢?能获取中间态的dom吗?

这里想验证是否会有多次视图更新的任务放入到异步队列 比如:[其他任务,视图更新,其他任务,视图更新. 其他任务. .]

image.png

image.png

可以看到并不能获取到更新的中间态呢,这是不是就说明了,vue的异步刷新机制会在同步任务执行完之后更新视图,更新视图的时候,count已经是最新的值了。

这里可以猜测上面的代码所产生的异步任务队列是:

[ nextTick的获取DOM回调,更新视图,nextTick的获取DOM回调,nextTick的获取DOM回调 ]

下面进入到源码调试看看,是不是跟想象中一致呢?

源码分析

首先在回调函数上的最后打上debugger,这样就能进入到此轮循环的异步任务当中了

image.png

到了这里next-tick.ts

image.png 这里明显就是调用之前我们猜测的4个异步任务:【nextTick的获取DOM回调,更新视图,nextTick的获取DOM回调,nextTick的获取DOM回调】

继续走下去,发现会到nextTick这里,

image.png

这是因为通过nextTick传入的回调函数被包裹了一层,才放到队列里头。当call的时候,自然就回到了clik里面了

image.png 此时打印到控制台的count:0

image.png

继续执行队列里的下一个任务,如果没猜错这里应该就是执行视图渲染的微任务了,可以看到来到了这个flushSchedulerQueue的函数,源码在这

image.png 分析一下这个函数的内容,主要是两部分

1.对在queue里的watcher 进行排序,主要作用是:

  • 保证父组件更新之后子组件再更新的顺序
  • 用户自定义的watcher的执行要在渲染的watcher之前
  • 在父组件的watcher.run的过程当中,子组件被销毁了,可以跳过子组件watcher的run

2.依次执行watcher.run去更新视图

然后就是紧接着执行完剩下两个nextTick的回调了

分析到这里,基本就捋清楚了nextTick回调函数执行过程了,并且它的执行函数有两种,一种是用户调用传入的,另一种是更新视图时候的flushSchedulerQueue

接下来分析下,handleClick当中同步代码执行时,是如何将nextTick的回调包装到任务队列(也就是flushCallbacks的callbacks)当中呢。

可以看看调用栈,就知道这个异步任务的来头了。 image.png

再结合nextTick函数

image.png

得到这样的流程:

click回调-->调用nextTick -->将任务包装一下放入到callbacks --> timerFunc -->flushCallbacks

也就是说 ,nextTick是一个收集入口,最后在timerFunc执行微任务,对nextTick收集到的回调队列进行异步执行,找到timeFunc定义的代码

image.png

简单分析下,这里不就是进行了一个优雅降级吗?从Promise.then到MutationObserver,到setImmediate最后到setTimeout。

因此纠正一下前面猜想nextTick里执行的异步任务,不一定是微任务,在降级的情况下是会到宏任务的,但在大多数浏览器及版本上都表现为微任务

最后再看看修改响应式数据的时候,是如何调用nextTick去做任务收集的 源码在这 image.png 可以看到在queueWatcher这个函数中,先对watcher进行判断,然后放入到收集watcher的queue当中以供后续的flushSchedulerQueue使用,最后用nextTick将执行queue的函数flushSchedulerQueue收集到callbacks里面。

前面的判断能确保相同的watcher不会重复放入到队列当中,也就是 多次修改响应式数据-->触发watcher.update-->queueWatcher--> 放入更新任务到nextTick当中以供调度 ,这个过程会在queueWatcher这一步被阻断,保证了异步队列里只有一个视图更新的任务,这就达到了优化的效果。

在Vue3当中

源码

image.png

可以看到是在更新视图的job之后,再去直接调用nextTick传入的回调了,也就是说在vue3当中,用户调用nextTick的放入异步任务,一定是在视图渲染完成之后执行了。并且也舍弃了原来优雅降级的内容,因为vue3的兼容性设计就是抛弃了IE11

总结

在Vue2当中,nextTick可以理解为就是收集异步任务到队列当中并且开启异步任务去执行它们。它可以同时收集组件渲染的任务,以及用户手动放入的任务。组件渲染的任务是由watcher的update触发,并且通过flushSchedulerQueue来包裹,最后推到nextTick的队列里,等待执行。

而在Vue3当中,nextTick则是利用promise的链式调用,将用户放入的回调放在更新视图之后的then里面调用,用户调用多少次nextTick,就接着多少个then。

附上链接

VUE2

VUE3