Vue 源碼解讀(6)—— 實例方法
- 2022 年 2 月 28 日
- 筆記
- 精通 Vue 技術棧的源碼原理
前言
上一篇文章 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() 做了什麼?
答:
負責完全銷毀一個實例。清理它與其它實例的連接,解綁它的全部指令和事件監聽器。在執行過程中會調用
beforeDestroy
和destroy
兩個鉤子函數。在大多數業務開發場景下用不到該方法,一般都通過 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。不過其中加了大量的異常處理程式碼。
鏈接
- 配套影片,微信公眾號回復:”精通 Vue 技術棧源碼原理影片版” 獲取
- 精通 Vue 技術棧源碼原理 專欄
- github 倉庫 liyongning/Vue 歡迎 Star
感謝各位的:點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊、收藏和評論。
新影片和文章會第一時間在微信公眾號發送,歡迎關註:李永寧lyn
文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。