-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vue:多角度剖析计算属性的运行机制 #219
Comments
计算属性的初始化过程在创建Vue实例时调用 其中就有调用 export function initState (vm: Component) {
// ...
if (opts.computed) initComputed(vm, opts.computed)
// ...
} initState会初始化计算属性:调用 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') {
// ...
}
}
} 遍历computed 先创建计算属性的watcher实例,留意 然后定义计算属性的属性的getter和setter
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()
}
// ...
}
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)
}
那么gtter就是由 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不是用户定义的回调,而是由 计算属性getter中主要由两个if控制流,
目前掌握的信息有:
我们先来看第一个控制流: // watcher.dirty = true
if (watcher.dirty) {
watcher.evaluate()
} 根据计算属性的初始化过程中创建计算属性watcher实例时就可以看出,第一次调用watcher.dirty肯定是 但不论watcher.dirty是不是“真”,我们都要去看看“evaluate ”时何方神圣,而且肯定会有访问它的时候。 evaluate () {
this.value = this.get()
this.dirty = false
} 显然,evaluate确实是用于更新计算属性值(watcher.value)的。 另外,你可以发现在 然后你会发现一个逻辑:
一切说明计算属性是懒加载的,在访问时根据状态值来判断使用缓存数据还是重新计算。 再者,我们还可以再总结一下dirty和lazy的信息: 对比普通的watcher实例创建: 构造函数中的逻辑
综上,可以看出
dirty(脏值)的意思
lazy属性只是一个说明性的标志位,主要用来表明当前watcher是惰性模式的。 再来看看 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()函数开头的地方调用 你可以看到是该方法来自于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,存放在全局变量 现在就知道 然后就是执行: value = this.getter.call(vm, vm) 计算属性watcher的getter是什么? watcher.getter = typeof userDef === 'function' ? userDef : userDef.get 是用户定义的回调函数,计算属性的回调函数。
到此,我们就可以清晰知道:用户定义的getter是在computedWatcher.get()中调用! computedGetter() {
computedWatcher.evaluate() {
computedWatcher.value = computedWatcher.get() {
return 用户定义的getter();
}
}
} 调用完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
},
// ...
} 记得刚刚调用了 则会执行 dep是依赖收集器,收集watcher,用一个数组(dep.subs)存放watcher, 而执行 小结
|
计算属性的更新机制如何通知变动计算属性所依赖属性的dep收集computed-watcher的意义何在呢? 假如现在更新计算属性依赖的任一个属性,会发生什么? 更新依赖的属性,当然是触发对应属性的setter,首先来看看基础属性setter的定义。 Object.defineProperty(obj, key, {
// ...
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
}) 首先是在setter里面调用 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收集的。因此, 所以,计算属性所依赖属性变动是通过调用计算属性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()的控制流逻辑( 每次变化通知都是只更新脏值状态,真是计算还是访问的时候再计算 计算属性如何被更新从上面我们就知道通知计算属性“变化”是不会直接引发计算属性的更新! 那么问题就来了,现实我们看到的是:绑定的视图上的计算属性的值,只要它所依赖的属性值更新,会直接响应到视图上。 那就说明在通知完之后,立即访问了计算属性,引起了计算属性值的更新,并且更新了视图。 对于,不是绑定在视图上的计算属性很好理解,毕竟我们也是在有需要的时候才会去访问他,相当于即时计算了(假如是脏值),因此不论是不是即时更新都无所谓,只要在访问时可以拿到最新的实际值就好。 但是对于视图却不一样,要即时反映出来,所以肯定是还有更新视图这一步的,我们现在需要做的测试找出vue是怎么做的。 其实假如你有去看过vue数据劫持的逻辑就知道:在访问属性时,只要当前的Dep.target(订阅者的引用)不为空,与这个属性关联的dep就会收集这个订阅者 这个订阅者之一是“render-watcher”,它是视图对应的watcher,只要在视图上绑定了的属性都会收集这个render-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的更新,他会更新视图上所有绑定的属性,不论属性有没有更新过 然而此时 如果真是如此,之后在调用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()
}
} 你可以看到控制流里面确实做了顺序控制 但是 很直观,在生成环境就进不了这个环境! 然而,现实表现出来的结果是,就算没有进入这个控制流里面,视图还是正确更新了 更令人惊异的是:更新的遍历顺序确实是按着 你可以看到是先遍历了 但是如果你细心的话你可以发现,render-watcher更新回调是在遍历完所有的watcher之后才执行的(白色框) 我们再来看看 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的逻辑,调用 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在最开始就会置为true,而flushing则是在调用 nextTick(flushSchedulerQueue) 这一句是关键,nextTick,可以理解为一个微任务,即会在主线程任务调用完毕之后才会执行回调, 此时回调即是 关于nextTick可以参考Vue:深入nextTick的实现 这样就可以解析:
小结
|
计算属性如何收集依赖在计算属性的更新机制中我们知道了计算属性所依赖属性的dep是会收集computed-watcher的,目的是为了通知计算属性当前依赖的属性已经发生变化。 那么计算属性为什么要收集依赖?是如何收集依赖的? “计算属性所依赖属性的dep具体怎么收集computed-watcher”并没有展开详细说。现在我们来详细看看这部分逻辑。那就必然要从第一次访问计算属性开始, 第一次访问必然会调用 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.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
} 很显然现在的全局watcher就是computed-watcher,而 // # 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就是接下来我们要说的点之一! 另外,上面的代码中还包含了之前没见过的三个变量 先看看他们的声明: 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()
// ...
}
到此,我们知道了计算属性是如何收集依赖的!并且,从上面知道了所收集的依赖是不重复的。 但是,到这里还没有结束! 这个 在调用 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
} 下面是执行完 从上面的分析我们可以知道:计算属性的watcher会在计算值(watcher.evalute())时,收集每个它依赖属性的dep,并最后存放在 接下来再来探究计算属性为什么要收集依赖。 还记得计算属性的getter中的另一个控制流,一直没有展开细说。 if (Dep.target) {
watcher.depend()
} 从这段代码可以知道,只有全局watcher(Dep.target)不为空,才会执行 首先来确认下全局watcher的update机制:
还记得computed的getter的逻辑吧! if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
} 在脏值状态下会执行 pushTarget和popTarget是成对出现的,显然只有在调用完 记得在计算属性被访问时的运行机制中有用表格对比过新建普通watcher和计算属性watcher实例的异同,其中普通watcher的创建就会在实例化的时候调用 此刻让我想到了 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.subs就必须包含 计算属性所依赖属性的dep.subs中肯定会包含 但是,是否会包含 而 对于这个推测
我们可以探讨一下。 要一个vue.$data属性的dep去收集dep.subs没有的watcher需要具备两个条件:
而没有绑定在视图上的属性,在render-watcher.get()调用的过程中就没有访问,没有访问就不会调用 可能有人会问,在访问计算属性的时候不是有调用用户定义的回调吗?不就访问了这些依赖的属性? 是!确实是访问了,那个时候的Dep.target是computed-watcher。 ok,render-watcher这个场景也差不多了。我们该抽离表象看本质! 首先想想属性dep为什么要收集依赖(订阅者),因为有函数依赖了这个属性,希望这个属性在更新的时候通知订阅者。可以以此类比一下计算属性,计算属性的deps为什么需要收集依赖(订阅者),是不是也是因为有函数依赖了计算属性,希望计算属性在更新时通知订阅者,在想深一层:怎么样才算是计算属性更新?不就是它所依赖的属性发生变动吗?计算属性所依赖属性更新 = 计算属性更新,计算属性更新就要通知依赖他的订阅者!再想想,计算属性所依赖属性更新就可以直接通知依赖计算属性的订阅者了,那么计算属性所依赖属性的dep直接收集依赖计算属性的订阅者就好了!这不就是 本质我们知道了,但是怎么才可以实现依赖计算属性! 首先全局watcher不为空! 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如果是 // 返回一个访问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访问计算属性:
const vm = new Vue({
data: {
name: 'isaac'
},
computed: {
msg() { return this.name; }
}
watch: {
msg(val) {
console.log(`this is computed property ${val}`);
}
}
}).$mount('#app'); 这种方法就是在创建实例时传进了一个路径,这个路径就是
vm.$watch(function() {
return this.msg;
}, function(val) {
console.log(`this is computed property ${val}`);
}); 这种方法直接就传入一个函数,即expOrFn是 上面两种都是在getter中访问了计算属性,从而让deps收集订阅者,计算属性的变动(当然并非真的更新了值,只是进入脏值状态)就会通知依赖他的订阅者,调用 小结
|
总结
|
优秀 |
这段话是不是写反了啊?初始时,dirty为真;执行evaluate后,dirty为假;dirty为真时,才会去更新计算属性的值。 |
@ThomasCho 谢谢指出,是写反了! const computedWatcherOptions = { lazy: true }
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
this.dirty = this.lazy // for lazy watchers
}
// ...
} 脏值重新计算,否则直接返回 |
欢迎 star 和 评论
大纲
The text was updated successfully, but these errors were encountered: