中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Vue異步更新機制和nextTick原理實例分析

發布時間:2022-02-24 15:51:50 來源:億速云 閱讀:154 作者:iii 欄目:開發技術

這篇文章主要介紹“Vue異步更新機制和nextTick原理實例分析”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Vue異步更新機制和nextTick原理實例分析”文章能幫助大家解決問題。

1. 異步更新

 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 < queue.length; index++) {   // 不要將length進行緩存
    watcher = queue[index]
    if (watcher.before) { // 如果watcher有before則執行
      watcher.before()
    }
    id = watcher.id
    has[id] = null // 將has的標記刪除
    watcher.run() // 執行watcher
    if (process.env.NODE_ENV !== 'production' && has[id] != null) { // 在dev環境下檢查是否進入死循環
      circular[id] = (circular[id] || 0) + 1 // 比如user watcher訂閱自己的情況
      if (circular[id] > 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 < queue.length 這里沒有將 length 進行緩存,因為在執行處理現有 watcher 對象期間,更多的 watcher 對象可能會被 push 進 queue。

那么數據的修改從 model 層反映到 view 的過程:數據更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新視圖

2. nextTick原理

2.1 宏任務/微任務

這里就來看看包含著每個 watcher 執行的方法被作為回調傳入 nextTick 之后,nextTick對這個方法做了什么。不過首先要了解一下瀏覽器中的 EventLoopmacro taskmicro task幾個概念

解釋一下,當主線程執行完同步任務后:

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

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

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

瀏覽器環境中常見的異步任務種類,按照優先級:

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

  • micro taskPromise.thenMutationObserver

有的文章把 micro task 叫微任務,macro task 叫宏任務,因為這兩個單詞拼寫太像了 -。- ,所以后面的注釋多用中文表示~

先來看看源碼中對 micro taskmacro 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 < copies.length; i++) {
    copies[i]()
  }
}


let microTimerFunc // 微任務執行方法
let macroTimerFunc // 宏任務執行方法
let useMacroTask = false // 是否強制為宏任務,默認使用微任務


// 宏任務
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    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;為什么優先使用 setImmediateMessageChannel 而不直接使用 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 方法中,可以自己看一看具體代碼。

3. 一個例子

說這么多,不如來個例子,執行參見 CodePen

<div id="app">
  <span id='name' ref='name'>{{ name }}</span>
  <button @click='change'>change name</button>
  <div id='content'></div>
</div>
<script>
  new Vue({
    el: '#app',
    data() {
      return {
        name: 'SHERlocked93'
      }
    },
    methods: {
      change() {
        const $name = this.$refs.name
        this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
        this.name = ' name改嘍 '
        console.log('同步方式:' + this.$refs.name.innerHTML)
        setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
        this.$nextTick(() => console.log('setter后:' + $name.innerHTML))
        this.$nextTick().then(() => console.log('Promise方式:' + $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異步更新機制和nextTick原理實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

昂仁县| 上林县| 化隆| 怀化市| 彰化县| 阳原县| 铅山县| 巴彦县| 交口县| 甘孜县| 邹城市| 临朐县| 赤城县| 建昌县| 自治县| 寻乌县| 三亚市| 宁河县| 托克托县| 石泉县| 若羌县| 柳林县| 九江市| 开平市| 弥渡县| 类乌齐县| 会宁县| 收藏| 志丹县| 重庆市| 南城县| 开鲁县| 苍溪县| 通榆县| 陵川县| 维西| 门头沟区| 贵德县| 孟州市| 涿州市| 宁津县|