响应系统 — computed&&watch
✓computed源码实现:
创建如下测试实例,逐步执行代码进行实现:
<body>
<div id="app"></div>
<script>
const { reactive, effect, computed } = Vue
const obj = reactive({
name: '张三'
})
const computedObj = computed(() => {
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerText = computedObj.value
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
</body>
首先执行
reactive函数生成响应式数据,逻辑与之前相同;接着执行
computed,进入computed函数,通过iSFunction函数判断传入的参数是否为函数,如果是函数则重新赋值给getter变量,执行new ComputedRefImpl得到实例并返回;该类中通过之前的
reactiveEffect类传入调用computed的匿名函数以及一个调度器函数,并赋值给ComputedRefImpl实例上的effect变量。
代码实现:
export class ComputedRefImpl {
public dep?: Dep //存放依赖set对象
public readonly effect
public _value
public __v_isRef = true //标记是否为ref数据
public _dirty = true //通过判断脏状态来确定是否需要触发依赖
constructor(getter) {
//通过reactiveEffect创建实例 该实例的run方法会执行fn函数,也即getter函数,也即调用computed传入的匿名函数,第二个为调度器函数 后续执行到相应的代码再做解释
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
//为effect增加computed属性为当前实例,以便后期触发区分
this.effect.computed = this
}
}
export function computed(getterOrOptions) {
let getter
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
}
const cRef = new ComputedRefImpl(getter)
return cRef
}
//isFunction方法
export const isFunction = (val: unknown) => {
return typeof val === "function"
}
代码向下执行,调用
effect函数,为activeEffect赋值,调用传入的匿名函数。
export class ReactiveEffect<T = any> {
constructor(public fn: () => T, public scheduler: EffectScheduler | null = null) {
}
run() {
activeEffect = this
return this.fn()
}
stop() { }
}
// effect
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn)
if (!options) {
_effect.run()
}
}
执行
document.querySelector('#app').innerText = computedObj.value;会触发
ComputedRefImpl实例的get value,通过trackRefValue方法收集依赖,与之前逻辑相同;然后通过判断脏状态来执行run方法,也即执行
() => { return '姓名:' + obj.name},执行这个会调用reactive函数的getter函数收集依赖;在
dep中建立起指定属性名与fn函数,即() => {document.querySelector('#app').innerText = computedObj.value }的关系
代码实现:
export class ComputedRefImpl {
public dep?: Dep
public readonly effect
public _value
public __v_isRef = true
public _dirty = true
constructor(getter) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
}
get value() {
//收集依赖
trackRefValue(this)
if (this._dirty) {
this._dirty = false
//执行当前传入的computed的函数
this._value = this.effect.run()
}
return this._value
}
}
export function computed(getterOrOptions) {
let getter
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
}
const cRef = new ComputedRefImpl(getter)
return cRef
}
接着两秒之后执行
obj.name = '李四',触发reactive函数的setter函数;此时要触发依赖,与之前不同的是,此时要触发的是
computed的依赖,之前的this.effect.computed = this这行代码就起到作用了,触发依赖时,首先会判断当前依赖中是否有conputed属性,需要先触发有computed属性的依赖;触发依赖又会判断当前是否存在调度器,如果有调度器则直接执行调度器;
也即再次执行
document.querySelector('#app').innerText = computedObj.value;调用
ComputedImpl实例的get value方法返回最新的值,视图发生改变,结束执行;调度器方法,即之前创建
ComputedImpl实例传入的第二个方法:() => { if (!this._dirty) { this._dirty = true triggerRefValue(this) } }//其余代码与之前相同 触发依赖时需要进行判断 export const triggerEffects = (dep: Dep) => { const effects = isArray(dep) ? dep : [...dep] for (const effect of effects) { if (effect.computed) { triggerEffect(effect) } } for (const effect of effects) { if (!effect.computed) { triggerEffect(effect) } } } export const triggerEffect = (effect: ReactiveEffect) => { if (effect.scheduler) { //执行computed传入的函数 effect.scheduler() } else { effect.run() } }
✓处理computed的缓存性:
当computed函数依赖的响应性数据没有发生变化时,他不会重新调用;
创建如下实例:
<script>
const { reactive, computed, effect } = Vue
const obj = reactive({
name: '张三'
})
const computedObj = computed(() => {
console.log('计算属性执行计算');
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
document.querySelector('#app').innerHTML = computedObj.value
})
setTimeout(() => {
obj.name = '李四'
}, 2000);
</script>
当调用
effect函数,第一次执行document.querySelector('#app').innerHTML = computedObj.value,触发
ComputedImpl实例的get value收集依赖,当前脏状态为真,将脏状态改为false;执行run方法,也即执行
computed传入的匿名函数;第二次执行
document.querySelector('#app').innerHTML = computedObj.value,因为脏状态为false,所以不会执行匿名函数直接返回之前的值。
() => {
// 判断当前脏的状态,如果为 false,表示需要《触发依赖》
if (!this._dirty) {
// 将脏置为 true,表示
this._dirty = true
triggerRefValue(this)
}
})
✓watch源码实现:
创建如下测试实例:
<script>
const { ref, watch, reactive } = Vue
const obj = reactive({
name: '张三'
})
watch(
obj,
(value, oldValue) => {
console.log('watch 监听被触发')
console.log('value', value)
},
{
immediate: true
}
)
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
这里我们跳过上篇文章的
reactive的执行,直接开始执行watch。
执行
watch方法,进入Watch方法,这个方法返回doWatch方法的调用,主要逻辑都在这个方法当中。export function watch(source, cb: Function, options?: WatchOptions) { return doWatch(source, cb, options) } function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) { }
首先声明
getter变量,判断传入的source是否为reactive响应式对象,source即调用watch传入的监听的对象,如果是reactive响应式对象,就为getter赋值为一个函数返回值是这个source,并且默认开始深度监听;function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) { let getter: () => any if (isReactive(source)) { getter = () => source deep = true } else { getter = () => { } } }
接下来会判断deep和调用watch是否传入了回调函数,会重新声明
baseGetter变量保存getter的变量,getter会被重新赋值为一个函数返回值是traverse方法,这个方法会处理baseGetter函数调用的返回值;function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) { let getter: () => any if (isReactive(source)) { //source即传入的响应式对象 getter = () => source deep = true } else { getter = () => { } } if (cb && deep) { //保存getter值 let baseGetter = getter getter = () => traverse(baseGetter()) } }//traverse方法 为了响应式对象的原始值 也即”张三“ 为了给watch回调函数中传入新值和旧值得参数 export function traverse(value) { if (!isObject(value)) { return value } for (const key in value) { traverse(value[key]) } return value }接下来会定义一个job方法,这个方法主要是获取新值,调用传入
watch的回调函数,并处理新旧值,也即watch的监听触发也是job的触发;function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) { //..............省略上述代码 let oldValue = {} const job = () => { if (cb) { const newValue = effect.run() //haschanged 判断新值和旧值是否相同 if (deep || hasChanged(newValue, oldValue)) { //调用回调函数 cb(newValue, oldValue) //新值给旧值处理 oldValue = newValue } } } }接下来会声明一个调度器
let scheduler = () => queuePreFlushCb(job),queuePreFlushCb这个方法的主要目的是将job方法推入到微任务当中,待同步代码执行完毕之后,然后才回去调用job方法;let scheduler = () => queuePreFlushCb(job)//queuePreFlushCb 逻辑 //标记状态 逐步执行 let isFlushPending = false //promise.resolve let resolvePromise = Promise.resolve() as Promise<any> let currentFlushPromise: Promise<void> | null = null //任务数组 let pendingPreFlushCbs: Function[] = [] //cb 即job方法 export function queuePreFlushCb(cb: Function) { queueCb(cb, pendingPreFlushCbs) } // 将所有需要处理的任务推到任务数组 function queueCb(cb: Function, pendingQueue: Function[]) { pendingQueue.push(cb) queueFlush() } // 依次处理队列中的函数 放入微任务 function queueFlush() { if (!isFlushPending) { isFlushPending = true //将flushjobs方法得执行放入微任务,当所有同步代码执行完毕之后才会执行这个方法 currentFlushPromise = resolvePromise.then(flushjobs) } } // 处理队列 function flushjobs() { isFlushPending = false flushPreFlushCbs() } // 依次执行微任务 循环遍历任务数组 调用函数 也即调用job函数 也即触发watch监听 function flushPreFlushCbs() { if (pendingPreFlushCbs.length > 0) { //相当于 let activeFlushCbs = pendingPreFlushCbs let activeFlushCbs = [...new Set(pendingPreFlushCbs)] pendingPreFlushCbs.length = 0 for (let i = 0; i < activeFlushCbs.length; i++) { activeFlushCbs[i]() } } }6.接下来会
new一个reactiveEffect实例,处理依赖关系。function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = {}) { //............ /** * @param getter getter = () => traverse(baseGetter()) 调用这个也即返回原始值张三 * @param scheduler scheduler = () => queuePreFlushCb(job) 调用这个也即将job放入任务队列 */ const effect = new ReactiveEffect(getter, scheduler) if (cb) { if (immediate) { //有immediate配置 立即执行job方法 job() } else { //调用run方法即调用getter函数,上篇文章中有reactiveEffect逻辑 可以查看 //拿到旧值 oldValue = effect.run() } } else { effect.run() } return () => { effect.stop() } }
✒watch方法执行完毕,两秒之后执行obj.name = '李四',也就是会触发reactive中的setter方法,他会进行依赖的触发,也即触发triggerEffect,这个方法会判断当前的effect中是否存在scheduler,存在则直接调用,这个方法的调用即将job方法放入到微任务当中,等待后续执行;
两秒完毕,reactive响应式对象obj中的name被改为李四,接下来会执行微任务数组,也就是会执行job方法;
//也即如下代码
// 处理队列
function flushjobs() {
isFlushPending = false
flushPreFlushCbs()
}
// 依次执行微任务
function flushPreFlushCbs() {
if (pendingPreFlushCbs.length > 0) {
let activeFlushCbs = [...new Set(pendingPreFlushCbs)]
pendingPreFlushCbs.length = 0
for (let i = 0; i < activeFlushCbs.length; i++) {
activeFlushCbs[i]()
}
}
}
✒job方法执行,意外着watch执行监听,触发回调函数,获取到新值,并进行新旧值处理,watch处理完毕。
const job = () => {
if (cb) {
const newValue = effect.run()
if (deep || hasChanged(newValue, oldValue)) {
cb(newValue, oldValue)
oldValue = newValue
}
}
}