computed主要要注意的有两点:
1、通过所谓的脏值检测,来保证多次使用该值但是只执行一次里边的回调函数并记录该返回值
2、依赖收集和更新,计算属性可能依赖多个值,需要保证只要依赖有变化就要去重新执行计算函数
js
export function initState(vm) {
const opts = vm.$options; // 获取所有选项
if(opts.data) {
initData(vm)
}
// 开始computed
if(opts.computed) {
initComputed(vm)
}
}
// vm上维护一个变量_computedWatchers 用来存放所有computed的副作用函数
function initComputed(vm) {
const computed = vm.$options.computed
const watchers = vm._computedWatchers = {}
for (let key in computed) {
let userDef = computed[key]
// 我们需要监控计算属性中get的变化
// computed写法有两种 一种是函数,一种是对象{set, get}
let fn = typeof userDef === 'function'? userDef: userDef.get
// 执行的副作用函数是通过watcher类来实现的,通过第三个参数来作为标示,区别渲染watcher
watchers[key] = new Watcher(vm, fn, {lazy:true})
// 这一步是跟data做一样的操作,通过this直接访问computed属性
defineComputed(vm, key, userDef)
}
}
function defineComputed(target, key, userDef) {
const setter = userDef.set || (() => {})
// conmputed 上的属性可以用this访问
Object.defineProperty(target, key, {
get: createComputedGetter(key), // 这个函数就是执行_computedWatchers对应key的watcher里的方法
set: setter
})
}
读取到计算属性时执行createComputedGetter,先说下这个函数的思路,这里可以拿到保存的计算属性watcher,在计算属性watcher里我们可以做下修改,通过第三个参数判断如果是计算属性就先不执行第二个参数(传进来的函数)而是重写一个方法evaluate专门给计算属性去调用get,然后通过一个变量第一次进来可以执行,之后将变量置反保证后续不再重复执行。
先修改下watcher
js
class Watcher {
constructor(vm, fn, options) {
this.id = id++
this.renderWatcher = options
this.getter = fn
this.deps = [] // 后续实现计算属性 和清理工作
this.depsId = new Set()
this.vm = vm
// 计算属性标示
this.lazy = options.lazy
this.dirty = this.lazy // 缓存值
this.lazy? undefined: this.get()
}
get() {
Dep.target = this // 调用的时候把当前watcher赋值给Dep.target
let value = this.getter.call(this.vm) // 会去vm上取值
Dep.target = null // 渲染完成置空
return value // 把返回值暴露出来
}
addDep(dep) { //一个组件对应多个属性 ,重复属性不用记录
let id = dep.id
if(!this.depsId.has(id)) {
this.deps.push(dep)
this.depsId.add(id)
dep.addSub(this) // watcher已经记住dep了而且去重 此时让dep也记住watcher
}
}
evaluate() {
this.value = this.get() // 获取用户的返回值 并且标记为脏
this.dirty = false
}
update() {
queueWatcher(this)
}
run() {
this.get()
}
}
完成createComputedGetter函数
js
function createComputedGetter( key) {
// 需要检测是否执行getter
return function() {
const watcher = this._computedWatchers[key]
if(watcher.dirty) {
// 如果是脏的就去执行 用户传入的函数
watcher.evaluate()
}
return watcher.value
}
}
检验下成果
看到虽然页面上两次使用fullName但是计算属性的函数只执行一次,到此computed写完一半了,即读取的时候有了脏值检测并进行缓存结果,剩下的就是更新了。
更新这个很复杂,我们顺着代码执行顺序开始捋。
1、进入页面执行渲染watcher
2、按之前的写法将当前渲染watcher赋值给Dep.target。
3、render函数读取值fullName,执行evaluate,将计算watcher赋值给Dep.target
4、读取到依赖firstName,lastName,因为Dep.target有值所以会进行depend,但是此时subs收集到的为计算watcher,deps会收集到两个dep。如果依赖更新只会通知计算watcher去执行,而不会进行视图更新。
问题找到了,所以目前要做的就是要改下Dep.target的赋值规则,在先生成渲染watcher后又生成计算watcher时,Dep.target后者不要覆盖前者,而是维护一个栈,先入后出,当后一个退出后重新赋值前一个,并在此时如果当前Dep.target还有值就认为是渲染watcher让subs重新进行一次收集watcher即收集到渲染watcher。
dep.js 暴露出两个方法
js
Dep.target = null
let stack = []
export function pushTarget(watcher) {
stack.push(watcher)
Dep.target = watcher
}
export function popTarget(watcher) {
stack.pop()
Dep.target = stack[stack.length -1]
}
watcher.js
js
class Watcher {
constructor(vm, fn, options) {
this.id = id++
this.renderWatcher = options
this.getter = fn
this.deps = [] // 后续实现计算属性 和清理工作
this.depsId = new Set()
this.vm = vm
// 计算属性标示
this.lazy = options.lazy
this.dirty = this.lazy // 缓存值
this.lazy? undefined: this.get()
}
get() {
pushTarget(this) // 调用的时候把当前watcher赋值给Dep.target
let value = this.getter.call(this.vm) // 会去vm上取值
popTarget(this) // 渲染完成置空
return value
}
// 当执行完计算属性的getter,重新赋值Dep.target后再调用此方法 让之前收集的dep收集一次watcher
depend() {
let i = this.deps.length
while(i--) {
this.deps[i].depend() //
}
}
// 更新的时候计算watcher只用放开dirty锁就行 渲染watcher会重新读一遍计算属性
update() {
if(this.lazy) {
this.dirty = true
} else {
queueWatcher(this)
}
}
}
完善createComputedGetter
js
function createComputedGetter( key) {
// 需要检测是否执行getter
return function() {
const watcher = this._computedWatchers[key]
if(watcher.dirty) {
// 如果是脏的就去执行 用户传入的函数
watcher.evaluate()
}
// 计算watcher的getter执行完成后 如果还有Dep.target就再进行一次依赖收集
if(Dep.target) {
watcher.depend()
}
return watcher.value
}
}
验证下效果
总算完成了,computed应该算vue2最难理解的一部分了,下一篇写个简单的watch