计算属性computed
<body>
<div id="app">
<div ref="name">{{getTotal}}</div>
<button @click="change">change</button>
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
return {
a: 1,
}
},
computed: {
getTotal() {
return this.a + 7
}
},
methods: {
change() {
this.a = 2
}
}
})
</script>
</body>
接下来以上面这个例子举例分析
为每个计算属性创建watcher
src/core/instance/state.ts
export function initState(vm: Component) {
const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed)
}
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
// $flow-disable-line
const watchers = (vm._computedWatchers = Object.create(null))
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = isFunction(userDef) ? userDef : userDef.get
if (__DEV__ && getter == null) {
warn(`Getter is missing for computed property "${key}".`, vm)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (__DEV__) {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(
`The computed property "${key}" is already defined as a method.`,
vm
)
}
}
}
}
- 首先定义
watchers和vm._computedWatchers:const watchers = (vm._computedWatchers = Object.create(null)) - 遍历computed选项,
const userDef = computed[key],获取每一个计算属性的getter,用户写的计算属性,要么是一个函数,要么就自己写一个get函数,否则在开发环境下,会抛出警告; - 在浏览器环境下,为每一个计算属性创建一个watcher,存到
watchers和vm._computedWatchers中,参数getter就是 getTotalCount, - 如果key不存在当前实例
vm,就调用defineComputed(vm, key, userDef),同时计算属性不能与data,props,methods中的属性重名,否则会抛出警告
计算属性的watcher
src/core/observer/watcher.ts
export default class Watcher implements DepTarget {
vm?: Component | null
expression: string
cb: Function
lazy: boolean
dirty: boolean
getter: Function
value: any
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this
}
if (options) {
this.lazy = !!options.lazy
} else {
}
this.cb = cb
this.dirty = this.lazy
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy ? undefined : this.get()
}
}
computedWatcherOptions = {lazy:true},故而,在getTotal计算watcher的构造函数中:this.lazy = true,this.dirty = this.lazy = true,this.value = this.lazy ? undefined : this.get(),计算属性没有立即求值,this.value = undefined
读取计算属性
src/core/instance/state.ts
export function defineComputed(
target: any,
key: string,
userDef: Record<string, any> | (() => any)
) {
const shouldCache = !isServerRendering()
if (isFunction(userDef)) {
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
}
if (__DEV__ && sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
- 通过Object.defineProperty给每个计算属性设置getter和setter,我们平时比较少给计算属性写set,这里我们主要关注getter,
- 通过
isFunction(userDef)判断用户写的计算属性是函数还是一个对象形式,如果是函数形式,并且是在浏览器环境下,shuoldCache为true,计算属性的getter = createComputedGetter(key),如果是对象形式,根据shouldCache && userDef.cache !== false判断,也会得到getter = createComputedGetter(key), - 所以当我们在模板中读取
getTotalCount时,得到的是createComputedGetter(key)
接下来看一下createComputedGetter(key)
src/core/instance/state.ts
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
}
}
}
watcher = this._computedWatchers && this._computedWatchers[key] ,取出计算属性getTotal对应的watcher,如果dirty为true,才会更新,调用watcher.evaluate()到底做了什么,
export default class Watcher implements DepTarget {
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate() {
this.value = this.get()
this.dirty = false
}
}
模板第一次读取getTotal,dirty为true,因为computedWatcherOptions = { lazy: true }作为watcher的option参数传入,接着执行this.get后,把dirty置为false
重要的逻辑来了:
export default class Watcher implements DepTarget {
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
} finally {
popTarget()
}
return value
}
}
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
通过专栏前面分析,模板读取了getTotal,targetStack=[ 渲染watcher ],
执行pushTarget(this):
targetStack=[ 渲染watcher,getTotal计算watcher ],
Dep.target=getTotal计算watcher,
然后执行value = this.getter.call(vm, vm)又读取了响应式数据a,触发a的getter,a的dep实例执行dep.depend(),接着 Dep.target.addDep(this),
export default class Dep {
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
}
export default class Watcher implements DepTarget {
addDep(dep: Dep) {
//省略去重优化
dep.addSub(this)
}
}
}
getTotal计算watcher收集了a的dep,同时a的dep也收集了getTotal计算watcher
接着执行popTarget():
targetStack=[ 渲染watcher ]
Dep.target=渲染watcherr,
在get中返回value值8,然后把this.dirty置为false
接着执行 if (Dep.target) { watcher.depend() },此时Dep.target=渲染watcherr,这个watcher是getTotal计算watcher,
export default class Watcher implements DepTarget {
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
通过这个方法,调用了a的dep.depend,然后又是Dep.target.addDep(this),dep.addSub(this),此时:
a的subs = [ getTotal计算watcher,渲染watcher]
计算属性中依赖的响应式数据更新
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
const dep = new Dep()
set: function reactiveSetter(newVal) {
// ...
dep.notify()
// ...
})
}
export default class Dep {
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
export default class Watcher implements DepTarget {
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
当a更新,调用a的dep.notify,变量a的subs,按照顺序:
先调用getTotal计算watcher的update和渲染watcher的update,因为getTotal计算watcher的lazy是true,所以这里只是把this.dirty = true,
接着调用渲染watcher,渲染watcher在模板中会读取计算属性getTotal,继续走到total的getter:
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
}
}
}
dirty为true了,就更新watcher.evaluate(),通过watcher.value拿到最新值渲染到页面
结论:
1. 计算属性watcher中lazy为true,dirty也是true,第一次读取计算属性,在它的getter中,计算最新值,同时dirty改为false
2. 此后无论怎么读取计算属性,由于dirty为false,都不会重新计算执行watcher.evaluate()
3. 只有当计算属性依赖的数据变化时,依赖数据会触发set,会调用它收集的计算属性watcher的update方法,把dirty改回true,读取计算属性就会重新计算
4. 但是,如果计算属性中依赖的状态变化,但是计算属性的返回值没有变化,组件渲染watcher也会执行,只是页面上没有UI变化看不出来