上一章节我们分析了Vue.js基于Object.defineProperty实现响应式原理的核心流程,引入了Watcher、Dep、依赖收集、派发更新等概念。
# Vue2.0响应式原理剖析。
# 侦听属性watch原理剖析。
本章趁热打铁,分析一下Vue.js内部计算属性computed的实现原理。
computed
应用场景: 计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来。
特点:
-
计算属性会基于某一个响应式数据来计算,只要依赖的数据不变,计算属性的值不会重新计算(
dirty缓存) -
计算属性只有真正访问到才会执行计算逻辑。只定义不使用的话内部计算逻辑不会执行(
lazy) -
计算属性本质上也是一个
watcher(computed watcher)。 -
computed的2种使用方式:
computed: {
sum() {
return this.count + 1
},
sumCount: {
get() {
return this.count + 1
},
set() {}
}
}
我们先来分析一下computed的初始化逻辑:
initState (vm: Component) {
const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch) { // watch的初始化
initWatch(vm, opts.watch)
}
}
initComputed函数主要做2件事:
- 定义一个
watchers对象(vm._computedWatchers),用来收集计算属性watcher。 - 遍历传入的
computed配置,拿到用户定义的key和执行函数,为每一个key创建对应watcher。 - 通过
defineComputed函数对计算属性的key值做了一层拦截,当我们访问vm.key时,实际会访问到该key值对应的computedGetter函数,computedGetter函数是由createComputedGetter(key)创建的,该函数内部会执行一系列计算逻辑。
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
...
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
...
}
}
我们看一下computed watcher和render watcher的实例化有哪些不同:
computed watcher: 实例化watcher传入了lazy属性,表明了这是个computed watcher,dirty也为true,并且不会执行get函数。getter为用户自定义的计算属性执行函数。(懒)
render watcher: getter为updateComponent函数,该函数在组件初始化和更新的过程中都会执行,并且第五个参数标明为渲染watcher,然后立马执行get函数。
// 计算属性watcher实例化
getter = () => {
return this.count + 1
}
new Watcher(
vm,
getter,
noop, // noop为空函数
{ lazy: true }
)
// 渲染watcher实例化
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true) // 标识是渲染watcher
class Watcher {
constructor (
vm: Component,
expOrFn: Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
if (options) {
this.lazy = !!options.lazy // lazy为true
}
this.cb = cb
this.dirty = this.lazy // for lazy watchers
this.getter = expOrFn
this.value = this.lazy
? undefined
: this.get()
}
get () {
...
}
}
defineComputed函数做了如下操作:
通过Object.defineProperty做了一层拦截,当访问计算属性key值时,会执行到createComputedGetter(key)方法。通过key值我们找到之前定义在vm._computedWatchers上面的计算属性watcher,然后会执行一系列计算。计算逻辑后续在介绍。
总结: 只有用户真正访问到计算属性时,计算属性才会计算求值
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
function defineComputed (
target: any,
key: string,
userDef: Object | Function // 只处理函数类型
) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
}
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
}
}
}
让我们结合一个demo来看一下计算属性的执行逻辑:
// 页面上一开始会显示2,当我们点击button按钮后,sum的取值变为了3
<div id="app">
<button @click="changeCount">{{ sum }}</button>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
count: 1
}
},
computed: {
sum() {
return this.count + 1
}
},
methods: {
changeCount() {
this.count = 2
}
}
})
</script>
计算属性依赖收集
new Vue之后会执行初始化(..., initState),组件的挂载流程,实例化一个render watcher,并且执行watcher.get()方法。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true) // 标识是渲染watcher
class Watcher {
...,
get () {
pushTarget(this) // targetStack.push(target) Dep.target = target
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 让updateComponent函数执行
} catch (e) {
} finally {
if (this.deep) {
traverse(value)
}
popTarget() // targetStack.pop() Dep.target = targetStack[targetStack.length - 1]
}
return value
}
}
get方法首先会执行pushTarget,把render watcher添加到targetStack数组中,并将当前的Dep.targget指向render watcher。
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
watcher.get方法会执行this.getter方法,实际执行到render watcher传入的updateComponent方法,执行vm_render时会执行已经编译好的render函数(template -> render function),后面会有专门章节分析编译原理。
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on: {"click":changeCount}},[_v(_s(sum))])])}
})
render函数中会有对计算属性sum的访问,通过vm._computedWatchers['sum']找到sum对应的计算属性watcher,判断如果当前watcher的dirty为true,执行计算逻辑(evaluate)。
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
class Watcher {
evaluate () {
this.value = this.get()
this.dirty = false
}
}
5.上面我们说的,实例化计算属性watcher时传入的lazy为true,此时dirty的值也为true。然后执行watcher上面的evaluate函数,此时才会真正执行watcher.get函数。同第3步一样,先执行pushTarget,把computed watcher添加到targetStack数组中,此时的targetStack会有两项,然后将当前的Dep.target指向computed watcher。
6.执行computed watcher传入的getter函数,此时传入的getter函数为
function () {
return this.count + 1
}
因为count本身是响应式数据,取值会触发count的依赖收集,当前的Dep.target指向了computed watcher, 所以count对应的dep就会把computed watcher收集起来:
const dep = new Dep()
Object.defineProperty(obj, 'count', {
get: function reactiveGetter () {
if (Dep.target) { // 指向计算属性watcher
dep.depend()
}
}
})
class Dep {
constructor () {
this.id = uid++
this.subs = []
},
...,
depend () {
if (Dep.target) { // 计算属性watcher
Dep.target.addDep(this) // 调用watcher的addDep方法,把dep传入
}
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
class Watcher {
...,
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this) // 调用dep的addSub,并把计算属性watcher传入
}
}
}
}
最后执行popTarget操作,targetStack删除最后一项,并把Dep.target重新指向render watcher。
function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
我们来看一下computed watcher上的变化:
7. 执行完
watcher.get函数后,把computed watcher上的dirty置为false。
8.由于Dep.target此时指向了render watcher,接下来继续执行computed watcher的depend方法,找到computed watcher上的deps集合,遍历deps,调用每一个dep的depend。dep.depend流程上面已经分析过了,执行完depend后,subs数组中会新增一项为render Watcher。
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend() // 依然是 'computed watcher'
}
return watcher.value
}
}
class Watcher {
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
执行完depend函数后,会返回watcher.value。至此完成了依赖收集。
计算属性派发更新
修改count,触发count的set拦截,调用dep.notify,找到count对应的dep.subs数组,遍历,依次执行watcher的update方法。这里分析一下computed watcher的更新流程: computed watcher只会把dirty重新改为true,别的什么都不做。render watcher会执行queueWatcher方法,这个流程在响应式已经分析了,此处就不做展示开了。
class Watcher {
...,
update () {
if (this.lazy) { // computed watcher
this.dirty = true
} else if (this.sync) { // 同步watcher
this.run()
} else {
queueWatcher(this)
}
}
}
总结:针对计算属性而言,第一次取值时dirty为true, 取完值后dirty置为false。如果依赖的值不发生变化,下次取值时会返回上次计算的值。由于取值函数也会访问到其依赖的值,所以依赖的值会先把computed watcher收集起来,再把render watcher收集起来。当我们修改依赖的值时,遍历收集的watcher,执行相应watcher的更新逻辑即可。