主题
计算属性computed
计算属性的使用方式
- 函数型
js
computed: {
newName() {
return this.name + '123'
}
}- 对象型
js
computed: {
newName: {
// get必须有
get() {
return this.name + 1231
},
// set可以没有
set(val) {
// 设置逻辑
}
}
}initComputed
初始化的时候, 会将computed遍历一个个取出来, 先看一下当前计算属性是否和vm身上的属性重名, 若是未重名则取到内部的getter属性, 若是函数则这个函数就是getter, 并且依次创建计算属性watcher, 并将创建的watcher存起来
js
function initComputed(vm) {
const computed = vm.$options.computed;
// 用于存储计算属性watcher, 建立key与watcher的映射关系, watcher为vm._computedWatchersd的把别名
// 将其挂到vm身上的原因是方便后续取用, 因为在vm[key]触发getter, get函数中的this就是vm
const watchers = (vm._computedWatchers = Object.create(null));
for (const key in computed) {
if (!(key in vm)) {
const userConf = computed[key];
// 取到get函数
const getter =
typeof userConf === "function" ? userConf : userConf.get || noop;
// 生成watcher,并存到watcher内, 建立与key的关系
watchers[key] = new Watcher(vm, getter, noop, { lazy: true });
// 将key写到vm身上(这就是computed的本质)
defineComputed(vm, key, userConf);
}
}
}计算属性 Watcher
提示
注意: computed不同与watch, 计算属性只有在调用这个计算属性的时候才会调用计算属性内的get函数, 因此在实例化 watcher 的时候不需要执行依次get函数进行依赖收集, 依赖收集是在主动调用get的时候才会进行,因此实例化时候需要给 options 添加一个lazy:true属性告诉 watcher 不用立即执行get
js
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
let uid = 0;
class Watcher {
constructor(vm, exprFn, cb, options) {
this.vm = vm;
this.cb = cb; // 回调函数
this.uid = uid++; // 每一个组件都有一个渲染watcher, 这是唯一标识
this.depsIds = new Set(); // 一个watcher上面有多个dep, 且不重复
this.deps = [];
this.options = !!options; // 是否为渲染watcher
this.user = !!options.user;
this.lazy = !!options.lazy;
this.dirty = !!options.lazy;
if (this.user) {
this.getter = function () {
let obj = vm;
const path = exprFn.split(".");
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]];
}
return obj;
};
} else {
this.getter = exprFn;
}
// 若不是计算属性,则首先就让组件渲染函数执行
this.value = this.get();
this.value = this.lazy ? undefined : this.get();
}
run() {
const newVal = this.get();
if (newVal !== this.value) {
this.value = newVal;
this.cb.call(this.vm, newVal, this.value);
}
}
// 添加dep
addDep(dep) {
const depId = dep.uid;
if (!this.depsIds.has(depId)) {
this.depsIds.add(depId);
this.deps.push(dep); // 将当前watcher与dep建立1对多的关系
dep.subs.push(this); // dep与watcher也建立1对多的关系
}
}
// 更新页面
update() {
// lazy=true的watcher为计算属性watcher, 计算属性默认没有渲染页面的能力
// 因此在依赖跟新的时候仅将标志变成true, 等待下一次取值的时候重新计算(执行计算属性的get函数)
if (this.lazy) {
this.dirty = true;
} else {
queueWatcher(this);
}
}
get() {
const vm = this.vm;
pushTarget(this);
// 这里需要特别注意,需要更改get函数的this指向
// let val = this.getter();
let val = this.getter.call(vm, vm);
popTarget();
return val;
}
evaluate() {
this.value = this.get(); //[!code ++] //执行计算属性取值操作, 取值时会收集计算属性依赖
this.dirty = false; //[!code ++] // 随后将标志符设为false, 避免再次取值时候重新计算
}
}
export default Watcher;defineComputed 定义计算属性
通过Object.defineProperty在vm上定义计算属性, 并且重写get方法,便于后面进行依赖收集
js
function defineComputed(vm, key, userConf) {
const defaultConf = {};
if (typeof userConf === "function") {
defaultConf.get = createComputedGetter(key);
} else {
defaultConf.get = createComputedGetter(key);
defaultConf.set = userConf.set || noop;
}
Object.defineProperty(vm, key, defaultConf);
}createComputedGetter 重写计算属性 get 函数
js
function createComputedGetter(key) {
return function getter() {
// 这里的this为vm
const watcher = this._computedWatchers[key];
if (watcher.dirty) {
// 缓存的开关, 默认是true
watcher.evaluate(); // 等价于watcher.get, 并且会将watcher.get()的值挂到watcher.value上, 且将dirty设置false
}
// 计算属性get函数返回值(缓存)
return watcher.value;
};
}计算属性依赖收集 🤣🤣🤣
到这里, 首次渲染的时候, 能将计算属性渲染到页面上了, 但是存在一个问题, 就是再次更新计算属性依赖的值时候, 计算属性也会随之更新, 但是页面不会重新渲染, 为何会这样? 是因为计算属性在进行依赖收集的时候, 仅收集了计算属性的watcher, 并没有收集渲染watcher, 因此在计算属性值更新之后没有主动渲染页面
假设现在有一个计算属性
js
<div>{{fullName}}</div>
computed: {
fullName() {
return this.firstname + this.lastname
}
}页面首次渲染时候,会首先收集到渲染watcher, 渲染过程中会进行取值操作, 即this.fillName, 首次取值会触发计算属性getter, 由于是首次,watcher.dirty=true, 因此会执行 watcher.evaluate();触发计算属性watcher进行依赖收集, 此时Dep.target为计算属性watcher, 在执行计算属getter时, 还会触发firstname,lastname进行取值, 但是此时的Dep.target依然是计算属性watcher, 因此, firstname和lastname的dep里面都会记录计算属性watcher, 计算属性watcher同时记录了flrstname,lastname的dep
当 flrstname或 lastname 值变化后, 会通过dep来执行watcher.update(), 由于dep上firstname和lastname的dep里面都会记录计算属性watcher还有渲染watcher,且在stack中栈顶肯定为计算属性watcher, 栈底为渲染watcher, 所以会先执行计算属性watcher.update()更改this.dirty = true; 随后执行渲染watcher.update()渲染`页面的时候,又会进行计算属性取值操作以及页面渲染依赖收集
因此在计算属性的getter中, 还需要对计算属性依赖的firstname与lastname进行依赖收集, 以便后面他们值更改后进行页面渲染
js
// state.js
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers[key];
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
};
}js
// watcher.js
// 计算属性依赖收集 Watcher实例方法
depend() {
let len = this.deps.length
while(len--) {
this.deps[len].depend();
}
}