简介
大家好,我是六六。在我们开发当中,当然少不了使用computed计算属性,都知道结果会被缓存起来,那是怎么做到的呢。当然了,在面试中也经常会问到与watch到底有什么不同,那儿接下来,我们将走进computed的内部,详细的去了解一下。
目录
- 你知道的computed特性
- computed原理是什么
- computed原理具体实现
- 详说整个computed过程
- 面试题与扩展
- 总结
1.你知道的computed特性:
- 计算属性在使用的时候,要当做普通属性使用就好,不需要加()
- 只要计算属性这个function内部所用到的data中的数据发生了变化,就会立即重新计算这个计算属性的值
- 计算属性的求值结果,会被缓存起来,方便下次继续使用;如果计算属性方法中,所依赖的任何数据,都没有发生过变化,则不会重新对计算属性求值
- 可以为函数或者对象
2.computed原理是什么:
学习中最常见听到的一句话就是,computed就是一个特殊的getter方法。在代理函数可以结合watcher实现缓存与收集依赖。
计算属性具有缓存性,如何知道计算属性的返回值发生变化呢?
这其实就是结合了watcher的dirty属性来分辨的,当dirty为true时,说明需要重新计算,当为false时,计算属性没有改变,不需要重新计算,直接读取缓存值就好。
模拟一下计算属性内容发生改变后:
- 计算属性的watcher和组件内的watcher都会得到通知
- 计算属性的watcher将自己的属性dirty设置为true
- 下次读取计算属性时,因为dirty为true重新计算一次值
- 组件watcher得到通知,从而执行render函数进行重新渲染
3.computed原理具体实现:
3.1 initComputed初始化
const computedWatcherOptions={lazy:true}
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
// userDef我们定义的计算属性
const userDef = computed[key]
// 获取getter
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 非SSR环境
if (!isSSR) {
// 创建watcher实例
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
具体讲解:
- initComputed函数接受两个参数:vm实例和computed对象
- 声明了一个变量watchers,并保存在vm._computedWatchers
- 声明变量isSSR用于判断是否在SSR环境
- 使用for…in循环computed对象,以此初始化每个计算属性
- 用getter保存计算结果的值
- 创建watcher实例
- 判断计算属性的值是否已经在vm实例上定义了,如果没有,执行defineComputed函数
到这里,我们就明白了,最后我们会执行defineComputed函数,对每个计算属性进行定义和初始化。下面我们来看看这个函数。
3.2 defineComputed定义计算属性
const sharePropertyDefinition={
enumerable:true,
configurable:true,
get:noop,
set:noop
}
function defineComputed(target,key,userDef){
// 判断环境
const shouldCache=!isServerRendering()
// 设置的为函数
if(typeof userDef==='function'){
sharePropertyDefinition.get=shouldCache?createComputedGetter(key):userDef
sharePropertyDefinition.set=noop
}
// 设置的为对象
else{
sharePropertyDefinition.get=userDef.get?shouldCache&&userDef.cache!=false?createComputedGetter(key):userDef.get:noop
sharePropertyDefinition.set=userDef.set?userDef.set:noop
}
Object.defineProperty(target,key,sharePropertyDefinition)
}
具体讲解:
- 首先定义sharePropertyDefinition变量,用于配合
Object.defineProperty
使用。 - 函数defineComputed接受target,key,userDef三个参数
- 使用shouldCache变量确认是否在SSR环境
- 判断userDef是否为函数,因为我们知道我们传入的computed支持函数或者对象
- 如果是函数,判断shouldCache,为true时执行createComputedGetter函数,并赋值给 sharePropertyDefinition.get
- 如果不是函数,是否写了get函数,判断shouldCache,为true时执行createComputedGetter函数,并赋值给 sharePropertyDefinition.get
- 在判断用户是否写了set函数,写了就赋值给sharePropertyDefinition,noop为空函数。
- 在最后,执行Object.defineProperty向实例挂载属性
所以,计算属性的缓存和响应式主要在于是否将getter方法设置为createComputedGetter。因为最终挂载到get方法的就是createComputedGetter函数。
3.3createComputedGetter缓存与响应式的关键
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
具体讲解:
- 在initComputed函数中我们定义了_computedWatchers属性,通过_computedWatchers[key]拿到我们定义的watcher,所以变量watcher就是每个watcher的实例
- 判断watcher.dirty值是否为真,为真就重新计算一下,也就是执行watcher.evaluate()
- 判断Dep.target的值,如果有,就执行 watcher.depend(),最后返回watcher的value值。
代码是非常简单的,但是理解起来是需要配合watcher和dep的,如果不了解可以参考我这篇文章: 深入浅出Vue变化侦测
3.4 与watcher密不可分
class Watcher{
constructor(vm,expOrFn,cb,options){
// 无关代码
if(options){
this.lazy=!!options.lazy
}
this.dirty = this.lazy
}
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
evaluate方法:就是从watcher重新获取一次表达式的值
depend方法:
- this.deps[i]就是计算属性所依赖的状态
- 调用depend方法可以将组件的watcher实例添加到dep实例中,意思就是计算属性所依赖的状态改变也会通知组件,更具体说,就是组件watcher也会观察计算属性所依赖的状态。(组件watcher是如何拿到的呢)
4.详说整个computed过程:
- 使用watcher读取计算属性
- 读取计算属性函数中的数据,定义响应式时,get读取的就是watcher.value
- 计算属性和组件watcher同时观察数据的变化
- 当数据改变后,计算属性和组件watcher都会收到通知
- 组件watcher会重新渲染组件
- 计算属性watcher因为数据改变,dirty属性为true,将重新计算
- 计算属性计算的结果用于本次渲染,并缓存起来
5.面试题:watch和computed的区别是什么?
其实我觉得这两个作用是完全不一样,不知道为什么总拿来比较。
- watch是一种行为,在状态改变之后需要做什么。
- computed就是一种状态,也可以说多种状态初始化后的结果。
我认为把,computed与filter作为比较不是更好一些吗?都是用来初始化状态用的。
- computed更适用于大量数据计算的结果,并且反复使用,而且不常更新。因为有缓存,大大提升性能。
- filter适用于少量数据进行初始化处理,计算量不能太大,因为每次渲染都会计算,并且可以频繁更新。
6.总结
一开始学这个知识点确实很懵,一个知识点看了很多遍,到最后才领悟过来,不过也有很多一知半解的地方,所以每天还是要继续努力。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: liu6 原文链接:https://juejin.im/post/6844904142138245128