深度解析 Vue 響應式原理

深度解析 Vue 響應式原理

該文章內容節選自團隊的開源項目 InterviewMap。項目目前內容包含了 JS、網絡、瀏覽器相關、性能優化、安全、框架、Git、數據結構、算法等內容,不管是基礎仍是進階,亦或是源碼解讀,你都能在本圖譜中獲得滿意的答案,但願這個面試圖譜可以幫助到你們更好的準備面試。html

Vue 初始化

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

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 和 datareact

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 的事件。git

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 中派發更新。接下來咱們先看如何進行依賴收集。github

依賴收集

依賴收集是經過 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 的部分實現express

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。

相關文章
相關標籤/搜索