话不多说,我们直接进入正题,模拟实现所有Vue3
响应式相关API
为了不混乱,我先将响应式相关API
进行分类,如图所示
由于文章篇幅较长,为了避免大家疲劳,先作出两点改善:
- 分篇;将文章按照上述分类和内容量分为多篇文章
- 插入图片;我将尽量多插入一些相关图片,一来缓解疲劳,二来帮助大家理解
此篇目标是深入了解9
个响应式基础API
中的reactive
,并模拟实现我们自己的数据响应式
思路
我的思路其实非常简单和非常清晰,首先去了解API
的基本使用,然后试着去使用和理解它,然后按照它所实现的功能模拟实现我们自己的功能,如下
工作准备
在开始前,我们需要做一点准备工作
- 需要创建一个vue3项目,方便使用对应的响应式
API
如果你不知道怎么创建,官网提供了多种创建方式:传送门 - 单独创建一个文件,用于模拟实现对应
API
为了方便,我将上篇文章(从0开始手动实现Vue3初始化流程)所用的文件拿来继续使用,当然你也可以使用这个文件,简单来说,这个文件实现了Vue3
的初始化流程相关的几个API
,比如createApp
和mount
方法,我们可以在这个文件的基础上进行模拟实现数据响应式API
mini-vue3
有了以上的准备,下面开始深入理解reactive
reactive函数
我们分两部分来说:reactive
的使用和模拟实现
reactive的使用
我们先来看官方对于reactive
的解释,官方的解释也非常简单
返回对象的响应式副本
但从这句话我们可以得到以下信息
-
reactive
接受一个对象作为参数 - 其返回值是经
reactive
函数包装过后的数据对象,这个对象具有响应式
但同样会有一些疑问
比如,reactive
的参数只能传递一个对象吗,如果传递其他值会怎么样?
比如,返回的响应式数据的本质是什么,为啥就能让数据变成响应式?
比如,"副本"是不是意味着响应式数据与原始数据没有关联?
比如,返回的响应式副本里头的数据是深度响应式吗,即是否递归监听对象的所有属性?等等
带着这些疑问我们一起来试试看
首先,通过reactive
创建一个响应数据
import { reactive } from "vue";
export default {
setup() {
const state = reactive({
count: 0,
});
},
};
如上代码就可以创建一个响应式数据state
,我具体来看一下这个
console.log(state)
可以看见,返回的响应副本state
其实就是Proxy
对象。所以reactive
实现响应式就是基于ES2015 Proxy
的实现的。那我们知道Proxy
有几个特点:
- 代理的对象是不等于原始数据对象
- 原始对象里头的数据和被
Proxy
包装的对象之间是有关联的。即当原始对象里头数据发生改变时,会影响代理对象;代理对象里头的数据发生变化对应的原始数据也会发生变化。
需要记住:是对象里头的数据变化,并不能将原始变量的重新赋值,那是大换血了
因此,既然reactive
实现响应式是基于Proxy
的实现的,那我们大胆猜测,原始数据与相应数据也是有关联的。那我们来测试一下
以上代码测试结果如下
验证,确实当响应式对象里头数据变化的时候原始对象的数据也会变化
如果反过来,结果也是一样
// ++state.count
++obj.count;
当响应式对象里头数据变化的时候原始对象的数据也会变化
那问题来了,我们操作数据的时候通过谁来操作呢?
官方的建议是
建议只使用响应式代理,避免依赖原始对象
再来解决另外一个问题看看reactive
是否会深度监听每一层呢?
const state = reactive({
a:{
b:{
c:{name:'c'}
}
}
});
console.log(state);
console.log(state.a);
console.log(state.a.b);
console.log(state.a.b.c);
可以看到结果reactive
是递归会将每一层包装成Proxy
对象的,深度监听每一层的property
最后测试一下如果reactive
传递是非对象而是原始值会怎么样
const state = reactive(0);
console.log(state)
结果是,原始值并不会被包装,所以也没有响应式特点
到这我们已经了解了reactive
,下面进行简单总结:
-
reactive
的参数可以传递对象也可以传递原始值。但是原始值并不会包装成响应式数据 - 返回的响应式数据的本质
Proxy
对象 - 返回的响应式"副本"与原始数据有关联,当原始对象里头的数据或者响应式对象里头的数据发生,会彼此相互影响。两种都可以触发界面更新,操作时建议只使用响应式代理对象
- 返回的响应式对象里头时深度递归监听每一层的,每一层都会被包装成
Proxy
对象
有了这些知识,我们下面开始模拟实现reactive
函数
模拟实现reactive
修改测试用例
首先测试用例得变
const { createApp } = Vue
const app = createApp({
setup() {
const state = reactive({
count: 0
})
setInterval(() => {
console.log(state.count)
state.count++
}, 2000);
return state
}
});
app.mount('#app');
如上代码,我希望实现一个reactive
函数,它接受一个对象,返回一个包装后的响应式对象,当响应式数据发生变化时,页面能及时跟新。
创建reactive函数
我们知道Vue3
是基于Proxy
实现响应式。作用是所以当数据发生变化时,我们可以拦截到并作出一些操作,比如更新UI视图。因此我们定义reactive
接受一个对象obj
,通过new Proxy
返回包装后的响应式数据
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
return target[key]
},
set(target, key, val) {
target[key] = val
// 这里当数据变化时,更新界面,于是我们考虑到这里需要update方法用户更新
// updata待实现...
}
})
}
上述代码中,我们需要封装一个update方法,当数据变化时调用,即用于更新和初始化,于是我们回到mount函数中实现封装
封装update
所以可以看到,update
函数做了三件事:
- 得到最新的元素
el
- 清空宿主元素
parent
的内容 - 追加
el
另外我们还需要在初始化时执行一次
this.update()
下面我们希望当render
函数的内部用到了响应式数据,并当数据发生变化时,再次执行update
函数
因此我们回到reactive
中,当执行set函数时,说明数据有变化,这是我们需要做更新,但是我们怎么调用update呢?使用app.update()吗?
虽然使用app.update()可以实现,但是耦合了app,失去了复用性。所以我们得想其他办法来解耦合
解耦合
其实我们希望当一个数据发生变化,一定要知道更新的是哪个对应的函数。因此我们需要一个依赖收集的过程,也叫添加副作用,于是我们可以创建一个effect函数,该函数接受一个函数fn作为参数,如果fn使用到了一些响应式数据,当数据发生变化,这个副作用函数fn将再次执行,同时返回副作用函数,如下
const effectStack = [];
function effect(fn) {
const eff = function () {
try {
effectStack.push(eff)
fn()
} finally {
effectStack.pop();
}
}
eff();// 执行一次,触发依赖收集
return eff
}
effectStack用于临时存储fn,将来在做依赖收集的时候把它拿出来,拿出来跟它相关的数据相映射
接着我们需要写一个依赖收集的函数track,track的作用是接受target、key,让traget[key]和副作用函数eff建立一个映射关系。兵器我们需要建立一个数据结构,来存储这个映射关系,于是实现如下:
function track(target, key) {
// 获取副作用函数
const effect = effectStack[effectStack.length - 1]
// 建立target和key和eff关系
if (effect) {
console.log(targetMap)
let map = targetMap[target]
if (!map) {
map = targetMap[target] = {}
}
let deps = map[key]
if (!deps) {
deps = map[key] = []
}
// 将副作用函数放入deps
if (deps.indexOf(effect) === -1) {
deps.push(effect)
}
}
}
然后,我们再reactive的get函数中,做依赖收集
track(target,key)
已经上面步骤,我们已将traget、key、和副作用函数建立一个映射关系,于是我们可以在用户改变值的时候去触发依赖。因此下面我们封装一个trigger方法来触发依赖
function trigger(target, key) {
const map = targetMap[target]
if (map) {
const deps = map[key]
if (deps) {
deps.forEach(dep => dep());
}
}
}
接着,我们reactive的set中调用trigger,触发依赖
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
// 可以做依赖收集
track(target, key)
return target[key]
},
set(target, key, val) {
target[key] = val
// 触发依赖
trigger(target, key)
}
})
}
最后要将update函数作为副作用函数,修改如下:
this.update = effect(() => {
const el = ops.render.call(this.proxy)
parent.innerHTML = ''
insert(el, parent)
})
最终,我们模拟实现了数据响应式,如下