1. 程式人生 > >基於原始碼分析Vue的nextTick

基於原始碼分析Vue的nextTick

摘要:本文通過結合官方文件、原始碼和其他文章整理後,對Vue的nextTick做深入解析。理解本文最好有瀏覽器事件迴圈的基礎,建議先閱讀上文《事件迴圈Event loop到底是什麼》。

一、官方定義


實際上在弄清楚瀏覽器的事件迴圈後,Vue的nextTick就非常好理解了,它就是利用了事件迴圈的機制。我們首先來看看nextTick在Vue官方文件中是如何描述的:

Vue在更新DOM時是非同步執行的,只要偵聽到資料變化,Vue將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料變更。如果同一個watcher被多次觸發,只會被推入到佇列中一次。這種在緩衝時去除重複資料對於避免不必要的計算和DOM操作是非常重要的。然後,在下一個事件迴圈“tick”中,Vue重新整理佇列並執行實際(已去重的)工作。Vue在內部對非同步佇列嘗試使用原生的Promise.then、MutationObserver和setImmediate,如果執行環境不支援,則會採用setTimeout(fn,0)代替。

當重新整理佇列時,元件會在下一個事件迴圈“tick”中更新。多數情況我們不需要關心這個過程,但是如果你想基於更新後的 DOM 狀態來做點什麼,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發人員使用“資料驅動”的方式思考,避免直接接觸 DOM,但是有時我們必須要這麼做。為了在資料變化之後等待 Vue 完成更新 DOM,可以在資料變化之後立即使用 Vue.nextTick(callback)。

簡單來說,Vue為了保證資料多次變化操作DOM更新的效能,採用了非同步更新DOM的機制,且同一事件迴圈中同一個資料多次修改只會取最後一次修改結果。而這種方式產生一個問題,開發人員無法通過同步程式碼獲取資料更新後的DOM狀態,所以Vue就提供了Vue.nextTick方法,通過這個方法的回撥就能獲取當前DOM更新後的狀態。

但只看官方解釋可能還是會有些疑問,比如描述中說到的下一個事件迴圈“tick”是什麼意思?為什麼會是下一個事件迴圈?接下來我們看原始碼到底是怎麼實現的。

二、原始碼解析


Vue.nextTick的原始碼部分主要分為Watcher部分和NextTick部分,由於Watcher部分的原始碼在前文《深入解析vue響應式原理》中,已經詳細分析過了,所以這裡關於Watcher的原始碼就直接分析觸發update之後的部分。

update

``` update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } ```

queueWatcher

``` export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { 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) } } } ```

flushSchedulerQueue

``` function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // call component updated and activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } } ```

根據前文《深入解析vue響應式原理》可以知道,資料變化後會首先觸發關聯Dep的notify方法,然後會呼叫所有依賴該資料的Watcher.update方法。接下來的步驟總結如下:

  1. update又呼叫了queueWatcher方法;
  2. queueWatcher方法中使用靜態全域性Watcher陣列queue來儲存當前的watcher,並且如果Watcher重複,只會保留最新的Watcher;
  3. 然後是flushSchedulerQueue方法,簡單來說,flushSchedulerQueue方法中主要就是遍歷queue陣列,依次執行了所有的Watcher.run,操作DOM更新;
  4. 但flushSchedulerQueue並不會立即執行,而是作為nextTick引數進入下一層。

重點來到了nextTick這一層。

nextTick

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

timerFunc

``` 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) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { 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) } } ```

flushCallbacks

``` function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } ```

nextTick程式碼流程總結如下:

  1. 結合前面程式碼分析來看,遍歷Watcher執行DOM更新的方法傳入了nextTick,在nextTick中被新增到了callbacks陣列,隨後執行了timerFunc方法;
  2. timerFunc方法使用了flushCallbacks方法,flushCallbacks執行了flushSchedulerQueue方法,即執行Watcher關聯的DOM更新。
  3. 而timerFunc是根據瀏覽器支援情況,將flushCallbacks(DOM更新操作)作為引數傳遞給Promise.then、MutationObserver、setImmediate或setTimeout(fn,0)。

到這裡我們明白了,原來在Vue中資料變更觸發DOM更新操作也是使用了nextTick來實現非同步執行的,而Vue提供給開發者使用的nextTick是同一個nextTick。所以官方文件強調了要在資料變化之後立即使用 Vue.nextTick(callback),這樣就能保證callback是插入佇列裡DOM更新操作的後面,並在同一個事件迴圈中按順序完成,因為開發者插入的callback在隊尾,那麼始終是在DOM操作後立即執行。

而針對官方文件“在下一個事件迴圈"tick"中,Vue重新整理佇列並執行實際(已去重的)工作”的描述我覺得是不夠嚴謹的,原因在於,根據瀏覽器的支援情況,結合瀏覽器事件迴圈巨集任務和微任務的概念,nextTick使用的是Promise.then或MutationObserver,那就應該是和script(整體程式碼)是同一個事件迴圈;當使用的是setImmediate或setTimeout(fn,0)),那才在下一個事件迴圈。
同時,聰明的你或許已經想到了,那按這個原理實際我不需要使用nextTick好像也可以達到同樣的效果,比如使用setTimeout(fn,0),那我們直接用一個例子來看一下吧。

```

相關推薦

從壹開始微服務 [ DDD ] 之十一 ║ 基於原始碼分析,命令分發的過程(二)

緣起 哈嘍小夥伴週三好,老張又來啦,DDD領域驅動設計的第二個D也快說完了,下一個系列我也在考慮之中,是 Id4 還是 Dockers 還沒有想好,甚至昨天我還想,下一步是不是可以寫一個簡單的Angular 入門教程,本來是想來個前後端分離的教學視訊的,簡單試了試,發現自己的聲音不好聽,真心不好聽那種,就作

基於原始碼分析Vue的nextTick

摘要:本文通過結合官方文件、原始碼和其他文章整理後,對Vue的nextTick做深入解析。理解本文最好有瀏覽器事件迴圈的基礎,建議先閱讀上文《事件迴圈Event loop到底是什麼》。 一、官方定義 實際上在弄清楚瀏覽器的事件迴圈後,Vue的nextTick就非常好理解了,它就是利用

菜鳥帶你看原始碼——看不懂你打我ArrayList原始碼分析基於java 8)

文章目錄 看原始碼並不難 軟體環境 成員變數: 構造方法 核心方法 get方法 remove方法 add方法 結束 看原始碼並不難 如何學好程式設計?如何寫出優質的程式碼?如

Flume NG原始碼分析(一)基於靜態properties檔案的配置模組

日誌收集是網際網路公司的一個重要服務,Flume NG是Apache的頂級專案,是分散式日誌收集服務的一個開源實現,具有良好的擴充套件性,與其他很多開源元件可以無縫整合。搜了一圈發現介紹Flume NG的文章有不少,但是深入分析Flume NG原始碼的卻沒有。準備寫一個系列分析一下Flume NG的

Java -- 基於JDK1.8的ArrayList原始碼分析

1,前言   很久沒有寫部落格了,很想念大家,18年都快過完了,才開始寫第一篇,爭取後面每週寫點,權當是記錄,因為最近在看JDK的Collection,而且ArrayList原始碼這一塊也經常被面試官問道,所以今天也就和大家一起來總結一下 2,原始碼解讀   當我們一般提到ArrayLi

Tomcat 原始碼分析 WebappClassLoader 分析 (基於8.0.5)

0. 疑惑 在剛接觸 Tomcat 中的ClassLoader時心中不免冒出的疑惑: "Tomcat 裡面是怎麼樣設計ClassLoader的, 這樣設計有什麼好處?"; 我們先把這個問題留著, 到最後在看 ! 1. Java 中 ClassLoader 類別 1. BootstrapC

LinkedHashMap及其原始碼分析基於JDK1.7)

LinkedHashMap及其原始碼分析 閱讀目錄 什麼是LinkedHashMap LinkedHashMap補充說明 LinkedHashMap的陣列結構 LinkedHashMap繼承的類與實現的介面 LinkedHashMap原始碼中雙向連結串列的

cgroup原始碼分析——基於centos3.10.0-693.25.4

  核心升級完測試兄弟跑ltprun套件,發現跑完後cgroup失效了。看系統一切執行正常核心也沒啥錯誤日誌,又不熟cgroup的實現,在一頓翻程式碼後發現cgroup註冊了CPU熱插拔的notifier chain。去翻ltp的測試內容發現,CPU熱插拔赫然在列。為啥不一開始先翻ltp

原始碼分析基於LinkedList手寫HahMap(二)

package com.mayikt.extLinkedListHashMap; import java.util.LinkedList; import java.util.concurrent.ConcurrentHashMap; /** * 基於linkedList實現hashMap *

原始碼分析基於ArrayList手寫HahMap(一)

import java.util.ArrayList; import java.util.List; /** * 基於arraylist實現hashmap集合(簡版:效率低) * @author zjmiec * */ public class ExtArrayListHashMap&

java集合之----ArrayList原始碼分析基於jdk1.8)

一、ArrayList 1、ArrayList是什麼: ArrayList就是動態陣列,用MSDN中的說法,就是Array的複雜版本,它提供了動態的增加和減少元素,實現了ICollection和IList介面,靈活的設定陣列的大小等好處,實現了Randomaccess介面,支援快速隨

java集合之----HashMap原始碼分析基於JDK1.7與1.8)

一、什麼是HashMap 百度百科這樣解釋: 簡而言之,HashMap儲存的是鍵值對(key和value),通過key對映到value,具有很快的訪問速度。HashMap是非執行緒安全的,也就是說在多執行緒併發環境下會出現問題(死迴圈) 二、內部實現 (1)結構 HashM

mmap原始碼分析--基於3.10.0-693.11.1

mmap是個既簡單又好用的東西,對於讀寫檔案,它減少了一次記憶體拷貝,對於記憶體申請,它可以方便的申請到大塊記憶體,用於自己管理。今天就來說說mmap的實現。 mmap的原型是這樣的: void *mmap(void *addr, size_t length,

python多執行緒探討 基於原始碼 初步分析

這篇文章 應該有人需要 因為我現在都需要 所以標了個關鍵字初步 摘自百度百科 儘管提高CPU的時鐘頻率和增加快取容量後的確可以改善效能,但這樣的CPU效能提高在技術上存在較大的難度。實際上在應用中基於很多原因,CPU的執行單元都沒有被充分使用。如果CPU不能正常讀取資料(

ArrayList的原始碼分析(基於jdk1.8)

1.初始化 transient Object[] elementData; //實際儲存元素的陣列 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; public ArrayList() { //初

LinkedList的原始碼分析(基於jdk1.8)

1.初始化 public LinkedList() { } 並未開闢任何類似於陣列一樣的儲存空間,那麼連結串列是如何儲存元素的呢?   2.Node型別 儲存到連結串列中的元素會被封裝為一個Node型別的結點。並且連結串列只需記錄第一個結點的位置和最後一個結點的位置。然後每一個結

Java -- 基於JDK1.8的LinkedList原始碼分析

1,上週末我們一起分析了ArrayList的原始碼並進行了一些總結,因為最近在看Collection這一塊的東西,下面的圖也是大致的總結了Collection裡面重要的介面和類,如果沒有意外的話後面基本上每一個都會和大家一起學習學習,所以今天也就和大家一起來看看LinkedList吧! 哦,不對,放錯圖了,

springmvc工作原理以及原始碼分析(基於spring3.1.0)

springmvc是一個基於spring的web框架.本篇文章對它的工作原理以及原始碼進行深入分析. 一、springmvc請求處理流程   引用spring in action上的一張圖來說明了springmvc的核心元件和請求處理流程:       

mmap核心原始碼分析基於核心版本3.10(三)

之前寫了(一)(二)其實就梳理到了get_unmapped_area的內容,而且有一點混亂,這裡進行第三篇的講解,講解在do_mmap_pgoff中除了get_unmapped_area的內容,來了解mmap的具體實現。通過(一)(二)(三)來將mmap核心原始碼進行一次梳理

基於Linux核心的UDP協議原始碼分析

  當一個數據包(package)經過IP層的處理之後,最終呼叫ip_local_deliever()函式,這個函式會根據這個資料包(packet)的傳輸層頭兒確定其採用的傳輸協議,如果是UDP協議,將會呼叫udp_rcv()函式。至此,進入傳輸層的範圍。 UDP協議棧的報頭定義如下: