搜尋
首頁web前端js教程Vue源碼中批次非同步更新與nextTick原理的解析

Vue源碼中批次非同步更新與nextTick原理的解析

Jul 20, 2018 am 11:53 AM
vue.js原始碼分析

這篇文章跟大家介紹的內容是關於Vue源碼中批量異步更新與nextTick原理的解析,有著一定的參考價值,有需要的朋友可以參考一下。

vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社區湧現了一大票vue源碼閱讀類的文章,在下藉這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀源碼時的想法進行總結,出產一些文章,作為自己思考的總結

#目標Vue版本:2.5.17-beta.0

vue原始碼註解:https://github.com/SHERlocked93/vue-analysis

聲明:文章中原始碼的語法都使用Flow,並且原始碼根據需要都有刪節(為了不被迷糊@_@),如果要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~

1. 非同步更新

我們在依賴收集原理的響應式化方法defineReactive 中的setter 存取器中有派發更新dep .notify() 方法,這個方法會挨個通知在depsubs 中收集的訂閱自己變動的watchers執行update。一起來看看update 方法的實作:

// src/core/observer/watcher.js

/* Subscriber接口,当依赖发生改变的时候进行回调 */
update() {
  if (this.computed) {
    // 一个computed watcher有两种模式:activated lazy(默认)
    // 只有当它被至少一个订阅者依赖时才置activated,这通常是另一个计算属性或组件的render function
    if (this.dep.subs.length === 0) {       // 如果没人订阅这个计算属性的变化
      // lazy时,我们希望它只在必要时执行计算,所以我们只是简单地将观察者标记为dirty
      // 当计算属性被访问时,实际的计算在this.evaluate()中执行
      this.dirty = true
    } else {
      // activated模式下,我们希望主动执行计算,但只有当值确实发生变化时才通知我们的订阅者
      this.getAndInvoke(() => {
        this.dep.notify()     // 通知渲染watcher重新渲染,通知依赖自己的所有watcher执行update
      })
    }
  } else if (this.sync) {      // 同步
    this.run()
  } else {
    queueWatcher(this)        // 异步推送到调度者观察者队列中,下一个tick时调用
  }
}

如果不是computed watcher 也非sync 會把呼叫update的目前watcher推送到調度者佇列中,下一個tick時調用,看看queueWatcher

// src/core/observer/scheduler.js

/* 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则
 * 该watcher将被跳过,除非它是在队列正被flush时推送
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {     // 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验
    has[id] = true
    queue.push(watcher)      // 如果没有正在flush,直接push到队列中
    if (!waiting) {          // 标记是否已传给nextTick
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

/* 重置调度者状态 */
function resetSchedulerState () {
  queue.length = 0
  has = {}
  waiting = false
}

這裡使用了一個has 的哈希map用來檢查是否當前watcher的id是否存在,若已存在則跳過,不存在則就push到queue 隊列中並標記哈希表has,用於下次檢驗,防止重複添加。這就是一個去重的過程,比每次查重都要去queue中找要文明,在渲染的時候就不會重複patch 相同watcher的變化,這樣就算同步修改了一百次視圖中用到的data,異步patch 的時候也只會更新最後一次修改。

這裡的waiting 方法是用來標記flushSchedulerQueue 是否已經傳遞給nextTick 的標記位,如果已經傳遞則只push到隊列中不傳遞flushSchedulerQueuenextTick,等到resetSchedulerState 重置調度者狀態的時候waiting 會被置回false 允許flushSchedulerQueue 被傳遞給下一個tick的回調,總之保證了flushSchedulerQueue 回調在一個tick內只允許被傳入一次。來看看傳遞給nextTick 的回呼flushSchedulerQueue 做了什麼:

// src/core/observer/scheduler.js

/* nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)                    // 排序

  for (index = 0; index  MAX_UPDATE_COUNT) {     // 持续执行了一百次watch代表可能存在死循环
        warn()                                  // 进入死循环的警告
        break
      }
    }
  }
  resetSchedulerState()           // 重置调度者状态
  callActivatedHooks()            // 使子组件状态都置成active同时调用activated钩子
  callUpdatedHooks()              // 调用updated钩子
}

nextTick 方法中執行flushSchedulerQueue # 方法,這個方法挨個執行queue 中的watcher的run 方法。我們看到在首先有個queue.sort() 方法把佇列中的watcher按id從小到大排了個序,這樣做可以保證:

  1. 元件更新的順序是從父元件到子元件的順序,因為父元件總是比子元件先建立。

  2. 一個元件的user watchers(偵聽器watcher)比render watcher先運行,因為user watchers往往比render watcher更早創建

  3. 如果一個元件在父元件watcher運作期間被銷毀,它的watcher執行將會被跳過

在挨個執行佇列中的for迴圈中,index 這裡沒有將length進行緩存,因為在執行處理現有watcher物件期間,更多的watcher物件可能會被push進queue。

那麼資料的修改從model層反映到view的過程:資料變更-> setter -> Dep -> Watcher -> nextTick -> patch -> 更新視圖

2. nextTick原理

2.1 巨集任務/微任務

這裡就來看看包含著每個watcher執行的方法被當作回呼傳入nextTick 之後,nextTick 對這個方法做了什麼。不過首先要了解瀏覽器中的EventLoopmacro taskmicro task幾個概念,不了解可以參考JS與Node.js中的事件循環這篇文章,這裡就用一張圖來表示一下後兩者在主執行緒的執行關係:

Vue源碼中批次非同步更新與nextTick原理的解析

解釋一下,當主執行緒執行完同步任務後:

  1. 引擎先從macrotask queue中取出第一個任務,執行完畢後,將microtask queue中的所有任務取出,依序全部執行;

  2. 然後再從macrotask queue中取下一個,執行完畢後,再次將microtask queue中的全部取出;

  3. 循環往復,直到兩個queue中的任務都取完。

浏览器环境中常见的异步任务种类,按照优先级:

  • macro task :同步代码、setImmediateMessageChannelsetTimeout/setInterval

  • micro taskPromise.thenMutationObserver

有的文章把 micro task 叫微任务,macro task 叫宏任务,因为这两个单词拼写太像了 -。- ,所以后面的注释多用中文表示~

先来看看源码中对 micro task macro task 的实现: macroTimerFuncmicroTimerFunc

// src/core/util/next-tick.js

const callbacks = []     // 存放异步执行的回调
let pending = false      // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送

/* 挨个同步执行callbacks中回调 */
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i  {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  MessageChannel.toString() === '[object MessageChannelConstructor]'  // PhantomJS
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
  }
} else {
  microTimerFunc = macroTimerFunc      // fallback to macro
}

flushCallbacks 这个方法就是挨个同步的去执行callbacks中的回调函数们,callbacks中的回调函数是在调用 nextTick 的时候添加进去的;那么怎么去使用 micro taskmacro task 去执行 flushCallbacks 呢,这里他们的实现 macroTimerFuncmicroTimerFunc 使用浏览器中宏任务/微任务的API对flushCallbacks 方法进行了一层包装。比如宏任务方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) },这样在触发宏任务执行的时候 macroTimerFunc() 就可以在浏览器中的下一个宏任务loop的时候消费这些保存在callbacks数组中的回调了,微任务同理。同时也可以看出传给 nextTick 的异步回调函数是被压成了一个同步任务在一个tick执行完的,而不是开启多个异步任务。

注意这里有个比较难理解的地方,第一次调用 nextTick 的时候 pending 为false,此时已经push到浏览器event loop中一个宏任务或微任务的task,如果在没有flush掉的情况下继续往callbacks里面添加,那么在执行这个占位queue的时候会执行之后添加的回调,所以 macroTimerFuncmicroTimerFunc 相当于task queue的占位,以后 pending 为true则继续往占位queue里面添加,event loop轮到这个task queue的时候将一并执行。执行 flushCallbackspending 置false,允许下一轮执行 nextTick 时往event loop占位。

可以看到上面 macroTimerFuncmicroTimerFunc 进行了在不同浏览器兼容性下的平稳退化,或者说降级策略

  1. macroTimerFuncsetImmediate -> MessageChannel -> setTimeout 。首先检测是否原生支持 setImmediate ,这个方法只在 IE、Edge 浏览器中原生实现,然后检测是否支持 MessageChannel,如果对 MessageChannel 不了解可以参考一下这篇文章,还不支持的话最后使用 setTimeout
    为什么优先使用 setImmediate MessageChannel 而不直接使用 setTimeout 呢,是因为HTML5规定setTimeout执行的最小延时为4ms,而嵌套的timeout表现为10ms,为了尽可能快的让回调执行,没有最小延时限制的前两者显然要优于 setTimeout

  2. microTimerFuncPromise.then -> macroTimerFunc 。首先检查是否支持 Promise,如果支持的话通过 Promise.then 来调用 flushCallbacks 方法,否则退化为 macroTimerFunc
    vue2.5之后 nextTick 中因为兼容性原因删除了微任务平稳退化的 MutationObserver 的方式。

2.2 nextTick实现

最后来看看我们平常用到的 nextTick 方法到底是如何实现的:

// src/core/util/next-tick.js

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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

/* 强制使用macrotask的方法 */
export function withMacroTask(fn: Function): Function {
  return fn._withTask || (fn._withTask = function() {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

nextTick 在这里分为三个部分,我们一起来看一下;

  1. 首先 nextTick 把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中,这么做是因为防止单个 cb 如果执行错误不至于让整个JS线程挂掉,每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。

  2. 然后检查 pending 状态,这个跟之前介绍的 queueWatcher 中的 waiting 是一个意思,它是一个标记位,一开始是 false 在进入 macroTimerFuncmicroTimerFunc 方法前被置为 true,因此下次调用 nextTick 就不会进入 macroTimerFuncmicroTimerFunc 方法,这两个方法中会在下一个 macro/micro tick 时候 flushCallbacks 异步的去执行callbacks队列中收集的任务,而 flushCallbacks 方法在执行一开始会把 pendingfalse,因此下一次调用 nextTick 时候又能开启新一轮的 macroTimerFuncmicroTimerFunc,这样就形成了vue中的 event loop

  3. 最后检查是否传入了 cb,因为 nextTick 还支持Promise化的调用:nextTick().then(() => {}),所以如果没有传入 cb 就直接return了一个Promise实例,并且把resolve传递给_resolve,这样后者执行的时候就跳到我们调用的时候传递进 then 的方法中。

Vue源码中 next-tick.js 文件还有一段重要的注释,这里就翻译一下:

在vue2.5之前的版本中,nextTick基本上基于 micro task 来实现的,但是在某些情况下 micro task 具有太高的优先级,并且可能在连续顺序事件之间(例如#4521,#6690)或者甚至在同一事件的事件冒泡过程中之间触发(#6566)。但是如果全部都改成 macro task,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。vue2.5之后版本提供的解决办法是默认使用 micro task,但在需要时(例如在v-on附加的事件处理程序中)强制使用 macro task

为什么默认优先使用 micro task 呢,是利用其高优先级的特性,保证队列中的微任务在一次循环全部执行完毕。

强制 macro task 的方法是在绑定 DOM 事件的时候,默认会给回调的 handler 函数调用 withMacroTask 方法做一层包装 handler = withMacroTask(handler),它保证整个回调函数执行过程中,遇到数据状态的改变,这些改变都会被推到 macro task 中。以上实现在 src/platforms/web/runtime/modules/events.js 的 add 方法中,可以自己看一看具体代码。

刚好在写这篇文章的时候思否上有人问了个问题 vue 2.4 和2.5 版本的@input事件不一样 ,这个问题的原因也是因为2.5之前版本的DOM事件采用 micro task ,而之后采用 macro task,解决的途径参考 中介绍的几个办法,这里就提供一个在mounted钩子中用 addEventListener 添加原生事件的方法来实现,参见 CodePen。

3. 一个例子

说这么多,不如来个例子,执行参见 CodePen

<p>
  <span>{{ name }}</span>
  <button>change name</button>
  </p><p></p>

<script>
  new Vue({
    el: &#39;#app&#39;,
    data() {
      return {
        name: &#39;SHERlocked93&#39;
      }
    },
    methods: {
      change() {
        const $name = this.$refs.name
        this.$nextTick(() => console.log(&#39;setter前:&#39; + $name.innerHTML))
        this.name = &#39; name改喽 &#39;
        console.log(&#39;同步方式:&#39; + this.$refs.name.innerHTML)
        setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
        this.$nextTick(() => console.log(&#39;setter后:&#39; + $name.innerHTML))
        this.$nextTick().then(() => console.log(&#39;Promise方式:&#39; + $name.innerHTML))
      }
    }
  })
</script>

执行以下看看结果:

同步方式:SHERlocked93 
setter前:SHERlocked93 
setter后:name改喽 
Promise方式:name改喽 
setTimeout方式:name改喽

为什么是这样的结果呢,解释一下:

  1. 同步方式: 当把data中的name修改之后,此时会触发name的 setter 中的 dep.notify 通知依赖本data的render watcher去 updateupdate 会把 flushSchedulerQueue 函数传递给 nextTick,render watcher在 flushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的DOM元素还是原来的内容。

  2. setter前: setter前為什麼還要列印原來的是原來內容呢,是因為nextTick 在被呼叫的時候把回調挨個push進callbacks數組,之後執行的時候也是for 循環出來挨個執行,所以是類似於隊列這樣一個概念,先入先出;在修改name之後,觸發把render watcher填入schedulerQueue隊列並且把他的執行函數flushSchedulerQueue 傳遞給nextTick ,此時callbacks隊列中已經有了setter前函數 了,因為這個cb 是在setter前函數 之後被push進callbacks佇列的,那麼先入先出的執行callbacks中回呼的時候先執行setter前函數,這時並未執行render watcher的watcher.run,所以印製DOM元素仍然是原來的內容。

  3. setter後: setter後這時已經執行完flushSchedulerQueue,這時render watcher已經把改變#patch到視圖上,所以此時取得DOM是改過之後的內容。

  4. Promise方式: 相當於 Promise.then 的方式執行這個函數,此時DOM已經改變。

  5. setTimeout方式: 最後執行macro task的任務,此時DOM已經更改。

注意,在執行setter前函數 這個非同步任務之前,同步的程式碼已經執行完畢,異步的任務都還未執行,所有的 $nextTick 函數也執行完畢,所有回呼都被push進了callbacks佇列中等待執行,所以在setter前函數 執行的時候,此時callbacks佇列是這樣的:[ setter前函數flushSchedulerQueuesetter後函數Promise方式函數],它是一個micro task佇列,執行完畢之後執行macro task setTimeout,所以印出上面的結果。

另外,如果瀏覽器的巨集任務佇列裡面有setImmediateMessageChannelsetTimeout/setInterval 各種類型的任務,那麼會依照上面的順序挨個依照加入到event loop中的順序執行,所以如果瀏覽器支援MessageChannelnextTick 執行的是macroTimerFunc,那麼如果macrotask queue中同時有nextTick 新增的任務和使用者自己新增的setTimeout 類型的任務,會優先執行nextTick 中的任務,因為MessageChannel 的優先權比setTimeout的高,setImmediate 同理。

相關推薦:

分析Vue中mixin的使用方法

以上是Vue源碼中批次非同步更新與nextTick原理的解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的演變:當前的趨勢和未來前景JavaScript的演變:當前的趨勢和未來前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

神秘的JavaScript:它的作用以及為什麼重要神秘的JavaScript:它的作用以及為什麼重要Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python還是JavaScript更好?Python還是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

如何安裝JavaScript?如何安裝JavaScript?Apr 05, 2025 am 12:16 AM

JavaScript不需要安裝,因為它已內置於現代瀏覽器中。你只需文本編輯器和瀏覽器即可開始使用。 1)在瀏覽器環境中,通過標籤嵌入HTML文件中運行。 2)在Node.js環境中,下載並安裝Node.js後,通過命令行運行JavaScript文件。

在Quartz中如何在任務開始前發送通知?在Quartz中如何在任務開始前發送通知?Apr 04, 2025 pm 09:24 PM

如何在Quartz中提前發送任務通知在使用Quartz定時器進行任務調度時,任務的執行時間是由cron表達式設定的。現�...

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
3 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境