之前读过的文章
在没有想自己去看源码的时候,在网上看过几篇文章,总结起来,主要说了以下几个方面:
- 计算属性初始化,代码位于
vue\src\core\instance\state.js, 源码如下,在initState时如果有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的实现,代码位于vue\src\core\instance\state.js,源码如下,对cpmputed对象中每个key创建一个Watcher监听
function initComputed(vm: Component, computed: Object) {
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
);
}
}
}
}
Watcher的实现,代码比较多我这边就不贴了,位于vue\src\core\observer\watcher.jsDep的实现,代码位于vue\src\core\observer\dep.js
最后,看完这些的结论是:我只知道这些代码在哪里,有什么用,computed 计算会用到这些代码,那为什么 computed 里面 data 变了会重新计算还是不明白...
开始自己的探索
在 initComputed 的时候,computed 定义的函数通过 getter 参数传给了 Watcher
const getter = typeof userDef === "function" ? userDef : userDef.get;
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
}
在 Watcher 中,getter 被赋值到了实例上
if (typeof expOrFn === "function") {
this.getter = expOrFn;
}
在 Watcher 的原型上,有一个 get 方法,是唯一调用这个 getter 的地方
get () {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
};
但是这个 get 方法我有点看不明白,首先调用了 pushTarget,于是看了 pushTarget 的实现
export function pushTarget(_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target);
Dep.target = _target;
}
Dep.target 又是什么,在这边卡了挺久的,于是我全局搜了下 Dep.target,在 vue\src\core\observer\index.js 搜到了如下代码
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
这个不是 vue 为自己的 data 创建 getter/setter 的代码吗?为什么在 get 中用到了 Dep.target,在回头看 Watcher 的 get,先 pushTarget(this),然后 popTarget(),似乎发现了什么。
在 Watcher.get 的时候(可以认为是第一次 computed 计算),Watcher 告诉执行环境我(this)开始监听了(pushTarget(this)),然后开始调用 getter,也就是我们定义的 computed 方法,在执行过程中,因为 Dep.target 不是 null,所以在 data 属性 get 的时候,就调用了 dep.depend()
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
Dep.target.addDep(this) 也就是 Watcher 里面添加了这个订阅,所以该值变化的时候,这个 Watcher 就可以知道了,data 属性 set 的时候,会调用 dep.notify()
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
改属性的每个订阅者都会执行 update()
具体细节还没细抠,可能有些地方表达的不是很准确...
基于以上的一个实验
在这次探索的过程中,我产生了一个疑问,如果计算属性只有 data get 的时候才会被加入监听,那么如果我的计算属性里有 if else 呢,计算属性中有副作用代码导致 if else 变化呢。
于是,我使用 vue-cli@2.x 生成了一个项目来做测试
首先,验证下上面的思路,我把一个 get 写在了不会执行到的 else 中
<template>
<div>
<div @click="aChange">A Change</div>
<div @click="bChange">B Change</div>
<h1>{{ num }}</h1>
</div>
</template>
<script>
export default {
data () {
return {
a: 1,
b: 2
}
},
computed: {
num () {
const a = this.a
console.log('change')
if (a) {
return a
} else {
return this.b
}
}
},
methods: {
aChange () {
this.a += 1
},
bChange () {
this.b += 1
}
}
}
</script>
最后的结果是,点 A Change,num 递增,点 B Change,没有任何反应,甚至连 log 都没打出来
于是我把 num 函数改成
const a = this.a;
const b = this.b;
console.log("change");
if (a) {
return a;
} else {
return b;
}
log 就有打出来了,但是结果是一样的,但是如果遇到了 if (a + window.xx),之类的副作用代码,就会产生问题了,所以建议计算属性的代码尽量纯,如果是纯函数的话,把 get 写在判断中还可以减少计算次数。
有观察到 vue 中 Watcher 的代码与之前版本相比有变化,原来代码只有一个 this.dep, 现在有出现了 this.newDeps,于是我又将代码改成了
export default {
data() {
return {
a: 1,
b: 2,
c: 3
};
},
computed: {
num() {
const a = this.a;
console.log("change");
if (a) {
return this.b;
} else {
return this.c;
}
}
},
methods: {
aChange() {
this.a = !this.a;
},
bChange() {
this.b += 1;
},
cChange() {
this.c += 1;
}
}
};
最后的现象就是 A Change 像一个开关,点击了 b c 的监听状态就会切换,不会同时监听 b c。