浅析Vue.nextTick()原理

267 阅读4分钟

原理

  1. 为什么需要 nextTickVue 是异步修改 DOM 的并且不鼓励开发者直接接触 DOM,但有时候业务需要必须对数据更改--刷新后的 DOM 做相应的处理, 这时候就可以使用 Vue.nextTick(callback)这个 api 了。
  2. 理解原理前的准备 首先需要知道事件循环中宏任务和微任务这两个 概念,常见的宏任务有 script, setTimeout, setInterval, setImmediate, I/O, UI rendering 常见的微任务有 process.nextTick(Nodejs),Promise.then(), MutationObserver;
  3. 理解 nextTick 的原理正是 Vue 通过异步队列控制 DOM 更新和 nextTick 回调函数先后执行的方式。如果大家看过这部分的源码,会发现其中 做了很多 isNative()的判断,因为这里还存在兼容性优雅降级的问题。可见 Vue 开发团队的深思熟虑,对性能的良苦用心。

使用逻辑

在 Vue.js 里是数据驱动视图变化,由于 JS 执行是单线程的,在一个 tick 的过程中,它可能会多次修改数据,但 Vue.js 并不会傻到每修改一次数据就去驱动一次视图变化,它会把这些数据的修改全部 push 到一个队列里,然后内部调用 一次 nextTick 去更新视图,所以数据到 DOM 视图的变化是需要在下一个 tick 才能完成。

大致分为以下几个步骤:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度被调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task

  • 在浏览器环境中,常见的 macro task 有 setTimeoutMessageChannelpostMessagesetImmediate
  • 常见的 micro task 有 MutationObsever 和 Promise.then

使用场景

语法Vue.nextTick([callback, context])

参数

  • {Function} [callback]:回调函数,不传时提供promise调用
  • {Object} [context]:回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上。
//改变数据
vm.message = 'changed'

//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'

//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
    // DOM 更新了
    //可以得到'changed'
    console.log(vm.$el.textContent)
})

// 作为一个 Promise 使用 即不传回调
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例。

源码

class Dep{
    static target=null
    constructor(){
        this.subs=[];
    }
    addSubs(watcher){
        this.subs.push(watcher)
    }
    notify(){
        for(let i=0;i<this.subs.length;i++){
            this.subs[i].update();
        }
    }
 }
 class Observer{
     constructor(data){
        if(typeof data=='object'){
            this.walk(data);
        }
     }
     walk(obj){
         const keys=Object.keys(obj);
         for (let i = 0; i < keys.length; i++) {
             this.defineReactive(obj, keys[i])
         }
     }
     defineReactive(obj,key){
         if(typeof obj[key]=='object'){
             this.walk(obj[key]);
         }
         const dep=new Dep();
         let val=obj[key];
         Object.defineProperty(obj, key, {
             enumerable: true,
             configurable: true,
             //get代理将Dep.target即Watcher对象添加到依赖集合中
             get: function reactiveGetter () {
               if (Dep.target) {
                 dep.addSubs(Dep.target);
               }
               return val;
             },
             set: function reactiveSetter (newVal) {
                  val=newVal;
                  dep.notify()
             } 
           })
     }
 }
 let uid=0
 class Watcher{
     constructor(vm,key,cb){
        this.vm=vm;
        this.key=key;
        this.uid=uid++;
        this.cb=cb;
        //调用get,添加依赖
        Dep.target=this;
        this.value=vm.$data[key];
        Dep.target=null;
     }
     update(){
         if(this.value!==this.vm.$data[this.key]){
             this.value=this.vm.$data[this.key];
             if(!this.vm.waiting){//控制变量,控制每次事件循环期间只添加一次flushUpdateQueue到callbacks
                this.vm.$nextTick(this.vm.flushUpdateQueue); 
                this.vm.waiting=true;
             }
             //不是立即执行run方法,而是放入updateQueue队列中
             if(!has[this.uid]){
                 has[this.uid]=true;
                 updateQueue.push(this);
             }
         }
     }
     run(){
         this.cb(this.value);
     }
 }
  const updateQueue=[];//异步更新队列
  let has={};//控制变更队列中不保存重复的Watcher
  const callbacks=[];
  let pending=false;
 class Vue{
    constructor(options){
        this.waiting=false
        this.$el=options.el;
        this._data=options.data;
        this.$data=this._data;
        this.$nextTick=this.nextTick;
        new Observer(this._data);
    }
    //简易版nextTick
    nextTick(cb){
         callbacks.push(cb);
         if(!pending){//控制变量,控制每次事件循环期间只执行一次flushCallbacks
             pending=true;
             setTimeout(()=>{
                 //会在同步代码(上一次宏任务)执行完成后执行
                 this.flushCallbacks();
             })
         }
     }
    //清空UpdateQueue队列,更新视图
    flushUpdateQueue(){
        while(updateQueue.length!=0){
           updateQueue.shift().run();
        }
        has={};
        this.waiting=false;
    }
    //清空callbacks
    flushCallbacks(){
       while(callbacks.length!=0){
         callbacks.shift()();
      }
      pending=false;
    }
 }

//======测试=======
let data={
    message:'hello',
    num:0
}
let app=new Vue({
    data:data
});
//模拟数据监听
let w1=new Watcher(app,'message',function(value){
    //模拟dom变更
    console.log('message 引起的dom变更--->',value);
})
//模拟数据监听
let w2=new Watcher(app,'num',function(value){
    //模拟dom变更
    console.log('num 引起的dom变更--->',value);
})
data.message='world'//数据一旦更新,会为nextTick的事件队列callbacks中加入一个flushUpdateQueue回调函数
data.message='world1'
data.message='world2'//message的变更push到updateQueue中,只保存最后一次赋值的结果
for(let i=0;i<=100;i++){
   data.num=i;//num的变更push到updateQueue中,只保存最后一次赋值的结果
}
//开发者为callbacks添加的异步回调事件
app.$nextTick(function(){
   console.log('这是dom更新完成后的操作')
})
//例子中的执行顺序是,先执行同步代码,其中第一次修改数据data.message='world'会把dom更新回调函数push到callbacks队列,并把dom更新操作的cb回调放入updateQueue,后续对message的变更操作