精讀《Vue3.0 Function API》 1. 引言 2. 概述 3. 精讀 4. 總結

1. 引言

Vue 3.0 的發佈引起了軒然大波,讓我們解讀下它的 function api RFC 詳細瞭解一下 Vue 團隊是怎麼想的吧!

首先官方回答了幾個最受關注的問題:

Vue 3.0 是否有 break change,就像 Python 3 / Angular 2 一樣?

不,100% 兼容 Vue 2.0,且暫未打算廢棄任何 API(未來也不)。之前有草案試圖這麼做,但由於用戶反饋太猛,被撤回了。

Vue 3.0 的設計蓋棺定論了嗎?

沒有呀,這次精讀的稿子就是 RFC(Request For Comments),翻譯成中文就是 “意見徵求稿”,還在徵求大家意見中哦。

這 RFC 咋這麼複雜?

RFC 是寫給貢獻者/維護者的,要考慮許多邊界情況與細節,所以當然會複雜很多嘍!當然 Vue 本身使用起來還是很簡單的。

Vue 本身 Mutable + Template 就註定了是個用起來簡單(約定 + 自然),實現起來複雜(解析 + 雙綁)的框架。

這次改動很像在模仿 React,爲啥不直接用 React?

首先 Template 機制還是沒變,其次模仿的是 Hooks 而不是 React 全部,如果你不喜歡這個改動,那你更不會喜歡用 React。

PS: 問這個問題的人,一定沒有同時理解 React 與 Vue,其實這兩個框架到現在差別蠻大的,後面精讀會詳細說明。

下面正式進入 Vue 3.0 Function API 的介紹。

2. 概述

Vue 函數式基本 Demo:

<template>
  <div>
    <span>count is {{ count }}</span>
    <span>plusOne is {{ plusOne }}</span>
    <button @click="increment">count++</button>
  </div>
</template>

<script>
import { value, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    // reactive state
    const count = value(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}
</script>

函數式風格的入口是 setup 函數,採用了函數式風格後可以享受如下好處:類型自動推導、減少打包體積。

setup 函數返回值就是注入到頁面模版的變量。我們也可以返回一個函數,通過使用 value 這個 API 產生屬性並修改:

import { value } from 'vue'

const MyComponent = {
  setup(props) {
    const msg = value('hello')
    const appendName = () => {
      msg.value = `hello ${props.name}`
    }
    return {
      msg,
      appendName
    }
  },
  template: `<div @click="appendName">{{ msg }}</div>`
}

要注意的是,value() 返回的是一個對象,通過 .value 才能訪問到其真實值。

爲何 value() 返回的是 Wrappers 而非具體值呢?原因是 Vue 採用雙向綁定,只有對象形式訪問值才能保證訪問到的是最終值,這一點類似 React 的 useRef() API 的 .current 規則。

那既然所有 value() 返回的值都是 Wrapper,那直接給模版使用時要不要調用 .value 呢?答案是否定的,直接使用即可,模版會自動 Unwrapping:

const MyComponent = {
  setup() {
    return {
      count: value(0)
    }
  },
  template: `<button @click="count++">{{ count }}</button>`
}

接下來是 Hooks,下面是一個使用 Hooks 實現獲得鼠標實時位置的例子:

function useMouse() {
  const x = value(0)
  const y = value(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

// in consuming component
const Component = {
  setup() {
    const { x, y } = useMouse()
    const { z } = useOtherLogic()
    return { x, y, z }
  },
  template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}

可以看到,useMouse 將所有與 “處理鼠標位置” 相關的邏輯都封裝了進去,乍一看與 React Hooks 很像,但是有兩個區別:

  1. useMouse 函數內改變 xy 後,不會重新觸發 setup 執行。
  2. x y 拿到的都是 Wrapper 而不是原始值,且這個值會動態變化。

另一個重要 API 就是 watch,它的作用類似 React Hooks 的 useEffect,但實現原理和調用時機其實完全不一樣。

watch 的目的是監聽某些變量變化後執行邏輯,比如當 id 變化後重新取數:

const MyComponent = {
  props: {
    id: Number
  },
  setup(props) {
    const data = value(null)
    watch(() => props.id, async (id) => {
      data.value = await fetchData(id)
    })
  }
}

之所以要 watch,因爲在 Vue 中,setup 函數僅執行一次,所以不像 React Function Component,每次組件 props 變化都會重新執行,因此無論是在變量、props 變化時如果想做一些事情,都需要包裹在 watch 中。

後面還有 unwatching、生命週期函數、依賴注入,都是一些語法定義,感興趣可以繼續閱讀原文,筆者就不贅述了。

3. 精讀

對於 Vue 3.0 的 Function API + Hooks 與 React Function Component + Hooks,筆者做一些對比。

Vue 與 React 邏輯結構

React Function Component 與 Hooks,雖然在實現原理上,與 Vue3.0 存在 Immutable 與 Mutable、JSX 與 Template 的區別,但邏輯理解上有着相通之處。

const MyComponent = {
  setup(props) {
    const x = value(0)

    const setXRandom = () => {
      x.value = Math.random()
    }

    return { x, setXRandom }
  },
  template: `
    <button @onClick="setXRandom"/>{{x}}</button>
  `
}

雖然在 Vue 中,setup 函數僅執行一次,看上去與 React 函數完全不一樣(React 函數每次都執行),但其實 Vue 將渲染層(Template)與數據層(setup)分開了,而 React 合在了一起。

我們可以利用 React Hooks 將數據層與渲染層完全隔離:

// 類似 vue 的 setup 函數
function useMyComponentSetup(props) {
  const [x, setX] = useState(0)

  const setXRandom = useCallback(() => {
    setX(Math.random())
  }, [setX])

  return { x, setXRandom }
}

// 類似 vue 的 template 函數
function MyComponent(props: { name: String }) {
  const { x, setXRandom } = useMyComponentSetup(props)

  return (
    <button onClick={setXRandom}>{x}</button>
  )
}

這源於 JSX 與 Template 的根本區別。JSX 使模版與 JS 可以寫在一起,因此數據層與渲染層可以耦合在一起寫(也可以拆分),但 Vue 採取的 Template 思路使數據層強制分離了,這也使代碼分層更清晰了。

而實際上 Vue3.0 的 setup 函數也是可選的,再配合其支持的 TSX 功能,與 React 真的只有 Mutable 的區別了:

// 這是個 Vue 組件
const MyComponent = createComponent((props: { msg: string }) => {
  return () => h('div', props.msg)
})

我們很難評價 Template 與 JSX 的好壞,但爲了更透徹的理解 Vue 與 React,需要拋開 JSX&Template,Mutable&Immutable 去看,其實去掉這兩個框架無關的技術選型,React@16 與 Vue@3 已經非常像了。

Vue3.0 的精髓是學習了 React Hooks 概念,因此正好可以用 Hooks 在 React 中模擬 Vue 的 setup 函數。

關於這兩套技術選型,已經是相對完美的組合,不建議在 JSX 中再實現類似 Mutable + JSX 的花樣來(因爲喜歡 Mutable 可以用 Vue 呀):

  • Vue:Mutable + Template
  • React:Immutable + JSX

真正影響編碼習慣的就是 Mutable 與 Immutable,使用 Vue 就堅定使用 Mutable,使用 React 就堅定使用 Immutable,這樣能最大程度發揮兩套框架的價值。

Vue Hooks 與 React Hooks 的差異

先看 React Hooks 的簡單語法:

const [ count, setCount ] = useState(0)

const setToOne = () => setCount(1)

Vue Hooks 的簡單語法:

const count = value(0)

const setToOne = () => count.value = 1

之所以 React 返回的 count 是一個數字,是因爲 Immutable 規則,而 Vue 返回的 count 是個對象,擁有 count.value 屬性,也是因爲 Vue Mutable 規則導致,這使得 Vue 定義的所有變量都類似 React 中 useRef 定義變量,因此不存 React capture value 的特性。

關於 capture value 更多信息,可以閱讀 精讀《Function VS Class 組件》 Capute Value 介紹

另外,對於 Hooks 的值變更機制也不同,我們看 Vue 的代碼:

const Component = {
  setup() {
    const { x, y } = useMouse()
    const { z } = useOtherLogic()
    return { x, y, z }
  },
  template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}

由於 setup 函數僅執行一次,怎麼做到當 useMouse 導致 xy 值變化時,可以在 setup 中拿到最新的值?

在 React 中,useMouse 如果修改了 x 的值,那麼使用 useMouse 的函數就會被重新執行,以此拿到最新的 x,而在 Vue 中,將 Hooks 與 Immutable 深度結合,通過包裝 x.value,使得當 x 變更時,引用保持不變,僅值發生了變化。所以 Vue 利用 Proxy 監聽機制,可以做到 setup 函數不重新執行,但 Template 重新渲染的效果。

這就是 Immutable 的好處,Vue Hooks 中,不需要 useMemo useCallback useRef 等機制,僅需一個 value 函數,直觀的 Mutable 修改,就可以實現 React 中一套 Immutable 性能優化後的效果,這個是 Mutable 的魅力所在。

Vue Hooks 的優勢

筆者對 RFC 中對 Vue、React Hooks 的對比做一個延展解釋:

首先最大的不同:setup 僅執行一遍,而 React Function Component 每次渲染都會執行。

Vue 的代碼使用更符合 JS 直覺。

這句話直截了當戳中了 JS 軟肋,JS 並非是針對 Immutable 設計的語言,所以 Mutable 寫法非常自然,而 Immutable 的寫法就比較彆扭。

當 Hooks 要更新值時,Vue 只要用等於號賦值即可,而 React Hooks 需要調用賦值函數,當對象類型複雜時,還需藉助第三方庫才能保證進行了正確的 Immutable 更新。

對 Hooks 使用順序無要求,而且可以放在條件語句裏。

對 React Hooks 而言,調用必須放在最前面,而且不能被包含在條件語句裏,這是因爲 React Hooks 採用下標方式尋找狀態,一旦位置不對或者 Hooks 放在了條件中,就無法正確找到對應位置的值。

而 Vue Function API 中的 Hooks 可以放在任意位置、任意命名、被條件語句任意包裹的,因爲其並不會觸發 setup 的更新,只在需要的時候更新自己的引用值即可,而 Template 的重渲染則完全繼承 Vue 2.0 的依賴收集機制,它不管值來自哪裏,只要用到的值變了,就可以重新渲染了。

不會再每次渲染重複調用,減少 GC 壓力。

這確實是 React Hooks 的一個問題,所有 Hooks 都在渲染閉包中執行,每次重渲染都有一定性能壓力,而且頻繁的渲染會帶來許多閉包,雖然可以依賴 GC 機制回收,但會給 GC 帶來不小的壓力。

而 Vue Hooks 只有一個引用,所以存儲的內容就非常精簡,也就是佔用內存小,而且當值變化時,也不會重新觸發 setup 的執行,所以確實不會造成 GC 壓力。

必須要總包裹 useCallback 函數確保不讓子元素頻繁重渲染。

React Hooks 有一個問題,就是完全依賴 Immutable 屬性。而在 Function Component 內部創建函數時,每次都會創建一個全新的對象,這個對象如果傳給子組件,必然導致子組件無法做性能優化。 因此 React 採取了 useCallback 作爲優化方案:

const fn = useCallback(() => /* .. */, [])

只有當第二個依賴參數變化時才返回新引用。但第二個依賴參數需要 lint 工具確保依賴總是正確的(關於爲何要對依賴誠實,感興趣可以移步 精讀《Function Component 入門》 - 永遠對依賴誠實)。

回到 Vue 3.0,由於 setup 僅執行一次,因此函數本身只會創建一次,不存在多實例問題,不需要 useCallback 的概念,更不需要使用 lint 插件 保證依賴書寫正確,這對開發者是實實在在的友好。

不需要使用 useEffect useMemo 等進行性能優化,所有性能優化都是自動的。

這也是實在話,畢竟 Mutable + 依賴自動收集就可以做到最小粒度的精確更新,根本不會觸發不必要的 Rerender,因此 useMemo 這個概念也不需要了。

useEffect 也需要傳遞第二個參數 “依賴項”,在 Vue 中根本不需要傳遞 “依賴項”,所以也不會存在用戶不小心傳錯的問題,更不需要像 React 寫一個 lint 插件保證依賴的正確性。(這也是筆者想對 React Hooks 吐槽的點,React 團隊如何保障每個人都安裝了 lint?就算裝了 lint,如果 IDE 有 BUG,導致沒有生效,隨時可能寫出依賴不正確的 “危險代碼”,造成比如死循環等嚴重後果)

4. 總結

通過對比 Vue Hooks 與 React Hooks 可以發現,Vue 3.0 將 Mutable 特性完美與 Hooks 結合,規避了一些 React Hooks 的硬傷。所以我們可以說 Vue 借鑑了 React Hooks 的思想,但創造出來的確實一個更精美的藝術品。

但 React Hooks 遵循的 Immutable 也有好的一面,就是每次渲染中狀態被穩定的固化下來了,不用擔心狀態突然變更帶來的影響(其實反而要注意狀態用不變更帶來的影響),對於數據記錄、程序運行的穩定性都有較高的可預期性。

最後,對於喜歡 Mutable 的開發者,Vue 3.0 是你的最佳選擇,基於 React + Mutable 搞的一些小輪子做到頂級可能還不如 Vue 3.0。對於 React 開發者來說,堅持你們的 Immutable 信仰吧,Vue 3.0 已經將 Mutable 發揮到極致,只有將 React Immutable 特性發揮到極致才能發揮 React 的最大價值。

討論地址是:精讀《Vue3.0 Function API》 · Issue #173 · dt-fe/weekly

如果你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章