前言:经过前面三篇笔记的讨论,vue2.x/3.0的响应式以及数据驱动有了大致的了解,那具体到渲染过程中呢?vue中的computed,watch具体有哪些区别和共性呢?这将是本篇笔记的主题
computed
2.x版本
computed画像:
- 基于
data(后续2.x版本的状态将被成为data)进行计算得出的值 - 当
data发生变更,将自动进行重新计算得出新的值 - 具有懒惰特性,只有在被使用的时候才会重新进行计算
- 具有缓存特性,data发生变更后将只进行一次计算,后续被使用直接返回缓存值
基于以上"画像",可以知道
computed在vue数据驱动系统中身负两种角色: 订阅者和依赖,对于data来说,computed为订阅者,而对于render函数(页面渲染的方法)来说其为依赖
为了便于理解,这里再贴一下前面笔记定义过的Watcher类代码
class Watcher {
constructor(getter) {
this.id = uid++
this.getter = getter
this.newDeps = [] // 存储新一轮的dep
this.newDepIds = new Set()
this.deps = [] // 存储老一轮的dep
this.depIds = new Set()
this.value = this.get()
}
get() {
window.Deptarget = this
this.value = this.getter()
window.Deptarget = null
this.cleanUpDeps()
}
update() {
this.run()
}
run() {
this.get()
}
cleanUpDeps() {
// 清除不再依赖的dep,将newDeps赋值给deps,清空newDeps
let i = this.deps.length
while(i--) {
if (!this.newDepIds.has(this.deps[i].id)) {
this.deps[i].removeSub(this)
}
}
let temp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = temp
this.newDepIds.clear()
temp = this.deps
this.deps = this.newDeps
this.newDeps = temp
this.newDeps.length = 0
}
// 新增addDep方法,进行依赖收集
addDep(dep) {
// 如果新依赖列表不包含dep,则将其加入新dep列表
if (!this.newDepIds.has(dep.id)) {
this.newDepIds.add(dep.id)
this.newDeps.push(dep)
// 如果老dep列表中也不包含dep,则立即将当前订阅者加入dep.subs列表中
if (!this.depIds.has(dep.id)) {
dep.addSub(this)
}
}
}
}
computed的实现代码:(初版)
// data (伪代码)
count: 1
// 用户定义的computed函数
function double() {
return this.count * 2
}
function initComputed(vm, key, getter) {
// 创建computed-watcher
var watcher = new Watcher(getter)
defineComputed(vm, key, watcher)
}
function defineComputed(vm, key, watcher) {
Object.defineProperty(vm, key, {
get() {
return watcher.value
}
set() {}
})
}
上面的代码只实现了computed画像的1,2两条,但是懒惰和缓存没有
如果要实现懒惰特性,在创建computed-watcher的时候就不能直接执行getter函数,所以需要在实例化Watcher的时候增加一个options参数标识Watcher的一些行为
第一次改造,使之满足初始化懒惰
class Watcher {
constructor(getter, options) {
this.lazy = options.lazy
this.value = this.lazy ? undefined : this.get() // 如果懒惰Watcher则在实例化的过程中不再执行this.get
}
get() {}
}
function initComputed(vm, key, getter) {
// 创建computed-watcher
var watcher = new Watcher(getter, {lazy: true})
defineComputed(vm, key, watcher)
}
Watcher经过上面改造在initComputed时computed-getter不会进行计算,但是当引用computed的时候直接返回watcher.value为undefined显然不合适
第二次改造,使之满足引用的时候能够更新计算
function defineComputed(vm, key, watcher) {
Object.defineProperty(vm, key, {
get() {
return watcher.get() // 每次引用的时候重新进行计算
}
set() {}
})
}
这样初始化的时候computed-getter不会立刻进行计算,当真正引用到computed的时候才会执行watcher.get()进行重新计算,但是当data发生变更后computed仍会立刻重新计算,要真正使用的时候立刻重新计算
第三次改造,使之满足data变更的时候不立刻重新计算
class Watcher {
constructor(getter, options) {
this.lazy = options.lazy
this.value = this.lazy ? undefined : this.get() // 如果懒惰Watcher则在实例化的过程中不再执行this.get
}
get() {}
update() {
if (this.lazy) {
// do nothing
} else {
this.run()
}
}
}
经过三次改造已经实现了懒惰的特性,即只有在真正引用的时候才进行计算,但是在每次引用的时候都会重新计算,computed变成了被引用才重新计算,依赖更新反而不做任何事情了这不符合其缓存的特性,要实现变更后先不进行计算,等引用后看是否需要重新计算则要在Watcher中引入一个状态先记录依赖是否更新需要重新计算,等引用的时候依据该状态决定是否重新计算
第四次改造,使之满足懒惰和缓存
class Watcher {
constructor(getter, options) {
this.lazy = options.lazy
this.dirty = options.lazy // 懒watcher扩展字段记录是否需要重新计算
this.value = this.lazy ? undefined : this.get() // 如果懒惰Watcher则在实例化的过程中不再执行this.get
}
get() {}
update() {
if (this.lazy) {
this.dirty = true // 懒watcher在变更通知阶段dirty置为true,标识依赖已发生变更需要重新计算,即脏了
} else {
this.run()
}
}
evaluate() {
this.value = this.get()
this.dirty = false
}
}
function defineComputed(vm, key, watcher) {
Object.defineProperty(vm, key, {
get() {
if (watcher.dirty) {
watcher.evaluate()
}
return watcher.value
}
set() {}
})
}
经过修改已经满足了computed的4个画像,但是仔细思考就会发现一点问题:
便于描述我们称computed的watcher为computed-watcher,称渲染函数的watcher为render-watcher.基于以上改造如果render-watcher的getter方法中没有直接引用data而是引用了computed,当data发生变更的时候会发生什么?只需要简单分析就知道了,这种情况下render-watcher压根接收不到变更通知也就不会进行更新
第五次改造,修复render-watcher简介依赖data的问题
function defineComputed(vm, key, watcher) {
Object.defineProperty(vm, key, {
get() {
if (watcher.dirty) {
watcher.evaluate()
}
if (Deptarget) { // Deptarget是当前订阅者,也是一个Watcher实例
Deptarget.depend()
}
return watcher.value
}
set() {}
})
}
问题点就在于当computed被引用的时候要将其依赖的data和其订阅者之间建立依赖关系,这样就相当于真正的订阅者间接得依赖了computed依赖的依赖
下面贴出Deptarget.depend对应的代码(获取翻看前面笔记)便于理解
class Watcher {
...
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
class Dep {
...
depend () {
if (Deptarget) {
Deptarget.addDep(this)
}
}
}
2.x版本computed小节
在2.x版本中computed实现的大致流程:
首次渲染
- 响应化data
- 基于用户自定义
computed-getter创建computed-watcher并使用Object.definneProperty将其定义为vm的属性getter,computed-getter中将引用data render-watcher创建执行render将Deptarget置为render-watcher,同时将render-watcher推入targetStack,render中引用computed触发computed-getter- 触发
computed重新计算,Deptarget置为computed-watcher,同时将computed-watcher进行依赖收集 + 计算,建立computed和data的依赖关系 computed计算完成,将computed-watcher出栈- 将
Deptarget置为targetStack栈顶值即为render-watcher - 遍历
computed-watcher的依赖项并使之与Deptarget即render-watcher建立依赖关系 - 返回
computed-watcher.value作为computed的计算值 render执行结束,targetStack出栈,Deptarget置空,第一轮渲染结束 数据变更,重新渲染- data变更,通知订阅者更新(data的订阅者实际上有两类:
computed-watcher和render-watcher) - data通知
computed-watcher更新,执行watcher.evaluate将watcher.dirty置为true标识依赖变更需要重新计算 - data通知
render-watcher更新,引用computed,检查watcher.dirty为true需要重新计算,执行watcher.evaluate进行重新计算,计算完毕后将watcher.dirty置为false
3.0版本
经过2.x版本的实现,思路已经比较清晰了,直接尝试上代码:
class ComputedRefImpl {
constructor(getter) {
this.dirty = true
this.effect = createReactiveEffect(getter)
}
get value() {
this._value = this.effect()
this.dirty = false
return this._value
}
set value() {
console.warn('computed value is readonly')
}
}
function computed(getter) {
return new ComputedRefImpl(getter)
}
回想vue3.0创建computed值得方法:const double = computed(() => count * 2);此时得流程:
render-effect创建并立即执行render,activeEffect为render-effectrender对computed.value发生引用触发其get valuecomputed-effect执行并将activeEffect置为自己- 执行computed回调函数进行计算,在计算中发生对
data的引用触发其getter 进行track依赖收集建立data和computed之间依赖关系 - 将
computeddirty置为false并返回计算结果 问题: data发生变更通知computed-effect,导致其立即更新不符合懒惰性data发生变更并不会触发render-effect更新
针对问题1需要对变更通知做文章,问题2有两种思路一是借鉴2.x的机制在computed被引用的时候额外去建立所依赖data和render-effect之间的依赖关系;还可以将computed作为依赖和订阅者render-effect直接建立依赖关系,当computeddirty的时候通知render-effect进行更新,vue3.0的选择的是第二种方案
基于以上思路进行改进:
class ComputedRefImpl {
constructor(getter) {
this.dirty = true
this.effect = createReactiveEffect(getter, {
scheduler: () => {
if (!this.dirty) {
this.dirty = true
trigger(this, 'set', 'value') // computed变更,通知所有订阅者
}
}
})
}
get value() {
if (this.dirty) {
this._value = this.effect()
this.dirty = false
}
track(this, 'get', 'value') // 对引用到computed的effect建立依赖关系
return this._value
}
set value() {
console.warn('computed value is readonly')
}
}
function computed(getter) {
return new ComputedRefImpl(getter)
}
function trigger(target, type, key, newValue) {
const effects = new Set()
const depsMap = targetMap.get(target)
if (!depsMap) return
function add(effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect) {
effects.add(effect)
}
})
}
if (!depsMap.get(key)) return
add(depsMap.get(key))
function run(effect) {
if (effect.options.scheduler) { // 改进在这里,如果effect本身包含scheduler函数,则调用effect自身的scheduler进行调度
effect.options.scheduler()
} else {
scheduler(effect) // 如果自身不包含调度器,则调用统一调度函数,该函数的定义在上一篇笔记此处省略
}
}
effects.forEach(run)
}
这样当data发生变更后:
- 通知
computed-effect调用自身调度器computed-effect.options.shceduler - 调度器函数中将dirty置为true,同时通知
computed订阅者变更 render-effect接收到变更通知进行更新,调用renderrender调用引用computed.value触发get value- 检查
computed.dirty如果为false直接返回缓存的value否则执行computed-effect - 执行
computed-effect将引用data触发依赖更新
watch
2.x版本
考虑watch的用法:
{
data() {
return {
count: 1
}
},
watch: {
count(newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
毫无疑问watch是一个订阅者,应该是一个Watcher实例,尝试实现初始化watch的方法
function initWatch(key/*要watch的key*/, handler/*回调函数*/, vm) {
const getter = createGetter(key)
return new Watcher(getter, handler, vm)
}
// 基于key创建getter,访问vm.key,从而触发data的getter
function createGetterByPath(key) {
// 如果key形如'a.b.c'将被转为['a', 'b', 'c']
const segments = path.split('.')
return function(obj) {
// 如果key形如:'a.b.c'将访问obj.a.b.c
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
}
return obj
}
}
// Watcher要添加一个参数cb,表示getter执行完毕后要执行一个回调,更新机制也要发生变更
class Watcher {
constructor(getter, cb, vm, options) {
this.vm = vm
this.getter = getter
this.cb = cb
this.lazy = options.lazy
this.dirty = this.lazy
this.value = this.lazy ? undefined : this.get()
}
get() { ... }
run() {
const oldValue = this.value
this.value = this.get()
if (this.cb) {
this.cb.call(this.vm, this.value, oldValue)
}
}
}
watch初始化过程:
- 基于要监听的
data属性key创建getter函数,该函数将访问vm.key,其作用是能引用到监听的data属性触发其getter - 基于上个步骤创建的getter创建
watch-watcher,该watcher的getter为上步创建,回调watch-watcher.cb为用户自定义的watch-handler - 实例化
watch-watcher中执行其get方法,将引用data.key触发依赖收集,使得watch-watcher添加到data.key的subs订阅者列表 watch监听过程: data.key发生变更,通知watch-watcherwatch-watcher.update执行- 获取oldValue,执行
watch-watcher.get获取新值newValue,并进行依赖更新 - 将newValue, oldValue作为参数执行
watch-watcher.cb
3.0版本
考虑3.0中watch的使用:
const count = ref(1)
watch(() => count.value, (val, oldVal) => {
console.log(val, oldVal)
})
尝试实现watch方法
function watch(
keyOrFn/*监听对象,可以是key或者函数,为了简化这里只考虑函数型*/,
cb/*用户自定义回调*/,
instance/*vue对象,从中可以访问data*/
) {
let oldValue
const getter = () => keyOrFn.call(instance)
const runner = createReactiveEffect(getter, {scheduler})
const job = () => {
const newValue = runner()
cb.call(instance, newValue, oldValue
}
const scheduler = () => {
// 调度器其实就是对job的调度执行,的具体实现参看前面日记
job()
}
oldValue = runner()
}
watch初始化过程:
- 基于传入getter,将其上下文绑定到
instance使其能够访问到data;创建getter - 基于构建的getter创建runner即为
watch-effect,主要用于收集依赖 - 创建job,该job将在监听的
data发生变更后执行 - 创建调度器scheduler,用于异步调度job
- 初始化oldValue,runner
watch-effect第一次执行,收集依赖
watch更新过程: - data更新通知
watch-effect,调用watch-effect.shceduler - 执行job,获取新值newValue,引用data更新依赖
- 将oldValue、newValue作为参数调用cb即用户自定义的回调
computed和watch有哪些差异和共性
经过前面的分析可以总结出:
- 存在目的或者说应用场景不同,computed的唯一作用是用于页面渲染,而且它的方法应该是一个纯函数;watch的作用是监听某些状态变更然后进行某些操作,通常和页面渲染无直接关系,倾向于产生副作用
- 在vue内部的角色有差异,computed可以说既是订阅者又是依赖(2.x中并非直接依赖而是作为中间者的身份),watch是订阅者
- 更新时机不同,computed只有在render函数调用过程中才会重新计算,watch在data更新后即被添加到更新队列等待执行
- computed在一轮依赖变更中只重新计算一次后续被引用将直接返回缓存
思考
- computed还能依赖computed吗
- watch能监听computed吗