轉發鏈接:
前言
這幾天 Vue 3.0 Beta 版本發布了 , 本以為是皆大歡喜的一件事情 , 但是論壇里還是看到了很多反對的聲音 。主流的反對論點大概有如下幾點:
意大利面代碼結構吐槽:
“太失望了 。雜七雜八一堆丟在setup里,我還不如直接用react”
我的天,3.0這么搞的話,代碼結構不清晰,語義不明確,無異于把vue自身優點都扔了
怎么感覺代碼結構上沒有2.0清晰了呢這要是代碼量上去了是不是不好維護啊
抄襲 React 吐槽:
抄來抄去沒自己的個性
有react香嗎?越來越像react了
在我看來,Vue 黑暗的一天還遠遠沒有過去,很多人其實并沒有認真的去看 Vue–Api 文檔中的 動機 章節,本文就以這個章節為線索,從 代碼結構、底層原理 等方面來一一打消大家的一些顧慮 。
想學Vue的小伙伴,推薦大家看看這篇《Vue真是太好了 壹萬多字的Vue知識點 超詳細!》
在文章的開頭,首先要表明一下作者的立場,我對于 React 和 Vue 都非常的喜歡 。他們都有著各自的優缺點,本文絕無引戰之意 。兩個框架都很棒!只是各有優缺點而已 。React 的其實也帶來了很多益處,并且 Hook 的思路還是團隊的大佬們首創的,真的是很讓人贊嘆的設計,我對 React 100% 致敬!
設計動機
大如 Vue3 這種全球熱門的框架,任何一個 – 的設計一定有它的深思熟慮和權衡,那么 -api 出現是為了解決什么問題呢?這是一個我們需要首先思考明白的問題 。
首先拋出 Vue2 的代碼模式下存在的幾個問題 。
隨著功能的增長,復雜組件的代碼變得越來越難以維護 。尤其發生你重新接收別人的代碼時 。根本原因是Vue的現有API通過「選項」組織代碼,但是在大部分情況下,通過邏輯考慮來組織代碼更有意義 。缺少一種比較「干凈」的在多個組件之間提取和復用邏輯的機制 。類型推斷不夠友好 。邏輯重用
相信很多接觸過 React Hook 的小伙伴已經對這種模式下組件間邏輯復用的簡單性有了一定的認知 , 自從 React 16.7 發布以來,社區涌現出了海量的 Hook 輪子,以及主流的生態庫 react-,react-redux 等等全部擁抱 Hook,都可以看出社區的同好們對于 Hook 開發機制的贊同 。
其實組件邏輯復用在 React 中是經歷了很長的一段發展歷程的 , mixin -> HOC & -props -> Hook,mixin 是 React 中最早啟用的一種邏輯復用方式 , 因為它的缺點實在是多到數不清,而后面的兩種也有著自己的問題 , 比如增加組件嵌套啊、props 來源不明確啊等等 。可以說到目前為止,Hook 是相對完美的一種方案 。
當然 , 我的一貫風格就是上代碼對比,我就拿 HOC 來說吧, 上的一個真實的開源項目里就出現了這樣的場景:
HOC 對比 Hook
class MenuBar extends React.Component {handleClickNew () {const readyToReplaceProject = this.props.confirmReadyToReplaceProject(this.props.intl.formatMessage(sharedMessages.replaceProjectWarning));this.props.onRequestCloseFile();if (readyToReplaceProject) {this.props.onClickNew(this.props.canSave && this.props.canCreateNew);}this.props.onRequestCloseFile();}handleClickRemix () {this.props.onClickRemix();this.props.onRequestCloseFile();}handleClickSave () {this.props.onClickSave();this.props.onRequestCloseFile();}handleClickSaveAsCopy () {this.props.onClickSaveAsCopy();this.props.onRequestCloseFile();}}export default compose(injectIntl,MenuBarHOC,connect(mapStateToProps,mapDispatchToProps))(MenuBar);復制代碼
沒錯,這里用函數組合了好幾個 HOC,其中還有這種 接受幾個參數返回一個接受組件作為函數的函數 這種東西 , 如果你是新上手(或者哪怕是 React 老手)這套東西的人,你會在 「這個 props 是從哪個 HOC 里來的?」,「這個 props 是外部傳入的還是 HOC 里得到的?」這些問題中迷失了大腦,最終走向墮落(誤) 。
不談 HOC,我的腦子已經快炸開來了 , 來看看用 Hook 的方式復用邏輯是怎么樣的場景吧?
function MenuBar () {// 菜單const { onClickRemix, onClickNew } = useMenuBar()// 國際化const { intl } = useIntl()}export default MenuBar復制代碼
一切都變得很明朗,我可以非常清楚的知道這個方法的來源,intl 是哪里注入進來的 , 點擊了后,就自動跳轉到對應的邏輯,維護和可讀性都極大的提高了 。
當然,這是一個比較「刻意」的例子,但是相信我,我在 React 開發中已經體驗過這種收益了 。隨著組件的「職責」越來越多,只要你掌握了這種代碼組織的思路,那么你的組件并不會膨脹到不可讀 。
常見的請求場景
再舉個非常常見的請求場景 。
在Vue2中如果我需要請求一份數據 , 并且在和error時都展示對應的視圖,一般來說,我們會這樣寫:
failed to loadloading...hello {{fullName}}!import { createComponent, computed } from 'vue'export default {data() {// 集中式的data定義 如果有其他邏輯相關的數據就很容易混亂return {data: {firstName: '',lastName: ''},loading: false,error: false,},},async created() {try {// 管理loadingthis.loading = true// 取數據const data = https://www.jianzixun.com/await this.$axios('/api/user')this.data = data} catch (e) {// 管理errorthis.error = true} finally {// 管理loadingthis.loading = false}},computed() {// 沒人知道這個fullName和哪一部分的異步請求有關 和哪一部分的data有關 除非仔細閱讀// 在組件大了以后更是如此fullName() {return this.data.firstName + this.data.lastName}}}復制代碼
這段代碼 , 怎么樣都談不上優雅,湊合的把功能完成而已,并且對于、error等處理的可復用性為零 。
數據和邏輯也被分散在了各個中,這還只是一個邏輯 , 如果又多了一些邏輯,多了data、、?如果你是一個新接手這個文件的人,你如何迅速的分辨清楚這個是和某兩個data中的字段關聯起來的?
讓我們把zeit/swr的邏輯照搬到Vue3中,
看一下swr在Vue3中的表現:
failed to loadloading...hello {{fullName}}!import { createComponent, computed } from 'vue'import useSWR from 'vue-swr'export default createComponent({setup() {// useSWR幫你管理好了取數、緩存、甚至標簽頁聚焦重新請求、甚至Suspense...const { data, loading, error } = useSWR('/api/user', fetcher)// 輕松的定義計算屬性const fullName = computed(() => data.firstName + data.lastName)return { data, fullName, loading, error }}})復制代碼
就是這么簡單 , 對嗎?邏輯更加聚合了 。
對了,順嘴一提,use-swr 的威力可遠遠不止看到的這么簡單,隨便舉幾個它的能力:
間隔輪詢請求重復數據刪除對于同一個 key 的數據進行緩存對數據進行樂觀更新在標簽頁聚焦的時候重新發起請求分頁支持完備的支持
等等等等……而這么多如此強大的能力,都在一個小小的 () 函數中,誰能說這不是魔法呢?
類似的例子還數不勝數 。
umi-hooks
react-use
代碼組織
上面說了那么多 , 還只是說了 Hook 的其中一個優勢 。這其實并不能解決「意大利面條代碼」的問題 。當邏輯多起來以后,組件的邏輯會糅合在一起變得一團亂麻嗎?
從 Vue 官方的例子講起
舉一個 Vue CLI UI file官方吐槽的例子,這個組件是 Vue-CLI 的 gui 中(也就是平常我們命令行里輸入 vue ui 出來的那個圖形化控制臺)的一個復雜的文件瀏覽器組件,這是 Vue 官方團隊的大佬寫的,相信是比較有說服力的一個案例了 。
這個組件有以下的幾個功能:
跟蹤當前文件夾狀態并顯示其內容處理文件夾導航(打開 , 關閉 , 刷新…)處理新文件夾的創建切換顯示收藏夾切換顯示隱藏文件夾處理當前工作目錄更改
文檔中提出了一個尖銳的靈魂之問,你作為一個新接手的開發人員,能夠在茫茫的 、data、 等選項中一目了然的發現這個變量是屬于哪個功能嗎?比如「創建新文件夾」功能使用了兩個數據屬性,一個計算屬性和一個方法 , 其中該方法在距數據屬性「一百行以上」的位置定義 。
當一個組價中 , 維護同一個邏輯需要跨越上百行的「空間距離」的時候 , 即使是讓我去維護 Vue 官方團隊的代碼 , 我也會暗搓搓的吐槽一句 , 「這寫的什么玩意,這變量干嘛用的!」
尤大很貼心的給出了一張圖,在這張圖中,不同的色塊代表著不同的功能點 。
其實已經做的不錯了,但是在維護起來的時候還是挺災難的 , 比如淡藍色的那個色塊代表的功能 。我想要完整的理清楚它的邏輯,需要「上下反復橫跳」,類似的事情我已經經歷過好多次了 。
而使用 Hook 以后呢?我們可以把「新建文件夾」這個功能美美的抽到一個函數中去:
function useCreateFolder (openFolder) {// originally data propertiesconst showNewFolder = ref(false)const newFolderName = ref('')// originally computed propertyconst newFolderValid = computed(() => isValidMultiName(newFolderName.value))// originally a methodasync function createFolder () {if (!newFolderValid.value) returnconst result = await mutate({mutation: FOLDER_CREATE,variables: {name: newFolderName.value}})openFolder(result.data.folderCreate.path)newFolderName.value = https://www.jianzixun.com/''showNewFolder.value = false}return {showNewFolder,newFolderName,newFolderValid,createFolder}}復制代碼
我們約定這些「自定義 Hook」以 use 作為前綴,和普通的函數加以區分 。
右邊用了 Hook 以后的代碼組織色塊:
我們想要維護紫色部分功能的邏輯,那就在紫色的部分去找就好了,反正不會有其他「色塊」里的變量或者方法影響到它,很快咱就改好了需求模板函數的聲明與實現 , 6 點準時下班!
這是 Hook 模式下的組件概覽 , 真的是一目了然 。感覺我也可以去維護 @vue/ui 了呢(假的) 。
export default {setup() { // ...}}function useCurrentFolderData(networkState) { // ...}function useFolderNavigation({ networkState, currentFolderData }) { // ...}function useFavoriteFolder(currentFolderData) { // ...}function useHiddenFolders() { // ...}function useCreateFolder(openFolder) { // ...}復制代碼
再來看看被吐槽成「意大利面條代碼」的 setup 函數 。
export default {setup () {// Networkconst { networkState } = useNetworkState()// Folderconst { folders, currentFolderData } = useCurrentFolderData(networkState)const folderNavigation = useFolderNavigation({ networkState, currentFolderData })const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData)const { showHiddenFolders } = useHiddenFolders()const createFolder = useCreateFolder(folderNavigation.openFolder)// Current working directoryresetCwdOnLeave()const { updateOnCwdChanged } = useCwdUtils()// Utilsconst { slicePath } = usePathUtils()return {networkState,folders,currentFolderData,folderNavigation,favoriteFolders,toggleFavorite,showHiddenFolders,createFolder,updateOnCwdChanged,slicePath}}}復制代碼
這是誰家的小仙女這么美?。≌飴嘸?蔡?邐?髁? ,和意大利面沒半毛錢關系?。?
從獲取鼠標位置的需求講起
我們有這樣一個跨組件的需求,我想在組件里獲得一個響應式的變量,能實時的指向我鼠標所在的位置 。
Vue 官方給出的自定義 Hook 的例子是這樣的:
import { ref, onMounted, onUnmounted } from 'vue'export function useMousePosition() {const x = ref(0)const y = ref(0)function update(e) {x.value = https://www.jianzixun.com/e.pageXy.value = e.pageY}onMounted(() => {window.addEventListener('mousemove', update)})onUnmounted(() => {window.removeEventListener('mousemove', update)})return { x, y }}復制代碼
在組件中使用:
import { useMousePosition } from './mouse'export default {setup() {const { x, y } = useMousePosition()// other logic...return { x, y }}}復制代碼
就這么簡單,無需多言 。
對比Hook 和 Mixin & HOC 對比
說到這里,還是不得不把官方對于「Mixin & HOC 模式」所帶來的缺點整理一下 。
渲染上下文中公開的屬性的來源不清楚 。例如,當使用多個mixin讀取組件的模板時,可能很難確定從哪個mixin注入了特定的屬性 。命名空間沖突 。可能會在屬性和方法名稱上發生沖突 , 而HOC可能會在預期的prop名稱上發生沖突 。性能問題 , HOC和無渲染組件需要額外的有狀態組件實例 , 這會降低性能 。
而 「Hook」模式帶來的好處則是:
暴露給模板的屬性具有明確的來源,因為它們是從 Hook 函數返回的值 。Hook 函數返回的值可以任意命名 , 因此不會發生名稱空間沖突 。沒有創建僅用于邏輯重用的不必要的組件實例 。
當然 , 這種模式也存在一些缺點,比如 ref 帶來的心智負擔,詳見 。
React Hook 和 Vue Hook 對比
其實 React Hook 的限制非常多,比如官方文檔中就專門有一個章節介紹它的限制:
不要在循環,條件或嵌套函數中調用 Hook確保總是在你的 React 函數的最頂層調用他們 。遵守這條規則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調用 。這讓 React 能夠在多次的和調用之間保持 hook 狀態的正確 。
而 Vue 帶來的不同在于:
與React Hooks相同級別的邏輯組合功能,但有一些重要的區別 。與React Hook不同,setup 函數僅被調用一次,這在性能上比較占優 。對調用順序沒什么要求,每次渲染中不會反復調用 Hook 函數,產生的的 GC 壓力較小 。不必考慮幾乎總是需要的問題,以防止傳遞函數prop給子組件的引用變化,導致無必要的重新渲染 。React Hook 有臭名昭著的閉包陷阱問題(甚至成了一道熱門面試題 , omg),如果用戶忘記傳遞正確的依賴項數組,和可能會捕獲過時的變量,這不受此問題的影響 。Vue的自動依賴關系跟蹤確保觀察者和計算值始終正確無效 。不得不提一句,React Hook 里的「依賴」是需要你去手動聲明的 , 而且官方提供了一個插件 , 這個插件雖然大部分時候挺有用的,但是有時候也特別煩人,需要你手動加一行丑陋的注釋去關閉它 。
我們認可React Hooks的創造力,這也是 Vue–Api 的主要靈感來源 。上面提到的問題確實存在于 React Hook 的設計中 , 我們注意到Vue的響應式模型恰好完美的解決了這些問題 。
順嘴一題,React Hook 的心智負擔是真的很嚴重,如果對此感興趣的話,請參考:
使用react hooks帶來的收益抵得過使用它的成本嗎? – 李元秋的回答 – 知乎 …
并且我自己在實際開發中 , 也遇到了很多問題,尤其是在我想對組件用 memo 進行一些性能優化的時候,閉包的問題爆炸式的暴露了出來 。最后我用大法解決了其中很多問題,讓我不得不懷疑這從頭到尾會不會就是 Dan 的陰謀……(別想逃過 )
React Hook + TS 購物車實戰(性能優化、閉包陷阱、自定義hook)
原理
既然有對比,那就從原理的角度來談一談兩者的區別 ,
在 Vue 中,之所以 setup 函數只執行一次,后續對于數據的更新也可以驅動視圖更新,歸根結底在于它的「響應式機制」,比如我們定義了這樣一個響應式的屬性:
Vue
{{count}}export default {setup() {const count = ref(0)const add = () => count.value++return { count, add }}}復制代碼
這里雖然只執行了一次 setup 但是 count 在原理上是個 「響應式對象」,對于其上 value 屬性的改動,
是會觸發「由編譯而成的函數」 的重新執行的 。
如果需要在 count 發生變化的時候做某件事 , 我們只需要引入函數:
{{count}}export default {setup() {const count = ref(0)const add = () => count.value++effect(function log(){console.log('count changed!', count.value)})return { count, add }}}復制代碼
這個 log 函數只會產生一次,這個函數在讀取 count.value 的時候會收集它作為依賴,那么下次 count.value 更新后 , 自然而然的就能觸發 log 函數重新執行了 。
仔細思考一下這之間的數據關系,相信你很快就可以理解為什么它可以只執行一次,但是卻威力無窮 。
實際上 Vue3 的 Hook 只需要一個「初始化」的過程,也就是 setup,命名很準確 。它的關鍵字就是「只執行一次」 。
React
同樣的邏輯在 React 中 , 則是這樣的寫法:
export default function Counter() {const [count, setCount] = useState(0)const add = () => setCount(prev => prev + 1)// 下文講解用const [count2, setCount2] = useState(0)return ({count})}復制代碼
它是一個函數,而父組件引入它是通過這種方式引入的,實際上它會被編譯成 React.() 這樣的函數執行 , 也就是說每次渲染,這個函數都會被完整的執行一次 。
而返回的 count 和則會被保存在組件對應的 Fiber 節點上 , 每個 React 函數每次執行 Hook 的順序必須是相同的,舉例來說 。這個例子里的在初次執行的時候,由于執行了兩次 ,會在 Fiber 上保存一個 { value,} -> { ,} 這樣的鏈表結構 。
而下一次渲染又會執行 count 的 、的 , 那么 React 如何從 Fiber 節點上找出上次渲染保留下來的值呢?當然是只能按順序找啦 。
第一次執行的就拿到第一個 { value,},第二個執行的就拿到第二個 { ,} ,
這也就是為什么 React 嚴格限制 Hook 的執行順序和禁止條件調用 。
假如第一次渲染執行量次 ,而第二次渲染時第一個被 if 條件判斷給取消掉了,那么第二個的就會拿到鏈表中第一條的值,完全混亂了 。
如果在 React 中,要監聽 count 的變化做某些事的話,會用到的話,那么下次
之后會把前后兩次中拿到的的第二個參數 deps 依賴值進行一個逐項的淺對比(對前后每一項依次調用 .is),比如
export default function Counter() {const [count, setCount] = useState(0)const add = () => setCount(prev => prev + 1)useEffect(() => {console.log('count updated!', count)}, [count])return ({count})}復制代碼
那么,當 React 在渲染后發現 count 發生了變化 , 會執行中的回調函數 。(細心的你可以觀察出來,每次渲染都會重新產生一個函數引用,也就是的第一個參數) 。
是的,React 還是不可避免的引入了 依賴 這個概念,但是這個 依賴 是需要我們去手動書寫的,實時上 React 社區所討論的「心智負擔」也基本上是由于這個 依賴 所引起的……
由于每次渲染都會不斷的執行,這當中所不斷產生的閉包和性能也會稍遜于 Vue3 。那么它的關鍵字是「每次渲染都重新執行」 。
關于抄襲 React Hook
其實前端開源界談抄襲也不太好,一種新的模式的出現的值得框架之間相互借鑒和學習的,畢竟框架歸根結底的目的不是為了「標榜自己的特立獨行」,而是「方便廣大開發者」 。這是值得思考的一點,很多人似乎覺得一個框架用了某種模式模板函數的聲明與實現,另一個框架就不能用,其實這對于框架之間的進步和發展并沒有什么好處 。
這里直接引用尤大在 17 年回應「Vue 借鑒虛擬dom」的一段話吧:
再說 vdom 。React 的 vdom 其實性能不怎么樣 。Vue 2.0 引入 vdom 的主要原因是 vdom 把渲染過程抽象化了,從而使得組件的抽象能力也得到提升,并且可以適配 DOM 以外的渲染目標 。這一點是借鑒 React 毫無爭議 , 因為我認為 vdom 確實是個好思想 。但要分清楚的是 Vue 引入 vdom 不是因為『react 有所以我們也要有』,而是因為它確實有技術上的優越性 。社區里基于 vdom 思想造的輪子海了去了,而 ng2 的渲染抽象層和 Ember2 的模板 ->編譯也跟 vdom 有很多思想上的相似性 。
這段話如今用到 Hook 上還是一樣的適用,程序員都提倡開源精神,怎么到了 Vue 和 React 之間有些人又變得小氣起來了呢?說的難聽點,Vue 保持自己的特立獨行,那你假如換了一家新公司要你用 Vue,你不是又得從頭學一遍嘛 。
更何況 React 社區也一樣有對 Vue 的借鑒,比如你看 react-@6 的 api,你會發現很多地方和 vue- 非常相似了 。比如的「配置式路由」,以及在組件中使子路由的代碼結構等等 。當然這只是我淺顯的認知 , 不對的地方也歡迎指正 。
【Vue 3.0 Beta 和React 開發者分別杠上了】本文到此結束,希望對大家有所幫助 。
- 互聯網新形態Web3.0到底是什么?與比特幣什么關系
- 面試被問到「對 vue 的看法」,怎么回答?
- vue和react的區別是什么
- cnBeta@“Snowda”的樂趣:星戰迷們打造可愛的尤達寶寶雪人
- cnBeta@美國一名7歲男孩不小心吞下AirPod被送往急診室檢查
- 你找的圖解在這里
