vue深入浅出-computed的缓存和响应式原理
灵魂三问
带着问题看源码:
computed是如何实现缓存的?computed是如何收集依赖数据的?computed依赖数据更新之后是如何更新视图的?
简介
相信computed都用过.那么,知其然知其所以然?
定义
computed 是 vue 中的计算属性, 根据依赖关系进行计算并缓存, 只有当依赖被改变的时候才会更新
computed 一般用于一些复杂的场景, 如受多个数据共同影响的场景
用法
computed 有两种用法
一种是常规的函数写法, 默认使用 getter
computed: {
getName() {
return `${this.firstName}-${this.lastName}`
}
}
其实还可以使用对象的写法, 设置 computed 的 getter, 当值被修改的时候同时修改依赖的属性
computed: {
getName: {
get() {
return `${this.firstName}-${this.lastName}`;
},
set(val) {
const [first, last] = val.split("-");
this.firstName = first;
this.lastName = last;
},
},
},
下面跟着源码直接进入正题
初始化过程
new Vue(computed) => initState => initComputed => defineComputed[key] => createComputedGetter =>mountComponent
大致介绍一下整个流程:
- 首先在
initState中对传入的computed进行初始化 - 初始化的过程中, 为每一个声明的
computed创建Watcher, 将声明时传入的函数(或者对象声明的get) 传递给创建的Watcher用于被访问时执行,利用defineProperty将声明的computed代理到vm实例上, 从而跟data一样可以通过this来访问, 同时用一个函数包装computed的getter(实现缓存的关键) , 当computed被访问时将执行该函数, 判断是否使用缓存值 - 初始化结束之后会执行
vm.$mount, 对视图进行渲染, 渲染过程中会执行vm._render生成vnode由于解析到{{computed}}会触发之前劫持的getter, 从而执行声明computed时的函数 - 执行声明时传入的函数时, 由于初始化
dirty=true, 因此会去获取最新值, 此时会触发其所引用的data中数据的getter, 从而触发响应式系统的依赖收集.由于此时的Dep.target为该computerWatcher, 因此会收集该computerWatcher为依赖项 - 当
computed依赖的数据被更新时, 会进行消息分发,执行watcher.update(), 若watcher为computedWatcher则将dirty标记为true, 当前订阅的computed被访问时, 触发之前被函数包装的getter, 函数内部识别到dirty===true则获取最新值, 获取完之后接着将dirty置位false. 由于被依赖的数据订阅者中还有用于视图更新的renderWatcher, 因此会接着对视图更新从而渲染最新数据, 这也说明computedWatcher要在renderWatcher之前去更新
主要代码如下:
initComputed => 创建 computedWatcher
- 在
Vue实例上挂载_computedWatchers属性用来存放所有computedWatcher - 为每一个计算属性创建
computedWatcher - 使用
defineComputed处理定义的每个computed
// src/core/instance/state.js
function initComputed(vm: Component, computed: Object) {
/** 在vm实例上挂载 _computedWatchers 属性存放 computerwatcher */
const watchers = vm._computedWatchers = Object.create(null)
/** 判断是否服务端 */
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
/**
* 1. 判断 computed 属于默认函数写法,还是对象写法
* 2. 如果是对象写法则将定义的 get 赋值给 getter
*/
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
/** 可以看出本质上 computed 就是一个 watchers 数组, 每一个定义的 computed 都是一个 watcher(computedWatcher) */
watchers[key] = new Watcher(
vm,
/** 将之前申明的 getter 传入 watcher 的 expOrFn, 当 dep.notify 的时候将会执行 */
getter || noop,
noop,
/** computed 实现缓存关键, 值为上面定义的 { lazy: true } */
computedWatcherOptions
)
}
/** 判断是否有重复的申明 */
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// ...
}
}
}
defineComputed => 劫持 getter, 实现缓存
- 使用
Object.defineProperty将计算属性挂载到vue实例上, 使其可以通过this访问 - 使用
createComputedGetter包装计算属性的getter函数, 当计算属性被访问的时候执行.通过dirty变量标记是否去获取最新数据
// src/core/instance/state.js
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
/** 判断 computed 属于函数式写法还是对象写法, 目的是拿到其执行函数 */
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
// ...
}
/** 挂载到 vue 实例, 通过 this 可以访问 */
Object.defineProperty(target, key, sharedPropertyDefinition);
}
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
/** 如果"脏了", 表示依赖数据被更新, 则需要获取最新数据 */
if (watcher.dirty) {
/**
* 1. 本质是调用创建 computedWatcher 时, 传入的方法即定义 computed 时写的方法, 从而更新 Watcher 的 value 为最新值
* 2. 获取数据之后, 同时将 dirty 置位 false, 进行缓存
* */
watcher.evaluate();
}
/** 如果有依赖正在收集, 则将该 watcher 下所有发布者添加到正在收集依赖的 watcer 发布者列表里 */
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
这里注意一下有这样一步watcher.depend(), 目的是将该 computedWatcher的发布者添加到当前正在收集依赖的Watcher.
首先初次渲染页面时, 由renderWatcher进行依赖收集, 当解析模板发现{{computed}}时, 触发计算属性的 getter, 执行 watcher.get,此时会将当前watcher压入targetStack依赖收集栈, 同时执行Dep.target = target.即将此时进行依赖收集的renderWatcher修改为当前computedWathcer. 执行计算属性定义函数时,访问到依赖数据,触发响应式系统将Dep.target加入订阅者subs列表中.依赖收集完毕,执行popTarget()弹出收集栈,此时Dep.target修改为之前的renderWatcher
因此watcher.depend()的最终目的就是将computedWatcher的发布者添加到renderWatcher的发布者列表中, 如果不执行这一步, 计算属性所依赖的属性修改之后,不会触发视图更新, 因为有可能template中只引用了计算属性而没有引用计算属性内部依赖的数据, renderWatcher并没有对依赖数据进行订阅.
数据更新
当计算属性依赖数据被更新时, 会触发响应式数据的setter, 执行dep.notify对所有订阅者进行订阅发布
当订阅者为computedWatcher时, 将内部的dirty置为true
当订阅者为renderWatcher时, 执行vm._update(vm._render)更新视图.扫描数据的同时, 访问到计算属性, 则会执行之前createComputedGetter包装的getter函数, 由于当前computedWatcher内部的dirty已经在上一步被标记为true, 因此会刷新watcher.value, 刷新之后将dirty置为false. 若dirty为false, 则直接获取watcher.value
// src/core/observer/watcher.js
update() {
/* 如果this.lazy为true, 说明是 computedWatcher, 通过dirty标记为有更新, 当下一次 computed 被访问的时候, 识别到该字段则会进行数据更新 */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
总结
回顾一下之前的问题:
computed是如何实现缓存的?
答: 通过createComputedGetter包装计算属性的getter,使用dirty标记所依赖的数据有没有更新, 若更新则刷新数据,否则直接返回watcher.value
computed是如何收集依赖数据的?
答: computed本质上就是一个watcher, 在执行watcher.get时会访问到计算属性所依赖的数据,触发依赖收集系统. 此时的订阅者Dep.target为该coomputedWatcher, 订阅方为所有依赖数据.
computed依赖数据更新之后是如何更新视图的?
答: 当computed内部依赖数据进行依赖收集的之后, 会将当前renderWatcher也加入到订阅队列中, 即依赖数据更新后先触发computedWatcher.update的, 然后触发renderWatcher.update更新视图
转载自:https://juejin.cn/post/7125610199666130974