Vue系列---理解Vue.nextTick使用及源碼分析(五)


閱讀目錄

一. 什么是Vue.nextTick()?

官方文檔解釋為:在下次DOM更新循環結束之后執行的延遲回調。在修改數據之后立即使用該方法,獲取更新后的DOM。

我們也可以簡單的理解為:當頁面中的數據發生改變了,就會把該任務放到一個異步隊列中,只有在當前任務空閑時才會進行DOM渲染,當DOM渲染完成以后,該函數就會自動執行。

2.1 更改數據后,進行節點DOM操作。

比如修改數據、修改節點樣式、等操作。比如說我修改data中的一個屬性數據后,如果我這個時候直接獲取該html內容的話,它還是老數據的,那么此時此刻,我們可以使用 Vue.nextTick(), 在該函數內部獲取該數據即可: 如下代碼:
<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      mounted() {
        this.updateData();
      },
      methods: {
        updateData() {
          this.name = 'kongzhi222';
          console.log(this.$refs.list.textContent); // 打印 kongzhi111
          this.$nextTick(() => {
            console.log('-------');
            console.log(this.$refs.list.textContent); // 打印 kongzhi222
          });
        }
      }
    })
  </script>
</body>
</html>

如上代碼,頁面初始化時候,頁面顯示的是 "kongzhi111"; 當頁面中的所有的DOM更新完成后,我在mounted()生命周期中調用 updateData()方法,然后在該方法內部修改 this.name 這個數據,再打印 this.$refs.list.textContent, 可以看到打印的數據 還是 'kongzhi111'; 為什么會是這樣呢?那是因為修改name數據后,我們的DOM還沒有被渲染完成,所以我們這個時候獲取的值還是之前的值,但是我們放在nextTick函數里面的時候,代碼會在DOM更新完成后 會自動執行 nextTick()函數,因此這個時候我們再去使用 this.$refs.list.textContent 獲取該值的時候,就可以獲取到最新值了。
理解DOM更新:在VUE中,當我們修改了data中的某一個值后,並不會立刻去渲染html頁面,而是將vue更改的數據放到watcher的一個異步隊列中,只有在當前任務空閑時才會執行watcher中的隊列任務,因此這就會有一個延遲時間,因此我們把代碼放到nextTick函數后就可以獲取到該 html 頁面的最新值了。

2.2 在created生命周期中進行DOM操作。

在Vue生命周期中,只有在mounted生命周期中我們的HTML才渲染完成,因此在該生命周期中,我們就可以獲取到頁面中的html DOM節點,但是如果我們在 created生命周期中是訪問不到DOM節點的。
在該生命周期中我們想要獲取DOM節點的話,我們需要使用 this.$nextTick() 函數。

比如如下代碼進行演示:

<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      created() {
        console.log(this.$refs.list); // 打印undefined
        this.$nextTick(() => {
          console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
        });
      },
      methods: {
        
      }
    })
  </script>
</body>
</html>

如上代碼,在created生命周期內,我們打印 this.$refs.list 值為undefined,那是因為在created生命周期內頁面的html沒有被渲染完成,因此打印出為undefined; 但是我們把它放入 this.$nextTick函數內即可 打印出值出來,這也印證了 nextTick 是在下次DOM更新循環結束之后執行的延遲回調。因此只有DOM渲染完成后才會自動執行的延遲回調函數。

Vue的特點之一就是能實現響應式,但數據更新時,DOM不會立即更新,而是放入一個異步隊列中,因此如果在我們的業務場景中,需要在DOM更新之后執行一段代碼時,這個時候我們可以使用 this.$nextTick() 函數來實現。

三. Vue.nextTick的調用方式如下:

Vue.nextTick([callback, context]) 和 vm.$nextTick([callback]);

Vue.nextTick([callback, context]); 該方法是全局方法,該方法可接收2個參數,分別為回調函數 和 執行回調函數的上下文環境。

vm.$nextTick([callback]): 該方法是實列方法,執行時自動綁定this到當前的實列上。

四:vm.$nextTick 與 setTimeout 的區別是什么?

在區別他們倆之前,我們先來看一個簡單的demo如下:
<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      created() {
        console.log(this.$refs.list); // 打印undefined
        setTimeout(() => {
          console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
        }, 0);
      }
    })
  </script>
</body>
</html>

如上代碼,我們不使用 nextTick, 我們使用setTimeout延遲也一樣可以獲取頁面中的HTML元素的,那么他們倆之間到底有什么區別呢?

通過看vue源碼我們知道,nextTick 源碼在 src/core/util/next-tick.js 里面。在vue中使用了三種情況來延遲調用該函數,首先我們會判斷我們的設備是否支持Promise對象,如果支持的話,會使用 Promise.then 來做延遲調用函數。如果設備不支持Promise對象,再判斷是否支持 MutationObserver 對象,如果支持該對象,就使用MutationObserver來做延遲,最后如果上面兩種都不支持的話,我們會使用 setTimeout(() => {}, 0); setTimeout 來做延遲操作。

在比較 nextTick 與 setTimeout 的區別,其實我們可以比較 promise 或 MutationObserver 對象 與 setTimeout的區別的了,因為nextTick會先判斷設備是否支持promise及MutationObserver 對象的,只要我們弄懂 promise 和 setTimeout的區別,也就弄明白 nextTick 與 setTimeout的區別了。

在比較promise與setTimeout之前,我們先來看如下demo。

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <meta charset="utf-8">
</head>
<body>
  <script type="text/javascript">
    console.log(1);
    setTimeout(function(){
      console.log(2);
    }, 0);
    new Promise(function(resolve) {
      console.log(3);
      for (var i = 0; i < 100; i++) {
        i === 99 && resolve();
      }
      console.log(4);
    }).then(function() {
      console.log(5);
    });
    console.log(6);
  </script>
</body>
</html>

如上代碼輸出的結果是:1, 3, 4, 6, 5, 2; 首先打印1,這個我們能理解的,其實為什么打印3,在promise內部也屬於同步的,只有在then內是異步的,因此打印 1, 3, 4 , 然后執行then函數是異步的,因此打印6. 那么結果為什么是 1, 3, 4, 6, 5, 2 呢? 為什么不是 1, 3, 4, 6, 2, 5呢?

我們都知道 Promise.then 和 setTimeout 都是異步的,那么在事件隊列中Promise.then的事件應該是在setTimeout的后面的,那么為什么Promise.then比setTimeout函數先執行呢?

理解Event Loop 的概念

我們都明白,javascript是單線程的,所有的任務都會在主線程中執行的,當主線程中的任務都執行完成之后,系統會 "依次" 讀取任務隊列里面的事件,因此對應的異步任務進入主線程,開始執行。

但是異步任務隊列又分為: macrotasks(宏任務) 和 microtasks(微任務)。 他們兩者分別有如下API:

macrotasks(宏任務): setTimeout、setInterval、setImmediate、I/O、UI rendering 等。

microtasks(微任務): Promise、process.nextTick、MutationObserver 等。

如上我們的promise的then方法的函數會被推入到 microtasks(微任務) 隊列中,而setTimeout函數會被推入到 macrotasks(宏任務) 任務隊列中,在每一次事件循環中 macrotasks(宏任務) 只會提取一個執行,而 microtasks(微任務) 會一直提取,直到 microtasks(微任務)隊列為空為止。

也就是說,如果某個 microtasks(微任務) 被推入到執行中,那么當主線程任務執行完成后,會循環調用該隊列任務中的下一個任務來執行,直到該任務隊列到最后一個任務為止。而事件循環每次只會入棧一個 macrotasks(宏任務), 主線程執行完成該任務后又會循環檢查 microtasks(微任務) 隊列是否還有未執行的,直到所有的執行完成后,再執行 macrotasks(宏任務)。 依次循環,直到所有的異步任務完成為止。

有了上面 macrotasks(宏任務) 和 microtasks(微任務) 概念后,我們再來理解上面的代碼,上面所有的代碼都寫在script標簽中,那么讀取script標簽中的所有代碼,它就是第一個宏任務,因此我們就開始執行第一個宏任務。因此首先打印 1, 然后代碼往下讀取,我們遇到setTimeout, 它就是第二個宏任務,會將它推入到 macrotasks(宏任務) 事件隊列里面排隊。
下面我們繼續往下讀取,
遇到Promise對象,在Promise內部執行它是同步的,因此會打印3, 4。 然后繼續遇到 Promise.then 回調函數,他是一個 microtasks(微任務)的,因此將他 推入到 microtasks(微任務) 事件隊列中,最后代碼執行 console.log(6); 因此打印6. 第一個macrotasks(宏任務)執行完成后,然后我們會依次循環執行 microtasks(微任務), 直到最后一個為止,因此我們就執行 promise.then() 異步回調中的代碼,因此打印5,那么此時此刻第一個 macrotasks(宏任務) 執行完畢,會執行下一個 macrotasks(宏任務)任務。因此就執行到 setTimeout函數了,最后就打印2。到此,所有的任務都執行完畢。因此我們最后的結果為:1, 3, 4, 6, 5, 2;

我們可以繼續多添加幾個setTimeout函數和多加幾個Promise對象來驗證下,如下代碼:

<script type="text/javascript">
  console.log(1);
  setTimeout(function(){
    console.log(2);
  }, 10);
  new Promise(function(resolve) {
    console.log(3);
    for (var i = 0; i < 10000; i++) {
      i === 9999 && resolve();
    }
    console.log(4);
  }).then(function() {
    console.log(5);
  });
  setTimeout(function(){
    console.log(7);
  },1);
  new Promise(function(resolve) {
    console.log(8);
    resolve();
  }).then(function(){
    console.log(9);
  });
  console.log(6);
</script>

如上打印的結果為: 1, 3, 4, 8, 6, 5, 9, 7, 2;

首先打印1,這是沒有任何爭議的哦,promise內部也是同步代碼,因此打印 3, 4, 然后就是第二個promise內部代碼,因此打印8,再打印外面的代碼,就是6。因此主線程執行完成后,打印的結果分別為:

1, 3, 4, 8, 6。 然后再執行 promise.then() 回調的 microtasks(微任務)。因此打印 5, 9。因此microtasks(微任務)執行完成后,就執行第二個宏任務setTimeout,由於第一個setTimeout是10毫秒后執行,第二個setTimeout是1毫秒后執行,因此1毫秒的優先級大於10毫秒的優先級,因此最后分別打印 7, 2 了。因此打印的結果是: 1, 3, 4, 8, 6, 5, 9, 7, 2;
總結: 如上我們也看到 microtasks(微任務) 包括 Promise 和 MutationObserver, 因此 我們可以知道在Vue中的nextTick 的執行速度上是快於setTimeout的。

我們從如下demo也可以得到驗證:

<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      created() {
        console.log(this.$refs.list); // 打印undefined
        setTimeout(() => {
          console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
        }, 0);
        this.$nextTick(function(){
          console.log('nextTick比setTimeout先執行');
        });
      }
    })
  </script>
</body>
</html>

如上代碼,先打印的是 undefiend, 其次是打印 "nextTick比setTimeout先執行" 信息, 最后打印出 "<div>kongzhi111</div>" 信息。

五:理解 MutationObserver

在Vue中的nextTick的源碼中,使用了3種情況來做延遲操作,首先會判斷我們的設備是否支持Promsie對象,如果支持Promise對象,就使用Promise.then()異步函數來延遲,如果不支持,我們會繼續判斷我們的設備是否支持 MutationObserver, 如果支持,我們就使用 MutationObserver 來監聽。最后如果上面兩種都不支持的話,我們會使用 setTimeout 來處理,那么我們現在要理解的是 MutationObserver 是什么?
5.1 MutationObserver是什么?

MutationObserver 中文含義可以理解為 "變動觀察器"。它是監聽DOM變動的接口,DOM發生任何變動,MutationObserver會得到通知。在Vue中是通過該屬性來監聽DOM更新完畢的。

它和事件類似,但有所不同,事件是同步的,當DOM發生變動時,事件會立刻處理,但是 MutationObserver 則是異步的,它不會立即處理,而是等頁面上所有的DOM完成后,會執行一次,如果頁面上要操作100次DOM的話,如果是事件的話會監聽100次DOM,但是我們的 MutationObserver 只會執行一次,它是等待所有的DOM操作完成后,再執行。

它的特點是:

1. 等待所有腳本任務完成后,才會執行,即采用異步方式。
2. DOM的變動記錄會封裝成一個數組進行處理。
3. 還可以觀測發生在DOM的所有類型變動,也可以觀測某一類變動。

當然 MutationObserver 也是有瀏覽器兼容的,我們可以使用如下代碼來檢測瀏覽器是否支持該屬性,如下代碼:

var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
// 監測瀏覽器是否支持
var observeMutationSupport = !!MutationObserver;

MutationObserver 構造函數

首先我們要使用 MutationObserver 構造函數的話,我們先要實列化 MutationObserver 構造函數,同時我們要指定該實列的回調函數,如下代碼:

var observer = new MutationObserver(callback);

觀察器callback回調函數會在每次DOM發生變動后調用,它接收2個參數,第一個是變動的數組,第二個是觀察器的實列。

MutationObserver 實列的方法

observe() 該方法是要觀察DOM節點的變動的。該方法接收2個參數,第一個參數是要觀察的DOM元素,第二個是要觀察的變動類型。

調用方式為:observer.observe(dom, options);

options 類型有如下:

childList: 子節點的變動。
attributes: 屬性的變動。
characterData: 節點內容或節點文本的變動。
subtree: 所有后代節點的變動。

需要觀察哪一種變動類型,需要在options對象中指定為true即可; 但是如果設置subtree的變動,必須同時指定childList, attributes, 和 characterData 中的一種或多種。

1. 監聽childList的變動

如下測試代碼:

<!DOCTYPE html>
<html>
<head>
  <title>MutationObserver</title>
  <meta charset="utf-8">
</head>
<body>
  <div id="app">
    <ul>
      <li>kongzhi111</li>
    </ul>
  </div>
  <script type="text/javascript">
    var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
    var list = document.querySelector('ul');
    var Observer = new MutationObserver(function(mutations, instance) {
      console.log(mutations);  // 打印mutations 如下圖對應的
      console.log(instance);   // 打印instance 如下圖對於的
      mutations.forEach(function(mutation){
        console.log(mutation); // 打印mutation
      });
    });
    Observer.observe(list, {
      childList: true, // 子節點的變動
      subtree: true // 所有后代節點的變動
    });
    var li = document.createElement('li');
    var textNode = document.createTextNode('kongzhi');
    li.appendChild(textNode);
    list.appendChild(li);
  </script>
</body>
</html>

如上代碼,我們使用了 observe() 方法來觀察list節點的變化,只要list節點的子節點或后代的節點有任何變化都會觸發 MutationObserver 構造函數的回調函數。因此就會打印該構造函數里面的數據。
打印如下圖所示:

2. 監聽characterData的變動

如下測試代碼:

<!DOCTYPE html>
  <html>
    <head>
      <title>MutationObserver</title>
      <meta charset="utf-8">
    </head>
    <body>
      <div id="app">
        <ul>
          <li>kongzhi111</li>
        </ul>
      </div>
      <script type="text/javascript">
        var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
        var list = document.querySelector('ul');
        var Observer = new MutationObserver(function(mutations, instance) {
          mutations.forEach(function(mutation){
            console.log(mutation);
          });
        });
        Observer.observe(list, {
          childList: true, // 子節點的變動
          characterData: true, // 節點內容或節點文本變動
          subtree: true // 所有后代節點的變動
        });
        // 改變節點中的子節點中的數據
        list.childNodes[0].data = "kongzhi222";
      </script>
    </body>
  </html>

打印如下效果:

3. 監聽屬性的變動

<!DOCTYPE html>
<html>
<head>
  <title>MutationObserver</title>
  <meta charset="utf-8">
</head>
<body>
  <div id="app">
    <ul>
      <li>kongzhi111</li>
    </ul>
  </div>
  <script type="text/javascript">
    var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
    var list = document.querySelector('ul');
    var Observer = new MutationObserver(function(mutations, instance) {
      mutations.forEach(function(mutation){
        console.log(mutation);
      });
    });
    Observer.observe(list, {
      attributes: true
    });
    // 設置節點的屬性,會觸發回調函數
    list.setAttribute('data-value', 'tugenhua111');

    // 重新設置屬性,會觸發回調函數
    list.setAttribute('data-value', 'tugenhua222');

    // 刪除屬性,也會觸發回調函數
    list.removeAttribute('data-value');
  </script>
</body>
</html>

如上就是MutationObserver的基本使用,它能監聽 子節點的變動、屬性的變動、節點內容或節點文本的變動 及 所有后代節點的變動。 下面我們來看下我們的 nextTick.js 中的源碼是如何實現的。

六:nextTick源碼分析

vue源碼在 vue/src/core/util/next-tick.js 中。源碼如下:
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc;
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) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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 {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    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(resolve => {
      _resolve = resolve
    })
  }
}

如上代碼,我們從上往下看,首先定義變量 callbacks = []; 該變量的作用是: 用來存儲所有需要執行的回調函數。let pending = false; 該變量的作用是表示狀態,判斷是否有正在執行的回調函數。
也可以理解為,如果代碼中 timerFunc 函數被推送到任務隊列中去則不需要重復推送。

flushCallbacks() 函數,該函數的作用是用來執行callbacks里面存儲的所有回調函數。如下代碼:

function flushCallbacks () {
  /*
   設置 pending 為 false, 說明該 函數已經被推入到任務隊列或主線程中。需要等待當前
   棧執行完畢后再執行。
  */
  pending = false;
  // 拷貝一個callbacks函數數組的副本
  const copies = callbacks.slice(0)
  // 把函數數組清空
  callbacks.length = 0
  // 循環該函數數組,依次執行。
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

timerFunc: 保存需要被執行的函數。

繼續看接下來的代碼,我們上面講解過,在Vue中使用了幾種情況來延遲調用該函數。

1. promise.then 延遲調用, 基本代碼如下: 

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
}

如上代碼的含義是: 如果我們的設備(或叫瀏覽器)支持Promise, 那么我們就使用 Promise.then的方式來延遲函數的調用。Promise.then會將函數延遲到調用棧的最末端,從而會做到延遲。

2. MutationObserver 監聽, 基本代碼如下:

else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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
}

如上代碼,首先也是判斷我們的設備是否支持 MutationObserver 對象, 如果支持的話,我們就會創建一個MutationObserver構造函數, 並且把flushCallbacks函數當做callback的回調, 然后我們會創建一個文本節點, 之后會使用MutationObserver對象的observe來監聽該文本節點, 如果文本節點的內容有任何變動的話,它就會觸發 flushCallbacks 回調函數。那么要怎么樣觸發呢? 在該代碼內有一個 timerFunc 函數, 如果我們觸發該函數, 會導致文本節點的數據發生改變,進而觸發MutationObserver構造函數。

3. setImmediate 監聽, 基本代碼如下:

else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
}

如果上面的 Promise 和 MutationObserver 都不支持的話, 我們繼續會判斷設備是否支持 setImmediate, 我們上面分析過, 他屬於 macrotasks(宏任務)的。該任務會在一個宏任務里執行回調隊列。

4. 使用setTimeout 做降級處理

如果我們上面三種情況, 設備都不支持的話, 我們會使用 setTimeout 來做降級處理, 實現延遲效果。如下基本代碼:

else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

現在我們的源碼繼續往下看, 會看到我們的nextTick函數被export了,如下基本代碼:

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    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(resolve => {
      _resolve = resolve
    })
  }
}

如上代碼, nextTick 函數接收2個參數,cb 是一個回調函數, ctx 是一個上下文。 首先會把它存入callbacks函數數組里面去, 在函數內部會判斷cb是否是一個函數,如果是一個函數,就調用執行該函數,當然它會在callbacks函數數組遍歷的時候才會被執行。其次 如果cb不是一個函數的話, 那么會判斷是否有_resolve值, 有該值就使用Promise.then() 這樣的方式來調用。比如: this.$nextTick().then(cb) 這樣的使用方式。因此在下面的if語句內會判斷賦值給_resolve:

if (!cb && typeof Promise !== 'undefined') {
  return new Promise(resolve => {
    _resolve = resolve
  })
}

使用Promise返回了一個 fulfilled 的Promise。賦值給 _resolve; 然后在callbacks.push 中會執行如下:

_resolve(ctx);

全局方法Vue.nextTick在 /src/core/global-api/index.js 中聲明,是對函數nextTick的引用,所以使用時可以顯式指定執行上下文。代碼初始化如下:

Vue.nextTick = nextTick;

我們可以使用如下的一個簡單的demo來簡化上面的代碼。如下demo:

<script type="text/javascript">
  var callbacks = [];
  var pending = false;
  function timerFunc() {
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (var i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  function nextTick(cb, ctx) {
    var _resolve;
    callbacks.push(() => {
      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(resolve => {
        _resolve = resolve
      })
    }
  }

  // 調用方式如下:
  nextTick(function() {
    console.log('打印出來了'); // 會被執行打印
  });
</script>

如上我們已經知道了 nextTick 是Vue中的一個全局函數, 在Vue里面會有一個Watcher, 它用於觀察數據的變化, 然后更新DOM, 但是在Vue中並不是每次數據改變都會觸發更新DOM的, 而是將這些操作都緩存到一個隊列中, 在一個事件循環結束后, 會刷新隊列, 會統一執行DOM的更新操作。

在Vue中使用的是Object.defineProperty來監聽每個對象屬性數據變化的, 當監聽到數據發生變化的時候, 我們需要把該消息通知到所有的訂閱者, 也就是Dep, 那么Dep則會調用它管理的所有的Watch對象,因此會調用Watch對象中的update方法, 我們可以看下源碼中的update的實現。源碼在 vue/src/core/observer/watcher.js 中如下代碼:

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    // 同步執行渲染視圖
    this.run()
  } else {
    // 異步推送到觀察者隊列中
    queueWatcher(this)
  }
}

如上代碼我們可以看到, 在Vue中它默認是使用異步執行DOM更新的。當異步執行update的時候,它默認會調用 queueWatcher 函數。

我們下面再來看下該 queueWatcher 函數代碼如下: (源碼在: vue/src/core/observer/scheduler.js) 中。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

如上源碼, 我們從第一句代碼執行過來, 首先獲取該 id = watcher.id; 然后判斷該id是否存在 if (has[id] == null) {} , 如果已經存在則直接跳過,不存在則執行if
語句內部代碼, 並且標記哈希表has[id] = true; 用於下次檢驗。如果 flushing 為false的話, 則把該watcher對象push到隊列中, 考慮到一些情況, 比如正在更新隊列中
的watcher時, 又有事件塞入進來怎么處理? 因此這邊加了一個flushing來表示隊列的更新狀態。

如果加入隊列到更新狀態時,又分為兩種情況:

1. 這個watcher還沒有處理, 就找到這個watcher在隊列中的位置, 並且把新的放在后面, 比如如下代碼:

if (!flushing) {
  queue.push(watcher)
}

2. 如果watcher已經更新過了, 就把這個watcher再放到當前執行的下一位, 當前的watcher處理完成后, 立即會處理這個最新的。如下代碼:

else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}

接着如下代碼:

if (!waiting) {
  waiting = true

  if (process.env.NODE_ENV !== 'production' && !config.async) {
    flushSchedulerQueue()
    return
  }
  nextTick(flushSchedulerQueue)
}

waiting 為false, 等待下一個tick時, 會執行刷新隊列。 如果不是正式環境的話, 會直接 調用該函數 flushSchedulerQueue; (源碼在: vue/src/core/observer/scheduler.js) 中。否則的話,  把該函數放入 nextTick 函數延遲處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM