综合前文提到的内容,我们可以实现Vue.js中一个非常有特色的功能——计算属性
在深入了解计算属性之前,需要先了解关于懒执行的effect,即lazy的effect。举个例子,现在所实现的effect函数会立即执行传递给它的副作用函数。
但在有些场景下,并不希望它立马执行,而是在需要的时候才执行,就例如计算属性。这里可以通过在options中添加lazy属性来实现,如下面代码所示:
effect(
// 指定了lazy选项,这个函数不会立即执行
()=>{
console.log(obj.foo)
},
// options
{
lazy: true
}
)
lazy选项和之前介绍的scheduler(调度器)一样,通过options选项对象指定。有了这个,就可以修改effect函数的实现逻辑了:当options.lazy为true时,则不立即执行副作用函数
function effect(fn, options =>{}){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.options = options
effectFn.deps = []
// 只有非lazy的时候,才执行
if(!options.lazy){
effectFn()
}
// 将副作用函数作为返回值返回
return effectFn
}
这里将副作用函数effectFn作为effect函数的返回值,就意味着当调用effect函数时,通过返回值能拿到对应的副作用函数,这样就能手动执行该副作用函数了
const effectFn = effect(()=>{
console.log(obj.foo)
}, {lazy:true})
// 手动执行副作用函数
effectFn()
但是仅仅是能手动执行副作用函数,意义并不大,但是如果把传递给effect的函数看作一个getter,那么这个getter函数可以返回任何值,例如
const effectFn = effect(
// getter 返回obj.foo与obj.bar的和
() => obj.foo + obj.bar,
{lazy:true}
)
// value 是 getter的返回值
// 这样我们在手动执行副作用函数时,就能够拿到其返回值
const value = effectFn()
为了实现这个目标,需要再对effect函数做一些修改,如以下代码所示:
function effect(fn, options =>{}){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
// 将fn()的执行结果存储到res中
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将res作为effectFn的返回值
return res
}
effectFn.options = options
effectFn.deps = []
// 只有非lazy的时候,才执行
if(!options.lazy){
effectFn()
}
// 将副作用函数作为返回值返回
return effectFn
}
通过新增的代码可以看到,传递给effect函数的参数fn才是真正的副作用函数,而effectFn是包装后的副作用函数。
现在已经能够实现懒执行的副作用函数,并且能够拿到副作用的执行结果,接下来就可以实现计算属性了,如下面代码:
function computed(getter){
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true
})
const obj = {
// 当读取value时才执行effectFn
get value(){
return effectFn()
}
}
return obj
}
computed函数的执行会返回一个对象,该对象的value属性是一个访问器属性,只有当读取value的值时,才会执行effectFn并将结果作为返回值返回
我们可以使用computed函数来创建一个计算属性:
扩展内容:
get和set的使用方法:
- get和set是方法,可以进行判断
- get要返回值;而set不用返回
- 如果调用对象内部的属性,则约定的命名方式是变量名前加_
var a={
_number:18,
get number(){
return this._number;
},
set number(val){
this._number=val;
}
}
a.number; // 18
a.number=20; // 20
a.number; // 20
.可以用computed函数来创建一个计算属性
const data = {foo:1, bar:2}
const obj = new Proxy(data, {/* ... */})
const sumRes = computed(()=> obj.foo+obj.bar)
console.log(sumRes.value) //3
这样代码可以正确运行了,但是现在实现的只做到懒计算,也就是只有真正读取sumRes.value的值时,才会进行计算并得到值。但还是做不到对值进行缓存,也就是假如我们多次访问sumRes.value的值,会导致effectFn进行多次计算,即使obj.foo和obj.bar的值本身并没有变化
那就要添加对值进行缓存的功能,见下面代码:
function computed(getter){
// value用来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true标识需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true
})
const obj = {
get value(){
// 只有“脏”时才计算值,并将得到的值缓存到value中
if(dirty){
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
这样只会在第一次访问时进行真正的计算,后续访问都会直接读取缓存的value值
但是如果此时修改obj.foo或obj.bar的值,再次访问sumRes.value会发现访问到的值没有发生变化
这是因为即使修改了obj.foo的值,但只要dirty的值为false,就不会重新计算。
解决方法很简单,当obj.foo或obj.bar的值发生变化时,只要dirty重置为true就可以了,那就要用到scheduler选项,如下面代码所示
function computed(getter){
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true
// 添加调度器,在调度器中将dirty重置为true
scheduler(){
dirty = true
}
})
const obj = {
get value(){
if(dirty){
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
这里为effect添加了scheduler调度器函数,其会在getter函数中所依赖的响应式数据变化时执行,这样在scheduler函数内将dirty重置为true,当下一次访问sumRes.value时,就会重新调用effectFn计算值,这样就能得到预期的结果
现在计算属性已经趋于完美,但还有缺陷,当我们在另外一个effect中读取计算属性的值是:
const sumRes = computed(()=>obj.foo + obj.bar)
effect(()=>{
// 在该副作用函数中读取 sumRes.value
console.log(sumRes.value)
})
// 修改obj.foo的值
obj.foo++
这里期望的结果是修改obj.foo的值,副作用函数重新执行,但是如果尝试运行上面这段代码,会发现修改obj.foo的值不会触发副作用函数的渲染
分析问题可以发现这是一个典型的effect嵌套:
一个计算属性内有自己的effect,并且是懒执行的。
对于计算属性的getter函数来说,这里面访问的响应式数据只会把computed内部的effect收集为依赖。
而把计算属性用于另一个effect时,就会发生effect嵌套,外层的effect不会被内层effect中的响应式数据收集
解决办法很简单,当读取计算属性的值时,可以手动调用track函数进行追踪;当计算属性依赖的响应式数据发生变化时,可以手动调用trigger函数触发响应
function computed(getter){
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true
scheduler(){
if(!dirty){
dirty = true
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应
trigger(obj,'value')
}
}
})
const obj = {
get value(){
if(dirty){
value = effectFn()
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
会建立如下的联系
computed(obj)
- value
- effectFn