vue源码-对于「计算属性」的理解
这是我最近学习vue源码的一个个人总结和理解,所以可能并不适合每一位读者
本文的整体脉络如下,首先尽可能去掉细节,对计算属性源码的大致实现有一个了解,然后举一例子,分别谈谈计算属性依赖收集和派发更新的流程。
- 计算属性的源码实现
- 举例来说,谈谈页面初次渲染时,整个依赖收集的过程
- 举例来说,计算属性的依赖被修改时,派发更新的过程
另外推荐2个开源的vue源码分析集合
计算属性的源码实现
- _init() --> initState() --> initComputed()
- 1.遍历computed选项,2.实例化computed watcher 3.defineComputed()
- defineComputed()核心就是把计算属性用Object.defineProperty包装成响应式对象,而getter就是把用户传入的函数作为getter
- 但是准确的说,是用户传递的fn的返回值是作为计算属性getter的return值,但是除此之外计算属性在getter时还做了一些其他的操作
- 1是watch.depend() 2.return watch.evaluate()。 也就是1.收集依赖2.把值返回
this._init() : 重点关注重点init方法中initState
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
initState() 重点关注这一句 if (opts.computed) initComputed(vm, opts.computed)
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initComputed() 核心就是遍历computed,每次循环都实例化一个computed watch,并且用defineComputed把计算属性包装成响应式对象
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 = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && 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 (process.env.NODE_ENV !== 'production') {
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)
}
}
}
}
defineComputed() 核心就是Object.defineProperty ,大段代码都是在给它设置get和set
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter : 计算属性的getter就是这个computedGetter,做了2件事情,1.收集render watcher作为自己的依赖,2.调用用户的那个函数作为返回值
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
到目前为止就结束了,也就是说初始化computed watcher都没有求值
直到render时,才会触发computed watcher的getter
举例来说,谈谈页面初次渲染时,整个依赖收集的过程
比如我们有一个计算属性,并且fullName是渲染在模板中的。
computed: {
fullName(){
return this.firstName + this.lastName
}
}
那么页面初次渲染时,整个依赖收集的过程如下
- render函数执行时,会读取计算属性fullName,那么会触发fullName的getter,那么就会执行到watch.depend()和return watch.evaluate()
- 这个computed watcher的depend()会把render watcher作为依赖收集到它的subs里。
- 这个computed watcher的evaluate()主要是把调用了用户给的那个函数,求值并返回
- 最后值得注意的是,调用用户的函数,也就是执行了this.firstName + this.lastName ,那么也会触发他们的getter,所以他们也会把computed watcher作为依赖,收集到subs里,将来如果被修改的话,用通知subs里的watch调用update,也就是去派发更新
举例来说,计算属性的依赖被修改时,派发更新的过程
- 当this.firstName或者this.lastName被修改时,会触发他们的setter,setter就干两个事情。1是赋值 2是派发更新
- 所以会通知他们的subs里的watch去调用自己的update方法。其中也包括computed watcher,
- 那么computed watcher在update方法跟普通的user watcher的update存在区别,computed watcher并不是直接推入异步更新队列,而是 this.dep.notify()发出之前计算属性所收集的依赖去派发更新,其中就包括render watcher,调用render watcher的update就会实现视图更新了
- 注意this.getAndInvoke的作用,就是如果this.lastName和this.firstName变化了,但是经过计算之后,计算属性的值不变,那么也不会触发notify的,也就不会更新视图
update () {
if (this.computed) {
if (this.dep.subs.length === 0) {
this.dirty = true
} else {
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这也是为什么,我们说计算属性的依赖属性不被修改的话,计算属性就不会变化。因为getter就是你那个函数嘛,而且其实就算依赖变化了,只要计算之后的计算属性变,也不会触发视图更新。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。