Vue的异步渲染和 nextTick

1. 什么是异步渲染?

环境补充:当数据在同步变化的时候,页面订阅的响应操作为什么不会与数据变化完全对应,而是在所有的数据变化操作做完之后,页面才会得到响应,完成页面渲染。

从一个例子体验一下异步渲染机制:

import Vue from 'Vue'
new Vue({
 el: '#app',
 template: '<div>{{val}}</div>', 
 data () {  return {   val: 'init'  } },
 mounted () { 
  this.val = '我是第一次页面渲染'  // debugger  
  this.val = '我是第二次页面渲染' 
  const st = Date.now()  
  while(Date.now() - st < 3000) {}
 }
})

上面这一段代码中,在 mounted 里给 val 属性进行了两次赋值,如果页面渲染与数据的变化完全同步的话,页面应该是在 mounted 里有两次渲染。

而由于 Vue 内部的渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的 val 只做一次页面渲染。

而且页面是在执行所有的同步代码执行完后才能得到渲染,在上述例子里的 while 阻塞代码之后,页面才会得到渲染,就像在熟悉的 setTimeout 里的回调函数的执行一样,这就是的异步渲染。

如果熟悉 React ,应该很快能想到多次执行 setState 函数时,页面 render 的渲染触发,实际上与上面所说的 Vue 的异步渲染有异曲同工之妙。

2. Vue 中如何实现异步渲染?

先总结一下原理,在 Vue 中异步渲染实际在数据每次变化时,将其所要引起页面变化的部分都放到一个异步 API 的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,最终执行一次渲染操作。

拿上面例子来说,当 val 第一次赋值时,页面会渲染出对应的文字,但是实际这个渲染变化会暂存,val 第二次赋值时,再次暂存将要引起的变化,这些变化操作会被丢到异步API,Promise.then的回调函数中,等到所有同步代码执行完后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将所有数组里数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作操作。

异步队列执行后,存储页面变化的全局数组得到遍历执行,执行的时候会进行一些筛查操作,将重复操作过的数据进行处理,实际就是先赋值的丢弃不渲染,最终按照优先级最终组合成一套数据渲染。

这里触发渲染的异步 API 优先考虑 Promise ,其次MutationObserver,如果没有MutationObserver的话,会考虑setImmediate,没有setImmediate的话最后考虑是setTimeout

接下来在源码层面梳理一下的 Vue 的异步渲染过程。

2.1 流程图

Vue的异步渲染和 nextTick

2.2 源码分析

  1. 属性发生变化,会触发属性setter函数,会调用dep.notify,对dep里面收集的所有watcher进行调用其update的操作。

    // Observer.defineReactive
    function defineReactive () {
      const dep = new Dep()
      Object.defineProperty(obj, key, {
        get: function reactiveGetter () {
           //...省略...
          return value
        },
        set: function reactiveSetter (newVal) {
           //...省略...
          dep.notify();
        }
      })
    }
    
    // Dep.notify
    Dep.prototype.notify = function notify () {
        // ...省略...
    	// 拷贝所有组件的watcher var subs = this.subs.slice(); 
    	for (var i = 0, l = subs.length; i < l; i++) {
    		subs[i].update(); 
        }
    };
    
  2. update函数得到执行后,默认情况下lazyfalsesync也是false,直接进入把所有响应变化存储进全局数组queueWatcher函数下。

    // Watcher.update
    Watcher.prototype.update = function update () {
     if (this.lazy) {
       this.dirty = true;
     } else if (this.sync) {
       this.run();
     }
     else { 
       queueWatcher(this);
     }
    };
    
  3. queueWatcher函数里,首先获取了watcherid,然后判断是否存在,如果不存在,那么就标记成true,而后继续了一个 if 判断,这个flushing就是队列是否正在更新的标志,当队列正在更新的时候,flushingtrue。所以,如果这时候队列没有更新,会先将组件的watcher存进全局数组变量queue里。默认情况下config.asynctrue,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。

    function queueWatcher (watcher) { 
    	// 在全局队列里存储将要响应的变化update函数 queue.push(watcher); 
        const id = watcher.id;
        if (has[id] == null) {
        	has[id] = true;
        if (!flushing) {
          queue.push(watcher)
        } else {
          // ...省略...
        }
        if (!waiting) {
          waiting = true; //无论调用多少次queueWatcher方法,该if内代码只执行一次
    	// 当async配置是false的时候,页面更新是同步的 
     	if (!config.async) { 
      		flushSchedulerQueue();
        	return;
          } 
    	// 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
     	nextTick(flushSchedulerQueue);
       }
    }
    
  4. nextTick函数的执行后,传入的flushSchedulerQueue函数又一次pushcallbacks全局数组里,pending在初始情况下是false,这时候将触发timerFunc

    // nextTick
    function nextTick (cb, ctx) { 
     var _resolve; 
     callbacks.push(function () { 
      if (cb) { 
        try { 
          cb.call(ctx); 
        } catch (e) { 
          handleError(e, ctx, 'nextTick'); 
        } 
      } else if (_resolve) { 
        _resolve(ctx);
     }
     }); 
     if (!pending) { 
     	 pending = true;
         timerFunc();	//触发
     } 
     // $flow-disable-line 
     if (!cb && typeof Promise !== 'undefined') { 
     	return new Promise(function (resolve) {
            _resolve = resolve;
        });
     }
    }
    
  5. timerFunc函数是由浏览器的Promise、MutationObserver、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks函数。

    var timerFunc;// 这里Vue内部对于异步API的选用,由Promise、MutationObserver、setImmediate、setTimeout里取一个
    //取用的规则是 Promise存在取由Promise,不存在取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在setTimeout。
    if (typeof Promise !== "undefined" && isNative(Promise)) {
      const p = Promise.resolve();
      timerFunc = () => {
        p.then(flushCallbacks);
        if (isIOS) setTimeout(noop);
      };
      isUsingMicroTask = true;
    } else if (
      !isIE &&
      typeof MutationObserver !== "undefined" &&
      (isNative(MutationObserver) ||
        MutationObserver.toString() === "[object MutationObserverConstructor]")
    ) {
      let counter = 1;
      const observer = new MutationObserver(flushCallbacks);
      const textNode = document.createTextNode(String(counter));
      observer.observe(textNode, {
        characterData: true,
      });
      timerFunc = () => {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
      };
      isUsingMicroTask = true;
    } else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
      timerFunc = () => {
        setImmediate(flushCallbacks);
      };
    } else {
      timerFunc = () => {
        setTimeout(flushCallbacks, 0);
      };
    }
    
  6. flushCallbacks函数中将遍历执行nextTickpushcallback全局数组,全局callback数组中实际是第4步的pushflushSchedulerQueue的执行函数。

    // 将nextTick里push进去的flushSchedulerQueue函数进行for循环依次调用
    function flushCallbacks () { 
      pending = false; 
      var copies = callbacks.slice(0); 
      callbacks.length = 0; 
      for (var i = 0; i < copies.length; i++) {
         copies[i]();
      }
    }
    
  7. callback遍历执行的flushSchedulerQueue函数中,flushSchedulerQueue里先按照id进行了优先级排序,接下来将第 3 步中的存储watcher对象全局queue遍历执行,触发渲染函数watcher.run

    function flushSchedulerQueue () {
    	var watcher, id;// 按照id从小到大开始排序,越小的越前触发的
        //updatequeue.sort(function (a, b) { 
    	//    return a.id - b.id;
        //});// queue是全局数组,它在queueWatcher函数里,每次update触发的时候将当时的watcher,push进去
        // ...省略...
        flushing = true; //	表明正在更新中
        for (index = 0; index < queue.length; index++) {
            watcher = queue[index]
            if (watcher.before) {
              //beforeUpdate
              watcher.before()
            }
            id = watcher.id
            has[id] = null
            // 响应式
            watcher.run()
            //...省略
    }
    

    watcher.run的实现在构造函数Watcher原型链上,初始状态下active属性为true,直接执行Watcher原型链的set方法。

    Watcher.prototype.run = function run () {
    	if (this.active) {
            var value = this.get();
    	}
    };
    

3. nextTick的实现原理

首先nextTick并不是浏览器本身提供的一个异步API,而是Vue中,用过由浏览器本身提供的原生异步API封装而成的一个异步封装方法,上面第 4 第 5 段是它的实现源码。

它对于浏览器异步API的选用规则如下,Promise存在取由Promise.then,不存在Promise则取MutationObserverMutationObserver不存在setImmediatesetImmediate不存在最后取setTimeout来实现。

从上面的取用规则也可以看出来,nextTick既有可能是微任务,也有可能是宏任务,从优先去PromiseMutationObserver可以看出nextTick优先微任务,其次是setImmediatesetTimeout宏任务。

记得同步代码执行完毕之后,优先执行微任务,其次才会执行宏任务。

4. Vue可以同步渲染嘛?

  1. Vue.config.async = false

    在 2.2 的第3段源码里,当config里的async的值为为false的情况下,并没有将flushSchedulerQueue加到nextTick里,而是直接执行了flushSchedulerQueue,就相当于把本次data里的值变化时,页面做了同步渲染。

    if (!config.async) { 
        flushSchedulerQueue();
        return;
      } 
    // 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
    nextTick(flushSchedulerQueue);
    
    import Vue from 'Vue'
    Vue.config.async = false //加入下一句即可让你的页面渲染同步进行
    
  2. this._watcher.sync = true

    Watchupdate方法执行源码里(2.2 部分第 2 段源码),可以看到当this.synctrue时,这时候的渲染也是同步的。

    if (this.lazy) {
       this.dirty = true;
     } else if (this.sync) {
       this.run();
     }
     else { 
       queueWatcher(this);
     }
    

参考文档:

深入解读VUE中的异步渲染的实现

Vue.$nextTick学习

上一篇:vue 设置滚动条的位置


下一篇:this.$nextTick()