一般咱們這樣來使用 useState 方法javascript
function App() {
const [num, setNum] = useState(0);
const add = () => {
setNum(num + 1);
};
return (
<div> <p>數字: {num}</p> <button onClick={add}> +1 </button> </div>
);
}
複製代碼
useState 的使用過程,咱們先模擬一個大概的函數html
function useState(initialValue) {
var value = initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
複製代碼
這個代碼有一個問題,在執行 useState 的時候每次都會 var _val = initialValue,初始化數據;java
因而咱們能夠用閉包的形式來保存狀態。react
const MyReact = (function() {
// 定義一個 value 保存在該模塊的全局中
let value
return {
useState(initialValue) {
value = value || initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
}
})()
複製代碼
這樣在每次執行的時候,就可以經過閉包的形式 來保存 value。ios
不過這個仍是不符合 react 中的 useState。由於在實際操做中會出現屢次調用,以下。axios
function App() {
const [name, setName] = useState('Kevin');
const [age, setAge] = useState(0);
const handleName = () => {
setNum('Dom');
};
const handleAge = () => {
setAge(age + 1);
};
return (
<div> <p>姓名: {name}</p> <button onClick={handleName}> 更名字 </button> <p>年齡: {age}</p> <button onClick={handleAge}> 加一歲 </button> </div>
);
}
複製代碼
所以咱們須要在改變 useState 儲存狀態的方式api
const MyReact = (function() {
// 開闢一個儲存 hooks 的空間
let hooks = [];
// 指針從 0 開始
let currentHook = 0
return {
// 僞代碼 解釋從新渲染的時候 會初始化 currentHook
render(Component) {
const Comp = Component()
Comp.render()
currentHook = 0 // 從新渲染時候改變 hooks 指針
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue
const setStateHookIndex = currentHook
// 這裏咱們暫且默認 setState 方式第一個參數不傳 函數,直接傳狀態
const setState = newState => (hooks[setStateHookIndex] = newState)
return [hooks[currentHook++], setState]
}
}
})()
複製代碼
所以當從新渲染 App 的時候,再次執行 useState 的時候傳入的參數 kevin , 0 也就不會去使用,而是直接拿以前 hooks 存儲好的值。數組
官網 hoos 規則中明確的提出 hooks 不要再循環,條件或嵌套函數中使用。閉包
咱們來看下dom
下面這樣一段代碼。執行 useState 從新渲染,和初始化渲染 順序不同就會出現以下問題
若是瞭解了上面 useState 模擬寫法的存儲方式,那麼這個問題的緣由就迎刃而解了。
初始化會 打印一次 ‘useEffect_execute’, 改變年齡從新render,會再打印, 改變名字從新 render, 不會打印。由於依賴數組裏面就監聽了 age 的值
import React, { useState, useEffect } from 'react';
function App() {
const [name, setName] = useState('Kevin');
const [age, setAge] = useState(0);
const handleName = () => {
setName('Don');
};
const handleAge = () => {
setAge(age + 1);
};
useEffect(()=>{
console.log('useEffect_execute')
}, [age])
return (
<div> <p>姓名: {name}</p> <button onClick={handleName}> 更名字 </button> <p>年齡: {age}</p> <button onClick={handleAge}> 加一歲 </button> </div>
);
}
export default App;
複製代碼
const MyReact = (function() {
// 開闢一個儲存 hooks 的空間
let hooks = [];
// 指針從 0 開始
let currentHook = 0 ;
// 定義個模塊全局的 useEffect 依賴
let deps;
return {
// 僞代碼 解釋從新渲染的時候 會初始化 currentHook
render(Component) {
const Comp = Component()
Comp.render()
currentHook = 0 // 從新渲染時候改變 hooks 指針
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue
const setStateHookIndex = currentHook
// 這裏咱們暫且默認 setState 方式第一個參數不傳 函數,直接傳狀態
const setState = newState => (hooks[setStateHookIndex] = newState)
return [hooks[currentHook++], setState]
}
useEffect(callback, depArray) {
const hasNoDeps = !depArray
// 若是沒有依賴,說明是第一次渲染,或者是沒有傳入依賴參數,那麼就 爲 true
// 有依賴 使用 every 遍歷依賴的狀態是否變化, 變化就會 true
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
// 若是有 依賴, 而且依賴改變
if (hasNoDeps || hasChangedDeps) {
// 執行
callback()
// 更新依賴
deps = depArray
}
},
}
})()
複製代碼
依賴須要想清楚。
剛開始使用 useEffect 的時候,我只有想從新觸發 useEffect 的時候纔會去設置依賴
那麼也就會出現以下的問題。
但願的效果是界面中一秒增長一歲
import React, { useState, useEffect } from 'react';
function App() {
const [name, setName] = useState('Kevin');
const [age, setAge] = useState(0);
const handleName = () => {
setName('Don');
};
const handleAge = () => {
setAge(age + 1);
};
useEffect(() => {
setInterval(() => {
setAge(age + 1);
console.log(age)
}, 1000);
}, []);
return (
<div> <p>姓名: {name}</p> <button onClick={handleName}> 更名字 </button> <p>年齡: {age}</p> <button onClick={handleAge}> 加一歲 </button> </div>
);
}
export default App;
複製代碼
其實你會發現 這裏界面就增長了 一次 年齡。究其緣由:
**在第一次渲染中,age
是0
。所以,setAge(age+ 1)
在第一次渲染中等價於setAge(0 + 1)
。然而我設置了0依賴爲空數組,那麼以後的 useEffect 不會再從新運行,它後面每一秒都會調用setAge(0 + 1) **
也就是當咱們須要 依賴 age 的時候咱們 就必須再 依賴數組中去記錄他的依賴。這樣useEffect 纔會正常的給咱們去運行。
因此咱們想要每秒都遞增的話有兩種方法
方法一:
真真切切的把你所依賴的狀態填寫到 數組中
// 經過監聽 age 的變化。來從新執行 useEffect 內的函數
// 所以這裏也就須要記錄定時器,當卸載的時候咱們去清空定時器,防止多個定時器從新觸發
useEffect(() => {
const id = setInterval(() => {
setAge(age + 1);
}, 1000);
return () => {
clearInterval(id)
};
}, [age]);
複製代碼
方法二
useState 的參數傳入 一個方法。
注:上面咱們模擬的 useState 並無作這個處理 後面我會講解源碼中去解析。
useEffect(() => {
setInterval(() => {
setAge(age => age + 1);
}, 1000);
}, []);
複製代碼
useEffect 只運行了一次,經過 useState 傳入函數的方式它再也不須要知道當前的age
值。由於 React render 的時候它會幫咱們處理
這正是setAge(age => age + 1)
作的事情。再從新渲染的時候他會幫咱們執行這個方法,而且傳入最新的狀態。
因此咱們作到了去時刻改變狀態,可是依賴中卻不用寫這個依賴,由於咱們將本來的使用到的依賴移除了。(這句話表達感受不到位)
剛開始使用 useEffect 的我,在接口請求的時候經常會這樣去寫代碼。
props 裏面有 頁碼,經過切換頁碼,但願監聽頁碼的變化來從新去請求數據
// 如下是僞代碼
// 這裏用 dva 發送請求來模擬
import React, { useState, useEffect } from 'react';
import { connect } from 'dva';
function App(props) {
const { goods, dispatch, page } = props;
useEffect(() => {
// 頁面完成去發情請求
dispatch({
type: '/goods/list',
payload: {page, pageSize:10},
});
// xxxx
}, [props]);
return (
<div> <p>商品: {goods}</p> <button>點擊切下一頁</button> </div>
);
}
export default connect(({ goods }) => ({
goods,
}))(App);
複製代碼
而後得意洋洋的刷新界面,發現 Network 中瘋狂循環的請求接口,致使頁面的卡死。
究其緣由是由於在依賴中,咱們經過接口改變了狀態 props 的更新, 致使從新渲染組件,致使會從新執行 useEffect 裏面的方法,方法執行完成以後 props 的更新, 致使從新渲染組件,依賴項目是對象,引用類型發現不相等,又去執行 useEffect 裏面的方法,又從新渲染,而後又對比,又不相等, 又執行。所以產生了無限循環。
該源碼位置: react/packages/react-reconciler/src/ReactFiberHooks.js
const Dispatcher={
useReducer: mountReducer,
useState: mountState,
// xxx 省略其餘的方法
}
複製代碼
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
/*
mountWorkInProgressHook 方法 返回初始化對象
{
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
}
*/
const hook = mountWorkInProgressHook();
// 若是傳入的是函數 直接執行,因此第一次這個參數是 undefined
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
/*
定義 dispatch 至關於
const dispatch = queue.dispatch =
dispatchAction.bind(null,currentlyRenderingFiber,queue);
*/
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
// 能夠看到這個dispatch就是dispatchAction綁定了對應的 currentlyRenderingFiber 和 queue。最後return:
return [hook.memoizedState, dispatch];
}
複製代碼
function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
//... 省略驗證的代碼
const alternate = fiber.alternate;
/* 這其實就是判斷這個更新是不是在渲染過程當中產生的,currentlyRenderingFiber只有在FunctionalComponent更新的過程當中纔會被設置,在離開更新的時候設置爲null,因此只要存在並更產生更新的Fiber相等,說明這個更新是在當前渲染中產生的,則這是一次reRender。 全部更新過程當中產生的更新記錄在renderPhaseUpdates這個Map上,以每一個Hook的queue爲key。 對於不是更新過程當中產生的更新,則直接在queue上執行操做就好了,注意在最後會發起一次scheduleWork的調度。 */
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
didScheduleRenderPhaseUpdate = true;
const update: Update<A> = {
expirationTime: renderExpirationTime,
action,
next: null,
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update: Update<A> = {
expirationTime,
action,
next: null,
};
flushPassiveEffects();
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
scheduleWork(fiber, expirationTime);
}
}
複製代碼
多勒第三個參數,是函數執行,默認初始狀態 undefined
其餘的和 上面的 mountState 大同小異
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
// 其餘和 useState 同樣
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
複製代碼
經過 react 源碼中,能夠看出 useState 是特殊的 useReducer
useState
不過就是個語法糖,本質其實就是useReducer
注:這裏僅是 react 源碼,至於從新渲染這塊 react-dom 尚未去深刻了解。
分兩種狀況,是不是 reRender,所謂reRender
就是說在當前更新週期中又產生了新的更新,就繼續執行這些更新知道當前渲染週期中沒有更新爲止
他們基本的操做是一致的,就是根據 reducer
和 update.action
來建立新的 state
,並賦值給Hook.memoizedState
以及 Hook.baseState
。
注意這裏,對於非reRender
得狀況,咱們會對每一個更新判斷其優先級,若是不是當前總體更新優先級內得更新會跳過,第一個跳過得Update
會變成新的baseUpdate
,他記錄了在以後全部得Update,即使是優先級比他高得,由於在他被執行得時候,須要保證後續的更新要在他更新以後的基礎上再次執行,由於結果可能會不同。
Preact 最優質的開源 React 替代品!(輕量級 3kb)
注意:這裏的替代是指若是不用 react 的話,可使用這個。而不是取代。
調用了 useReducer 源碼
export function useState(initialState) {
return useReducer(invokeOrReturn, initialState);
}
複製代碼
// 模塊全局定義
/** @type {number} */
let currentIndex; // 狀態的索引,也就是前面模擬實現 useState 時候所說的指針
let currentComponent; // 當前的組件
export function useReducer(reducer, initialState, init) {
/** @type {import('./internal').ReducerHookState} */
// 經過 getHookState 方法來獲取 hooks
const hookState = getHookState(currentIndex++);
// 若是沒有組件 也就是初始渲染
if (!hookState._component) {
hookState._component = currentComponent;
hookState._value = [
// 沒有 init 執行 invokeOrReturn
// invokeOrReturn 方法判斷 initialState 是不是函數
// 是函數 initialState(null) 由於初始化沒有值默認爲null
// 不是函數 直接返回 initialState
!init ? invokeOrReturn(null, initialState) : init(initialState),
action => {
// reducer == invokeOrReturn
const nextValue = reducer(hookState._value[0], action);
// 若是當前的值,不等於 下一個值
// 也就是更新的狀態的值,不等於以前的狀態的值
if (hookState._value[0]!==nextValue) {
// 儲存最新的狀態
hookState._value[0] = nextValue;
// 渲染組件
hookState._component.setState({});
}
}
];
}
// hookState._value 數據格式也就是 [satea:any, action:Function] 的數據格式拉
return hookState._value;
}
複製代碼
function getHookState(index) {
if (options._hook) options._hook(currentComponent);
const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], _pendingLayoutEffects: [] });
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
複製代碼
function invokeOrReturn(arg, f) {
return typeof f === 'function' ? f(arg) : f;
}
複製代碼
使用 hooks 幾個月了。基本上全部類組件我都使用函數式組件來寫。如今 react 社區的不少組件,都也開始支持hooks。大概瞭解了點重要的源碼,作到知其然也知其因此然,那麼在實際工做中使用他能夠減小沒必要要的 bug,提升效率。
全文章,若有錯誤或不嚴謹的地方,請務必給予指正,謝謝!
參考: