# Vue 计算属性(computed)源码解析
<div id="app">{{fullName}}</div>
<script>
const vm = new Vue({
el: "#app",
data: {
age: 100,
firstName: "guo",
lastName: "mei",
},
computed: {
// 相当于 Object.defineProperty 中的 getter,不取值不会执行
fullName: {
get() {
console.log("取值");
return this.firstName + this.lastName;
},
set(newValue) {
console.log(newValue);
}
}
}
})
console.log(vm.fullName)
console.log(vm.fullName)
setTimeout(() => {
vm.firstName = "su";
console.log(vm.fullName);
}, 1000);
</script>
computed
1、计算属性默认不会执行,只有获取计算属性的值的时候才会执行,相当于Object.defineProperty
中的getter
方法。
2、计算属性(computed
)会对最终的返回值进行缓存,取值时不是每次取值都重新执行,只有依赖的值产生变化时才会重新执行,获取新的值。
计算属性也可以写成函数,或者写成对象(get、set)
的形式
# 计算属性的初始化
// src/state.js
import Watcher from "./observer/watcher";
export function initState(vm) {
// 状态的初始化
const opts = vm.$options;
if (opts.data) {
initData(vm);
}
if (opts.computed) {
initComputed(vm, opts.computed);
}
}
function initComputed(vm, computed) {
const watchers = (vm._computedWatchers = {});
for (let key in computed) {
// useDef可能是 函数、对象
let useDef = computed[key];
let getter = typeof useDef === "function" ? useDef : useDef.get;
// 将计算属性watcher存起来,以便后面做缓存的时候使用
watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true });
// 将key定义在vm上才能在页面上直接使用 {{fullName}}
defineComputed(vm, key, useDef);
}
}
const shardProperty = {
enumerable: true,
configurable: true,
get: () => {},
set: () => {},
};
function defineComputed(vm, key, useDef) {
if (typeof useDef === "function") {
shardProperty.get = createComputedGetter(key);
} else {
shardProperty.get = createComputedGetter(key);
shardProperty.set = useDef.set;
}
Object.defineProperty(vm, key, shardProperty);
}
cumputed
initState
的时候如果cumputed
属性存在,会调用initComputed(vm, cumputed)
初始化属性,根据属性创建watcher
,将watcher
保存在vm
实例的_computedWatchers
属性上,以便后面做缓存使用。
defineComputed
的作用
1、把cumputed
的属性都挂载到vm
实例上,这样才能在模板上使用,或者通过vm
来调用。
2、拦截计算属性的get
方法,判断是否需要重新取值。
# createComputedGetter
// src/state.js
function createComputedGetter(key) {
return function() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
// 如果其他的属性值改变了,则重新取值(其他值改变后,dirty会变为true)
if (watcher.dirty) {
watcher.evaluate();
}
return watcher.value;
}
};
}
createComputedGetter
createComputedGetter
是主要的判断计算属性是否需要重新取值的方法,当watcher
的dirty
属性为true
,代表计算属性需要重新取值。
# 多个watcher的收集(dep升级)
// src/observer/dep.js
// 最初采用Dep.target来存放watcher,这样只能存一个
Dep.target = null;
// 增加栈结构存放多个watcher
let stack = [];
export function pushTarget(watcher) {
Dep.target = watcher;
stack.push(watcher);
}
export function popTarget() {
stack.pop();
Dep.target = stack[stack.length - 1]; // 取栈中最后一个watcher(上一个Dep.target的watcher)
}
TIP
最初只有渲染watcher
,所以不需要栈结构。当计算属性watcher
引入后,计算属性依赖的值不只要收集计算属性watcher
,还要收集渲染watcher
,以便依赖值更新后通知渲染watcher
更新视图对计算属性重新取值。此时就需要引入栈结构来存放多个watcher
。
# Watcher 类的改造
// src/observer/watcher.js
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options) {
// this.vm = vm;
// this.exprOrFn = exprOrFn;
// this.user = !!options.user;
this.lazy = !!options.lazy; // 是不是计算属性watcher
this.dirty = this.lazy; //计算属性watcher的更新标识(true:需要更新,false:不更新)
// this.cb = cb;
// this.options = options;
// this.id = id++;
// this.deps = [];
// this.depsId = new Set();
// if (typeof exprOrFn == "string") {
// this.getter = function() {
// let path = exprOrFn.split(".");
// let obj = vm;
// for (let i = 0; i < path.length; i++) {
// obj = obj[path[i]];
// }
// return obj;
// };
// } else {
// this.getter = exprOrFn;
// }
// 如果是计算属性watcher,默认不需要取值
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this); // Dep.target = watcher
const value = this.getter.call(this.vm); // render() 方法会去vm上取值 vm._update(vm._render)
popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了
return value;
}
update() {
// 计算属性依赖的值更新了会通知计算属性watcher更新
// 如果是计算属性watcher,就把dirty改为true 代表计算属性需要重新取值
if(this.lazy){
this.dirty = true
}else{
queueWatcher(this);
}
}
// 计算属性重新取值方法
evaluate(){
this.value = this.get()
this.dirty = false
},
// 计算属性watcher存了依赖的属性的dep,当Dep.target上还有值时,需要依赖的值去收集渲染watcher
depend(){
let i = this.deps.length
while(i--){
this.deps[i].depend()
}
},
// run() {
// let newValue = this.get();
// let oldValue = this.value;
// this.value = newValue;
// if (this.user) {
// if (newVal !== oldVal || isObject(newVal)) {
// this.cb.call(this.vm, newVal, oldVal);
// }
// } else {
// this.cb.call(this.vm);
// }
// }
// addDep(dep) {
// let id = dep.id;
// if (!this.depsId.has(id)) {
// this.depsId.add(id);
// this.deps.push(dep);
// dep.addSub(this);
// }
// }
}
export default Watcher;
Watcher 改造
1、对计算属性watcher(options.lazy = true)
增加lazy、dirty
属性,lazy
表示计算属性watcher
,dirty
表示计算属性watcher
是否需要重新取值。计算属性watcher
在初始化时是不需要调用this.get
取值的
2、计算属性依赖的值更新了会通知计算属性watcher
更新(update
方法),此时需要把计算属性watcher
的dirty
设为true
,这样下次计算属性取值时就会更新值。
3、evaluate
:计算属性watcher
更新值的方法
4、depend
:计算属性watcher
存了依赖的属性的dep
,当Dep.target
上还有值时,需要依赖的属性去收集渲染watcher
# 再次修改 createComputedGetter
// src/state.js
function createComputedGetter(key) {
return function() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
// if (watcher.dirty) {
// watcher.evaluate();
// }
// Dep.target存在则说明存在渲染watcher,计算属性依赖的值也需要收集渲染watcher,方便后续值改变了更新视图
if (Dep.target) {
watcher.depend();
}
// return watcher.value;
}
};
}
createComputedGetter
计算属性本身并没有走observe
方法进行观测,所以并没有dep
,并不会去收集渲染watcher
。
计算属性依赖的值比如firstName
,只会收集计算属性fullName
的watcher
,如果firstName
没有取过值(get
时才会做依赖收集),就不会收集渲染watcher
。当firstName
发生变化后,只会通知计算属性watcher
调用update
方法将ditry
属性改为true
,但是没有收集渲染watcher
,不会触发视图更新,计算属性也就不会重新取值。
所以需要让依赖的属性也收集渲染watcher
,依赖的值改变了之后不仅要通知计算属性watcher
将ditry
改为true
,还需要通知渲染watcher
更新视图,对计算属性fullName
重新取值。
在计算属性首次取值时,会执行劫持后的createComputedGetter
包装后的返回函数。对应的watcher
的dirty
属性为true
,调用evaluate
方法,内部会走get
方法。此时依赖值将收集计算属性watcher
,之后调用popTarget
将Dep.target
设为栈中上一个watcher
。此时需要调用watcher.depend
让依赖值收集渲染watcher
。