Vue3.0 亮点
Performance
:性能比Vue 2.x快1.2~2倍Tree shaking support
:按需编译,体积比Vue2.x更小Composition API
: 组合API(类似React Hooks)Better TypeScript support
:更好的 Ts 支持Custom Renderer API
:暴露了自定义渲染APIFragment, Teleport(Protal), Suspense
:更先进的组件
Vue3.0是如何变快的
diff算法优化
https://vue-next-template-explorer.netlify.app/
+ Vue2中的虚拟dom是进行全量的对比,内存中新建一个虚拟dom树,每一个节点去对比,然后更新。
+ Vue3新增了静态标记(PatchFlag),在与上次虚拟节点进行对比时候,只对比带有patch flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容。_toDisplayString方法告诉算法具体要对比的内容,后面的就是 PatchFlag。
// diff算法内部原理
<div>
<p>第一行</p>
<p>{{msg}}}</p>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "第一行"),
_createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1 /* TEXT */)
]))
}
// 附录: PatchFlags
export const enum PatchFlags {
TEXT = 1,// 动态文本节点
CLASS = 1 << 1, // 2 // 动态 class
STYLE = 1 << 2, // 4 // 动态 style
PROPS = 1 << 3, // 8 // 动态属性,但不包含类名和样式
FULL_PROPS = 1 << 4, // 16 // 具有动态 key 属性,当 key 改变时,需要进行完整的 diff 比较。
HYDRATE_EVENTS = 1 << 5, // 32 // 带有监听事件的节点
STABLE_FRAGMENT = 1 << 6, // 64 // 一个不会改变子节点顺序的 fragment
KEYED_FRAGMENT = 1 << 7, // 128 // 带有 key 属性的 fragment 或部分子字节有 key
UNKEYED_FRAGMENT = 1 << 8, // 256 // 子节点没有 key 的 fragment
NEED_PATCH = 1 << 9, // 512 // 一个节点只会进行非 props 比较
DYNAMIC_SLOTS = 1 << 10, // 1024 // 动态 slot
HOISTED = -1, // 静态节点
// 指示在 diff 过程应该要退出优化模式
BAIL = -2
}
hoistStatic静态提升
+ Vue2中无论元素是否参与更新, 每次都会重新创建, 然后再渲染
+ Vue3中对于不参与更新的元素, 会做静态提升, 只会被创建一次, 在渲染时直接复用即可
<div>
<p>第一行</p>
<p>{{msg}}}</p>
</div>
// 静态提升之前:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "第一行"),
_createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1 /* TEXT */)
]))
}
// 静态提升之后:
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "第一行", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_createVNode("p", null, _toDisplayString(_ctx.msg) + "}", 1 /* TEXT */)
]))
}
cacheHandlers事件侦听器缓存
+ 默认情况下绑定的click事件会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可。
<div>
<button @click="onClick">按钮</button>
</div>
// 开启事件监听缓存之前:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, ["onClick"])
]))
}
// 开启事件监听缓存之后:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "按钮")
]))
}
// 注意点:转换之后的代码,观察有没有静态标记.在Vue3的 diff 算法中,只有有静态标记的才会进行比较,才会进行追踪.
ssr渲染
+ 当有大量静态的内容时候,这些内容会被当做纯字符串推进一个buffer里面,即使存在动态的绑定,会通过模板插值嵌入进去。这样会比通过虚拟dom来渲染的快上很多很多。
+ 当静态内容大到一定量级时候,会用_createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染。
Vue3.0-快速上手
创建Vue3的3种方式
Vue-CLI
Webpack
Vite
什么是Vite
Vite
是Vue
作者开发的一款意图取代webpack
的工具,其实现原理是利用ES6
的import
会发送请求去加载文件的特性,拦截这些请求,做一些预编译,省去webpack
冗长的打包时间。
安装Vite
npm install -g create-vite-app
利用Vite创建Vue3项目 create-vite-app projectName
// vite vue3 中 main.js 使用vue的区别
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
/*
new Vue({
el: '#app',
store: store,
router: router,
render: c => c(App)
})
new Vue({
store: store,
router: router,
render: c => c(App)
}).$mount("#app");
*/
组合API
基本用法
import { ref } from 'vue';
export default {
name: 'App',
setup(){ // setup函数是组合API的入口函数
// 定义了一个名称叫做count变量, 这个变量的初始值是0;这个变量发生改变之后, Vue会自动更新UI
let count = ref(0); // 相当于 let count = 0;
// 在组合API中, 如果想定义方法, 不用定义到 methods 中, 直接定义即可
function myFn() {
count.value += 1; // count是一个对象
}
// 在组合API中定义的变量/方法, 要想在外界使用, 必须通过return {xxx, xxx}暴露出去
return{count, myFn}
}
}
抽取
// 解决 Vue2 中业务逻辑和数据分散的问题
import {reactive} from 'vue';
export default {
name: 'App',
setup() {
/*
let state = reactive({ // ref函数只能监听简单类型的变化, 不能监听复杂类型的变化(对象/数组)
stus:[
{id:1, name:'zs', age:10},
{id:2, name:'ls', age:20},
]
});
function remStu(index) {
state.stus = state.stus.filter((stu, idx) => idx !== index);
}
*/
let { state, remStu } = useRemoveStudent();
return { state1, remStu }
}
}
function useRemoveStudent() { // 把业务逻辑和数据放在一起
let state = reactive({
stus:[
{id:1, name:'zs', age:10},
{id:2, name:'ls', age:20},
]
});
function remStu(index) {
state.stus = state.stus.filter((stu, idx) => idx !== index);
}
return { state, remStu };
}
组合
方法和数据可以放在一起,提取到单独的js文件。
Composition API
和 Option API
(Vue2.X定义数据方法的风格)
可以混合使用。
Composition API
本质 (组合API/注入API)
,会将组合的数据和方法提取后变成Option API
的方式。
setup函数执行时机和注意点
1.setup执行时机
setup
beforeCreate: 表示组件刚刚被创建出来, 组件的data和methods还没有初始化好
Created : 表示组件刚刚被创建出来, 并且组件的data和methods已经初始化好
2.setup注意点
- 由于在执行setup函数的时候, 还没有执行Created生命周期方法, 所以在setup函数中, 是无法使用data和methods
- 由于我们不能在setup函数中使用data和methods,所以Vue为了避免我们错误的使用,它直接将setup函数中this修改成了undefined
- setup函数只能是同步的不能是异步的
// Composition API 和 Option API 可以混用
data: function(){
return {
name: 'lnj',
}
},
methods:{
myFn1(){
alert('abc');
},
},
setup() {
let age = ref(18);
// console.log(this); // undefined
return {age}
}
reactive
reactive
是Vue3
中提供的实现响应式数据的方法,在Vue2
中响应式数据是通过defineProperty
来实现的,而在Vue3
中响应式数据是通过ES6
的Proxy
来实现的。
reactive
注意点:
reactive
参数必须是对象(json/arr)
,如果给reactive
传递了其它对象( eg:Date )
,默认情况下修改对象,界面不会自动更新,如果想更新,可以通过重新赋值的方式。
创建一个响应式数据本质:就是将传入的数据包装成一个Proxy对象
let state = reactive(123);
let state = reactive({age: 123});
let state = reactive([1, 3, 5]);
ref
ref
和reactive
一样,也是用来实现响应式数据的方法,由于reactive
必须传递一个对象,如果只想让某个变量实现响应式的时候会非常麻烦,所以Vue3就提供了ref
方法,实现对简单值的监听。
ref本质:
ref
底层的本质其实还是reactive
,系统会自动根据我们给ref
传入的值将它转换成,ref(xx) -> reactive({value:xx})
。
let state = reactive({
age: 18
})
// 等价于
let age = ref(18);
// 修改时
function myFn() {
// age = 666;
age.value = 666; // 必须这样修改 <p>{{age}}</p>
}
ref注意点:
在Vue中使用ref
的值不用通过value
获取,因为Vue会自动给我们添加.value
,在JS中使用ref
的值必须通过value
获取。
ref获取渲染界面元素
<div ref="box">我是div</div>
import {ref, onMounted} from 'vue';
setup() {
let box = ref(null); // reactive({value: null})
onMounted(()=>{
console.log('onMounted',box.value); // 等同于vue2中的 mounted, 拿到界面元素
});
console.log(box.value); // null 先执行
}
ref和reactive区别
- 如果在
template
里使用的是ref
类型的数据,那么Vue会自动帮我们添加.value
,如果在template
里使用的是reactive
类型的数据,那么Vue不会自动帮我们添加.value
。 - Vue在解析数据之前,会自动判断这个数据是否是
ref
类型的,如果是就自动添加.value
,如果不是就不自动添加.value
。 - Vue是通过当前数据的
__v_ref
来判断的当前的数据是否是ref
类型的,如果有这个私有的属性, 并且取值为true, 那么就代表是一个ref
类型的数据
import {reactive, ref, isRef, isReactive} from 'vue';
let age = ref(18); let age = reactive({value: 18});
isRef(age) isReactive(age)
递归监听
默认情况下,无论是通过ref还是reactive都是递归监听。
递归监听存在的问题:
如果数据量比较大, 非常消耗性能。
非递归监听
shallowRef
/ shallowReactive
如果是shallowRef
类型数据(针对ref)
, 可以通过triggerRef
来触发监听对象某一层数据的变化,默认只能监听到 .value
的第一层变化。
如果是shallowReactive
类型数据(针对reactive)
, 只会监听数据的第一层变化。
// 注意点: Vue3只提供了triggerRef方法, 没有提供triggerReactive方法, 所以如果是reactive类型的数据, 那么是无法主动触发界面更新的.
let state = shallowRef({
a:'a',
gf:{
b:'b',
}
});
state.value = {
a:'1',
gf:{
b:'2',
}
}
state.value.gf.b = '4'; // 修改第二层的数据
triggerRef(state); // 调用方法更新
// 注意点: 如果是通过shallowRef创建数据, 那么Vue监听的是 .value 的变化, 并不是第一层的变化.
console.log(state); // shallowReactive将第一层 包装成proxy对象,可以监听到。对象里key对应的值还是对象监听不到
console.log(state.value); // shallowRef 包装成proxy对象,可以监听到
一般情况下我们使用 ref 和 reactive 即可,只有在需要监听的数据量比较大的时候,我们才使用shallowRef
/shallowReactive
shallowRef本质
shallowRef
底层是 shallowReactive
实现的,shallowRef(10) -> shallowReactive({value: 10})
;所以如果是通过shallowRef
创建的数据,它监听的是.value
的变化,因为底层本质上value
才是第一层。
toRow
从Reactive
或 Ref
中得到原始数据的方法。toRaw
作用是做一些不想被监听的事情(提升性能)。
ref/reactive
数据类型,每次修改都会被追踪,都会更新UI界面,但是这样其实是非常消耗性能的;所以如果我们有一些操作不需要追踪,不需要更新UI界面,那么这个时候我们就可以通过toRaw方法拿到它的原始数据,对原始数据进行修改,这样就不会被追踪,这样就不会更新UI界面,这样性能就好了。
import {reactive, toRaw} from 'vue';
let obj = {name:'zs', age:18};
let state = reactive(obj);
let obj2 = toRaw(state); // obj !== state, obj === obj2
// state 和 obj 是引用关系, state的本质是一个Proxy对象, 在这个Proxy对象中引用了obj。
ref本质
: ref(obj) -> reactive({value: obj})
,如果想通过toRaw拿到ref类型的原始数据(创建时传入的那个数据),那么就必须明确的告诉toRaw方法, 要获取的是.value的值。
import {ref, toRaw} from 'vue';
let obj = {name:'zs', age:18};
let state = ref(obj);
let obj2 = toRaw(state.value);
markRaw
将数据标记为永远不能追踪的数据,一般在编写自己的第三方库时使用。这样就不会被Vue监听到。
import {reactive, markRaw} from 'vue';
let obj = {name: 'zs', age: 18};
obj = markRaw(obj);
let state = reactive(obj); // 失效,修改obj也不会触发更新
toRef
创建一个ref
类型数据,并和以前的数据关联。
ref
和toRef
区别:
ref - 复制,创建出来的数据和以前无关(复制); 数据变化会自动更新界面
toRef - 引用,创建出来的数据和以前的有关(引用); 数据变化不会自动更新界面
如果利用ref
将某一个对象中的属性变成响应式的数据,我们修改响应式的数据是不会影响到原始数据的。
let obj = {name:'zs'};
let state = ref(obj.name); // 相当于 reactive({value: zs})
let state = toRef(obj, 'name');
state.value = 'ls'; // 修改
toRefs
批量创建ref
类型数据, 并和以前数据关联。
toRef
和ref
区别:
toRef - 创建一个ref类型数据, 并和以前的数据关联
toRefs - 批量创建ref类型数据, 并和以前数据关联
let obj = {name:'zs', age:18};
let name = toRef(obj, 'name');
let age = toRef(obj, 'age');
// 等价于
let state = toRefs(obj);
state.name.value = 'ls'; // 修改
state.age.value = 666;
customRef 异步获取监听数据
返回一个ref
对象,可以显式地控制依赖追踪和触发响应,用来自己实现ref
功能。
<template>
<div>
<p>{{age}}</p>
</div>
</template>
import {ref, customRef} from 'vue';
function myRef(value) {
return customRef((track, trigger)=>{
return {
get(){ // 界面渲染会执行一次get方法
track(); // 告诉Vue这个数据是需要追踪变化的
return value;
},
set(newValue){ // 修改值会执行一次set法
value = newValue;
trigger(); // 告诉Vue触发界面更新
}
}
});
}
// let age = ref(18); // reactive({value: 18})
let age = myRef(18);
age.value += 1;
利用customRef
实现异步获取数据更新界面,setup() 不能执行异步代码,所以可以通过这种方式。
import {ref, customRef} from 'vue';
function myRef(value) {
return customRef((track, trigger) => {
fetch(value) // 利用es6的fetch获取json数据
.then((res)=>{
return res.json();
})
.then((data)=>{
console.log(data);
value = data;
trigger();
})
.catch((err)=>{
console.log(err);
})
return {
get(){
track(); // 告诉Vue这个数据是需要追踪变化的
// 注意点: 不能在get方法中发送网络请求
// 渲染界面 -> 调用get -> 发送网络请求
// 保存数据 -> 更新界面 -> 调用get
return value;
},
set(newValue){
value = newValue;
trigger(); // 告诉Vue触发界面更新
}
}
});
}
// setup() 方法中
let state = myRef('../public/data.json');
readonly & shallowReadonly
readonly
用于创建一个只读的数据,并且是递归只读。shallowReadonly
用于创建一个只读的数据,但是不是递归只读的。
import {readonly, isReadonly, shallowReadonly} from 'vue'
let state = readonly({name:'zs', attr:{age:18, height: 1.88}});
state.name = 'ls'; // 修改失败
let state = shallowReadonly({name:'lnj', attr:{age:18, height: 1.88}}); // 只能保护第一层,也就是na me,attr一级
state.name = 'ls'; // 修改失败
state.attr.age = 666; // 修改成功
console.log(isReadonly(state)); // true
const value = {name:'zs', age:123};
value.name = 'ls'; // 修改成功
💡 const
和readonly
区别:
const
赋值保护, 不能给变量重新赋值readonly
属性保护, 不能给属性重新赋值
Vue3响应式数据本质
在Vue2.x
中是通过defineProperty
来实现响应式数据的,在Vue3.x
中是通过Proxy
来实现响应式数据的。
let obj = {name:'zs', age:18};
let state = new Proxy(obj, {
get(obj, key){
console.log(obj, key); // { name: 'zs', age: 18 } name
return obj[key];
},
set(obj, key, value){
console.log(obj, key, value); // { name: 'zs', age: 18 } name ls
obj[key] = value;
console.log('更新UI界面');
}
});
// console.log(state.name); // lnj
state.name = 'ls';
console.log(state); // { name: 'ls', age: 18 }
Proxy注意点
set方法必须通过返回值告诉Proxy此次操作是否成功,proxy才会继续向下执行后面的set工作。
let arr = [1, 3, 5]; // [1, 3, 5, 7]
let state = new Proxy(arr, {
get(obj, key){
console.log(obj, key);
return obj[key];
},
set(obj, key, value){
// [ 1, 3, 5 ] 3 7
// [ 1, 3, 5, 7 ] length 4
console.log(obj, key, value); // [ 1, 3, 5 ] 3 7 key是3,表示要设置的是索引为3的元素
obj[key] = value;
console.log('更新UI界面');
return true; // 返回true告诉proxy上一步push(7)的操作成功,接下来会继续执行set方法,修改数组的length [ 1, 3, 5, 7 ] length 4
}
});
// console.log(state[1]); // [ 1, 3, 5 ] 1
state.push(7);
手写shallowRef/shallowReactive
function shallowRef(val) {
return shallowReactive({value:val}); // 本质上监听的是value
}
function shallowReactive(obj) {
return new Proxy(obj, {
get(obj, key){
return obj[key];
},
set(obj, key, val){
obj[key] = val;
console.log('更新UI界面');
return true;
}
})
}
let obj = { a:'a', gf:{b:'b'} };
/*
let state = shallowReactive(obj);
state.a = '1'; // 更新UI界面
state.gf.b = '2'; // 无效
*/
let state = shallowRef(obj);
// state.value.a = '1'; // 无效,原因就是shallowRef只能监听value
// state.value.gf.b = '2';
state.value = obj = { a:'a', gf:{b:'b'} };
// state.value.a = '1'; // 更新UI界面
// state.value.gf.b = '2'; // 无效
手写ref/reactive
function reactive(obj) {
if(typeof obj === 'object') {
if(obj instanceof Array) {
// 如果是一个数组,那么取出数组中的每一个元素,判断每一个元素是否又是一个对象,
// 如果是一个对象,那么也需要包装成Proxy
obj.forEach((item, index) => {
if(typeof item === 'object') {
obj[index] = reactive(item)
}
})
} else {
// 如果是一个对象,那么取出对象属性的值,判断对象属性的取值是否又是一个对象,
// 如果又是一个对象,那么也需要包装成Proxy
for(let key in obj) {
let item = obj[key]
if(typeof item === 'object') {
obj[key] = reactive(item)
}
}
}
} else {
console.log(`${obj} is not object`)
}
return new Proxy(obj, {
get(obj, key){
return obj[key];
},
set(obj, key, val){
obj[key] = val;
console.log(obj, key , val)
console.log('更新UI界面');
return true;
}
})
}
// ref实现
function ref(val) {
return shallowReactive({value:val});
}
let obj = { a:'a', gf:{b:'b'} };
let state = reactive(obj)
state.gf.b = '4'; // 更新UI
let arr = [{id: 1, name: 'zs'}, {id: 2, name: 'ls'}]
let state2 = reactive(arr)
state2[0].name = 'ww' // 更新UI
手写shallowReadonly/readonly
function shallowReadonly(obj) {
// readonly只需要在这里递归遍历obj并调用自身readOnly方法
return new Proxy(obj, {
get(obj, key){
return obj[key];
},
set(obj, key, val){
console.warn(`${obj[key]}是只读的,不能修改`)
}
})
}
let obj = { a:'a', gf:{b:'b'}};
let state = shallowReadonly(obj)
state.a = '1'; // a是只读的,不能修改
state.gf.b = '2'; // 无输出
function readonly(obj) {
if(typeof obj === 'object') {
if(obj instanceof Array) {
// 如果是一个数组,那么取出数组中的每一个元素,判断每一个元素是否又是一个对象,
// 如果是一个对象,那么也需要包装成Proxy
obj.forEach((item, index) => {
if(typeof item === 'object') {
obj[index] = readonly(item)
}
})
} else {
// 如果是一个对象,那么取出对象属性的值,判断对象属性的取值是否又是一个对象,
// 如果又是一个对象,那么也需要包装成Proxy
for(let key in obj) {
let item = obj[key]
if(typeof item === 'object') {
obj[key] = readonly(item)
}
}
}
}
return new Proxy(obj, {
get(obj, key){
return obj[key];
},
set(obj, key, val){
console.warn(`${obj[key]}是只读的,不能修改`)
}
})
}
let obj = { a:'a', gf:{b:'b'}};
let state = readonly(obj)
state.a = '1'; // a是只读的,不能修改
state.gf.b = '2'; // 无输出