前言
哈喽大家好!我是 嘟老板。Vue3 用了这么久,响应式系统玩明白了吗,今天我们从一个简单的 html 页面切入,逐步实现一个简易版的响应式系统,感受下 Vue3 响应式的魅力。
抛砖引玉
实现一个”简陋“的响应式数据渲染。
页面框架
目标:实现一个 html 页面,展示一个数字 count,点击数字时, count +1。
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Reactive</title>
</head>
<body>
<script>
// 定义数据
let count = 0
// dom
const body = document.body
// 渲染函数
function render() {
body.innerHTML = `<h1>${count}<h1>`
}
body.addEventListener('click', () => {
count++
render()
})
render()
</script>
</body>
</html>
浏览器打开 index.html,运行效果如下:
OK,效果还行...
定义并渲染响应式数据
现在我想在 count 发生变化时自动更新页面,而不是每次都调用 render 函数。就像这样:
body.addEventListener('click', () => {
count++
// 不再调用 render,而是 count 发生变化时自动更新页面
// render()
})
这要怎么处理呢?稍微暂停思考一下。
OK,揭晓答案...
首先我们需要让程序知道 count
发生了变化,然后去更新页面。那怎么才能让程序知道 count
发生变化了呢?答案是代理,也就是我们常用的 Proxy。
这也正是 Vue3 的响应式数据定义的核心(Vue2 使用 Object.defineProperty)。
<script>
部分调整如下:
<script>
// 定义数据
const obj = {
count: 0
}
// 代理 handler
const proxyHandler = {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
Reflect.set(target, key, value)
render()
}
}
// 代理数据对象,
const proxyObj = new Proxy(obj, proxyHandler)
// dom
const body = document.body
// 渲染函数
function render() {
body.innerHTML = `<h1>${proxyObj.count}<h1>`
}
body.addEventListener('click', () => {
proxyObj.count++
})
render()
</script>
主要调整点如下:
- 将原来的
count
变量改为obj
对象,并通过new Proxy
创建代理对象proxyObj
。 - 在
new Proxy()
的 handler 中,定义getter/setter
,其中setter
变更数据后,调用render
函数重新渲染。 - 原本所有使用
count
的位置,全部改为使用proxyObj.count
。
现在我们运行看下效果,刷新浏览器 index.html 页面:
OK,效果与原本的一致
小结一下
我们借助 Proxy
实现了一个简单的响应式数据渲染,通过定义代理对象的 getter/setter
,实现在数据更改时,重新渲染页面。
然而,以上只能叫做定义响应式数据,而不能称作响应式系统,我们只是通过 Proxy
定义了一个响应式对象,后续若需要更多的响应式对象,每次都需要手动定义,这显然是不合理的。而且还有一个最明显的缺点,数据(proxyObj.count) 和 渲染的逻辑(render) 耦合在一起,没有抽离出来,这也是响应式系统需要解决的核心问题。
那响应式系统到底是怎样的呢,请继续往下看。
响应系统
Vue3 响应式系统源码在 packages/reactivity 下。
整体介绍
响应式系统通过发布订阅模式实现数据的依赖追踪和更新。其中 Dep
负责依赖追踪,Watcher
负责数据侦听。
整个响应式系统核心构成主要有以下几个部分:
Dep
:依赖收集、追踪。Watcher
:数据侦听。effect
:副作用。reactive
(或ref
,方便起见,本文仅实现reactive
):定义响应式对象。
我们新建一个 index.js 文件,专门编写响应式系统相关代码。
Dep
Dep
的作用是收集依赖,追踪依赖,及通知更新。
我们新建一个 Dep
类,并定义如下属性和方法:
subs
:存储依赖。addSub
: 添加依赖notify
:通知依赖更新
代码如下:
// Dep: 依赖收集
class Dep {
constructor() {
this.subs = new Set()
}
// 添加依赖
addSub(sub) {
if (activeEffect) {
this.subs.add(activeEffect)
}
}
// 通知依赖更新
notify() {
this.subs.forEach(effect => {
effect.run()
});
}
}
可以发现代码十分精简,便于理解,此处仅实现最基础的部分。
注
最新的 Vue3
Dep
以双向链表结构实现,这里方便理解,使用Set
结构。
Watcher
在 Dep
的 addSub
函数中,用到了一个变量 activeEffect
,这个变量是干什么的呢?
在 Watcher
中我们就能看到它的身影了。
Watcher
的核心作用是数据侦听,侦听了以后怎么办呢?执行相关的副作用。
什么是副作用?举个例子,我发烧了,吃了退烧药,有点犯困,这里”犯困“就是副作用,因为我吃药的目的是退烧,而不是为了”犯困“。对应到程序中,我对数据做修改,会触发一些其他的关联操作,比如更新页面,这些关联操作就属于副作用。
let activeEffect = null
class Watcher {
constructor(effect) {
// 当前副作用
this.effect = effect
}
run() {
// 激活的 effect 指向当前实例
activeEffect = this
// 执行副作用
this.effect()
// 执行完后置空
activeEffect = null
}
}
我们看到,Watcher
中定义了一个 run
方法,用来执行副作用。副作用执行前,将 activeEffect
指向了当前实例,可见 activeEffect
就是当前正在激活的副作用对象,当副作用函数执行完毕,需要将 activeEffect
置空。
effect
effect
函数主要用来创建、执行副作用,接受一个函数参数,可通过如下方式使用:
effect(() => {
console.log(obj.count)
})
它的参数就是一个副作用函数,假设 obj 是一个响应式对象,当 obj.count 发生变化时,就会执行该副作用函数,打印 obj.count。
简单点的 effect
函数就是创建一个 Watcher
实例,并执行 run
方法。
export function effect(fn) {
const _effect = new Watcher(fn)
_effect.run()
}
reactive
reactive
函数用来创建响应式数据,核心就是借助 Proxy
创建并返回一个响应式对象。
核心需要做以下两件事:
- 定义
getter
:追踪依赖(track) - 定义
setter
:触发更新(trigger)
在 getter
中,通过 dep.addSub
函数,向 dep
中添加依赖,实现依赖收集;
在 setter
中,通过 dep.notify
函数,触发副作用更新。
export function reactive(target) {
const mutableCollectionHandlers = {
get(target, key) {
// 追踪依赖 track deps
const dep = getDepFromReactive(target, key)
dep.addSub()
return Reflect.get(target, key)
},
set(target, key, value) {
// 先更新值,在触发更新
const result = Reflect.set(target, key, value)
// 触发更新 trigger deps
const dep = getDepFromReactive(target, key)
dep.notify()
return result
}
}
return new Proxy(target, mutableCollectionHandlers)
}
这里需要定义一个工具函数 - getDepFromReactive
,用来获取目标对象某个键值(target.key)对应的依赖集 dep
。
// 存储所有的 target 对应的 key-dep
const targetMap = new WeakMap()
// 获取 target key 对应的 dep
function getDepFromReactive(target, key) {
let depMap = targetMap.get(target)
if (!depMap) {
depMap = new Map()
targetMap.set(target, depMap)
}
let dep = depMap.get(key)
if (!dep) {
dep = new Dep()
depMap.set(key, dep)
}
return dep
}
突然一问:为什么 targetMap 使用
WeakMap
结构?
WeakMap
的键可以是对象类型。WeakMap
是弱引用,方便进行垃圾回收。
OK,一个简易版的响应式系统,到这就结束了,Vue
源码实现远远要比这复杂得多,还包括 computed
实现等等,本着纯粹好理解的原则,我们就不涉及太多的东西。
应用
现在我们来应用下上面的响应式系统。
清空 index.html 中原本的 <script>
部分,在 <body>
页签下添加如下代码:
<script type="module">
import { effect, reactive } from './index.js'
const obj = reactive({
count: 0
})
// dom
const body = document.body
// 渲染函数
function render() {
body.innerHTML = `<h1>${obj.count}<h1>`
}
effect(render)
body.addEventListener('click', () => {
obj.count++
})
</script>
主要借助响应式api - reactive
和 effect
,创建响应式数据并监听,当 obj.count 发生改变时,执行 render 函数,重新渲染页面。
我们启一个本地服务进行测试。这里借助工具 - http-server,快速启动一个本地服务。
终端执行以下命令:
npx http-server -c-1
若原本没有全局安装,会提示你是否安装,输入 y
即可。
-c-1
是命令行配置,表示禁用缓存,更多详情,请前往 http-server 查看。
终端展示如下,启动成功:
浏览器输入网址:http://127.0.0.1:8080/
访问:
OK,效果与预期一致。
结语
本文从一个简单的 html 页面入手,逐步实现了一个简易版的响应式系统,重点介绍了响应式系统的核心构件及完整实现过程,不能说与 Vue3
实现完全相同,但是核心思想大同小异。希望对您有所帮助!
如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。
技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。
往期干货
- 🚀前端懂算法,一个顶俩!超细解读常用的排序算法,Passion!!! (持续更新版~~~)
- 🚀面试离不开的首屏性能优化是什么,到底该怎么做
- 🔜想开发 vscode 插件却不知从何入手?超速入门,助力你弹射起步
- 💯What?维护新老项目频繁切换node版本太恼火?开发一个vscode插件自动切换版本,从此告别烦恼
- (⊙ˍ⊙)哦? ElementPlus 官网导航栏有点意思,来看看咋实现的
- 🧨🧨🧨你想要的 RBAC 权限管理实现全流程来啦!~~ 代码含量过多,请谨慎阅读 ~~
- 💡💡💡Vue3 用了这么久还没体验过 JSX/TSX?来封装个业务弹窗玩玩
- 👏👏👏厉害了 Vue Vine !Vue 组件还能这样写!!!
- 听说过 BEM 吗?聊聊如何落地 BEM 规范
- 一文带你了解多数企业系统都在用的 RBAC 权限管理策略