4.8 计算属性computed与lazy
// 存储副作用函数的桶
const bucket = new WeakMap()
// 原始数据
const data = { foo: 1, bar: 2 }
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
trigger(target, key)
}
})
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn())
}
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压栈
effectStack.push(effectFn)
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
// 将 options 挂在到 effectFn 上
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
// 执行副作用函数
if (!options.lazy) {
effectFn()
}
return effectFn
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
总结以上实现的响应式的代码
1.实现了将代理代码分离,track收集副作用函数,trigger执行副作用函数,
2.effect触发收集依赖
3.cleanup,处理了 xx.innerHTML=obj.ok?obj.text:"not"这样的分支问题,在trigger执行之前清除
4.effectStack处理了effect需要嵌套的问题
5. effectFn.options.scheduler(effectFn)处理的调度问题,让用户传递参数给effect来控制副作用函数的执行
下面看一用上面的代码实现computed方法
// =========================
function computed(getter) {
let value // 缓存值
let dirty = true // true表示getter中的多个数据,改变了,需要重新计算
const effectFn = effect(getter, {
lazy: true,//设置这个属性为true的时候 getter就不会立即执行,会返回到effectFn
scheduler() {
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
get value() {
if (dirty) { // 当computed依赖的两个数据发生改变,才执行
value = effectFn() // 执行getter,计算两个值
dirty = false
}
track(obj, 'value')
return value
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)
console.log(sumRes.value)
obj.foo++
console.log(sumRes.value)
effect(() => {
console.log(sumRes.value)
})
obj.foo++
当第一次打印sumRes.value,value = effectFn()执行了,访问了obj.foo + obj.bar,他们用track将这个副作用函数存储起来,dirty = false 第二次打印的时候,直接返回value,
修改依赖数据obj.foo++,trigger执行要执行track收集的函数,但这里有scheduler这个参数,所以执行的是```
if (!dirty) {
dirty = true
trigger(obj, ‘value’)
}
将dirty = true,再打印sumRes.value,会重新执行value = effectFn() 更新了value的值
其中 trigger(obj, 'value')和 track(obj, 'value')连个语句用于
```javascript
effect(() => {
console.log(sumRes.value)
})
obj.foo++
如果没有上面这种,写法,那么 trigger(obj, ‘value’)和 track(obj, ‘value’)他们收集的副作用为空,
让我们看看这种情况下如何执行的
effect执行,打印suRes.value, activeEffect有值,但是这个时候不会执行 obj的proxy,因为上面只是代理的obj(最上面的),所以打印suRes.value的时候是访问computed中的obj
执行
if (dirty) { // 当computed依赖的两个数据发生改变,才执行
value = effectFn() // 执行getter,计算两个值
dirty = false
}
track(obj, 'value')
return value
这个时候dirty为false,但是由于 activeEffect有值,手动 track(obj, ‘value’)收集了这个obj的副作用函数
下面再修改obj.foo++
scheduler() {
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
这个dirty为false,trigger执行刚才存储的副作用函数,执行 console.log(sumRes.value)
4.9 watch的实现原理
实现watch监听代理对象数据变化,回调函数中获取新旧值的功能
代码就补贴了,traverse就是变量对象,把所有对象访问一遍,做可以一次让所有对象属性都绑定自己的副作用函数,
watch(getter,cb)getter可以是一个已经代理的对象,也可以是一个有返回值的函数,对象会被 traverse给变量然后返回,注意一定要返回需要监听的值,
然后watch内部的effect 使用了lazy,不立刻执行副作用函数, 最后执行effectFn(),获得第一次的旧值,同时触发track收集副作用函数,当监听的值改变,scheduler中执行effectFn()获得新值,执行cb()然后将新值赋值给旧值,这样,下一次改变监听的数据,旧值就是上一次的新值
4.10 立即执行的watch与回调执行时机
这个主要是让watch中立即执行一次回调函数由,watch(obj,cb,options)中options.immediate为true控制,回调执行时机是用options.flush来控制回调函数执行是否添加到微任务中
4.11 过期的副作用
这个主要是watch监听的数据改变,回调函数如果是异步函数,那么每次数据改变,就会触发异步函数,但是如果确定那个异步函数的结果是我们想要的,这里让最后一个异步函数的结果为最终结果。让前面的异步函数过期,
主要是由异步函数的第三个参数onInvalidate 来控制,不过需要手动设置 let valid = true onInvalidate(() => { valid = false })
valid用于判断当前函数是否过期true表示未过期,false表示过期,onInvalidate是将参数的函数传递给,下一次修改监听数据,
由于是异步的当前await是没执行玩,如果下一次修改,还没执行玩。onInvalidate的函数就执行,valid变为false,这样上一次的函数就过期啦,就不采纳他的值。
let valid = true
onInvalidate(() => {
valid = false})
// 存储副作用函数的桶
const bucket = new WeakMap()
// 原始数据
const data = { foo: 1, bar: 2 }
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
trigger(target, key)
}
})
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn())
}
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压栈
effectStack.push(effectFn)
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
// 将 options 挂在到 effectFn 上
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
// 执行副作用函数
if (!options.lazy) {
effectFn()
}
return effectFn
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// =========================
// 变量代理对象所有的属性
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
for (const k in value) {
traverse(value[k], seen)
}
return value
}
function watch(source, cb, options = {}) {
let getter
// 和computed一样最终都会将第一个参数变成函数
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
let cleanup //存储上一次,onInvalidate传入的函数
function onInvalidate(fn) { // 上一次执行,将参数,赋值给 cleanup
cleanup = fn
}
const job = () => {
newValue = effectFn()
if (cleanup) {
cleanup() // 执行上一个异步函数的方法,改变他的valid 值
}
cb(oldValue, newValue, onInvalidate)
oldValue = newValue
}
const effectFn = effect(
// 执行 getter
() => getter(),
{
lazy: true,
scheduler: () => {
if (options.flush === 'post') { // 控制回调执行时机
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
let count = 0
function fetch() {
count++
const res = count === 1 ? 'A' : 'B'
return new Promise(resolve => {
setTimeout(() => {
resolve(res)
}, count === 1 ? 1000 : 100);
})
}
let finallyData
watch(() => obj.foo, async (newVal, oldVal, onInvalidate) => {
let valid = true
onInvalidate(() => {
valid = false
}) // 将方法传递给下一次job函数执行
const res = await fetch()
if (!valid) return
finallyData = res
console.log(finallyData)
})
obj.foo++
setTimeout(() => {
obj.foo++
}, 200);
问题总结:
4.8 计算属性computed与lazy
(十一)调度器的options中的lazy的含义?computed的实现原理?scheduler中的trigger和obj中的track有啥作用?
(1)lazy为true,就表示我们effect函数中不直接执行effeteFn,而是返回effectFn,在effectFn中也返回执行fun()副作用函数的结果,所以如果lazy为true,那么执行effect函数的结果会得到fun()执行的结果。这和computed的实现原理就很接近了
(2)computed就是由effect(getter,{lazy:true})改造而来,getters是一个有代理对象属性返回的函数,他执行会被收集,computed中用一个obj.value对象来接受getters返回的值,当getters中的数据改变的时候,访问computed的结果的时候,他会执行track(obj, ‘value’) 将obj变成响应式数据,每次访问computed的结果,并且getters中的数据有变化的时候都会重新执行effectFn,重新计算
(3)这两个手动收集和执行副作用函数,是为了实现effetet中嵌套computed,因为computed的结果并不是data中的代理,所以effect中执行,不会自动触发track和trigger,
track写在obj的get value中,用effect执行,可以触发track添加obj,更新getters依赖的的时候自动执行obj的effectFn函数,
4.9 watch的实现原理
(十二)watch的参数有哪一些?新旧值如何实现的?它的实现原理是?watch和computed相同和区别?
(1)watch(obj,cb,{ immediate,deep,flush })
其中第一个参数是obj,可以是一个被代理的对象,或者是一个返回代理对象属性的函数,都会在effect中被读取,被代理,这个和deep参数有关,deep为true,则会用traverse遍历全部,实现全部追踪,
cb是当监听的数据发生变化的时候的回调函数,是放在effect中scheduler函数中执行的,
deep:表示是不是需要深监听,
immediate:这个表示是不是watch执行的时候就执行一次回调函数(没有旧value)
flush:表示回调函数执行的时机,是在渲染前还是渲染后执行回调函数,这个的实现是scheduler中调用回调函数,可以使用队列,来异步调用,这样结果就会在最后
(2)这个先要设置effect的lazy为true,新值就是执行effectFn得到,旧值就是cb执行完后的新值
(3)从源码上来看,computed会根据getters中的依赖,生成一个新的结果,每次读取这个结果,他会先查看依赖是否有变化,如果有变化才会更新,也就是有缓存,computed的返回值本身也可以成为代理数据。
而watch其实本质是在监听代理数据的改变,也就是当代理数据触发trigger的时候,执行watch的回调函数,
他们都使用effect中的lazy和scheduler这两个参数选项,
computed用lazy获取更新后的依赖的值,scheduler用来设置是否更新缓存,
watch用lazy获取到回调函数的旧值,scheduler来执行回调函数
4.11 过期的副作用
(十三)竞态问题是什么?watch中如何解决竞态问题?
(1)竞态问题是指如果watch的回调函数是一个异步函数,例如是一个请求,那么每次watch监听的对象变化,会导致异步函数发送请求,我们该如何确定请求返回的结果是我们想要的结果,
(2)watch中遇到这种情况,默认是使用最后一次的结果,他使用的是回调函数cb的第三个参数,他是一个函数,函数的参数也是一个函数,用来修改expire,expire表示是否过期,每一次执行都会注册一个全局函数,作用是设置当前回调函数为过期,这样如果连续执行异步函数,下一次就会执行上一次的全局函数,将上一次回调函数expire设置为过期,如果上一个还没有结果,它的结果就会作废掉