vue为什么采用异步渲染

214 阅读5分钟

1、为什么需要异步渲染?

  • 从用户体验角度,如果每次修改值后都要进行渲染,页面会有闪烁效果,会造成不好的用户体验

  • 从性能角度,每次修改后进行渲染,同一个值多次修改,最后一次修改前还需要一次无用的渲染,增加了性能的消耗

  • 对于浏览器来说,每次的渲染都会引起的重绘或者回流,进行性能消耗

异步渲染和熟悉的节流函数最终目的是一致的,将多次数据变化所引起的响应变化收集后合并成一次页面渲染,从而更合理的利用机器资源,提升性能与用户体验

2、vue中如何实现异步渲染?数据为什么频繁变化但只会更新一次?

在vue中主线程执行过程就是一个tick,在一个tick中只会更新一次。vue每次更新不会每次都重新渲染,会将变化放入到异步更新队列中,在下一个tick批量处理这些,所以在一个事件处理程序中多次修改数据,只会更新一次组件。

  • 检测到数据变化
  • 开启一个队列
  • 在同一事件循环中缓冲所有数据
  • 如果同一个watcher被多次触发,只会推入到队列中一次

3、在源码层面梳理的Vue的异步渲染过程?

关于vue2渲染过程:juejin.cn/post/710428…

1、当我们修改赋值的时候,val属性所绑定的Object.defineProperty的setter函数触发,setter函数将所订阅的notify函数触发执行。

defineReactive() { 
 set: function reactiveSetter (newVal) { 
  dep.notify(); 
 } 
}

2、notify函数中,将所有的订阅组件watcher中的update方法执行一遍。

Dep.prototype.notify = function notify () { 
    // 拷贝所有组件的watcher
    var subs = this.subs.slice(); 
    ... 
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};

3、update函数得到执行后,默认情况下lazy是false,sync也是false,直接进入把所有响应变化存储进全局数组queueWatcher函数下。

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

4、queueWatcher函数里,会先将组件的watcher存进全局数组变量queue里。

  默认情况下config.async是true,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。

function queueWatcher (watcher) { 
  ...
  // 在全局队列里存储将要响应的变化update函数
  queue.push(watcher); 
  ...
  // 当async配置是false的时候,页面更新是同步的 
  if (!config.async) { 
    flushSchedulerQueue();
    return
  } 
  // 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
  nextTick(flushSchedulerQueue);
}

5、nextTick函数的执行后,传入的flushSchedulerQueue函数又一次push进callbacks全局数组里,pending在初始情况下是false,这时候将触发timerFunc。

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

6、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)) {
  var p = Promise.resolve();
  timerFunc = function () { 
    p.then(flushCallbacks);  
    if (isIOS) { setTimeout(noop); }
  }; 
  isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||                              
  // PhantomJS and iOS 7.x                                 
  MutationObserver.toString() === '[object MutationObserverConstructor]')) { 
  var counter = 1; 
  var observer = new MutationObserver(flushCallbacks);      
  var textNode = document.createTextNode(String(counter)); 
  observer.observe(textNode, {characterData: true}); 
  timerFunc = function () {  
    counter = (counter + 1) % 2;  
    textNode.data = String(counter); 
  }; 
  isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { 
  timerFunc = function () {  
    setImmediate(flushCallbacks); 
  };
} else {
  timerFunc = function () {  
    setTimeout(flushCallbacks, 0);
  };
}

7、flushCallbacks函数中将遍历执行nextTick里push的callback全局数组,全局callback数组中实际是第5步的push的flushSchedulerQueue的执行函数。

// 将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]();
  }
}

8、callback遍历执行的flushSchedulerQueue函数中,flushSchedulerQueue里先按照id进行了优先级排序,接下来将第4步中的存储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进去 
  for (index = 0; index < queue.length; index++) { 
    ...
    watcher.run(); 
    // 渲染  ... 
  }
}

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

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

10、get函数中,将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。

Watcher.prototype.get = function get () { 
  pushTarget(this); 
  // 将实例push到全局数组targetStack 
  var vm = this.vm; 
  value = this.getter.call(vm, vm); 
  ...
}

11、实例的getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update。

function () {
  vm._update(vm._render(), hydrating);
};

12、实例的_update函数执行后,将会把两次的虚拟节点传入vm的 patch 方法执行渲染操作。

Vue.prototype._update = function (vnode, hydrating) { 
  var vm = this; 
  ...
  var prevVnode = vm._vnode;
  vm._vnode = vnode;
  if (!prevVnode) { 
    // initial render  
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); 
  } else {  
    // updates  
    vm.$el = vm.__patch__(prevVnode, vnode); 
  } 
  ...
};

4、nextTick实现原理

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

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

  • nextTick即有可能是微任务,也有可能是宏任务,从优先Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。

5、vue能不能同步渲染

1. Vue.config.async = false

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

function queueWatcher (watcher) { 
  ...
  // 在全局队列里存储将要响应的变化update函数
  queue.push(watcher); 
  ...
  // 当async配置是false的时候,页面更新是同步的 
  if (!config.async) { 
    flushSchedulerQueue();
    return
  } 
  // 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
  nextTick(flushSchedulerQueue);
}

在我们的开发代码里,只需要加入下一句即可让你的页面渲染同步进行。

import Vue from 'Vue'
Vue.config.async = false

2. this._watcher.sync = true

在Watch的update方法执行源码里,可以看到当this.sync为true时,这时候的渲染也是同步的。

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else { 
    queueWatcher(this);
  }
};
  • 在开发代码中,需要将本次watcher的sync属性修改为true,对于watcher的sync属性变化只需要在需要同步渲染的数据变化操作前执行this._watcher.sync=true,这时候则会同步执行页面渲染动作。

  像下面的写法中,页面会渲染出val为1,而不会渲染出2,最终渲染的结果是3,但是官网未推荐该用法,请慎用。

new Vue({ 
  el: '#app',
  sync: true, 
  template: '<div>{{val}}</div>', 
  data () {  return { val: 0 } }, 
  mounted () { 
    this._watcher.sync = true 
    this.val = 1
    debugger  
    this._watcher.sync = false 
    this.val = 2
    this.val = 3
  }
})

我们了解了Vue中为什么采用异步渲染页面的原因,并且从源码的角度剖析了整个渲染前的操作链路,同时剖析出Vue中的异步方法nextTick的实现与原生的异步API直接的联系。最后也从源码角度下了解到,Vue并非不能同步渲染,当我们的页面中需要同步渲染时,做适当的配置即可满足。