對比學習VR - 邏輯復用的進化

在過去一年的時間里,我都是用Vue開發(fā)項目。對于這種高度封裝的框架,在習慣了Vue的語法后,常處于一種“面向Vue開發(fā)”的錯覺中。近期有React相關(guān)的需求,寫起來非常不適應,想重溫React語法,發(fā)現(xiàn)已經(jīng)有了中文文檔,也更新了Hook相關(guān)的知識,自己目前還是個弱雞,在此粗淺寫下學習后的感想。

這篇文章只做概覽,并不是學習邏輯復用的最好途徑,推薦:

【React深入】從Mixin到HOC再到Hook - 掘金

文中提到了“正值壯年”的高階組件(Higher-Order Components)非常多的用途以及注意事項。

函數(shù)式組件

函數(shù)式組件是定義React組件最簡單的方式:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

函數(shù)式組件在React早期擔任無狀態(tài)函數(shù)式組件(Stateless Functional Component)的角色。無狀態(tài)概念在Vue中也有體現(xiàn),即函數(shù)式組件(functional component),兩者名字非常接近,作用也非常接近,由于他們只是函數(shù),提供了更低的渲染開銷(無this)。

在單文件組件(Single File Component)中,實現(xiàn)相同功能的代碼如下所示:

<template functional>
  <h1>Hello, {{props.name}}</h1>
</template>

值得注意的是,這種函數(shù)式組件的聲明方式將在Vue 3.0中被放棄,改為僅能在普通的函數(shù)中聲明:

rfcs/0000-functional-async-api-change.md at functional-async-api-change · vuejs/rfcs · GitHub

In 3.x, we intend to support functional componentsonly as plain functions

import { h } from 'vue'

const FunctionalComp = (props, slots) => {
  return h('div', `Hello! ${props.name}`)
}

盡早擁抱JSX吧。

但是在這里順帶一提,Vue的JSX支持讓我覺得非常怠惰,我最近嘗試的時候,官方倉庫里提到的下面這種寫法事實上已經(jīng)不生效了:

<input vOn:click={this.newTodoText} />

反而是onClick起作用。詳見GitHub - vuejs/jsx: monorepo for Babel / Vue JSX related packages

在16.7版本后,React的Typescript聲明文件中的React.SFC被廢棄,由React.FC取而代之,原因可以在下面這個PR中看到:

Rename React’s SFC to ‘FunctionalComponent’ by rhysforyou · Pull Request #30364 · DefinitelyTyped/DefinitelyTyped · GitHub

摘錄關(guān)鍵如下:

The motivation for this is twofold: firstly the addition of hooks means function components can have state, and secondly the term “stateless component” can equally apply to class components which don’t use any state, causing some confusion.

即:

  1. 由于Hooks的加入,函數(shù)式組件可以擁有狀態(tài);
  2. 在class語法中,也有無狀態(tài)組件的說法,兩者可引起混淆;

Hook是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。

import React, { useState } from 'react';

function Example() {
  // 聲明一個新的叫做 “count” 的 state 變量
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Hook僅能夠在React的函數(shù)式組件中使用,相當于是原函數(shù)式組件的一個擴展,抽象來看,Hook作為一種思想,完全是可以獨立于React之外的,這一點尤雨溪老師在下面這個倉庫里做了很好的示范:

GitHub - yyx990803/vue-hooks: Experimental React hooks implementation in Vue

用庫中所提供的寫法,完成上述同樣功能的Vue代碼大致如下所示:

import { hooks, useData, useComputed } from 'vue-hooks'
Vue.use(hooks)
new Vue({
  template: `
    <div @click="data.count++">
      {{ data.count }} {{ double }}
    </div>
  `,
  hooks() {
    const data = useData({ count: 0 })
    const double = useComputed(() => data.count * 2)
    return { data, double }
  }
})

這里有一篇很棒的文章介紹了如何使用useState、useEffect進行數(shù)據(jù)的處理(包括異步處理、異常處理等):

How to fetch data with React Hooks? - RWieruch

關(guān)于Vue中所提供的useComputed,在React中也有相應的useMemo可以使用:Hook API 索引 – useMemo

Mixin

天下大勢,分久必合。條條大路,都通向了邏輯復用的難題。

一個Vue中邏輯被生命周期所分隔的例子如下所示:

export default {
  ...other,
  mounted() { register(); },
  beforeDestroy() { unregister(); }
}

但Vue其實也提供了對策:

export default {
  ...other,
  mounted() {
    register();
    this.$once('hook:beforeDestroy', () => {
      unregister();
    })
  }
}

在獨此一處的情形下,這樣的可讀性更強,也更便于邏輯抽離。但是要在多組件中復用邏輯,這種寫法也難免需要進行代碼的復制粘貼。

Vue和React都提供了Mixin:

image.png

在實際開發(fā)的過程中,Mixin的做法是有問題的,這里有一些關(guān)于Mixin的壞處的摘錄:

  1. Mixin引入了隱式依賴(Mixins introduce implicit dependencies)
    一個例子:當Mixin依賴組件內(nèi)部屬性時,更改組件屬性會使Mixin失效
  2. Mixin引起了命名沖突(Mixins cause name clashes)
  3. Mixin復雜度有滾雪球效應(Mixins cause snowballing complexity)

高階組件

高階組件是目前實現(xiàn)邏輯復用非常流行的手段。能夠幫助我們解決一些橫切關(guān)注點。關(guān)于什么是橫切關(guān)注點,可以觀看下面這個視頻:

生化危機6中的香港記者

嚴肅點,還是看下面這個鏈接吧。

使用 HOC 解決橫切關(guān)注點問題

我們先來回憶一下高階函數(shù),下面這個也許是非常不合理的例子利用高階函數(shù)給原函數(shù)附上了打點功能:

// 高階函數(shù)

function roar() {
  console.log('hello');
}

const logRoar = (() => {
  return function addLog() {
    console.log('log');
    roar();
  };
})();

logRoar();

// log
// hello

React中的高階組件(來源):

function logHoc(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      this.start = Date.now();
    }
    componentDidMount() {
      this.end = Date.now();
      console.log(`${WrappedComponent.dispalyName} 渲染時間:${this.end - this.start} ms`);
      console.log(`${user}進入${WrappedComponent.dispalyName}`);
    }
    componentWillUnmount() {
      console.log(`${user}退出${WrappedComponent.dispalyName}`);
    }
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

“繼承”是不是解決問題的方案呢?React在組合 vs 繼承 – React一文中提到:

在 Facebook,我們在成百上千個組件中使用 React。我們并沒有發(fā)現(xiàn)需要使用繼承來構(gòu)建組件層次的情況。

如果你想要在組件間復用非 UI 的功能,我們建議將其提取為一個單獨的 JavaScript 模塊,如函數(shù)、對象或者類。組件可以直接引入(import)而無需通過 extend 繼承它們。

在非常年輕的語言golang(2009年)中,也推薦使用組合來進行代碼的組織:

type Car struct {
    weight int
    name   string
}

type Bike struct {
    Car
    lunzi int
}

相比繼承,組合更為靈活,且便于擴展。

但高階組件也有自己的問題,在一些復雜的情況下,我們可能會寫出俄羅斯套娃式的組件,并且有可能引起命名沖突,這些情況與Mixin的處境是類似的。

Vue 組合函數(shù)

組合函數(shù)(Composition Function)是Vue 3.0中最重要的變化,下面這篇文章我想業(yè)內(nèi)多數(shù)人已經(jīng)看過:

Vue Function-based API RFC - 知乎

這是文中的一段鼠標偵聽的例子:

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 }
}

// 在組件中使用該函數(shù)
const Component = {
  setup() {
    const { x: myX, y } = useMouse()
    // 與其它函數(shù)配合使用
    const { z } = useOtherLogic()
    return { myX, y, z }
  },
  template: `<div>{{ myX }} {{ y }} {{ z }}</div>`
}

摘錄文中的話如下:

  1. 暴露給模版的屬性來源清晰(從函數(shù)返回)
  2. 返回值可以被任意重命名,所以不存在命名空間沖突;
  3. 沒有創(chuàng)建額外的組件實例所帶來的性能損耗。

另外:

  1. 使用時不需要關(guān)注鼠標位置內(nèi)部的實現(xiàn)邏輯;
  2. 不需要關(guān)注調(diào)用順序(React Hook約束這一點);

下面簡單舉一個我自己在開發(fā)中遇到的例子,這是通過Mixin復用Vant單例Toast Loading的代碼:

import Vue from 'vue';
import { Toast } from 'vant';
export default Vue.extend({
  data() {
    return {
      // 可以看到我在命名這方面下了很大的功夫
      // 實際開發(fā)可以感受到 $ 讓人很不爽 雙擊無法選中全名
      $_m_loadingToaster: '',
    };
  },
  methods: {
    // 附帶文案的Toast
    startLoading(message = '加載中') {
      this.$_m_loadingToaster = Toast.loading({ message, duration: 0, mask: true });
    },
    // 停止loading
    stopLoading() {
      this.$_m_loadingToaster.clear();
    },
  },
});

我們使用Vue的新API實現(xiàn)會是怎樣的效果呢?

import { value } from 'vue';
import { Toast } from 'vant';
function useLoading() {
  const toaster = value('');
  const startLoading = () => {
    toaster.value = Toast.loading({ message, duration: 0, mask: true });
  }
  const stopLoading = () => {
    toaster.value.clear();
  }
  return { startLoading, stopLoading }
}

在組件中使用:

// 在組件中使用該函數(shù)
const Component = {
  setup() {
    const { startLoading, stopLoading } = useLoading()
    return { startLoading, stopLoading };
  }
}

看起來是不是好多了?來源清晰,作用可控,且沒有新建Vue實例,引起不必要的性能開銷。

當然也有不舒服的地方。

比如對于重型組件來說,在上方的setup函數(shù)中將要返回非常多的值才能在template中使用(展開運算符不能解決命名沖突且沒有直觀顯示變量來源),看著是非常多余的,RFC中也承認了這一點:

Hooks 代碼和 JSX 并置使得對值的使用更簡潔也是其優(yōu)點。

還有,怎么在composition function中使用scoped css呢?用css module嗎?

另外,目前template的高亮問題,以及code intelligence支持非常不友好,當然對于大佬們來說這都是一個插件能解決的小事情。

最后

Vue和React越走越近。在我現(xiàn)在的直覺里,Vue倡導數(shù)據(jù)驅(qū)動,提供了非常多常用的設(shè)計模式語法糖,React倡導你自己弄。

回想起尤大之前在知乎的回答:

React 從一開始的定位就是提出 UI 開發(fā)的新思路。

Vue 從一開始的定位就是盡可能的降低前端開發(fā)的門檻,讓更多的人能夠更快地上手開發(fā)。

可見將來的Vue 3,也不會脫離擁抱傳統(tǒng)、關(guān)注點分離的核心定位的,不會對現(xiàn)有的Vue項目造成毀滅性的打擊。

我個人覺得編輯器的支持也是非常重要的一方面,如果template也能夠得到同JSX般的支持(代碼提示、跳轉(zhuǎn)到定義、跳轉(zhuǎn)到class等),不過偶爾會心里想這么一下,支持得這么緩慢,是在等我去寫嗎?

參考文章

  1. 【React深入】從Mixin到HOC再到Hook - 掘金
  2. Vue Function-based API RFC - 知乎
  3. Hook 簡介 – React
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容