欢迎 star 和 评论,Vue:多角度剖析计算属性的运行机制 #219vue
在建立Vue实例时调用this._init
初始化。react
其中就有调用initState
初始化git
export function initState (vm: Component) {
// ...
if (opts.computed) initComputed(vm, opts.computed)
// ...
}
复制代码
initState会初始化计算属性:调用initComputed
github
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// ...
for (const key in computed) {
if (!isSSR) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// ...
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// ...
}
}
}
复制代码
遍历computedtypescript
先建立计算属性的watcher实例,留意computedWatcherOptions
这个option决定了计算属性的watcher和普通watcher的不一样express
而后定义计算属性的属性的getter和setterapi
export default class Watcher {
// ...
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) {
// ...
if (options) {
// ...
this.lazy = !!options.lazy
// ...
}
this.dirty = this.lazy // for lazy watchers
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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()
}
// ...
}
复制代码
watcher.lazy = true
;watcher.dirty = true
;watcher.getter = typeof userDef === 'function' ? userDef : userDef.get
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed ( target: any, key: string, userDef: Object | Function ) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
// ...
Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码
shouldCache
,浏览器渲染都是 shouldCache = true
数组
那么gtter就是由createComputedGetter
方法建立浏览器
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
复制代码
以上就是计算属性的的初始化过程。缓存
如上,假设计算属性当前被调用
就是触发计算属性的getter,再次强调:计算属性的getter不是用户定义的回调,而是由createComputedGetter
返回的函数(详细参考计算属性的初始化过程的最后一段代码)。 用户定义的回调则是在计算属性getter的逻辑中进行调用。
计算属性getter中主要由两个if控制流, 这个两个if组合起来就可能由四种可能, 对于第二个控制流的逻辑watcher.depend
,若是有看到Vue的Dep的功能的话,能够推测这段代码是用于收集依赖, 结合以上能够以下推测:
序号 | if (watcher.dirty) |
if (Dep.target) |
功能 |
---|---|---|---|
1 | N | N | 返回旧值 |
2 | N | Y | 收集依赖 |
3 | Y | N | 更新计算属性值(watcher.value) |
4 | Y | Y | 收集依赖,并更新计算属性值(watcher.value) |
目前掌握的信息有:
watcher.value
,说明计算属性的值保存在watcher.value
;咱们先来看第一个控制流:
// watcher.dirty = true
if (watcher.dirty) {
watcher.evaluate()
}
复制代码
根据计算属性的初始化过程中建立计算属性watcher实例时就能够看出,第一次调用watcher.dirty确定是true
。
但不论watcher.dirty是否是“真”,咱们都要去看看“evaluate ”时何方神圣,并且确定会有访问它的时候。
evaluate () {
this.value = this.get()
this.dirty = false
}
复制代码
显然,evaluate确实是用于更新计算属性值(watcher.value)的。
另外,你能够发如今this.value = this.get()
执行完后,还执行了一句代码:this.dirty = false
。
而后你会发现一个逻辑:
一切说明计算属性是懒加载的,在访问时根据状态值来判断使用缓存数据仍是从新计算。
再者,咱们还能够再总结一下dirty和lazy的信息:
对比普通的watcher实例建立:
构造函数中的逻辑
normal | computed |
---|---|
this.value = this.get() |
this.value = undefined |
this.lazy = false |
this.lazy = true |
this.dirty = false |
this.dirty = true |
综上,能够看出 lazy的意思
实例化时调用get就是非lazy
非实例化时调用get就是lazy
dirty(脏值)的意思
watcher.value
仍是undefined(或者还不是当前真是)
就是dirtywatcher.value
已经存有当前计算的实际值就不是dirtylazy属性只是一个说明性的标志位,主要用来代表当前watcher是惰性模式的。 而dirty则是对lazy的实现,做为状态为表示当前是否是脏值状态。
再来看看watcher.get()
的调用,其内部的动做
import Dep, { pushTarget, popTarget } from './dep'
export default class Watcher {
// ...
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
}
// ...
}
复制代码
在get()函数开头的地方调用pushTarget
函数,为了接下来的内容,有必要先说明下pushTarget
和结尾处的popTarget
,根据字面意思就知道是对什么进行入栈出栈。
你能够看到是该方法来自于dep,具体函数实现以下:
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码
显然,pushTarget和popTarget操做的对象是Watcher,存放在全局变量targetStack
中。每次出栈入栈都会更新Dep.target的值,而它值由上可知是targetStack的栈顶元素。
如今就知道pushTarget(this)
的意思是:将当前的watcher入栈,并设置Dep.Target为当前watcher。
而后就是执行:
value = this.getter.call(vm, vm)
复制代码
计算属性watcher的getter是什么?
watcher.getter = typeof userDef === 'function' ? userDef : userDef.get
复制代码
是用户定义的回调函数,计算属性的回调函数。 回顾这一节开头的结论:
用户定义的回调则是在计算属性getter的逻辑中进行调用。
到此,咱们就能够清晰知道:用户定义的getter是在计算属性的getter中的computedWatcher.evaluate()中的computedWatcher.value = computedWatcher.get()中调用!
调用完getter算是完事没有呢?没有,这里还有一层隐藏的逻辑!
咱们知道通常计算属性都依赖于$data
的属性,而调用计算属性的回调函数就会访问这些属性,就会触发这些属性的getter。
这些基础属性的getter就是隐藏的逻辑,若是你有看过基础属性的数据劫持就知道他们的getter都是有收集依赖的逻辑。
这些基本属性的getter都是在数据劫持的时候定义的,咱们去看看会发生什么!
Object.defineProperty(obj, key, {
// ...
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// ...
}
return value
},
// ...
}
复制代码
记得刚刚调用了pushTarget
吧,如今Dep.target
已经不为空,而且Dep.target
就是当前计算属性的watcher。
则会执行dep.depend()
,dep是每一个$data
属性关联的(经过闭包关联)。
dep是依赖收集器,收集watcher,用一个数组(dep.subs)存放watcher,
而执行dep.depend()
,除了执行其余逻辑,里面还有一个关键逻辑就是将Dep.target
push到当前属性关联的dep.subs,言外之意就是,计算属性的访问在条件适合的状况下是会让计算属性所依赖的属性收集它的wathcer,而这个收集操做的做用且听下回分解。
计算属性所依赖属性的dep收集computed-watcher的意义何在呢?
假如如今更新计算属性依赖的任一个属性,会发生什么?
更新依赖的属性,固然是触发对应属性的setter,首先来看看基础属性setter的定义。
Object.defineProperty(obj, key, {
// ...
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
})
复制代码
首先是在setter里面调用dep.notify()
,通知变更。dep固然就是与属性关联的依赖收集器,notfiy必然是去通知订阅者它们订阅的数据之一已经发生变更。
export default class Dep {
// ...
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
复制代码
在notify方法里面能够看出,遍历了当前收集里面全部(订阅者)watcher,而且调用了他们的update方法。
在计算属性被访问时的运行机制已经知道,计算属性的watcher是会被它所依赖属性的dep收集的。所以,notify
中的subs
确定也包含了计算属性的watcher。
因此,计算属性所依赖属性变更是经过调用计算属性watcher的update方法通知计算属性的。
接下来,在深刻去看看watcher.update是怎么更新计算属性的。
export default class Watcher {
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
复制代码
在计算属性被访问时的运行机制中就知道,计算属性watcher是lazy的,因此,comuptedWatcher.update的对应逻辑就是下面这一句:
this.dirty = true
复制代码
再回想一下计算属性被访问时的运行机制中计算属性getter调用evalute()的控制流逻辑(if(watcher.dirty)
),这下计算属性的访问和他的被动更新就造成闭环!
每次变化通知都是只更新脏值状态,真是计算仍是访问的时候再计算
从上面咱们就知道通知计算属性“变化”是不会直接引起计算属性的更新!
那么问题就来了,现实咱们看到的是:绑定的视图上的计算属性的值,只要它所依赖的属性值更新,会直接响应到视图上。
那就说明在通知完以后,当即访问了计算属性,引发了计算属性值的更新,而且更新了视图。
对于,不是绑定在视图上的计算属性很好理解,毕竟咱们也是在有须要的时候才会去访问他,至关于即时计算了(假如是脏值),所以不管是不是即时更新都无所谓,只要在访问时能够拿到最新的实际值就好。
可是对于视图却不同,要即时反映出来,因此确定是还有更新视图这一步的,咱们如今须要作的测试找出vue是怎么作的。
其实假如你有去看过vue数据劫持的逻辑就知道:在访问属性时,只要当前的Dep.target(订阅者的引用)不为空,与这个属性关联的dep就会收集这个订阅者
这个订阅者之一是“render-watcher”,它是视图对应的watcher,只要在视图上绑定了的属性都会收集这个render-watcher,因此每一个属性的dep.subs
都有一个render-watcher。
没错,就是这个render-watcher完成了对计算属性的访问与视图的更新。
到这里咱们就能够小结一下计算属性对所依赖属性的响应机制: 所依赖属性更新,会通知该属性收集的全部watcher,调用update方法,其中就包含计算属性的watcher(computed-watcher),若是计算属性绑定在视图上,则还包含render-watcher,computed-watcher负责更新计算属性的脏值状态,render-watcher负责更新访问计算属性和更新视图。
可是这里又引出了一个问题!
假设如今计算属性就绑定在视图上,那么如今计算属性响应更新就须要两个watcher,分别是computed-watcher和render-watcher。
你细心点就会发现,要达到预期的效果,对这两个watcher.update()的调用顺序是有要求的!
必需要先调用computed-watcher.update()更新脏值状态,而后再调用render-watcher.update()去访问计算属性,才会去从新算计算属性的值,否者只会直接缓存的值watcher.value。
好比说有模板是
<span>{{ attr }}<span>
<span>{{ computed }}<span>
复制代码
attr的dep.subs中的watcher顺序就是
状况1:
[render-watcher, computed-watcher]
复制代码
反之就是
状况2:
[computed-watcher, render-watcher]
复制代码
咱们知道deo.notify的逻辑遍历调用subs里面的每一个watcher.update
假如这个遍历的顺序是按照subs数组的顺序来更新的话,状况1就会有问题
状况1
是先触发视图watcher的更新,他会更新视图上全部绑定的属性,不论属性有没有更新过
然而此时computed-watcher
的属性dirty
仍是 false
,这意味这着这个计算属性不会从新计算,而是使用已有的挂在watcher.value
的旧值。
若是真是如此,以后在调用computred-watcher的update也没有意义了,除非从新调用render-watcher的update方法。
很明显,vue不可能那么蠢,确定会作控制更新顺序的逻辑
咱们看看notify方法的逻辑:
notify (key) {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
复制代码
你能够看到控制流里面确实作了顺序控制
可是process.env.NODE_ENV !== 'production' && !config.async
的输出是false呢
很直观,在生成环境就进不了这个环境!
然而,现实表现出来的结果是,就算没有进入这个控制流里面,视图仍是正确更新了
更使人惊异的是:更新的遍历顺序确实是按着[render-watcher, computed-watcher]
进行的
你能够看到是先遍历了render-watcher
(render-watcher的id确定是最大的,越日后建立的watcher的id越大,计算属性是在渲染前建立,而render-watcher则是在渲染时)
可是若是你细心的话你能够发现,render-watcher更新回调是在遍历完全部的watcher以后才执行的(白色框)
咱们再来看看watcher.update
的内部逻辑
update () {
/* istanbul ignore else */
console.log(
'watcher.id:', this.id
);
if (this.lazy) {
this.dirty = true
console.log(`update with lazy`)
} else if (this.sync) {
console.log(`update with sync`)
this.run()
} else {
console.log(`update with queueWatcher`)
queueWatcher(this)
}
console.log(
'update finish',
this.lazy ? `this.dirty = ${this.dirty}` : ''
)
}
复制代码
根据打印的信息,能够看到render-watcher进入了else的逻辑,调用queueWatcher(this)
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
console.log('queueWatcher:', queue)
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
复制代码
根据函数名,能够知道是个watcher的队列
has是一个用于判断待处理watcher是否存在于队列中,而且在队中的每一个watcher处理完都会将当前has[watcher.id] = null
flushing这个变量是一个标记:是否正在处理队列
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
复制代码
以上是不一样的将待处理watcher推入队列的方式。
而后接下来的逻辑,才是处理watcher队列
waitting
和flushing
这两个标志标量大体相同,他们都会在watcher队列处理完以后重置为false
而不一样的是waitting在最开始就会置为true,而flushing则是在调用flushSchedulerQueue
函数的时候才会置为true
nextTick(flushSchedulerQueue)
复制代码
这一句是关键,nextTick,能够理解为一个微任务,即会在主线程任务调用完毕以后才会执行回调,
此时回调便是flushSchedulerQueue
。
关于nextTick能够参考Vue:深刻nextTick的实现
这样就能够解析:
更使人惊异的是:更新的遍历顺序确实是按着
[render-watcher, computed-watcher]
进行的可是若是你细心的话你能够发现,render-watcher更新回调是在遍历完全部的watcher以后才执行的(白色框)
在计算属性的更新机制中咱们知道了计算属性所依赖属性的dep是会收集computed-watcher的,目的是为了通知计算属性当前依赖的属性已经发生变化。
那么计算属性为何要收集依赖?是如何收集依赖的?
“计算属性所依赖属性的dep具体怎么收集computed-watcher”并无展开详细说。如今咱们来详细看看这部分逻辑。那就必然要从第一次访问计算属性开始, 第一次访问必然会调用watcher.evaluate
去算计算属性的值,那就是必然会调用computed-watcher.get()
,而后在get方法里面去调用用户定义的回调函数,算计算属性的值,调用用户定义的回调函数就必然会访问计算属性所依赖属性,那就必然触发他们的getter,没错咱们就是要从这里开始看详细的逻辑,也是从这里开始收集依赖:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// ...
}
return value
},
// ...
}
复制代码
计算属性依赖的属性经过dep.depend()
收集computed-watcher
,展开dep.depend()
看看详细逻辑:
// # dep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
复制代码
很显然如今的全局watcher就是computed-watcher,而this
则是当前计算属性所依赖属性的dep(下面简称:prop-dep
),继续展开computed-watcher.addDep(prop-dep)
。
// # watcher.js
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)
}
}
}
复制代码
在dep收集watcher的以前(dep.addSub(this)),watcher也在收集dep。
`this.newDeps.push(dep)`
复制代码
watcher收集dep就是接下来咱们要说的点之一!
另外,上面的代码中还包含了以前没见过的三个变量this.newDepIds
,this.newDeps
,this.depIds
先看看他们的声明:
export default class Watcher {
// ...
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// ...
}
复制代码
depIds
和newDepIds
都是Set的数据结构,结合if (!this.newDepIds.has(id))
和!this.depIds.has(id)
就能够推断他们的功能是防止重复操做的。
到此,咱们知道了计算属性是如何收集依赖的!而且,从上面知道了所收集的依赖是不重复的。
可是,到这里尚未结束!
这个newDeps
并非最终存放存放点,真实的dep存放点是deps,在上面声明你就能够看见它。
在调用computed-watcher.get()
的过程当中还有一个比较关键的方法没有给出:
get () {
// ...
// 在最后调用
this.cleanupDeps()
}
复制代码
形如其名,就是用来清除dep的,清除newDeps,而且转移newDeps到Deps上。
cleanupDeps () {
let i = this.deps.length
// 遍历deps,对比newDeps,看看哪些dep已经没有被当前的watcher收集
// 若是没有,一样也解除dep对当前watcher的收集
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 转存newDepIds到depIds
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
// 转存newDeps到Deps
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
复制代码
下面是执行完computed-watcher.get()
后的打印信息:
从上面的分析咱们能够知道:计算属性的watcher会在计算值(watcher.evalute())时,收集每一个它依赖属性的dep,并最后存放在watcher.deps
中
接下来再来探究计算属性为何要收集依赖。
还记得计算属性的getter中的另外一个控制流,一直没有展开细说。
if (Dep.target) {
watcher.depend()
}
复制代码
从这段代码能够知道,只有全局watcher(Dep.target)不为空,才会执行watcher.depend()
,这就是要想的第一个问题:什么状况下全局watcher是不为空?
首先来确认下全局watcher的update机制:
还记得computed的getter的逻辑吧!
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
复制代码
在脏值状态下会执行watcher.evaluate()
,执行完已经完成watcher.get()的调用,因此watcher.evaluate不会影响到下面的if (Dep.target)
判断。
pushTarget和popTarget是成对出现的,显然只有在调用完pushTarget
后,且未调用popTarget这个时间段内调用计算属性才会执行watcher.depend()
。另外,只有watcher.get()才会入栈非空的watcher,因此咱们就能够再次缩小范围到:在调用watcher.get()的过程当中访问了计算属性!
记得在计算属性被访问时的运行机制中有用表格对比过新建普通watcher和计算属性watcher实例的异同,其中普通watcher的建立就会在实例化的时候调用this.get()
。
此刻让我想到了render-watcher
,它就是一个普通的watcher,并且render-watcher是会访问绑定在视图上的所用属性,并且它访问视图上属性的过程就是在get方法里面的getter的调用中。
get () {
// 那么全局watcer就是render-watcher了
pushTarget(this)
// ...
try {
// 视图上的全部属性都在getter方法被访问,包括计算属性
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
// ...
popTarget()
this.cleanupDeps()
}
return value
}
复制代码
接下展开watcher.depend看看:
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
复制代码
已经很明了,上面已经说过this.deps是计算属性收集的dep(它所依赖的dep),而后如今遍历deps,调用dep.depend()
,上面也一样已经说过dep.depend()
的功能是收集全局watcher。
因此,watcher.depend()
的功能就是让计算属性收集的deps去收集当前的全局watcher。 而如今的全局watcher就是render-watcher!
如今咱们知道watcher.depend
的功能是让prop-dep去收集全局watcher,可是为何要这么作? 不放将问题细化到render-watcher的场景上。为何prop-watcher要去收集render-watcher?
首先,我要再次强调:一个绑定在视图上的计算属性要即时响应所依赖属性的更新,那么这些依赖属性的dep.subs就必须包含computed-watcher
和render-watcher
,前者是用来更新计算属性的脏值状态,后者用来访问计算属性,让计算属性从新计算。并更新视图。
计算属性所依赖属性的dep.subs中确定会包含computed-watcher
,这一点不须要质疑,上面已经证实分析过!
可是,是否会包含render-watcher
就不必定了!首先上面也有间接地提过,绑定在视图上的属性,它的dep会收集到render-watcher。那么,计算属性所依赖的属性,有可能存在一些是没有绑定在视图上,而是直接定义在data
上而已,对于这些属性,它的dep.subs是确定没有render-watcher
的了。没有render-watcher
意味着没有更新视图的能力。那么怎么办?那固然就是去保证它!
而watcher.depend()
就起到了这个做用!它让计算属性所依赖的属性
对于这个推测
绑定在视图上的属性,它的dep会收集到render-watcher
咱们能够探讨一下。
要一个vue.$data属性的dep去收集dep.subs没有的watcher须要具有两个条件:
而没有绑定在视图上的属性,在render-watcher.get()调用的过程当中就没有访问,没有访问就不会调用dep.depend()
去收集render-watcher
!
可能有人会问,在访问计算属性的时候不是有调用用户定义的回调吗?不就访问了这些依赖的属性?
是!确实是访问了,那个时候的Dep.target是computed-watcher。
ok,render-watcher这个场景也差很少了。咱们该抽离表象看本质!
首先想一想属性dep为何要收集依赖(订阅者),由于有函数依赖了这个属性,但愿这个属性在更新的时候通知订阅者。能够以此类比一下计算属性,计算属性的deps为何须要收集依赖(订阅者),是否是也是由于有函数依赖了计算属性,但愿计算属性在更新时通知订阅者,在想深一层:怎么样才算是计算属性更新?不就是它所依赖的属性发生变更吗?计算属性所依赖属性更新 = 计算属性更新,计算属性更新就要通知依赖他的订阅者!再想一想,计算属性所依赖属性更新就能够直接通知依赖计算属性的订阅者了,那么计算属性所依赖属性的dep直接收集依赖计算属性的订阅者就行了!这不就是watcher.depend()
在作的事情吗?!
本质咱们知道了,可是怎么才能够实现依赖计算属性!
首先全局watcher不为空! 怎么才会让Dep.target不为空!只有一个方法:调用watcher.get()
,在vue里面只有这个方法会入栈非空的watcher,另外咱们知道pushTarget和popTarget是成对出现的,即要在未调用popTarget前访问计算属性,怎么访问呢?pushTarget和popTarget分别在get方法的一头一尾,中间能够用户定义的只有一个地方!
get () {
pushTarget(this)
// ...
value = this.getter.call(vm, vm)
// ...
popTarget()
}
复制代码
就是getter,getter是能够由用户定义的~
再来getter具体存储的是什么
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
// ...
}
}
// ...
}
复制代码
由上能够知道,一个有效的getter是有expOrFn决定,expOrFn若是是Function
则getter就是用户传入的函数!若是是String
则由parsePath进行构造:
// 返回一个访问vm属性(包含计算属性)的函数
export function parsePath (path: string): any {
// 判断是不是一个有效的访问vm属性的路径
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
复制代码
由上可知,咱们有两种手段可让getter访问计算属性: 而且在此我不作说明,直接说结论,watch一个属性(包含计算属性),包括使用$watch
都是会建立一个watcher实例的,并且是普通的watcher,即会在构造函数直接调用watcher.get()
。
const vm = new Vue({
data: {
name: 'isaac'
},
computed: {
msg() { return this.name; }
}
watch: {
msg(val) {
console.log(`this is computed property ${val}`);
}
}
}).$mount('#app');
复制代码
这种方法就是在建立实例时传进了一个路径,这个路径就是msg
,即expOrFn是String
,而后由parsePath
构造getter,从而访问到计算属性。
$watch
监听一个函数,函数中包含计算属性 vm.$watch(function() {
return this.msg;
}, function(val) {
console.log(`this is computed property ${val}`);
});
复制代码
这种方法直接就传入一个函数,即expOrFn是Function
,就是$watch
的第一个参数!一样在getter中访问了计算属性。
上面两种都是在getter中访问了计算属性,从而让deps收集订阅者,计算属性的变更(固然并不是真的更新了值,只是进入脏值状态)就会通知依赖他的订阅者,调用watcher.update()
,若是没有传入什么特殊的参数,就会调用watch的回调函数,若是在回调函数中有访问计算属性就会从新计算计算属性,更新状态为非脏值!
prop-dep.subs
中;computed-watcher.deps
中,为了确保计算属性得到通知依赖他的订阅者能够监听到他的变化,经过watcher.depend()
来收集依赖它的订阅者。watcher.depend()
来收集依赖它的订阅者。