Vue3丨從 5 個維度來說 Vue3 變化

一些概念

Vue Composition API(VCA) 在實現上也其實只是把 Vue 自己就有的響應式系統更顯式地暴露出來而已。

這不是函數式,只是 API 暴露爲函數。

3.0 Template 編譯出來的性能會比手寫 jsx 快好幾倍。

——尤雨溪javascript

Vue2 傳統的 data,computed,watch,methods 寫法,咱們稱之爲「選項式api(Options API )」
Vue3 使用 Composition API (VCA)能夠根據邏輯功能來組織代碼,一個功能相關的 api 會放在一塊兒。html

Vue 和 React 的邏輯複用手段

到目前爲止,前端

Vue:Mixins(混入)、HOC(高階組件)、做用域插槽、Vue Composition API(VCA/組合式API)。vue

React:Mixins、HOC、Render Props、Hook。java

咱們能夠看到都是一段愈來愈好的成長史,這裏就再也不舉例贅述,本文重心在 VCA,VCA 更偏向於「組合」的概念。react

5個維度來說 Vue3

1. 框架

一個例子先來了解 VCA

在 Vue 中,有了抽象封裝組件的概念,解決了在頁面上模塊越多,越顯臃腫的問題。但即便進行組件封裝,在應用愈來愈大的時候,會發現頁面的邏輯功能點愈來愈多, data/computed/watch/methods 中會被不斷塞入邏輯功能,因此要將邏輯再進行抽離組合、複用,這就是 VCA。webpack

舉個簡單的例子:es6

咱們要實現 3 個邏輯web

  1. 根據 id 獲取表格的數據
  2. 可對錶格數據進行搜索過濾
  3. 彈框新增數據到表格中

Vue2 options api 的處理

爲了閱讀質量,省略了部分代碼,但不影響咱們瞭解 VCAapi

// 邏輯功能(1)
const getTableDataApi = id => {
  const mockData = {
    1: [
      { id: 11, name: '張三1' },
      { id: 12, name: '李四1' },
      { id: 13, name: '王五1' }
    ],
    2: [
      { id: 21, name: '張三2' },
      { id: 22, name: '李四2' },
      { id: 23, name: '王五2' }
    ]
  };
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(mockData[id] || []);
    }, 1000);
  });
};

export default {
  name: 'VCADemo',
  components: { Modal },
  data() {
    return {
      // 邏輯功能(1)
      id: 1,
      table: [],
      // 邏輯功能(2)
      search: '',
      // 邏輯功能(3)
      modalShow: false,
      form: {
        id: '',
        name: ''
      }
    };
  },
  computed: {
    // 邏輯功能(2)
    getTableDataBySearch() {
      return this.table.filter(item => item.name.indexOf(this.search) !== -1);
    }
  },
  watch: {
    // 邏輯功能(1)
    id: 'getTableData'
  },
  mounted() {
    // 邏輯功能(1)
    this.getTableData();
  },
  methods: {
    // 邏輯功能(1)
    async getTableData() {
      const res = await getTableDataApi(this.id);
      this.table = res;
    },
    // 邏輯功能(3)
    handleAdd() {
      this.modalShow = true;
    },
    // 邏輯功能(3)
    handlePost() {
      const { id, name } = this.form;
      this.table.push({ id, name });
      this.modalShow = false;
    }
  }
};
複製代碼

這裏只是舉例簡單的邏輯。若是項目複雜了,邏輯增多了。涉及到一個邏輯的改動,咱們就可能須要修改分佈在不一樣位置的相同功能點,提高了維護成本。

Vue3 composion api 的處理

讓咱們來關注邏輯,抽離邏輯,先看主體的代碼結構

import useTable from './composables/useTable';
import useSearch from './composables/useSearch';
import useAdd from './composables/useAdd';

export default defineComponent({
  name: 'VCADemo',
  components: { Modal },
  setup() {
    // 邏輯功能(1)
    const { id, table, getTable } = useTable(id);
    // 邏輯功能(2)
    const { search, getTableBySearch } = useSearch(table);
    // 邏輯功能(3)
    const { modalShow, form, handleAdd, handlePost } = useAdd(table);
    return {
      id,
      table,
      getTable,

      search,
      getTableBySearch,

      modalShow,
      form,
      handleAdd,
      handlePost
    };
  }
});
複製代碼

setup 接收兩個參數:props,context。能夠返回一個對象,對象的各個屬性都是被 proxy 的,進行監聽追蹤,將在模板上進行響應式渲染。

咱們來關注其中一個邏輯,useTable,通常來講咱們會用 use 開頭進行命名,有那味了~

// VCADemo/composables/useTable.ts
// 邏輯功能(1)相關
import { ref, onMounted, watch, Ref } from 'vue';
import { ITable } from '../index.type';

const getTableApi = (id: number): Promise<ITable[]> => {
  const mockData: { [key: number]: ITable[] } = {
    1: [
      { id: '11', name: '張三1' },
      { id: '12', name: '李四1' },
      { id: '13', name: '王五1' }
    ],
    2: [
      { id: '21', name: '張三2' },
      { id: '22', name: '李四2' },
      { id: '23', name: '王五2' }
    ]
  };
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(mockData[id] || []);
    }, 1000);
  });
};
export default function useTable() {
  const id = ref<number>(1);
  const table = ref<ITable[]>([]);
  const getTable = async () => {
    table.value = await getTableApi(id.value);
  };
  onMounted(getTable);
  watch(id, getTable);
  return {
    id,
    table,
    getTable
  };
}
複製代碼

咱們把相關邏輯獨立抽離,並「組合」在一塊兒了,能夠看到在 vue 包暴露不少獨立函數提供咱們使用,已經再也不 OO 了,嗅到了一股 FP 的氣息~

上面這個例子先說明了 VCA 的帶來的好處,Vue3 的核心固然是 VCA,Vue3 不只僅是 VCA,讓咱們帶着好奇往下看~

生命週期,Vue2 vs Vue3

選項式 API(Vue2) Hook inside setup(Vue3)
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

Hook inside setup,顧名思義,VCA 建議在 setup 這個大方法裏面寫咱們的各類邏輯功能點。

Teleport 組件

傳送,將組件的 DOM 元素掛載在任意指定的一個 DOM 元素,與 React Portals 的概念是一致的。

一個典型的例子,咱們在組件調用了 Modal 彈框組件,咱們但願的彈框是這樣子的,絕對居中,層級最高,如:

組件的結構是這樣子的

<Home>
  <Modal />
</Home>
複製代碼

可是若是在父組件 Home 有相似這樣的樣式,如 transform

就會影響到 Modal 的位置,即便 Modal 用了 position:fixed 來定位,如:

這就是爲何咱們須要用 Teleport 組件來幫助咱們 「跳出」 容器,避免受到父組件的一些約束控制,把組件的 DOM 元素掛載到 body 下,如:

<Teleport to="body">
  <div v-if="show">
    ...Modal 組件的 DOM 結構...
  </div>
</Teleport>
複製代碼

注意:即便 Modal 跳出了容器,也保持 「父子組件關係」,只是 DOM 元素的位置被移動了而已 。

異步組件(defineAsyncComponent)

咱們都知道在 Vue2 也有異步組件的概念,但總體上來講不算完整~,Vue3 提供了 defineAsyncComponent 方法與 Suspense 內置組件,咱們能夠用它們來作一個優雅的異步組件加載方案。

直接看代碼:

HOCLazy/index.tsx

import { defineAsyncComponent, defineComponent } from 'vue';
import MySuspense from './MySuspense.vue';
export default function HOCLazy(chunk: any, isComponent: boolean = false) {
  const wrappedComponent = defineAsyncComponent(chunk);
  return defineComponent({
    name: 'HOCLazy',
    setup() {
      const props = { isComponent, wrappedComponent };
      return () => <MySuspense {...props} />;
    }
  });
}
複製代碼

解釋:HOCLazy 接收了兩個參數,chunk 就是咱們常常採用的組件異步加載方式如:chunk=()=>import(xxx.vue)isComponent 表示當前的「組件」是一個 組件級 or 頁面級,經過判斷 isComponent 來分別對應不一樣的 「loading」 操做。

HOCLazy/MySuspense.vue

<template>
  <Suspense>
    <template #default>
      <component :is="wrappedComponent" v-bind="$attrs" />
    </template>
    <template #fallback>
      <div>
        <Teleport to="body" :disabled="isComponent">
          <div v-if="delayShow" class="loading" :class="{component:isComponent}">
            <!-- 組件和頁面有兩種不同的loading方式,這裏再也不詳細封裝 -->
            <div> {{isComponent?'組件級':'頁面級'}}Loading ...</div>
          </div>
        </Teleport>
      </div>
    </template>
  </Suspense>
</template>

<script lang="ts"> import { defineComponent, defineAsyncComponent, ref, onMounted } from 'vue'; export default defineComponent({ name: 'HOCLazy', props: ['isComponent', 'wrappedComponent'], setup(props) { const delayShow = ref<boolean>(false); onMounted(() => { setTimeout(() => { delayShow.value = true; // delay 本身拿捏,也能夠以 props 的方式傳入 }, 300); }); return { ...props, delayShow }; } }); </script>

<style lang="less" scoped>
.loading {
  // 組件級樣式
  &.component {
  }
  // 頁面級樣式
}
</style>
複製代碼

解釋:

  1. Suspense 組件有兩個插槽,具名插槽 fallback 咱們這裏能夠理解成一個 loading 的佔位符,在異步組件還沒顯示以前的後備內容。
  2. 這裏還用了 Vue 的動態組件 component 來靈活的傳入一個異步組件,v-bind="$attrs" 來保證咱們傳遞給目標組件的 props 不會消失。
  3. fallback 中咱們利用了判斷 isComponent 來展現不一樣的 loading ,由於咱們但願頁面級的 loading 是「全局」的,組件級是在原來的文檔流,這裏用了 Teleport :disabled="isComponent" 來控制是否跳出。
  4. 細心的小夥伴會發現這裏作了一個延遲顯示 delayShow,若是咱們沒有這個延遲,在網絡環境良好的狀況下,loading 每次都會一閃而過,會有一種「反優化」的感受。

調用 HOCLazy:
爲了更好的看出效果,咱們封裝了 slow 方法來延遲組件加載:

utils/slow.ts

const slow = (comp: any, delay: number = 1000): Promise<any> => {
  return new Promise(resolve => {
    setTimeout(() => resolve(comp), delay);
  });
};
export default slow;
複製代碼

調用(組件級)

<template>
  <LazyComp1 str="hello~" />
</template>
const LazyComp1 = HOCLazy(
  () => slow(import('@/components/LazyComp1.vue'), 1000),
  true
);
// ...
components: {
  LazyComp1
},
// ...
複製代碼

看個效果:

其實這與 React 中的 React.lazy + React.Suspense 的概念是一致的,以前寫過的一篇文章 《React丨用戶體驗丨hook版 lazy loading》,小夥伴能夠看看作下對比~

ref,reactive,toRef,toRefs 的區別使用

ref(reference)

ref 和 reactive 的存在都是了追蹤值變化(響應式),ref 有個「包裝」的概念,它用來包裝原始值類型,如 string 和 number ,咱們都知道不是引用類型是沒法追蹤後續的變化的。ref 返回的是一個包含 .value 屬性的對象。

setup(props, context) {
  const count = ref<number>(1);
  // 賦值
  count.value = 2;
  // 讀取
  console.log('count.value :>> ', count.value);
  return { count };
}
複製代碼

在 template 中 ref 包裝對象會被自動展開(Ref Unwrapping),也就是咱們在模板裏不用再 .value

<template>  
  {{count}}
</template>
複製代碼

reactive

與 Vue2 中的 Vue.observable() 是一個概念。
用來返回一個響應式對象,如:

const obj = reactive({
  count: 0
})
// 改變
obj.count++
複製代碼

注意:它用來返回一個響應式對象,自己就是對象,因此不須要包裝。咱們使用它的屬性,不須要加 .value 來獲取。

toRefs

官網:由於 props 是響應式的,你不能使用 ES6 解構,由於它會消除 prop 的響應性。

讓咱們關注 setup 方法的 props 的相關操做:

<template>
  {{name}}
  <button @click="handleClick">點我</button>
</template>
// ...
props: {
  name: { type: String, default: ' ' }
},
setup(props) {
  const { name } = props;
  const handleClick = () => {
    console.log('name :>> ', name);
  };
  return { handleClick };
}
// ...
複製代碼

注意:props 無需經過 setup 函數 return,也能夠在 template 進行綁定對應的值

咱們都知道解構是 es6 一種便捷的手段,編譯成 es5 ,如:

// es6 syntax
const { name } = props;
// to es5 syntax
var name = props.name;
複製代碼

假設父組件更改了 props.name 值,當咱們再點擊了 button 輸出的 name 就仍是以前的值,不會跟着變化,這實際上是一個基礎的 js 的知識點。

爲了方便咱們對它進行包裝,toRefs 能夠理解成批量包裝 props 對象,如:

const { name } = toRefs(props);
const handleClick = () => {
  // 由於是包裝對象,因此讀取的時候要用.value
  console.log('name :>> ', name.value);
};
複製代碼

能夠理解這一切都是由於咱們要用解構,toRefs 所採起的解決方案。

toRef

toRef 的用法,就是多了一個參數,容許咱們針對一個 key 進行包裝,如:

const name = toRef(props,'name');
console.log('name :>> ', name.value);
複製代碼

watchEffect vs watch

Vue3 的 watch 方法與 Vue2 的概念相似,watchEffect 會讓咱們有些疑惑。其實 watchEffect 與 watch 大致相似,區別在於:

watch 能夠作到的

  • 懶執行反作用
  • 更具體地說明什麼狀態應該觸發偵聽器從新運行
  • 訪問偵聽狀態變化先後的值

對於 Vue2 的 watch 方法,Vue3 的 "watch" 多了一個「清除反作用」 的概念,咱們着重關注這點。

這裏拿 watchEffect 來舉例:

watchEffect:它當即執行傳入的一個函數,同時響應式追蹤其依賴,並在其依賴變動時從新運行該函數。

watchEffect 方法簡單結構

watchEffect(onInvalidate => {
  // 執行反作用
  // do something...
  onInvalidate(() => {
    // 執行/清理失效回調
    // do something...
  })
})
複製代碼

執行失效回調,有兩個時機

  • 反作用即將從新執行時,也就是監聽的數據發生改變時
  • 組件卸載時

一個例子:咱們要經過 id 發起請求獲取「水果」的詳情,咱們監聽 id,當 id 切換過於頻繁(還沒等上個異步數據返回成功)。可能會致使最後 id=1 的數據覆蓋了id=2 的數據,這並非咱們但願的。

咱們來模擬並解決這個場景:

模擬接口 getFruitsById

interface IFruit {
  id: number;
  name: string;
  imgs: string;
}
const list: { [key: number]: IFruit } = {
  1: { id: 1, name: '蘋果', imgs: 'https://xxx.apple.jpg' },
  2: { id: 2, name: '香蕉', imgs: 'https://xxx.banana.jpg' }
};
const getFruitsById = (
  id: number,
  delay: number = 3000
): [Promise<IFruit>, () => void] => {
  let _reject: (reason?: any) => void;
  const _promise: Promise<IFruit> = new Promise((resolve, reject) => {
    _reject = reject;
    setTimeout(() => {
      resolve(list[id]);
    }, delay);
  });
  return [
    _promise,
    () =>
      _reject({
        message: 'abort~'
      })
  ];
};
複製代碼

這裏封裝了「取消請求」的方法,利用 reject 來完成這一動做。

在 setup 方法

setup() {
  const id = ref<number>(1);
  const detail = ref<IFruit | {}>({});

  watchEffect(async onInvalidate => {
    onInvalidate(() => {
      cancel && cancel();
    });
    // 模擬id=2的時候請求時間 1s,id=1的時候請求時間 2s
    const [p, cancel] = getFruitsById(id.value, id.value === 2 ? 1000 : 2000);
    const res = await p;
    detail.value = res;
  });
  // 模擬頻繁切換id,獲取香蕉的時候,獲取蘋果的結果尚未回來,取消蘋果的請求,保證數據不會被覆蓋
  id.value = 2;
  // 最後 detail 值爲 { "id": 2, "name": "香蕉", "imgs": "https://xxx.banana.jpg" }
}
複製代碼

若是沒有執行 cancel() ,那麼 detail 的數據將會是 { "id": 1, "name": "蘋果", "imgs": "https://xxx.apple.jpg" },由於 id=1 數據比較「晚接收到」。

這就是在異步場景下常見的例子,清理失效的回調,保證當前反作用有效,不會被覆蓋。感興趣的小夥伴能夠繼續深究。

fragment(片斷)

咱們都知道在封裝組件的時候,只能有一個 root 。在 Vue3 容許咱們有多個 root ,也就是片斷,可是在一些操做值得咱們注意。

inheritAttrs=true[默認] 時,組件會自動在 root 繼承合併 class ,如:

子組件

<template>
  <div class="fragment">
    <div>div1</div>
    <div>div2</div>
  </div>
</template>
複製代碼

父組件調用,新增了一個 class

<MyFragment class="extend-class" />
複製代碼

子組件會被渲染成

<div class="fragment extend-class">
  <div> div1 </div>
  <div> div2 </div>
</div>
複製代碼

若是咱們使用了 片斷 ,就須要顯式的去指定綁定 attrs ,如子組件:

<template>
  <div v-bind="$attrs">div1</div>
  <div>div2</div>
</template>
複製代碼

emits

在 Vue2 咱們會對 props 裏的數據進行規定類型,默認值,非空等一些驗證,能夠理解 emits 作了相似的事情,把 emit 規範起來,如:

// 也能夠直接用數組,不作驗證
// emits: ['on-update', 'on-other'],
emits: {
  // 賦值 null 不驗證
  'on-other': null,
  // 驗證
  'on-update'(val: number) {
    if (val === 1) {
      return true;
    }
    // 自定義報錯
    console.error('val must be 1');
    return false;
  }
},
setup(props, ctx) {
  const handleEmitUpdate = () => {
    // 驗證 val 不爲 1,控制檯報錯
    ctx.emit('on-update', 2);
  };
  const handleEmitOther = () => {
    ctx.emit('on-other');
  };
  return { handleEmitUpdate, handleEmitOther };
}
複製代碼

在 setup 中,emit 已經再也不用 this.$emit 了,而是 setup 的第二個參數 context 上下文來獲取 emit 。

v-model

我的仍是挺喜歡 v-model 的更新的,能夠提高封裝組件的體驗感~

在Vue2,假設我須要封裝一個彈框組件 Modal,用 show 變量來控制彈框的顯示隱藏,這確定是一個父子組件都要維護的值。由於單向數據流,因此須要在 Modal 組件 emit 一個事件,父組件監聽事件接收並修改這個 show 值。
爲了方便咱們會有一些語法糖,如 v-model,可是在 Vue2 一個組件上只能有一個 v-model ,由於語法糖的背後是 value@input 的組成, 若是還有多個相似這樣的 「雙向修改數據」,咱們就須要用語法糖 .sync 同步修飾符。

Vue3 把這兩個語法糖統一了,因此咱們如今能夠在一個組件上使用 多個 v-model 語法糖,舉個例子:

先從父組件看

<VModel v-model="show" v-model:model1="check" v-model:model2.hello="textVal" />
複製代碼

hello爲自定義修飾符

咱們在一個組件上用了 3 個 v-model 語法糖,分別是

v-model 語法糖 對應的 prop 對應的 event 自定義修飾符對應的 prop
v-model(default) modelValue update:modelValue
v-model:model1 model1 update:model1
v-model:model2 model2 update:model2 model2Modifiers

這樣子咱們就更清晰的在子組件咱們要進行一些什麼封裝了,如:

VModel.vue

// ...
props: {
  modelValue: { type: Boolean, default: false },
  model1: { type: Boolean, default: false },
  model2: { type: String, default: '' },
  model2Modifiers: {
    type: Object,
    default: () => ({})
  }
},
emits: ['update:modelValue', 'update:model1', 'update:model2'],
// ...
複製代碼

key attribute

<template>
  <input type="text" placeholder="請輸入帳號" v-if="show" />
  <input type="text" placeholder="請輸入郵箱" v-else />
  <button @click="show=!show">Toggle</button>
</template>
複製代碼

相似這樣的 v-if/v-else,在 Vue2 中,會盡量高效地渲染元素,一般會複用已有元素而不是從頭開始渲染,因此當咱們在第一個 input 中輸入,而後切換第二個 input 。第一個 input 的值將會被保留複用。

有些場景下咱們不要複用它們,須要添加一個惟一的 key ,如:

<template>
  <input type="text" placeholder="請輸入帳號" v-if="show" key="account" />
  <input type="text" placeholder="請輸入郵箱" v-else key="email" />
  <button @click="show=!show">Toggle</button>
</template>
複製代碼

可是在 Vue3 咱們不用顯式的去添加 key ,這兩個 input 元素也是徹底獨立的,由於 Vue3 會對 v-if/v-else 自動生成惟一的 key。

全局 API

在 Vue2 咱們對於一些全局的配置多是這樣子的,例如咱們使用了一個插件

Vue.use({
  /* ... */
});
const app1 = new Vue({ el: '#app-1' });
const app2 = new Vue({ el: '#app-2' });
複製代碼

可是這樣子這會影響兩個根實例,也就是說,會變得不可控。

在 Vue3 引入一個新的 API createApp 方法,返回一個實例:

import { createApp } from 'vue';
const app = createApp({ /* ... */ });
複製代碼

而後咱們就能夠在這個實例上掛載全局相關方法,並只對當前實例生效,如:

app
  .component(/* ... */)
  .directive(/* ... */ )
  .mixin(/* ... */ )
  .use(/* ... */ )
  .mount('#app');
複製代碼

須要注意的是,在 Vue2 咱們用了 Vue.prototype.$http=()=>{} 這樣的寫法,來對 「根Vue」 的 prototype 進行掛載方法,使得咱們在子組件,能夠經過原型鏈的方式找到 $http 方法,即 this.$http

而在 Vue3 咱們相似這樣的掛載須要用一個新的屬性 globalProperties

app.config.globalProperties.$http = () => {}
複製代碼

在 setup 內部使用 $http

setup() {
  const {
    appContext: {
      config: {
        globalProperties: { $http }
      }
    }
  } = getCurrentInstance()
}
複製代碼

2. 底層優化

Proxy 代理

Vue2 響應式的基本原理,就是經過 Object.defineProperty,但這個方式存在缺陷。使得 Vue 不得不經過一些手段來 hack,如:

  • Vue.$set() 動態添加新的響應式屬性
  • 沒法監聽數組變化,Vue 底層須要對數組的一些操做方法,進行再封裝。如 pushpop 等方法。

而在 Vue3 中優先使用了 Proxy 來處理,它代理的是整個對象而不是對象的屬性,可對於整個對象進行操做。不只提高了性能,也沒有上面所說的缺陷。

簡單舉兩個例子:

  1. 動態添加響應式屬性
const targetObj = { id: '1', name: 'zhagnsan' };
const proxyObj = new Proxy(targetObj, {
  get: function (target, propKey, receiver) {
    console.log(`getting key:${propKey}`);
    return Reflect.get(...arguments);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting key:${propKey},value:${value}`);
    return Reflect.set(...arguments);
  }
});
proxyObj.age = 18;
// setting key:age,value:18
複製代碼

如上,用 Proxy 咱們對 proxyObj 對象動態添加的屬性也會被攔截到。

Reflect 對象是ES6 爲了操做對象而提供的新 API。它有幾個內置的方法,就如上面的 get / set,這裏能夠理解成咱們用 Reflect 更加方便,不然咱們須要如:

get: function (target, propKey, receiver) {
  console.log(`getting ${propKey}!`);
  return target[propKey];
},
複製代碼
  1. 對數組的操做進行攔截
const targetArr = [1, 2];
const proxyArr = new Proxy(targetArr, {
  set: function (target, propKey, value, receiver) {
    console.log(`setting key:${propKey},value:${value}`);
    return Reflect.set(...arguments);
  }
});
proxyArr.push('3');
// setting key:2,value:3
// setting key:length,value:3
複製代碼

靜態提高(hoistStatic) vdom

咱們都知道 Vue 有虛擬dom的概念,它能爲咱們在數據改變時高效的渲染頁面。

Vue3 優化了 vdom 的更新性能,簡單舉個例子

Template

<div class="div">
  <div>content</div>
  <div>{{message}}</div>
</div>
複製代碼

Compiler 後,沒有靜態提高

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", { class: "div" }, [
    _createVNode("div", null, "content"),
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}
複製代碼

Compiler 後,有靜態提高

const _hoisted_1 = { class: "div" }
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "content", -1 /* HOISTED */)

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _hoisted_2,
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}
複製代碼

靜態提高包含「靜態節點」和「靜態屬性」的提高,也就是說,咱們把一些靜態的不會變的節點用變量緩存起來,提供下次 re-render 直接調用。
若是沒有作這個動做,當 render 從新執行時,即便標籤是靜態的,也會被從新建立,這就會產生性能消耗。

3. 與 TS

3.0 的一個主要設計目標是加強對 TypeScript 的支持。本來咱們指望經過 Class API 來達成這個目標,可是通過討論和原型開發,咱們認爲 Class 並非解決這個問題的正確路線,基於 Class 的 API 依然存在類型問題。——尤雨溪

基於函數的 API 自然 與 TS 完美結合。

defineComponent

在 TS 下,咱們須要用 Vue 暴露的方法 defineComponent,它單純爲了類型推導而存在的。

props 推導

import { defineComponent } from 'vue';
export default defineComponent({
  props: {
    val1: String,
    val2: { type: String, default: '' },
  },
  setup(props, context) {
    props.val1;
  }
})
複製代碼

當咱們在 setup 方法訪問 props 時候,咱們能夠看到被推導後的類型,

  • val1 咱們沒有設置默認值,因此它爲 string | undefined
  • 而 val2 的值有值,因此是 string,如圖:

PropType

咱們關注一下 props 定義的類型,若是是一個複雜對象,咱們就要用 PropType 來進行強轉聲明,如:

interface IObj {
  id: number;
  name: string;
}

obj: {
  type: Object as PropType<IObj>,
  default: (): IObj => ({ id: 1, name: '張三' })
},
複製代碼

或 聯合類型

type: {
  type: String as PropType<'success' | 'error' | 'warning'>,
  default: 'warning'
},
複製代碼

4. build丨更好的 tree-sharking(搖樹優化)

tree-sharking 即在構建工具構建後消除程序中無用的代碼,來減小包的體積。

基於函數的 API 每個函數均可以用 import { method1,method2 } from "xxx";,這就對 tree-sharking 很是友好,並且函數名同變量名均可以被壓縮,對象去不能夠。舉個例子,咱們封裝了一個工具,工具提供了兩個方法,用 method1method2 來代替。

咱們把它們封裝成一個對象,而且暴露出去,如:

// utils
const obj = {
  method1() {},
  method2() {}
};
export default obj;
複製代碼
// 調用
import util from '@/utils';
util.method1();
複製代碼

通過webpack打包壓縮以後爲:

a={method1:function(){},method2:function(){}};a.method1();
複製代碼

咱們不用對象的形式,而用函數的形式來看看:

// utils
export function method1() {}
export function method2() {}
複製代碼
// 調用
import { method1 } from '@/utils';
method1();
複製代碼

通過webpack打包壓縮以後爲:

function a(){}a();
複製代碼

用這個例子咱們就能夠了解 Vue3 爲何能更好的 tree-sharking ,由於它用的是基於函數形式的API,如:

import {
  defineComponent,
  reactive,
  ref,
  watchEffect,
  watch,
  onMounted,
  toRefs,
  toRef
} from 'vue';
複製代碼

5. options api 與 composition api 取捨

咱們上面的代碼都是在 setup 內部實現,可是目前 Vue3 還保留了 Vue2 的 options api 寫法,就是能夠「並存」,如:

// ...
setup() {
  const val = ref<string>('');
  const fn = () => {};
  return {
    val,
    fn
  };
},
mounted() {
  // 在 mounted 生命週期能夠訪問到 setup return 出來的對象
  console.log(this.val);
  this.fn();
},
// ...
複製代碼

結合 react ,咱們知道 「函數式」,hook 是將來的一個趨勢。

因此我的建議仍是採用都在 setup 內部寫邏輯的方式,由於 Vue3 能夠徹底提供 Vue2 的所有能力。

總結

我的以爲無論是 React Hook 仍是 Vue3 的 VCA,咱們均可以看到如今的前端框架趨勢,「更函數式」,讓邏輯複用更靈活。hook 的模式新增了 React / Vue 的抽象層級,「組件級 + 函數級」,可讓咱們處理邏輯時分的更細,更好維護。

Vue3 One Piece,nice !

最後,前端精本精祝您聖誕快樂🎄~ (據說公衆號關注「前端精」會更快樂哦~

相關文章
相關標籤/搜索