筆名言川, 前端工程師,精通 Vue/Webpack/Git
等,熟悉Node/React
等,涉獵普遍,對算法/後端/人工智能/linux等都有必定研究。開源愛好者,github上目前總計5000+ Star。html
此博客原地址:https://github.com/lihongxun945/myblog/issues/25前端
若是你以前看過個人這一篇文章 Vue1.0源碼解析系列:實現數據響應化 ,那麼你能夠很輕鬆看懂 Vue2.x版本中的響應化,由於基本思路以及大部分代碼其實都沒有變化。固然沒看過也不要緊,不用去看,由於這裏我會講的很是詳細。vue
數據響應我會分兩章來說,本章講 Observer
相關,下一章講 Watcher
。react
state 的初始化是從 initState
函數開始的,下面是 initState
的完整代碼:linux
core/instance/state.jsgit
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
, data
和 watch
,爲了方便起見,讓咱們從最簡單的,可是也能完整揭示數據響應化原理的 data
做爲切入點。爲何選它呢,由於 props
還涉及到如何從模板中解析,而另外兩個實際上是函數。github
讓咱們先看一下 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
訪問呢?原來是這裏作了一個代理。
到目前爲止雖說了這麼多,可是作的事情很簡單,除了一些異常處理以外,咱們主要作了三件事:
getData
把options中傳入的data取出來,這期間作了一些 依賴
的處理this._data = data
data
上的key,都在 vm
上作一個代理,實際操做的是 this._data
這樣結束以後,其實vm會變成這樣:
弄懂了這個以後咱們再看最後一段代碼:
observe(data, true /* asRootData */)
複製代碼
observe
是如何工做的?咱們來看看他的代碼,這是響應式的核心代碼。
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
, dep
,vmCount
, 和 __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
依賴 name
和 age
呢?是由於在第一次獲取 info
的值的時候,會取 name
和 age
的值,所以就能夠在他們的 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
中通知哪些依賴們。讓咱們把以前的一張圖完善下,變成這樣:
下一章 咱們看看 什麼是 Watcher