fe_interview
fe_interview copied to clipboard
你知道Vue中的computed是怎么实现的吗?
这里先给一个结论:计算属性computed
的本质是 computed Watcher
,其具有缓存。
一张图了解下computed
的实现:
- 首先是在组件实例化时会执行
initComputed
方法。对应源码src/core/instance/state.js
的 169 行。
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
initComputed
函数拿到 computed
对象然后遍历每一个计算属性。判断如果不是服务端渲染就会给计算属性创建一个 computed Watcher
实例赋值给watchers[key]
(对应就是vm._computedWatchers[key]
)。然后遍历每一个计算属性调用 defineComputed
方法,将组件原型,计算属性和对应的值传入。
-
defineComputed
定义在源码src/core/instance/state.js
210 行。
// src/core/instance/state.js
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering();
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (
process.env.NODE_ENV !== "production" &&
sharedPropertyDefinition.set === noop
) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
首先定义了 shouldCache
表示是否需要缓存值。接着对 userDef
是函数或者对象分别处理。这里有一个 sharedPropertyDefinition
,我们来看它的定义:
// src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
sharedPropertyDefinition
其实就是一个属性描述符。
回到 defineComputed
函数。如果 userDef
是函数的话,就会定义 getter
为调用 createComputedGetter(key)
的返回值。
因为
shouldCache
是true
而 userDef
是对象的话,非服务端渲染并且没有指定 cache
为 false
的话,getter
也是调用 createComputedGetter(key)
的返回值,setter
则为 userDef.set
或者为空。
所以 defineComputed
函数的作用就是定义 getter
和 setter
,并且在最后调用 Object.defineProperty
给计算属性添加 getter/setter
,当我们访问计算属性时就会触发这个 getter
。
对于计算属性的
setter
来说,实际上是很少用到的,除非我们在使用computed
的时候指定了set
函数。
- 无论是
userDef
是函数还是对象,最终都会调用createComputedGetter
函数,我们来看createComputedGetter
的定义:
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;
}
};
}
我们知道访问计算属性时才会触发这个
getter
,对应就是computedGetter
函数被执行。
computedGetter
函数首先通过 this._computedWatchers[key]
拿到前面实例化组件时创建的 computed Watcher
并赋值给 watcher
。
在
new Watcher
时传入的第四个参数computedWatcherOptions
的lazy
为true
,对应就是watcher
的构造函数中的dirty
为true
。在computedGetter
中,如果dirty
为false
(即依赖的值没有发生变化),就不会重新求值。相当于computed
被缓存了。
接着有两个 if
判断,首先调用 evaluate
函数:
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
首先调用 this.get()
将它的返回值赋值给 this.value
,来看 get
函数:
// src/core/observer/watcher.js
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
get
函数第一步是调用 pushTarget
将 computed Watcher
传入:
// src/core/observer/dep.js
export function pushTarget(target: ?Watcher) {
targetStack.push(target);
Dep.target = target;
}
可以看到 computed Watcher
被 push 到 targetStack
同时将 Dep.target
置为 computed Watcher
。而 Dep.target
原来的值是渲染 Watcher
,因为正处于渲染阶段。回到 get
函数,接着就调用了 this.getter
。
回到 evaluate
函数:
evaluate () {
this.value = this.get()
this.dirty = false
}
执行完get
函数,将dirty
置为false
。
回到computedGetter
函数,接着往下进入另一个if
判断,执行了depend
函数:
// src/core/observer/watcher.js
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
这里的逻辑就是让 Dep.target
也就是渲染 Watcher
订阅了 this.dep
也就是前面实例化 computed Watcher
时候创建的 dep
实例,渲染 Watcher
就被保存到 this.dep
的 subs
中。
在执行完 evaluate
和 depend
函数后,computedGetter
函数最后将 evaluate
的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。
大佬,computed的缓存特征以及惰性特征没有讲哦 查找 https://www.yuque.com/lihaohao-f0jle/apohhm/dwkp57