Vue源碼解讀(四):更新策略

之前介紹過初始化時 Vue 對數(shù)據(jù)的響應(yīng)式處理是利用了Object.defifineProperty()敛惊,通過定義對象屬性 getter 方法攔截對象屬性的訪問,進(jìn)行依賴的收集凸克,依賴收集的作用就是在數(shù)據(jù)變更的時候能通知到相關(guān)依賴進(jìn)行更新。

通知更新

setter

當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變更時闷沥,會觸發(fā)攔截的 setter 函數(shù)萎战,先來看看 setter :

// src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // ...
    // 劫持修改操作
    set: function reactiveSetter (newVal) {
      // 舊的 obj[key] 
      const value = getter ? getter.call(obj) : val
      // 如果新舊值一樣,則直接 return舆逃,無需更新
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // setter 不存在說明該屬性是一個只讀屬性蚂维,直接 return
      if (getter && !setter) return
      // 設(shè)置新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 對新值進(jìn)行觀察,讓新值也是響應(yīng)式的
      childOb = !shallow && observe(newVal)
      // 依賴通知更新
      dep.notify()
    }
  })
}

dep.notify()

  // src/core/observer/dep.js
  // 通知更新
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

遍歷 dep 中存儲的 watcher路狮,執(zhí)行 watcher.update()虫啥。

watcher.update()

// src/core/observer/watcher.js
export default class Watcher {
  // ...
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // 懶執(zhí)行時走這里,比如 computed watcher
      // 將 dirty 置為 true奄妨,計算屬性的求值就會重新計算
      this.dirty = true
    } else if (this.sync) {
      // 同步執(zhí)行涂籽,在使用 vm.$watch 或者 watch 選項時可以傳一個 sync 選項,
      // 當(dāng)為 true 時在數(shù)據(jù)更新時該 watcher 就不走異步更新隊列砸抛,直接執(zhí)行 this.run方法進(jìn)行更新
      // 這個屬性在官方文檔中沒有出現(xiàn)
      this.run()
    } else {
      // 更新時一般都這里评雌,將 watcher 放入 watcher 隊列
      queueWatcher(this)
    }
  }
}

queueWatcher

// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
 * 將 watcher 放入 queue 隊列
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果 watcher 已經(jīng)存在,則跳過
  if (has[id] == null) {
    // 緩存 watcher.id直焙,用于判斷 watcher 是否已經(jīng)入隊
    has[id] = true
    if (!flushing) {
      // 當(dāng)前沒有處于刷新隊列狀態(tài)景东,watcher 直接入隊
      queue.push(watcher)
    } else {
      // 正在刷新隊列,這時用戶可能添加新的 watcher,就會走到這里
      // 從后往前找奔誓,找到第一個 watcher.id 比當(dāng)前隊列中 watcher.id 大的位置斤吐,然后將自己插入到該位置。保持隊列是有序的。
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // waiting 保證了 nextTick 的調(diào)用只有一次
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        // 直接刷新調(diào)度隊列
        // 一般不會走這兒曲初,Vue 默認(rèn)是異步執(zhí)行,如果改為同步執(zhí)行杯聚,性能會大打折扣
        flushSchedulerQueue()
        return
      }
      // nextTick => vm.$nextTick臼婆、Vue.nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}

nextTick 等會再看,它的作用主要就是把 flushSchedulerQueue 使用異步任務(wù)去執(zhí)行幌绍,先嘗試用微任務(wù)颁褂,不支持的情況再用宏任務(wù)去執(zhí)行。

那么先看看 flushSchedulerQueue 的作用:

flushSchedulerQueue

// src/core/observer/scheduler.js
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // 對隊列做了從小到大的排序傀广,目的:
  // 1. 組件的更新由父到子,因為父組件在子組件之前被創(chuàng)建,所以 watcher 的創(chuàng)建也是先父后子颁独,執(zhí)行順序也應(yīng)該保持先父后子。
  // 2. 一個組件的用戶 watcher 先于渲染 watcher 執(zhí)行伪冰,以為用戶 watcher 創(chuàng)建先于渲染 watcher誓酒。
  // 3. 如果一個組件在父組件的 watcher 執(zhí)行期間被銷毀,那么它對應(yīng)的 watcher 執(zhí)行都可以被跳過贮聂,所以父組件的 watcher 應(yīng)該先執(zhí)行靠柑。
  queue.sort((a, b) => a.id - b.id)
  // 在遍歷的時候每次都會對 queue.length 求值,因為在 watcher.run() 的時候吓懈,很可能用戶會再次添加新的 watcher
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // 執(zhí)行 beforeUpdate 生命周期鉤子歼冰,在 mount 階段創(chuàng)建 Watcher 時傳入
    if (watcher.before) {
      watcher.before()
    }
    // 將緩存的 watcher 清除
    id = watcher.id
    has[id] = null
    // 執(zhí)行 watcher.run,最終觸發(fā)更新函數(shù)
    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
      }
    }
  }
  // 在重置狀態(tài)之前保留隊列的副本
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  //重置刷新隊列狀態(tài)
  resetSchedulerState()
  // keep-alive 組件相關(guān)
  callActivatedHooks(activatedQueue)
  // 執(zhí)行 updated 生命周期鉤子
  callUpdatedHooks(updatedQueue)
  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

/**
 * 把這些控制流程狀態(tài)的一些變量恢復(fù)到初始值耻警,把 watcher 隊列清空隔嫡。
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

/**
 * 由子組件到父組件依次執(zhí)行 updated 生命周期鉤子
 */
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

上面代碼可以看出 flushSchedulerQueue 的作用就是執(zhí)行更新隊列。通過 watcher.run() 觸發(fā)最終的更新甘穿。

watcher.run()

// src/core/observer/watcher.js
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.cb = cb
  } 
  run () {
    if (this.active) {
      // 調(diào)用 this.get 方法
      const value = this.get()
      if (
        value !== this.value ||  // 新舊值不相等
        isObject(value) ||   // 新值是對象
        this.deep   // deep模式
      ) {
        // 更新舊值為新值
        const oldValue = this.value
        this.value = value
        if (this.user) {
          // 如果是用戶 watcher
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          // 渲染 watcher腮恩,this.cb = noop,一個空函數(shù)
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

這里有兩種情況扒磁,當(dāng) this.usertrue 的時候代表用戶 watcher庆揪,在之前介紹過也就是 user watcher , 否則執(zhí)行渲染 watcher 的邏輯。

  • user watcher

invokeWithErrorHandling 接收的第一個參數(shù)就是我們自定義偵聽屬性的回調(diào)函數(shù)妨托,在初始化偵聽屬性 initWatch 方法過程中缸榛,實例化 new Watcher(vm, expOrFn, cb, options) 的時候傳入。
第三個參數(shù)就是 [value, oldValue] (新值和舊值)兰伤,這也就是為什么在偵聽屬性的回調(diào)函數(shù)中能獲得新值和舊值内颗。

// src/core/util/error.js
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  // 利用 try catch 做一些錯誤處理
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
  • 渲染 watcher

如果是渲染 watcher 則執(zhí)行 this.cb.call(this.vm, value, oldValue)。渲染 Wather 的實例化是在掛載時 mountComponent 方法中執(zhí)行的:

  //  src/core/instance/lifecycle.js
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

export function noop (a?: any, b?: any, c?: any) {} 是一個空函數(shù)敦腔,所以 this.cb.call(this.vm, value, oldValue)均澳,就是在執(zhí)行一個空函數(shù)。

渲染 watcher 在執(zhí)行 watcher.run 會調(diào)用 this.get() ,也就會執(zhí)行 this.getter.call(vm, vm)找前。this.getter 實際就是實例化時傳入的第二個參數(shù) updateComponent 糟袁。

//  src/core/instance/lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

所以這就是當(dāng)我們?nèi)バ薷慕M件相關(guān)的響應(yīng)式數(shù)據(jù)的時候,會觸發(fā)組件重新渲染的原因躺盛,接著就會進(jìn)入 patch 的過程项戴。

nextTick

前面介紹了 flushSchedulerQueue 的作用就是去執(zhí)行更新隊列,那么我們看看 queueWatcher 中的這段代碼是怎么回事:

nextTick(flushSchedulerQueue)

nextTick

// src/core/util/next-tick.js
const callbacks = []
let pending = false

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 用 callbacks 數(shù)組存儲經(jīng)過包裝的 cb 函數(shù)
  callbacks.push(() => {
    if (cb) {
      // 用 try catch 包裝回調(diào)函數(shù)槽惫,便于錯誤捕獲
      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 第一個參數(shù)是一個回調(diào)函數(shù)周叮,這里的回調(diào)函數(shù)對應(yīng)的就是 flushSchedulerQueue 了。通過 try catch 將回調(diào)函數(shù)包裝界斜,用于錯誤捕獲仿耽,然后將其放入 callbacks 中。

這里使用 callbacks 而不是直接在 nextTick 中執(zhí)行回調(diào)函數(shù)的原因是保證在同一個 tick 內(nèi)多次執(zhí)行 nextTick各薇,不會開啟多個異步任務(wù)项贺,而把這些異步任務(wù)都壓成一個同步任務(wù),在下一個 tick 執(zhí)行完畢得糜。

接下來當(dāng) pendingfalse 的時候執(zhí)行 timerFunc 敬扛,pendingtrue,表示正在將任務(wù)放入瀏覽器的任務(wù)隊列中朝抖;pendingfalse 啥箭,表示任務(wù)已經(jīng)放入瀏覽器任務(wù)隊列中了。

最后治宣,nextTick 在沒有傳入 cb 回調(diào)函數(shù)的時候急侥,會返回 promise,提供了一個 .then 的調(diào)用侮邀。

nextTick().then(() => {})

timerFunc

// src/core/util/next-tick.js
// 可以看到 timerFunc 的作用很簡單坏怪,就是將 flushCallbacks 函數(shù)放入瀏覽器的異步任務(wù)隊列中
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 首選 Promise
    p.then(flushCallbacks)
    /**
     * 在有問題的UIWebViews中,Promise.then不會完全中斷绊茧,但是它可能會陷入怪異的狀態(tài)铝宵,
     * 在這種狀態(tài)下,回調(diào)被推入微任務(wù)隊列华畏,但隊列沒有被刷新鹏秋,直到瀏覽器需要執(zhí)行其他工作,例如處理一個計時器亡笑。
     * 因此侣夷,我們可以通過添加空計時器來“強制”刷新微任務(wù)隊列。
     */
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 然后使用 MutationObserver
  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)) {
  // 然后 setImmediate仑乌,宏任務(wù)
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最后 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

flushCallbacks

// src/core/util/next-tick.js
/**
 *   1百拓、將 pending 置為 false
 *   2琴锭、清空 callbacks 數(shù)組
 *   3、執(zhí)行 callbacks 數(shù)組中的每一個函數(shù)(比如 flushSchedulerQueue衙传、用戶調(diào)用 nextTick 傳遞的回調(diào)函數(shù))
 */
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

不管是全局 API Vue.nextTick决帖,還是實例方法 vm.$nextTick,最后都是調(diào)用 next-tick.js 中的 nextTick 方法蓖捶。

相關(guān)鏈接

Vue源碼解讀(預(yù)):手寫一個簡易版Vue

Vue源碼解讀(一):準(zhǔn)備工作

Vue源碼解讀(二):初始化和掛載

Vue源碼解讀(三):響應(yīng)式原理

Vue源碼解讀(四):更新策略

Vue源碼解讀(五):render和VNode

Vue源碼解讀(六):update和patch

Vue源碼解讀(七):模板編譯

如果覺得還湊合的話古瓤,給個贊吧!O傺簟!也可以來我的個人博客逛逛 https://www.mingme.net/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末穿香,一起剝皮案震驚了整個濱河市亭引,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌皮获,老刑警劉巖焙蚓,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異洒宝,居然都是意外死亡购公,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門雁歌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宏浩,“玉大人,你說我怎么就攤上這事靠瞎”茸” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵乏盐,是天一觀的道長佳窑。 經(jīng)常有香客問我,道長父能,這世上最難降的妖魔是什么神凑? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮何吝,結(jié)果婚禮上溉委,老公的妹妹穿的比我還像新娘。我一直安慰自己岔霸,他們只是感情好薛躬,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呆细,像睡著了一般型宝。 火紅的嫁衣襯著肌膚如雪八匠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天趴酣,我揣著相機與錄音梨树,去河邊找鬼。 笑死岖寞,一個胖子當(dāng)著我的面吹牛抡四,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仗谆,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼兑凿,長吁一口氣:“原來是場噩夢啊……” “哼越走!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捧请,失蹤者是張志新(化名)和其女友劉穎蘸嘶,沒想到半個月后胆描,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盼铁,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年蹋偏,在試婚紗的時候發(fā)現(xiàn)自己被綠了便斥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡威始,死狀恐怖枢纠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情黎棠,我是刑警寧澤京郑,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站葫掉,受9級特大地震影響些举,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜俭厚,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一户魏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挪挤,春花似錦叼丑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至论寨,卻和暖如春星立,著一層夾襖步出監(jiān)牢的瞬間爽茴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工绰垂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留室奏,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓劲装,卻偏偏與公主長得像胧沫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子占业,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354