Vue2.x源碼解析系列四:數據響應之Observer

NO IMAGE

寫在前面的話:關於作者言川

筆名言川, 前端工程師,精通 Vue/Webpack/Git等,熟悉Node/React等,涉獵廣泛,對算法/後端/人工智能/linux等都有一定研究。開源愛好者,github上目前總計5000+ Star

  • 我的github主頁:https://github.com/lihongxun945
  • 我的博客地址:https://github.com/lihongxun945/myblog
  • 我的掘金主頁:https://juejin.im/user/1398234518660551/posts
  • 我的知乎專欄:https://zhuanlan.zhihu.com/c_1007281871281090560

此博客原地址:https://github.com/lihongxun945/myblog/issues/25

如果你之前看過我的這一篇文章 Vue1.0源碼解析系列:實現數據響應化 ,那麼你可以很輕鬆看懂 Vue2.x版本中的響應化,因為基本思路以及大部分代碼其實都沒有變化。當然沒看過也沒關係,不用去看,因為這裡我會講的非常詳細。

數據響應我會分兩章來講,本章講 Observer 相關,下一章講 Watcher

從data開始

state 的初始化是從 initState 函數開始的,下面是 initState 的完整代碼:

core/instance/state.js

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)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

這裡包括了四個部分:props, methods, datawatch,為了方便起見,讓我們從最簡單的,但是也能完整揭示數據響應化原理的 data 作為切入點。為什麼選它呢,因為 props 還涉及到如何從模板中解析,而另外兩個其實是函數。

讓我們先看一下 initData 的完整代碼:

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 (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

看起來並不算短,不過我們可以先把開發模式下的一些友好警告給忽略掉,畢竟對我們分析源碼來說這些警告不是很重要,其中有三段警告,讓我們分別看看:

  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
)
}

上面這段的意思是,如果發現 data 竟然不是一個平凡對象,那麼就打印一段警告,告訴你必須應該返回一個對象。

if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}

大的循環體都是在循環 data 上的 key,上面這一段是說,如果發現 methods 中有和 data 上定義重複的key,那麼就打印一個警告。

 if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
}

上面這一段是說,如果發現 props 中發現了重複的 key,那麼也會打印一段警告。當然上述兩種警告都只有在開發模式下才有的。弄懂了這兩段警告的意思,讓我們把它刪了,然後在看看代碼變成這樣了:

function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
}
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

是不是簡單了很多,我們把上面這段代碼拆成三段來分別看看。其中最上面的一段代碼是:

  let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
}

首先把 vm.$options.data 取個別名,免得後面這樣寫太長了,然後判斷了它的類型,如果是函數,就通過 getData 獲取函數的返回值。然後還有一個操作就是把 data 放到了 this._data 上,至於為什麼這麼做,下一段代碼我們就會明白。

這裡大家會有另一個疑問了,為什麼不是直接調用函數獲得返回值,而是需要一個 getData 呢,它除了調用函數肯定還做了別的事,讓我們看看 getData 的源碼:

export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}

其實它確實是調用了函數,並獲得了返回值,除了一段異常處理代碼外,他在調用我們的 data 函數前進行了一個 pushTarget 操作,而在結束後調用了一個 popTarget 操作。我們繼續來看這兩個函數,他們在 **core/observer/dep.js`中有定義,而且異常簡單。

Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}

雖然看起來代碼很簡單,就是在一個全局的 Dep.target 中把自己記錄了一下,也就是在 data 函數調用前記錄了一下,然後調用後又恢復了之前的值。這裡暫時理解起來會比較困難,因為我們要結合本文後面講到的內容才能理解。簡單的說,在 getData 的時候,我們調用 pushTarget 卻沒有傳參數,目的是把 Dep.target 給清空,這樣不會在獲取 data 初始值的過程中意外的把依賴記錄下來。

我們再回到 initState 的第二段代碼:

 const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}

就是遍歷了 data 的key,然後做了一個 proxy,我們來看 proxy 的代碼:

function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}

這裡target是就是我們的 vm 也就是我們的組件自身,sourceKey 就是 _data,也就是我們的 data,這段代碼會把對 vm 上的數據讀寫代理到 _data 上去。哈哈,我們這樣就明白了一個問題,為什麼我們是通過 data.msg 定義的數據,卻可以通過 this.msg 訪問呢?原來是這裡做了一個代理。

到目前為止雖然說了這麼多,但是做的事情很簡單,除了一些異常處理之外,我們主要做了三件事:

  1. 通過 getData 把options中傳入的data取出來,這期間做了一些 依賴 的處理
  2. this._data = data
  3. 對於每一個 data 上的key,都在 vm 上做一個代理,實際操作的是 this._data

這樣結束之後,其實vm會變成這樣:

Vue2.x源碼解析系列四:數據響應之Observer

弄懂了這個之後我們再看最後一段代碼:

observe(data, true /* asRootData */)

observe 是如何工作的?我們來看看他的代碼,這是響應式的核心代碼。

深入 Observer

observer 的定義在 core/observer/index.js 中,我們看看 代碼:

export function observe (value: any, asRootData: ?boolean): Observer | void {
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
}

其中有一些很多if的判斷,包括對類型的判斷,是否之前已經做過監聽等。我們暫且拋開這些,把代碼精簡一下,就只剩下兩行了:

export function observe (value: any, asRootData: ?boolean): Observer | void {
ob = new Observer(value)
return ob
}

可以看到主要邏輯就是創建了一個 Observer 實例,那麼我們再看看 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
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

這個類包括構造函數在內,總共有三個函數。

  constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}

構造函數代碼如上,主要做了這麼幾件事:

    this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)

這裡記錄了 value, depvmCount, 和 __ob__四個值,其中值得注意的是這兩個:

  • this.dep 是 明顯是記錄依賴的,記錄的是對這個value 的依賴,我們在下面馬上就能看到怎麼記錄和使用的
  • __ob__ 其實是把自己記錄一下,避免重複創建
    if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}

這一段代碼會判斷 value 的類型,進行遞歸的 observe,對數組來說,就是對其中每一項都進行遞歸 observe:

observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}

顯然,直到碰到數組中非數組部分後,最終就會進入 walk 函數,在看 walk 函數之前,我們先看看這一段代碼:

const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)

這裡我不打算詳細講解每一行,如果你看源碼其實很容易看懂。這裡的作用就是把 數組上的原生方法進行了一次劫持,因此你調用比如 push 方法的時候,其實調用的是被 劫持 一個方法,而在這個方法內部,Vue會進行 notify 操作,因此就知道了你對數組的修改了。不過這個做法沒法劫持直接通過下標對數組的修改。

好,讓我們回到 walk 函數:

walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

walk 函數會對每一個 key 進行 defineReactive 操作,在這個函數內部其實就會調用 getter/setter 攔截讀寫操作,實現響應化。那麼這時候可能有人會有一個疑問了,如果某個 key 的值也是一個對象呢?難道不能進行深度的依賴麼?當然可以的,不過對對象嵌套的遞歸操作不是在這裡進行的,而是在 defineReactive 中進行了遞歸。讓我們看看 defineReactive 函數:

export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
if (!getter && arguments.length === 2) {
val = obj[key]
}
const setter = property && property.set
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

終於看到了傳說中的 getter/setter,上面是完整的代碼,有些長,按照慣例我們分別進行講解。

const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
if (!getter && arguments.length === 2) {
val = obj[key]
}
const setter = property && property.set

這段代碼中,第一步是創建了一個 dep 來收集對當前 obj.key 的依賴,這裡可能大家又會問:之前 new Observer 的時候不是已經創建了嗎,這裡怎麼又創建一次?這是一個深度依賴的問題,為了回答這個問題我們還得先往下看代碼。

dep 之後是獲取了getter/setter ,比較簡單,我們再往下看:

 let childOb = !shallow && observe(val)

這一段代碼非常重要,如果 val 是一個對象,那麼我們要遞歸進行監聽。也就是又回到了 new Observer 中,可以知道,childOb 返回的是一個 observer 實例。有了這個對孩子的監聽器之後,當孩子改變的時候我們就能知道了。讓我們繼續往下看最重要的一段代碼getter

  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
},

首先,我們自定義的 getter 中,會把需要取出的值拿出來,通過原來的 getter。然後會判斷 Dep.target 存在就進行一個 dep.depend() 操作,並且如果有孩子,也會對孩子進行 dep.depend() 操作。

dep.depend() 的代碼如下:

depend () { 
if (Dep.target) { 
Dep.target.addDep(this) 
} 
} 

也就是把當前這個 dep 加入到 target 中。

那麼這個 target 就非常重要了,他到底是什麼呢?我們在 getData 的時候設置過 Dep.target ,但當時我們目的是清空,而不是設置一個值。所以這裡我們依然不知道 target 是什麼。代碼看到當前位置其實是肯定無法理解 target 的作用的,沒關係,我們可以帶著這個疑問繼續往下看。

但是這裡我簡單說明一下,這個target其實是一個 watcher,我們在獲取一個數據的時候,比如 this.msg 並是不直接去 this._data.msg 上取,而是先創建一個watcher,然後通過 watcher.value來取,而watcher.value === msg.getter 所以在取值的時候,我們就知道 watcher 是依賴於當前的 dep 的,而 dep.depend() 相當於 watcher.deps.push(dep)

如果你面試的時候被問到 Vue的原理,那麼有一個常見的考點是問你 Vue 是怎麼收集依賴的,比如 computed 中有如下代碼:

info () {
return this.name + ',' + this.age
}

Vue 是如何知道 info 依賴 nameage 呢?是因為在第一次獲取 info 的值的時候,會取 nameage 的值,因此就可以在他們的 getter 中記錄依賴。當然由於我們現在還沒有看 Watcher 的代碼,所以這一塊並不能理解的很透徹,沒關係,讓我們暫且繼續往下看。這裡只要記住** Vue 在第一次取值的時候收集依賴 就行了**。

再看看 setter 函數,我刪除了部分不影響整體邏輯的代碼:

    set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}

拋開一些異常情況的處理,主要代碼其實做了兩件事,第一件事是設置值,不過這裡的 setter 是什麼呢?其實是我們自定義的 setter,如果我們有自定義,那麼就調用我們的 setter,否則就直接設置。

然後如果發現我們設置的新值是一個對象,那麼就遞歸監聽這個對象。

最後,通過 dep.notify 來通知響應的 target 們,我更新啦。

還記得上面我們留了一個深度依賴的問題嗎?我們舉個栗子說明,假設我們的 data 是這樣的:

data: {
people: {
name: '123'
}
}

我們對 people 進行 defineReactive 的時候,我們當然可以處理 this.people={} 的操作。但是如果我進行了 this.people.name='xx' 的操作的時候要怎麼辦呢?顯然我們此時是無法檢測到這個更新的。所以我們會創建對 {name:123} 再創建一個 childObj ,然後我們的 target 也依賴於這個孩子,就能檢測到他的更新了。

到這裡我們就講完 Observer 了,總結一下,Observer就是通過 getter/setter 監聽數據讀寫,在 getter 中記錄依賴, 在 setter 中通知哪些依賴們。讓我們把之前的一張圖完善下,變成這樣:

Vue2.x源碼解析系列四:數據響應之Observer

下一章 我們看看 什麼是 Watcher

下一章:Vue2.x源碼解析系列五:數據響應之Watcher

相關文章

要實現60FPS動畫,你需要了解這些

體驗WebAssembly

Vue2.x源碼解析系列七:深入Compiler理解render函數的生成過程

Vue2.x源碼解析系列五:數據響應之Watcher