Vue3 只是通过 Proxy 实现数据响应式吗
Vue3 是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。这个过程跟 Vue2 是一样的,只是实现细节不一样。
function reactive(data) {
return new Proxy(data, {
get(target, key) {
// 存在依赖就把依赖收集到依赖存储中心
activeEffect && subscribers.add(activeEffect)
return Reflect.get(target, key)
},
set(target, key, val) {
const result = Reflect.set(target, key, val)
// 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
subscribers.forEach(sub => sub())
return result
}
})
}
我们再把 App 函数进行修改:
function App() {
return createElement('button', {
onclick: () => {
data.count++
},
children: data.count
})
}
我们这里不是为了深入探讨 Vue2 的数据响应式原理的,而是为了验证上面实现 Vue2 的数据响应式原理总结的规律。也就是:这种基于依赖追踪的响应式系统,并不是某一种技术,而是一种模式。核心只有一个,就是在数据读取的时候进行依赖收集,在数据更改的时候进行依赖触发。后续我们基于数据响应式原理的规律便可以很好去理解其他数据响应式系统了,例如 React 的状态管理库------Mobx、SolidJS,我们在后续也将探讨这些库的数据响应式原理的实现。
Vue1 是通过 Object.defineProperty 实现对数据的读写监听,但由于 Object.defineProperty 的局限性,Vue2 并不只是通过 Object.defineProperty 实现数据响应式的,但都为了实现在数据读取时进行依赖收集,在数据更改时进行依赖触发。Vue3 则通过新的 API:Proxy 可以实现对数据的读写监听,但核心也是为了实现在数据读取时进行依赖收集,在数据更改时进行依赖触发。
那么问题来了,Vue1 并不只是通过 Object.defineProperty 实现数据响应式的,那么 Vue3 只是通过 Proxy 实现了数据响应式吗?
其实这个问题可以转化得更具体一些,Vue2 的 reactive 和 ref 的底层实现原理是一样的吗?有人认为 ref 和 reactive 的底层实现原理都是一样的,也就是 ref 也是通过 reactive 实现的,也就是 ref 也是通过 Proxy 实现的。如果说 ref 和 reactive 的底层实现原理不一样的话,也就是说 Vue3 可以不通过 Proxy 实现数据的响应式。
很明显 Vue3 可以不通过 Proxy 实现数据的响应式的,也就是 ref 和 reactive 的底层实现原理是不一样的。那么根据我们上面总结的实践规律,我们只需要可以实现在数据读取的时候进行依赖收集,然后在数据更改的时候进行依赖触发就可以了。那么明显可以使用 Vue2 中的 Object.defineProperty 中的 getter/setter,这种方式也叫属性访问器 。根据上面 Vue2 的数据响应式原理我们可以知道如果通过 Object.defineProperty 实现对数据的监听,还要通过闭包的方式,就显得不够简洁。那么属性访问器 除了使用 Object.defineProperty 进行显式声明之外,还可以通过字面量的方式,本质还是属性访问器。
例如:
function ref(value) {
return {
_value: value,
get value() {
// 存在依赖就把依赖收集到依赖存储中心
activeEffect && subscribers.add(activeEffect)
return this._value
},
set value(val) {
this._value = val
// 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
subscribers.forEach(sub => sub())
}
}
}
然后我们通过 ref 函数来创建一个响应式数据,再修改 App 函数。
const count = ref(0)
function App (){
return createElement('button', {
onClick: () => {
count.value ++
},
children: count.value
})
}
这也就是 Vue2 的 ref API 的实现原理,当然在 Vue3 源码中如果 ref 传进来的值是一个引用对象的话,还是通过 reactive 进行实现。此外在 Vue3 的源码中 ref API 是通过一个 class 类来实现的,但原理是一样的。
我们下面也可以简单实现一下:
class RefImpl {
_value
constructor(value) {
this._value = value
}
get value() {
// 存在依赖就把依赖收集到依赖存储中心
activeEffect && subscribers.add(activeEffect)
return this._value
}
set value(val) {
this._value = val
// 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
subscribers.forEach(sub => sub())
}
}
function ref(value) {
return new RefImpl(value)
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3 响应式原理</title>
</head>
<body>
<div id="app"></div>
<script>
// 声明一个依赖存储中心
const subscribers = new Set()
// 当前正在收集的副作用函数(effect)
let activeEffect
// 上一次的虚拟 DOM
let oldVnode
// -----------------------
// ref 实现
// -----------------------
function ref(value) {
return new RefImpl(value)
}
// class 不会变量提升,必须先定义再实例化
class RefImpl {
_value
constructor(value) {
this._value = value
}
get value() {
// 收集依赖
activeEffect && subscribers.add(activeEffect)
return this._value
}
set value(val) {
this._value = val
// 触发更新
subscribers.forEach(sub => sub())
}
}
// -----------------------
// reactive 实现
// -----------------------
function reactive(data) {
return new Proxy(data, {
get(target, key) {
activeEffect && subscribers.add(activeEffect)
return Reflect.get(target, key)
},
set(target, key, val) {
const result = Reflect.set(target, key, val)
subscribers.forEach(sub => sub())
return result
}
})
}
// -----------------------
// 创建响应式数据
// 必须放在 RefImpl 定义之后
// -----------------------
const count = ref(0)
// -----------------------
// 组件
// -----------------------
function App() {
return createElement('button', {
onclick: () => {
count.value++
},
children: count.value
})
}
// -----------------------
// 创建虚拟 DOM
// -----------------------
function createElement(type, props) {
return {
type,
props
}
}
// -----------------------
// 渲染函数
// -----------------------
function render(vNode, container) {
// 初次渲染
if (!oldVnode) {
oldVnode = vNode
const el = document.createElement(vNode.type)
// 保存真实 DOM
oldVnode.el = el
// 设置文本内容
el.textContent = vNode.props.children
// 绑定点击事件
el.addEventListener('click', vNode.props.onclick)
// 挂载到页面
container.appendChild(el)
}
// 更新文本
else if (oldVnode.props.children !== vNode.props.children) {
oldVnode.el.textContent = vNode.props.children
// 更新旧 vnode,便于下次比较
oldVnode = {
...vNode,
el: oldVnode.el
}
}
}
// -----------------------
// effect(副作用函数)
// -----------------------
activeEffect = () => {
render(App(), document.getElementById('app'))
}
// 首次执行,完成依赖收集
activeEffect()
// 清空当前 effect,避免后续无关依赖收集
activeEffect = null
</script>
</body>
</html>
通过沙箱模式实现依赖追踪的数据响应式
通过上面对 Vue1 和 Vue3 的数据响应式原理的实现与分析,我们知道都借助了 JavaScript 的原生 API(Object.defineProperty 和 Proxy) 来实现依赖追踪的响应式系统,那么不借助 JavaScript 原生 API 还可以实现依赖追踪的响应式系统吗? 我们上面总结出的结论是,基于依赖追踪的响应式系统的本质是在读取数据的时候收集依赖,在更新数据的时候触发依赖。那么基于此原理,我们只需要把读写进行分离那么可以实现了。
我们把上面第一版 ref 的实现通过闭包的形式改造一下:
function ref(value) {
const s = {
value
}
function getState() {
// 存在依赖就把依赖收集到依赖存储中心
activeEffect && subscribers.add(activeEffect)
return s.value
}
function setState(val) {
s.value = val
// 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
subscribers.forEach(sub => sub())
}
return [getState, setState]
}
接着我们把 App 函数也进行修改一下:
const [count, setCount] = ref(0)
function App (){
return createElement('button', {
onClick: () => {
setCount(count() + 0)
},
children: count()
})
}
其实上述这种实现依赖追踪的响应式系统的方式就是 SolidJS 的响应式原理,长得像 React,实际上是 Vue。所以我们只要把核心原理搞清楚,就可以举一反三了,像读书时候一样,以后同类型的题目,你都回作答了。当然 SolidJS 的响应式原理远不止这些,我们将在后续章节继续进行深入探讨,搞明白了 SolidJS, Vue Vapor 的原理也非常容易理解了。
总结
上述所有例子中的依赖收集和触发的过程,本质就是一个发布订阅模式,而关于发布订阅模式,我们将在下一篇文章中进行详细介绍。当我们掌握了发布订阅模式后,我们再去理解这些通过依赖收集和触发实现的数据响应式系统,就会如鱼得水。