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

一些概念

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

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

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

——尤雨溪

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

Vue 和 React 的邏輯復用手段

到目前為止,

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

React:Mixins、HOC、Render Props、Hook。

我們可以看到都是一段越來越好的成長史,這里就不再舉例贅述,本文重心在 VCA,VCA 更偏向于「組合」的概念。

5個維度來講 Vue3

1. 框架

一個例子先來了解 VCA

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

舉個簡單的例子:

我們要實現 3 個邏輯

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

Vue2 options api 的處理

為了閱讀質量,省略了部分代碼,但不影響我們了解 VCA

// 邏輯功能(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 {
    ctx: { $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 !

最后,前端精本精祝您圣誕快樂??~ (聽說公眾號關注「前端精」會更快樂哦~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容

  • 我們一直都有關注和閱讀很多關于Vue3的新特性和功能即將到來。但是我們沒有一個具體的概念在開發中會有如何的改變和不...
    bayi_lzp閱讀 850評論 0 4
  • Vue3.0的優勢 性能比Vue2.x快1.2~2倍 按需編譯,體積比Vue2.x更小 組合API(類似React...
    強某某閱讀 1,552評論 0 5
  • 我們一直都有關注和閱讀很多關于Vue3的新特性和功能即將到來。但是我們沒有一個具體的概念在開發中會有如何的改變和不...
    三鉆閱讀 11,781評論 4 23
  • 因為這個月的月初給自己定了個小目標,學完Vue3的基本使用,并使用Vue3親手做一個小項目(稍微透露一下,我制作的...
    1kesou閱讀 4,783評論 0 8
  • Vue 3 的 Template 支持多個根標簽,Vue 2 不支持 Vue 3 有 createApp(),而 ...
    sweetBoy_9126閱讀 30,439評論 0 15