Vue3
介绍
2020年9月18日Vue.js 3.0“海贼王”正式发布。该框架的这个新主要版本提供了改进的性能、更小的包大小、更好的 TypeScript 集成、用于处理大规模用例的新 API,以及该框架未来长期迭代的坚实基础。
特点:
- 随着时间的推移,它演变成我们所说的“渐进式框架”:一个可以逐渐学习和采用的框架,同时随着用户处理越来越多的要求的场景提供持续的支持。
- Vue 3.0 核心仍然可以通过一个简单的
<script>
标签使用,但它的内部已经从头开始重写为一组解耦模块。新架构提供了更好的可维护性,并允许最终用户通过 tree-shaking 将运行时大小减少一半。 - 2.x 基于对象的 API 在 Vue 3 中基本保持不变。但是,3.0 还引入了Composition API——一组新的 API,旨在解决 Vue 在大规模应用程序中使用的痛点。
- 性能改进
- Vue 3 的代码库是用 TypeScript 编写的,具有自动生成、测试和捆绑的类型定义,因此它们始终是最新的
快速上手
使用 vue-cli 创建Vue3.0工程
先查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
1 | vue --version |
如果版本过低,就需要安装或者升级你的@vue/cli
1 | npm install -g @vue/cli |
运行以下命令来创建一个新项目:
1 | vue create hello-world |
启动项目
1 | cd hello-world |
常用 Composition API
setup
使用 (data
、computed
、methods
、watch
) 组件选项来组织逻辑通常都很有效。然而,当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。
这是一个大型组件的示例,其中逻辑关注点按颜色进行分组。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果能够将同一个逻辑关注点相关代码收集在一起会更好。
setup的设计是为了使用组合式api,使相关逻辑的代码集中在一起.
setup
组件选项
新的 setup
选项在组件创建之前执行,一旦 props
被解析,就将作为组合式 API 的入口。
在
setup
中你应该避免使用this
,因为它不会找到组件实例。setup
的调用发生在data
property、computed
property 或methods
被解析之前,所以它们无法在setup
中被获取。
setup
选项是一个接收 props
和 context
的函数,此外,我们将 setup
返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
1 | export default { |
使用 setup
函数时,它将接收两个参数:
props
context
Props
setup
函数中的第一个参数是 props
。正如在一个标准组件中所期望的那样,setup
函数中的 props
是响应式的,当传入新的 prop 时,它将被更新。
1 | // MyBook.vue |
但是,因为
props
是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。
如果需要解构 prop,可以在 setup
函数中使用 toRefs
函数来完成此操作:
1 | // MyBook.vue |
如果 title
是可选的 prop,则传入的 props
中可能没有 title
。在这种情况下,toRefs
将不会为 title
创建一个 ref 。你需要使用 toRef
替代它:
1 | // MyBook.vue |
props
在浏览器的控制台输出如下图:
Context
传递给 setup
函数的第二个参数是 context
。context
是一个普通的 JavaScript 对象,它暴露组件的三个 property:
的三个 property:
1 | // MyBook.vue |
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6 解构。
1 | // MyBook.vue |
attrs
和 slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x
或 slots.x
的方式引用 property。
context
在浏览器的控制台输出如下图:
setup
注意点:
- 尽量不要与Vue2.x配置混用
- Vue2.x配置(data、methos、computed…)中可以访问到setup中的属性、方法。
- 但在setup中不能访问到Vue2.x配置(data、methos、computed…)。
- 如果有重名, setup优先。
- setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
- 在
setup()
内部,this
不是该活跃实例的引用
执行 setup
时,组件实例尚未被创建。因此,你只能访问以下 property:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value
。
示例:
1 | setup() { |
如果将对象分配为 ref 值,则通过 reactive 方法使该对象具有高度的响应式。
使用数据
1 | <h2>count: {{count}}</h2> |
- 接收的数据可以是:基本类型、也可以是对象类型。
- 基本类型的数据:响应式依然是靠
Object.defineProperty()
的get
与set
完成的。 - 对象类型的数据:内部 使用了Vue3.0中的一个新函数——
reactive
函数。
reactive函数
返回对象的响应式副本,Proxy的实例对象
1 | const obj = reactive({ count: 0 }) |
reactive
将解包所有深层的 refs,同时维持 ref 的响应性。
1 | const count = ref(1) |
Vue3.0中的响应式原理
vue2.x的响应式
实现原理:
对象类型:通过
Object.defineProperty()
对属性的读取、修改进行拦截(数据劫持)。数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
1
2
3
4Object.defineProperty(data, 'count', {
get () {},
set () {}
})
存在问题:
- 新增属性、删除属性, 界面不会更新。
- 直接通过下标修改数组, 界面不会自动更新。
Vue3.0的响应式
实现原理:
通过Proxy(代理): Proxy 是一个对象,它包装了另一个对象,并允许你拦截对该对象的任何交互。
通过Reflect(反射): 使用 Proxy 的一个难点是
this
绑定。我们希望任何方法都绑定到这个 Proxy,而不是目标对象,这样我们也可以拦截它们。值得庆幸的是,ES6 引入了另一个名为Reflect
的新特性,它允许我们以最小的代价消除了这个问题:MDN文档中描述的Proxy与Reflect:
1 | new Proxy(data, { |
计算属性和侦听器
计算属性
有时我们需要依赖于其他状态的状态——在 Vue 中,这是用组件计算属性处理的,以直接创建计算值,我们可以使用 computed
方法:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象。
1 | setup() { |
使用数据
1 | <template> |
效果
或者,它可以使用一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
1 | setup() { |
使用数据
1 | <template> |
效果
Watch
watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
侦听单个数据源
侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref
:
1 | // 侦听一个 getter |
侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
1 | const firstName = ref("zhang"); |
尽管如此,如果你在同一个方法里同时改变这些被侦听的来源,侦听器仍只会执行一次:
1 | function changeFullName() { |
注意多个同步更改只会触发一次侦听器。
两个小“坑”:
- 监视reactive定义的响应式数据时:oldValue无法正确获取、默认已经强制开启了深度监视(deep配置失效)。
- 监视reactive定义的响应式数据中某个属性时:deep配置有效。
watchEffect
watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
watchEffect有点像computed:
但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
1
2
3
4
5
6//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(()=>{
const x1 = sum.value
const x2 = person.age
console.log('watchEffect配置的回调执行了')
})
生命周期
下表包含如何在 setup () 内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
beforeCreate |
Not needed* |
created |
Not needed* |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
activated |
onActivated |
deactivated |
onDeactivated |
因为
setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写。
toRef
可以用来为源响应式对象上的某个 property 新创建一个 ref
。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。
1 | const state = reactive({ |
toRefs
将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref
。
当从组合式函数返回响应式对象时,toRefs
非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:
1 | function useFeatureX() { |
toRefs
只会为源对象中包含的 property 生成 ref。如果要为特定的 property 创建 ref,则应当使用 toRef
其他 Composition API
shallowReactive 与 shallowRef
shallowReactive
:创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换1
2
3
4
5
6
7
8
9
10
11
12const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
shallowRef
:只处理基本数据类型的响应式, 不进行对象的响应式处理。1
2
3
4
5const foo = shallowRef({})
// 改变 ref 的值是响应式的
foo.value = {}
// 但是这个值不会被转换。
isReactive(foo.value) // false什么时候使用?
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===>
shallowReactive
。 - 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===>
shallowRef
。
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===>
readonly 与 shallowReadonly
readonly
: 接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。1
2
3
4
5
6
7
8
9
10
11
12
13
14const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 用于响应性追踪
console.log(copy.count)
})
// 变更 original 会触发依赖于副本的侦听器
original.count++
// 变更副本将失败并导致警告
copy.count++ // 警告!shallowReadonly
:创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。1
2
3
4
5
6
7
8
9
10
11
12const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的 property 将失败
state.foo++
// ...但适用于嵌套对象
isReadonly(state.nested) // false
state.nested.bar++ // 适用应用场景: 不希望数据被修改时。
toRaw 与 markRaw
toRaw:
- 作用:将一个由
reactive
生成的响应式对象转为普通对象。 - 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
- 作用:将一个由
markRaw:
作用:标记一个对象,使其永远不会再成为响应式对象。返回对象本身。
1
2
3
4
5
6const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 嵌套在其他响应式对象中时也可以使用
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false有些值不应该是响应式的,例如复杂的第三方类实例或 Vue 组件对象
当渲染具有不可变数据源的大列表时,跳过 proxy 转换可以提高性能。
customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track
和 trigger
函数作为参数,并且应该返回一个带有 get
和 set
的对象。
自定义customRef配合自定义hook函数使用。
什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。类似于vue2.x中的mixin。自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
在项目中新建hooks文件夹,在hooks文件夹下新建useDebounce.js文件
1 | import { customRef } from "vue" |
使用自定义 ref 通过 v-model
实现 debounce 的示例:
1 | <input v-model="text" /> |
1 | import useDebouncedRef from '../hooks/useDebounce' |
provide 与 inject
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对 provide
和 inject
。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据。
用法:
父组件中提供:
1 | setup(){ |
子组件中使用:
1 | setup(props,context){ |
响应式数据的判断
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由
reactive
创建的响应式代理 - isReadonly: 检查一个对象是否是由
readonly
创建的只读代理 - isProxy: 检查一个对象是否是由
reactive
或者readonly
方法创建的代理
新的组件
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
让我们修改 modal-button
以使用 <teleport>
,并告诉 Vue “Teleport 这个 HTML 到该‘body’标签”。
1 | app.component('modal-button', { |
其他
全局API的转移
Vue 2.x 有许多全局 API 和配置。
例如:注册全局组件、注册全局指令等。
1
2
3
4
5
6
7
8
9
10
11
12//注册全局组件
Vue.component('MyButton', {
data: () => ({
count: 0
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
//注册全局指令
Vue.directive('focus', {
inserted: el => el.focus()
}
Vue3.0中对这些API做出了调整:
将全局的API,即:
Vue.xxx
调整到应用实例(app
)上2.x 全局 API( Vue
)3.x 实例 API ( app
)Vue.config.xxxx app.config.xxxx Vue.config.productionTip 移除 Vue.component app.component Vue.directive app.directive Vue.mixin app.mixin Vue.use app.use Vue.prototype app.config.globalProperties
其他改变
data选项应始终被声明为一个函数。
过度类名的更改:
Vue2.x写法
1
2
3
4
5
6
7
8.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}Vue3.x写法
1
2
3
4
5
6
7
8
9.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
移除keyCode作为 v-on 的修饰符,同时也不再支持
config.keyCodes
移除
v-on.native
修饰符父组件中绑定事件
1
2
3
4<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>子组件中声明自定义事件
1
2
3
4
5<script>
export default {
emits: ['close']
}
</script>
移除过滤器(filter)
过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。