前言 最近看了看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后,继续解析并请求依赖的文件,服务器对请求的文件进行编译和处理,返回给浏览器运行,一直重复这个过程
编译过后的index.css
再来看看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 <div id ="app1" > </div > <div id ="app2" > </div > <script > Vue .use (...); Vue .mixin (...); Vue .component (...); new Vue ({ }).$mount("#app1" ) new Vue ({ }).$mount("#app2" ) </script >
1 2 3 4 5 6 7 <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对这两个概念进行了区分,一定程度上避免了可能造成的思维混乱
最后一点就是Vue2的构造函数集成了太多功能,不利于tree shaking,Vue3把这些功能使用普通函数导出,能够充分利用tree shaking优化打包体积,所以Vue3的打包体积是小于Vue2的
使用了proxy来进行数据响应式 优点:
使用proxy比递归对象设置访问器属性要高效
可以观测到对象的新增属性和删除属性
可以观测到数组下标的变化
缺点:
模板的变化 组件允许多个根节点 如图,下图的模板语法现在被支持了,在一个组件里允许了多个根结点而存在
1 2 3 4 5 6 7 8 <template > <div > wow </div > <div > funny </div > </template >
实现原理非常easy
编译后
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 )) }
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 <ChildComponent :value ="pageTitle" @input ="pageTitle = $event" /> <ChildComponent v-model ="pageTitle" /> <ChildComponent :modelValue ="pageTitle" @update:modelValue ="pageTitle = $event" /> <ChildComponent v-model ="pageTitle" />
去掉了.sync
修饰符,它原本的功能由v-model
的参数替代
1 2 3 4 5 6 7 8 9 <ChildComponent :title ="pageTitle" @update:title ="pageTitle = $event" /> <ChildComponent :title.sync ="pageTitle" /> <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 >
渲染结果
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 >
这段代码会报错
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 = _createVNode ("div" , null , "Hello world" , -1 ) 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 = _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 )])])) }
预字符串化在下面两种情况出现:
如果节点没有属性,有连续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 ), _createElementVNode ("div" , null , "Sakura" ) ], 64 )) : (_openBlock (), _createElementBlock (_Fragment, { key : 1 }, [ _createElementVNode ("div" , null , _toDisplayString (_ctx.age ), 1 ), _createElementVNode ("div" , null , "Snow" ) ], 64 )) ]), _createElementVNode ("div" , null , [ (_openBlock (true ), _createElementBlock (_Fragment, null , _renderList (_ctx.arr , (item ) => { return (_openBlock (), _createElementBlock ("span" , null , _toDisplayString (item), 1 )) }), 256 )) ]) ], 64 )) }
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 , ["id" ])) }
9这个数字就标记了div标签里可能变化的内容
Composition Api setup setup函数会在所有生命周期函数前执行,这个函数是使用Composition API的入口
1 2 3 4 5 6 7 export default { setup (props, context ){ } }
context对象的成员
成员
类型
说明
attrs
对象
同vue2
的this.$attrs
slots
对象
同vue2
的this.$slots
emit
方法
同vue2
的this.$emit
数据响应式的创建
API
传入
返回
备注
reactive
plain-object
对象代理
深度代理对象中的所有成员
readonly
plain-object
or proxy
对象代理
只能读取代理对象中的成员,不可修改
ref
any
{ value: ... }
对value的访问是响应式的 如果给value的值是一个对象, 则会通过reactive
函数进行代理 如果已经是代理,则直接使用代理
computed
function
{ value: ... }
当读取value值时, 会根据情况 决定是否要运行函数
应用:
如果想要让一个对象变为响应式数据,可以使用reactive
或ref
如果想要让一个对象的所有属性只读,使用readonly
如果想要让一个非对象数据变为响应式数据,使用ref
如果想要根据已知的响应式数据得到一个新的响应式数据,使用computed
响应式数据判断
响应式数据的转化
API
含义
unref
等同于:isRef(val) ? val.value : val
toRef
得到一个响应式对象某个属性的ref格式
toRefs
把一个响应式对象的所有属性转换为ref格式,然后包装到一个plain-object
中返回
数据监听 watchEffect
1 2 3 4 5 6 const stop = watchEffect (() => { }) stop ();
watch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ( ) { let router = useRouter (); 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 (() => { store.commit ("updateName" , msgRef.value ) }); watchEffect (() => { msgRef.value = store.state .name ; }) return { msg : msgRef, showState ( ) { console .log (store.state ) }, initState ( ) { store.commit ("updateName" , "" ) }, } } }); </script >
refer Vite JavaScript modules 模块 Vue3 Compiler 优化细节,如何手写高性能渲染函数