Vue3官网-高级指南(十六)vue响应式原理\步骤\本质(副作用effect、响应式状态reactive、响应式原始值ref(解包)、构造响应值toRefs)、proxy(代理、this)
文章目录
- Vue3官网-高级指南(十六)vue响应式原理\步骤\本质(副作用effect、响应式状态reactive、响应式原始值ref(解包)、构造响应值toRefs)、proxy(代理、this)
- 1. 深入响应性原理
- 什么是响应性
- Vue 如何知道哪些代码在执行
- Vue 如何跟踪变化
- 被代理的对象
- Proxy vs 原始标识
- 如何让渲染响应变化
- 2. 响应性基础
- 声明响应式状态
- 创建独立的响应式值作为 `refs`
- Ref 解包
- 访问响应式对象
- 响应式状态解构
- 使用 `readonly` 防止更改响应式对象
总结:
补充
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
truthy(真值):在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除
false
、0
、""
、null
、undefined
和NaN
以外皆为真值)。括号内都是假值falsy。.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault()
==Event.preventDefault方法取消浏览器对当前事件的默认行为。==比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。
vue:用组件(app.component)构建一个模板(template),并反复使用模板
父组件、子组件
const app = Vue.createApp({ components: { 'component-a': ComponentA, 'component-b': ComponentB } })
上面代码中app为父组件,ComponentA和ComponentB为子组件
context.emit
- 父组件通过
:data="data"
传递数据- 父组件通过
@fun="fun"
传递方法- 子组件通过
props
接收父组件传递的值- 子组件通过
$emit
调用父组件的方法并传递数据Vue中美元$符号的意思
- 除了数据属性,Vue 实例还暴露了一些有用的实例属性与方法。它们都有前缀
$
,以便与用户定义的属性区分开来。- vue中$refs的作用?
- 当我们需要获取一个dom元素进行操作的时候 可以利用原生js的getElementByXxx 而在vue中可以设置refs来获取这个dom元素
第三方网站
- greensock API(GSAP):是一套用于所有主流浏览器中制作高性能html5动画的工具。他们有一个很棒的 ease visualizer,帮助你建立精心制作的画架。
- GSAP的全名是==GreenSock Animation Platform,这个名字是有些怪异(官网还在各种安利你加入它的Club),但它的确是一个从flash时代一直发展到今天的专业动画库。==参照CSDN:https://blog.csdn.net/weixin_39939012/article/details/80833073
- animate.css :集成第三方 CSS 动画库
- CSS Triggers :来查看哪些属性会在动画时触发重绘。这个网站是用来告诉开发者不同内核浏览器对css属性修改的
重绘
/回流
情况,开发者知道了这些细节可以提高页面性能。JavaScript钩子
- 添加
:css="false"
,也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。解构(Destructuring)
- ES6允许按照一定模式从数组和对象中提取值,然后对变量进行复制,这被称为解构(Destructuring)
API参考
- https://v3.cn.vuejs.org/api/refs-api.html#ref
自定义指令
- 注意,在 Vue 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
渲染函数
- Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
深入响应性原理
proxy
- 网址:https://blog.csdn.net/qq_43456781/article/details/119422451
- 定义
- Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
- this问题
- 使用 Proxy 的一个难点是
this
绑定。我们希望任何方法都绑定到这个 Proxy(代理本身),而不是目标对象(target),这样我们也可以拦截它们。- 虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的
this
关键字会指向 Proxy 代理。- ES6 引入了另一个名为
Reflect
的新特性,它允许我们以最小的代价消除了这个问题:vue响应术语
副作用——effect
- Vue 通过一个副作用 (effect) 来跟踪当前正在运行的函数。副作用是一个函数的包裹器,在函数被调用之前就启动跟踪。Vue 知道哪个副作用在何时运行,并能在需要时再次执行它
- 副作用是许多关键功能的起点。例如,组件的渲染和计算属性都在内部使用副作用。任何时候,只要有东西对数据变化做出奇妙的回应,你就可以肯定它已经被包裹在一个副作用中了。
- 虽然 Vue 的公开 API 不包括任何直接创建副作用的方法,但它确实暴露了一个叫做
watchEffect
的函数,它的行为很像我们例子中的createEffect
函数。我们会在该指南后面的部分详细讨论这个问题。响应式状态——reactive
从 Vue 3 开始,我们的响应性现在可以在一个独立包中使用。将
$data
包裹在一个代理中的函数被称为reactive
。我们可以自己直接调用这个函数,允许我们在不需要使用组件的情况下将一个对象包裹在一个响应式代理中。JavaScript 对象创建响应式状态,可以使用
reactive
方法如果你不想要访问实际的对象实例,可将其用
reactive
包裹响应式状态——reactive解构,torefs
我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联
import { reactive, toRefs } from 'vue' const book = reactive({ author: 'Vue Team', year: '2020', title: 'Vue 3 Guide', description: 'You are reading this book right now ;)', price: 'free' }) let { author, title } = toRefs(book) title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref console.log(book.title) // 'Vue 3 Detailed Guide'
vue响应的关键步骤
- 当一个值被读取时进行追踪:proxy 的
get
处理函数中track
函数记录了该 property 和当前副作用。- 当某个值改变时进行检测:在 proxy 上调用
set
处理函数。- 重新运行代码来读取原始值:
trigger
函数查找哪些副作用依赖于该 property 并执行它们。vue响应系统本质
- Vue 中响应式状态(reactive)的基本用例是我们可以在渲染期间使用它。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。这就是 Vue 响应性系统的本质。
- 当从组件中的
data()
返回一个对象时,它在内部交由reactive()
使其成为响应式对象。模板会被编译成能够使用这些响应式 property 的渲染函数。refs 独立的响应式原始值
基础
- 独立的原始值 (例如,一个字符串),我们想让它变成响应式的。
- Vue 为我们提供了一个可以做相同事情的方法——
ref
ref
会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是ref
名称的来源。ref解包
当 ref 作为渲染上下文 (从 setup() 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动浅层次解包内部值
只有访问嵌套的 ref 时需要在模板中添加
.value
Ref 解包仅发生在被响应式
Object
嵌套的时候。当从Array
或原生集合类型如Map
访问 ref 时,不会进行自动解包(需要加上.value):const books = reactive([ref('Vue 3 Guide')]) // 这里需要 .value console.log(books[0].value) const map = reactive(new Map([['count', ref(0)]])) // 这里需要 .value console.log(map.get('count').value)
1. 深入响应性原理
现在是时候深入了!Vue 最独特的特性之一,是其非侵入性的响应性系统。数据模型是被代理的 JavaScript 对象。而当你修改它们时,视图会进行更新。这让状态管理非常简单直观,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下 Vue 响应性系统的底层的细节。
什么是响应性
这个术语在程序设计中经常被提及,但这是什么意思呢?响应性是一种允许我们以声明式的方式去适应变化的编程范例。人们通常展示的典型例子,是一份 excel 电子表格 (一个非常好的例子)。
如果将数字 2 放在第一个单元格中,将数字 3 放在第二个单元格中并要求提供 SUM,则电子表格会将其计算出来给你。不要惊奇,同时,如果你更新第一个数字,SUM 也会自动更新。
JavaScript 通常不是这样工作的——如果我们想用 JavaScript 编写类似的内容:
let val1 = 2
let val2 = 3
let sum = val1 + val2
console.log(sum) // 5
val1 = 3
console.log(sum) // 仍然是 5
如果我们更新第一个值,sum 不会被修改。
那么我们如何用 JavaScript 实现这一点呢?
作为一个高阶的概述,我们需要做到以下几点:
- 当一个值被读取时进行追踪,例如
val1 + val2
会同时读取val1
和val2
。 - 当某个值改变时进行检测,例如,当我们赋值
val1 = 3
。 - 重新运行代码来读取原始值,例如,再次运行
sum = val1 + val2
来更新sum
的值。
我们不能直接用前面的例子中的代码来继续,但是我们后面会再来看看这个例子,以及如何调整它来兼容 Vue 的响应性系统。
首先,让我们深入了解一下 Vue 是如何实现上述核心响应性要求的。
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)
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property) {
console.log('intercepted!')
return target[property]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// intercepted!
// tacos
这里我们截获了读取目标对象 property 的举动。像这样的处理函数也称为一个捕捉器 (trap)。有许多可用的不同类型的捕捉器,每个都处理不同类型的交互。
除了控制台日志,我们可以在这里做任何我们想做的事情。如果我们愿意,我们甚至可以不返回实际值。这就是为什么 Proxy 对于创建 API 如此强大。
使用 Proxy 的一个难点是 this
绑定。我们希望任何方法都绑定到这个 Proxy,而不是目标对象,这样我们也可以拦截它们。值得庆幸的是,ES6 引入了另一个名为 Reflect
的新特性,它允许我们以最小的代价消除了这个问题:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
使用 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 如何实现这些关键步骤有了答案:
- 当一个值被读取时进行追踪:proxy 的
get
处理函数中track
函数记录了该 property 和当前副作用。 - 当某个值改变时进行检测:在 proxy 上调用
set
处理函数。 - 重新运行代码来读取原始值:
trigger
函数查找哪些副作用依赖于该 property 并执行它们。
该被代理的对象对于用户来说是不可见的,但是在内部,它们使 Vue 能够在 property 的值被访问或修改的情况下进行依赖跟踪和变更通知。有一点需要注意,控制台日志会以不同的方式对 proxy 对象进行格式化,因此你可能需要安装 vue-devtools,以提供一种更易于检查的界面。
如果我们要用一个组件重写我们原来的例子,我们可以这样做:
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
data
返回的对象将被包裹在响应式代理中,并存储为 this.$data
。Property this.val1
和 this.val2
分别是 this.$data.val1
和 this.$data.val2
的别名,因此它们通过相同的代理。
Vue 将把 sum
的函数包裹在一个副作用中。当我们试图访问 this.sum
时,它将运行该副作用来计算数值。包裹 $data
的响应式代理将会追踪到,当副作用运行时,property val1
和 val2
被读取了。
从 Vue 3 开始,我们的响应性现在可以在一个独立包中使用。将 $data
包裹在一个代理中的函数被称为 reactive
。我们可以自己直接调用这个函数,允许我们在不需要使用组件的情况下将一个对象包裹在一个响应式代理中。
const proxy = reactive({
val1: 2,
val2: 3
})
在指南接下来的几页中,我们将探索响应性包所暴露的功能。这包括我们已经见过的 reactive
和 watchEffect
等函数,以及使用其他响应性特性的方法,如不需要创建组件的 computed
和 watch
。
被代理的对象
Vue 在内部跟踪所有已经被转成响应式的对象,所以它总是为同一个对象返回相同的代理。
当从一个响应式代理中访问一个嵌套对象时,该对象在被返回之前也被转换为一个代理:
const handler = {
get(target, property, receiver) {
track(target, property)
const value = Reflect.get(...arguments)
if (isObject(value)) {
// 将嵌套对象包裹在自己的响应式代理中
return reactive(value)
} else {
return value
}
}
// ...
}
Proxy vs 原始标识
Proxy 的使用确实引入了一个需要注意的新警告:在身份比较方面,被代理对象与原始对象不相等 (===
)。例如:
const obj = {}
const wrapped = new Proxy(obj, handlers)
console.log(obj === wrapped) // false
其他依赖严等于比较的操作也会受到影响,例如 .includes()
或 .indexOf()
。
这里的最佳实践是永远不要持有对原始对象的引用,而只使用响应式版本。
const obj = reactive({
count: 0
}) // 未引用原始
这确保了等值的比较和响应性的行为都符合预期。
请注意,Vue 不会在 Proxy 中包裹数字或字符串等原始值,所以你仍然可以对这些值直接使用 ===
来比较:
const obj = reactive({
count: 0
})
console.log(obj.count === 0) // true
如何让渲染响应变化
一个组件的模板被编译成一个 render
函数。渲染函数创建 VNodes,描述该组件应该如何被渲染。它被包裹在一个副作用中,允许 Vue 在运行时跟踪被“触达”的 property。
一个 render
函数在概念上与一个 computed
property 非常相似。Vue 并不确切地追踪依赖关系是如何被使用的,它只知道在函数运行的某个时间点上使用了这些依赖关系。如果这些 property 中的任何一个随后发生了变化,它将触发副作用再次运行,重新运行 render
函数以生成新的 VNodes。然后这些举动被用来对 DOM 进行必要的修改。
如果你使用的是 Vue2.x 及以下版本,你可能会对这些版本中存在的一些更改检测警告感兴趣,在这里进行更详细的探讨。
2. 响应性基础
声明响应式状态
要为 JavaScript 对象创建响应式状态,可以使用 reactive
方法:
import { reactive } from 'vue'
// 响应式状态
const state = reactive({
count: 0
})
reactive
相当于 Vue 2.x 中的 Vue.observable()
API,为避免与 RxJS 中的 observables 混淆因此对其重命名。该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响传递对象的所有嵌套 property。
Vue 中响应式状态的基本用例是我们可以在渲染期间使用它。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。
这就是 Vue 响应性系统的本质。当从组件中的 data()
返回一个对象时,它在内部交由 reactive()
使其成为响应式对象。模板会被编译成能够使用这些响应式 property 的渲染函数。
在响应性基础 API 章节你可以学习更多关于 reactive
的内容。
创建独立的响应式值作为 refs
想象一下,我们有一个独立的原始值 (例如,一个字符串),我们想让它变成响应式的。当然,我们可以创建一个拥有相同字符串 property 的对象,并将其传递给 reactive
。Vue 为我们提供了一个可以做相同事情的方法——ref
:
import { ref } from 'vue'
const count = ref(0)
==ref
会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref
名称的来源。==该对象只包含一个名为 value
的 property:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Ref 解包
当 ref 作为渲染上下文 (从 setup() 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动浅层次解包内部值。只有访问嵌套的 ref 时需要在模板中添加 .value
:
<template>
<div>
<span>{{ count }}</span>
<button @click="count ++">Increment count</button>
<button @click="nested.count.value ++">Nested Increment count</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count,
nested: {
count
}
}
}
}
</script>
TIP
如果你不想要访问实际的对象实例,可将其用
reactive
包裹:
nested: reactive({ count })
访问响应式对象
当 ref
作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动解包内部值:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1
Ref 解包仅发生在被响应式 Object
嵌套的时候。当从 Array
或原生集合类型如 Map
访问 ref 时,不会进行解包(需要加上.value):
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
响应式状态解构
当我们想使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构来获取我们想要的 property:
import { reactive } from 'vue'
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})
let { author, title } = book
遗憾的是,使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:
import { reactive, toRefs } from 'vue'
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})
let { author, title } = toRefs(book)
title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
你可以在 Refs API 部分中了解更多有关 refs
的信息
使用 readonly
防止更改响应式对象
有时我们想跟踪响应式对象 (ref
或 reactive
) 的变化,但我们也希望防止在应用程序的某个位置更改它。例如,当我们有一个被 provide 的响应式对象时,我们不想让它在注入的时候被改变。为此,我们可以基于原始对象创建一个只读的 proxy 对象:
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// 通过 original 修改 count,将会触发依赖 copy 的侦听器
original.count++
// 通过 copy 修改 count,将导致失败并出现警告
copy.count++ // 警告: "Set operation on key 'count' failed: target is readonly."