vue響應式原理學習(二)— Observer的實現
之前我的一篇文章ofollow,noindex">vue響應式原理學習(一)
講述了vue資料響應式原理的一些簡單知識。
眾所周知,Vue
的data
屬性,是預設深度監聽的,這次我們再深度分析下,Observer
觀察者的原始碼實現。
先寫個深拷貝熱熱身
既然data
屬性是被深度監聽,那我們就首先自己實現一個簡單的深拷貝,理解下思路。
深拷貝的原理有點像遞迴, 其實就是遇到引用型別,呼叫自身函式再次解析。
function deepCopy(source) { // 型別校驗,如果不是引用型別 或 全等於null,直接返回 if (source === null || typeof source !== 'object') { return source; } let isArray = Array.isArray(source), result = isArray ? [] : {}; // 遍歷屬性 if (isArray) { for(let i = 0, len = source.length; i < len; i++) { let val = source[i]; // typeof [] === 'object', typeof {} === 'object' // 考慮到 typeof null === 'object' 的情況, 所以要加個判斷 if (val && typeof val === 'object') { result[i] = deepCopy(val); } else { result[i] = val; } } // 簡寫 // result = source.map(item => { //return (item && typeof item === 'object') ? deepCopy(item) : item // }); } else { const keys = Object.keys(source); for(let i = 0, len = keys.length; i < len; i++) { let key = keys[i], val = source[key]; if (val && typeof val === 'object') { result[key] = deepCopy(val); } else { result[key] = val; } } // 簡寫 // keys.forEach((key) => { //let val = source[key]; //result[key] = (val && typeof val === 'object') ? deepCopy(val) : val; // }); } return result; } 複製程式碼
為什麼是簡單的深拷貝,因為沒考慮 RegExp, Date, 原型鏈,DOM/BOM物件等等。要寫好一個深拷貝,不簡單。
有的同學可能會問,為什麼不直接一個for in
解決。如下:
function deepCopy(source) { let result = Array.isArray(source) ? [] : {}; // 遍歷物件 for(let key in source) { let val = source[key]; result[key] = (val && typeof val === 'object') ? deepCopy(val) : val; } return result; } 複製程式碼
其實for in
有一個痛點就是原型鏈上的非內建方法
也會被遍歷。例如開發者自己在物件的prototype
上擴充套件的方法。
又有的同學可能會說,加hasOwnProperty
解決呀。如果是Object
型別,確實可以解決,但如何是Array
的話,就獲取不到陣列的索引啦。
說到for in
,再加個注意項,就是for in
也是可以continue
的,而陣列的forEach
方法不可以。因為forEach
的內部實現是在一個for
迴圈中依次執行你傳入的函式。
分析 Vue 的 Observer
這裡我主要是為程式碼添加註釋,建議看官們最好打開源碼來看。
程式碼來源:Vue專案下的src/core/observer/index.js
Vue 將Observer
封裝成了一個class
Observer
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 // 每觀察一個物件,就在物件上新增 __ob__ 屬性,值為當前 Observer 例項 // 當然,前提是 value 本身是一個數組或物件,而非基礎資料型別,如數字,字串等。 def(value, '__ob__', this) // 如果是陣列 if (Array.isArray(value)) { // 這兩行程式碼後面再講解 // 這裡程式碼的作用是 為陣列的操作函式賦能 // 也就是,當我們使用 push pop splice 等陣列的api時,也可以觸發資料響應,更新檢視。 const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) // 遍歷陣列並觀察 this.observeArray(value) } else { // 遍歷物件並觀察 // 這裡會有存在 value 不是 Object 的情況, // 不過沒事,Object.keys的引數為數字,字串時 會 返回一個空陣列。 this.walk(value) } } // 遍歷物件並觀察 walk(obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { // 觀察物件,defineReactive 函式內部呼叫了 observe 方法, // observe 內部 呼叫了 Observer 建構函式 defineReactive(obj, keys[i]) } } // 遍歷陣列並觀察 observeArray(items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { // 觀察物件,observe 內部 呼叫了 Observer 建構函式 observe(items[i]) } } } function protoAugment(target, src: Object, keys: any) { target.__proto__ = src } function copyAugment(target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } } 複製程式碼
上面的程式碼中,細心的同學可能對observe
、def
,defineReactive
這些函式不明所以,接下來說說這幾個函式
observe
函式
用來呼叫Observer
建構函式
export function observe(value: any, asRootData: ?boolean): Observer | void { // 如果不是物件,或者是VNode例項,直接返回。 if (!isObject(value) || value instanceof VNode) { return } // 定義一個 變數,用來儲存 Observer 例項 let ob: Observer | void // 如果物件已經被觀察過,Vue會自動給物件加上一個 __ob__ 屬性,避免重複觀察 // 如果物件上已經有 __ob__屬性,表示已經被觀察過,就直接返回 __ob__ if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve &&// 是否應該觀察 !isServerRendering() &&// 非服務端渲染 (Array.isArray(value) || isPlainObject(value)) &&// 是陣列或者Object物件 Object.isExtensible(value) &&// 物件是否可擴充套件,也就是是否可向物件新增新屬性 !value._isVue // 非 Vue 例項 ) { ob = new Observer(value) } if (asRootData && ob) {// 暫時還不清楚,不過我們可以先忽略它 ob.vmCount++ } return ob// 返回 Observer 例項 } 複製程式碼
可以發現observe
函式,只是 返回 一個Observer
例項,只是多了些許判斷。為了方便理解,我們完全可以把程式碼縮減:
// 這就清晰多了 function observe(value) { let ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.___ob___ } else { ob = new Observer(value) } return ob; } 複製程式碼
def
函式
其實就是Object.defineProperty
的封裝
export function def(obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, // 預設不可列舉,也就意味著正常情況,Vue幫我們在物件上新增的 __ob__屬性,是遍歷不到的 enumerable: !!enumerable, writable: true, configurable: true }) } 複製程式碼
defineReactive
函式
defineReactive
函式的功能較多,主要是用來初始化時收集依賴
和改變屬性時觸發依賴
export function defineReactive( obj: Object,// 被觀察物件 key: string,// 物件的屬性 val: any,// 使用者給屬性賦值 customSetter?: ?Function,// 使用者額外自定義的 set shallow?: boolean// 是否深度觀察 ) { // 用於收集依賴 const dep = new Dep() // 如果不可修改,直接返回 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 如果使用者自己 未在物件上定義get 或 已在物件上定義set,且使用者沒有傳入 val 引數 // 則先計算物件的初始值,賦值給 val 引數 const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // !shallow 表示 深度觀察,shallow 不為 true 的情況下,表示預設深度觀察 // 如果是深度觀察,執行 observe 方法觀察物件 let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // 獲取物件的原有值 const value = getter ? getter.call(obj) : val // 收集依賴。收集依賴和觸發依賴是個比較大的流程,日後再說 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } // 返回物件的原有值 return value }, set: function reactiveSetter(newVal) { // 獲取物件的原有值 const value = getter ? getter.call(obj) : val // 判斷值是否改變 // (newVal !== newVal && value !== value) 用來判斷 NaN !== NaN 的情況 if (newVal === value || (newVal !== newVal && value !== value)) { return } // 非生產環境,觸發使用者額外自定義的 setter if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // 觸發物件原有的 setter,如果沒有的話,用新值(newVal)覆蓋舊值(val) if (setter) { setter.call(obj, newVal) } else { val = newVal } // 如果是深度觀察,屬性被更改後,重新觀察 childOb = !shallow && observe(newVal) // 觸發依賴。收集依賴和觸發依賴是個比較大的流程,日後再說 dep.notify() } }) } 複製程式碼
入口在哪
說了這麼多,那Vue觀察物件的初始化入口在哪裡呢,當然是在初始化Vue例項的地方了,也就是new Vue
的時候。
程式碼來源:Vue專案下src/core/instance/index.js
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options)// 這個方法 定義在 initMixin 函式內 } // 就是這裡,initMixin 函式會在 Vue 的 prototype 上擴充套件一個 _init 方法 // 我們 new Vue 的時候就是執行的 this._init(options) 方法 initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) 複製程式碼
initMixin
函式在Vue.prototype
上擴充套件一個_init
方法,_init
方法會有一個initState
函式進行資料初始化
initState(vm)// vm 為當前 Vue 例項,Vue 會將我們傳入的 data 屬性賦值給 vm._data 複製程式碼
initState
函式會在內部執行一段程式碼,觀察vm
例項上的data
屬性
程式碼來源:Vue專案下src/core/instance/state.js
。無用的程式碼我先註釋掉了,只保留初始化data
的程式碼。
export function initState(vm: Component) { // vm._watchers = [] // const opts = vm.$options // if (opts.props) initProps(vm, opts.props) // if (opts.methods) initMethods(vm, opts.methods) // 如果傳入了 data 屬性 // 這裡的 data 就是我們 new Vue 時傳入的 data 屬性 if (opts.data) { // initData 內部會將 我們傳入的 data屬性 規範化。 // 如果傳入的 data 不是函式,則直接 observe(data) // 如果傳入的 data 是函式,會先執行函式,將 返回值 賦值給 data,覆蓋原有的值,再observe(data)。 // 這也就是為什麼我們寫元件時 data 可以傳入一個函式 initData(vm) } else { // 如果沒傳入 data 屬性,觀察一個空物件 observe(vm._data = {}, true /* asRootData */) } // if (opts.computed) initComputed(vm, opts.computed) // if (opts.watch && opts.watch !== nativeWatch) { //initWatch(vm, opts.watch) // } } 複製程式碼
總結
我們new Vue
的時候 Vue 對我們傳入的data
屬性到底做了什麼操作?
-
如果我們傳入的
data
是一個函式,會先執行函式得到返回值。並賦值覆蓋data
。如果傳入的是物件,則不做操作。 -
執行
observe(data)
-
observe 內部會執行
new Observer(data)
-
new Observer(data)
會在data
物件 上擴充套件一個不可列舉 的屬性__ob__
,這個屬性有大作用。 -
如果
data
是個陣列-
執行
observeArray(data)
。這個方法會遍歷data
物件,並對每一個數組項執行observe
。之後的流程參考第2步
-
執行
-
如果
data
是物件-
執行
walk(data)
。這個方法會遍歷data
物件,並對每一個屬性執行defineReactive
。 -
defineReactive
內部會對傳入的物件屬性執行observe
。之後的流程參考第2步
-
執行
-
observe 內部會執行
篇幅和精力有限,關於protoAugment
和copyAugment
的作用,defineReactive
內如何收集依賴與觸發依賴的實現,日後再說。
文章內容如果有錯誤之處,還請指出。
參考:
JavaScript/">JavaScript 如何完整實現深度Clone物件