携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
前言
源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大
计算属性的初始化
计算属性可以写成一个函数形式,也可以写成对象形式,但对象形式必须要有get方法
// /src/core/instance/state.js
function initComputed (vm: Component, computed: Object) {
// 定义缓存watcher的值
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 判断key是否在vm上定义过
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
...
}
if (!isSSR) {
// 将每个属性初始化watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,//回调函数noop()=>{}
computedWatcherOptions// 配置,初始值为{lazy:true}表示为计算属性
)
}
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 判断是不是ssr
const shouldCache = !isServerRendering()
//当计算属性为方法时,定义get方法
if (typeof userDef === 'function') {
// 重写get方法
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
// 获取对象时的get方法
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
// 将计算属性放到vm上并对计算属性的get和set做劫持
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 重写计算属性的get方法,判断是否需要重新计算
function createComputedGetter (key) {
return function computedGetter () {
// 获取相应计算属性key定义的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果是脏的,需要重新求值,初始值时true,初次要经过一次计算
if (watcher.dirty) {
watcher.evaluate()
}
// 如果dep还存在target,这时候一般为渲染watcher,计算属性依赖的数据也需要收集
// 在对象形式时,这一步非常重要,在重新计算完值后,全局的栈中还有一个渲染watcher,得靠他去更新视图
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
watcher中计算属性相关
重点两个变量lazy:表示是否为计算属性,计算属性初始化时默认值为true和dirty:表示是否为脏数据,如果为脏数据,需要重新计算,计算属性初始化时默认值为true
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
vm._watchers.push(this) //为了能强制更新
// options
if (options) {
this.user = !!options.user
this.lazy = !!options.lazy// 标识计算属性watcher
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.active = true
this.dirty = this.lazy // 表示watcher是否需要重新计算,默认为true
this.deps = []
this.newDeps = []
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
// 非计算属性实例化会默认调用get方法进行取值,计算属性的实例化时候不会去调用get
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 计算属性在这里执行用户定义的get函数,访问计算属性依赖项 从而把自身计算watcher添加到依赖项dep里面收集起来
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps() //不清理 可能上次的数据还要被再次收集 vm.a = [1,2,3] => vm.a = {}数组不需要在收集
}
return value
}
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)
}
}
}
update () {
// 计算属性的依赖值发生了变化,只需要把dirty设置为true,下次访问到了就重新计算
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 异步队列机制
queueWatcher(this)
}
}
run () {
if (this.active) {
// 为当前watcher设置最新值,此处的value和oldValue是为watch功能做工作
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// 获取值,并把dirty设置为false,表示已经计算过值
evaluate () {
this.value = this.get()
this.dirty = false
}
depend () {
// 计算属性的watcher储存了依赖项的dep
let i = this.deps.length
while (i--) {
this.deps[i].depend()//调用依赖项的dep去收集watcher,将当前watcher储存起来
}
}
}
通过案例掌握计算属性执行逻辑
理一理计算属性的执行逻辑,分两种情况:
- 函数形式
<body>
<div id = "app">
<li>{{sum}}</li>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
one: 1,
two: 2
},
computed:{
sum(){
return this.one + this.two
}
}
})
setTimeout(() => {
vm.two = 3
}, 3000)
</script>
</body>
one和two是在data里定义的属性,在初始化数据时做了劫持(Object.defineProperty)。computed也做了初始化,函数形式不会重写get方法,只是定义了watcher,同时利用Object.defineProperty将计算属性定义在vm上做了劫持(这里的劫持和data的劫持是不一样的)。渲染时会访问sum,它会去执行sum函数,执行函数时会访问one和two,触发get劫持,会将当前的渲染watcher(因为在$mount时,Dep.target是渲染watcher),添加到自己的dep中,同时渲染watcher会将one和two也存起来,返回其值,此时sum变为3
在3秒后,改变two的值,触发two的set方法,如果与上次的值不一样,执行notify方法就会通知dep里存的watcher去更新,此时,two的dep里只有渲染watcher,执行渲染watcher的update方法更新视图
- 对象形式
<body>
<div id = "app">
<li>{{sum}}</li>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
one: 1,
two: 2
},
computed:{
sum: {
get: function(){
return this.one + this.two
}
}
}
})
setTimeout(() => {
vm.two = 3
}, 3000)
</script>
</body>
与函数形式不同的是,如果是对象形式,在初始化计算属性时会重写get方法,渲染时会访问sum,会被get劫持,如果dirty是true,就会调用计算属性watcher的evaluate方法进行重新求值,同时会将dirty设为false,表示已经取过值,此处注意,在重新求值时会调用自定义的get方法,此时是计算属性watcher,此时全局的栈中有两个watcher[渲染watcher,计算属性watcher],在访问one和two时,会将计算属性watcher添加到每个属性dep中,此时计算属性watcher中有one,two两个dep,重要一步:经过计算后,全局还有一个渲染watcher,所以,得将渲染watcher存放在one和two的watcher队列中[计算属性watcher,渲染watcher],如果缺少这一步,在后面更改one或者two值时将不会更新视图,得到计算的值,此时sum为3
在3秒后,改变two的值,触发two的set方法,如果与上次的值不一样,执行notify方法就会通知dep里存的所有watcher去更新,也就是执行update方法,首先执行的是计算属性watcher的update方法,他会将dirty设置true,代表值发生变化,接着执行渲染watcher的update方法,此时会更新视图,更新视图是会再次访问sum,由于dirty为true,所以会再次执行evaluate方法进行重新求值,得到计算值为4
注意点
- 计算属性可以是一个函数,也可以是对象,当为对象时,必须设置
get函数 - 计算属性的命名不能和
data、methods、prop重名,在初始化时会做校验,也就是说,可以用watch去监听计算属性的
总结
计算属性有两种书写方式,一种是对象形式,一种是函数形式。重点在于对象形式的watcher收集,在重写get方法时在经过计算后会将渲染watcher添加到每个依赖的值中,那么计算属性依赖的值会收集计算属性watcher和渲染watcher两个,在改变其中一个依赖值时,首先会触发计算属性watcher的update方法进行更新值,之后在调用渲染watcher的update方法更新视图
系列链接
【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理
【Vue2.x原理剖析六】diff算法原理
【Vue2.x原理剖析七】组件原理