本篇会分析Vue2 中 computed 的创建过程
先看我们的demo:
<template>
<section>
<div>param1: {{ param1 }}</div>
<div>param2: {{ param2 }}</div>
<div>sum: {{ sum }}</div>
<div>操作:
<button @click="handlehangeRawBtn(1)" name="button">make param1 ++</button>
<button @click="handlehangeRawBtn(2)" name="button">make param2 ++</button>
<button @click="handleConcleComputedData" name="button">将param1 的值复制成打印模板中未用到的computed minus 的值</button>
</div>
<section>
</section>
</section>
</template>
<script>
import child1 from './components/child1.vue'
export default {
name: 'demo',
components: { child1 },
data() {
return {
param1: 1,
param2: 2,
param3: 3,
}
},
computed: {
sum({ param1, param2 }) {
return param1 + param2
},
minus({ param3, param2 }) {
return param3 - param2
}
},
watch: {
param3(val) {
this.param1 = val
}
},
mounted() {
console.log('App mounted this', this)
},
methods: {
handlehangeRawBtn(type) {
this[`param${type}`]++
},
handleConcleComputedData() {
this.param1 = this.minus
},
}
}
</script>
initComputed 源码:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
function initComputed (vm: Component, computed: Object) {
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.
// 为computed 的没个key去创建watcher 实例,watcher的第二个参数就是computed 定义的函数/get函数
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.
/*
根据上篇文章,组件在继承的时候(VueComponent的调用)就建立了props 和 computed 数据的代理,
定义了对应的 getter, setter 函数, 在实例化的这里就是真正去创建了computed watcher
*/
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)
}
}
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.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)
}
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
}
}
}
其实看源码也能知道computed 并不是所有都会走缓存。
下图就是组件继承时(function VueComponent 的调用),对props 和 computed 属性建立数据代理的过程
在实例化的时候,就如代码所示,去创建真正的computed watcher,
我们接下来以 sumComputedWatcher 代表为 sum 创建的watcher 实例,
render函数的调用,用到了vm.sum, 首先会触发了绑定在原型上的vm.sum.getter的劫持代理,
走到了对应computedGetter 的调用
( 如果你没理解上篇中,为什么要在VueComponent 调用 initComputed 和 initProps,
以及为什么要建立Sub Vue 来继承Vue, 这里其实就能慢慢明白。
越看越觉得尤大yyds
)
不要把原型上的computed getter, sumComputedWatcher.get() 以及 sumComputedWatcher.getter()搞混淆。
1| sumComputedWatcher.evaluate()
/* class Watcher {
...,
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;
},
evaluate() {
this.value = this.get();
/* 调用 sumComputedWatcher 的get函数,将Dep.target推入targetStack,
接着调用sumComputedWatcher 的getter 函数,
这里不要把watcher类 的get 函数和watcher的劫持 getter函数搞混淆
*/
this.dirty = false; // dirty属性变为false
}
...,
}
*/
2| sumComputedWatcher.get()
3| pushTarget()
4| sumComputedWatcher.getter.call(vm, vm)//等同于 sum(vm),这里也就能看出来为什么computed函数内直接能拿到vm上的变量
/*
sum({ param1, param2 }) {
return param1 + param2
}
其实看到computed函数就能明白,sum的调用,依赖了param1 和 param2,
要做加法运算,首先得去 vm上 拿到这两个变量,那就势必会触发 param1 和param2 的数据劫持.
(param1 和 param2 早就在initData时创建了自己的Observer 实例, 从调用栈也能明显的看出来这个关系)
*/
看下图,sum 的调用分别触发了param1Dep 和 param2Dep的get。
接着就开始了收集依赖的一系列过程
这个过程完成了sumComputedWatcher 和 param1Dep, param2Dep 之间发布订阅关系的建设,接着函数的调用
调用完watcher.evaluate();后接着又调用了watcher.depend(),经过前面的章节,以我们的例子来说 watcher.depend()无非就是又去收集render watcher 和param1,param2 的关系,明明在render 函数调用时收集过了,为什么还要去收集?这里其实也很好理解,模板中用到的变量并不一定是vm初始化时定义的所有变量,就拿computed 来说,我可以定义供method调用。对于method用computed 的变量,就和render 函数调用时一个道理,在首次调用时收集依赖。
这里其实可以理解为computed watcher 是在调用时完成依赖收集的过程的。
接着我们看下watch 的初始化
function initWatch(vm, watch) {
for (const key in watch) {
const handler = watch[key];
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
function createWatcher(vm, keyOrFn, handler, options) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(keyOrFn, handler, options); // 调用$watch 函数
}
Vue.prototype.$watch = function (expOrFn, cb, options) {
const vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true;
const watcher = new Watcher(vm, expOrFn, cb, options); // 创建watcher 实例
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn() {
watcher.teardown();
};
};
}
在组件初始化的时候就已经完成了param3Wacter的依赖收集,从watch 的key中分析到自己的依赖。 对比computed 来说,computed watcher 在大多数情况下会有缓存,只有真正依赖的数据变化时,才会调用更新,但watch 更多的思想是观察自己的数据去做一些操作。
写到这里已经不想写了....