本章目标
- 计算属性是如何实现的?
- 计算属性缓存原理 - 带有dirty属性的watcher
- 洋葱模型的应用
初始化
// 初始化状态
export function initState(vm {
const opts = vm.$options // 获取所有的选项
// 初始化数据
if (opts.data { initData(vm }
// 初始化计算属性
if (opts.computed { initComputed(vm }
}
我们给每个计算属性都创建了一个 Watcher实例,标识为lazy:true
,在初始化watcher时不会立即执行 get方法(计算属性方法)
然后使用Object.defineProperty
去劫持计算属性
// 初始化计算属性
function initComputed(vm {
const computed = vm.$options.computed
const watchers = (vm._computedWatchers = {} // 将每个计算属性对应的watcher 都保存到 vm上
for (let key in computed {
let userDef = computed[key]
// 兼容不同写法 函数方式 和 对象getter/setter方式
let fn = typeof userDef === 'function' ? userDef : userDef.get
// 给每个计算属性都创建一个 watcher,并标识为 lazy,不会立即执行 get-fn
watchers[key] = new Watcher(vm, fn, { lazy: true }
// 劫持计算属性getter/setter
defineComputed(vm, key, userDef
}
}
// 劫持计算属性
function defineComputed(target, key, userDef {
const setter = userDef.set || (( => {}
Object.defineProperty(target, key, {
get: createComputedGetter(key,
set: setter,
}
}
当我们劫持到计算属性被访问时,根据 dirty 值去决定是否更新 watcher缓存值
// 劫持计算属性的访问
function createComputedGetter(key {
return function ( {
const watcher = this._computedWatchers[key] // this就是 defineProperty 劫持的targer。获取到计算属性对应的watcher
// 如果是脏的,就去执行用户传入的函数
if (watcher.dirty {
watcher.evaluate( // 重新求值后 dirty变为false,下次就不求值了,走缓存值
}
// 当前计算属性watcher 出栈后,还有渲染watcher 或者其他计算属性watcher,我们应该让当前计算属性watcher 订阅的 dep,也去收集上一层的watcher 即Dep.target(可能是计算属性watcher,也可能是渲染watcher
if (Dep.target {
watcher.depend(
}
// 返回watcher上的值
return watcher.value
}
Dep
-
stack
:存放 watcher 的栈。 利用 pushTarget、popTarget 这两个方法做入栈出栈操作
Dep.target
:当前渲染的 watcher,静态变量
// 当前渲染的 watcher
Dep.target = null
// 存放 watcher 的栈
let stack = []
// 当前 watcher 入栈,Dep.target 指向 当前 watcher
export function pushTarget(watcher {
stack.push(watcher
Dep.target = watcher
}
// 栈中最后一个 watcher 出栈,Dep.target指向栈中 最后一个 watcher,若栈为空,则为 undefined
export function popTarget( {
stack.pop(
Dep.target = stack[stack.length - 1]
}
计算属性Watcher
在初始化Vue实例时,我们会给每个计算属性都创建一个对应watcher(我们称之为计算属性watcher,除此之外还有 渲染watcher 和 侦听器watcher ),他有一个 value 属性用于缓存计算属性方法的返回值。
默认标识 dirty: true,脏的,当我们劫持到计算属性访问时,如果是脏的,我们会通过watcher.evaluate
重新计算 watcher 的 value值 并将其标识为干净的;如果是干净的,则直接取 watcher 缓存值
实现洋葱模型的核心方法
dep.notify派发更新 并 调用 update 方法,只需更新 dirty 为 true即可。我们会在后续的渲染watcher 更新时,劫持到计算属性的访问操作,并通过 watcher.evaluate
重新计算其 value值
class Watcher {
constructor(vm, fn, options {
// 计算属性watcher 用到的属性
this.vm = vm
this.lazy = options.lazy // 懒的,不会立即执行get方法
this.dirty = this.lazy // 脏的,决定重新读取get返回值 还是 读取缓存值
this.value = this.lazy ? undefined : this.get( // 存储 get返回值
}
// 重新渲染
update( {
console.log('watcher-update'
if (this.lazy {
// 计算属性依赖的值发生改变,触发 setter 通知 watcher 更新,将计算属性watcher 标识为脏值即可
// 后面还会触发渲染watcher,会走 evaluate 重新读取返回值
this.dirty = true
} else {
queueWatcher(this // 把当前的watcher 暂存起来,异步队列渲染
// this.get(; // 重新渲染
}
}
// 计算属性watcher为脏时,执行 evaluate,并将其标识为干净的
evaluate( {
this.value = this.get( // 重新获取到用户函数的返回值
this.dirty = false
}
// 用于洋葱模型中计算属性watcher 订阅的dep去 depend收集上层watcher 即Dep.target(可能是计算属性watcher,也可能是渲染watcher
depend( {
let i = this.deps.length
while (i-- {
this.deps[i].depend(
}
}
}
缓存原理
计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。 缓存原理如下:
Object.defineProperty劫持了计算属性,并做了一些 getter/setter操作
当我们劫持到计算属性被访问时,如果 dirty 为 true,则执行 evaluate 更新 watcher 的 value值 并 将 dirty 标识为 false;如果为 false,则直接取 watcher 的缓存值
洋葱模型
在初始化Vue实例时,我们会给每个计算属性都创建一个对应的懒的watcher,不会立即调用计算属性方法
watcher.evaluate(让其直接依赖的属性去收集当前的计算属性watcher,并且还会通过watcher.depend(
让其订阅的所有 dep都去收集上层watcher,可能是渲染watcher,也可能是计算属性watcher(如果存在计算属性嵌套计算属性的话)。这样依赖的属性发生变化也可以让视图进行更新
让我们一起来分析下计算属性嵌套的例子
<p>{{fullName}}</p>
computed: {
fullAge( {
return '今年' + this.age
},
fullName( {
console.log('run'
return this.firstName + ' ' + this.lastName + ' ' + this.fullAge
},
}
- 初始化组件时,渲染watcher 入栈
stack:[渲染watcher]
computed watcher1.evaluate(
,watcher1
入栈
stack:[渲染watcher, watcher1]
watcher1
的 get方法时,其直接依赖的 firstName 和 lastName 会去收集当前的 watcher1;然后又访问 fullAge 并执行computed watcher2.evaluate(
,watcher2
入栈
watcher1:[firstName, lastName]
stack:[渲染watcher, watcher1, watcher2]
watcher2
的 get方法时,其直接依赖的 age 会去收集当前的 watcher2
watcher2:[age]
watcher2
出栈,并执行watcher2.depend(
,让watcher2
订阅的 dep再去收集当前watcher1
stack:[渲染watcher, watcher1]
watcher1:[firstName, lastName, age]
watcher1
出栈,执行watcher1.depend(
,让watcher1
订阅的 dep再去收集当前的渲染watcher
stack:[渲染watcher]
渲染watcher:[firstName, lastName, age]