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'; // 無輸出
