浅析computed计算属性的实现原理
API
,但它是Watcher
类最复杂的一种实例化的使用,还是很有必要分析的。其实主要就是分析计算属性为何可以做到当它的依赖项发生改变时才会进行重新的计算,否则当前数据是被缓存的。计算属性的值可以是对象,这个对象需要传入get
和set
方法,这种并不常用,所以这里的分析还是介绍常用的函数形式,它们之间是大同小异的,不过可以减少认知负担,聚焦核心原理实现。一、计算属性的初始化
function initState(vm) { // 初始化所有状态时
vm._watchers = [] // 当前实例watcher集合
const opts = vm.$options // 合并后的属性
... // 其他状态初始化
if(opts.computed) { // 如果有定义计算属性
initComputed(vm, opts.computed) // 进行初始化
}
...
}
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
for(const key in computed) {
const getter = computed[key] // computed每项对应的回调函数
watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 实例化computed-watcher
...
}
}
其实主要就2点:
(1)若合并options中有computed,则执行 initComputed() 方法
(2)初始化方法里也就是声明一个纯净的对象,用于保存key对应的watcher。然后遍历computed,拿到其每个key对应的回调函数,然后进行 new Watcher() 将回调函数传入,并将实例保存在纯净对象上
二、计算属性的实现原理
如上初始化的时候,其实还是按照惯例,将定义的computed
属性的每一项使用Watcher
类进行实例化,不过这里是按照computed-watcher
的形式,来看下如何实例化的:
class Watcher{
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this._watchers.push(this)
if(options) {
this.lazy = !!options.lazy // 表示是computed
}
this.dirty = this.lazy // dirty为标记位,表示是否对computed计算
this.getter = expOrFn // computed的回调函数
this.value = undefined
}
}
这里就点到为止,实例化已经结束了。并没有和之前render-watcher
以及user-watcher
那般,执行get
方法,这是为什么?我们接着分析为何如此,补全之前初始化computed
的方法:
function initComputed(vm, computed) {
...
for(const key in computed) {
const getter = computed[key] // // computed每项对应的回调函数
...
if (!(key in vm)) {
defineComputed(vm, key, getter)
}
... key不能和data里的属性重名
... key不能和props里的属性重名
}
}
这里的App
组件在执行extend
创建子组件的构造函数时,已经将key
挂载到vm
的原型中了,不过之前也是执行的defineComputed
方法,所以不妨碍我们看它做了什么:
function defineComputed(target, key) {
...
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: createComputedGetter(key),
set: noop
})
}
这个方法的作用就是让computed
成为一个响应式数据,并定义它的get
属性,也就是说当页面执行渲染访问到computed
时,才会触发get
然后执行createComputedGetter
方法,所以之前的点到为止再这里会续上,看下get
方法是怎么定义的:
function createComputedGetter (key) { // 高阶函数
return function () { // 返回函数
const watcher = this._computedWatchers && this._computedWatchers[key] // 得到key对应的computed-watcher
if (watcher) {
if (watcher.dirty) { // 在实例化watcher时为true,表示需要计算
watcher.evaluate() // 进行计算属性的求值
}
if (Dep.target) { // 当前的watcher,这里是页面渲染触发的这个方法,所以为render-watcher
watcher.depend() // 收集当前watcher
}
return watcher.value // 返回求到的值或之前缓存的值
}
}
}
class Watcher {
...
evaluate () {
this.value = this.get() // 计算属性求值
this.dirty = false // 表示计算属性已经计算,不需要再计算
}
depend () {
let i = this.deps.length // deps内是计算属性内能访问到的响应式数据的dep的数组集合
while (i--) {
this.deps[i].depend() // 让每个dep收集当前的render-watcher
}
}
}
这里的变量watcher
就是之前computed
对应的computed-watcher
实例,接下来会执行Watcher
类专门为计算属性定义的两个方法,在执行evaluate
方法进行求值的过程中又会触发computed
内可以访问到的响应式数据的get
,它们会将当前的computed-watcher
作为依赖收集到自己的dep
里,计算完毕之后将dirty
置为false
,表示已经计算过了。
然后执行depend
让计算属性内的响应式数据订阅当前的render-watcher
,所以computed
内的响应式数据会收集computed-watcher
和render-watcher
两个watcher
,当computed
内的状态发生变更触发set
后,首先通知computed
需要进行重新计算,然后通知到视图执行渲染,再渲染中会访问到computed
计算后的值,最后渲染到页面。
computed
内的响应式数据变更后触发的通知:
class Watcher {
...
update() { // 当computed内的响应式数据触发set后
if(this.lazy) {
this.dirty = true // 通知computed需要重新计算了
}
...
}
}
最后还是以一个示例结合流程图来帮大家理清楚这里的逻辑:
export default {
data() {
return {
manName: "cc",
womanName: "ww"
};
},
computed: {
newName() {
return this.manName + ":" + this.womanName;
}
},
methods: {
changeName() {
this.manName = "ss";
}
}
};
computed-watcher
,变更后通知计算属性要进行计算,也会通知页面重新渲染,渲染时会读取到重新计算后的值。