主题
计算属性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();
}
}