useState 解析
useState 运用
一般咱们这样来运用 useState 办法
function App() {
const [num, setNum] = useState(0);
const add = () => {
setNum(num + 1);
};
return (
<div>
<p>数字: {num}</p>
<button onClick={add}> +1 </button>
</div>
);
}
useState 的运用过程,咱们先模仿一个大概的函数
function useState(initialValue) {
var value = initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
这个代码有一个问题,在履行 useState 的时分每次都会 var _val = initialValue,初始化数据;
于是咱们能够用闭包的办法来保存状况。
const MyReact = (function() {
// 界说一个 value 保存在该模块的大局中
let value
return {
useState(initialValue) {
value = value || initialValue
function setState(newVal) {
value = newVal
}
return [value, setState]
}
}
})()
这样在每次履行的时分,就能够经过闭包的办法 来保存 value。
不过这个仍是不符合 react 中的 useState。由于在实际操作中会呈现多次调用,如下。
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 贮存状况的办法
相关参阅视频讲解:进入学习
useState 模仿完结
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 存储好的值。
hooks 规矩
官网 hoos 规矩中清晰的提出 hooks 不要再循环,条件或嵌套函数中运用。
为什么不能够?
咱们来看下
下面这样一段代码。履行 useState 从头烘托,和初始化烘托 顺序不一样就会呈现如下问题
假如了解了上面 useState 模仿写法的存储办法,那么这个问题的原因就迎刃而解了。
useEffect 解析
useEffect 运用
初始化会 打印一次 ‘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;
useEffect 的模仿完结
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 的时分,我只有想从头触发 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 里边的办法,又从头烘托,然后又对比,又不持平, 又履行。因而发生了无限循环。
Hooks 源码解析
该源码方位: react/packages/react-reconciler/src/ReactFiberHooks.js
const Dispatcher={
useReducer: mountReducer,
useState: mountState,
// xxx 省略其他的办法
}
mountState 源码
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];
}
dispatchAction 源码
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);
}
}
mountReducer 源码
多勒第三个参数,是函数履行,默许初始状况 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
- updateState 复用了 updateReducer(差异仅仅 updateState 将 reducer 设置为 updateReducer)
- mountState 虽没直接调用 mountReducer,可是几乎迥然不同(差异仅仅 mountState 将 reducer 设置为basicStateReducer)
注:这儿仅是 react 源码,至于从头烘托这块 react-dom 还没有去深入了解。
更新:
分两种状况,是否是 reRender,所谓reRender
便是说在当时更新周期中又发生了新的更新,就继续履行这些更新知道当时烘托周期中没有更新停止
他们根本的操作是一致的,便是依据 reducer
和 update.action
来创立新的 state
,并赋值给Hook.memoizedState
以及 Hook.baseState
。
留意这儿,关于非reRender
得状况,咱们会对每个更新判别其优先级,假如不是当时全体更新优先级内得更新会越过,第一个越过得Update
会变成新的baseUpdate
,他记载了在之后一切得Update,即便是优先级比他高得,由于在他被履行得时分,需要保证后续的更新要在他更新之后的基础上再次履行,由于结果可能会不一样。
来源
preact 中的 hooks
Preact 最优质的开源 React 代替品!(轻量级 3kb)
留意:这儿的代替是指假如不用 react 的话,能够运用这个。而不是取代。
useState 源码解析
调用了 useReducer 源码
export function useState(initialState) {
return useReducer(invokeOrReturn, initialState);
}
useReducer 源码解析
// 模块大局界说
/** @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;
}
getHookState 办法
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];
}
invokeOrReturn 办法
function invokeOrReturn(arg, f) {
return typeof f === 'function' ? f(arg) : f;
}
总结
运用 hooks 几个月了。根本上一切类组件我都运用函数式组件来写。现在 react 社区的很多组件,都也开端支持hooks。大概了解了点重要的源码,做到知其然也知其所以然,那么在实际工作中运用他能够减少不必要的 bug,进步效率。