依賴收集
通過上一節(jié)的分析我們了解 Vue 會(huì)把普通對(duì)象變成響應(yīng)式對(duì)象胸完,響應(yīng)式對(duì)象 getter 相關(guān)的邏輯就是做依賴收集郊愧,這一節(jié)我們來詳細(xì)分析這個(gè)過程誓禁。
我們先來回顧一下 getter 部分的邏輯:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() // ???
const property = Object.getOwnPropertyDescriptor(obj, key) // Object.getOwnPropertyDescriptor() 方法返回指定對(duì)象上一個(gè)自有屬性對(duì)應(yīng)的屬性描述符肉盹。(自有屬性指的是直接賦予該對(duì)象的屬性,不需要從原型鏈上進(jìn)行查找的屬性)
if (property && property.configurable === false) { // 當(dāng)且僅當(dāng)指定對(duì)象的屬性描述可以被改變或者屬性可被刪除時(shí)豁延,為true。
return
}
// cater for pre-defined getter/setters
const getter = property && property.get // 拿到屬性原生get
const setter = property && property.set
// val不是對(duì)象則不添加Observe
let childOb = !shallow && observe(val) // observe(val) 判斷子屬性val是否還是一個(gè)對(duì)象腊状,是的話再次調(diào)用 observe(val)
Object.defineProperty(obj, key, { // 給key定義響應(yīng)式屬性诱咏;
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() // 派發(fā)更新
}
})
}
這段代碼我們只需要關(guān)注 2 個(gè)地方,一個(gè)是 const dep = new Dep()
實(shí)例化一個(gè) Dep
的實(shí)例缴挖,另一個(gè)是在 get
函數(shù)中通過 dep.depend
做依賴收集袋狞,這里還有個(gè)對(duì) childObj
判斷的邏輯,我們之后會(huì)介紹它的作用。
Dep
Dep
是整個(gè) getter
依賴收集的核心苟鸯,它的定義在 src/core/observer/dep.js
中:
/* @flow */
import type Watcher from './watcher'
import { remove } from '../util/index'
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
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 () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
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
是一個(gè) Class
法焰,它定義了一些屬性和方法,這里需要特別注意的是它有一個(gè)靜態(tài)屬性 target
倔毙,這是一個(gè)全局唯一 Watcher
埃仪,這是一個(gè)非常巧妙的設(shè)計(jì),因?yàn)樵谕粫r(shí)間只能有一個(gè)全局的 Watcher
被計(jì)算陕赃,另外它的自身屬性 subs
也是 Watcher
的數(shù)組卵蛉。
Dep
實(shí)際上就是對(duì) Watcher
的一種管理,Dep
脫離 Watcher 單獨(dú)存在是沒有意義的么库,為了完整地講清楚依賴收集過程傻丝,我們有必要看一下 Watcher
的一些相關(guān)實(shí)現(xiàn),它的定義在 src/core/observer/watcher.js
中:
Watcher
/* @flow */
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError
} from '../util/index'
import type { ISet } from '../util/index'
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: ISet;
newDepIds: ISet;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function, // updateComponent
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
// options
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 // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
Watcher
是一個(gè) Class
诉儒,在它的構(gòu)造函數(shù)中葡缰,定義了一些和 Dep 相關(guān)的屬性:
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
其中,this.deps
和 this.newDeps
表示 Watcher
實(shí)例持有的 Dep
實(shí)例的數(shù)組忱反;而 this.depIds
和 this.newDepIds
分別代表 this.deps
和 this.newDeps
的 idSet
(這個(gè) Set
是 ES6
的數(shù)據(jù)結(jié)構(gòu)泛释,它的實(shí)現(xiàn)在 src/core/util/env.js
中)。那么這里為何需要有 2 個(gè) Dep
實(shí)例數(shù)組呢温算,稍后我們會(huì)解釋怜校。
Watcher
還定義了一些原型的方法,和依賴收集相關(guān)的有 get
注竿、addDep
和 cleanupDeps
方法茄茁,單個(gè)介紹它們的實(shí)現(xiàn)不方便理解,我會(huì)結(jié)合整個(gè)依賴收集的過程把這幾個(gè)方法講清楚巩割。
過程分析
之前我們介紹當(dāng)對(duì)數(shù)據(jù)對(duì)象的訪問會(huì)觸發(fā)他們的 getter
方法裙顽,那么這些對(duì)象什么時(shí)候被訪問呢?還記得之前我們介紹過 Vue
的 mount
過程是通過 mountComponent
函數(shù)宣谈,其中有一段比較重要的邏輯愈犹,大致如下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
當(dāng)我們?nèi)?shí)例化一個(gè)渲染 watcher
的時(shí)候,首先進(jìn)入 watcher
的構(gòu)造函數(shù)邏輯蒲祈,然后會(huì)執(zhí)行它的 this.get()
方法甘萧,進(jìn)入 get
函數(shù),首先會(huì)執(zhí)行:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // this.getter 對(duì)應(yīng)就是 updateComponent 函數(shù)梆掸,這實(shí)際上就是在執(zhí)行:
vm._update(vm._render(), hydrating)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
pushTarget(this)
pushTarget
的定義在 src/core/observer/dep.js
中:
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target // _target Watcher.js Watcher 類
}
實(shí)際上就是把 Dep.target
賦值為當(dāng)前的渲染 watcher
并壓棧(為了恢復(fù)用)扬卷。接著又執(zhí)行了:
value = this.getter.call(vm, vm)
this.getter
對(duì)應(yīng)就是 updateComponent
函數(shù),這實(shí)際上就是在執(zhí)行:
vm._update(vm._render(), hydrating)
它會(huì)先執(zhí)行 vm._render()
方法酸钦,因?yàn)橹胺治鲞^這個(gè)方法會(huì)生成 渲染 VNode
怪得,并且在這個(gè)過程中會(huì)對(duì) vm
上的數(shù)據(jù)訪問,這個(gè)時(shí)候就觸發(fā)了數(shù)據(jù)對(duì)象的 getter
。
那么每個(gè)對(duì)象值的 getter
都持有一個(gè) dep
徒恋,在觸發(fā) getter
的時(shí)候會(huì)調(diào)用 dep.depend()
方法蚕断,也就會(huì)執(zhí)行 Dep.target.addDep(this)
。
剛才我們提到這個(gè)時(shí)候 Dep.target
已經(jīng)被賦值為渲染 watcher
入挣,那么就執(zhí)行到 addDep
方法:
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
這時(shí)候會(huì)做一些邏輯判斷(保證同一數(shù)據(jù)不會(huì)被添加多次)后執(zhí)行 dep.addSub(this)
亿乳,那么就會(huì)執(zhí)行 this.subs.push(sub)
,也就是說把當(dāng)前的 watcher
訂閱到這個(gè)數(shù)據(jù)持有的 dep
的 subs
中径筏,這個(gè)目的是為后續(xù)數(shù)據(jù)變化時(shí)候能通知到哪些 subs
做準(zhǔn)備葛假。
所以在 vm._render()
過程中,會(huì)觸發(fā)所有數(shù)據(jù)的 getter
滋恬,這樣實(shí)際上已經(jīng)完成了一個(gè)依賴收集的過程聊训。那么到這里就結(jié)束了么,其實(shí)并沒有恢氯,再完成依賴收集后带斑,還有幾個(gè)邏輯要執(zhí)行,首先是:
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
這個(gè)是要遞歸去訪問 value
勋拟,觸發(fā)它所有子項(xiàng)的 getter
勋磕,這個(gè)之后會(huì)詳細(xì)講。接下來執(zhí)行:
popTarget()
popTarget
的定義在 src/core/observer/dep.js
中:
export function popTarget () {
Dep.target = targetStack.pop() // pop() 方法用于刪除并返回?cái)?shù)組的最后一個(gè)元素
}
實(shí)際上就是把 Dep.target
恢復(fù)成上一個(gè)狀態(tài)指黎,因?yàn)楫?dāng)前 vm
的數(shù)據(jù)依賴收集已經(jīng)完成朋凉,那么對(duì)應(yīng)的渲染Dep.target
也需要改變。最后執(zhí)行:
this.cleanupDeps()
其實(shí)很多人都分析過并了解到 Vue
有依賴收集的過程醋安,但我?guī)缀鯖]有看到有人分析依賴清空的過程,其實(shí)這是大部分同學(xué)會(huì)忽視的一點(diǎn)墓毒,也是 Vue
考慮特別細(xì)的一點(diǎn)吓揪。
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
考慮到 Vue
是數(shù)據(jù)驅(qū)動(dòng)的,所以每次數(shù)據(jù)變化都會(huì)重新 render
所计,那么 vm._render()
方法又會(huì)再次執(zhí)行柠辞,并再次觸發(fā)數(shù)據(jù)的 getters
,所以 Wathcer
在構(gòu)造函數(shù)中會(huì)初始化 2 個(gè) Dep
實(shí)例數(shù)組主胧,newDeps
表示新添加的 Dep
實(shí)例數(shù)組叭首,而 deps
表示上一次添加的 Dep
實(shí)例數(shù)組。
在執(zhí)行 cleanupDeps
函數(shù)的時(shí)候踪栋,會(huì)首先遍歷 deps
焙格,移除對(duì) dep
的訂閱,然后把 newDepIds
和 depIds
交換夷都,newDeps
和 deps
交換眷唉,并把 newDepIds
和 newDeps
清空。
那么為什么需要做 deps
訂閱的移除呢,在添加 deps
的訂閱過程冬阳,已經(jīng)能通過 id
去重避免重復(fù)訂閱了蛤虐。
考慮到一種場景,我們的模板會(huì)根據(jù)v-if
去渲染不同子模板 a
和 b
肝陪,當(dāng)我們滿足某種條件的時(shí)候渲染 a
的時(shí)候驳庭,會(huì)訪問到 a
中的數(shù)據(jù),這時(shí)候我們對(duì) a
使用的數(shù)據(jù)添加了 getter
氯窍,做了依賴收集嚷掠,那么當(dāng)我們?nèi)バ薷?a
的數(shù)據(jù)的時(shí)候,理應(yīng)通知到這些訂閱者荞驴。那么如果我們一旦改變了條件渲染了 b
模板不皆,又會(huì)對(duì) b
使用的數(shù)據(jù)添加了 getter
,如果我們沒有依賴移除的過程熊楼,那么這時(shí)候我去修改 a
模板的數(shù)據(jù)霹娄,會(huì)通知 a
數(shù)據(jù)的訂閱的回調(diào),這顯然是有浪費(fèi)的鲫骗。
因此 Vue
設(shè)計(jì)了在每次添加完新的訂閱犬耻,會(huì)移除掉舊的訂閱,這樣就保證了在我們剛才的場景中执泰,如果渲染 b
模板的時(shí)候去修改 a
模板的數(shù)據(jù)枕磁,a
數(shù)據(jù)訂閱回調(diào)已經(jīng)被移除了,所以不會(huì)有任何浪費(fèi)术吝,真的是非常贊嘆 Vue
對(duì)一些細(xì)節(jié)上的處理计济。
總結(jié)
通過這一節(jié)的分析,我們對(duì)
Vue
數(shù)據(jù)的依賴收集過程已經(jīng)有了認(rèn)識(shí)排苍,并且對(duì)這其中的一些細(xì)節(jié)做了分析沦寂。收集依賴的目的是為了當(dāng)這些響應(yīng)式數(shù)據(jù)發(fā)送變化,觸發(fā)它們的setter
的時(shí)候淘衙,能知道應(yīng)該通知哪些訂閱者去做相應(yīng)的邏輯處理传藏,我們把這個(gè)過程叫派發(fā)更新,其實(shí)Watcher
和Dep
就是一個(gè)非常經(jīng)典的觀察者設(shè)計(jì)模式的實(shí)現(xiàn)彤守,下一節(jié)我們來詳細(xì)分析一下派發(fā)更新的過程毯侦。