Vue.js设计与实现之六-computed计算属性的实现

开发 前端
计算属性computed其实是一个懒执行的副作用函数,可以通过lazy选项使得副作用函数可以懒执行,被标记为懒执行的副作用函数可以通过手动执行。

1、写在前面

在前面文章介绍了effect的实现,可以用于注册副作用函数,同时允许一些选项参数options,可以指定调度器去控制副作用函数的执行时机和次数等。还有用于追踪和收集依赖的track函数,以及用于触发副作用函数重新执行的trigger函数,结合这些我们可以实现一个计算属性--computed。

2、懒执行的effect

在研究计算属性的实现之前,需要先去了解下懒执行的effect(lazy的effect)。在当前设计的effect函数中,它会在调用时立即执行传递过来的副作用函数。但是事实上,希望在某些场景并不希望它立即执行,而是在需要的时候才执行,前面了解到想要改变effect的执行可以在options参数中设置。

const data = {
name:"pingping",
age:18,
flag:true
}
const state = new Proxy(data,{
/*...*/
})
effect(()=>{
console.log(state.name);
},{
//指定lazy选项,这样函数不会立即执行
lazy: true
})

就这样,通过设置options选项,去修改effect函数的实现逻辑,当options.lazy为true时不会立即执行副作用函数:

// effect用于注册副作用函数
function effect(fn,options={}){
const effectFn = ()=>{
// 调用函数完成清理遗留副作用函数
cleanupEffect(effectFn)
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = effectFn;
// 在副作用函数执行前压栈
effectStack.push(effectFn)
// 执行副作用函数
fn();
// 执行完毕后出栈
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将options挂载到effectFn函数上
effectFn.options = options
//deps是用于存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才执行
if(!options.lazy){
// 执行副作用函数effectFn
effectFn()
}
//否则返回副作用函数
return effectFn
}

在上面代码片段中,在effect函数中先判断了是否需要懒执行,对此会判断options.lazy的值为true时,则将effectFn副作用函数作为参数返回到effect。这样,用户在调用执行effect函数时,可以通过返回值去拿到对应的effectFn函数,这样可以手动执行该函数。

const effectFn = effect(()=>{
console.log(state.name);
},{
//指定lazy选项,这样函数不会立即执行
lazy: true
});
//手动执行副作用函数
effectFn();

但是仅仅实现手动执行副作用函数,对于我们的使用意义并不大,如果将返回到effect的副作用函数作为getter,那么通过这个取值函数就能获取返回任何值。

const effectFn = effect(
()=>state.name + state.age,
{
//指定lazy选项,这样函数不会立即执行
lazy: true
});

//手动执行副作用函数,可以获取到返回的值
const value = effectFn();

这样就可以实现在调用的时候,手动执行获取到各种想要得到的值。在effect函数内部只需要做出些改变,只需要在执行副作用函数时将副作用的值返回即可:

// effect用于注册副作用函数
function effect(fn,options={}){
const effectFn = ()=>{
// 调用函数完成清理遗留副作用函数
cleanupEffect(effectFn)
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = effectFn;
// 在副作用函数执行前压栈
effectStack.push(effectFn)
// 执行副作用函数,将执行结果存储到res
const res = fn();
// 执行完毕后出栈
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将res作为effectFn的返回值
return res
}
// 将options挂载到effectFn函数上
effectFn.options = options
//deps是用于存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才执行
if(!options.lazy){
// 执行副作用函数effectFn
effectFn()
}
//否则返回副作用函数
return effectFn

}

现在,我们已经实现了能够进行懒执行的副作用函数,能够拿到执行返回的结果,做后续的处理。

3、computed属性

懒计算的computed属性

其实,基于前面的设计和代码实现,大概有了computed属性函数的实现雏形,就是接收一个getter函数作为副作用函数,用于创建一个懒执行的effect。computed函数的执行会返回包含一个访问器属性的对象,只有在读取value值的时候才会去执行effectFn并返回结果。

function computed(getter){
const effectFn = effect(
getter,
{
//指定lazy选项,这样函数不会立即执行
lazy: true
});
const state = {
//当对value进行读取操作时,执行effectFn并将结果进行返回
get value(){
return effectFn();
}
}
return state;
}

在上面代码中,只是粗略做了懒计算处理,只有在真正对sumRes.value的值进行读取操作时,才会去进行计算并得到值。但是在进行多次读取sumRes.value的值,每次访问计算得到的值都是相同的,并不符合我们需要使用上次计算值的要求。『计算属性需要有缓存机制,这样就可以使用到上次计算的结果。』

const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);

运行结果:

之所以发生这种情况,多次读取sumRes.value的值时,每次访问都会重新调用effectFn重新计算。

带有缓存的computed

为了解决前面获取不到上次计算值的问题,需要在实现computed函数时,添加对计算值的缓存操作。其实实现很简单,就是添加两个变量value和dirty,value用于缓存上次计算的值,dirty则标识是否需要重新计算。

function computed(getter){
let value;
let dirty = true;
const effectFn = effect(
getter,
{
//指定lazy选项,这样函数不会立即执行
lazy: true
//在调度器重置dirtytrue
scheduler(){
dirty = true
}
});
const state = {
//当对value进行读取操作时,执行effectFn并将结果进行返回
get value(){
//只有当dirty标识为true值时,才会将计算值进行缓存,下一次访问直接使用缓存的值
if(dirty){
value = effectFn();
dirty = false
}
return value
}
}
return state;
}

在上面代码中,初始化设置dirty为true,这样就会把计算值进行缓存,下次进行同样computed计算操作时,就会直接使用缓存的值,而非每次重新计算。同时,在computed函数的effect中添加scheduler属性,在函数内部将dirty的值重置为true,在下次访问sumRes.value时重新调用effectFn的计算值。

const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
state.age++;
console.log("hello", sumRes.value);

执行结果为:

但是,在当前设计的计算属性在另一个effect函数中读取时,修改响应数据state上的属性值并不会触发副作用函数的重新渲染。其实根本原因就是这里存在一个effect嵌套问题,computed内部是effect函数实现的,而在effect中读取computed的值相当于对effect进行了嵌套,外层的effect不会被内层effect的响应式数据收集。

当然,问题很简单,解决方法同样很简单。只需要在读取计算属性值的时候,手动调用track函数进行追踪,当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应:

function computed(getter){
let value;
let dirty = true;

const effectFn = effect(
getter,
{
//指定lazy选项,这样函数不会立即执行
lazy: true,
//在调度器重置dirtytrue
scheduler(){
dirty = true
trigger(state, "value")
}
}
);
const state = {
//当对value进行读取操作时,执行effectFn并将结果进行返回
get value(){
//只有当dirty标识为true值时,才会将计算值进行缓存,下一次访问直接使用缓存的值
if(dirty){
value = effectFn();
dirty = false
}
// 对value进行取值操作时,手动调用track函数进行追踪
track(state, "value")
return value
}
}
return state;
}

写一段简单的demo进行实验:

const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
effect(()=>{
console.log(sumRes.value);
})
state.age++

console.log("hello", sumRes.value);

执行结果:

根据上面的实现demo可以分析出对应的计算属性的响应联系图:

计算属性的响应联系

4、写在最后

计算属性computed其实是一个懒执行的副作用函数,可以通过lazy选项使得副作用函数可以懒执行,被标记为懒执行的副作用函数可以通过手动执行。在读取计算属性的值时,可以手动执行副作用函数,在依赖的响应式数据发生变化时,通过scheduler将dirty标记设置为true,即为脏数据,在下次读取计算属性的值,就会重新计算得到真正的值。

责任编辑:姜华 来源: 前端一码平川
相关推荐

2022-04-12 08:08:57

watch函数options封装

2022-04-25 07:36:21

组件数据函数

2022-04-01 08:08:27

Vue.js框架命令式

2022-04-04 16:53:56

Vue.js设计框架

2022-04-18 08:09:44

渲染器DOM挂载Vue.js

2022-05-03 21:18:38

Vue.js组件KeepAlive

2022-04-14 09:35:03

Vue.js设计Reflect

2022-04-05 16:44:59

系统Vue.js响应式

2022-04-17 09:18:11

响应式数据Vue.js

2022-04-09 17:53:56

Vue.js分支切换嵌套的effect

2022-04-03 15:44:55

Vue.js框架设计设计与实现

2022-04-26 05:55:06

Vue.js异步组件

2022-04-16 13:59:34

Vue.jsJavascript

2022-04-20 09:07:04

Vue.js的事件处理

2022-04-19 23:01:54

Vue.jsDOM节点DOM树

2023-12-11 07:34:37

Computed计算属性Vue3

2016-11-01 19:10:33

vue.js前端前端框架

2021-09-18 10:07:23

开发技能代码

2019-04-01 19:38:28

Vue.jsJavascript前端

2022-06-26 00:00:02

Vue3响应式系统
点赞
收藏

51CTO技术栈公众号