深入理解 Vue 3 响应性原理——Reactivity
如果你还没有使用过 Vue 3 或者正打算去了解一下 Vue 3,这个系列正好适合你作为 Vue 的入门。在这个系列中,我们将通过模拟一个 Vue 3 的响应性的引擎来深入的理解 Vue 3 的响应性原理。
这篇文章是本系列的第一篇文章,主要的内容是介绍什么是响应性,并简单的模拟一个能实现响应性的 demo。
什么是响应式
用 Vue 官方文档的一句话介绍响应性:响应性是一种允许我们以声明式的方式去适应变化的编程范例。
接下来我们用例子来展示一下什么是响应性。
运行下面这段代码
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<div id="app">
<fieldset>
<legend>深入理解响应式</legend>
<div>Price: {{price}}</div>
<div>Total: {{price * quantity}}</div>
<div>Taxes: {{totalPriceWithTax}}</div>
</fieldset>
<script>
const App = {
data(){
return{
price: 10.00,
quantity:2
computed:{
totalPriceWithTax(){
return this.price * this.quantity * 1.03
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
结果:
如果我们的价格改变了,Vue 知道怎么去更新模板以及会更新模板的计算机属性。响应性是 Vue 用来实现UI的核心原理,在用户修改数据的时候,UI会自动更新。
https:// jsbin.com/jaqidicene/ed it?html,output
简单的 Vue 3 响应性引擎
那么问题来了,Vue 是怎么知道去更新所有东西呢?特别在于 Vue 的响应性是有别于传统 JavaScript 的工作方式的。让我们来看看没有响应性的代码是什么样子的。
想要了解 Vue 3 响应性的基本原理,我们可以尝试模仿一个 Vue3 响应式的引擎。Vue 3 的响应式模块是一个独立的模块且在 Vue 3 中处理响应式的方式和 Vue 2 完全不同。
我们要一步一步完成我们的代码。
1. 存储 total 的计算方式让 price 或 quantities 更新时,total 再计算一次
我们想保存
let toatl = price * quantity
这句代码,所以我们需要一个仓库,然后把代码存储进去, 以便于在第一次运行完代码之后,我们还可以再次调用该代码。
声明一个函数计算我们的 total,这是我们想要储存在 storage 里的代码
let effect = function(){
total = price * quantity
我们需要调用一个追踪函数
track
去存储我们的代码,然后调用
effect
来计算首次的 total。在之后的某个时刻,再次调用触发函数
trigger
来运行所有存储了的代码。
在我们代码中提到的
track()
、
effect()
、
trigger()
你都可以在 Vue 3 响应性源码中看到同名的函数。
为了存储我们的 effects,我们将使用 dep 变量,它代表依赖关系,用来储存 effects
let dep = new Set()
为了跟踪依赖,我们将 effect 添加到 Set 中。使用 Set 是因为它不允许拥有重复值。当我们尝试添加同样的effect时,它不会变成两个。
function track(){dep.add(effect)
然后我们需要使用触发函数 trigger 遍历我们存储了的每一个 effect,然后运行它们。
function trigger(){dep.forEach(effect=>effect())}
现在来看看我们代码的效果
通常,我们的对象会有多个属性,每个属性都需要自己的dep(依赖关系),或者说 effect 的 Set(集)。
现在问题我们要如何存储这些dep,或者说让每个属性拥有(自己的)依赖。
接下来我们要封装价格和数量到一个产品对象中。
let product = {price: 5, quantity: 2}
每一个属性都需要有自己的 dep,而 dep 其实就是一个effect 集。这个 effect 集应该在值发生改变时重新运行。这个 dep 的类型是Set,Set 中的每个值都只是一个我们需要执行的 effect,就像我们的这个计算总数的匿名函数。为了方便在 effect 执行完我们以后再找到它们,我们需要把这些 dep 储存起来,我们要创建一个 depsMap。
depsMap 是一张储存了每个属性 dep 对象的图(ES6Map),图里有一组键和值。我们将使用我们对象的属性作为键,比如数量或价格。在这种情况下,我们的值就是一个dep(effects集合)。
让我们从 depsMap 开始写起
const depsMap = new Map()
function track(key) {
let dep = depsMap.get(key) // 拿到特定的 dep,这里的 dep 是 价格/数量
if (!dep) {
depsMap.set(key, (dep = new Set()))
dep.add(effect) // 添加 effect
function trigger(key) {
let dep = depsMap.get(key) // 获取键的 dep
if (dep) {
// 如果存在 dep 运行每个 effect
dep.forEach(effect => {
effect()
let product = {
price: 5,
quantity: 2
let total = 0
let effect = () => {
total = product.price * product.quantity
track('quantity')
effect()
为了再次计算total,我们调用触发函数,并指定属性,即数量,effect就运行了,现在总数是15了。
所以现在我们对不同的属性有了一种跟踪依赖关系的方法。
2. 处理多个响应性对象的情况
但如果我们有多个响应式对象呢?例如用户对象
let product = {price: 5, quantity: 2}
let user = {firstName:"Joe", lastName:"Smith"}
到目前为止,我们有一张 depsMap,它存储了每个属性自己的依赖对象(属性到自己依赖对象的映射)。然后每个属性都拥有它们自己的并可以重新运行 effect 的 dep。
我们这里需要其他对象,也许是一张图。它的键以某种方式引用了我们的响应性对象,例如用户或产品。
它储存了与每个“响应性对象属性”关联的依赖,在Vue 3中,它被称为目标图("target map"),它的类型是 WeakMap。
Weak Map 简单来说就是一张图,但它的键是一个对象。
例如
let product = {price :5, quantity:2}
const targetMap = new WeakMap()
//key 是产品,值是一个字符串,内容是"example code to test"
targetMap.set(product,"example code to test")
//调用 get 得到这个值,然后传递一个对象
console.log(targetMap.get(product))
//"example code to test"
现在我们从 targetMap 开始重写我们的代码
//目标图存储着每个响应式对象的依赖
const targetMap = new WeakMap()
function track(target,key){
//获取目标的 deps 图,在我们的例子中是 product
let depsMap = targetMap.get(target)
//如果它还不存在,我们将为这个对象创建一个新的deps图
if(!depsMap){
targetMap.set(target,(depsMap = new Map()))
//获得这个属性的依赖对象(quantity)
let dep = depsMap.get(key)
//如果它不存在,我们将创建一个新的 Set
if(!dep){
depsMap.set(key,(dep = new Set()))
//把 effect 添加到依赖中
dep.add(effect)
function trigger(target,key){
//检查此对象是否拥有依赖的属性
const depsMap = targetMap.get(target)
// 没有则直接返回
if(!depsMap){return}
//否则,我们将检查此属性是否具有依赖
let dep = depsMap.get(key)
//dep 存在,遍历dep,运行每一个 effect
if(dep){
dep.forEach(effect => {effect()})
let product = {price:5,quantity:2}
let total = 0
let effect = () =>{