vue3响应式系统的基本实现

3 篇文章 0 订阅

响应式数据的基本实现

我们先了解一个副作用函数,什么是副作用函数呢?

副作用函数:一个会产生副作用的函数,副作用是指函数在正常工作任务之外对外部环境所施加的影响。

副作用很容易产生,例如有一个函数修改了全局变量,这其实也是一个副作用,如:

	let text = '1// 全局变量
	function effect () {
		text = '2// 修改全局变量,产生副作用
	} 

我们再来了解什么是响应式数据?
比如 我们有一个副作用函数,其内部读取了某个对象的属性,当前该属性发生变化时,我们能重复执行副作用函数:

const obj = { text: 'hello world!' }
function effect () { // 副作用函数中 读取了 obj对象的text值,同时将该值设置为body的文本
	document.body.innerText = obj.text
}
// 当前 obj.text 的值 发生变化时,我们需要能再次执行上面的effect 副作用函数
obj.text = 'hello vue3'

上面代码中,我们如何才是实现响应式呢?

如果我们能拦截obj对象的 get(读取) 与 set(设置)操作,这样当我们监测到obj.text 读取值的时候,我们可以将副作用函数 effect 存储到一个“桶”里面,如下面所示:
在这里插入图片描述上图 存储副作用函数

当 obj.text 值 发生变化时,我们再从“桶”里面取出对应的副作用函数并执行,如图所示:
在这里插入图片描述
上图 触发副作用函数

基于 代理对象 Proxy,实现对象值的读取及设置拦截, 下面我们就根据 Proxy 来实现响应式:

// “桶” 用于存储 副作用函数
const bucket = new Set()

// 原数据
const data = { text: 'hello world!' }
// 对原数据的代理
const obj = new Proxy(data, {
	get (target, key) { // 拦截读取
		// 将副作用函数添加到 “桶”中
		bucket.add(effect)
		return target[key]
	},
	set (target, key, newValue) { // 拦截设置
		target[key] = newValue
		// 执行“桶”中的副作用函数
		bucket.forEach(fn => fn())
	}
})
// 重点看上面代码

function effect () { // 副作用函数中 读取了 obj对象的text值,同时将该值设置为body的文本
	document.body.innerText = obj.text
}
effect() // 执行副作用函数 触发读取

// 1s后修改 obj.text 值
setTimeout(() => obj.text = 'hello vue3', 1000)

上面代码中,我们先创建了 bucket 且是一个 Set 类型的值,它是用于存储副作用函数的桶。还定义了 原数据 data 及 代理对象 obj,设置里 get 和 set 拦截函数,用于拦截读取与设置。当读取属性的时候 我们将 effect 副作用函数添加到bucket中,同时返回属性值,当设置属性值时先更新原数据,然后再遍历 bucket 并执行里面的副作用函数。然后,定义 effect 函数,其将obj.text 属性值设置为body的文本,我们又执行了 effect 函数,后面设置了一个定时器 1s 后执行 将 obj.text 修改为 hello vue3。在控制台中执行了上面代码,来验证我们响应式的实现是成功了的。

当然在我们上面代码的响应式监听中还是不完善的,有很多bug,我们来继续完善它。

微型响应式系统

副作用函数当然不能是我们写死的一个函数,我们需要设置个桶 来 存储 副作用函数,然后提供一个用于注册副作用函数的机制,如下所示:

// 设置一个变量用于存储被注册的副作用函数
let activeEffect
// effect 函数哟哦那个鱼注册副作用函数
function effect (fn) {
	// 将副作用函数 fn 赋值给 activeEffect
	activeEffect = fn
	fn() // 执行 副作用函数
}

// 示例 注册副作用函数:掉用effect 方法 并传入 对应的副作用函数
effect(() => document.body.innerText = obj.text)

我们使用一个匿名的副作用函数作为 effect 函数的参数。当 effect 函数执行时,首先会把匿名的副作用函数 fn 赋值给全局变量 activeEffect 。接着执行被注册的匿名副作用函数 fn,这将会触发响应式数据 obj.text 的读取操作,进而触发代理对象 Proxy 的 get 拦截函数,我们在get 拦截函数中 修改 副作用函数 读取,从以前的 effect 修改成 activeEffect,判断activeEffect是否有值,如果有我们将这个副作用函数添加到 bucket 中去,这样响应系统就不依赖副作用函数名字,代码如下所示 :

// 下面我来完善 Proxy
// 对原数据的代理
const obj = new Proxy(data, {
	get (target, key) { // 拦截读取
		if (activeEffect) {
			// 将副作用函数添加到 “桶”中
			bucket.add(activeEffect)
		}
		return target[key]
	},
	set (target, key, newValue) { // 拦截设置
		target[key] = newValue
		// 执行“桶”中的副作用函数
		bucket.forEach(fn => fn())
	}
})

如果我们在响应式数据 obj 上设置一个不存在的属性(新增属性)时,我们会发现我们的副作用函数也会执行,按照理论上来说,新增一个属性,同时这个属性与副作用函数没有简历响应关系,我们修增或修改与副作用函数无关的值,不应该出发副作用函数重新执行。为了解决这个问题我们现在需要重新设计一个 bucket 的数据结构。

我们上面设置的 bucket 是一个 Set 数据结构,我们没有在副作用函数与被操作的目标字段之间建立明确的联系。现在问题很明确就是副作用函数与被操作的字段之间没有建立联系,如果需要建立联系,我们就得重新设计 bucket 的数据结构,上面使用 Set 类型 作用"桶"的数据结构已经满足不了我们的需求来。

那么我们应该怎么样来设计这个数据结构才能符合我们的需求呢?我们需要仔细观察一下我们副作用函数代码,上面的副作用函数来说:

// 示例 注册副作用函数:掉用effect 方法 并传入 对应的副作用函数(这里使用 effectFn 来表示被注册的副作用函数)
effect(fucntion effectFn() { document.body.innerText = obj.text })

在这段代码中存在三个角色:

  • 代理对象 obj
  • 代理对象字段名 text
  • 使用 effect 函数注册的副作用函数 effectFn

如果用 target 来表示一个代理对象所代理的原始对象,用 key 来表示被操作的字段名,用 effectFn 来表示被注册的副作用函数,那么可以为这三个角色建立如下关系:

 target
    └── key
         └── effectFn

下面举几个例子来对其进行补充说明:

// 如果有两个副作用函数同时读取同一个对象的属性值:
effect(function effectFn1() {
   obj.text
})
effect(function effectFn2() {
  obj.text
})
// 关系如下:
target
    └── text
         └── effectFn1
         └── effectFn2


// 一个副作用函数中读取了同一个对象的两个不同属性:
effect(function effectFn3() {
  obj.text1
  obj.text2
})
// 关系如下:
target
    └── text1
         └── effectFn3
    └── text2
         └── effectFn3


// 在不同的副作用函数中读取了两个不同对象的不同属性:
effect(function effectFn4() {
  obj1.text1
})
effect(function effectFn5() {
  obj2.text2
})
// 关系如下:
target1
    └── text1
         └── effectFn4
target2
    └── text2
         └── effectFn5

了解了上面案例,我相信你是理解了新的桶数据结构,其实就是一个树型数据结构。接下来我们就尝试实现这个新的“桶”,首先需要先将bucket 变更为 WeakMap 类型:

// “桶” 用于存储 副作用函数
const bucket = new WeakMap() // 后面会解释为什么用WeakMap 类型 而不是其他类型

然后我们再修改 代理拦截get、set代码:

// 给原数据、属性、副作用函数 建立关系
// 对原数据的代理
const obj = new Proxy(data, {
	get (target, key) { // 拦截读取
		// 如果没有 activeEffect,不需要建立关系,直接返回对应属性值
		if (!activeEffect) { 
			return target[key]
		}
		
		// 根据 target 从 bucket 桶中 获取 depsMap
		let depsMap = bucket.get(target)
		
		// 如果 depsMap 不存在,那么就新建一个 Map 与 target 关联
		if (!depsMap) {
			bucket.set(target, (depsMap = new Map()))
		}
		
		// 根据 key 从 depsMap 中取得 deps 值, 它是一个 Set 类型
		// 里面存储所有与当前 key 相关的副作用函数
		let deps = depsMap.get(key)
		
		// 如果 deps 不存在,那么就新建一个 Set 与 key 关联
		if (!deps) {
			depsMap.set(key, (deps = new Set()))
		} 

		// 将当前副作用函数添加到 deps 中
		deps.add(activeEffect)
		
		return target[key]
	},
	set (target, key, newValue) { // 拦截设置
		target[key] = newValue
		
		// 这里 就是 负责取值同时执行
		// 根据 target 从 bucket 桶中 获取 depsMap
		const depsMap = bucket.get(target)
		
		// 如果没有直接 return
		if (!depsMap) return
		
		// 根据 key 取得对应的 副作用函数集
		const effects = depsMap.get(key)
	
		// 执行副作用函数
		effects?.forEach(fn => fn && fun())
	}
})

根据上面段代码可以看出构建数据结构的方式,我们分别使用了 WeakMap、Map 和 Set:

  • WeakMap 由 target --> Map 构成
  • Map 由 key --> Set 构成

WeakMap、Map 和 Set 之间的关系图:
在这里插入图片描述
搞清了它们之间的关系,我们有必要解释一下这里为什么要使用 WeakMap,这其实涉及 WeakMap 和 Map 的区别。

我们引用一下《JavaScript高级程序设计(第四版)》的原话:

ECMAScript6新增的”弱映射“(WeakMap)是一种新的集合类型,为这门语言带来了增强的键值对存储机制。WeakMap是Map的”兄弟“类型,其API也是Map的子集。WeakMap中的”weak“(弱),描述的是JavaScript垃圾回收程序对待的”弱映射“中键的方式。 ---- 《JavaScript高级程序设计(第四版)》6.5

Map与WeakMap简单区别

  • Map的键值可以是原始数据类型和引用类型,WeakMap的键值只能说引用类型(object)
  • Map可以迭代遍历键,WeakMap不可迭代遍历键

WeakMap中的”weak“表示弱映射的键是弱引用,意思就是这些键不属于正式的引用。换言之,WeakMap所构建的实例中,其key键所对应引用地址的引用断开或不属于指向同一个内存地址的时候,其对应value值就会被加入垃圾回收队伍。
如果使用 Map 来代替 WeakMap,那么即使用户侧的代码对 target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出。

我们用一段代码来讲解:

const map = new Map()
const weakmap = new WeakMap()
(function(){
    const foo = {foo: 1}
    const bar = {bar: 2}
    map.set(foo, 1)
    weakmap.set(bar, 2)
})()

我们定义了 map 和 weakmap 常量,分别对应 Map 和 WeakMap 的实例。接着定义了一个立即执行的函数表达式(IIFE),在函数表达式内部定义了两个对象:foo 和 bar,这两个对象分别作为 map 和 weakmap 的 key。当该函数表达式执行完毕后,对于对象 foo 来说,它仍然作为 map 的 key 被引用着,因此垃圾回收器(grabage collector)不会把它从内存中移除,我们仍然可以通过map.keys 打印出对象 foo。然而对于对象 bar 来说,由于 WeakMap 的 key 是弱引用,它不影响垃圾回收器的工作,所以一旦表达式执行完毕,垃圾回收器就会把对象 bar 从内存中移除,并且我们无法获取 weakmap 的 key 值,也就无法通过 weakmap 取得对象 bar。

最后,我们对上面代码做一些封装处理:

// 对原数据的代理
const obj = new Proxy(data, {
	get (target, key) { // 拦截读取
		// 调用 track 处理函数 将 activeEffect 添加到 bucket 副作用函数关联桶里
		track(target, key)
		
		return target[key]
	},
	set (target, key, newValue) { // 拦截设置
		target[key] = newValue
		
		// 把 关联的副作用函数 从 bucket 桶中取出并执行
		trigger(target, key)
	}
})

// 在 get 拦截函数内掉用 track 函数追踪变化
function track (target, key) {
	// 如果没有 activeEffect,不需要建立关系
	if (!activeEffect) { 
		return
	}
	
	// 根据 target 从 bucket 桶中 获取 depsMap
	let depsMap = bucket.get(target)
	
	// 如果 depsMap 不存在,那么就新建一个 Map 与 target 关联
	if (!depsMap) {
		bucket.set(target, (depsMap = new Map()))
	}
	
	// 根据 key 从 depsMap 中取得 deps 值, 它是一个 Set 类型
	// 里面存储所有与当前 key 相关的副作用函数
	let deps = depsMap.get(key)
	
	// 如果 deps 不存在,那么就新建一个 Set 与 key 关联
	if (!deps) {
		depsMap.set(key, (deps = new Set()))
	} 

	// 将当前副作用函数添加到 deps 中
	deps.add(activeEffect)
}

// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger (target, key) {
	// 这里 就是 负责取值同时执行
	// 根据 target 从 bucket 桶中 获取 depsMap
	const depsMap = bucket.get(target)
	
	// 如果没有直接 return
	if (!depsMap) return
	
	// 根据 key 取得对应的 副作用函数集
	const effects = depsMap.get(key)

	// 执行副作用函数
	effects?.forEach(fn => fn && fun())
}

我们将 get 拦截函数里编写把副作用函数收集到“桶”里的这部分逻辑单独封装到一个 track 函数中,函数的名字叫 track 是为了表达追踪的含义。同样,我们也可以把触发副作用函数重新执行的逻辑封装到trigger 函数中,这能为我们带来极大的灵活性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值