深度解析 Vue 響應式原理


深度解析 Vue 響應式原理

該文章內容節選自團隊的開源項目 InterviewMap。項目目前內容包含了 JS、網絡、瀏覽器相關、性能優化、安全、框架、Git、數據結構、算法等內容,無論是基礎還是進階,亦或是源碼解讀,你都能在本圖譜中得到滿意的答案,希望這個面試圖譜能夠幫助到大家更好的准備面試。

Vue 初始化

在 Vue 的初始化中,會先對 props 和 data 進行初始化

Vue.prototype._init = function(options?: Object) { // ... // 初始化 props 和 data initState(vm) initProvide(vm) callHook(vm, 'created') if (vm.$options.el) { // 掛載組件 vm.$mount(vm.$options.el) } } 復制代碼

接下來看下如何初始化 props 和 data

export function initState (vm: Component) { // 初始化 props if (opts.props) initProps(vm, opts.props) if (opts.data) { // 初始化 data initData(vm) } } function initProps (vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {} const props = vm._props = {} // 緩存 key const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent // 非根組件的 props 不需要觀測 if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) // 驗證 prop const value = validateProp(key, propsOptions, propsData, vm) // 通過 defineProperty 函數實現雙向綁定 defineReactive(props, key, value) // 可以讓 vm._props.x 通過 vm.x 訪問 if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) } function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (props && hasOwn(props, key)) { } else if (!isReserved(key)) { // 可以讓 vm._data.x 通過 vm.x 訪問 proxy(vm, `_data`, key) } } // 監聽 data observe(data, true /* asRootData */) } export function observe (value: any, asRootData: ?boolean): Observer | void { // 如果 value 不是對象或者使 VNode 類型就返回 if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 使用緩存的對象 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 創建一個監聽者 ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 // 通過 defineProperty 為對象添加 __ob__ 屬性,並且配置為不可枚舉 // 這樣做的意義是對象遍歷時不會遍歷到 __ob__ 屬性 def(value, '__ob__', this) // 判斷類型,不同的類型不同處理 if (Array.isArray(value)) { // 判斷數組是否有原型 // 在該處重寫數組的一些方法,因為 Object.defineProperty 函數 // 對於數組的數據變化支持的不好,這部分內容會在下面講到 const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } // 遍歷對象,通過 defineProperty 函數實現雙向綁定 walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } // 遍歷數組,對每一個元素進行觀測 observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } 復制代碼

Object.defineProperty

無論是對象還是數組,需要實現雙向綁定的話最終都會執行這個函數,該函數可以監聽到 set 和 get 的事件。

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 創建依賴實例,通過閉包的方式讓 // set get 函數使用 const dep = new Dep() // 獲得屬性對象 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 獲取自定義的 getter 和 setter const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 如果 val 是對象的話遞歸監聽 let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // 攔截 getter,當取值時會觸發該函數 get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 進行依賴收集 // 初始化時會在初始化渲染 Watcher 時訪問到需要雙向綁定的對象 // 從而觸發 get 函數 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, // 攔截 setter,當賦值時會觸發該函數 set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 判斷值是否發生變化 if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } // 如果新值是對象的話遞歸監聽 childOb = !shallow && observe(newVal) // 派發更新 dep.notify() } }) } 復制代碼

在 Object.defineProperty 中自定義 get 和 set 函數,並在 get 中進行依賴收集,在 set 中派發更新。接下來我們先看如何進行依賴收集。

依賴收集

依賴收集是通過 Dep 來實現的,但是也與 Watcher 息息相關

export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } // 添加觀察者 addSub (sub: Watcher) { this.subs.push(sub) } // 移除觀察者 removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) {、 // 調用 Watcher 的 addDep 函數 Dep.target.addDep(this) } } // 派發更新 notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } // 同一時間只有一個觀察者使用,賦值觀察者 Dep.target = null 復制代碼

對於 Watcher 來說,分為兩種 Watcher,分別為渲染 Watcher 和用戶寫的 Watcher。渲染 Watcher 是在初始化中實例化的。

export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... let updateComponent if (process.env.NODE_ENV !== 'production' && config.performance && mark) {} else { // 組件渲染,該回調會在初始化和數據變化時調用 updateComponent = () => { vm._update(vm._render(), hydrating) } } // 實例化渲染 Watcher new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */ ) return vm } 復制代碼

接下來看一下 Watcher 的部分實現

export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { // ... if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } get () { // 該函數用於緩存 Watcher // 因為在組件含有嵌套組件的情況下,需要恢復父組件的 Watcher pushTarget(this) let value const vm = this.vm try { // 調用回調函數,也就是 updateComponent 函數 // 在這個函數中會對需要雙向綁定的對象求值,從而觸發依賴收集 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } // 恢復 Watcher popTarget() // 清理依賴,判斷是否還需要某些依賴,不需要的清除 // 這是為了性能優化 this.cleanupDeps() } return value } // 在依賴收集中調用 addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { // 調用 Dep 中的 addSub 函數 // 將當前 Watcher push 進數組 dep.addSub(this) } } } } export function pushTarget (_target: ?Watcher) { // 設置全局的 target if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() } 復制代碼

以上就是依賴收集的全過程。核心流程是先對配置中的 props 和 data 中的每一個值調用 Obeject.defineProperty() 來攔截 set 和 get 函數,再在渲染 Watcher 中訪問到模板中需要雙向綁定的對象的值觸發依賴收集。

派發更新

改變對象的數據時,會觸發派發更新,調用 Dep 的 notify 函數

notify () {
  // 執行 Watcher 的 update const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } update () { if (this.computed) { // ... } else if (this.sync) { // ... } else { // 一般會進入這個條件 queueWatcher(this) } } export function queueWatcher(watcher: Watcher) { // 獲得 id const id = watcher.id // 判斷 Watcher 是否 push 過 // 因為存在改變了多個數據,多個數據的 Watch 是同一個 if (has[id] == null) { has[id] = true if (!flushing) { // 最初會進入這個條件 queue.push(watcher) } else { // 在執行 flushSchedulerQueue 函數時,如果有新的派發更新會進入這里 // 插入新的 watcher let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // 最初會進入這個條件 if (!waiting) { waiting = true // 將所有 Watcher 統一放入 nextTick 調用 // 因為每次派發更新都會引發渲染 nextTick(flushSchedulerQueue) } } } function flushSchedulerQueue() { flushing = true let watcher, id // 根據 id 排序 watch,確保如下條件 // 1. 組件更新從父到子 // 2. 用戶寫的 Watch 先於渲染 Watch // 3. 如果在父組件 watch run 的時候有組件銷毀了,這個 Watch 可以被跳過 queue.sort((a, b) => a.id - b.id) // 不緩存隊列長度,因為在遍歷的過程中可能隊列的長度發生變化 for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { // 執行 beforeUpdate 鈎子函數 watcher.before() } id = watcher.id has[id] = null // 在這里執行用戶寫的 Watch 的回調函數並且渲染組件 watcher.run() // 判斷無限循環 // 比如在 watch 中又重新給對象賦值了,就會出現這個情況 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 } } } // ... } 復制代碼

以上就是派發更新的全過程。核心流程就是給對象賦值,觸發 set 中的派發更新函數。將所有 Watcher 都放入 nextTick 中進行更新,nextTick 回調中執行用戶 Watch 的回調函數並且渲染組件。

Object.defineProperty 的缺陷

以上已經分析完了 Vue 的響應式原理,接下來說一點 Object.defineProperty 中的缺陷。

如果通過下標方式修改數組數據或者給對象新增屬性並不會觸發組件的重新渲染,因為 Object.defineProperty 不能攔截到這些操作,更精確的來說,對於數組而言,大部分操作都是攔截不到的,只是 Vue 內部通過重寫函數的方式解決了這個問題。

對於第一個問題,Vue 提供了一個 API 解決

export function set (target: Array<any> | Object, key: any, val: any): any { // 判斷是否為數組且下標是否有效 if (Array.isArray(target) && isValidArrayIndex(key)) { // 調用 splice 函數觸發派發更新 // 該函數已被重寫 target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } // 判斷 key 是否已經存在 if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ 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 } // 如果對象不是響應式對象,就賦值返回 if (!ob) { target[key] = val return val } // 進行雙向綁定 defineReactive(ob.value, key, val) // 手動派發更新 ob.dep.notify() return val } 復制代碼

對於數組而言,Vue 內部重寫了以下函數實現派發更新

// 獲得數組原型 const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) // 重寫以下函數 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 緩存原生函數 const original = arrayProto[method] // 重寫函數 def(arrayMethods, method, function mutator (...args) { // 先調用原生函數獲得結果 const result = original.apply(this, args) const ob = this.__ob__ let inserted // 調用以下幾個函數時,監聽新數據 switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 手動派發更新 ob.dep.notify() return result }) }) 復制代碼

求職

最近本人在尋找工作機會,如果有杭州的不錯崗位的話,歡迎聯系我 zx597813039@gmail.com。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM