深入响应性原理

Posted 前端精髓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入响应性原理相关的知识,希望对你有一定的参考价值。

现在是时候深入了!Vue 最独特的特性之一,是其非侵入性的响应性系统。数据模型是被代理的 javascript 对象。而当你修改它们时,视图会进行更新。这让状态管理非常简单直观,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下 Vue 响应性系统的底层的细节。

如果我们想用 JavaScript 编写类似的内容:

let val1 = 2
let val2 = 3
let sum = val1 + val2

console.log(sum) // 5

val1 = 3

console.log(sum) // 仍然是 5

1、当一个值被读取时进行追踪,例如 val1 + val2 会同时读取 val1 和 val2。

2、当某个值改变时进行检测,例如,当我们赋值 val1 = 3。

3、重新运行代码来读取原始值,例如,再次运行 sum = val1 + val2 来更新 sum 的值。

Vue 如何知道哪些代码在执行?

为了能够在数值变化时,随时运行我们的总和,我们首先要做的是将其包裹在一个函数中。

const updateSum = () => {
  sum = val1 + val2
}

但我们如何告知 Vue 这个函数呢?

Vue 通过一个副作用 (effect) 来跟踪当前正在运行的函数。副作用是一个函数的包裹器,在函数被调用之前就启动跟踪。Vue 知道哪个副作用在何时运行,并能在需要时再次执行它。

为了更好地理解这一点,让我们尝试脱离 Vue 实现类似的东西,以看看它如何工作。

我们需要的是能够包裹总和的东西,像这样:

createEffect(() => {
  sum = val1 + val2
})

我们需要 createEffect 来跟踪和执行。我们的实现如下:

// 维持一个执行副作用的栈
const runningEffects = []

const createEffect = fn => {
  // 将传来的 fn 包裹在一个副作用函数中
  const effect = () => {
    runningEffects.push(effect)
    fn()
    runningEffects.pop()
  }

  // 立即自动执行副作用
  effect()
}

当我们的副作用被调用时,在调用 fn 之前,它会把自己推到 runningEffects 数组中。这个数组可以用来检查当前正在运行的副作用。

副作用是许多关键功能的起点。例如,组件的渲染和计算属性都在内部使用副作用。任何时候,只要有东西对数据变化做出奇妙的回应,你就可以肯定它已经被包裹在一个副作用中了。

虽然 Vue 的公开 API 不包括任何直接创建副作用的方法,但它确实暴露了一个叫做 watchEffect 的函数,它的行为很像我们例子中的 createEffect 函数。

但知道什么代码在执行只是难题的一部分。Vue 如何知道副作用使用了什么值,以及如何知道它们何时发生变化?

Vue 如何跟踪变化?

我们不能像前面的例子中那样跟踪局部变量的重新分配,在 JavaScript 中没有这样的机制。我们可以跟踪的是对象 property 的变化。

当我们从一个组件的 data 函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 get 和 set 处理程序的 Proxy 中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。

那看起来灵敏,不过,需要一些 Proxy 的知识才能理解!所以让我们深入了解一下。有很多关于 Proxy 的文档,但你真正需要知道的是,Proxy 是一个对象,它包装了另一个对象,并允许你拦截对该对象的任何交互。

我们这样使用它:new Proxy(target, handler)

使用 Proxy 实现响应性的第一步就是跟踪一个 property 何时被读取。我们在一个名为 track 的处理器函数中执行此操作,该函数可以传入 target 和 property 两个参数。

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, property, receiver) {
    track(target, property)
    return Reflect.get(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// tacos

这里没有展示 track 的实现。它将检查当前运行的是哪个副作用,并将其与 target 和 property 记录在一起。这就是 Vue 如何知道这个 property 是该副作用的依赖项。

最后,我们需要在 property 值更改时重新运行这个副作用。为此,我们需要在代理上使用一个 set 处理函数:

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, property, receiver) {
    track(target, property)
    return Reflect.get(...arguments)
  },
  set(target, property, value, receiver) {
    trigger(target, property)
    return Reflect.set(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// tacos

现在,我们对 Vue 如何实现这些关键步骤有了答案:

1、当一个值被读取时进行追踪:proxy 的 get 处理函数中 track 函数记录了该 property 和当前副作用。

2、当某个值改变时进行检测:在 proxy 上调用 set 处理函数。

3、重新运行代码来读取原始值:trigger 函数查找哪些副作用依赖于该 property 并执行它们。

该被代理的对象对于用户来说是不可见的,但是在内部,它们使 Vue 能够在 property 的值被访问或修改的情况下进行依赖跟踪和变更通知。

如果我们要用一个组件重写我们原来的例子,我们可以这样做:

const vm = createApp({
  data() {
    return {
      val1: 2,
      val2: 3
    }
  },
  computed: {
    sum() {
      return this.val1 + this.val2
    }
  }
}).mount('#app')

console.log(vm.sum) // 5

vm.val1 = 3

console.log(vm.sum) // 6

vue3.0 中为什么要使用 Proxy,它相比以前的实现方式有什么改进?

1、Vue2.x通过给每个对象添加getter setter属性去改变对象,实现对数据的观测,Vue3.x通过Proxy代理目标对象,且一开始只代理最外层对象,嵌套对象 lazy by default,性能会更好。

2、支持数组索引修改,对象属性的增加,删除

以上是关于深入响应性原理的主要内容,如果未能解决你的问题,请参考以下文章

深入探讨vue响应式原理

深入浅出 Vue 响应式原理!

Vuejs - 深入浅出响应式系统

深入理解AsyncTask的工作原理

深入理解PHP原理之Opcodes

Vue 深入响应式原理