【vue设计与实现】响应系统的作用与实现 5-计算属性computed与lazy

综合前文提到的内容,我们可以实现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的使用方法:

  1. get和set是方法,可以进行判断
  2. get要返回值;而set不用返回
  3. 如果调用对象内部的属性,则约定的命名方式是变量名前加_
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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值