背景
React Hooks是React 16.8发布以来最吸引人的特性之一。
react-router、redux等主流包,都已经提供了完备的 Hooks api,且官网上的 demo 都已经切换成 Hooks 书写的形式。
Redux 的官方教程里明确指出,要求阅读文档的人需要了解 Hooks 的使用。
antd 团队也完成了底层使用Hooks重写一部分组件的功能。
为什么用hooks
React提供的组件化和自上而下的数据流帮助我们把一个大的UI交互拆分为小的、可重用的、独立的小块。但是,对于一些复杂的组件,因为逻辑依赖没法进一步拆分
类组件进行逻辑复用一般通过HOC和renderProps的形式,出现的问题如:层级嵌套,难以区分props的层级来源,props丢失,逻辑复用时必须渲染ui...
历史的函数组件无法进行状态管理和生命周期函数...
hooks的优点:
- 写法简单:每一个Hook都是一个函数,因此它的写法十分简单,而且开发者更容易理解。
- 组合简单:Hook组合起来十分简单,组件只需要同时使用多个hook就可以使用到它们所有的功能。
- 容易扩展:Hook具有很高的可扩展性,你可以通过自定义Hook来扩展某个Hook的功能。
- 没有wrapper hell:Hook不会改变组件的层级结构,也就不会有wrapper hell问题的产生。
- 为函数组件提供的一套api,使得我们的函数式组件能拥有和 Class 一样的功能和特性,进行状态管理、生命周期控制、context、ref等
hooks使用和原理
定义全部变量hookStates保存hooks,定义全部hookIndex保存hooks对应的索引。react内部,并不是全局的,而是挂载每个fiber上
let hookStates = []; // 存放所有的状态
let hookIndex = 0; // 表示当前hook
useState
- 介绍
参数:状态初始值
返回:状态和状态更新函数。执行更新函数时,不会将旧的状态和新的状态合并
初始渲染时,返回的state为初始值。更新函数,接收新的状态值,并将组件的更新渲染加入队列
- 原理
function useState(initialState) {
hookStates[hookIndex] = hookStates[hookIndex] || initialState
let currentIndex = hookIndex
function setState(newState) {
hookStates[currentIndex] = newState
// scheduleUpdate(); // 状态改变后,更新应用 先改变hookStates中的state,然后改变vdom,然后改变真实dom
}
return [hookStates[hookIndex++], setState]
}
scheduleUpdate方法,主要用于更新vdom,然后改变真实dom,然后重置hookIndex。
不能在if语句中使用hooks,是因为每个hook有个对应的索引,当放在条件语句时,可能导致索引和hook对应不上
useMemo
- 介绍
参数:对象等创建函数+依赖项
返回:创建函数执行结果。仅当依赖项变化时才会重新计算memoized值,避免在每次渲染时进行高开销的计算
主要用于缓存对象
- 使用
function Counter() {
let [name, setName] = useState('hh');
let [number, setNumber] = useState(0);
const addClick = useCallback(() => setNumber(number => number+1),[number]);
const data = useMemo(() => ({number}), [number]);
return (
<div>
<input type='text' value={name} onChange={e => setName(e.target.value)}/>
<Child addClick={addClick} data={data}/>
</div>
)
}
- 原理
function useMemo(factory, deps) {
if(hookStates[hookIndex]) {
let [lastMemo, lastDeps] = hookStates[hookIndex]
let same = deps.every((item, index) => item === lastDeps[index]) // 新旧依赖一一对比
if(same) { // 返回上一个memo
hookIndex++
return lastMemo
} else { // 执行factory,创建新的memo
let newMemo = factory();
hookStates[hookIndex++] = [newMemo, deps]
return newMemo
}
} else {
let newMemo = factory();
hookStates[hookIndex++] = [newMemo, deps]
return newMemo
}
}
useCallback
- 介绍
参数:内联回调函数+依赖项。依赖项应该添加的值:所有effect内部用到的变量和函数
返回:内联回调函数的memoized版本,这个函数仅在依赖项改变时更新
主要用于缓存函数
- 原理
function useCallback(callback, deps) {
if(hookStates[hookIndex]) {
let [lastCallback, lastDeps] = hookStates[hookIndex]
let same = deps.every((item, index) => item === lastDeps[index]) // 新旧依赖一一对比
if(same) { // 返回lastCallback
hookIndex++
return lastCallback
} else { // 存放新的callback
hookStates[hookIndex++] = [callback, deps]
return callback
}
} else {
hookStates[hookIndex++] = [callback, deps]
return callback
}
}
useReducer
- 介绍
参数:形如 (state, action) => newState 的 reducer + 初始状态
返回:当前state + dispatch
主要用于state更改逻辑较为复杂的场景
- 使用
let initialState = {number:0};
let ADD = 'ADD';
let MINUS = 'MINUS';
function reducer(state, action) {
switch (action.type) {
case ADD:
return {number: state.number + 1}
case MINUS:
return {number: state.number - 1}
default:
break;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>{state.number}</p>
<button onClick= {()=> dispatch({type: ADD})}>+</button>
<button onClick= {()=> dispatch({type: MINUS})}>-</button>
</div>
)
}
- 原理
function useReducer(reducder, initialState) {
hookStates[hookIndex] = hookStates[hookIndex] || initialState)
let currentIndex = hookIndex
function dispatch(action) {
let lastState = hookStates[currentIndex]
hookStates[hookIndex] = reducder(lastState, action)
// scheduleUpdate(); // 更新渲染
}
return [hookStates[hookIndex++], dispatch]
}
- useState为useReducer的语法糖
function useReducer(reducder, initialState) {
hookStates[hookIndex] = hookStates[hookIndex] || initialState
let currentIndex = hookIndex
function dispatch(action) {
let lastState = hookStates[currentIndex]
let nextState
if(reducder) { // reducer存在时,执行得到新的state
nextState = reducder(lastState, action)
} else {// reducer为null时,相当于action为新的状态值;
nextState = typeof action === 'function' ? action(lastState) : action
}
hookStates[hookIndex] = nextState
// scheduleUpdate();
}
return [hookStates[hookIndex++], dispatch]
}
----------------------------------------------------
function newUseState(initialState) {
return useReducer(null, initialState)
}
useEffect
- 介绍
参数:callback + 依赖项deps。callback会在组件挂载和更新完成后执行。如果deps不存在,callback在每次render后都会执行;如果存在,当依赖项发生变化后,才会执行;如果是个空数组,依赖项即不会改变,相当于只执行了一次
返回:清理函数。会在下次执行useEffect的时候执行
它是个effect hook,给函数组件增加了操作副作用的能力。如果定时器,生命周期等
赋值给useEffect的函数会在组件渲染到屏幕后执行
function useEffect(callback,deps){
if(hookStates[hookIndex]){
let [destroyFunc,lastDeps] = hookStates[hookIndex];
let same = deps && deps.every((item,index)=>item === lastDeps[index]);
if(same){
hookIndex++;
}else{
destroyFunc && destroyFunc();
setTimeout(() => {
let destroyFunc = callback();
hookStates[hookIndex++] = [destroyFunc, deps]
});
}
}else{ // 第一次渲染 开启一个宏任务,当render后执行callback
setTimeout(() => {
let destroyFunc = callback();
hookStates[hookIndex++] = [destroyFunc, deps]
});
}
}
useLayoutEffect
与useEffect相对,useEffect在浏览器渲染完成后执行。但,useLayoutEffect是在dom更新完成浏览器绘制前执行。所以,useLayoutEffect会阻塞浏览器渲染
所有的dom变更 ==> useLayoutEffect ==> painting ==> useEffect
function useLayoutEffect(callback,deps){
if(hookStates[hookIndex]){
let [destroyFunc,lastDeps] = hookStates[hookIndex];
let same = deps && deps.every((item,index)=>item === lastDeps[index]);
if(same){
hookIndex++;
}else{
destroyFunc && destroyFunc();
queueMicrotask(() => { // 和useEffect的区别为queueMicrotask,将函数放在微任务队列,微任务队列在绘制前执行
let destroyFunc = callback();
hookStates[hookIndex++] = [destroyFunc, deps]
});
}
}else{
queueMicrotask(() => {
let destroyFunc = callback();
hookStates[hookIndex++] = [destroyFunc, deps]
});
}
}
useRef
返回的值在组件的整个生命周期内保持不变,用于缓存一个不变的值。类似于类组件的this;当ref.current发生改变时,不会re-render
用途:如:类组件时,可以通过实例拿到属性和方法。函数组件没有自己的实例,因此可以通过ref.current拿到缓存的属性和方法
function useRef(initialState) {
hookStates[hookIndex] = hookStates[hookIndex] || { current: initialState };
return hookStates[hookIndex++];
}
forwardRef + useImperativeHandle
函数组件没有实例,直接在函数组件上使用ref时会报错。forwardRef,创建一个 React 组件,该组件能够将其接收的 ref 属性转发到内部的一个组件中
useImperativeHandle可以让你在使用ref时,自定义暴露给父组件的实例值,避免到子组件误操作
function Child(props,ref){
const inputRef = useRef();
useImperativeHandle(ref,()=>(
{
focus(){ inputRef.current.focus();},
}
));
return (
<input type="text" ref={inputRef}/>
)
}
Child = forwardRef(Child);
function Parent(){
let [number,setNumber] = useState(0);
const inputRef = useRef();
function getFocus(){
inputRef.current.focus();
inputRef.current.remove(); // 不会执行
}
return (
<>
<Child ref={inputRef}/>
<button onClick={()=>setNumber({number:number+1})}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
Fiber上的hooks实现
链表结构
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
每个结点包括两个部分:数据和指针
LinkedList类包含三个属性:firstUpdate,指向第一个节点;lastUpdate,指向链表的最后一个节点;nextUpdate指向下个节点
// 表示一个节点
class Update {
constructor(payload, nextUpdate) {
// payload挂载数据,nextUpdate指向下一个节点
this.payload = payload
this.nextUpdate = nextUpdate
}
}
// 模拟链表
class UpdateQueue {
constructor() {
this.firstUpdate = null
this.lastUpdate = null
}
enqueueUpdate(update) { // 将更新放在队列中
if (!this.firstUpdate) {
this.firstUpdate = this.lastUpdate = update
} else {
this.lastUpdate.nextUpdate = update
this.lastUpdate = update
}
}
forceUpdate() { // forceUpdate将所有节点挂载的数据合并
let currentState = this.baseState || {}
let currentUpdate = this.firstUpdate
while(currentUpdate) {
const nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
currentState = {
...currentState,
...nextState
}
currentUpdate = currentUpdate.nextUpdate
}
this.firstUpdate = this.lastUpdate = null
return this.baseState = currentState
}
}
模拟useReducer
let workInProgressFiber = null; //正在工作中的fiber
let hookIndex = 0; //hooks索引
// updateFunctionComponent: 每次render时,对函数组件进行调度,并进行hooks和hookIndex的初始化,给当前fiber添加hooks数组
function updateFunctionComponent(currentFiber) {
workInProgressFiber = currentFiber; // 初始化workInProgressFiber,hooks和hookIndex,在执行hooks是使用
hookIndex = 0;
workInProgressFiber.hooks = [];
const newChildren = [currentFiber.type(currentFiber.props)];
reconcileChildren(currentFiber, newChildren);
}
// useReducer: 首次render时,将hook存到hooks数组中;再次render时,根据索引获取上次hook,执行forceUpdate更新state
function useReducer(reducer, initialValue) {
let newHook = workInProgressFiber.alternate.hooks[hookIndex]; // 上次fiber🌲上的hooks数组--对应索引
if (newHook) {
newHook.state = newHook.updateQueue.forceUpdate(newHook.state);
} else {
newHook = {
state: initialValue,
updateQueue: new UpdateQueue()
};
}
const dispatch = action => { // 执行dispatch时,在链表上增加update对象
newHook.updateQueue.enqueueUpdate(
new Update(reducer ? reducer(newHook.state, action) : action)
);
scheduleRoot(); // 重新调度
}
workInProgressFiber.hooks[hookIndex++] = newHook;
return [newHook.state, dispatch];
}
function useState(initState) {
return useReducer(null, initState)
}
思考题
function useLoading() {
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
}, [])
return loading;
}
function useRefObject() {
const ref = useRef();
const [ready, setReady] = useState(false);
useEffect(() => {
if(ref.current) {
setReady(true);
}
}, [ref])
return [ref, ready];
}
const App = (props) => {
const loading = useLoading();
const [ref, ready] = useRefObject();
return (
<div>
<div>ref with useEffect</div>
{loading && <div ref={ref}>
{ready.toString()}
期望的结果是true,但真实的结果是?为什么?怎么得到期望的?
</div>}
</div>
)
}