学不完的技术😊

简介

2020 年 9 月 18 日,Vue.js 发布 3.0 版本,代号:One Piece(海贼王)

Vue3 相比于 Vue2 打包大小减少 41%,初次渲染快 55%, 更新渲染快 133%,内存减少 54%。这些都是来自官方的数据,Vue3 的性能确实有很大的提升。除此之外 Vue3 还使用 Proxy 代替 defineProperty 实现响应式,重写虚拟 DOM 的实现和 Tree-Shaking,拥抱 TypeScript,Vue3 可以更好的支持 TypeScript。看来 Vue3 可以更好的支持 TypeScript 也是要学习一下的啊,虽然我是一个后端!

新的特性

  • Composition API(组合 API)

    1. setup 配置
    2. ref 与 reactive
    3. watch 与 watchEffect
    4. provide 与 inject
  • 新的内置组件

    1. Fragment
    2. Teleport
    3. Suspense
  • 其他改变

    1. 新的生命周期钩子
    2. data 选项应始终被声明为一个函数
    3. 移除 keyCode 支持作为 v-on 的修饰符

文件目录

  • main.js
1
2
3
4
5
6
7
8
// 引入的不再是 Vue 构造函数了,引入的是一个名为 createApp 的工厂函数
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例对象 app(类似于之前 Vue2 中的 vm,但 app 比 vm 更'轻')
const app = createApp(App)
// 挂载
app.mount('#app')
  • App.vue,在 template 标签里可以没有根标签了
1
2
3
4
5
<template>
<!-- Vue3 组件中的模板结构可以没有根标签 -->
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

Composition API

官方翻译的是组合式 API

在 Vue2 中,我们使用的是 Options API ,配置项式的 API,我们要创建一个 Vue 实例,然后在里面传入一个配置对象,里面要写 data、methods、watch等的东西,而 Vue3 提出了全新的 Composition API,组合式 API,我们不用直接创建 Vue 实例,而是创建一个 app,然后按需引入需要的 API,来进行使用

setup

Vue3.0 中一个新的配置项,值为一个函数。setup 是所有 Composition API(组合 API)表演的舞台,组件中所用到的:数据方法等等,均要配置在 setup 中。setup 函数的两种返回值:

  • 若返回一个对象,则对象中的属性,方法,在模板中均可以直接使用(重点关注!
  • 若返回一个渲染函数:则可以自定义渲染内容(了解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<h1>博主的信息</h1>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>性别:{{gender}}</h2>
<button @click="sayInfo">显示信息</button>
</template>

<script>
// import { h } from 'vue'
export default {
name: "App",
// 此处只是测试一下 setup 暂时不考虑响应式的问题
setup(){
// 数据
let name = "WXZ"
let age = 18
let gender = "男"

// 方法
function sayInfo(){
alert(`你好${name},你太厉害了吧`)
}
return {
name,age, gender,sayInfo
}
// return () => h('h1', 'WXZ')
}
};
</script>

如果返回的是渲染函数,那你在 template 里写的模板都不奏效了,页面渲染的就是你写的 h 函数中的内容

注意

  • 尽量不要与 Vue2.x 配置混用
    1. Vue2.x 配置(data、method、computed…)中可以访问到 setup 中的属性,方法
    2. 但在 setup 中不能访问到 Vue2.x 配置(data、method、computed…)
    3. 如果有重名, setup 优先
  • setup 不能是一个 async 函数,因为返回值不再是对象, 而是 promise, 模板看不到 return 对象中的属性。(后期也可以返回一个 Promise 实例,但需要 Suspense 和异步组件的配合)

ref

  • 作用:定义一个响应式的数据
  • 语法:const xxx = ref(initValue)
    1. 创建一个包含响应式数据的引用对象(reference 对象,简称 ref 对象)
    2. JS 中操作数据: xxx.value
    3. 模板中读取数据: 不需要 .value,直接:<div>{{ xxx }}</div>
  • 接收的数据可以是基本类型,也可以是对象类型
    1. 基本类型的数据:响应式依靠的是类上的getter与setter完成的
    2. 对象类型的数据:内部"求助"了 Vue3.0 中的一个新函数 -> reactive 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<h1>博主的信息</h1>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>职业: {{ job.type }}</h2>
<h2>工资:{{ job.salary }}</h2>
<button @click="sayInfo">显示信息</button>
<button @click="changeInfo">修改信息</button>
</template>

<script>
import { ref } from "vue";
export default {
name: "App",
setup() {
// 数据
let name = ref("WXZ");
let age = ref(18);
let job = ref({
type: "JAVA工程师",
salary: "30K",
});
// 方法
function sayInfo() {
alert(`你好${name.value},你太厉害了吧,薪水${job.value.salary}这么高`);
}
function changeInfo() {
name.value = "三十年后的WXZ";
age.value = 48;
job.value.type = "工程师";
job.value.salary = "30000000K";
}
return {
name,
age,
job,
sayInfo,
changeInfo,
};
},
};
</script>

通过看源码可以知道调用 ref 会返回一个 RefImpl 的实例对象,RefImpl 类中有 getter 和 setter 可以检测到数据的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function ref(value) {
return createRef(value, false);
}

function createRef(rawValue, shallow) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}

class RefImpl {
constructor(value, _shallow) {
this._shallow = _shallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = _shallow ? value : toRaw(value);
this._value = _shallow ? value : convert(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this._shallow ? newVal : convert(newVal);
triggerRefValue(this, newVal);
}
}
}

reactive

  • 作用:定义一个对象类型的响应式数据(基本类型不要用它,要用 ref 函数)
  • 语法:const 代理对象 = reactive(源对象) 接收一个对象(或数组),返回一个代理对象(Proxy 的实例对象,简称 proxy 对象)
  • reactive 定义的响应式数据是"深层次的"
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<h1>博主的信息</h1>
<h2>姓名:{{ yk.name }}</h2>
<h2>年龄:{{ yk.age }}</h2>
<h2>职业: {{ yk.job.type }}</h2>
<h2>工资:{{ yk.job.salary }}</h2>
<h2>爱好:{{ yk.hobby }}</h2>
<h3>测试数据:{{ yk.job.a.b.c }}</h3>
<button @click="changeInfo">修改信息</button>
</template>

<script>
import { reactive } from "vue";
export default {
name: "App",
setup() {
// 数据
let yk = reactive({
name: "WXZ",
age: 18,
hobby: ["写博客", "学习", "看书"],
job: {
type: "JAVA工程师",
salary: "30K",
a: {
b: {
c: 666,
},
},
},
});

// 方法
function changeInfo() {
yk.name = "三十年后的WXZ";
yk.age = 48;
yk.job.type = "工程师";
yk.job.salary = "30000000K";
yk.job.a.b.c = 888;
// 直接通过数组下标修改,可以触发响应式
yk.hobby[0] = "写小说";
}
return {
yk,
changeInfo,
};
},
};
</script>

响应式原理

Vue2.x 的响应式

  • 实现原理
    1. 对象类型:通过 Object.defineProperty() 对属性的读取,修改进行拦截(数据劫持)
    2. 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)
  • 存在问题
    1. 新增属性,删除属性, 界面不会更新
    2. 直接通过下标修改数组, 界面不会自动更新
  • 解决方案:使用 Vue.setVue.delete 或者 vm.$setvm.$delete 这些API来更新数据

Vue3.0 的响应式

  • 通过 Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写,属性的添加,属性的删除等
  • 通过Reflect(反射): 对源对象的属性进行操作

reactive对比ref

  • 从定义数据角度对比:ref 用来定义:基本类型数据。reactive 用来定义:对象(或数组)类型数据。备注:ref 也可以用来定义对象(或数组)类型数据, 它内部会自动通过 reactive 转为代理对象
  • 从原理角度对比:ref 通过类中的的 getter 与 setter 来实现响应式(数据劫持)。reactive 通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据
  • 从使用角度对比:ref 定义的数据:操作数据需要 .value,读取数据时模板中直接读取不需要 .value。reactive 定义的数据:操作数据与读取数据:均不需要 .value

setup注意点

  1. setup 执行的时机:在 beforeCreate 之前执行一次,this 是 undefined
  2. setup 的参数:setup 接收两个参数(props, context)。props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。context:上下文对象,包含attrs、slots、emit

computed

与 Vue2.x 中 computed 配置功能一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { computed } from 'vue'

setup() {
// 计算属性(简写)
let fullName = computed(() => {
return person.firstName + '-' + person.lastName
})
// 计算属性(完整)
let fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
}

watch

与 Vue2.x 中 watch 配置功能一致

两个小"坑"

  1. 监听 reactive 定义的响应式数据时:oldValue 无法正确获取,强制开启了深度监视(deep 配置失效)
  2. 监视 reactive 定义的响应式数据中某个属性时:deep 配置有效
  • 监听 ref 定义的响应式数据
1
2
3
watch(sum, (newValue, oldValue) => {
console.log('sum变化了', newValue, oldValue)
}, { immediate: true })
  • 如果用 ref 定义了一个对象
1
2
3
watch(person.value,(newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
})
1
2
3
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
}, { deep: true })
  • 监听多个ref定义的响应式数据
1
2
3
watch([sum, msg], (newValue, oldValue) => {
console.log('sum或msg变化了', newValue, oldValue)
})
  • 监听 reactive 定义的响应式数据
1
2
3
watch(person, (newValue, oldValue)=>{
console.log('person变化了', newValue, oldValue)
}, { immediate: true, deep: false }) // 此处的 deep 配置不再奏效
  • 监视 reactive 定义的响应式数据中的某个属性
1
2
3
watch(() => person.job,(newValue, oldValue)=>{
console.log('person的job变化了', newValue, oldValue)
}, { immediate: true, deep: true })
  • 监视 reactive 定义的响应式数据中的某些属性
1
2
3
watch([() => person.job, () => person.name], (newValue, oldValue) => {
console.log('person的job变化了', newValue, oldValue)
}, { immediate: true, deep: true })
  • 特殊情况
1
2
3
watch(() => person.job,(newValue, oldValue) => {
console.log('person的job变化了', newValue, oldValue)
}, { deep: true }) // 此处由于监视的是 reactive 定义的对象中的某个属性,所以 deep 配置有效

watchEffect

watch 的套路是:既要指明监视的属性,也要指明监视的回调。watchEffect 的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。watchEffect 有点像 computed,但 computed 注重的计算出来的值(回调函数的返回值),所以必须要写返回值;而 watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值

1
2
3
4
5
6
// watchEffect 所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(() => {
const x1 = sum.value
const x2 = person.age
console.log('watchEffect配置的回调执行了')
})

生命周期

Vue3.0 中可以继续使用 Vue2.x 中的生命周期钩子,但有有两个被更名:beforeDestroy 改名为 beforeUnmountdestroyed 改名为 unmounted。可以直接已配置项的形式使用生命周期钩子,也可以使用组合式 API 的形式使用,尽量统一。一般来说,组合式 API 里的钩子会比配置项的钩子先执行,组合式 API 的钩子名字有变化。Vue3.0 也提供了 Composition API 形式的生命周期钩子,与 Vue2.x 中钩子对应关系如下:

  • beforeCreate => setup()
  • created => setup()
  • beforeMount => onBeforeMount()
  • mounted => onMounted()
  • beforeUpdate => onBeforeUpdate()
  • updated => onUpdated()
  • beforeUnmount => onBeforeUnmount()
  • unmounted => onUnmounted()

自定义hook函数(重点)

什么是 hook?—— 本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装。类似于 Vue2.x中的 mixin。自定义 hook 的优势: 复用代码, 让 setup 中的逻辑更清楚易懂

创建一个 hooks 文件夹,里面创建文件 usePoint.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { reactive, onMounted, onBeforeUnmount } from "vue";
export default function() {
// 实现鼠标"打点"相关的数据
let point = reactive({
x: 0,
y: 0,
});

// 实现鼠标"打点"相关的方法
function savePoint(event) {
point.x = event.pageX;
point.y = event.pageY;
console.log(event.pageX, event.pageY);
}

// 实现鼠标"打点"相关的生命周期钩子
onMounted(() => {
window.addEventListener("click", savePoint);
});

onBeforeUnmount(() => {
window.removeEventListener("click", savePoint);
});

return point;
}

在组件种使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h2>我是HelloWorld组件</h2>
<h2>当前点击时鼠标的坐标为:x:{{point.x}},y:{{point.y}}</h2>
</template>

<script>
import usePoint from '../hooks/usePoint'
export default {
name: 'HelloWorld',
setup() {
const point = usePoint()
return {point}
}
}
</script>

toRef

  • 作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性
  • 语法:const name = toRef(person, 'name')
  • 应用: 要将响应式对象中的某个属性单独提供给外部使用时
  • 扩展:toRefs 与 toRef 功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person)

其它 Composition API

shallowReactive 与 shallowRef

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)
  • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理
  • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 => shallowReactive
  • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 => shallowRef

readonly 与 shallowReadonly

  • readonly: 让一个响应式数据变为只读的(深只读)
  • shallowReadonly:让一个响应式数据变为只读的(浅只读)

toRaw 与 markRaw

  • toRaw

将一个由 reactive 生成的响应式对象转为普通对象。用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新

  • markRaw

标记一个对象,使其永远不会再成为响应式对象。应用场景: 1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。 2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能

customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<input type="text" v-model="keyWord" />
<h3>{{ keyWord }}</h3>
</template>

<script>
import { customRef } from "vue";
export default {
name: "App",
setup() {
//自定义一个 ref 名为:myRef
function myRef(value, delay) {
let timer;
return customRef((track, trigger) => {
return {
get() {
console.log(`有人从myRef这个容器中读取数据了,我把${value}给他了`);
track(); // 通知 Vue 追踪 value 的变化(提前和 get 商量一下,让他认为这个 value 是有用的)
return value;
},
set(newValue) {
console.log(`有人把myRef这个容器中数据改为了:${newValue}`);
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger(); // 通知 Vue 去重新解析模板
}, delay);
},
};
});
}

// let keyWord = ref('hello') // 使用 Vue 提供的 ref
let keyWord = myRef("hello", 500); // 使用程序员自定义的 ref

return { keyWord };
},
};
</script>

provide 与 inject

  • 作用:实现祖与后代组件间通信
  • 套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据
1
2
3
4
setup() {
let car = reactive({name: '奔驰', price: '40万'})
provide('car',car) // 给自己的后代组件传递数据
}
1
2
3
4
setup(props,context){
const car = inject('car') // 拿到祖先的数据
return {car}
}

响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

新的组件

Fragment

在 Vue2 中: 组件必须有一个根标签。在 Vue3 中: 组件可以没有根标签, 内部会将多个标签包含在一个 Fragment 虚拟元素中

Teleport

Teleport 是一个内置组件,它可以将一个组件内部的一部分模板"传送"到该组件的 DOM 结构外层的位置去

Suspense

等待异步组件时渲染一些额外内容,让应用有更好的用户体验

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中......</h3>
</template>
</Suspense>
</div>
</template>

其他

全局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 选项应始终被声明为一个函数
  • 过渡类名的更改
1
2
3
4
5
6
7
8
9
/** Vue2.x写法 */
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}
1
2
3
4
5
6
7
8
9
10
/** Vue3.x写法 */
.v-enter-from,
.v-leave-to {
opacity: 0;
}

.v-leave-from,
.v-enter-to {
opacity: 1;
}
  • 移除 keyCode 作为 v-on 的修饰符,同时也不再支持 config.keyCodes
  • 移除 v-on.native 修饰符
  • 移除过滤器(filter)

过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 只是"JavaScript"的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器