高级前端开发者必会的34道Vue面试题解析(三)


前言

经过前面的文章,咱们认识了页面的响应是由Vue实例里的data函数所返回的数据变化而驱动,也重点学习了页面的响应与数据变化之间是是如何来联系起来的,而且分别在Vue2.x与3.x中,从零到一实现了两个版本下的数据变化驱动页面响应原理。javascript

接下来在本文里一块儿看看当数据变化时,从源码层面逐步分析一下触发页面的响应动做以后,如何作渲染到页面上,展现到用户层面的。vue

同时也会了解在Vue中的异步方法NextTick的源码实现,看一看NextTick方法与浏览器的异步API有何联系。java

注意,本文涉及的Vue源码版本为2.6.11。node

什么是异步渲染?

这个问题应该先要作一个前提补充,当数据在同步变化的时候,页面订阅的响应操做为何不会与数据变化彻底对应,而是在全部的数据变化操做作完以后,页面才会获得响应,完成页面渲染。react

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

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里有两次渲染。github

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

并且页面是在执行全部的同步代码执行完后才能获得渲染,在上述例子里的while阻塞代码以后,页面才会获得渲染,就像在熟悉的setTimeout里的回调函数的执行同样,这就是的异步渲染。浏览器

熟悉React的同窗,应该很快能想到屡次执行setState函数时,页面render的渲染触发,实际上与上面所说的Vue的异步渲染有殊途同归之妙。微信

Vue为何要异步渲染?

咱们能够从用户和性能两个角度来探讨这个问题。

从用户体验角度,从上面例子里便也能够看出,实际上咱们的页面只须要展现第二次的值变化,第一次只是一个中间值,若是渲染后给用户展现,页面会有闪烁效果,反而会形成很差的用户体验。

从性能角度,例子里最终的须要展现的数据其实就是第二次给val赋的值,若是第一次赋值也须要页面渲染则意味着在第二次最终的结果渲染以前页面还须要渲染一次无用的渲染,无疑增长了性能的消耗。

对于浏览器来讲,在数据变化下,不管是引发的重绘渲染仍是重排渲染,都有可能会在性能消耗之下形成低效的页面性能,甚至形成加载卡顿问题。

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

Vue中如何实现异步渲染?

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

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

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

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

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

接下来从源码角度一步一分析一下。

一、当咱们使用this.val='343'赋值的时候,val属性所绑定的Object.defineProperty的setter函数触发,setter函数将所订阅的notify函数触发执行。

defineReactive() {
  ...
  set: function reactiveSetter (newVal) {
    ...
    dep.notify();
    ...
  }
  ...
}复制代码

二、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();
  }
};复制代码

三、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);
  }
};复制代码

四、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);
}复制代码

五、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;
    })
  }
}复制代码

六、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);
  };
}复制代码

七、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]();
  }
}复制代码

八、callback遍历执行的flushSchedulerQueue函数中,flushSchedulerQueue里先按照id进行了优先级排序,接下来将第4步中的存储watcher对象全局queue遍历执行,触发渲染函数watcher.run。

function flushSchedulerQueue () {
var watcher, id;
// 安装id从小到大开始排序,越小的越前触发的update
queue.sort(function (a, b) { return a.id - b.id; });
// queue是全局数组,它在queueWatcher函数里,每次update触发的时候将当时的watcher,push进去
  for (index = 0; index < queue.length; index++) {
    ...
    watcher.run(); // 渲染
    ...
  }
}复制代码

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

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

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

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

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

function () {
  vm._update(vm._render(), hydrating);
};复制代码

十二、实例的_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);
  }
  ...
};复制代码

nextTick的实现原理

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

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

从上面的取用规则也能够看出来,nextTick即有多是微任务,也有多是宏任务,从优先去Promise和MutationObserver能够看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。

对于微任务与宏任务的区别这里不深刻,只要记得同步代码执行完毕以后,优先执行微任务,其次才会执行宏任务。

Vue能不能同步渲染?

一、 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复制代码

二、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并不是不能同步渲染,当咱们的页面中须要同步渲染时,作适当的配置便可知足。

References

[1] https://github.com/vuejs/vue

[2] https://cn.vuejs.org/

后记

若是你喜欢探讨技术,或者对本文有任何的意见或建议,你能够扫描下方二维码,关注微信公众号“ 全栈者 ”,也欢迎加做者微信,与做者随时互动。欢迎!衷心但愿能够碰见你。

欢迎小伙伴加群,反馈或者提问。

相关文章
相关标签/搜索