# 前言

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

# vite

# vite 介绍和用法

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

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

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

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

$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

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

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

├── 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 启动了一个本地代理服务器,没有进行任何的编译和打包操作

[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 文件

<!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

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() 来创建,为什么要这么做呢,来看下面的例子

<!-- 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>
<!-- 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 也难以完全支持

# 模板的变化

# 组件允许多个根节点

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

<template>
    <div>
        wow
    </div>
    <div>
        funny
    </div>
</template>

实现原理非常 easy

<div></div>
<div></div>

编译后

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
<!-- 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 的参数替代
<!-- 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" />

详细写法

<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>
<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

<template>
    <div class="about">
        <h1>This is an about page</h1>
        <ul>
            <li v-for="(item, index) in items" v-if="item.flag"></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

<template>
    <div>
        <p v-for="(item, index) in items" v-if="item.flag"></p>
    </div>
</template>
<script>
export default {
    data() {
        return {
            items : [
                {
                    flag : false,
                    value : "rua"
                },
                {
                    flag : true,
                    value : "qaq"
                }
            ]
        }
    }
}
</script>

这段代码会报错

file

# Vue 效率提升

# 静态节点提升

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

<template>
    <div id="app">
        <div>Hello world</div>
    </div>
</template>

编译结果

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 来创建真实节点

<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></span>
        </div>
    </div>
</template>
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 个及以上的节点是有属性绑定的节点

# 事件处理函数缓存

对下面的模板

<template>
    <div class="container">
        <button @click="handleClick"></button>
    </div>
</template>

编译结果

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 的编译结果

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 节点

比如

<div>
  <template v-if="flag">
    <div></div>
    <div>Sakura</div>
  </template>
  <template v-else>
    <div></div>
    <div>Snow</div>
  </template>
</div>
<div>
  <span v-for="item in arr"></span>
</div>

编译后

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

比如下面这段代码

<template>
    <div class="container" :id="id">
        
    </div>
</template>

编译结果

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 的入口

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

context 对象的成员

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

# 数据响应式的创建

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

应用:

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

# 响应式数据判断

API含义
isProxy判断某个数据是否是由 reactivereadonly
isReactive判断某个数据是否是通过 reactive 创建的 <br /> 详细: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

const stop = watchEffect(() => {
  // 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
})
// 通过调用 stop 函数,会停止监听
stop(); // 停止监听

watch

// 等效于 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 apivue3 option apivue 3 composition api
beforeCreatebeforeCreate不再需要,代码可直接置于 setup 中
createdcreated不再需要,代码可直接置于 setup 中
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroy beforeUnmountonBeforeUnmount
destroyed unmountedonUnmounted
errorCapturederrorCapturedonErrorCaptured
- renderTrackedonRenderTracked
- renderTriggeredonRenderTriggered

新增钩子函数说明:

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

DebuggerEvent:

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

# vue-router 的使用

应该很好理解吧 orz

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

# vuex 的使用

应该很好理解吧 orz

import {createStore} from 'vuex'
export default createStore({
    state: {
        name: "sena"
    },
    mutations: {
        updateName(store, payload) {
            store.name = payload;
        }
    },
    actions: {},
    modules: {}
})
<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 优化细节,如何手写高性能渲染函数

更新于 阅读次数