Vue 原始碼解讀(6)—— 例項方法

李永寧發表於2022-02-28

前言

上一篇文章 Vue 原始碼解讀(5)—— 全域性 API 詳細介紹了 Vue 的各個全域性 API 的實現原理,本篇文章將會詳細介紹各個例項方法的實現原理。

目標

深入理解以下例項方法的實現原理。

  • vm.$set

  • vm.$delete

  • vm.$watch

  • vm.$on

  • vm.$emit

  • vm.$off

  • vm.$once

  • vm._update

  • vm.$forceUpdate

  • vm.$destroy

  • vm.$nextTick

  • vm._render

原始碼解讀

入口

/src/core/instance/index.js

該檔案是 Vue 例項的入口檔案,包括 Vue 建構函式的定義、各個例項方法的初始化。

// Vue 的建構函式
function Vue (options) {
  // 呼叫 Vue.prototype._init 方法,該方法是在 initMixin 中定義的
  this._init(options)
}

// 定義 Vue.prototype._init 方法
initMixin(Vue)
/**
 * 定義:
 *   Vue.prototype.$data
 *   Vue.prototype.$props
 *   Vue.prototype.$set
 *   Vue.prototype.$delete
 *   Vue.prototype.$watch
 */
stateMixin(Vue)
/**
 * 定義 事件相關的 方法:
 *   Vue.prototype.$on
 *   Vue.prototype.$once
 *   Vue.prototype.$off
 *   Vue.prototype.$emit
 */
eventsMixin(Vue)
/**
 * 定義:
 *   Vue.prototype._update
 *   Vue.prototype.$forceUpdate
 *   Vue.prototype.$destroy
 */
lifecycleMixin(Vue)
/**
 * 執行 installRenderHelpers,在 Vue.prototype 物件上安裝執行時便利程式
 * 
 * 定義:
 *   Vue.prototype.$nextTick
 *   Vue.prototype._render
 */
renderMixin(Vue)

vm.$data、vm.$props

src/core/instance/state.js

這是兩個例項屬性,不是例項方法,這裡簡單介紹以下,當然其本身實現也很簡單

// data
const dataDef = {}
dataDef.get = function () { return this._data }
// props
const propsDef = {}
propsDef.get = function () { return this._props }
// 將 data 屬性和 props 屬性掛載到 Vue.prototype 物件上
// 這樣在程式中就可以通過 this.$data 和 this.$props 來訪問 data 和 props 物件了
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

vm.$set

/src/core/instance/state.js

Vue.prototype.$set = set

set

/src/core/observer/index.js

/**
 * 通過 Vue.set 或者 this.$set 方法給 target 的指定 key 設定值 val
 * 如果 target 是物件,並且 key 原本不存在,則為新 key 設定響應式,然後執行依賴通知
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 更新陣列指定下標的元素,Vue.set(array, idx, val),通過 splice 方法實現響應式更新
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 更新物件已有屬性,Vue.set(obj, key, val),執行更新即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // 不能向 Vue 例項或者 $data 新增動態新增響應式屬性,vmCount 的用處之一,
  // this.$data 的 ob.vmCount = 1,表示根元件,其它子元件的 vm.vmCount 都是 0
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // target 不是響應式物件,新屬性會被設定,但是不會做響應式處理
  if (!ob) {
    target[key] = val
    return val
  }
  // 給物件定義新屬性,通過 defineReactive 方法設定響應式,並觸發依賴更新
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

vm.$delete

/src/core/instance/state.js

Vue.prototype.$delete = del

del

/src/core/observer/index.js

/**
 * 通過 Vue.delete 或者 vm.$delete 刪除 target 物件的指定 key
 * 陣列通過 splice 方法實現,物件則通過 delete 運算子刪除指定 key,並執行依賴通知
 */
export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }

  // target 為陣列,則通過 splice 方法刪除指定下標的元素
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__

  // 避免刪除 Vue 例項的屬性或者 $data 的資料
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // 如果屬性不存在直接結束
  if (!hasOwn(target, key)) {
    return
  }
  // 通過 delete 運算子刪除物件的屬性
  delete target[key]
  if (!ob) {
    return
  }
  // 執行依賴通知
  ob.dep.notify()
}

vm.$watch

/src/core/instance/state.js

/**
 * 建立 watcher,返回 unwatch,共完成如下 5 件事:
 *   1、相容性處理,保證最後 new Watcher 時的 cb 為函式
 *   2、標示使用者 watcher
 *   3、建立 watcher 例項
 *   4、如果設定了 immediate,則立即執行一次 cb
 *   5、返回 unwatch
 * @param {*} expOrFn key
 * @param {*} cb 回撥函式
 * @param {*} options 配置項,使用者直接呼叫 this.$watch 時可能會傳遞一個 配置項
 * @returns 返回 unwatch 函式,用於取消 watch 監聽
 */
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // 相容性處理,因為使用者呼叫 vm.$watch 時設定的 cb 可能是物件
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  // options.user 表示使用者 watcher,還有渲染 watcher,即 updateComponent 方法中例項化的 watcher
  options = options || {}
  options.user = true
  // 建立 watcher
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 如果使用者設定了 immediate 為 true,則立即執行一次回撥函式
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回一個 unwatch 函式,用於解除監聽
  return function unwatchFn() {
    watcher.teardown()
  }
}

vm.$on

/src/core/instance/events.js

const hookRE = /^hook:/
/**
 * 監聽例項上的自定義事件,vm._event = { eventName: [fn1, ...], ... }
 * @param {*} event 單個的事件名稱或者有多個事件名組成的陣列
 * @param {*} fn 當 event 被觸發時執行的回撥函式
 * @returns 
 */
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  if (Array.isArray(event)) {
    // event 是有多個事件名組成的陣列,則遍歷這些事件,依次遞迴呼叫 $on
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    // 將註冊的事件和回撥以鍵值對的形式儲存到 vm._event 物件中 vm._event = { eventName: [fn1, ...] }
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    // hookEvent,提供從外部為元件例項注入宣告週期方法的機會
    // 比如從元件外部為元件的 mounted 方法注入額外的邏輯
    // 該能力是結合 callhook 方法實現的
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}

關於 hookEvent,下一篇文章會詳細介紹。

vm.$emit

/src/core/instance/events.js

/**
 * 觸發例項上的指定事件,vm._event[event] => cbs => loop cbs => cb(args)
 * @param {*} event 事件名
 * @returns 
 */
Vue.prototype.$emit = function (event: string): Component {
  const vm: Component = this
  if (process.env.NODE_ENV !== 'production') {
    // 將事件名轉換為小些
    const lowerCaseEvent = event.toLowerCase()
    // 意思是說,HTML 屬性不區分大小寫,所以你不能使用 v-on 監聽小駝峰形式的事件名(eventName),而應該使用連字元形式的事件名(event-name)
    if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
      tip(
        `Event "${lowerCaseEvent}" is emitted in component ` +
        `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
        `Note that HTML attributes are case-insensitive and you cannot use ` +
        `v-on to listen to camelCase events when using in-DOM templates. ` +
        `You should probably use "${hyphenate(event)}" instead of "${event}".`
      )
    }
  }
  // 從 vm._event 物件上拿到當前事件的回撥函式陣列,並一次呼叫陣列中的回撥函式,並且傳遞提供的引數
  let cbs = vm._events[event]
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs
    const args = toArray(arguments, 1)
    const info = `event handler for "${event}"`
    for (let i = 0, l = cbs.length; i < l; i++) {
      invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    }
  }
  return vm
}

vm.$off

/src/core/instance/events.js

/**
 * 移除自定義事件監聽器,即從 vm._event 物件中找到對應的事件,移除所有事件 或者 移除指定事件的回撥函式
 * @param {*} event 
 * @param {*} fn 
 * @returns 
 */
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
  const vm: Component = this
  // vm.$off() 移除例項上的所有監聽器 => vm._events = {}
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }
  // 移除一些事件 event = [event1, ...],遍歷 event 陣列,遞迴呼叫 vm.$off
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
    return vm
  }
  // 除了 vm.$off() 之外,最終都會走到這裡,移除指定事件
  const cbs = vm._events[event]
  if (!cbs) {
    // 表示沒有註冊過該事件
    return vm
  }
  if (!fn) {
    // 沒有提供 fn 回撥函式,則移除該事件的所有回撥函式,vm._event[event] = null
    vm._events[event] = null
    return vm
  }
  // 移除指定事件的指定回撥函式,就是從事件的回撥陣列中找到該回撥函式,然後刪除
  let cb
  let i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1)
      break
    }
  }
  return vm
}

vm.$once

/src/core/instance/events.js

/**
 * 監聽一個自定義事件,但是隻觸發一次。一旦觸發之後,監聽器就會被移除
 * vm.$on + vm.$off
 * @param {*} event 
 * @param {*} fn 
 * @returns 
 */
Vue.prototype.$once = function (event: string, fn: Function): Component {
  const vm: Component = this

  // 呼叫 $on,只是 $on 的回撥函式被特殊處理了,觸發時,執行回撥函式,先移除事件監聽,然後執行你設定的回撥函式
  function on() {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  on.fn = fn
  vm.$on(event, on)
  return vm
}

vm._update

/src/core/instance/lifecycle.js

/**
 * 負責更新頁面,頁面首次渲染和後續更新的入口位置,也是 patch 的入口位置 
 */
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // 首次渲染,即初始化頁面時走這裡
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 響應式資料更新時,即更新頁面時走這裡
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

vm.$forceUpdate

/src/core/instance/lifecycle.js

/**
 * 直接呼叫 watcher.update 方法,迫使元件重新渲染。
 * 它僅僅影響例項本身和插入插槽內容的子元件,而不是所有子元件
 */
Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

vm.$destroy

/src/core/instance/lifecycle.js

/**
 * 完全銷燬一個例項。清理它與其它例項的連線,解綁它的全部指令及事件監聽器。
 */
Vue.prototype.$destroy = function () {
  const vm: Component = this
  if (vm._isBeingDestroyed) {
    // 表示例項已經銷燬
    return
  }
  // 呼叫 beforeDestroy 鉤子
  callHook(vm, 'beforeDestroy')
  // 標識例項已經銷燬
  vm._isBeingDestroyed = true
  // 把自己從老爹($parent)的肚子裡($children)移除
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // 移除依賴監聽
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // 呼叫 __patch__,銷燬節點
  vm.__patch__(vm._vnode, null)
  // 呼叫 destroyed 鉤子
  callHook(vm, 'destroyed')
  // 關閉例項的所有事件監聽
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

vm.$nextTick

/src/core/instance/render.js

Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this)
}

nextTick

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

const callbacks = []
/**
 * 完成兩件事:
 *   1、用 try catch 包裝 flushSchedulerQueue 函式,然後將其放入 callbacks 陣列
 *   2、如果 pending 為 false,表示現在瀏覽器的任務佇列中沒有 flushCallbacks 函式
 *     如果 pending 為 true,則表示瀏覽器的任務佇列中已經被放入了 flushCallbacks 函式,
 *     待執行 flushCallbacks 函式時,pending 會被再次置為 false,表示下一個 flushCallbacks 函式可以進入
 *     瀏覽器的任務佇列了
 * pending 的作用:保證在同一時刻,瀏覽器的任務佇列中只有一個 flushCallbacks 函式
 * @param {*} cb 接收一個回撥函式 => flushSchedulerQueue
 * @param {*} ctx 上下文
 * @returns 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 用 callbacks 陣列儲存經過包裝的 cb 函式
  callbacks.push(() => {
    if (cb) {
      // 用 try catch 包裝回撥函式,便於錯誤捕獲
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 執行 timerFunc,在瀏覽器的任務佇列中(首選微任務佇列)放入 flushCallbacks 函式
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

vm._render

/src/core/instance/render.js

/**
 * 通過執行 render 函式生成 VNode
 * 不過裡面加了大量的異常處理程式碼
 */
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }

  // 設定父 vnode。這使得渲染函式可以訪問佔位符節點上的資料。
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    currentRenderingInstance = vm
    // 執行 render 函式,生成 vnode
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    // 到這兒,說明執行 render 函式時出錯了
    // 開發環境渲染錯誤資訊,生產環境返回之前的 vnode,以防止渲染錯誤導致元件空白
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
      } catch (e) {
        handleError(e, vm, `renderError`)
        vnode = vm._vnode
      }
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // 如果返回的 vnode 是陣列,並且只包含了一個元素,則直接打平
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // render 函式出錯時,返回一個空的 vnode
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' +
        'should return a single root node.',
        vm
      )
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

installRenderHelpers

src/core/instance/render-helpers/index.js

該方法負責在例項上安裝大量和渲染相關的簡寫的工具函式,這些工具函式用在編譯器生成的渲染函式中,比如 v-for 編譯後的 vm._l,還有大家最熟悉的 h 函式(vm._c),不過它沒在這裡宣告,是在 initRender 函式中宣告的。

installRenderHelpers 方法是在 renderMixin 中被呼叫的。

/**
 * 在例項上掛載簡寫的渲染工具函式
 * @param {*} target Vue 例項
 */
export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

如果對某個方法感興趣,可以自行深究。

總結

  • 面試官 問:vm.$set(obj, key, val) 做了什麼?

    vm.$set 用於向響應式物件新增一個新的 property,並確保這個新的 property 同樣是響應式的,並觸發檢視更新。由於 Vue 無法探測物件新增屬性或者通過索引為陣列新增一個元素,比如:this.obj.newProperty = 'val'this.arr[3] = 'val'。所以這才有了 vm.$set,它是 Vue.set 的別名。

    • 為物件新增一個新的響應式資料:呼叫 defineReactive 方法為物件增加響應式資料,然後執行 dep.notify 進行依賴通知,更新檢視

    • 為陣列新增一個新的響應式資料:通過 splice 方法實現


  • 面試官 問:vm.$delete(obj, key) 做了什麼?

    vm.$delete 用於刪除物件上的屬性。如果物件是響應式的,且能確保能觸發檢視更新。該方法主要用於避開 Vue 不能檢測屬性被刪除的情況。它是 Vue.delete 的別名。

    • 刪除陣列指定下標的元素,內部通過 splice 方法來完成

    • 刪除物件上的指定屬性,則是先通過 delete 運算子刪除該屬性,然後執行 dep.notify 進行依賴通知,更新檢視


  • 面試官 問:vm.$watch(expOrFn, callback, [options]) 做了什麼?

    答:

    vm.$watch 負責觀察 Vue 例項上的一個表示式或者一個函式計算結果的變化。當其發生變化時,回撥函式就會被執行,併為回撥函式傳遞兩個引數,第一個為更新後的新值,第二個為老值。

    這裡需要 注意 一點的是:如果觀察的是一個物件,比如:陣列,當你用陣列方法,比如 push 為陣列新增一個元素時,回撥函式被觸發時傳遞的新值和老值相同,因為它們指向同一個引用,所以在觀察一個物件並且在回撥函式中有新老值是否相等的判斷時需要注意。

    vm.$watch 的第一個引數只接收簡單的響應式資料的鍵路徑,對於更復雜的表示式建議使用函式作為第一個引數。

    至於 vm.$watch 的內部原理是:

    • 設定 options.user = true,標誌是一個使用者 watcher

    • 例項化一個 Watcher 例項,當檢測到資料更新時,通過 watcher 去觸發回撥函式的執行,並傳遞新老值作為回撥函式的引數

    • 返回一個 unwatch 函式,用於取消觀察


  • 面試官 問:vm.$on(event, callback) 做了什麼?

    監聽當前例項上的自定義事件,事件可由 vm.$emit 觸發,回撥函式會接收所有傳入事件觸發函式(vm.$emit)的額外引數。

    vm.$on 的原理很簡單,就是處理傳遞的 event 和 callback 兩個引數,將註冊的事件和回撥函式以鍵值對的形式儲存到 vm._event 物件中,vm._events = { eventName: [cb1, cb2, ...], ... }。


  • 面試官 問:vm.$emit(eventName, [...args]) 做了什麼?

    觸發當前例項上的指定事件,附加引數都會傳遞給事件的回撥函式。

    其內部原理就是執行 vm._events[eventName] 中所有的回撥函式。

    備註:從 $on 和 $emit 的實現原理也能看出,元件的自定義事件其實是誰觸發誰監聽,所以在這會兒再回頭看 Vue 原始碼解讀(2)—— Vue 初始化過程 中關於 initEvent 的解釋就會明白在說什麼,因為元件自定義事件的處理內部用的就是 vm.$on、vm.$emit。


  • 面試官 問:vm.$off([event, callback]) 做了什麼?

    移除自定義事件監聽器,即移除 vm._events 物件上相關資料。

    • 如果沒有提供引數,則移除例項的所有事件監聽

    • 如果只提供了 event 引數,則移除例項上該事件的所有監聽器

    • 如果兩個引數都提供了,則移除例項上該事件對應的監聽器


  • 面試官 問:vm.$once(event, callback) 做了什麼?

    監聽一個自定義事件,但是該事件只會被觸發一次。一旦觸發以後監聽器就會被移除。

    其內部的實現原理是:

    • 包裝使用者傳遞的回撥函式,當包裝函式執行的時候,除了會執行使用者回撥函式之外還會執行 vm.$off(event, 包裝函式) 移除該事件

    • vm.$on(event, 包裝函式) 註冊事件


  • 面試官 問:vm._update(vnode, hydrating) 做了什麼?

    官方文件沒有說明該 API,這是一個用於原始碼內部的例項方法,負責更新頁面,是頁面渲染的入口,其內部根據是否存在 prevVnode 來決定是首次渲染,還是頁面更新,從而在呼叫 __patch__ 函式時傳遞不同的引數。該方法在業務開發中不會用到。


  • 面試官 問:vm.$forceUpdate() 做了什麼?

    迫使 Vue 例項重新渲染,它僅僅影響元件例項本身和插入插槽內容的子元件,而不是所有子元件。其內部原理到也簡單,就是直接呼叫 vm._watcher.update(),它就是 watcher.update() 方法,執行該方法觸發元件更新。


  • 面試官 問:vm.$destroy() 做了什麼?

    負責完全銷燬一個例項。清理它與其它例項的連線,解綁它的全部指令和事件監聽器。在執行過程中會呼叫 beforeDestroydestroy 兩個鉤子函式。在大多數業務開發場景下用不到該方法,一般都通過 v-if 指令來操作。其內部原理是:

    • 呼叫 beforeDestroy 鉤子函式

    • 將自己從老爹肚子裡($parent)移除,從而銷燬和老爹的關係

    • 通過 watcher.teardown() 來移除依賴監聽

    • 通過 vm.__patch__(vnode, null) 方法來銷燬節點

    • 呼叫 destroyed 鉤子函式

    • 通過 vm.$off 方法移除所有的事件監聽


  • 面試官 問:vm.$nextTick(cb) 做了什麼?

    vm.$nextTick 是 Vue.nextTick 的別名,其作用是延遲迴調函式 cb 的執行,一般用於 this.key = newVal 更改資料後,想立即獲取更改過後的 DOM 資料:

    this.key = 'new val'
    
    Vue.nextTick(function() {
      // DOM 更新了
    })
    

    其內部的執行過程是:

    • this.key = 'new val',觸發依賴通知更新,將負責更新的 watcher 放入 watcher 佇列

    • 將重新整理 watcher 佇列的函式放到 callbacks 陣列中

    • 在瀏覽器的非同步任務佇列中放入一個重新整理 callbacks 陣列的函式

    • vm.$nextTick(cb) 來插隊,直接將 cb 函式放入 callbacks 陣列

    • 待將來的某個時刻執行重新整理 callbacks 陣列的函式

    • 然後執行 callbacks 陣列中的眾多函式,觸發 watcher.run 的執行,更新 DOM

    • 由於 cb 函式是在後面放到 callbacks 陣列,所以這就保證了先完成的 DOM 更新,再執行 cb 函式


  • 面試官 問:vm._render 做了什麼?

    官方文件沒有提供該方法,它是一個用於原始碼內部的例項方法,負責生成 vnode。其關鍵程式碼就一行,執行 render 函式生成 vnode。不過其中加了大量的異常處理程式碼。

連結

感謝各位的:點贊收藏評論,我們下期見。


當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊收藏評論

新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

相關文章