重構過程當中,確定會遇到新的代碼如何作技術選型的問題,要考慮到這套技術的生命力,也就是他是不是更新的技術,還有他的靈活和拓展性,指望可以達到在將來至少 3 年內不須要作大的技術棧升級。個人此次重構經歷是把 jQuery 的代碼變爲 React ,你品品,算是最難,勞動最密集的重構任務了吧。看多了以前代碼動輒上千行的 Class ,混亂的全局變量使用,愈來愈以爲,代碼必定要寫的簡單,不要使用過多的黑科技,尤爲是各類設計模式,爲了複用而迭代出來的海量 if 判斷。代碼不是給機器看的,是給人看的,他須要讓後來人快速的看懂,還要能讓別人在你的代碼的基礎上快速的迭代新的需求。因此咱們須要想清楚,用什麼技術棧,怎麼組織代碼。javascript
對於 Class Component 和 Function Component 之爭由來已久。從我自身的實踐來看,我以爲這是兩種不一樣的編程思路。css
Class Component | 面向對象編程 | 繼承 | 生命週期 |
---|---|---|---|
Function Component | 函數式編程 | 組合 | 數據驅動 |
首先,若是咱們使用面向對象這種編程方式,咱們要注意,他不僅是定義一個 Class 那麼簡單的事情,咱們知道面向對象有三大特性,繼承,封裝,多態。前端
首先前端真的適合繼承的方式嗎?準確的說,UI 真的適合繼承的方式嗎?在真實世界裏,抽象的東西更適合定義成一個類,類原本的意思就是分類和類別,正如咱們把老虎,貓,獅子這些生物統稱爲動物,因此咱們就能夠定義一個動物的類,可是真實世界並無動物這種實體,可是頁面 UI 都是真實存在能夠看到的東西,咱們能夠把一個頁面分紅不一樣的區塊,而後區塊之間採用的是「組合」的方式。所以我認爲 UI 組件不適合繼承,更應該組合。若是你寫過繼承類的組件,你將很難去重構,甚至是重寫他。java
封裝講究使用封裝好的方法對外暴露類中的屬性,可是咱們的組件基本是經過 props 暴露內部事件和數據,經過 Ref 暴露內部方法,本質上並無使用封裝的特性。react
多態就更少用了,多態更可能是基於接口,或者抽象類的,可是 JS 這塊比較弱,用 TS 或許會好一些。程序員
綜上,做爲前端 UI 編程,我更傾向於使用函數組合的方式。編程
不管是在 React 或者在 Vue 裏,都講究數據的變化,數據與視圖的綁定關係,數據驅動,數據的變化引發 UI 的從新渲染,可是生命週期在描述這個問題的時候,並不直接,在 Class Component 裏,咱們如何檢測某個數據的變化呢,基本是用 shouldUpdate 的生命週期,爲何咱們在編程的時候,正在關注數據和業務的時候,還要關心一個生命週期呢,這部份內容對於業務來講更像是反作用,或者不該該暴露給開發者的。設計模式
綜上,是我認爲 Function Component + Hooks 編程體驗更好的地方,可是這也只是一個相對片面的角度,並無好壞之分,畢竟連 React 的官方都說,兩種寫法沒有好壞之分,性能差距也幾乎能夠忽略,並且 React 會長期支持這兩種寫法。數組
究竟是什麼是響應式編程?你們各執一詞,模模糊糊,懵懵懂懂。不少人沒有把他的本質說明白。從我多年的編程經驗來看,響應式編程就是「使用異步數據流編程」。咱們來看看前端在處理異步操做的時候一般是怎麼作的,常見的異步操做有異步請求和頁面的鼠標操做事件,在處理這樣的操做的時候,咱們一般採起的方法是事件循環,也就是異步事件流的方式。可是事件循環並無顯式的解決事件依賴問題,而是須要咱們本身在編碼的時候作好調用順序的管理,好比:antd
const x = 1; const a = (x) => new Promise((r, j)=>{ const y = x + 1; r(y); }); const b = (y) => new Promise((r, j)=>{ const z = y + 1; r(z); }); const c = (z) => new Promise((r, j)=>{ const w = z + 1; r(w); }); // 上面是三個異步請求,他們之間有依賴關係,咱們一般的操做是 a(x).then((y)=>{ b(y).then((z)=>{ c(z).then((w)=>{ // 最終的結果 console.log(w); }) }) })
上述的基於事件流的回調方式,咱們使用 Hooks 來替換的話,就是這樣的:
import { useState, useEffect } from 'react'; const useA = (x) => { const [y, setY] = useState(); useEffect(()=>{ // 假設此處包含異步請求 setY(x + 1); }, [x]); return y; } const useB = (y) => { const [z, setZ] = useState(); useEffect(()=>{ // 假設此處包含異步請求 setZ(y + 1); }, [y]); return z; } const useC = (z) => { const [w, setW] = useState(); useEffect(()=>{ // 假設此處包含異步請求 setW(z + 1); }, [z]); return w; } // 上面是三個是自定義 Hooks,他代表了每一個變量數據之間的依賴關係,你甚至不須要 // 知道他們每一個異步請求的返回順序,只須要知道數據是否發生了變化。 const x = 1; const y = useA(x); const z = useB(y); const w = useC(z); // 最終的結果 console.log(w);
咱們從上面的例子看到, Hooks 的寫法,簡直就像是在進行簡單的過程式編程同樣,步驟化,邏輯清晰,並且每一個自定義 Hooks 你能夠把他理解爲一個函數,他不須要與外界共享狀態,他是自封閉的,能夠很方便的進行測試。
咱們基於 React Hooks 提供的工具和上面講的響應式編程的思惟,開始咱們的精簡代碼之旅,此次旅程能夠歸納爲:遇到千行代碼文件怎麼辦?拆分最有效!怎麼拆分?先按照功能模塊來分文件,這裏的功能模塊是指相同的語法結構,好比反作用函數,事件處理函數等。單個文件內能夠按照具體實現寫多個自定義 Hooks 和函數。這樣作的最終目的就是,讓主文件裏只保留這個組件要實現的業務邏輯的步驟。
若是咱們把一個組件的全部代碼都寫到一個組件裏,那麼極有可能會出現一個文件裏有上千行代碼的狀況,若是你用的是 Function Component 來寫這個組件的話,那麼就會出現一個函數裏有上千行代碼的狀況。固然上千行代碼的文件對於一個健全的開發者來講都是不可忍受的,對於後來的重構者來講也是一個大災難。
爲何要把這個代碼都放到一個文件裏?拆分下不香嗎?那下面的問題就變成了如何拆分一個組件,要拆分一個組件,咱們要先知道一個典型的組件是什麼樣子的。
Hooks 是個新東西,他像函數同樣靈活,甚至不包含我選用了上面的方式來編寫新的代碼,那咱們來看看一個典型的基於 Function Component + Hooks 的組件包含什麼?
import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Row, Select, } from 'antd'; import Service from '@/services'; let originList = []; const Demo = ({ onChange, value, version, }) => { // 狀態管理 const [list, setList] = useState([]); // 反作用函數 useEffect(() => { const init = async () => { const list = await Service.getList(version); originList = list; setList(list); }; init(); }, []); // 事件 handler const onChangeHandler = useCallback((data) => { const item = { ...val, value: val.code, label: val.name }; onChange(item); }, [onChange]); const onSearchHandler = useCallback((val) => { if (val) { const listFilter = originList.filter(item => item.name.indexOf(val) > -1); setList(listFilter); } else { setList(originList); } }, []); // UI 組件渲染 return ( <Row> <Select labelInValue showSearch filterOption={false} value={value} onSearch={onSearchHandler} onChange={onChangeHandler} > {list.map(option => (<Option value={option.id} key={option.id}>{option.name}</Option>))} </Select> </Row> ); }; export default Demo;
從上面的例子咱們能夠看出,一個基本的 Function Component 包含哪些功能模塊:
首先,咱們把上面講到的功能模塊拆分紅多個文件:
|— container |— hooks.js // 各類自定義的 hooks |— handler.js // 轉換函數,以及不須要 hooks 的事件處理函數 |— index.js // 主文件,只保留實現步驟 |— index.css // css 文件
我重構過太多別人的代碼,但凡遇到那種看着邏輯代碼一大堆放在一塊兒的,我就頭大,後來發現,這些代碼都犯了一個相同的錯誤。沒有分清楚什麼是步驟,什麼是實現細節。當你把步驟和細節寫在一塊兒的時候,災難也就發生了,尤爲是那種終年累月迭代出來的代碼,if 遍地。
Hooks 是一個作代碼拆分的高效工具,可是他也很是的靈活,業界一直沒有比較通用行的編碼規範,可是我有點不一樣的觀點,我以爲他不須要像 Redux 同樣的模式化的編碼規範,由於他就是函數式編程,他遵循函數式編程的通常原則,函數式編程最重要的是拆分好步驟和實現細節,這樣的代碼就好讀,好讀的代碼纔是負責任的代碼。
到底怎麼區分步驟和細節?有一個很簡單的方法,在你梳理需求的時候,你用一個流程圖把你的需求表示出來,這時候的每一個節點基本就是步驟,由於他不牽扯到具體的實現。解釋太多,有點囉嗦了,相信你確定懂,對吧。
步驟和細節分清楚之後,對重構也有很大的好處,由於每一個步驟都是一個函數,不會有像 class 中 this 這種全局變量,當你須要刪除一個步驟或者重寫這個步驟的時候,不用影響到其餘步驟函數。
一樣,函數化之後,無疑單元測試就變得很是簡單了。
目的是主文件裏只保留業務步驟。
import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Row, Select, } from 'antd'; import { onChangeHandler } from './handler'; import { useList } from './hooks'; import Service from '@/services'; const Demo = ({ onChange, value, version, }) => { // list 狀態的操做,其中有搜索改變 list const [originList, list, onSearchHandler] = useList(version); // UI 組件渲染 return ( <Row> <Select labelInValue showSearch filterOption={false} value={value} onSearch={onSearchHandler} onChange={() => onChangeHandler(originList, data, onChange)} > {list.map(option => (<Option value={option.id} key={option.id}>{option.name}</Option>))} </Select> </Row> ); }; export default Demo;
看到上面是基於步驟和細節分離的思路,將上面的組件作了一次重構,只包含兩步:
經過拆分之後主文件代碼裏就只包含一些步驟了,所有使用自定義的 hooks 替換了,自定義的 hooks 能夠寫到 hooks.js 文件中。
hooks.js 裏文件內容以下:
import { useState, useEffect, useCallback } from 'react'; let originList = []; export const useList = (version) => { // 狀態管理 const [list, setList] = useState([]); // 反作用函數 useEffect(() => { const init = async () => { const list = await Service.getList(version); originList = list; setList(list); }; init(); }, []); // 處理 select 搜索 const onSearchHandler = useCallback((val) => { if (val) { const listFilter = originList.filter(item => item.name.indexOf(val) > -1); setList(listFilter); } else { setList(originList); } }, []); return [originList, list, onSearchHandler]; }
能夠看到 hooks.js 文件裏包含的就是數據和改變數據的方法,全部的反作用函數都包含在裏面。同時建議全部的異步請求都是用 await 來處理。啥好處能夠自行 Google。
handler.js 文件內容以下:
// 事件 handler export const onChangeHandler = (originList, data, onChange) => { const val = originList.find(option => (option.id === data.value)); const item = { ...val, value: val.code, label: val.name }; onChange(item); };
上面的例子很是簡單,你可能以爲根本不須要這樣重構,由於原本代碼量就不大,這樣拆分增長了太多文件。很好!這樣擡槓說明你有了思考,我贊成你的觀點,一些簡單的組件根本不須要如此拆分,可是我將這種重構方法不是一種規範,不是一種強制要求,相反他是一種價值觀,一種對於什麼是好的代碼的價值觀。這種價值觀歸根結底就是一句話:讓你的代碼易於變動。 Easier To Change! 簡稱 ETC。
ETC 這種編碼的價值觀是不少好的編碼原則的本質,好比單一職責原則,解耦原則等,他們都體現了 ETC 這種價值觀念。能適應使用者的就是好的設計,對於代碼而言,就是要擁抱變化,適應變化。所以咱們須要信奉 ETC 。價值觀念是幫助你在寫代碼的時候作決定的,他告訴你應該作這個?仍是作那個?他幫助你在不一樣編碼方式之間作選擇,他甚至應該成爲你編碼時的一種潛意識,若是你接受這種價值觀,那麼在編碼的時候,請時刻提醒本身,遵循這種價值觀。
文章可隨意轉載,但請保留此原文連接。
很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj@alibaba-inc.com 。