Vue3丨從 5 個(gè)維度來(lái)講 Vue3 變化

一些概念

Vue Composition API(VCA) 在實(shí)現(xiàn)上也其實(shí)只是把 Vue 本身就有的響應(yīng)式系統(tǒng)更顯式地暴露出來(lái)而已。

這不是函數(shù)式,只是 API 暴露為函數(shù)。

3.0 Template 編譯出來(lái)的性能會(huì)比手寫 jsx 快好幾倍萨驶。

——尤雨溪

Vue2 傳統(tǒng)的 data,computed艇肴,watch腔呜,methods 寫法叁温,我們稱之為「選項(xiàng)式api(Options API )」
Vue3 使用 Composition API (VCA)可以根據(jù)邏輯功能來(lái)組織代碼,一個(gè)功能相關(guān)的 api 會(huì)放在一起核畴。

Vue 和 React 的邏輯復(fù)用手段

到目前為止膝但,

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

React:Mixins咖刃、HOC、Render Props憾筏、Hook嚎杨。

我們可以看到都是一段越來(lái)越好的成長(zhǎng)史,這里就不再舉例贅述氧腰,本文重心在 VCA枫浙,VCA 更偏向于「組合」的概念。

5個(gè)維度來(lái)講 Vue3

1. 框架

一個(gè)例子先來(lái)了解 VCA

在 Vue 中古拴,有了抽象封裝組件的概念箩帚,解決了在頁(yè)面上模塊越多,越顯臃腫的問(wèn)題黄痪。但即使進(jìn)行組件封裝紧帕,在應(yīng)用越來(lái)越大的時(shí)候,會(huì)發(fā)現(xiàn)頁(yè)面的邏輯功能點(diǎn)越來(lái)越多桅打, data/computed/watch/methods 中會(huì)被不斷塞入邏輯功能是嗜,所以要將邏輯再進(jìn)行抽離組合、復(fù)用挺尾,這就是 VCA鹅搪。

舉個(gè)簡(jiǎn)單的例子:

我們要實(shí)現(xiàn) 3 個(gè)邏輯

  1. 根據(jù) id 獲取表格的數(shù)據(jù)
  2. 可對(duì)表格數(shù)據(jù)進(jìn)行搜索過(guò)濾
  3. 彈框新增數(shù)據(jù)到表格中

Vue2 options api 的處理

為了閱讀質(zhì)量,省略了部分代碼遭铺,但不影響我們了解 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;
    }
  }
};

這里只是舉例簡(jiǎn)單的邏輯丽柿。如果項(xiàng)目復(fù)雜了,邏輯增多了魂挂。涉及到一個(gè)邏輯的改動(dòng)航厚,我們就可能需要修改分布在不同位置的相同功能點(diǎn),提升了維護(hù)成本锰蓬。

Vue3 composion api 的處理

讓我們來(lái)關(guān)注邏輯幔睬,抽離邏輯,先看主體的代碼結(jié)構(gòu)

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 接收兩個(gè)參數(shù):props芹扭,context麻顶∩舛叮可以返回一個(gè)對(duì)象,對(duì)象的各個(gè)屬性都是被 proxy 的辅肾,進(jìn)行監(jiān)聽(tīng)追蹤队萤,將在模板上進(jìn)行響應(yīng)式渲染。

我們來(lái)關(guān)注其中一個(gè)邏輯矫钓,useTable要尔,一般來(lái)說(shuō)我們會(huì)用 use 開頭進(jìn)行命名,有那味了~

// VCADemo/composables/useTable.ts
// 邏輯功能(1)相關(guān)
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
  };
}

我們把相關(guān)邏輯獨(dú)立抽離新娜,并「組合」在一起了赵辕,可以看到在 vue 包暴露很多獨(dú)立函數(shù)提供我們使用,已經(jīng)不再 OO 了概龄,嗅到了一股 FP 的氣息~

上面這個(gè)例子先說(shuō)明了 VCA 的帶來(lái)的好處还惠,Vue3 的核心當(dāng)然是 VCA,Vue3 不僅僅是 VCA私杜,讓我們帶著好奇往下看~

生命周期蚕键,Vue2 vs Vue3

選項(xiàng)式 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 這個(gè)大方法里面寫我們的各種邏輯功能點(diǎn)锣光。

Teleport 組件

傳送,將組件的 DOM 元素掛載在任意指定的一個(gè) DOM 元素铝耻,與 React Portals 的概念是一致的誊爹。

一個(gè)典型的例子,我們?cè)诮M件調(diào)用了 Modal 彈框組件田篇,我們希望的彈框是這樣子的替废,絕對(duì)居中,層級(jí)最高泊柬,如:

組件的結(jié)構(gòu)是這樣子的

<Home>
  <Modal />
</Home>

但是如果在父組件 Home 有類似這樣的樣式椎镣,如 transform

就會(huì)影響到 Modal 的位置,即使 Modal 用了 position:fixed 來(lái)定位兽赁,如:

這就是為什么我們需要用 Teleport 組件來(lái)幫助我們 “跳出” 容器状答,避免受到父組件的一些約束控制,把組件的 DOM 元素掛載到 body 下刀崖,如:

<Teleport to="body">
  <div v-if="show">
    ...Modal 組件的 DOM 結(jié)構(gòu)...
  </div>
</Teleport>

注意:即使 Modal 跳出了容器惊科,也保持 “父子組件關(guān)系”,只是 DOM 元素的位置被移動(dòng)了而已 亮钦。

異步組件(defineAsyncComponent)

我們都知道在 Vue2 也有異步組件的概念馆截,但整體上來(lái)說(shuō)不算完整~,Vue3 提供了 defineAsyncComponent 方法與 Suspense 內(nèi)置組件,我們可以用它們來(lái)做一個(gè)優(yōu)雅的異步組件加載方案蜡娶。

直接看代碼:

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 接收了兩個(gè)參數(shù)混卵,chunk 就是我們經(jīng)常采用的組件異步加載方式如:chunk=()=>import(xxx.vue)isComponent 表示當(dāng)前的“組件”是一個(gè) 組件級(jí) or 頁(yè)面級(jí)窖张,通過(guò)判斷 isComponent 來(lái)分別對(duì)應(yīng)不同的 “l(fā)oading” 操作幕随。

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}">
            <!-- 組件和頁(yè)面有兩種不一樣的loading方式,這里不再詳細(xì)封裝 -->
            <div> {{isComponent?'組件級(jí)':'頁(yè)面級(jí)'}}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 {
  // 組件級(jí)樣式
  &.component {
  }
  // 頁(yè)面級(jí)樣式
}
</style>

解釋:

  1. Suspense 組件有兩個(gè)插槽赘淮,具名插槽 fallback 我們這里可以理解成一個(gè) loading 的占位符,在異步組件還沒(méi)顯示之前的后備內(nèi)容睦霎。
  2. 這里還用了 Vue 的動(dòng)態(tài)組件 component 來(lái)靈活的傳入一個(gè)異步組件梢卸,v-bind="$attrs" 來(lái)保證我們傳遞給目標(biāo)組件的 props 不會(huì)消失。
  3. fallback 中我們利用了判斷 isComponent 來(lái)展示不同的 loading 碎赢,因?yàn)槲覀兿M?yè)面級(jí)的 loading 是“全局”的低剔,組件級(jí)是在原來(lái)的文檔流速梗,這里用了 Teleport :disabled="isComponent" 來(lái)控制是否跳出肮塞。
  4. 細(xì)心的小伙伴會(huì)發(fā)現(xiàn)這里做了一個(gè)延遲顯示 delayShow,如果我們沒(méi)有這個(gè)延遲姻锁,在網(wǎng)絡(luò)環(huán)境良好的情況下枕赵,loading 每次都會(huì)一閃而過(guò),會(huì)有一種“反優(yōu)化”的感覺(jué)位隶。

調(diào)用 HOCLazy:
為了更好的看出效果拷窜,我們封裝了 slow 方法來(lái)延遲組件加載:

utils/slow.ts

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

調(diào)用(組件級(jí))

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

看個(gè)效果:

其實(shí)這與 React 中的 React.lazy + React.Suspense 的概念是一致的,之前寫過(guò)的一篇文章 《React丨用戶體驗(yàn)丨h(huán)ook版 lazy loading》涧黄,小伙伴可以看看做下對(duì)比~

ref篮昧,reactive,toRef笋妥,toRefs 的區(qū)別使用

ref(reference)

ref 和 reactive 的存在都是了追蹤值變化(響應(yīng)式)懊昨,ref 有個(gè)「包裝」的概念,它用來(lái)包裝原始值類型春宣,如 string 和 number 酵颁,我們都知道不是引用類型是無(wú)法追蹤后續(xù)的變化的。ref 返回的是一個(gè)包含 .value 屬性的對(duì)象月帝。

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

在 template 中 ref 包裝對(duì)象會(huì)被自動(dòng)展開(Ref Unwrapping)躏惋,也就是我們?cè)谀0謇锊挥迷?.value

<template>  
  {{count}}
</template>

reactive

與 Vue2 中的 Vue.observable() 是一個(gè)概念。
用來(lái)返回一個(gè)響應(yīng)式對(duì)象嚷辅,如:

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

注意:它用來(lái)返回一個(gè)響應(yīng)式對(duì)象簿姨,本身就是對(duì)象,所以不需要包裝簸搞。我們使用它的屬性扁位,不需要加 .value 來(lái)獲取深寥。

toRefs

官網(wǎng):因?yàn)?props 是響應(yīng)式的,你不能使用 ES6 解構(gòu)贤牛,因?yàn)樗鼤?huì)消除 prop 的響應(yīng)性惋鹅。

讓我們關(guān)注 setup 方法的 props 的相關(guān)操作:

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

注意:props 無(wú)需通過(guò) setup 函數(shù) return,也可以在 template 進(jìn)行綁定對(duì)應(yīng)的值

我們都知道解構(gòu)是 es6 一種便捷的手段殉簸,編譯成 es5 闰集,如:

// es6 syntax
const { name } = props;
// to es5 syntax
var name = props.name;

假設(shè)父組件更改了 props.name 值,當(dāng)我們?cè)冱c(diǎn)擊了 button 輸出的 name 就還是之前的值般卑,不會(huì)跟著變化武鲁,這其實(shí)是一個(gè)基礎(chǔ)的 js 的知識(shí)點(diǎn)。

為了方便我們對(duì)它進(jìn)行包裝蝠检,toRefs 可以理解成批量包裝 props 對(duì)象沐鼠,如:

const { name } = toRefs(props);
const handleClick = () => {
  // 因?yàn)槭前b對(duì)象,所以讀取的時(shí)候要用.value
  console.log('name :>> ', name.value);
};

可以理解這一切都是因?yàn)槲覀円媒鈽?gòu)叹谁,toRefs 所采取的解決方案饲梭。

toRef

toRef 的用法,就是多了一個(gè)參數(shù)焰檩,允許我們針對(duì)一個(gè) key 進(jìn)行包裝憔涉,如:

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

watchEffect vs watch

Vue3 的 watch 方法與 Vue2 的概念類似,watchEffect 會(huì)讓我們有些疑惑析苫。其實(shí) watchEffect 與 watch 大體類似兜叨,區(qū)別在于:

watch 可以做到的

  • 懶執(zhí)行副作用
  • 更具體地說(shuō)明什么狀態(tài)應(yīng)該觸發(fā)偵聽(tīng)器重新運(yùn)行
  • 訪問(wèn)偵聽(tīng)狀態(tài)變化前后的值

對(duì)于 Vue2 的 watch 方法,Vue3 的 "watch" 多了一個(gè)「清除副作用」 的概念衩侥,我們著重關(guān)注這點(diǎn)国旷。

這里拿 watchEffect 來(lái)舉例:

watchEffect:它立即執(zhí)行傳入的一個(gè)函數(shù),同時(shí)響應(yīng)式追蹤其依賴茫死,并在其依賴變更時(shí)重新運(yùn)行該函數(shù)跪但。

watchEffect 方法簡(jiǎn)單結(jié)構(gòu)

watchEffect(onInvalidate => {
  // 執(zhí)行副作用
  // do something...
  onInvalidate(() => {
    // 執(zhí)行/清理失效回調(diào)
    // do something...
  })
})

執(zhí)行失效回調(diào),有兩個(gè)時(shí)機(jī)

  • 副作用即將重新執(zhí)行時(shí)璧榄,也就是監(jiān)聽(tīng)的數(shù)據(jù)發(fā)生改變時(shí)
  • 組件卸載時(shí)

一個(gè)例子:我們要通過(guò) id 發(fā)起請(qǐng)求獲取「水果」的詳情特漩,我們監(jiān)聽(tīng) id,當(dāng) id 切換過(guò)于頻繁(還沒(méi)等上個(gè)異步數(shù)據(jù)返回成功)骨杂⊥可恚可能會(huì)導(dǎo)致最后 id=1 的數(shù)據(jù)覆蓋了id=2 的數(shù)據(jù),這并不是我們希望的搓蚪。

我們來(lái)模擬并解決這個(gè)場(chǎng)景:

模擬接口 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~'
      })
  ];
};

這里封裝了“取消請(qǐng)求”的方法蛤售,利用 reject 來(lái)完成這一動(dòng)作。

在 setup 方法

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

  watchEffect(async onInvalidate => {
    onInvalidate(() => {
      cancel && cancel();
    });
    // 模擬id=2的時(shí)候請(qǐng)求時(shí)間 1s,id=1的時(shí)候請(qǐng)求時(shí)間 2s
    const [p, cancel] = getFruitsById(id.value, id.value === 2 ? 1000 : 2000);
    const res = await p;
    detail.value = res;
  });
  // 模擬頻繁切換id悴能,獲取香蕉的時(shí)候揣钦,獲取蘋果的結(jié)果還沒(méi)有回來(lái),取消蘋果的請(qǐng)求漠酿,保證數(shù)據(jù)不會(huì)被覆蓋
  id.value = 2;
  // 最后 detail 值為 { "id": 2, "name": "香蕉", "imgs": "https://xxx.banana.jpg" }
}

如果沒(méi)有執(zhí)行 cancel() 冯凹,那么 detail 的數(shù)據(jù)將會(huì)是 { "id": 1, "name": "蘋果", "imgs": "https://xxx.apple.jpg" },因?yàn)?id=1 數(shù)據(jù)比較“晚接收到”炒嘲。

這就是在異步場(chǎng)景下常見(jiàn)的例子宇姚,清理失效的回調(diào),保證當(dāng)前副作用有效夫凸,不會(huì)被覆蓋浑劳。感興趣的小伙伴可以繼續(xù)深究。

fragment(片段)

我們都知道在封裝組件的時(shí)候夭拌,只能有一個(gè) root 魔熏。在 Vue3 允許我們有多個(gè) root ,也就是片段鸽扁,但是在一些操作值得我們注意蒜绽。

當(dāng) inheritAttrs=true[默認(rèn)] 時(shí),組件會(huì)自動(dòng)在 root 繼承合并 class 献烦,如:

子組件

<template>
  <div class="fragment">
    <div>div1</div>
    <div>div2</div>
  </div>
</template>

父組件調(diào)用滓窍,新增了一個(gè) class

<MyFragment class="extend-class" />

子組件會(huì)被渲染成

<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 我們會(huì)對(duì) props 里的數(shù)據(jù)進(jìn)行規(guī)定類型,默認(rèn)值此蜈,非空等一些驗(yàn)證即横,可以理解 emits 做了類似的事情,把 emit 規(guī)范起來(lái)裆赵,如:

// 也可以直接用數(shù)組东囚,不做驗(yàn)證
// emits: ['on-update', 'on-other'],
emits: {
  // 賦值 null 不驗(yàn)證
  'on-other': null,
  // 驗(yàn)證
  'on-update'(val: number) {
    if (val === 1) {
      return true;
    }
    // 自定義報(bào)錯(cuò)
    console.error('val must be 1');
    return false;
  }
},
setup(props, ctx) {
  const handleEmitUpdate = () => {
    // 驗(yàn)證 val 不為 1,控制臺(tái)報(bào)錯(cuò)
    ctx.emit('on-update', 2);
  };
  const handleEmitOther = () => {
    ctx.emit('on-other');
  };
  return { handleEmitUpdate, handleEmitOther };
}

在 setup 中战授,emit 已經(jīng)不再用 this.$emit 了页藻,而是 setup 的第二個(gè)參數(shù) context 上下文來(lái)獲取 emit 。

v-model

個(gè)人還是挺喜歡 v-model 的更新的植兰,可以提升封裝組件的體驗(yàn)感~

在Vue2份帐,假設(shè)我需要封裝一個(gè)彈框組件 Modal,用 show 變量來(lái)控制彈框的顯示隱藏楣导,這肯定是一個(gè)父子組件都要維護(hù)的值废境。因?yàn)閱蜗驍?shù)據(jù)流,所以需要在 Modal 組件 emit 一個(gè)事件,父組件監(jiān)聽(tīng)事件接收并修改這個(gè) show 值噩凹。
為了方便我們會(huì)有一些語(yǔ)法糖巴元,如 v-model,但是在 Vue2 一個(gè)組件上只能有一個(gè) v-model 驮宴,因?yàn)檎Z(yǔ)法糖的背后是 value@input 的組成逮刨, 如果還有多個(gè)類似這樣的 “雙向修改數(shù)據(jù)”,我們就需要用語(yǔ)法糖 .sync 同步修飾符堵泽。

Vue3 把這兩個(gè)語(yǔ)法糖統(tǒng)一了禀忆,所以我們現(xiàn)在可以在一個(gè)組件上使用 多個(gè) v-model 語(yǔ)法糖,舉個(gè)例子:

先從父組件看

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

hello為自定義修飾符

我們?cè)谝粋€(gè)組件上用了 3 個(gè) v-model 語(yǔ)法糖落恼,分別是

v-model 語(yǔ)法糖 對(duì)應(yīng)的 prop 對(duì)應(yīng)的 event 自定義修飾符對(duì)應(yīng)的 prop
v-model(default) modelValue update:modelValue 無(wú)
v-model:model1 model1 update:model1 無(wú)
v-model:model2 model2 update:model2 model2Modifiers

這樣子我們就更清晰的在子組件我們要進(jìn)行一些什么封裝了箩退,如:

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="請(qǐng)輸入賬號(hào)"
         v-if="show" />
  <input type="text"
         placeholder="請(qǐng)輸入郵箱"
         v-else />
  <button @click="show=!show">Toggle</button>
</template>

類似這樣的 v-if/v-else,在 Vue2 中佳谦,會(huì)盡可能高效地渲染元素戴涝,通常會(huì)復(fù)用已有元素而不是從頭開始渲染,所以當(dāng)我們?cè)诘谝粋€(gè) input 中輸入钻蔑,然后切換第二個(gè)
input 啥刻。第一個(gè) input 的值將會(huì)被保留復(fù)用。

有些場(chǎng)景下我們不要復(fù)用它們咪笑,需要添加一個(gè)唯一的 key 可帽,如:

<template>
  <input type="text"
         placeholder="請(qǐng)輸入賬號(hào)"
         v-if="show"
         key="account" />
  <input type="text"
         placeholder="請(qǐng)輸入郵箱"
         v-else
         key="email" />
  <button @click="show=!show">Toggle</button>
</template>

但是在 Vue3 我們不用顯式的去添加 key ,這兩個(gè) input 元素也是完全獨(dú)立的窗怒,因?yàn)?Vue3 會(huì)對(duì) v-if/v-else 自動(dòng)生成唯一的 key映跟。

全局 API

在 Vue2 我們對(duì)于一些全局的配置可能是這樣子的,例如我們使用了一個(gè)插件

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

但是這樣子這會(huì)影響兩個(gè)根實(shí)例扬虚,也就是說(shuō)努隙,會(huì)變得不可控。

在 Vue3 引入一個(gè)新的 API createApp 方法辜昵,返回一個(gè)實(shí)例:

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

然后我們就可以在這個(gè)實(shí)例上掛載全局相關(guān)方法荸镊,并只對(duì)當(dāng)前實(shí)例生效,如:

app
  .component(/* ... */)
  .directive(/* ... */ )
  .mixin(/* ... */ )
  .use(/* ... */ )
  .mount('#app');

需要注意的是堪置,在 Vue2 我們用了 Vue.prototype.$http=()=>{} 這樣的寫法躬存,來(lái)對(duì) “根Vue” 的 prototype 進(jìn)行掛載方法,使得我們?cè)谧咏M件舀锨,可以通過(guò)原型鏈的方式找到 $http 方法岭洲,即 this.$http

而在 Vue3 我們類似這樣的掛載需要用一個(gè)新的屬性 globalProperties

app.config.globalProperties.$http = () => {}

在 setup 內(nèi)部使用 $http

setup() {
  const {
    ctx: { $http }
  } = getCurrentInstance();
}

2. 底層優(yōu)化

Proxy 代理

Vue2 響應(yīng)式的基本原理雁竞,就是通過(guò) Object.defineProperty钦椭,但這個(gè)方式存在缺陷拧额。使得 Vue 不得不通過(guò)一些手段來(lái) hack,如:

  • Vue.$set() 動(dòng)態(tài)添加新的響應(yīng)式屬性
  • 無(wú)法監(jiān)聽(tīng)數(shù)組變化彪腔,Vue 底層需要對(duì)數(shù)組的一些操作方法侥锦,進(jìn)行再封裝。如 push德挣,pop 等方法恭垦。

而在 Vue3 中優(yōu)先使用了 Proxy 來(lái)處理,它代理的是整個(gè)對(duì)象而不是對(duì)象的屬性格嗅,可對(duì)于整個(gè)對(duì)象進(jìn)行操作番挺。不僅提升了性能,也沒(méi)有上面所說(shuō)的缺陷屯掖。

簡(jiǎn)單舉兩個(gè)例子:

  1. 動(dòng)態(tài)添加響應(yīng)式屬性
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 我們對(duì) proxyObj 對(duì)象動(dòng)態(tài)添加的屬性也會(huì)被攔截到粪摘。

Reflect 對(duì)象是ES6 為了操作對(duì)象而提供的新 API。它有幾個(gè)內(nèi)置的方法绍坝,就如上面的 get / set徘意,這里可以理解成我們用 Reflect 更加方便,否則我們需要如:

get: function (target, propKey, receiver) {
  console.log(`getting ${propKey}!`);
  return target[propKey];
},
  1. 對(duì)數(shù)組的操作進(jìn)行攔截
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

靜態(tài)提升(hoistStatic) vdom

我們都知道 Vue 有虛擬dom的概念把介,它能為我們?cè)跀?shù)據(jù)改變時(shí)高效的渲染頁(yè)面勤讽。

Vue3 優(yōu)化了 vdom 的更新性能,簡(jiǎn)單舉個(gè)例子

Template

<div class="div">
  <div>content</div>
  <div>{{message}}</div>
</div>

Compiler 后劳澄,沒(méi)有靜態(tài)提升

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 后地技,有靜態(tài)提升

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 */)
  ]))
}

靜態(tài)提升包含「靜態(tài)節(jié)點(diǎn)」和「靜態(tài)屬性」的提升,也就是說(shuō)秒拔,我們把一些靜態(tài)的不會(huì)變的節(jié)點(diǎn)用變量緩存起來(lái),提供下次 re-render 直接調(diào)用飒硅。
如果沒(méi)有做這個(gè)動(dòng)作砂缩,當(dāng) render 重新執(zhí)行時(shí),即使標(biāo)簽是靜態(tài)的三娩,也會(huì)被重新創(chuàng)建庵芭,這就會(huì)產(chǎn)生性能消耗。

3. 與 TS

3.0 的一個(gè)主要設(shè)計(jì)目標(biāo)是增強(qiáng)對(duì) TypeScript 的支持雀监。原本我們期望通過(guò) Class API 來(lái)達(dá)成這個(gè)目標(biāo)双吆,但是經(jīng)過(guò)討論和原型開發(fā)眨唬,我們認(rèn)為 Class 并不是解決這個(gè)問(wèn)題的正確路線,基于 Class 的 API 依然存在類型問(wèn)題好乐∝腋停——尤雨溪

基于函數(shù)的 API 天然 與 TS 完美結(jié)合。

defineComponent

在 TS 下蔚万,我們需要用 Vue 暴露的方法 defineComponent岭妖,它單純?yōu)榱祟愋屯茖?dǎo)而存在的。

props 推導(dǎo)

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

當(dāng)我們?cè)?setup 方法訪問(wèn) props 時(shí)候反璃,我們可以看到被推導(dǎo)后的類型昵慌,

  • val1 我們沒(méi)有設(shè)置默認(rèn)值,所以它為 string | undefined
  • 而 val2 的值有值淮蜈,所以是 string斋攀,如圖:

PropType

我們關(guān)注一下 props 定義的類型,如果是一個(gè)復(fù)雜對(duì)象梧田,我們就要用 PropType 來(lái)進(jìn)行強(qiáng)轉(zhuǎn)聲明蜻韭,如:

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

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

或 聯(lián)合類型

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

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

tree-sharking 即在構(gòu)建工具構(gòu)建后消除程序中無(wú)用的代碼,來(lái)減少包的體積柿扣。

基于函數(shù)的 API 每一個(gè)函數(shù)都可以用 import { method1肖方,method2 } from "xxx";,這就對(duì) tree-sharking 非常友好未状,而且函數(shù)名同變量名都可以被壓縮俯画,對(duì)象去不可以。舉個(gè)例子司草,我們封裝了一個(gè)工具艰垂,工具提供了兩個(gè)方法,用 method1埋虹,method2 來(lái)代替猜憎。

我們把它們封裝成一個(gè)對(duì)象,并且暴露出去搔课,如:

// utils
const obj = {
  method1() {},
  method2() {}
};
export default obj;
// 調(diào)用
import util from '@/utils';
util.method1();

經(jīng)過(guò)webpack打包壓縮之后為:

a={method1:function(){},method2:function(){}};a.method1();

我們不用對(duì)象的形式胰柑,而用函數(shù)的形式來(lái)看看:

// utils
export function method1() {}
export function method2() {}
// 調(diào)用
import { method1 } from '@/utils';
method1();

經(jīng)過(guò)webpack打包壓縮之后為:

function a(){}a();

用這個(gè)例子我們就可以了解 Vue3 為什么能更好的 tree-sharking 算利,因?yàn)樗玫氖腔诤瘮?shù)形式的API丹锹,如:

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

5. options api 與 composition api 取舍

我們上面的代碼都是在 setup 內(nèi)部實(shí)現(xiàn),但是目前 Vue3 還保留了 Vue2 的 options api 寫法伪货,就是可以“并存”袍啡,如:

// ...
setup() {
  const val = ref<string>('');
  const fn = () => {};
  return {
    val,
    fn
  };
},
mounted() {
  // 在 mounted 生命周期可以訪問(wèn)到 setup return 出來(lái)的對(duì)象
  console.log(this.val);
  this.fn();
},
// ...

結(jié)合 react 踩官,我們知道 “函數(shù)式”,hook 是未來(lái)的一個(gè)趨勢(shì)境输。

所以個(gè)人建議還是采用都在 setup 內(nèi)部寫邏輯的方式蔗牡,因?yàn)?Vue3 可以完全提供 Vue2 的全部能力颖系。

總結(jié)

個(gè)人覺(jué)得不管是 React Hook 還是 Vue3 的 VCA,我們都可以看到現(xiàn)在的前端框架趨勢(shì)辩越,“更函數(shù)式”嘁扼,讓邏輯復(fù)用更靈活。hook 的模式新增了 React / Vue 的抽象層級(jí)区匣,「組件級(jí) + 函數(shù)級(jí)」偷拔,可以讓我們處理邏輯時(shí)分的更細(xì),更好維護(hù)亏钩。

Vue3 One Piece莲绰,nice !

最后姑丑,前端精本精祝您圣誕快樂(lè)??~ (聽(tīng)說(shuō)公眾號(hào)關(guān)注「前端精」會(huì)更快樂(lè)哦~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛤签,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子栅哀,更是在濱河造成了極大的恐慌震肮,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件留拾,死亡現(xiàn)場(chǎng)離奇詭異戳晌,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)痴柔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門沦偎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人咳蔚,你說(shuō)我怎么就攤上這事豪嚎。” “怎么了谈火?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵侈询,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我糯耍,道長(zhǎng)扔字,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任谍肤,我火速辦了婚禮,結(jié)果婚禮上荒揣,老公的妹妹穿的比我還像新娘。我一直安慰自己焊刹,他們只是感情好系任,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布恳蹲。 她就那樣靜靜地躺著,像睡著了一般俩滥。 火紅的嫁衣襯著肌膚如雪嘉蕾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天霜旧,我揣著相機(jī)與錄音错忱,去河邊找鬼。 笑死挂据,一個(gè)胖子當(dāng)著我的面吹牛以清,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播崎逃,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼掷倔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了个绍?” 一聲冷哼從身側(cè)響起勒葱,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巴柿,沒(méi)想到半個(gè)月后凛虽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡广恢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年凯旋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袁波。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瓦阐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出篷牌,到底是詐尸還是另有隱情睡蟋,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布枷颊,位于F島的核電站戳杀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏夭苗。R本人自食惡果不足惜信卡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望题造。 院中可真熱鬧傍菇,春花似錦、人聲如沸界赔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至咐低,卻和暖如春揽思,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背见擦。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工钉汗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鲤屡。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓损痰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親执俩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子徐钠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 我們一直都有關(guān)注和閱讀很多關(guān)于Vue3的新特性和功能即將到來(lái)。但是我們沒(méi)有一個(gè)具體的概念在開發(fā)中會(huì)有如何的改變和不...
    bayi_lzp閱讀 835評(píng)論 0 4
  • Vue3.0的優(yōu)勢(shì) 性能比Vue2.x快1.2~2倍 按需編譯役首,體積比Vue2.x更小 組合API(類似React...
    強(qiáng)某某閱讀 1,535評(píng)論 0 5
  • 我們一直都有關(guān)注和閱讀很多關(guān)于Vue3的新特性和功能即將到來(lái)尝丐。但是我們沒(méi)有一個(gè)具體的概念在開發(fā)中會(huì)有如何的改變和不...
    三鉆閱讀 11,757評(píng)論 4 23
  • 因?yàn)檫@個(gè)月的月初給自己定了個(gè)小目標(biāo),學(xué)完Vue3的基本使用衡奥,并使用Vue3親手做一個(gè)小項(xiàng)目(稍微透露一下爹袁,我制作的...
    1kesou閱讀 4,710評(píng)論 0 8
  • Vue 3 的 Template 支持多個(gè)根標(biāo)簽,Vue 2 不支持 Vue 3 有 createApp()矮固,而 ...
    sweetBoy_9126閱讀 30,409評(píng)論 0 15