你不知道的Vue.nextTick源碼系列

前言

衆所周知,隨着 Vue 技術的愈來愈熱,大量的前端開發者開始探究這門神奇的框架,筆者也是從 JQuery 時代一腳邁進了 Vue 的世界。談到Vue,在這呢,就不得不提一下筆者在研究一個Vue項目的時候碰到的問題,父組件修改標誌位變量,而子組件的相應組件並無顯示,後來經過多方研究,發現了 Vue.nextTick這個原型方法能夠達到我想要的這個效果,因此筆者今天也來談談這個神奇的方法。前端

Vue.nextTick

  • 參數node

    • {Function} [callback]
    • {Object} [context]
  • 用法es6

    在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。web

// 修改數據
vm.msg = 'Hello'
// DOM 尚未更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 做爲一個 Promise 使用 (2.1.0 起新增,詳見接下來的提示)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })
複製代碼

這裏其實涉及到 js 的事件循環機制,有興趣的話能夠右轉 js事件循環數組

具體使用場景各位小夥伴應該也不用筆者多囉嗦了,今天筆者的重點仍是研究一下這個東西源碼是怎麼實現的,畢竟做爲當代前端一員至少不能只會用 API 了,我們仍是去底層僞裝研究一下是吧。promise

Vue.nextTick源碼解析

js事件循環機制

其實話提及來,咱們就得來了解一下這個 js 是單線程的這個特性上來了,它其實全部事件的處理都依賴於這一個事件循環機制,,主線程的執行過程就是一個 tick,而全部的異步結果都是經過 「任務隊列」 來調度被調度,消息隊列中存放的是一個個的任務task。 規範中規定 task 分爲兩大類,分別是macro task(宏任務) 和micro task(微任務),而且每一個 macro task 結束後,都要清空全部的micro task瀏覽器

回到正題,Vue.nextTick 怎麼實現當前頁面更新完以後最先執行它所綁定的回調呢,這就用到了咱們上面所說的這個任務隊列,每次當前宏任務執行完畢以前,都會清空全部微任務,那麼爲了在界面更新完以後最短期內執行回調,最佳選擇不就是這個微任務了麼,利用這個機制,咱們總能在下次事件循環以前把咱們要處理的事件處理掉。bash

微任務 宏任務

常見的宏任務有 setTimeoutMessageChannel``、postMessagesetImmediate閉包

微任務有 MutationObserverPromise.then 以及 nodeprocess.nextTick框架

固然,爲了程序的優化和性能提高,咱們的最佳選擇固然是 Promise 啦,但是呢,Promise 屬於es6中提出的,部分瀏覽器可能出現不兼容的狀況 (PS: IE:你看我幹嗎?),因此官方就給了一個優雅降級策略,若是當前瀏覽器支持 Promise 則使用Promise,其次就是MutationObserver,若是以上兩個都不支持,就只能搬出咱們的setTimeout了。話很少說,下面開始搬代碼。

//存儲須要觸發的回調函數
  var callbacks=[];
  /**是否正在等待的標誌(false:容許觸發在下次事件循環觸發callbacks中的回調,
  *  true: 已經觸發過,須要等到下次事件循環)
  */
  var pending=false;
  //設置在下次事件循環觸發callbacks的觸發函數
  var timerFunc;
複製代碼

上面的這個timerFunc 將用於達到觸發條件後觸發全部回調函數

//處理callbacks的函數
  function nextTickHandler() {
      // 能夠觸發timeFunc
      pending=false;
      //複製callback
      var copies=callbacks.slice(0);
      //清除callback
      callbacks.length=0;
      for(var i=0;i<copies.length;i++){
          //觸發callback的回調函數
          copies[i]();
      }
  }
複製代碼

這部分代碼就是實現觸發全部綁定的回調函數的主要邏輯部分,下面咱們來看看官方的的優雅降級策略怎麼實現的

//若是支持promise,使用promise實現
  if(typeof Promise !=='undefined' && isNative(promise)){
      var p=Promise.resolve();
      var logError=function (err) {
          console.error(err);
      };
      timerFunc=function () {
          p.then(nextTickHandler).catch(logError);
          //iOS的webview下,須要強制刷新隊列,執行上面的回調函數
          if(isIOS) {setTimeout(noop);}
      };
  //    若是Promise不支持,但支持MutationObserver
  //    H5新特性,異步,當dom變更是觸發,注意是全部的dom都改變結束後觸發
  } else if (typeof MutationObserver !=='undefined' && (
      isNative(MutationObserver) ||
      MutationObserver.toString()==='[object MutationObserverConstructor]')){
          var counter = 1;
          var observer=new MutationObserver(nextTickHandler);
          var textNode=document.createTextNode(String(counter));
          observer.observe(textNode,{
              characterData:true
          });
          timerFunc=function () {
              counter=(counter+1)%2;
              textNode.data=String(counter);
          };
  } else {
      //上面兩種都不支持,用setTimeout
      timerFunc=function () {
          setTimeout(nextTickHandler,0);
      };
  }
複製代碼

看完這段代碼,你們可能對官方的這個降級策略有了一種恍然大悟的感受,不過可能你們也會有疑問,這個MutationObserver的實現方式怎麼這麼詭異,那讓咱們來看看它的用法吧。

MutationObserver 概述

  • 監視 DOM 變更的接口當監視的 DOM 發生變更時 MutationObserver 將收到通知並觸發事先設定好的回調函數。
  • 相似於事件,可是異步觸發。添加監視時,MutationObserver 上的 observer 函數與 addEventListener 有類似之處,但不一樣於後者的同步觸發,MutationObserver是異步觸發,此舉是爲了不 DOM 頻繁變更致使回調函數被頻繁調用,形成瀏覽器卡頓。

MutationObserver 構造函數

該構造函數用於實例化一個新的 MutaionObserver ,同時指定觸發 DOM 變更時的回調函數:

var observer = new MutationObserver(callback);
複製代碼

callback,即回調函數接收兩個參數,第一個參數是一個包含了全部 MutationRecord 對象的數組,第二個參數則是這個MutationObserver 實例自己。具體詳細介紹能夠參考 深刻了解MutationObserver

咳咳咳,回到正題

//nextTick接收的函數,參數1:回調函數 參數2:回調函數的執行上下文
  return function queueNextTick(cb,ctx) {
      //用於接收觸發Promise.then中回調的函數
      //向回調函數中pushcallback
      var _resolve;
      callbacks.push(function () {
          //若是有回調函數,執行回調函數
          if(cb) {cb.call(ctx);}
          //觸發Promise的then回調
          if(_resolve) {_resolve(ctx);}
      });
      //是否執行刷新callback隊列
      if(!pending){
          pending=true;
          timerFunc();
      }
      //若是沒有傳遞迴調函數,而且當前瀏覽器支持promise,使用promise實現
      if(!cb && typeof  Promise !=='undefined'){
          return new Promise(function (resolve) {
              _resolve=resolve;
          })
      }
  }
複製代碼

以上其實就是你調用這個方法實際調用的函數啦,利用閉包原理保存了前面提到的各個函數的引用,首先他會把你傳入的回調函數包裝一下保存到callback數組中。

若是當前隊列還未執行過回調,那麼開始執行回調,並把pending標誌位置爲true,表示當前任務隊列已經執行過回調。

而後最後加一層判斷,若是當前瀏覽器具備Promise環境且未傳遞迴調函數則採用Promise執行。

最後附上完整代碼

export const nextTick=(function () {
  //存儲須要觸發的回調函數
  var callbacks=[];
  //是否正在等待的標誌(false:容許觸發在下次事件循環觸發callbacks中的回調,
  // true: 已經觸發過,須要等到下次事件循環)
  var pending=false;
  //設置在下次事件循環觸發callbacks的觸發函數
  var timerFunc;

  //處理callbacks的函數
  function nextTickHandler() {
      // 能夠觸發timeFunc
      pending=false;
      //複製callback
      var copies=callbacks.slice(0);
      //清除callback
      callbacks.length=0;
      for(var i=0;i<copies.length;i++){
          //觸發callback的回調函數
          copies[i]();
      }
  }
  //若是支持promise,使用promise實現
  if(typeof Promise !=='undefined' && isNative(promise)){
      var p=Promise.resolve();
      var logError=function (err) {
          console.error(err);
      };
      timerFunc=function () {
          p.then(nextTickHandler).catch(logError);
          //iOS的webview下,須要強制刷新隊列,執行上面的回調函數
          if(isIOS) {setTimeout(noop);}
      };
  //    若是Promise不支持,但支持MutationObserver
  //    H5新特性,異步,當dom變更是觸發,注意是全部的dom都改變結束後觸發
  } else if (typeof MutationObserver !=='undefined' && (
      isNative(MutationObserver) ||
      MutationObserver.toString()==='[object MutationObserverConstructor]')){
          var counter = 1;
          var observer=new MutationObserver(nextTickHandler);
          var textNode=document.createTextNode(String(counter));
          observer.observe(textNode,{
              characterData:true
          });
          timerFunc=function () {
              counter=(counter+1)%2;
              textNode.data=String(counter);
          };
  } else {
      //上面兩種都不支持,用setTimeout
      timerFunc=function () {
          setTimeout(nextTickHandler,0);
      };
  }
  //nextTick接收的函數,參數1:回調函數 參數2:回調函數的執行上下文
  return function queueNextTick(cb,ctx) {
      //用於接收觸發Promise.then中回調的函數
      //向回調函數中pushcallback
      var _resolve;
      callbacks.push(function () {
          //若是有回調函數,執行回調函數
          if(cb) {cb.call(ctx);}
          //觸發Promise的then回調
          if(_resolve) {_resolve(ctx);}
      });
      //是否執行刷新callback隊列
      if(!pending){
          pending=true;
          timerFunc();
      }
      //若是沒有傳遞迴調函數,而且當前瀏覽器支持promise,使用promise實現
      if(!cb && typeof  Promise !=='undefined'){
          return new Promise(function (resolve) {
              _resolve=resolve;
          })
      }
  }
})()
複製代碼

附上筆者當時的心情

好啦本文暫時介紹到這裏,若是發現筆者寫的不對的地方,歡迎給筆者留言。

相關文章
相關標籤/搜索