这一段时间,有好几个同学找我讨论computed的问题,普遍觉得computed在vue中的这块逻辑有点绕,但从网上并没有找到讲的非常详细,或者说没有找到正好自己不懂的那个点的详细讲解。讲完几遍后,我觉得是可以梳理成文,大家可作为参考的一部分,希望能对您的解惑有些帮助。如果对您有所帮助,烦劳您给点个赞哈~ 毕竟纯手打不易,哈哈。
开始正文。
在这篇里,我就不讲我们在data中定义的数据是如何在底层处理的了,大家如果有困惑可以查查别的文章。直接从computed开讲(这个系列将的还是vue2的内容哈)。
要理解computed底层,那就要从computed最开始的地方下手:initState(vm)
,从而找到 initComputed
方法定义。
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
// 这里的getter就是用户自定义计算函数
const getter = typeof userDef === 'function' ? userDef : userDef.get
// ....
// 把每个computed属性都初始化一个watcher,并且传入{lazy: true}, 比如; vm._computedWatchers['someComputed'] = new Watcher({...}); lazy的意思是不需要马上执行。
// 并且把自定义计算函数传入
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
if (!(key in vm)) {
// 初始化时,computed中的key是还没有挂载到vm下的,所以会执行下面逻辑,
// 走到这里,是把真正用户写的计算函数进行挂载
// userDef -> 计算函数
defineComputed(vm, key, userDef)
}
}
}
ok, 这一段的主思路是:
- 遍历我们开发者定义的computed属性, 把每个属性的函数传到Wacther中并实例化,同时,把这些watcher都挂载的 vm._computedWatchers上, 这些watcher在后面会频繁使用,是载体。
- 执行defineComputed(),目的是把计算函数进行处理后,通过Object.defineProperty挂载到vm上。
好了,详细看一下 defineComputed()
我会把代码进行简化,只留着主流程,各种边界条件等这里不考虑。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
// ...
export function defineComputed() {
// ...
if (typeof userDef === 'function') {
// 会进入这里,因为userDef就是开发者写的computed函数
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
// 告知开发者computed没有setter,也就是说计算属性无法进行setter操作
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 把computed的key挂载到vm上,以后就可以直接在代码中使用 this.xxx来访问了
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter里面让我们猜测的话,肯定就是与执行相关的了,比如当在<template>
标签中调用计算属性的话,就会首先执行开发者写的计算函数了吧?
当然一般也不会只是这么简单。为什么这么说呢?我先通过一个例子来说明:
new Vue({
...
computed: {
// 一般不会写的情况
initComputed() {
return '这是初始计算'
},
// 正常情况
normalComputed() {
return this.aa + this.bb + '这才是正常情况'
}
}
})
从上面这段伪代码来看,initComputed
函数返回的就是一个静态字符串,是比较简单,但是如果是这样,我们大可不必使用计算属性来处理。
所以还得看正常的情况,因为我们的函数中还引用了 this.aa
和this.bb
两个变量,当这两个变量任何一个有变化的时候,我们还得更新计算函数,然后再更新页面视图。这没错吧?
所以看懂上面这个小插曲,那如果让我们来看这个createComputedGetter
函数对功能的实现承载了些什么。
好,继续看这个createComputedGetter
函数
function createComputedGetter (key) {
// 当使用计算属性的值的时候,才会执行。
return function computedGetter () {
// 还记得前面我们把computed属性都绑定到this._computedWatchers上了么??
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果我们的computed函数中有引用别的变量,那么初始化时dirty一定是true,当引用的变量变了以后,也会最终把dirty置为true。
// dirty为true,就说明原来数据脏了,该进行更新了
if (watcher.dirty) {
// 所以会执行,继而执行开发者定义的计算函数。
watcher.evaluate()
}
// 那么这时Dep.target实际上就是栈顶的另外一个watcher,是谁不一定,需要看业务代码的执行,有可能是render函数
if (Dep.target) {
// 从这句话来看,这段createComputedGetter的整体逻辑是:要执行比如某个计算函数时,
// 1.先把当前这个对应的watcher推入一个Dep维护的栈,然后Dep.target更新为当前watcher,
// 2. 执行watcher中个getter函数,也就是计算函数,
// 3. 执行完成后,当前的watcher出栈,并且Dep.target重新指向栈顶的另外一个watcher
// 4. 把计算函数的结果返回
// 所以这时,重新进行watcher的depend操作,作用是什么呢? 那么来详细的看一看
watcher.depend()
}
// 把value返回
return watcher.value
}
}
}
先看到 watcher.evaluate()
, 可以先不向下看了。需要重点关注这个函数。 evaluate
可以翻译为评价,也就是要评价这个watcher要不要进行更新了。
evaluate () {
this.value = this.get()
this.dirty = false
}
// 要看get函数做了什么。
get () {
/**
* export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
*/
// 看起来貌似没有用的pushTarget 和popTarget,其实有大用处,后面再细说
pushTarget(this)
let value
const vm = this.vm
try {
// 执行开发者定义的computed函数
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
}
抛开这对 pushTarget 和popTarget
, 貌似就让开发者写的函数执行了,你可能感觉也没有什么特别的嘛。但是我相信满满的疑问就上来了,那computed的强大功能是怎么实现的呢?
我可以很负责的告诉你,这一对pushTarget 和popTarget
太重要啦~~ 太重要
啦~~
现在带着疑问,我们先执行一下开发者写的计算函数:
normalComputed() {
return this.aa + '这才是正常情况'
}
这时,一定会执行this.aa, 如果没有学习过 initData
的知识,建议你深入了解一下data函数的实现(默认大家写的都是函数形式)。
好,进入this.aa逻辑吧~~
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 先搞明白当前的Dep.target是谁,总体来说,哪个watcher调用的data中的值,那么这个
// Dep.target就是哪个watcher。还记得我上面说的那一堆很很很重要的pushTarget函数么
// 那么当这个data中的值发生变化的时候,是又怎么通知到computed-watcher进行更新的呢?
// 原因就在于在执行dep.depend时,最终调用了watcher的addDep方法,这个方法除了把Dep对象赋值到watcher的newDep[]中, 还做了一个很骚的操作,把这个watcher实例又反向赋值给了dep实例的subs[]中
// 证据在这里:
/*
addDep (dep: Dep) {
//....
if (!this.depIds.has(id)) {
// 这里!!!
dep.addSub(this)
}
}
*/
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 意义是:如果新更改的数据变成了非基本类型,那么就需要重新进行observe,否则无法做到响应式
childOb = !shallow && observe(newVal)
// 通知更新
dep.notify()
}
})
通过我这段代码中的很细致的解释,大家是不是有些明白了?我讲的再明白一点:
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
pushTarget会把Dep.target设置为当前的watcher,目前就是我们现在这个normalComputed
watcher。所以,在上面的getter执行时,会把当前的Dep实例赋给watcher.newDeps数组,而且,还会把当前这个watcher反向赋值给这个Dep实例。 而这个Dep实例是绑定在例子中的this.aa
中的。
正如代码中的注释说的一样,我们是有足够证据的:
// dep.depend(),指向 depend函数
// dep.js中
depend () {
if (Dep.target) {
// Dep.target是个watcher,这是毋容置疑的
Dep.target.addDep(this)
}
}
// 然后又走到watcher中
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 把dep push到watcher的newDeps数组中
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 看到了吧!看到了吧!我说的主要证据就是这!!!
// 把watcher又反向放入Dep实例中
dep.addSub(this)
}
}
}
走到这里,我有必要进行一下阶段总结了,防止大家刚明白点,又乱了,这块逻辑确实比较绕,也比较讨巧。
- 这个dep,是响应式
aa
中的一个属性 - 当执行开发者的计算函数时,会执行
this.aa
, 进而进入aa的响应式逻辑中 - 响应式逻辑获取到
Dep.target
,也就是当前的computed watcher, 把dep赋值给watcher的newDeps属性 - 同时,又把watcher反向赋值给 dep.sub
- 这样,就形成了双向关系。 当我们执行computed函数时,把dep进行了存储,当
thsi.aa
,变化时,会执行setter 中的dep.notify()
, 进而去执行this.sub中的watcher。 - 这样,就又通知到了computed函数,函数重新执行, 进而再重新更新视图。
怎么样,大家看懂了么?
希望这篇不太长的文字可以帮助到大家。下一篇可能是分析一下watch的底层,但由于比较忙,可能更新较慢。敬请期待吧~~