深入响应式原理

前端精髓

共 3188字,需浏览 7分钟

 · 2021-10-19

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


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

let val1 = 2let val2 = 3let 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、支持数组索引修改,对象属性的增加,删除


浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报