一分钟带你读懂vue中nextTick源码及原理

144 阅读4分钟

概念

$nextTick 是 vue 中的异步更新,在官网是这样解释的:Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0)代替。

用法

nextTick 接收一个回调函数作为参数, 它的作用是将回调延迟到下次dom更新周期之后, 如果没有回调且在支持Promise的环境中,则返回一个Promise

  methods: { 
    handle () { 
        // 修改数据
        this.msg = 'change'
        this.$nextTick(() => { // dom更新了, 做点什么... 
        })
      }
    }

应用场景

开发过程中,开发者需要在更新完数据之后,需要对新DOM做一些操作,其实我们当时无法对新DOM进行操作,因为这时候还没有重新渲染。

什么时候dom更新完成?

原理:利用异步队列

在每个 macro-task 运行完以后,UI 都会重渲染,那么在 miscro-task (异步事件回调) 中就完成数据更新,当前 次事件循环 结束就可以得到最新的 UI 了。反之如果新建一个 macro-task 来做数据更新,那么渲染就会进行两次。

vue的降级策略(兼容)

  (micro-task) promise->MutationObserver->(macro-task) setTimeout

想要创建一个新的job,优先使用Promise,如果浏览器不支持,再尝试MutationObserver。实在不行,只能用 setTimeout 创建 task 了。

MutationObserver原理?

MutationObserver 是 h5 新加的一个功能,其功能是监听dom节点的变动,在所有 dom 变动完成后,执行回调函数。

具体有一下几点变动的监听:

childList:子元素的变动

attributes:属性的变动

characterData:节点内容或节点文本的变动

subtree:所有下属节点(包括子节点和子节点的子节点)的变动

image.png


//vue@2.2.5 /src/core/util/env.js
if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {
  var counter = 1
  var observer = new MutationObserver(nextTickHandler)
  var textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
      characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
}

如果检测到浏览器支持MutationObserver,则创建一个文本节点,监听这个文本节点的改动事件,把回调放进micro-task 中, 等DOM更新完毕后,执行此回调(nextTickHandler)

问:为什么自己创建的文本节点更新完毕,就能代表其他DOM节点更新完毕呢?

 答:js事件循环机制

vue2.5的降级策略

上面我们讲到了,队列控制的最佳选择是microtask,而microtask的最佳选择是Promise.但microtask: Promise , MutatioObserver

如果当前环境不支持Promise,MutatioObserver,vue就不得不降级为macrotask来做队列控制了。

macrotask:setImmediate、MessageChannel、setTimeout.

setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。

MessageChannel的onmessage回调也是microtask,但也是个新API,面临兼容性的尴尬...

所以最后的兜底方案就是setTimeout了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法了。

vue.nextTcik源码分析

var nextTick=(function () {
    //存储需要触发的回调函数
    var callbacks=[];
    //是否正在等待的标志(false:允许触发在下次事件循环触发callbacks中的回调,
    // true: 已经触发过,需要等到下次事件循环)
    var pending=false;
    //设置在下次事件循环触发callbacks的触发函数
    var timerFunc;
    //处理callbacks的函数
    function nextTickHandler() {
        // 可以触发timeFunc
        pending=false;
        //复制callback
        var copies=callbacks.slice(0);
        //清除callback
        callbacks.length=0;
        for(var i=0;i<copies.length;i++){
            //触发callback的回调函数
            copies[i]();
        }
    }
    //如果支持promise,使用promise实现
    if(typeof Promise !=='undefined' && isNative(promise)){
        var p=Promise.resolve();
        var logError=function (err) {
            console.error(err);
        };
        timerFunc=function () {
            p.then(nextTickHandler).catch(logError);
            //iOS的webview下,需要强制刷新队列,执行上面的回调函数
            if(isIOS) {setTimeout(noop);}
        };
    //    如果Promise不支持,但支持MutationObserver
    //    H5新特性,异步,当dom变动是触发,注意是所有的dom都改变结束后触发
    } else if (typeof MutationObserver !=='undefined' && (
        isNative(MutationObserver) ||
        MutationObserver.toString()==='[object MutationObserverConstructor]')){
            var counter = 1;
            var observer=new MutationObserver(nextTickHandler);
            var textNode=document.createTextNode(String(counter));
            observer.observe(textNode,{
                characterData:true
            });
            timerFunc=function () {
                counter=(counter+1)%2;
                textNode.data=String(counter);
            };
    } else {
        //上面两种都不支持,用setTimeout
        timerFunc=function () {
            setTimeout(nextTickHandler,0);
        };
    }
    //nextTick接收的函数,参数1:回调函数 参数2:回调函数的执行上下文
    return function queueNextTick(cb,ctx) {
        //用于接收触发Promise.then中回调的函数
        //向回调函数中pushcallback
        var _resolve;
        callbacks.push(function () {
            //如果有回调函数,执行回调函数
            if(cb) {cb.call(ctx);}
            //触发Promise的then回调
            if(_resolve) {_resolve(ctx);}
        });
        //是否执行刷新callback队列
        if(!pending){
            pending=true;
            timerFunc();
        }
        //如果没有传递回调函数,并且当前浏览器支持promise,使用promise实现
        if(!cb && typeof  Promise !=='undefined'){
            return new Promise(function (resolve) {
                _resolve=resolve;
            })
        }
    }
})

原理总结

以上就是vue的nextTick方法的实现原理了,总结一下就是:

  1. vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
  2. micro-task因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 因为兼容性问题,vue不得不做了microtask向macrotask的降级方案

image.png