Vue3 寫法入門


Vue3.0 亮點

  1. Performance:性能比Vue 2.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
  6. Fragment, 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

ViteVue作者開發的一款意圖取代webpack的工具,其實現原理是利用ES6import會發送請求去加載文件的特性,攔截這些請求,做一些預編譯,省去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 APIOption 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

reactiveVue3中提供的實現響應式數據的方法,在Vue2中響應式數據是通過defineProperty來實現的,而在Vue3中響應式數據是通過ES6Proxy來實現的。

reactive注意點:

reactive參數必須是對象(json/arr),如果給reactive傳遞了其它對象( eg:Date ),默認情況下修改對象,界面不會自動更新,如果想更新,可以通過重新賦值的方式。

創建一個響應式數據本質:就是將傳入的數據包裝成一個Proxy對象


let state = reactive(123);
let state = reactive({age: 123});
let state = reactive([1, 3, 5]);

ref

refreactive一樣,也是用來實現響應式數據的方法,由於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

ReactiveRef 中得到原始數據的方法。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類型數據,並和以前的數據關聯。

reftoRef區別:

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類型數據, 並和以前數據關聯。

toRefref區別:

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'; // 修改成功

💡 constreadonly區別:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM