前言

最近看了看vue3,发现变化还是挺大的,写篇文章来记录一波

vite

vite介绍和用法

Vite是一个由原生ESM驱动的Web开发构建工具。在开发环境下基于浏览器原生ES imports开发,在生产环境下基于Rollup打包。

其实简单来说,vite就是一个和webpack用处差不多的代码构建工具,但是它在代码开发阶段有着非常显著的优势,它大大降低了开启本地服务器和代码热更新需要的时间,他的主要优点有下面几个

  • 快速的冷启动
  • 即时的模块热更新
  • 真正的按需编译

那么怎么使用呢,我们直接运行下面的命令就可以了

1
2
3
4
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

这个命令会在本地临时安装vite,然后用vite创建一个新项目,所以每次运行的时候使用的都是最新的vite

而从生成的目录树中可以看出来,vite和vue-cli生成的代码并没有太大的差别

1
2
3
4
5
6
7
8
9
10
11
12
13
├── index.html
├── package.json
├── public
│ └── favicon.ico
└── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── HelloWorld.vue
├── index.css
└── main.js

在开发时要注意在导入文件时除了导入的文件是js类型的,其他时候都要补全后缀

vite的原理

在运行npm run dev后,vite借用了koa启动了一个本地代理服务器,没有进行任何的编译和打包操作

1
2
3
4
5
6
[vite] Optimizable dependencies detected:
vue

Dev server running at:
> Network: http://192.168.2.67:3000/
> Local: http://localhost:3000/

在访问http://localhost:3000/时,vite直接返回了项目的index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

然后浏览器就会解析这段html文件,值的一提的是,这里的main.js是使用了module的方式引入的,所以天生支持import和export语句,浏览器会向本地服务器请求main.js

本地服务器返回处理过的main.js

1
2
3
4
5
import {createApp} from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

createApp(App).mount('#app');

浏览器拿到main.js后,继续解析并请求依赖的文件,服务器对请求的文件进行编译和处理,返回给浏览器运行,一直重复这个过程

file

编译过后的index.css

file

再来看看vite的几个优点

  • 快速的冷启动(开启服务器不进行打包和编译,只是开启一个服务器返回index.html)
  • 即时的模块热更新
  • 真正的按需编译(浏览器的import语法天生支持按需引入,服务器只对浏览器请求的文件进行编译)

Api和数据响应式的变化

去掉了Vue构造函数

从刚刚vite搭建的项目中可以看到,vue3不再使用new Vue()的方式来创建vue应用,而是使用createApp()来创建,为什么要这么做呢,来看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- vue2 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
Vue.use(...); // 此代码会影响所有的vue应用
Vue.mixin(...); // 此代码会影响所有的vue应用
Vue.component(...); // 此代码会影响所有的vue应用
new Vue({
// 配置
}).$mount("#app1")
new Vue({
// 配置
}).$mount("#app2")
</script>
1
2
3
4
5
6
7
<!-- vue3 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
createApp(根组件).mount("#app2")
</script>

可以看出,vue2的一些方法会影响所有的vue应用,而vue3就可以解决了这个问题,让开发者可以对不同的Vue应用进行不同的配置,方便了不同Vue应用的相互隔离

另外createApp创建的是一个Vue应用,Vue应用不是一个特殊的组件,这点是和Vue2不同的,Vue3对这两个概念进行了区分,一定程度上避免了可能造成的思维混乱

file

最后一点就是Vue2的构造函数集成了太多功能,不利于tree shaking,Vue3把这些功能使用普通函数导出,能够充分利用tree shaking优化打包体积,所以Vue3的打包体积是小于Vue2的

使用了proxy来进行数据响应式

优点:

  • 使用proxy比递归对象设置访问器属性要高效
  • 可以观测到对象的新增属性和删除属性
  • 可以观测到数组下标的变化

缺点:

  • 兼容性较差。而且polyfill也难以完全支持

模板的变化

组件允许多个根节点

如图,下图的模板语法现在被支持了,在一个组件里允许了多个根结点而存在

1
2
3
4
5
6
7
8
<template>
<div>
wow
</div>
<div>
funny
</div>
</template>

实现原理非常easy

1
2
<div></div>
<div></div>

编译后

1
2
3
4
5
6
7
8
9
10
import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div"),
_createElementVNode("div")
], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST

v-model的升级

vue2提供了两种双向绑定:v-model.sync,在vue3中,去掉了.sync修饰符,只需要使用v-model进行双向绑定即可。

为了让v-model更好的针对多个属性进行双向绑定,vue3作出了以下修改

  • 当对自定义组件使用v-model指令时,绑定的属性名由原来的value变为modelValue,事件名由原来的input变为update:modelValue
1
2
3
4
5
6
7
8
9
10
11
12
<!-- vue2 -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
<!-- 简写为 -->
<ChildComponent v-model="pageTitle" />

<!-- vue3 -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
<!-- 简写为 -->
<ChildComponent v-model="pageTitle" />
  • 去掉了.sync修饰符,它原本的功能由v-model的参数替代
1
2
3
4
5
6
7
8
9
<!-- vue2 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
<!-- 简写为 -->
<ChildComponent :title.sync="pageTitle" />

<!-- vue3 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
<!-- 简写为 -->
<ChildComponent v-model:title="pageTitle" />

详细写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="ChildView">
<input type="text" :value="inputValue" @input="updateInputValue">
</div>
</template>


<script lang="ts">
import {defineComponent} from 'vue';

export default defineComponent({
name: 'Child',
props : {
inputValue : {
type : String
}
},
methods : {
updateInputValue($event : any) {
this.$emit('update:inputValue', $event.target.value)
}
}
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="wrapper">
<div>
<Child v-model:inputValue="childValue"></Child>
</div>
</div>
</template>

<script lang="ts">
import {defineComponent} from 'vue';
import Child from "@/components/Child.vue";
export default defineComponent({
name: 'HelloWorld',
components: {
Child,
},
data() {
return {
childValue : ""
}
}
});
</script>

相关文档:vue3 : v-model

修改v-if和v-for的优先级

vue2的优先级是v-for高于v-if

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
<template>
<div class="about">
<h1>This is an about page</h1>
<ul>
<li v-for="(item, index) in items" v-if="item.flag">{{item.value}}</li>
</ul>
</div>
</template>
<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';

@Component({})
export default class About extends Vue {
public items : Array<{
flag : boolean,
value : string
}> = [{
flag : false,
value : "rua"
}, {
flag : true,
value : "qaq"
}]

}
</script>

渲染结果

file

vue3的优先级是v-if高于v-for

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
<template>
<div>
<p v-for="(item, index) in items" v-if="item.flag">{{item.value}}</p>
</div>
</template>


<script>
export default {
data() {
return {
items : [
{
flag : false,
value : "rua"
},
{
flag : true,
value : "qaq"
}
]
}
}
}
</script>

这段代码会报错

file

Vue效率提升

静态节点提升

编译时期会对静态节点和静态属性进行提升,用于这减少render函数中创建VNode的消耗

1
2
3
4
5
<template>
<div id="app">
<div>Hello world</div>
</div>
</template>

编译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
import {createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"

const _hoisted_1 = {
id: "app"
}
const _hoisted_2 = /*#__PURE__*/
_createVNode("div", null, "Hello world", -1 /* HOISTED */
)

export function render(_ctx, _cache) {
return (_openBlock(),
_createBlock("div", _hoisted_1, [_hoisted_2]))
}

预字符串化

当编译器遇到大量连续的静态内容,将这些静态节点序列化为字符串并生成一个Static类型的VNode,静态节点在运行时会通过 innerHTML来创建真实节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="container">
<div class="logo">
<h1>logo</h1>
</div>
<ul class="nav">
<li><a href="">menu1</a></li>
<li><a href="">menu2</a></li>
<li><a href="">menu3</a></li>
<li><a href="">menu4</a></li>
<li><a href="">menu5</a></li>
</ul>
<div class="user">
<span>{{ user.name }}</span>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {createVNode as _createVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"

const _hoisted_1 = {
class: "container"
}
const _hoisted_2 = /*#__PURE__*/
_createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu1</a></li><li><a href=\"\">menu2</a></li><li><a href=\"\">menu3</a></li><li><a href=\"\">menu4</a></li><li><a href=\"\">menu5</a></li></ul>", 2)
const _hoisted_4 = {
class: "user"
}

export function render(_ctx, _cache) {
return (_openBlock(),
_createBlock("div", _hoisted_1, [_hoisted_2, _createVNode("div", _hoisted_4, [_createVNode("span", null, _toDisplayString(_ctx.user.name), 1 /* TEXT */
)])]))
}

预字符串化在下面两种情况出现:

  • 如果节点没有属性,有连续20个及以上的静态节点存在
  • 连续的节点中有5个及以上的节点是有属性绑定的节点

事件处理函数缓存

对下面的模板

1
2
3
4
5
<template>
<div class="container">
<button @click="handleClick"></button>
</div>
</template>

编译结果

1
2
3
4
5
6
7
8
9
10
11
12
import {createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"

const _hoisted_1 = {
class: "container"
}

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(),
_createBlock("div", _hoisted_1, [_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args)=>($setup.handleClick(...args)))
})]))
}

相比于vue2的编译结果

1
2
3
4
5
6
7
render(ctx){
return createVNode("button", {
onClick: function(){
// ...
}
})
}

Block Tree

在线尝试:https://vue-next-template-explorer.netlify.app/

vue2在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上。

vue3新增了block tree这一个概念,block tree会对动态变化的节点进行标记,从而减少了vue2一层层diff的时间,在更新时也只要查找动态的节点就可以了(动态的节点会存到一个数组里用于查找),也就是说,把一个树的diff拍平成了数组的diff

你可以从vNode的dynamicChildren属性里看到动态节点,动态节点的标记是在createVNode时标记的

结构不稳定(比如v-if)和结构数量(v-for)不一样的,会重新创建block节点

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>
<template v-if="flag">
<div>{{name}}</div>
<div>Sakura</div>
</template>
<template v-else>
<div>{{age}}</div>
<div>Snow</div>
</template>
</div>


<div>
<span v-for="item in arr">{{item}}</span>
</div>

编译后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, renderList as _renderList } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", null, [
(_ctx.flag)
? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
_createElementVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */),
_createElementVNode("div", null, "Sakura")
], 64 /* STABLE_FRAGMENT */))
: (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
_createElementVNode("div", null, _toDisplayString(_ctx.age), 1 /* TEXT */),
_createElementVNode("div", null, "Snow")
], 64 /* STABLE_FRAGMENT */))
]),
_createElementVNode("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.arr, (item) => {
return (_openBlock(), _createElementBlock("span", null, _toDisplayString(item), 1 /* TEXT */))
}), 256 /* UNKEYED_FRAGMENT */))
])
], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST

PatchFlag

vue2在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此要将节点的所有属性进行一次比对

PatchFlag可以标记一个节点具体哪些内容更新了,比如说
数字 1:代表节点有动态的 textContent
数字 2:代表元素有动态的 class 绑定
数字 3:代表xxxxx

比如下面这段代码

1
2
3
4
5
<template>
<div class="container" :id="id">
{{name}}
</div>
</template>

编译结果

1
2
3
4
5
6
7
8
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(),
_createBlock("div", {
class: "container",
id: $setup.id
}, _toDisplayString($setup.name), 9 /* TEXT, PROPS */
, ["id"]))
}

9这个数字就标记了div标签里可能变化的内容

Composition Api

setup

setup函数会在所有生命周期函数前执行,这个函数是使用Composition API的入口

1
2
3
4
5
6
7
export default {
setup(props, context){
// 该函数在组件属性被赋值后立即执行,早于所有生命周期钩子函数
// props 是一个对象,包含了所有的组件属性值
// context 是一个对象,提供了组件所需的上下文信息
}
}

context对象的成员

成员 类型 说明
attrs 对象 vue2this.$attrs
slots 对象 vue2this.$slots
emit 方法 vue2this.$emit

数据响应式的创建

API 传入 返回 备注
reactive plain-object 对象代理 深度代理对象中的所有成员
readonly plain-object or proxy 对象代理 只能读取代理对象中的成员,不可修改
ref any { value: ... } 对value的访问是响应式的
如果给value的值是一个对象,
则会通过reactive函数进行代理
如果已经是代理,则直接使用代理
computed function { value: ... } 当读取value值时,
根据情况决定是否要运行函数

应用:

  • 如果想要让一个对象变为响应式数据,可以使用reactiveref
  • 如果想要让一个对象的所有属性只读,使用readonly
  • 如果想要让一个非对象数据变为响应式数据,使用ref
  • 如果想要根据已知的响应式数据得到一个新的响应式数据,使用computed

响应式数据判断

API 含义
isProxy 判断某个数据是否是由reactivereadonly
isReactive 判断某个数据是否是通过reactive创建的
详细:https://v3.vuejs.org/api/basic-reactivity.html#isreactive
isReadonly 判断某个数据是否是通过readonly创建的
isRef 判断某个数据是否是一个ref对象

响应式数据的转化

API 含义
unref 等同于:isRef(val) ? val.value : val
toRef 得到一个响应式对象某个属性的ref格式
toRefs 把一个响应式对象的所有属性转换为ref格式,然后包装到一个plain-object中返回

数据监听

watchEffect

1
2
3
4
5
6
const stop = watchEffect(() => {
// 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
})

// 通过调用stop函数,会停止监听
stop(); // 停止监听

watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 等效于vue2的$watch

// 监听单个数据的变化
const state = reactive({ count: 0 })
watch(() => state.count, (newValue, oldValue) => {
// ...
}, options)

const countRef = ref(0);
watch(countRef, (newValue, oldValue) => {
// ...
}, options)

// 监听多个数据的变化
watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
// ...
});

注意:无论是watchEffect还是watch,当依赖项变化时,回调函数的运行都是异步的(微队列)

应用:除非遇到下面的场景,否则均建议选择watchEffect

  • 不希望回调函数一开始就执行
  • 数据改变时,需要参考旧值
  • 需要监控一些回调函数中不会用到的数据

生命周期函数

vue2 option api vue3 option api vue 3 composition api
beforeCreate beforeCreate 不再需要,代码可直接置于setup中
created created 不再需要,代码可直接置于setup中
beforeMount beforeMount onBeforeMount
mounted mounted onMounted
beforeUpdate beforeUpdate onBeforeUpdate
updated updated onUpdated
beforeDestroy ==改== beforeUnmount onBeforeUnmount
destroyed ==改==unmounted onUnmounted
errorCaptured errorCaptured onErrorCaptured
- ==新==renderTracked onRenderTracked
- ==新==renderTriggered onRenderTriggered

新增钩子函数说明:

钩子函数 参数 执行时机
renderTracked DebuggerEvent 渲染vdom收集到的每一次依赖时
renderTriggered DebuggerEvent 某个依赖变化导致组件重新渲染时

DebuggerEvent:

  • target: 跟踪或触发渲染的对象
  • key: 跟踪或触发渲染的属性
  • type: 跟踪或触发渲染的方式

vue-router的使用

应该很好理解吧orz

1
2
3
4
5
6
7
8
9
10
11
import {useRoute, useRouter} from "vue-router";

export default defineComponent({
name: 'Home',
setup() {
// 相当于this.$router
let router = useRouter();
// 相当于this.$route
let route = useRoute();
}
});

vuex的使用

应该很好理解吧orz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {createStore} from 'vuex'

export default createStore({
state: {
name: "sena"
},
mutations: {
updateName(store, payload) {
store.name = payload;
}
},
actions: {},
modules: {}
})
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
<template>
<div class="home">
<input type="text" v-model="msg">
<button @click="showState">点我获取state</button>
<button @click="initState">初始化state</button>
</div>
</template>

<script lang="ts">
import {defineComponent, ref, watchEffect, reactive} from 'vue';
import {useStore} from "vuex";

export default defineComponent({
name: 'Home',
setup() {
let store = useStore();
let msgRef = ref(store.state.name);
watchEffect(() => {
// msg更新时保存值到vuex中
store.commit("updateName", msgRef.value)
});
watchEffect(() => {
// vuex中的值更新时更新msg
msgRef.value = store.state.name;
})

return {
msg : msgRef,
showState() {
console.log(store.state)
},
initState() {
store.commit("updateName", "")
},
}
}
});
</script>

refer

Vite
JavaScript modules 模块
Vue3 Compiler 优化细节,如何手写高性能渲染函数