最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue nextTick彻底理解

    正文概述 掘金(一拳小和尚)   2021-02-18   716

    前言

    含义和使用


    nextTick的官方解释:


    啥意思呢,即我们对Vue中data数据的修改会导致界面对应的响应变化,而通过nextTick方法,可以在传入nextTick的回调函数中获取到变化后的DOM,讲起来可能还是有点梦幻,下面我们直接使用nextTick体验一下效果。

    比如我们有如下代码:

    <template>
       <div>
          <button @click='update'>更新数据</button>
          <span id='content'>{{message}}</span>
       </div>
    </template>
    <script>
      export default{
          data:{
              message:'hello world'
          },
          methods:{
              update(){
                  this.message='Hello World'
                  console.log(document.getElementById('content').textContent);
                  this.$nextTick(()=>{
                      console.log(document.getElementById('content').textContent);
                  })
              }
          }
      }
    </script>
    


    上述代码第一次输出结果为hello world,第二次结果为更新后的Hello World


    即我们在update方法中第一行对message的更新,并不是马上同步到span中,而是在完成span的更新之后回调了我们传入nextTick的函数。

    // 修改数据
    vm.data = 'Hello'
    //---> DOM 还没有更新
    
    Vue.nextTick(function () {
      //---> DOM 更新了
    })
    


    这里我们也可以理解为Vue中数据的更新不会同步触发dom元素的更新,也就是说dom更新是异步执行的,并且在更新之后调用了我们传入nextTick的函数。

    那么问题来了,Vue为什么需要nextTick呢?nextTick又是如何实现的呢

    探索


    这里我们就抱着好奇的心态,理解一下nextTick函数的实现原理,加深对Vue底层原理的理解。

    要想理解nextTick的设计意图和实现原理我们需要两块的前置知识理解:

    1. Vue响应式原理(理解设计意图)
    2. 浏览器事件循环机制(理解原理)


    因此本次行文先简单讲解以上两部分内容,最后将知识整合详细介绍nextTick的实现原理。

    响应式原理


    Vue响应原理的核心是数据劫持和依赖收集,主要是利用Object.defineProperty()实现对数据存取操作的拦截,我们把这个实现称为数据代理;同时我们通过对数据get方法的拦截,可以获取到对数据的依赖,并将出所有的依赖收集到一个集合中。

     Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        //拦截get,当我们访问data.key时会被这个方法拦截到
        get: function reactiveGetter () {
            //我们在这里收集依赖
            return data[key];
        },
        //拦截set,当我们为data.key赋值时会被这个方法拦截到
        set: function reactiveSetter (newVal) {
            //当数据变更时,通知依赖项变更UI
        } 
    })
    


    下面为了更好的理解之后nextTick的实现原理,我们需要先实现一个简化版的Vue。

    Vue类


    首先我们实现一个Vue类,用于创建Vue对象,它的的构造方法接收一个options参数,用于初始化Vue。

    class Vue{
        constructor(options){
           this.$el=options.el;
           this._data=options.data;
           this.$data=this._data;
           //对data进行响应式处理
           new Observe(this._data);
       }
    }
    //创建Vue对象
    new Vue({
        el:'#app',
        data:{
          message:'hello world'
        }
    })
    


    上面的代码中我们首先创建了一个Vue的类,构造函数跟我们平时使用的Vue大致一致,为了容易理解我们这里只处理了参数eldata
    我们发现构造函数的最后一行创建了一个Observe类的对象,并传入data作为参数,这里的Observe就是对data数据进行响应式处理的类,接下来我们看一下Observe类的简单实现。

    Observe类


    我们在Observe类中实现对data的监听,就是通过Object.defineProperty()方法实现的数据劫持,代码如下。

    class Observe{
        constructor(data){
           //如果传入的数据是object
           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'){
                //如果属性是对象,那么那么递归调用walk方法
                this.walk(obj[key]);
            }
            const dep=new Dep();//Dep类用于收集依赖
            const val=obj[key];
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                //get代理将Dep.target即Watcher对象添加到依赖集合中
                get() {
                  //这里在创建Watcher对象时会给Dep.target赋值
                  if (Dep.target) {
                    dep.addSubs(Dep.target);
                  }
                  return val;
                },
                set(newVal) {
                    val=newVal;
                    //依赖的变更响应
                    dep.notify(newVal)
                } 
              })
        }
    }
    


    上述代码中我们使用到了Dep类,我们在劫持到的数据的get方法中收集到的依赖会被放到Dep类中保存。

    Dep类


    下面代码是Dep类的实现,他有一个subs的数组,用于保存依赖,这里的依赖是我们后面要定义的Watcher,Watcher即观察者,

    class Dep{
       static target=null
       constructor(){
           this.subs=[];
       }
       addSubs(watcher){
           this.subs.push(watcher)
       }
       notify(newVal){
           for(let i=0;i<this.subs.length;i++){
               this.subs[i].update(newVal);
           }
       }
    }
    

    Watcher类


    观察者类

    let uid=0
    class Watcher{
        //vm即一个Vue对象,key要观察的属性,cb是观测到数据变化后需要做的操作,通常是指DOM变更
        constructor(vm,key,cb){
           this.vm=vm;
           this.uid=uid++;
           this.cb=cb;
           //调用get触发依赖收集之前,把自身赋值给Dep.taget静态变量
           Dep.target=this;
           //触发对象上代理的get方法,执行get添加依赖
           this.value=vm.$data[key];
           //用完即清空
           Dep.target=null;
        }
        //在调用set触发Dep的notify时要执行的update函数,用于响应数据变化执行run函数即dom变更
        update(newValue){
            //值发生变化才变更
            if(this.value!==newValue){
                this.value=newValue;
                this.run();
            }
        }
        //执行DOM更新等操作
        run(){
            this.cb(this.value);
        }
    }
    


    通过以上的代码我们就实现了一个去除了模板编译的简易版的Vue,我们用简单化模拟dom的变更。

    //======测试=======
    let data={
        message:'hello',
        num:0
    }
    let app=new Vue({
        data:data
    });
    //模拟数据监听
    new Watcher(app,'message',function(value){
        //模拟dom变更
        console.log('message 引起的dom变更--->',value);
    })
    new Watcher(app,'num',function(value){
        //模拟dom变更
        console.log('num 引起的dom变更--->',value);
    })
    data.message='world';
    data.num=100;
    


    以上测试代码输出

    为什么要用nextTick


    我们仔细观察会发现,按照以上的响应式原理实现,当我们对某项数据进行频繁的更新时会有很严重的性能问题。比如我们对上述的num属性进行修改:

    for(let i=0;i<100;i++){
        data.num=i;//每次的data数据的变化都会调用Watcher的update去更新DOM
    }
    


    上面的代码会导致num对应的Watcher的回调频繁执行(100次),其对应的就是100次的DOM更新,我们知道,DOM更新的性能成本是昂贵的,我们开发中应当尽量减少Dom操作。

    优秀Vue作者肯定也是不允许这种情况发生的,vue就是使用nextTick来优化这个问题的。

    简单的说就是每次数据变化之后不是立刻去执行DOM更新,而是要把数据变化的动作缓存起来,在合适的时机只执行一次的dom更新操作。这里就需要要设置一个合适的时间间隔,通过下面要介绍的事件循环机制可以很完美的解决。

    事件循环机制


    简单理解浏览器事件循环机制,即在js代码中执行中包括两种类型的任务,宏任务和微任务。宏任务即我们编写的顺序执行的代码和诸如setTimeout创建的任务,微任务则为通过诸如Promise.then中回调函数中执行的代码。

    事件执行顺序:

    • 宏任务
    • 本次宏任务产生的所有微任务
    • render(视图更新)
    • 下一次宏任务


    如此循环反复,为了方便理解,我们举一个简单的例子。

    console.log('宏任务1')
    setTimeout(()=>{
        console.log('宏任务2')
    })
    Promise.resolve().then(()=>{
        console.log('微任务1')
    })
    Promise.resolve().then(()=>{
        console.log('微任务2')
    })
    


    上面代码的执行结果为:


    这里主要讲nextTick的实现原理,因此只是简单讲一下事件循环的原理,如需想要对事件循环深层的理解可以参考这篇 浏览器与Node的事件循环(Event Loop)有何区别?


    聪明的你肯定发现了,我们的数据变化缓存可以依赖事件循环来完成;因为每次事件循环之间都有一次视图渲染,我们只需要在render之前完成对dom的更新即可,因此我们为了避免无效的DOM操作,需要将数据变更缓存起来,只保存最后一次数据最终的变更结果。

    这里简单给出两种实现方法:setTimeout和Promise,我们常用的setTimeout会创建一个宏任务,而Promise.then创建一个微任务。

    如果使用setTimeout宏任务实现异步更新队列,那么就是本次同步代码执行完成不执行视图更新,而是在下一次宏任务开始清空异步更新队列,处理缓存的DOM更新和开发者添加的nextTick回调。

    使用Promise创建的是微任务,微任务会在本次事件循环同步代码执行结束后执行,使用setTimeout创建的是宏任务,同样会在此次同步代码执行完成后执行,区别是在setTimeout代码执行之前会穿插一次无效的视图渲染,因此我们尽量使用Promise创建微任务实现异步更新。

    重头戏:nextTick


    核心原理及异步更新队列。

    说到Vue中nextTick的实现,必须提到一个新概念异步更新队列,这里有两个关键字异步,更新队列。不知道你还记不记得前面我们写的简易版的Vue是如何响应数据并模拟dom更新的,这里我们在整体捋一遍流程:

    Observe为数据添加代理,当我们使用到数据时,通过get代理方法我们可以收集到依赖该数据的Watcher对象,并且保存到Dep中作为该数据的依赖,这个过程就是依赖收集;

    然后当我们修改数据时,会触发数据的set代理方法,进而执行Dep的notify方法触发所有依赖项的update方法执行更新。

    而问题就出在了更新这一步,这里我们触发更新是同步执行的,即立即执行,像前面的for循环会频繁更新n多次,这造成了性能的浪费,尤其对于dom更新来说,一来是dom更新是昂贵的,二来这其中大多数是用户无法观测到的无效更新(因为浏览器事件循环机制中,一次循环中只有一次界面渲染)。

    因此这里我们就可以借助浏览器事件循环机制实现异步更新,对发生变化的数据,每次事件循环期间只执行一次dom更新操作。

    即在Watcher的update方法中不再直接出发dom更新,而是把变化后的Watcher放入一个更新队列中,在本次事件循环结束时,依次将更新队列中的Watcher出队并执行更新。

    因此我们需要改进Watcher的实现,我们先看原来的Watcher中update方法的实现:

     update(newValue){
            //值发生变化才变更
            if(this.value!==newValue){
                this.value=newValue;
                this.run();
            }
        }
        //执行DOM更新等操作
        run(){
            this.cb(this.value);
        }
    


    这里的update方法中发现数据变更之后是立即执行run方法进行dom更新操作的,我们对它进行修改:

        update(newValue){
            //值发生变化才变更
            if(this.value!==newValue){
                this.value=newValue;
                //在异步更新队列中添加Watcher,用于后续更新
                updateQueue.push(this);
            }
        }
        //执行DOM更新等操作
        run(){
            this.cb(this.value);
        }
    


    上面的代码我们把变更了的Watcher添加到更新队列updateQueque中,用于后续的更新,下面我们编写一个清空更新队列并依次执行更新的函数。

    function flushUpdateQueue(){
        while(updateQueue.length>0){
            updateQueue.shift().run();
        }
    }
    


    现在我们有了一个处理更新队列的函数,但是现在还缺少一个很重要的元素,就是执行此函数的时机,这时我们回忆一下我们的更新队列是异步更新队列,这里的异步即使用setTimeout或者Promise实现异步更新,这个实现过程就是nextTick的代码实现了,下面是简化版nextTick函数实现:

    let callbacks=[];//事件队列,包含异步dom更新队列和用户添加的异步事件
    let pending=false;//控制变量,每次宏任务期间执行一次flushCallbacks清空callbacks
    funciton nextTick(cb){
       callbacks.push(cb);
       if(!pending){
          pending=true;
          //这里也可以使用Promise,Promise创建的是微任务,微任务会在本次事件循环同步代码执行结束后执行,使用setTimeout创建的是宏任务,同样会在此次同步代码执行完成后执行,区别是在setTimeout代码执行之前会穿插一次无效的视图渲染,因此我们尽量使用Promise创建微任务实现异步更新。
          if(Promise){
              Promise.resovle().then(()=>{
                  flushCallbacks();
              })
          }
          setTimeout(()=>{
              flushCallbacks();
          })
       }
    }
    function flushCallbacks(){
        pending=false;//状态重置
        callbacks.forEach(cb=>{
            callbacks.shift()();
        })
    }
    


    主要做了两件事,创建callbacks数组作为保存事件的队列,我们每次调用nextTick函数就往callbacks事件队列中入队一个事件,然后我们在setTimeout或者Promise.then创建的异步事件中,通过flushCallbacks将异步队列中的函数一次出队并执行。

    这里使用pending变量控制本次同步(宏)任务期间不重复创建异步任务(setTimeout或者Promise.then)。

    把上述代码添加到Vue类上:

    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;
                 if(Promise){
                      Promise.resovle().then(()=>{
                          this.flushCallbacks();
                      })
                  }
                  setTimeout(()=>{
                      this.flushCallbacks();
                  })
             }
        }
          //清空callbacks
        flushCallbacks(){
           while(callbacks.length!=0){
             callbacks.shift()();
          }
          pending=false;
        }
        //清空UpdateQueue队列,更新视图
        flushUpdateQueue(){
            while(updateQueue.length!=0){
               updateQueue.shift().run();
            }
            has={};
            this.waiting=false;
        }
     }
    


    对Watcher进行进一步优化如下:

    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);
         }
     }
    


    之前Watcher中的update方法是立即执行的,
    观察上面的代码我们发现,update方法不再立即执行更新,得是把变更通过nextTick缓存到updateQueue队列中,这个队列保存了本次事件循环期间发生了变更的Watcher。

    完整源码

    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的变更操作
    

    总结


    以上就是对Vue中nextTick实现原理的介绍,作为前置知识,也简单介绍了Vue响应式的实现原理以及js事件循环机制。如有收获,多多点,如有不足,还望不吝指出。

    参考文献:Vue运行机制


    起源地下载网 » Vue nextTick彻底理解

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元