vue的nextTic源码解析、实现原理

Scroll Down

几个星期前笔者在面试时被面试官问到:vue的nextTick的作用是什么,当时我知道的并不清楚,只是知道:这个东西的作用和setTimeout(()=>{},0)的作用挺像的,就理所当然的说了一个:用来跳当前事件循环的。显然这个答案是不正确滴,准确的说是大部分时候不正确。为什么这样子说叻,来,一起去nextTick的源码里找一找答案。

一:主体部分

nextTick的源码在vue > src > core > utile > next-tick.js中。先找到这部分代码。
image.png
依照我自己的分析,我把代码分成了图中标注出的四部分,想要回答面试官的问题的话,我们只要明白timerFunc的实现就可以了,但是我先不讲这个,个人认为这个是nextTick的核心,同时是最好理解的一部分,所以放到最后再说。
先来看看nextTick的主体方法,也就是我们直接调用的nextTick方法的逻辑。

1.1 参数——有两个:1. cb; 2. ctx

第一个,不难理解就是我们常用的:传入的回调;
第二个,我个人是从来没有用过的,但是很明显context就是上下文的意思,js里面更愿意说成当前的this的指向,也就是说nextTick还提供了改变当前回调执行的this指向的能力。

1.2 逻辑分析

首先声明了一个*_resolve*,但是没有立即赋值,所以也呆会儿讲;
然后是callBacks队列的push操作,如果有回调函数cb,则立即执行回调函数,并用try-cat捕捉可能出现的异常。如果没有cb,则判断有无_resolve,有就执行_resolve(你可能会有疑问不是_resolve没有赋值吗,这个看到最后,并且解释了timerFunc以后自然就懂了);
然后更改状态为pending,执行已经实现了的timerFunc;
最后如果没有cb,并且当前环境支持Promise时,创建了一个Promise对象,并且将这个对象return,讲resolve赋值给_resolve。

我相信你肯定会有下面三个问题,如果没有,你肯定是懂nextTick实现的,建议您就不要继续了,多多少少有点浪费时间哈哈哈。

Q1、timerFunc究竟干了什么?
Q2、为什么最后才给_resolve赋值?
Q3、这个写文章的是不是脑子不对,废话这么多。

好,现在来解答一下这三个问题。
A3:脑子不是很有问题,废话多,其实是因为自己文笔不是很行,但是想解释清楚,请各位见谅。

二、timerFunc和flushCallbacks

为了解密Q2,我们先来把nextTick方法拆开,只保留和_resolve相关的代码。拆开后,大概长这个样子。

let callbacks = []

function nextTick (cb, ctx) {
  let _resolve
  callbacks.push(() => {
    if(_resolve) {
      console.log("我已经有_resolve了");
      _resolve(ctx)
    }else {
      console.log("没有");
    }
  })
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

这个时候我们用node运行一下我们nextTick方法。

nextTick(undefined).then((res) => {
  console.log("我执行了空的nextTick", res);
})

当然,控制台不会有任何输出,因为,在_resolve调用在_resolve被赋值之前,肯定不会被调用。我们再来看看源代码,注意这段代码

  if (!pending) {
    pending = true
    timerFunc(
  }

必定是timerFunc里发生了什么,让_resolve的调用变成了_resolve赋值之后执行了!

2.1 timerFunc

我们来看看timerFunc的实现z
截屏20210729 下午2.30.39.png
其实很简单对吧,就是说:
以Promise > MutationObserver > setImmediate > setTimeout的顺序,当前环境(浏览器)支持哪一个就用那一个去运行flushCallbacks。
简单介绍一下Promise、MutationObserver、setImmediate、setTimeout吧。

Promise:ES6提供的异步编程解决方案,个人认为最大作用是把回调写法,变成了链式调用的写法可读性更好了。
MutationObserver:MutationObserver接口提供了监视对DOM树所做更改的能力。
setImmediate:该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数。
(ps:兼容性很不好仅在node和新版IE浏览器中有效。)
setTimeout:法用于在指定的毫秒数后调用函数或计算表达式。

好了到这一步就可以回答我开篇说的那个问题了“为什么完全正确”,因为nextTick完全没有办法了,才会使用setTimeout跳出当前事件循环,而其余的方法均属于微任务,只不过执行时出现在微任务队列的比较靠后的位置而已。

2.2 flushCallbacks

image.png
flushCallbacks方法也很简单,就是把pending状态关闭了,然后执行回调,并把回调参数清空。

2.3 timerFunc和flushCallbacks的组合

所有我们在把timerFunc和flushCallbacks结合起来看,timerFunc就是告诉当前环境应该用什么方式去执行我们nextTick的回调滴。再回到第二节一开始的问题,_resolve是什么这么被赋值的?,Q:假设我们的浏览器支持Promise,当我们执行了timerFunc会执行这么一段代码

const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }

Promise.resolve()返回一个新的Promise或者thenable对象,当前没有传参数,固返回一个新的Promise对象,这个对象的then()的回调回被压入微任务队列的尾部,显然在nextTick()中

    return new Promise(resolve => {
      _resolve = resolve
    })

这一步的赋值操作是在微任务队列里相对于

p.then(flushCallbacks)

靠前的

截屏20210729 19.34.24.png
至此 *Q2:为什么最后才给_resolve赋值?*也找到了答案。当然还有其他几种情况,setTimeout就不说了,到下次宏任务执行才会执行_resolve,当然不会出问题。另外MutationObserver是监听dom改变的,vue的响应式是数据改变了才会改变dom,所以也一定是一切数据操作完成以后,才会执行_resolve,具体的还要结合vue的响应式原理和MutationObserverAPI详解,这里我能力有限,就不多说了,大家有兴趣的就自己去了解了解,然后自己写一篇相关文章,并在评论区留下链接,笔者一定会去仔细拜读。谢谢各位,该开饭喽