vue3.0浅析
- 前提
vue2.X中很多内容被保存下来;
vue3.0采用的是typescript进行重构的 - 几大亮点
1.performance:性能比vue2.X快了1.2~2倍。
2.Tree shaking support : 按需编译体积比vue2.X更小。
3.composition API :组合API(类似React Hooks)。
4.Better Typescript support : 更好的支持TS。
5.custom Renderer API :暴露了自定义渲染API。
- Fragment Teleport(Protal),suspense:更先进的组件。
- vue3.0是如何变快的?
1.diff方法的优化:
vue2.X中的虚拟DOM是进行全量的对比
vue3.0新增了静态标记(PathFlage),在于上次虚拟节点进行对比的时候,只进行对比带有path flag的节点,并且可以通过flag的消息得知当前节点要对比的具体内容
// dom元素
<div>Hello World!</div>
<div>Hello World!</div>
<div>Hello World!</div>
<div>{{ msg }}</div>
// 编译后
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, 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", null, "Hello World!"),
_createElementVNode("div", null, "Hello World!"),
_createElementVNode("div", null, "Hello World!"),
_createElementVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) // 1即为动态标记
], 64 /* STABLE_FRAGMENT */))
}
// 如果是动态绑定的val在渲染dom的时候会在 _createElementVNode函数后面追加动态标记1
2.hoiststatic静态提升
vue2.X中元素无论是否参与更新,每次都会创建
vue3.0中对不参与更新的元素只会被创建一次 ,之后每次渲染时不停的复用
// hoiststatic之前
// dom元素
<div>Hello World!</div>
<div>Hello World!</div>
<div>Hello World!</div>
<div>{{ msg }}</div>
// 编译后
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, 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", null, "Hello World!"),
_createElementVNode("div", null, "Hello World!"),
_createElementVNode("div", null, "Hello World!"),
_createElementVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) // 1即为动态标记
], 64 /* STABLE_FRAGMENT */))
}
// hoiststatic 提升后
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "Hello World!", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "Hello World!", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("div", null, "Hello World!", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_hoisted_2,
_hoisted_3,
_createElementVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
// 没有改变的dom不会出发render函数的_createElementVNode方法构建,而是放在外面不断的重复调用。
3.cacheHandlers事件侦听器缓存
默认情况下onclick会被视为动态绑定,所以每次都会追踪其变化,但是因为同一函数,并无追踪变化,直接缓存使用即可
// 事件侦听器缓存之前
<div>
<button @click="buttonCil"></button>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("button", { onClick: _ctx.buttonCil }, null, 8 /* PROPS */, ["onClick"])
]))
}
// 这里会存在一个静态的标记8,因此每次发生改变的时候都会去追踪变化。
// 事件侦听器缓存之后
<div>
<button @click="buttonCil"></button>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.buttonCil && _ctx.buttonCil(...args)))
})
]))
}
// 没有静态标记,因此事件侦听缓存优化 。
4.ssr渲染
当有大量静态内容时,这些内容会被当做纯字符串推进一个buffers里面,即使存在动态绑定,也会通过模板插值嵌入
当静态内容大到一定量时会用_createStaticVNode方法在客户端生成static node这些静态的node会直接innerHtml,不需要创建对象再渲染
- 组合API
注意:在使用组合API前需要先使用vue3.0构建项目(三种方式)
- vue-cil 脚手架工具
vue create 项目名
- webpack 打包工具的创建。
-
vite工具
·目前vite有可能会想成为取代webpack
的工具
·原理是利用ES6中import会发送请求加载文件的特性,拦截请求,做一些预编译,省去了webpack冗长的打包时间
安装:npm install -g create-vite-app
利用vite创建项目:create-vite-app 项目名称
安装依赖及运行项目:cd 项目名称npm install
npm run dev
**
组合API
-
注:在vue3.0搭建的项目中使用
setup
函数,此函数就是组合API的入口函数。如果在此函数中定义变量此函数是无法监听到变量的变化的
使用:
在setup
函数前先导入import {ref} from 'vue'
// ref函数只能监听简单类型的变化,不能监听复杂类型的变化(数组,对象等)
import { ref } from 'vue'
export default{
setup(){
let count=ref(0)
//定义了一个名字为count的变量,且初始值为0,此值发生变化时vue会自动更新UI
function myFn(){
console.log(count.value)
count.value+=1
}
return { count,myFn }
//需要注意的是在组合API中定义方法,变量时要想在外部使用,必须return暴露出去
}
}
-
在组合API中定义方法,不用定义到
methods
中,直接定义使用即可,但是必须return出去
注意:ref函数只能用来监听简单类型的变化,不能监听复杂类型的变化(数组,对象等)
解决:使用reactive
函数来监听复杂数据类型的变化
import {reactive} from 'vue'
export default{
setup(){
let state=reactive({
stue:[{id:1,name:'张三'},{id:2,name:'李四'}]
})
return {state}
}
}
- 对数据和方法进行抽离
import {reactive} from 'vue'
export default{
setup(){
let {state,remStu}=userRemoveStudent()
return {state,remStu}
}
function userRemoveStudent(){
let state=reactive({
stus:[{id:1,name:'张三'},{id:2,name:'李四'}]
})
function remStu(idnex){
state.stus=state.stus.filter((stu,idx)=>idx!==index)
}
return {state,remStu}
}
}
好处:解决了vue2.X中数据和业务逻辑分散的问题,将数据和业务逻辑集中在一起处理,同时也可以将单独的业务逻辑与数据进行拆分成单独的文件,在拆分时注意引入对应的数据处理函数{ref,reactive}
等,在单独的文件中写完业务后抛出暴露,在需要使用的页面进行引入使用即可
例子:
App.vue文件中:
import userRemoveStudent from 'a.js'
import userAddStudent from 'b.js'
export default {
setup(){
let {state,remStu}=userRemoveStudent ()
let {state2,remStu2}=userAddStudent ()
return {state,state2,remStu,remStu2}
}
}
a.js文件中:
import {reactive} from 'vue'
function userRemoveStudent (){
//要进行的操作
}
export default userRemoveStudent
b.js文件中:
import {reactive} from 'vue'
function userAddStudent(){
//要进行的操作
}
export default userAddStudent
好处:大型项目更好维护,方便管理
理解:composition API(即组合API)的本质是将特定的值与方法注入到option API来使用,实际还是vue2.X中的使用方式,只是抽离出来,更方便维护和管理,解决了数据和方法分离的操作,只是将组合API中的return的数据注入到option API中
- setup函数的执行时机及注意点
- setup函数是在beforeCreate与created之间执行的
beforeCreate // 组件刚被创建出来,组件中的data,methods还没有初始化好
setup
created // 组件刚被创建出来,组件中的data,methods已经初始化好了
- 在setup函数中不能使用data和methods的
原因:setup函数执行的时候是在create之前的,因此没有data和methdos的
注意点:vue为了防止错误的使用,直接将setup函数中的this指向修改为undefined - setup函数只能是一个同步的函数
-
reactive定义及注意点
定义:reactive是vue3中提供的实现响应式数据的方法,
- 在vue 2.X的版本中响应式数据是通过defineproperty实现的。
- 在vue 3.X的版本中响应式数据是通过ES6中的proxy来实现的。
注意点:
reactive参数必须是一个对象(Json/arr),如果不传对象无法实现数据的响应式。
<div id="app">
{{ state.age }}
<button @cilck="myFun"></button>
</div>
Js:
import reactive from 'vue'
setup() {
let state = reactive({
age: 123
})
function myFun() {
state.age = 888
console.log('state', state)
}
return { myFun, state }
}
// proxy { age: 123 }
如果给reactive传递了其他对象(时间对象new Date()),默认情况下修改了对象,页面无法自动更新;如果想实现数据的更新,可以通过重新赋值的方式
<div id="app">
{{ state.time }}
<button @cilck="myFun"></button>
</div>
Js:
import reactive from 'vue'
setup() {
let state = reactive({
time: new Date()
})
function myFun() {
// 点击按钮天数增加1天
const newTime = new Date(state.time.getTime())
newTime.setDate(state.time.getDate() + 1)
console.log(state.time)
}
return { myFun, state }
}
- ref的定义、本质、注意点
- 定义:ref与reactive一样,用来实现响应式数据数据的方法。由于reactive必须传递一个对象,如果想让某个变量实现响应式数据会很麻烦,故vue3.X提供 了ref方法,实现对简单值的监听。
- 本质:底层的本质还是reactive,系统会将ref传入的值转为ref(XXX)--->reactive({value:XXX})
- 注意点:vue中使用ref的值不能通过value来取;JS中使用ref的值必须同过value来取。
<div>{{ age }}</div>
<!-- 如果值是通过ref创建的话,在<template>中使用是不需要加.value的 -->
<button @cilck="myFn"></button>
Js:
import ref from 'vue'
setup() {
let age = ref(18) // =>> reactive({ value: 18 })
myFn() {
// age = 666 如果直接对age进行数据的更改操作页面是不变的,因为其底层的原理是转换为reactive的形式进行数据的操作处理。
age.value = 666 // 我们是要对age的value值进行更改,因此需要获取到age下的value参数。
console.log('age', age)
}
}
-
ref与reactive区别
页面获取值的情况:
如果在template里使用的是ref的数据,vue会自动添加.value;如果在template里面使用的是reactive的数据,vue不会自动添加.value。
注意
vue是如何判断当前的数据是否为ref类型的数据??通过--v-ref来判断的,如果存在私有的属性,且取值为true那就代表是一个ref类型的。
import ref from 'vue'
import reactive from 'vue'
setup() {
let age = ref(18) // =>> reactive({ value: 18 })
let ages = reactive({ value: 18 })
myFn() {
age.value = 666 // 我们是要对age的value值进行更改,因此需要获取到age下的value参数。
console.log('age', age)
// RefImpl { _rawValue: 666, shallow: false, _v_isRef: true, _value: 666 }
console.log('ages', ages)
// proxy { value: 18 }
}
}
递归监听
默认情况下,无论是ref还是reactive都是递归监听。
问题:如果数据量非常大,非常消耗性能。
原因:做了一个递归的操作,把每一层的数据包装成一个proxy对象,因此会消耗性能。非递归监听
非递归监听只会对第一层数据生效,第一层会成为proxy对象。
使用:
ref:import {shallowRef} from 'vue'
reactive:import {shallowReactive} from 'vue'
注意:如果是通过shallowRef
建立的数据,那么vue监听的是.value的变化,并不是第一层的变化。
如果仅想修改内层中的某条数据,可以使用triggerRef
方法来实现:
import {shallowRef,triggerRef} from 'vue'
//修改数据后调用,修改主动更新界面
stup(){
let state=shallowRef({
a:'a',
gf:{
b:'b',
f:{
c:'c',
s:{
d:'d',
}
}
}
})
function myfn (){
state.value.gf.f.s.d='4'
triggerRef(state)
//这样就会根据修改后的state来更新界面
}
}
注意点:vue3.X提供了triggerRef的方法,没有提供triggerReactive的方法,因此如果是Reactive的数据是无法除法数据更新的
-
递归监听与非递归监听的应用场景
一般情况下只是用ref与reactive即可,当我们需要监听的数据量比较大时才使用shallowRef或者shallowReactive。
11.shallowRef与shallowReactive的本质
原理与ref的本质是一样的,(refclo)-->recvtive({value:10})
shallowRef的本质是:shallowRef(10)-->shallowReactive({value:10})
如果是shallowRef创建的数据,它监听的是.value的变化,因为其本质上value才是第一层。
Vue3.x中的toRaw方法
例子:
import {reactive,toRaw} from 'vue'
setup(){
let obj={name:"张三",age:19}
let state=reactive(obj)
//obj==state?false 不相等
//两者是引用关系,state本质是proxy对象,此对象中引用了obj。
// ------------------------ -------ooffect obj-------
// | state | | name:'ls' |
// | xxx:ooffect | | age:10 |
// ------------------------ ------------------------------
function myfn(){
obj.name:'Ls'
}
return {state,myfn}
}
//此时修改obj的值,在内存中的值是修改后的,但是页面没有发生改变,无法触发。
toRow方法
从ref或者reactive中得到原始数据。
作用:
做一些不想被监听的事情(提升性能)
ref和reactive的数据特点是每次修改都会被追踪,都会更新UI界面,这样会非常消耗性能的,但是如果有些操作不需要被跟踪,无需更新UI界面,可以使用toRaw方法,拿到原始数据,对原始数据进行更改,提升性能。
例子:
let obj={name:"测试",age:12}
let state=reactive(obj)
let obj2=toRaw(state)
//obj==obj2 ??? ---->true相等
这样的做法是为了方便在任何时候拿到原始数据。
注意点:
如果想通过toRaw拿到ref类型的原始数据(创建时传入的数据)必须告诉toRaw方法要获取的值是.value的值。在经过vue处理后,.value才是但是创建时传入的原始数据。
例子:
import {ref,toRaw} from 'vue'
setup(){
let obj={name:"zhangsan",age:10}
let state=ref(obj)
let obj2=toRaw(state.vale)
}
markRaw方法
如果说某一数据永远不想被追踪,使用markRaw方法
例子:
<div>{{ state.name }}</div>
<button @cilck="myFn"></button>
import { reactive, markRaw } from 'vue'
setup() {
let obj = { name: 'zhangsan', age: 10 }
obj = markRaw(obj) // 这样这个obj以后就不会被追踪到了,页面的值也不会变
let state = reactive(obj)
function myFn() {
state.name = '里斯'
}
return { myFn, state }
}
toRef方法
与ref方法相似,都是创建响应式的数据的
ref方法修改数据变成响应式的
结论:如果利用ref将某个对象中的属性变成响应式数据,我们修改的数据是不会影响到原始数据的
<div>{{ state.name }}</div>
<button @cilck="myFn"></button>
import { ref } from 'vue'
setup() {
let obj = { name: 'zhangsan' } // 原始数据
let state = ref(obj.name) // 响应式数据
function myFn() {
state.value = '里斯'
console.log('obj', obj) // { name: 'zhangsan' }
console.log('state', state) // RefImpl { _object: 里斯, _key: undefined, __v_isRef: true }
}
return { myFn, state }
}
使用toRef方法变更响应式数据
<div>{{ state.name }}</div>
<button @cilck="myFn"></button>
import { ref } from 'vue'
setup() {
let obj = { name: 'zhangsan' } // 原始数据
let state = toRef(obj, 'name') // toRef方法中使用obj.name会有警告,使用toRef(obj, 'name')
function myFn() {
state.value = '里斯'
console.log('obj', obj) // { name: '里斯' }
console.log('state', state) // RefImpl { _object: 里斯, _key: undefined, __v_isRef: true }
}
return { myFn, state }
}
结论: 如果利用toRef将某个对象中的属性变成响应式的数据,我们在修改响应式数据时会修改原始数据的。但是如果相应的数据是通过toRef创建的,那么修改了 数据并不会触发UI界面的更新的。
ref与toRef的区别:
ref =>>复制,修改响应式数据不会影响以前的数据。
toRef =>>引用,修改响应式数据会影响以前的数据。
ref =>>数据发生改,变界面会自动触发更新。
toRef =>>数据发生改变,界面不会自动更新。
toRef应用场景:如果想让响应式数据和以前的数据关联起来,并且更新响应式数据之后不更新UI,可以使用,toRef
toRefs方法
使用:将对象中的多个数据变成响应式的
<div>{{ state }}</div>
<div>{{ age }}</div>
<button @cilck="myFn"></button>
import { ref } from 'vue'
setup() {
let obj = { name: 'zhangsan', age: 18 } // 原始数据
let state = toRef(obj, 'name') // toRef不可以这样子使用toRef(obj, 'name', 'age') 会报警告
let age = toRef(obj, 'age')
function myFn() {
state.value = '里斯'
age.value = 222
}
return { myFn, state, age }
}
toRefs使用:
<div>{{ state }}</div>
<button @cilck="myFn"></button>
import { ref } from 'vue'
setup() {
let obj = { name: 'zhangsan', age: 18 } // 原始数据
let state = toRefs(obj)
function myFn() {
state.name.value = '里斯'
state.age.value = 222
console.log('数据', obj, state)
// 原始数据: { name: 里斯, age: 222 }
// 响应式的数据: { { name: { _object: { name: 里斯, age: 222 }, _key: 'name', __v_isRef: true }}, { name: { _object: { name: 里斯, age: 222 }, _key: 'age', __v_isRef: true }}}
}
return { myFn, state }
}
注释:在企业开发中使用的最多的就是ref与reactive这两个方法,剩余这些方法主要是考虑到性能优化方面的。
customRef方法
这个方法是自定义一个ref,返回一个ref对象,可以显示的控制依赖追踪和触发响应
为什么要自定义一个ref方法?
我们在请求数据的时候是一个异步的操作,但是在我们的setup中不能使用异步的操作,因此customRef可以解决我们异步获取数据页面渲染的问题。
// 通过ref的方式对数据进行响应式处理(点击按钮进行累加操作)
<div>{{ age }}</div>
<button @cilck="myFn"></button>
import { ref } from 'vue'
export default{
setup() {
let age = ref(18)
function myFn() {
age += age
}
return { myFn, age }
}
}
// customRef方式
<div>{{ age }}</div>
<button @cilck="myFn"></button>
import { customRef } from 'vue'
function myRef(value) {
return customRef((track, trigger) => { // 这个方法是一个箭头函数
return { // return一个对象出去在里面触发get()和set()方法
get() {
track() // 告诉vue这个数据是需要追踪变化的
console.log('get', value)
return value
},
set(newValue) {
console.log('set', newValue)
value = newValue
trigger() // 告诉vue触发界面更新
}
}
})
}
export default{
setup() {
let age = myRef(18)
function myFn() {
age += age
}
return { myFn, age }
}
}
使用customRef获取数据动态渲染到UI页面
// 模拟后端返回的数据新建一个data.json的文件
// data.json
[
{
id: 1,
name: '张三'
},
{
id: 2,
name: '李四'
},
{
id: 3,
name: '王五'
}
]
// 在vue中使用customRef
<ul>
<li v-for="item in dataAll" :key="item.id">{{ item.name }}</li>
</ul>
import { customRef } from 'vue'
function myRef(value) {
return customRef((track, trigger) => { // 这个方法是一个箭头函数
fech(value)
.then((res) => { return res.json() })
.then((data) => { value = data; trigger() })
.catch((err) => { console.log(err) })
return { // return一个对象出去在里面触发get()和set()方法
get() {
track() // 告诉vue这个数据是需要追踪变化的
console.log('get', value)
return value
},
set(newValue) {
console.log('set', newValue)
value = newValue
trigger() // 告诉vue触发界面更新
}
}
})
}
export default{
setup() {
let dataAll = customRef('./data.json')
return { dataAll }
}
}
ref方法获取元素
在vue2.X的版本中,我们通常使用this.$ref.XXX获取元素,但是在vue3中我们无法通过这种方式获取到,因为vue3中setup方法是在beforeCreate和Created生命周期之间执行的方法,此时dom还未创建出来,因此无法使用。
<div ref="box"></div>
Js:
import { ref, onMounted } from 'vue'
setup() {
let box = ref(null)
onMounted(() => {
console.log('box', box.value) // 因此我们在生命周期的这个方法中使用箭头函数即可获取元素
})
return { box }
}
readonly、shallowReadonly方法
readonly将数据处理为只读(递归的只读,不管数据里面嵌套了多少层都会处理为只读的)
shallowReadonly将数据处理为只读(只读,不是递归,是指对第一层的数据)
// readonly是用这个方法 处理数据的时候不管里面嵌套了多少都会被处理成只读的,是递归只读
// shallowReadonly方法是只将第一层的数据处理为只读的
<div>{{ state.name }}</div>
<div>{{ state.age }}</div>
<div>{{ state.title.height }}</div>
<div>{{ shallstate.name }}</div>
<div>{{ shallstate.age }}</div>
<div>{{ shallstate.title.height }}</div>
<button @cilck="myFn">按钮</button>
Js:
import { readonly, shallowReadonly } from 'vue'
setup() {
let state = readonly({ name: '章三', age: '11', title: { height: 180 } })
let shallstate = shallowReadonly({ name: '章三', age: '11', title: { height: 180 } })
function myFn() {
shallstate.name = '王五' // 第一层的数据不会变的
shallstate.age = '1' // 第一层的数据不会变的
shallstate.title.height = '130' // 第二层的数据改变了
state.name = '李四', // 数据不变
state.age = '22', // 数据不变
state.title.height = '150', // 数据不变
}
return { state, myFn }
}
还有一种处理为只读数据的方式就是使用const进行赋值的操作,但如果const声明的是一个对象或者复杂类型的数据时就可以对其进行赋值的操作的。
const与readonly的区别:
- const是赋值保护,不能重新赋值。
- readonly是属性保护,不能重新赋值
vue3响应式数据的本质
- 在vue2中是通过defineProperty来实现响应式数据的。
- 在vue3中是通过proxy来实现响应式数据的。
将一个数据包装成proxy对象来实现的
手写proxy实现数据的响应式
let obj = { name: '章三‘, age: 12 }
let state = new Proxy(obj, {
get(obj, key) {
console.log('obj, key', obj, key) // obj =>> { name: '章三‘, age: 12 } key =>> name
return obj[key]
},
set(obj, key, newValue){ // 第一个参数是当前操作的对象,第二个参数是操作的属性,第三个参数是新赋值的参数
console.log('set', obj, key, newValue) // obj =>> { name: '章三‘, age: 12 } key =>> name newValue =>> 李四
obj[key] = newValue
return true
// 在set中一定要return true,因为Proxy如果处理的数据为多次操作的话就会导致异常(定一个数组,想数组中追加一个值,不只需要追加向数组追加值,还要将数组的长度增加因此需要return true来表示上一次的操作完成了)。
}
})
state.name = '李四'
console.log('state', state) // { name: '李四‘, age: 12 }