Vue3官方文档翻译之Reactivity Fundamentals

359 阅读4分钟

引言

突然不知道这章写了啥, 感觉内容有点干, 顾着翻译去了, 没有吸收消化. 还是总结下, 主要是就是响应式的对象如何和template结合的, 响应式对象是通过JavaScript的代理对象实现的; 响应式的对象有分为深响应和浅响应; 完成响应式的对象在作为函数参数或者解构的时候会有局限性,因此提出了ref()来避免此问题,并讲接了ref的拆箱问题.

原文地址: blog.duhbb.com/2022/02/11/…

欢迎访问我的博客: blog.duhbb.com/

Vue3官方文档翻译之Reactivity Fundamentals

Reactivity Fundamentals: 我也不知道怎么翻译, 就当作是响应式原理或者基础吧.

P.S. 我看的这个版本是 Composition API, 所以如果有偏差的话, 应该是API风格上的不同, 请自行甄别.

声明响应式的状态

我们可以使用 reactive() 函数创建一个响应式的对象或者数组.

妙啊, 创建之后呢?

import { reactive } from 'vue'

const state = reactive({ count: 0 })

响应时的对象都是JavaScript代理对象, 学过代理模式的应该都懂吧, 不会有人觉得用common 对象就能响应式吧, 我瞎逼逼的.

但是这些代理对象看上去和普通对象并没有什么区别. 不同的是Vue可以跟踪代理对象的属性访问以及响应式对象的变更. 如果你好奇这个在Vue中的实现原理, 那么我推荐你了解一下Reactivity in Depth, 不要猴急, 你先把这篇看完了, 再去深入了解.

也可以参考: Typing Reactive

为了在组件模板中使用响应式的对象, 声明并且从组件的setup()函数中返回这个对象.

P.S. Options API 应该是从data()返回的吧.

import { reactive } from 'vue'

export default {
  // `setup`是一个特殊的钩子, 专门用于composition API.
  setup() {
    const state = reactive({ count: 0 })

    // 将响应式状态暴露给模板
    return {
      state
    }
  }
}

这样, state就和template关联起来了, 你瞧瞧多么牛逼的框架......

<div>{{ state.count }}</div>

类似的, 我们可以在同一个域中声明函数来操作响应式的状态, 并将将她作为方法和state一起暴露.

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    // 擦, mutation的函数是放在scope中的?
    function increment() {
      state.count++
    }

    // don't forget to expose the function as well.
    return {
      state,
      increment
    }
  }
}

被暴露的方法一般都是作为事件响应的监听器:

<button @click="increment">
  {{ state.count }}
</button>

<script setup>

通过setup手动的暴露响应式对象或者方法可能会比较繁琐.

幸运滴是, 不需要build步骤的时候才需要这么做.

当使用Single-File Component的时候, 我们可以通过<script setup>进行极大的简化:


<!-- 歪日, 还真是方便 -->
<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
}
</script>

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

<script setup>中的顶层引入以及变量都会在同一个组件的模板中变得可用.

在本文档的剩下内容中, 我们将会主要使用SFC + <script setup> 语法来编写Composition API风格的代码例子, 应为这种用法对于Vue开发者来说是最常用的.

我是个伪前端, 伪Vue开发者......

DOM更新Timing

不好意思, timing我不会翻译了, 感觉应该是"时机"的意思.

当你修改了响应式的对象的时候, DOM会被自动地更新. But, 但是, 然而, 你应该注意到, DOM的更新并不是同步的.

令人震惊的消息, ??? 什么, 就这点更新, 为什么不能同步呢?

实际上Vue会将这些变更缓冲起来, 直到下一次"next click"的更新周期中来确保每个组件只被更新一次, (我擦, 这还整上了时钟周期?), 而不管你怎么修改state.

哟, 怎么玩儿的呢?

为了在state修改后, DOM的所有的update都能被执行完毕, 你可以使用nextTick()这个API:

import { nextTick } from 'vue'

function increment() {
  count.value++
  // nextTick的回调就表示DOM中的组件都被个更新了?
  nextTick(() => {
    // access updated DOM
  })
}

更深层次的响应性

在Vue中, state默认是深度响应(这翻译我都看不下去了, 应该是和深拷贝浅拷贝类似吧?).

这意味着, 即使你对嵌套的对象, 或者数组中的对象进行修改的时候, 这种响应也能被检测到.

(果然是深拷贝的概念, 所以嵌套的对象和数组也是代理的? 代理模式果然牛逼, Spring框架也是靠代理模式)

import { reactive } from 'vue'

const obj = reactive({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // these will work as expected.
  obj.nested.count++
  obj.arr.push('baz')
}

当然, 你可以显式地创建浅响应对象, 那么只根节点的对象的变更才会被追踪, 但是这种变态的用法只是在高级场所, 啊不, 高级场景中才用得到.

响应式的代理对象 VS. 原始对象

需要注意的式, 从reactive()方法中返回的对象式原始对象的代理, 和原始的对象不是等同滴.

const raw = {}
const proxy = reactive(raw)

// proxy is NOT equal to the original.
console.log(proxy === raw) // false

只有代理对象是reactive的, 修改原始对象并不会触发更新. 隐刺, 当使用Vue响应式系统的最佳方式就是使用你的状态的代理对象, 不要直接去操作原始对象(后面是我加的).

为了确保对代理对象访问的一致性, 对同一个对象使用reactive()返回的总是同一个代理对象, 哇偶, 这玩意儿还是幂等的咧, 同样对代理对方使用reactive()方法, 返回的还是它自己(应该很好理解吧).

// calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true

// calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true

这个规则也同样适用于嵌套对象. 由于是深层响应的, 响应式对象中的嵌套对象也是代理的:

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

reactive()函数的局限性

reactive()这么牛逼的函数也有她的局限性:

  1. 她只能作用于对象类型(对象, 数组或者集合类型, 比如MapSet). 而不能作用于原始类型, 比如string, number或者boolean. (没得拆箱装箱咩?)
  2. 由于Vue的响应式追踪属性的访问, 我们对响应式对象保留同一个引用. 这意味着我们不能替换响应式的对象:
let state = reactive({ count: 0 })

// this won't work!
state = reactive({ count: 1 })

这也意味着当我们赋值或者解构一个响应式对象的属性到局部变量或者当我们将属性传递给函数,或者从响应式对象解构属性,我们都将失去响应式链接(good! 感觉和Hibernate的状态管理有点类似鸭, de-attach):

const state = reactive({ count: 0 })

// n is a local variable that is disconnected
// from state.count.
let n = state.count
// does not affect original state
n++

// count is also disconnected from state.count.
let { count } = state
// does not affect original state
count++

// the function receives a plain number and
// won't be able to track changes to state.count
callSomeFunction(state.count)

通过ref()获得响应式变量(大概就这?)

为了解决reactive()的不便之处, Vue也提供了一个ref()函数, 它允许我们创建任何值类型的响应式引用:

import { ref } from 'vue'

const count = ref(0)

ref()接受参数, 并返回一个ref对象, 属性是.value:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

P.S.: 这他喵的是装箱和拆箱?

参考: Typing Refs

和响应式对象的属性类似, ref的.value属性也是响应式的. 除此之外, 当手里拿着一个对象类型的时候, ref会自动将.value交给reactive()处理.

ref中包含的对象值可以响应式地替代整个对象(没看懂?):

const objectRef = ref({ count: 0 })

// this works reactively
objectRef.value = { count: 1 }

哦哦, 懂了, reactive()不是不能替换吗, ref可以替换.

refs可以被传递到函数中或者从普通的对象解构而不会失去reactivity:

const obj = {
  foo: ref(1),
  bar: ref(2)
}

// the function receives a ref
// it needs to access the value via .value but it
// will retain the reactivity connection
callSomeFunction(obj.foo)

// still reactive
const { foo, bar } = obj

换句话说, ref() 允许我们创建一个对任何值得引用, 并且传递她的时候不会丢失reactivity.

这个特性非常重要, 在将逻辑提取到Composable Functions的时候就显得非常重要了.

在模板中进行引用拆箱

当引用在模板中被作为顶层属性使用的时候, 她们会自动地被拆箱, 因此就不需要我们再使用 .value 了. 下面将之前计数地例子用ref()进行了改造:

<script setup>
import { ref } from 'vue'

/* 嘿嘿, 这个是时候原始值也可以获得reactivity了 */
const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }} <!-- no .value needed -->
  </button>
</template>

小心: 自动地拆箱仅适用于顶层属性, 对于refs的嵌套访问则不会被拆箱:

const object = { foo: ref(1) }
<!-- object只是一个普通的对象, 刚才的规则也说了只有top-level的才会被自动拆箱 -->
{{ object.foo }} <!-- does NOT get unwrapped -->

正如设想的那样, foo 并不会被拆箱, 呵呵, 我还没有验证.

响应式对象中的ref的拆箱

当一个ref作为一个响应式的对象的属性进行访问或者修改的时候, 她会被自动的进行拆箱, 就像正常的属性那样:

const count = ref(0)

/* state已经是一个响应式的对象, 她有一个ref形式的属性, 这个会被自动拆箱, 不需要是top-level了 */
/* 感觉我已经理解了这个, 但是没有实践 */
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

如果一个新的ref替换了原来的ref, 她将会替代原来的ref:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// original ref is now disconnected from state.count
console.log(count.value) // 1

ref的拆箱只会发生在作为深层的响应式对象的时候, 而作为浅层响应对象的属性时, 则不会被拆箱.

数组和集合中的ref的拆箱

和响应式对象不一样, 当ref作为响应式数组或者集合类型的元素被访问的时候并不会发生拆箱(这是为什么呢?):

const books = reactive([ref('Vue 3 Guide')])
// need .value here
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// need .value here
console.log(map.get('count').value)

响应式的传递性(实验特性)

必须将 .value 和 refs 进行配合使用时JavaScript这个语言所施加的限制. 然而, 通过运行期的转换在合适的位置追加 .value 则可以提高 ergonomics. Vue提供了编译期的转换使我们可以将之前的 "counter" 例子写成这样:

<script setup>
let count = $ref(0)

function increment() {
  // no need for .value
  count++
}
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

关于Reactivity Transform可以在她的专题了解, 提醒各位这个目前还只是实验性滴.

结束语

突然不知道这章写了啥, 感觉内容有点干, 顾着翻译去了, 没有吸收消化. 还是总结下, 主要是就是响应式的对象如何和template结合的, 响应式对象是通过JavaScript的代理对象实现的; 响应式的对象有分为深响应和浅响应; 完成响应式的对象在作为函数参数或者解构的时候会有局限性,因此提出了ref()来避免此问题,并讲接了ref的拆箱问题.

原文地址: blog.duhbb.com/2022/02/11/…

欢迎访问我的博客: blog.duhbb.com/