前言
本系列文章的目标是模拟实现所有Vue3
响应式相关API
为了不混乱,我先将响应式相关API
进行分类,如图所示
由于文章篇幅较长,为了避免大家疲劳,先作出两点改善:
- 分篇;将文章按照上述分类和内容量分为上、中、下或更多文章
- 插入图片;我将尽量多插入一些相关图片,一来缓解疲劳,二来帮助大家理解
此篇目标是深入了解9
个响应式基础API
中的reactive
,并模拟实现我们自己的数据响应式系统
思路
我的思路其实非常简单,首先去了解API
的基本使用,然后试着去使用和理解它,然后按照它所实现的功能模拟实现我们自己的功能,如下
工作准备
在开始前,我们需要做一点准备工作
-
需要创建一个vue3项目,方便使用对应的响应式
API
如果你不知道怎么创建,官网提供了多种创建方式:传送门 -
单独创建一个文件,用于模拟实现对应
API
为了方便,我将上篇文章(从0开始手动实现Vue3初始化流程)所用的文件拿来继续使用,你也可以使用这个文件,简此文最下方附件 1
这个文件实现了Vue3
的初始化流程相关的几个API
,比如createApp
和mount
方法,我们在这个文件的基础上进行模拟实现数据响应式API
有了以上的准备,下面开始深入理解reactive
reactive函数
我们分两部分来说:reactive
的使用和模拟实现
reactive的使用
graph TD
从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
的实现的,那我们大胆猜测,原始数据与相应数据也是有关联的。
原始数据与响应式数据是否有关联
那我们来测试一下
<template>
<button @click="change">
{{ state.count }}
</button>
</template>
<script>
import { reactive } from "vue";
export default {
setup() {
const obj = {
count: 0,
};
const state = reactive(obj);
function change(){
++state.count
console.log(obj);
console.log(state);
}
return { state,change};
},
};
</script>
以上代码测试结果如下
验证,确实当响应式对象里头数据变化的时候原始对象的数据也会变化 如果反过来,结果也是一样
// ++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
,将来在做依赖收集的时候把它拿出来,拿出来跟它相关的数据相映射
graph TD
effectStack --> 临时存储fn
effectStack --> 收集依赖时拿出来
接着我们需要写一个依赖收集的函数track
,track
的作用是接受target
、key
,让traget
key
和副作用函数eff
建立一个映射关系。兵器我们需要建立一个数据结构,来存储这个映射关系,于是实现如下:
function track(target, key) {
// 获取副作用函数
const effect = effectStack[effectStack.length - 1]
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)
}
}
}
记住,track的目的就是建立target和key和副作用eff之间关系
graph TD
target --> key --> eff
接着,我们再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)
})
最终,我们成功实现了reactive
,完成了数据响应式
测试代码运行成功,如下
最终代码见文章底部 附件2
总结
reactive
的作用其实就是将接收到的对象,通过Proxy
打包成响应式对象,当响应式对象的数据发生变化时,页面视图可以对应进行更新。
整个的实现过程从创建reactive
开始,里头通过Proxy
拦截到对象的相关操作,当代理对象数据发生变化时,我们可以同时在set内部通知更新,于是这里封装了update
方法,但是为了解决耦合问题,我们分别实现了添加副作用函数effect
、依赖收集的函数track
以及触发依赖的trigger
方法等
END
附件 1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mini-vue3</title>
</head>
<body>
<div id="app"></div>
<script>
const Vue = {
createApp(ops) {
const renderer = Vue.createRenderer({
querySelector(selector) {
return document.querySelector(selector)
},
insert(child, parent, anchor) {
parent.insertBefore(child, anchor || null)
}
})
return renderer.createApp(ops)
},
createRenderer({ querySelector, insert }) {
return {
createApp(ops) {
return {
mount(selector) {
const parent = querySelector(selector)
if (!ops.render) {
ops.render = this.compile(parent.innerHTML)
}
if (ops.setup) {
this.setupState = ops.setup()
} else {
this.data = ops.data();
}
this.proxy = new Proxy(this, {
get(target, key) {
if (key in target.setupState) {
return target.setupState[key]
} else {
return target.data[key]
}
},
set(target, key, val) {
if (key in target.setupState) {
target.setupState[k] = val
} else {
target.data[key] = val
}
}
})
const el = ops.render.call(this.proxy)
parent.innerHTML = ''
insert(el, parent)
},
compile(template) {
return function render() {
const h1 = document.createElement('h1')
h1.textContent = this.count
return h1;
}
}
}
}
}
}
}
</script>
<script>
// 测试用例
const { createApp } = Vue
const app = createApp({
setup() {
let count = 1
return { count }
}
});
app.mount('#app');
</script>
</body>
</html>
附件 2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mini-vue3</title>
</head>
<body>
<div id="app"></div>
<script>
// `reactive`接受一个对象`obj`,返回包装后的响应式数据
function reactive(obj) {
// Vue3中基于Proxy实现响应式。作用是所以当数据发生变化时,我们可以拦截到并作出一些操作,比如更新UI视图,即数据响应式
return new Proxy(obj, {
get(target, key) {
// 可以做依赖收集
track(target, key)
return target[key]
},
set(target, key, val) {
target[key] = val
// 这里当数据变化时,更新界面,于是我们可以创建一个update方法,并在这里调用
// updata()
// app.update()
//这有个问题,app耦合了,没有通用性
// 为了解决这个问题
// 我们希望有一条神秘的线,当一个数据发生变化,我一定要知道更新的是哪个对应的函数。
// 因此,我们需要一个依赖收集的过程,或者叫添加副作用,即数据发生改变,产生一个副作用
// 触发依赖
trigger(target, key)
}
})
}
//effectStack用于临时存储fn,将来在做依赖收集的时候把它拿出来,拿出来跟它相关的数据相映射
const effectStack = [];
// 添加副作用函数fn
function effect(fn) {
// effect的作用是将传入的fn作为副作用函数,如果fn使用到了一些响应式数据,当数据发生变化,这个副作用函数fn将再次执行
const eff = function () {
try {
effectStack.push(eff)
fn()
} finally {
effectStack.pop();
}
}//eff的作用是处理错误,入栈,执行函数,出栈
// 执行一次,触发依赖收集
eff();
return eff
}
// 依赖收集函数,希望在副作用函数执行时,去触发track
// track的作用是接受target、key,让traget[key]和副作用函数eff建立一个映射关系
// 所以,我建立一个数据结构,来存储这个映射关系
const targetMap = {}//大概结构是这样的{target: {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)
}
}
}
function trigger(target, key) {
const map = targetMap[target]
if (map) {
const deps = map[key]
if (deps) {
deps.forEach(dep => dep());
}
}
}
const Vue = {
createApp(ops) {
const renderer = Vue.createRenderer({
querySelector(selector) {
return document.querySelector(selector)
},
insert(child, parent, anchor) {
parent.insertBefore(child, anchor || null)
}
})
return renderer.createApp(ops)
},
createRenderer({ querySelector, insert }) {
return {
createApp(ops) {
return {
mount(selector) {
const parent = querySelector(selector)
if (!ops.render) {
ops.render = this.compile(parent.innerHTML)
}
if (ops.setup) {
// 经过上面修改,this.setupState已经是响应式对象
this.setupState = ops.setup()
} else {
this.data = ops.data();
}
this.proxy = new Proxy(this, {
get(target, key) {
if (key in target.setupState) {
return target.setupState[key]
} else {
return target.data[key]
}
},
set(target, key, val) {
if (key in target.setupState) {
target.setupState[k] = val
} else {
target.data[key] = val
}
}
})
// 封装一个update方法,当数据变化时调用,即用于更新和初始化
this.update = effect(() => {
// 得到最新的元素、清空、追加
const el = ops.render.call(this.proxy)
parent.innerHTML = ''
insert(el, parent)
})
// 在初始化是需要先执行一次
this.update()
},
compile(template) {
return function render() {
const h1 = document.createElement('h1')
h1.textContent = this.count
return h1;
}
}
}
}
}
}
}
</script>
<script>
// 测试用例
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');
</script>
</body>
</html>