高級前端開發者必會的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/

後記

若是你喜歡探討技術,或者對本文有任何的意見或建議,你能夠掃描下方二維碼,關注微信公衆號「 全棧者 」,也歡迎加做者微信,與做者隨時互動。歡迎!衷心但願能夠碰見你。

歡迎小夥伴加羣,反饋或者提問。

相關文章
相關標籤/搜索