讓我們手動實現 React Hooks
正在找工作,有沒有大佬帶帶我的?
前言
本人是個 React 小白,用了快一個月時間 React 了,感覺還挺好用的。最近剛好看到了新的 React Hooks API 貌似討論挺激烈,就順便看了一眼,簡單說下感覺。
方便了很多
更方便元件拆得更小
更方便把資料、業務邏輯拆到元件外面
基本上就是真香預定了。
不過呢,最讓我驚訝的是,我一直以為 React 以及其生態是標榜「純」的,居然還能把副作用玩那麼6,這是我沒有想到的。
所以呢,我當然是自己手擼了一個出來:
先講講副作用
「純」是什麼,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 元件,和之前分配一個堆的意義是一樣的。
程式碼的話就直接看下面吧,我就就不一一寫了。
到這裡,大家對於給一個函式注入狀態的技巧大家學會了嗎?