准备
vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。
回顾
如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》
写在前面
computed
的内部原理较为复杂,需要对Dep
和Watcher
类需要有较深的理解,如果还有同学不理解Dep
和Watcher
类可以去看我之前的文章:理解Dep类和Watcher类。
这里我简单提两句。试想:一个data
数据会不会被多个computed
所依赖?那么一个computed
会不会同时依赖多个data
属性?
对后台数据库表关系设计有了解的同学应该知道表与表之间有一种关系:多对多的关系。多对多的关系是数据库表设计的精髓所在,在Vue中,其实Dep
和Watcher
类就是一种多对多的关系。
Dep
实例上有个属性,this.subs
,subs其实就是英文:订阅者subscriber 的缩写,在Dep
和Watcher
的关系中,订阅者为Watcher
,发布者为Dep
,所以Dep
实例上的subs
存储的就是多个Watcher
实例。Watcher
实例上有个属性this.deps
,顾名思义,就是存储多个依赖,也就是Dep
实例的。
computed
面试官经常会问:“请问computed和watch的区别是什么?”
你可能会答:“computed和watch非常类似,都是在data数据改变的过程中可以触发对应的方法。而computed不同的是,首先computed有一个懒加载机制,在初始化后如果不获取他的值,是不会触发计算的。其次computed有一个缓存机制,当data数据没有发生改变时,computed不会重新计算,而是拿出上一次计算好的值;只有当computed依赖的data发生改变时,computed才会重新计算。”
其实这就是computed的主要原理了,但是这里有几个关键词我们需要标注下:懒加载、缓存、依赖。
带着这几个关键词,去阅读源码会更加清晰。
computed
初始化
computed
初始化在_init
中:initState(vm)
。initState
中又有这样一段代码:
if (opts.computed) initComputed(vm, opts.computed);
调用initComputed
,传入vm.options.computed
。
进入initComputed
initComputed
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];
//定义getter,如果computed是个函数,getter就是函数本身,如果computed是个对象,getter就是对象的get属性
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.
//非服务器渲染环境下实例化watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions //lazy watcher 此时不会进行求值
);
}
// 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)) {
//如果compouted中的变量在vm中没有定义过,就调用 defineComputed
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
);
}
}
}
}
initComputed
主要做了三件事:
- 遍历
vm.options.computed
对象,挨个创建对应的Watcher
实例并全部挂载到vm._computedWatchers
- 对每一个
computed
计算属性都用defineComputed
方法进行处理
在创建computed watcher
时,传入getter
,当计算属性为一个对象时,getter
是对象中的get
方法;当计算属性为函数时,getter
就是计算属性函数本身。接下来传入了一个配置选项computedWatcherOptions
,在initComputed
方法定义的上面可以找到computedWatcherOptions
:
const computedWatcherOptions = { lazy: true };
接着我们进入Watcher
的构造器看看做了什么操作。
Watcher.constructor
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression =
process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
// parse expression for getter
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
);
}
}
//computed不会立刻求值
this.value = this.lazy ? undefined : this.get();
在这时,这边的关键代码只有两行:
this.dirty = this.lazy; // for lazy watchers
...
this.value = this.lazy ? undefined : this.get();
可以看到将lazy
赋值给了dirty
,至于这个dirty
有什么用我们接下来会介绍。
之后不执行this.get()
,所以此时的value
是空。
在这里就印证了我们刚刚在上面提到过的:computed有一个懒加载机制,在初始化后如果不获取他的值,是不会触发计算的。这边在初始化果然不会立即取值。
好了,我们回到initComputed
继续向下解读defineComputed
方法。
defineComputed
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;
}
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);
}
首先判断是不是服务端渲染,我们这边以客户端渲染为例,如果不是服务端渲染的话就定义shouldCache
为true
。
如果computed
是以函数形式为定义的话就设置sharedPropertyDefinition.get
为createComputedGetter(key)
执行后的结果。
如果是以对象形式定义的话,且用户设置cache
为true
,也是设置sharedPropertyDefinition.get
为createComputedGetter(key)
执行后的结果。
sharedPropertyDefinition
的定义:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
其实就是一个对象属性描述符。
在defineComputed
最后,使用Object.defineProperty
将computed
直接挂在到了Vue实例vm
上,这就是为什么我们平时可以直接使用this.xxx
来访问computed
中的属性了。在以this.xxx
访问computed
属性的过程中,就会触发刚刚定义的get
,也就是调用了createComputedGetter(key)
。
接下来,我们来解析createComputedGetter(key)
做了什么。
createComputedGetter()
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
//首次调用时:数据为 脏 状态(具体可见watcher的构造函数),所以需要立即求值
//非首次调用时:需判断数据是否为 脏 状态
//如果为 脏 状态,重新进行求值
//如果为 干净 状态,不进行重新计算
if (watcher.dirty) {
//Dep.target = computedWatcher
watcher.evaluate();
//Dep.target = renderWatcher
}
//render watcher被添加到computed watcher的依赖中
if (Dep.target) {
//Dep.target = renderWatcher
watcher.depend();
}
return watcher.value;
}
};
}
别看createComputedGetter
就这么短短几行代码,这几行可是computed
的全部精髓所在,在这里要佩服一下尤大,实在是太厉害了。
首先这是一个闭包结构,将key
放入了闭包中,这样调用多次computed
的get
都不需要去关心key
的获取。
进入内部函数:
首先根据key
值获取了之前挂载在Vue实例vm
上的所有computed watcher
如果computed watcher
存在,就判断computed watcher
实例属性dirty
,这边我们刚刚在构造函数中看到了dirty
为true
,所以会调用watcher
的实例方法evaluate
。
接下来又判断Dep.target
是否存在,如果存在就调用watcher
的实例方法depend
。
最后返回了watcher
实例属性value
,也就是computed
计算后的结果。
这边有两个方法我们需要关注:watcher.evalute()
和watcher.depend()
。
watcher.evalute()
在computed
的get
过程中,如果computed
依赖已经发生过变化时,就会先调用watcher.evalute
获取最新的值。
evaluate() {
this.value = this.get();
//将数据设置为 干净 状态,表示缓存已更新
this.dirty = false;
}
这里的逻辑很简单,调用实例方法get()
获取最新的值,之后再设置dirty
状态为false
(缓存值)
watcher.depend()
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
//dep.addSub(Dep.target)
}
}
别看这里就三行代码,这里的逻辑可是是整个computed
最难的部分。
- 从后往前遍历实例上的
deps
属性 - 调用各个
deps
中的depend
方法
要坐稳了,开始上天了:
1.this.deps
中收集的是和这个computed
有关联的所有data
数据的Dep
依赖收集器。
2. 调用这些data
的依赖收集器的方法depend
。
3.dep.depend
中会调用当前的Dep.target
上的addDep
。(Dep.target
就是当前正在运行的Watcher
实例,此处为页面的render watcher
)
4. 调用Dep.target.addDep
,将对应的data
的依赖收集器放入自身的deps
属性内,然后再调用dep.addSub(this)
让data
的依赖收集器收集自身。
我的内心:???
好了好了,我来简化一下流程,让这段逻辑能更加容易理解一些:
假设现在页面上有个computed
属性,当页面渲染时:
- 此时的
Dep.target
是页面的render watcher
。 - 调用
computed
属性上的get
方法,由于首次渲染,computed
还没有缓存过值,
所以直接调用watcher.evaluate()
。 watcher.evaluate()
会调用watcher.get()
,在watcher.get()
中会先调用一次pushTarget
改变当前的Dep.target
,此时的Dep.target
是这个computed wathcer
。计算完毕后又会调用popTarget
,归还原先的Dep.target
,此时Dep.target
是原来的render watcher
。- 调用
watcher.depend
。 - 查找
computed watcher
的依赖列表,调用这个computed
属性所依赖的所有data
属性的依赖收集器中的depend
方法,将此时的Dep.target
收集到这些data
属性的依赖收集器内。此时data
的依赖收集器中的依赖一共有两种类型:render watcher
和computed wathcer
- 返回
computed
中计算后的值,渲染值页面。
此时,用户的交互改变了data
属性中的值:
- 触发
data
中的setter
函数 - 通知
data
依赖收集器中的所有依赖于这个data
的依赖进行更改 computed watcher
触发update
更改值render watcher
重新渲染页面上computed
的显示值
总结
虽然页面中需要用到computed
属性。但是!页面并不直接依赖于computed
!页面还是依赖于data
!因为只有data
才会触发computed
的更改,只有data
的更改才会引起页面的重新渲染!computed
只是起到了一个牵线作用,将页面与data
连接了起来!