讓我們手動實現 React Hooks
前言
本人是個 React 小白,用了快一個月時間 React 了,感覺還挺好用的。最近剛好看到了新的 React Hooks API 貌似討論挺激烈,就順便看了一眼,簡單說下感覺。
- 方便了很多
- 更方便元件拆得更小
- 更方便把資料、業務邏輯拆到元件外面
基本上就是真香預定了。
不過呢,最讓我驚訝的是,我一直以為 React 以及其生態是標榜「純」的,居然還能把副作用玩那麼6,這是我沒有想到的。
所以呢,我當然是自己手擼了一個出來:
ofollow,noindex">bramblex/react-hooks先講講副作用
「純」是什麼,React 系的小夥伴並不會陌生,但與「純」相對的副作用,很多小夥伴就開始犯迷糊了。為什麼會有「純」與「不純」的區別,為什麼在「函式式」的領域裡面,大家會那麼在乎純呢?
其實吧,道理很簡單。以一個函式為例,想想我們執行一個函式的過程。
你以為的函式執行過的公式是:
函式 + 引數 = 返回值
但上面這個公式是錯誤的,只有純函式的情況下才滿足上面公式,一般情況下都是如下公式:
函式 + 引數 + 環境 = 返回值 + 環境'
一個純函式意味著,這個函式的執行既不會被外部的環境所改變,也無法改變外部的環境。
通過副作用執行時注入狀態
副作用這東西,就跟 goto,eval 等等各種規範裡面嚴禁使用的東西一樣,具有很強的自由度和很強不可控屬性,用的好可以實現各種奇技淫巧,用的不好坑死人不償命。我們這裡講的就是副作用的奇技淫巧之一,如何通過副作用,給函式注入狀態。
我們先拋開 React 獨立從一個函式的角度來看這件事情,我們要現在有一個 useState函式和一個render 函式,現在要給 render 函式用 useState 注入狀態,讓他每一次執行都不一樣,怎麼做呢?
let state function useState(defaultState) { function setState(newState) { state = newState } if (!state) { state = defaultState } return [state, setState] } function render() { const [state, setState] = useState(0) console.log(state) setState(state + 1) }
這時候我們只需要在外面存在一個變數,就能記錄這個函式的狀態了,第一次執行初始化狀態,第二次執行就已經有狀態了。
嘗試多個狀態
上面我們實現的函式只能設定一個狀態,我們如何設定多個狀態,同時還能在函式執行的時候恢復狀態呢?
這時候情況稍微就複雜一些了,我們需要多加一個計數器,用於記錄多個狀態的序列,並且每次執行結束或者執行開始需要把計數器置為0。
useState的順序是一個序列,我們用一個 key 為 number 的 map 來記錄 useState的序列。
最後我們寫一個函式包裝一下我們具體的render 函式。
let states = {} let currentNu = 0 function useState(defaultState) { const nu = currentNu++ function setState(newState) { states[nu] = newState } if (!states[nu]) { states[nu] = defaultState } return [states[nu], setState] } function withState(func) { return (...args) => { currentNu = 0 return func(...args) } } const render = withState( function render() { const [state, setState] = useState(0) const [state1, setState1] = useState(1) const [state2, setState2] = useState(2) console.log(state, state1, state2) setState(state + 1) setState1(state1 + 2) setState2(state2 + 3) } )
狀態堆與上下文棧
一個函式的例子我們解決了,那多個函式怎麼辦呢?多個函式怎麼辦呢?多個函式最複雜的情況,是相互呼叫,那麼相互呼叫就會打亂我們記錄的useState順序,怎麼辦?
我們先看看函式互相呼叫為什麼就沒有打亂計算機裡面的狀態呢?原因是函式呼叫的時候會變生一個函式棧,那我們就用函式該有的解決方式——棧來解決問題。
同樣的,在函式呼叫的時候,函式會把狀態等東西存在堆空間裡,我們也建一個堆來儲存狀態,這個堆直接用函式閉包存起來就行了。
let contextStack = [] function useState(defaultState) { const context = contextStack[contextStack.length - 1] const nu = context.nu++ const { states } = context function setState(newState) { states[nu] = newState } if (!states[nu]) { states[nu] = defaultState } return [states[nu], setState] } function withState(func) { const states = {} return (...args) => { contextStack.push({ nu: 0, states }) const result = func(...args) contextStack.pop() return result } } const render = withState( function render() { const [state, setState] = useState(0) render1() console.log('render', state) setState(state + 1) } ) const render1 = withState( function render1() { const [state, setState] = useState(0) console.log('render1', state) setState(state + 2) } )
這樣,我們哪怕遞迴,都不會搞亂我們程式應有的狀態了。
結合React
上面,我已經講完了實現的基本原理,那麼最後就是結合 React 。不過,我們包裝函式生成的不再是另一個函式,而是一個 React 元件。因為 React 元件自身能夠儲存個管理狀態,我們可以直接用,避免了我們要手動管理狀態的麻煩。這裡的React 元件,和之前分配一個堆的意義是一樣的。
程式碼的話就直接看下面吧,我就就不一一寫了。
bramblex/react-hooks到這裡,大家對於給一個函式注入狀態的技巧大家學會了嗎?