Vue3 响应式数据设计相关文章:
# Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统
# Vue3 响应式数据设计(二)从4个方面思考完善响应式式数据设计
在上一篇中我们实现了一个比较完善的响应式数据系统。利用调度器可以控制副作用函数的执行时机,执行次数等。本篇将基于之前的实现,实现一个Vue中一个重要的功能computed。
在深入讲解计算属性之前,需要先了解懒执行。现在我们实现的effect 函数会立即执行传递给它的副作用函数,例如 :
js
effect(
// 这个函数会立即执行
() => {
console.log(obj.count)
}
)
懒执行的effect
但有些场景下,我们并不希望它立即执行,而是希望它在需要的时候执行,例如计算属性。这时我们可以通过在options 中添加lazy 属性来达到目的。如下面的代码所示:
js
effect(
// 指定了lazy选项,这个函数不会立即执行
() => {
console.log(obj.count)
},{
lazy: true
}
)
有了lazy 属性之后我们就可以修改effect函数的实现了,当options.lazy为true时,则不立即执行函数:
js
function effect(fn, options) {
function effectFn() {
clearUp(effectFn)
acctiveEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中。
effectStack.push(effectFn)
fn()
// 在当前副作用函数执行完毕之后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值。
effectStack.pop()
acctiveEffect = effectStack[effectStack.length - 1]
}
effectFn.options = options
//
effectFn.deps = []
// 只有非lazy的时候才立即执行
if (!options.lazy) {
effectFn()
}
// 将副作用函数作为返回值返回。
return effectFn
}
通过这个判断,我们实现了让副作用函数不立即执行的功能。但问题是副作用函数什么时候执行呢?通过上面的代码可以看到,我们将副作用函数effectFn 作为effect函数的返回值,这就意味着当调用effect函数时,通过其返回值能够拿到对应的副作用函数,这样我们就能手动执行该副作用函数了:
js
const effectFn = effect(
// 指定了lazy选项,这个函数不会立即执行
() => {
console.log(obj.count)
},{
lazy: true
}
)
// 手动执行副作用函数
effectFn()
如果仅仅能够手动执行副作用函数,其实没有什么用。但如果我们把传递给effect的函数看作一个getter, 那么getter函数可以返回一任意值,例如:
js
const effectFn = effect(
// getter返回proxy.count 和proxy.num的和
() => proxy.count + proxy.num,
{
lazy: true
}
)
// 手动执行副作用函数
const value = effectFn()
为了实现这个目标我们需要对effect函数做一些修改,如以下代码:
js
function effect(fn, options) {
function effectFn() {
clearUp(effectFn)
acctiveEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中。
effectStack.push(effectFn)
const res = fn()
// 在当前副作用函数执行完毕之后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值。
effectStack.pop()
acctiveEffect = effectStack[effectStack.length - 1]
return res
}
effectFn.options = options
//
effectFn.deps = []
// 只有非lazy的时候才立即执行
if (!options.lazy) {
effectFn()
}
// 将副作用函数作为返回值返回。
return effectFn
}
computed 的初步实现
上面代码我们已经能实现懒执行的副作用函数,并且能拿到副作用函数执行的结果了,接下来就可以实现一下计算属性了,如下所示:
js
function computed (getter) {
const effectFn = effect(getter, {
lazy: true
})
const obj = {
// 当读取value时才执行effectFn
get value() {
return effectFn()
}
}
return obj
}
现在我们用实现的computed 创建一个计算属性:
js
const data = {
name: 'jame',
age: 30
}
const proxy = new Proxy(data, {
get (target, key) {
track(target, key)
return target[key]
},
set (target, key, newVal) {
target[key] = newVal
trigger(target, key)
return true
}
})
js
const sumres = computed(() => proxy.name + ':' + proxy.age + '岁')
proxy.age = 18
console.log(sumres.value)
computed缓存功能的实现
代码运行之后能得到我们想要的结果。但是我们现在实现的计算属性只实现了懒计算,并没有实现缓存功能。我们现在如果多次读取sumres.value ,effect 会多次执行。所以现在我们还需要添加缓存的功能。来看下具体代码:
js
function computed (getter) {
// 用来保存上一次的值
let value
// 用来保存是否需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler () {
dirty = true
}
})
const obj = {
get value() {
// 只有dirty为真时才需要重新计算。
if (dirty) {
value = effectFn()
// 将dirty 设置为false, 下一次读取时就会读取缓存中的值。
dirty = false
}
return value
}
}
return obj
}
现在我们已经实现了值的缓存功能。但是如果认真思考一下,就会发现问题。如果我们修改proxy.name 的值,再次读取sumres.value的值会发现值没发生改变。为什么会这样呢?因为当第一次读取sumres.value的值后,变量dirty会设置为false,代表不需要计算。即使我们修改了obj.foo的值,但只要dirty的值为false 就不会重新计算,所以就导致了修改之后重新读取计算属性的值也不会变。
解决计算属性更新的问题
怎么解决上面这个问题呢?其实很简单,当proxy.name 或proxy.age的值发生变化时,将dirty的值重置为true 就可以了。这可以用到之前说到的调度器来实现,代码如下:
js
function computed (getter) {
// 用来保存上一次的值
let value
// 用来保存是否需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler () {
dirty = true
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
现在我们再来运行下代码,结果就会符合预期了。
以下是目前我们响应式数据设计的完整代码:
js
const data = {
name: 'jame',
age: 30
}
const proxy = new Proxy(data, {
get (target, key) {
track(target, key)
return target[key]
},
set (target, key, newVal) {
target[key] = newVal
trigger(target, key)
return true
}
})
let acctiveEffect = null
const effectStack = []
function effect(fn, options = {}) {
function effectFn() {
clearUp(effectFn)
// 当effectFn执行时,将其设置为当前激活的副作用函数
acctiveEffect = effectFn
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
acctiveEffect = effectStack[effectStack.length - 1]
return res
}
effectFn.options = options
// 用来存储所有与该副作用相关的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
let bucket = new WeakMap()
/**
* desc 读取属性值时和副作用函数建立联系
* @param {代理的目标对象} target
* @param {属性,键} key
* @returns
*/
function track(target, key) {
if (!acctiveEffect) 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中
deps.add(acctiveEffect)
// deps 就是一个与当前副作用函数存在联系的集合
// 将其添加到activeEffect.deps数组中
acctiveEffect.deps.push(deps)
}
/**
* desc 当修改属性值时触发副作用函数处理逻辑
* @param {目标对象} target
* @param {键, 属性} key
* @returns
*/
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// 用一个新变量是为了避免无限循环
const newEffects = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== acctiveEffect) {
newEffects.add(effectFn)
}
})
newEffects && newEffects.forEach(fn => {
if (fn.options && fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
function clearUp (effectFn) {
for (var i = 0; i < effectFn.deps.length; i++ ) {
// deps 是依赖的集合
const deps = effectFn.deps[i]
// 将effectFn从依赖集合中移除
deps.delete(effectFn)
}
// 最后需要重置effectFn.deps数组
effectFn.deps.length = 0
}
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJop () {
if (isFlushing) {
return
}
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
function computed (getter) {
// 用来保存上一次的值
let value
// 用来保存是否需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler () {
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
track(obj, 'value')
return value
}
}
return obj
}
Vue3 响应式数据设计(三)computed 实现 就分享到这里了,感谢收看,一起学习一起进步