Hi,大家好~ 在上一篇 Vue2 响应式原理解析(一):从设计登程 中我讲了一下 Vue2 是如何形象和设计响应式的,data
是如何实现响应式的,包含依赖收集和双向依赖记录的设计思路和要害代码。在这一篇中,咱们来一起康康 Vue 中十分弱小的响应式性能:计算属性。我次要会从性能需要的角度来剖析计算属性的实现和要害代码,心愿能带给大家一些在别的文章里看不到的货色吧。以下内容请先看过 第一篇 再来比拟好~
计算属性 computed
在 Vue 的 文档 中有提到计算属性的设计初衷是为了解决模板内表达式过于简单、难以了解。当然解决此问题还有一个计划就是用 methods
中定义的办法,但计算属性有个十分弱小的个性:缓存。这意味着计算属性依赖的数据如果没有发生变化,则再次拜访计算属性时就不会从新计算,间接返回缓存的后果,这对于计算简单的场景十分实用。
那计算属性的实现怎么和后面咱们讲过的响应式设计联合起来呢~?这里咱们先看一张图:
这张图形容的就是当你申明了一个计算属性后(这里举的栗子就是申明 fullName
计算属性),Vue 转换成了图左边的 getter
+ watcher
的构造来实现计算属性的所有性能。如果看起来有点懵逼的话不要急,上面就来一一揭秘计算属性是如何实现和工作的。
实现细节
首先来到 src/core/instance/state.js
文件,有个 initComputed
函数,这个函数就是初始化计算属性的中央,上面咱们来看看要害局部的代码:
function initComputed (vm: Component, computed: Object) {
// vm 对象上减少了 _computedWatchers 寄存计算属性对应的 watcher
const watchers = vm._computedWatchers = Object.create(null)
// ...
for (const key in computed) {
// 计算属性反对 setter,为了简洁阐明重点咱们只关注计算属性申明为函数的状况哈
const getter = typeof userDef === 'function' ? userDef : userDef.get
// ...
// 服务器端渲染的状况也先不关注哈
if (!isSSR) {
// 留神这里,每个计算属性对应生成了一个 watcher,并把计算属性的函数作为 getter 传进去了
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
// 这里就是在 vm 对象上定义计算属性的描述符了
defineComputed(vm, key, userDef)
}
// ...
}
}
这就是计算属性的次要实现过程。首先呢,咱们先把视角拉高一点,只关注重点流程,不要陷入太多细节哈,细节前面会讲到。重点流程就是下面那张图上形容的:Vue 为每个计算属性生成了一个 watcher
,并在 vm
对象上申明了跟计算属性同名的存取描述符,一会他们俩要配合应用。这里须要留神的是 Watcher 构造函数传入的 computedWatcherOptions
,这个对象有个 lazy: true
的属性,待会就晓得是干嘛用的了。
从计算属性的应用动手来讲缓存
上面就是计算属性实现的细节和精髓局部了。首先咱们来到下面说的 defineComputed
函数,仍然去掉一些神马服务端渲染逻辑的烦扰,只看次要实现细节的代码:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 先不论服务器端渲染
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
// 留神这里调用了 createComputedGetter 来生成描述符的 getter
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
// ...
}
// ...
// 在 vm 上生成计算属性同名的存取描述符
Object.defineProperty(target, key, sharedPropertyDefinition)
}
在 defineComputed
里咱们看到,最终描述符的 get
是由 createComputedGetter
生成的,这个函数就是要害中的要害了~
在持续之前,咱们先回忆一下计算属性的应用场景和缓存的利用。通常咱们定义好计算属性之后,就会在 template
里去应用。当界面第一次显示时,计算属性会计算值,除非计算属性的依赖项发生变化(比方:依赖的 data
对象的属性从新赋值了),否则前面的刷新不会导致计算属性从新计算,而是会间接返回上一次的缓存值。从这里能够看出,template
里去读取计算属性的值,实际上就是调用 vm
上计算属性描述符的 get
了。
理清了场景后,咱们把 createComputedGetter
分为两局部来看,先关注跟缓存相干的前半部分代码:
function createComputedGetter (key) {return function computedGetter () {
// 首先在这里取到 vm 上的 watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {if (watcher.dirty) {watcher.evaluate()
}
// ...
return watcher.value
}
}
}
下面代码中的 watcher.dirty
就是示意计算属性以后的值是否须要从新计算,如果不须要从新计算就间接返回 watcher.value
了,这就是实现了缓存的作用。那么这里咱们回忆一下计算属性在初始化 watcher
的时候传入了一个 lazy: true
,并且在 Watcher
的构造函数中有这样的逻辑:
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
this.lazy = !!options.lazy
// ...
this.dirty = this.lazy // 是不是脏了须要求值,初始化的时候就是 true
//...
// 如果是计算属性,初始化 watcher 不会求值
this.value = this.lazy
? undefined
: this.get()}
}
这个意思就是说:如果是计算属性,初始化 watcher
时不会求值,只会标记脏了——缓存有效。那么在下面计算属性的 get
第一次被调用时 watcher.dirty
(界面第一次显示),会调用 watcher.evaluate()
:
evaluate () {this.value = this.get()
this.dirty = false
}
在 evaluate
里就执行 get()
去求值了,并且标记缓存无效。回忆一下当依赖的 dep
产生 set
时,会执行 watcher.update()
:
update () {if (this.lazy) {
// 如果是计算属性,这里就会标记为 dirty
this.dirty = true
}
// ...
}
所以依赖发生变化,缓存就会生效,计算属性又会从新计算了~
以上就是计算属性缓存的设计和实现细节了,我尽量只摘取要害代码把要害的事件说分明。 这个中央咱们须要留神的是,Vue 把计算属性这种场景形象成一种 lazy watcher
,lazy watcher 只在须要的时候计算值,并且有缓存性能!所以形象能力是值得咱们学习的中央~
依赖传递
咱们回过头来看看 createComputedGetter
的后半局部代码:
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 后面代码是判断缓存是否要更新
// ...
// 留神这里就是依赖传递了
if (Dep.target) {watcher.depend()
}
return watcher.value
}
}
}
前半部分的缓存曾经讲过了,当初咱们留神这里有个依赖传递的逻辑,这是什么意思呢?
这里咱们还是用场景来举例吧。比方在计算属性的申明中你是能够援用另外一个计算属性的!因为计算属性在初始化 watcher
时不会求值,也就是 lazy watcher
,所以这样是没问题的。比方上面的代码:
const vm = new Vue({
el: '#demo',
data: {
a: 1,
b: 2
},
computed: {c: function () {return a + b},
d: function () {return c * 2}
}
})
这里就用这个简略的例子来阐明为什么须要依赖传递:计算属性 c
依赖了 data
上的 a
和 b
,计算属性 d
又依赖了 c
。那么问题来了,当 a
和 b
产生扭转时 c
会 dirty
,当然 d
也须要 dirty
,不然 d
就会有缓存不会从新求值了。那么 d
怎么失去告诉呢?
要害的代码就是下面的 watcher.depend()
了。首先,d
取值时会调用 d
本身的 watcher.get()
,这个时候会把 d
的 watcher
设置为 Dep.target
;接着 c
的 watcher.get()
执行时也会把 c
的 watcher
设置为 Dep.target
。这里留神了,设置 Dep.target
时是调用的 pushTarget
,这个函数会调用 targetStack
数组把以后曾经记录的 Dep.target
推入数组:
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {targetStack.push(target)
Dep.target = target
}
export function popTarget () {targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
// 过后说了 targetStack 数组将在当前的文章中解析,这里圆回来了
执行完了这些之后呢,c
的 watcher
首先和 a
和 b
的 dep
建设了依赖关系,而后求值 watcher.evaluate()
。求值完后也就是 c
执行完本人的 watcher.get()
,留神在 get()
办法的最初执行了 popTarget()
,也就是说 c
把本人的 watcher
弹出来了,目前的 Dep.target
又变成了 d
的 watcher
!
当 c
求值完后,如果以后还有 Dep.target
存在就会执行 watcher.depend()
来传递依赖了,咱们来看看 watcher.depend()
到底做了什么:
depend () {
let i = this.deps.length
while (i--) {this.deps[i].depend()}
}
代码的意思很明确了,就是让 c
以后依赖的这些 deps
也去建设与 Dep.target
的依赖(也就是 d
了)。这样当 a
和 b
产生扭转时 d
也会 dirty
了。
通过下面这个场景的形容你应该明确为什么须要依赖传递了吧~
结尾
以上呢就是 Vue 计算属性的细节和我的解读,如果有不分明的请联合 第一篇 来看看。
到这里 Vue2 的响应式大体讲的就差不多了。前面还会再写一篇说说侦听属性,而后再回到设计从整体上坚固一下。如果有说的不对或有其余见解欢送留言探讨哇~
欢送 star 和关注我的 JS 博客:小声比比 Javascript