仔細閱讀註解內容。會針對源碼原理深度講解 原文轉載地址
使用vuex
中store
中的數據,基本上離不開vue
中一個常用的屬性computed
。官方一個最簡單的例子如下
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 計算屬性的 getter
reversedMessage: function () {
// `this` 指向 vm 實例
return this.message.split('').reverse().join()
}
}
})
不知大家有沒有思考過,vue的
computed
是如何更新的,爲什麼當vm.message
發生變化時,vm.reversedMessage
也會自動發生變化?
我們來看看vue
中data
屬性和computed
相關的源代碼。
// src/core/instance/state.js
// 初始化組件的state
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屬性
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 當組件存在 computed屬性
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initState
方法當組件實例化時會自動觸發,該方法主要完成了初始化data,methods,props,computed,watch
這些我們常用的屬性,我們來看看我們需要關注的initData
和initComputed
(爲了節省時間,去除了不太相關的代碼)
先看看 initData 這條線
// src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// .....省略無關代碼
// 將vue的data傳入observe方法
observe(data, true /* asRootData */)
}
// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value)) {
return
}
let ob: Observer | void
// ...省略無關代碼
ob = new Observer(value)
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
在初始化的時候observe
方法本質上是實例化了一個Observer
對象,這個對象的類是這樣的
// src/core/observer/index.js
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
// 關鍵代碼 new Dep對象
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// ...省略無關代碼
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 給data的所有屬性調用defineReactive
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
在對象的構造函數中,最後調用了
walk
方法,該方法即遍歷data
中的所有屬性,並調用defineReactive
方法,defineReactive
方法是vue
實現MDV(Model-Driven-View)
的基礎,本質上就是代理了數據的set,get
方法,當數據修改或獲取的時候,能夠感知(當然vue
還要考慮數組,Object
中嵌套Object
等各種情況,本文不在分析)。
我們具體看看defineReactive
的源代碼
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 重點,在給具體屬性調用該方法時,都會爲該屬性生成唯一的dep對象
const dep = new Dep()
// 獲取該屬性的描述對象
// 該方法會返回對象中某個屬性的具體描述
// api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果該描述不能被更改,直接返回,因爲不能更改,那麼就無法代理set和get方法,無法做到響應式
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
// 重新定義data當中的屬性,對get和set進行代理。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 收集依賴, reversedMessage爲什麼會跟着message變化的原因
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
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 通知依賴進行更新
dep.notify()
}
})
}
我們可以看到,在所代理的屬性的get
方法中,當dep.Target
存在的時候會調用dep.depend()
方法,這個方法非常的簡單,不過在說這個方法之前,我們要認識一個新的類Dep
Dep
是vue
實現的一個處理依賴關係的對象,
主要起到一個紐帶的作用,就是連接reactive data
與watcher
,代碼非常的簡單
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 更新 watcher 的值,與 watcher.evaluate() 類似,
// 但 update 是給依賴變化時使用的,包含對 watch 的處理
subs[i].update()
}
}
}
// 當首次計算 computed 屬性的值時,Dep 將會在計算期間對依賴進行收集
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
// 在一次依賴收集期間,如果有其他依賴收集任務開始(比如:當前 computed 計算屬性嵌套其他 computed 計算屬性),
// 那麼將會把當前 target 暫存到 targetStack,先進行其他 target 的依賴收集,
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
// 當嵌套的依賴收集任務完成後,將 target 恢復爲上一層的 Watcher,並繼續做依賴收集
Dep.target = targetStack.pop()
}
代碼非常的簡單,回到調用dep.depend()
方法的時候,當Dep.Target
存在,就會調用,而depend
方法則是將該dep
加入watcher
的newDeps
中,同時,將所訪問當前屬性的dep
對象中的subs
插入當前Dep.target
的watcher
.看起來有點繞,不過沒關係,我們一會跟着例子講解一下就清楚了。
講完了代理的
get
,方法,我們講一下代理的set
方法,set
方法的最後調用了dep.notify(),
當設置data
中具體屬性值的時候,就會調用該屬性下面的dep.notify()
方法,通過class Dep
瞭解到,notify
方法即將加入該dep
的watcher
全部更新,也就是說,當你修改data
中某個屬性值時,會同時調用dep.notify()
來更新依賴該值的所有watcher
。
介紹完了initData
這條線,我們繼續來介紹initComputed
這條線,這條線主要解決了什麼時候去設置Dep.target
的問題(如果沒有設置該值,就不會調用dep.depend()
, 即無法獲取依賴)。
// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// 初始化watchers列表
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// 關注點1,給所有屬性生成自己的watcher, 可以在this._computedWatchers下看到
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
// 關注點2
defineComputed(vm, key, userDef)
}
}
}
在初始化computed
時,有2個地方需要去關注
對每一個屬性都生成了一個屬於自己的
Watcher
實例,並將{ lazy: true }
作爲options
傳入
對每一個屬性調用了defineComputed
方法(本質和data
一樣,代理了自己的set
和get
方法,我們重點關注代理的get
方法)
我們看看Watcher
的構造函數
// src/core/observer/watcher.js
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // 如果初始化lazy=true時(暗示是computed屬性),那麼dirty也是true,需要等待更新
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.getter = expOrFn // 在computed實例化時,將具體的屬性值放入this.getter中
// 省略不相關的代碼
this.value = this.lazy
? undefined
: this.get()
}
除了日常的初始化外,還有2行重要的代碼
this.dirty = this.lazy
this.getter = expOrFn
在
computed
生成的watcher
,會將watcher
的lazy
設置爲true
,以減少計算量。因此,實例化時,this.dirty
也是true
,標明數據需要更新操作。我們先記住現在computed
中初始化對各個屬性生成的watcher
的dirty
和lazy
都設置爲了true
。同時,將computed
傳入的屬性值(一般爲funtion
),放入watcher
的getter
中保存起來。
我們在來看看第二個關注點defineComputed
所代理屬性的get
方法是什麼
// src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
// 如果找到了該屬性的watcher
if (watcher) {
// 和上文對應,初始化時,該dirty爲true,也就是說,當第一次訪問computed中的屬性的時候,會調用 watcher.evaluate()方法;
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
當第一次訪問
computed
中的值時,會因爲初始化watcher.dirty = watcher.lazy
的原因,從而調用evalute()
方法,evalute()
方法很簡單,就是調用了watcher
實例中的get
方法以及設置dirty = false
,我們將這兩個方法放在一起
// src/core/instance/state.js
evaluate () {
this.value = this.get()
this.dirty = false
}
get () {
// 重點1,將當前watcher放入Dep.target對象
pushTarget(this)
let value
const vm = this.vm
try {
// 重點2,當調用用戶傳入的方法時,會觸發什麼?
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
popTarget()
// 去除不相關代碼
}
return value
}
在get
方法中中,第一行就調用了pushTarget
方法,其作用就是將Dep.target
設置爲所傳入的watcher,
即所訪問的computed
中屬性的watcher
,
然後調用了value = this.getter.call(vm, vm)
方法,想一想,調用這個方法會發生什麼?
this.getter
在Watcher
構建函數中提到,本質就是用戶傳入的方法,也就是說,this.getter.call(vm, vm)
就會調用用戶自己聲明的方法,那麼如果方法裏面用到了 this.data
中的值或者其他被用defineReactive
包裝過的對象,那麼,訪問this.data.
或者其他被defineReactive
包裝過的屬性,是不是就會訪問被代理的該屬性的get
方法。我們在回頭看看
get
方法是什麼樣子的。
注意:我講了其他被用
defineReactive
,這個和後面的vuex
有關係,我們後面在提
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 這個時候,有值了
if (Dep.target) {
// computed的watcher依賴了this.data的dep
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
}
代碼註釋已經寫明瞭,就不在解釋了,這個時候我們走完了一個依賴收集流程,知道了
computed
是如何知道依賴了誰。最後根據this.data
所代理的set
方法中調用的notify
,就可以改變this.data
的值,去更新所有依賴this.data
值的computed
屬性value
了。
那麼,我們根據下面的代碼,來簡易拆解獲取依賴並更新的過程
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 計算屬性的 getter
reversedMessage: function () {
// `this` 指向 vm 實例
return this.message.split('').reverse().join()
}
}
})
vm.reversedMessage // => olleH
vm.message = 'World' //
vm.reversedMessage // => dlroW
初始化
data
和computed
,分別代理其set
以及get
方法, 對data
中的所有屬性生成唯一的dep
實例。
對computed
中的reversedMessage
生成唯一watcher
,並保存在vm._computedWatchers
中
訪問reversedMessage
,設置Dep.target
指向reversedMessage
的watcher
,調用該屬性具體方法reversedMessage
。
方法中訪問
this.message
,即會調用this.message
代理的get
方法,將this.message
的dep
加入reversedMessage
的watcher
,同時該dep
中的subs
添加這個watcher
設置vm.message = 'World'
,調用message
代理的set
方法觸發dep
的notify
方法’
因爲是computed
屬性,只是將watcher
中的dirty
設置爲true
最後一步vm.reversedMessage
,訪問其get
方法時,得知reversedMessage
的watcher.dirty
爲true
,調用watcher.evaluate()
方法獲取新的值。
這樣,也可以解釋了爲什麼有些時候當
computed
沒有被訪問(或者沒有被模板依賴),當修改了this.data
值後,通過vue-tools
發現其computed
中的值沒有變化的原因,因爲沒有觸發到其get
方法。
現在在回頭看 vuex的使用會有一種豁然開朗的感覺 回看vuex 的設計思路