以前個人一篇文章vue響應式原理學習(一)講述了vue數據響應式原理的一些簡單知識。 衆所周知,
Vue
的data
屬性,是默認深度監聽的,此次咱們再深度分析下,Observer
觀察者的源碼實現。javascript
既然data
屬性是被深度監聽,那咱們就首先本身實現一個簡單的深拷貝,理解下思路。html
深拷貝的原理有點像遞歸, 其實就是遇到引用類型,調用自身函數再次解析。vue
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對象等等。要寫好一個深拷貝,不簡單。java
有的同窗可能會問,爲何不直接一個 for in
解決。以下:react
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
上擴展的方法。api
又有的同窗可能會說,加 hasOwnProperty
解決呀。若是是 Object
類型,確實能夠解決,但如何是 Array
的話,就獲取不到數組的索引啦。數組
說到 for in
,再加個注意項,就是 for in
也是能夠 continue
的,而數組的 forEach
方法不能夠。由於 forEach
的內部實現是在一個for
循環中依次執行你傳入的函數。函數
這裏我主要是爲代碼添加註釋,建議看官們最好打開源碼來看。post
代碼來源:Vue項目下的 src/core/observer/index.js
學習
Vue 將 Observer
封裝成了一個 class
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)
new Observer(data)
new Observer(data)
會在 data
對象 上擴展一個不可枚舉的屬性 __ob__
,這個屬性有大做用。data
是個數組
observeArray(data)
。這個方法會遍歷data
對象,並對每個數組項執行observe
。以後的流程參考第2步data
是對象
walk(data)
。這個方法會遍歷data
對象,並對每個屬性執行 defineReactive
。defineReactive
內部會對傳入的對象屬性執行 observe
。以後的流程參考第2步篇幅和精力有限,關於 protoAugment
和copyAugment
的做用,defineReactive
內如何收集依賴與觸發依賴的實現,往後再說。
文章內容若是有錯誤之處,還請指出。
參考: