之前我的一篇文章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
內如何收集依賴與觸發依賴的實現,日後再說。
文章內容如果有錯誤之處,還請指出。
參考: