当前位置:首页 > 数据库

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

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,

//在调度器重置dirty为true

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,

//在调度器重置dirty为true

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,即为脏数据,在下次读取计算属性的值,就会重新计算得到真正的值。

分享到:

滇ICP备2023006006号-16