首发于 前端学习
深入理解 Vue 3 响应性原理——Reactivity

深入理解 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会自动更新。


jsbin.com/jaqidicene/ed

简单的 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 = () =>{