Vue 之所以能够实现声明式的 UI,是因为 Vue 通过响应式将数据和 UI 进行了绑定,当数据发生变化时 Vue 会自动调用相应的函数来重新渲染受到影响的 UI。本系列文章就来简单分析下 Vue 响应式系统的实现。本文先从响应式的基础原理开始,实现一个简单的响应式系统。
系统设计
设计目标
我们希望实现这样的系统:能够将某些变量包装成响应式的变量,当该变量的值发生变化时,依赖于该变量的函数能够重新执行以达到刷新状态的目的,例如重新渲染 UI。
具体地,我们需要实现以下函数:
reactive()
: 包装响应式变量effect()
: 包装能够响应数据变化的副作用函数,使其能够在所涉及的响应式变量发生变化时重新执行
例如,对于以下代码,首先使用 reactive()
方法将
obj
包装成响应式对象,然后使用 effect()
方法包装渲染页面文字内容的函数 fn
,使得当
data.text
发生变化时,能够自动调用 fn
来更新页面的文字内容。
1 |
|
技术原理
对于以上的设计目标,我们可以通过 Proxy
对象来实现。整体的思路是这样:
- 当调用
reactive(obj)
时,返回一个对原始数据obj
的代理data
。并拦截它的get
和set
操作。 - 在拦截
get
时,记录读取该属性的函数,例如fn
中读取了data.text
,则将fn
函数收集起来 - 在拦截
set
操作时,将上一步收集起来的函数重新执行 effect
方法则用来处理对副作用函数fn
的标记管理
简而言之,对于响应式数据,当读取某个属性时收集依赖于该属性的副作用函数,当修改该属性的值时把之前收集的副作用函数重新执行。
编码实现
reactive
首先实现 reactive
方法:
1 |
|
下面实现追踪和触发方法 tarck
和
trigger
。注意这两个方法里使用到了全局变量
activeEffect
和 bucket
,其中
activeEffect
是用来标记当前由哪个副作用函数读取了响应式变量,它在effect
方法里被设置bucket
是一个哈希结构,用来关联响应式变量和与之相关的副作用函数
1 |
|
effect
如上文所述 effect()
方法主要用来设置
activeEffect
:
1 |
|
注意到这里 bucket
使用了 WeakMap
数据结构主要是从性能出发考虑的,参考WeakMap。
测试和小结
使用设计目标一节中代码进行测试可以发现,2 秒后页面的文本会自动更新。
See the Pen v1_basic by Hozen (@Hozen) on CodePen.
由于 Proxy 的拦截是同步进行的,所以代码的执行过程很容易最终和分析,具体如下:
- 首先包装响应式对象,对数据的读取和设置进行拦截。
- 在
effect
函数包装的副作用函数中,由于读取了data.text
,并设置了activeEffect
。被代理对象data
的get
所拦截。将当前副作用函数收集到了集合bucket[data].text
中。 - 当 2 秒后执行
data.text = 'Hello World'
时,被set
拦截触发trigger
,将bucket[data].text
集合中所有的副作用函数依次执行。页面的文本被更新。
参考资料
[1] vuejs/core: 🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.[EB/OL]. [2023-09-22]. https://github.com/vuejs/core.
[2] Vue.js - 渐进式 JavaScript 框架 | Vue.js[EB/OL]. [2023-09-22]. https://cn.vuejs.org/.
[3] 霍春阳. Vue.js设计与实现[M/OL]. 人民邮电出版社, 2022[2023-09-19]. https://book.douban.com/subject/35768338/.
[4] JavaScript | MDN[EB/OL]. 2023-04-10. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript.