Hooks简介和概述?
Hooks 是 React 函数组件内一类特殊的函数(通常以 "use" 开头,比如 "useState"),使开发者能够在 function component 里依旧使用 state 和 life-cycles,以及使用 custom hooks 复用业务逻辑。
为什么要引进Hooks,要解决什么问题
当前react经常遇见的问题:
- 很难复用逻辑(只能用HOC,或者render props),会导致组件树层级很深
- 大型组件很难拆分和重构,也很难测试。
- 类组件很难理解,比如方法需要
bind
,this
指向不明确 - 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
Hooks让我们更好地进行代码逻辑复用。 函数组件可以很好地进行逻辑复用,但是函数组件是无状态的,只能作为【纯组件】展示,不能处理局部state。Hooks让函数组件拥有了局部state,可以处理状态逻辑。
Hooks的种类
- State hooks (在 function component 中使用 state)
- Effect hooks (在 function component 中使用生命周期和 side effect)
- Custom hooks (自定义 hooks 用来复用组件逻辑,解决了上述的第一个动机中阐述的问题)
State Hooks
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Hooks会返回一个Tuple,结构为[value, setValue]
。
这两个返回值分别对应之前react里的
- this.state
- this.setState
我们还可以在函数中同时使用多个state
function ExampleWithManyStates() {
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
之前更新state中值,通过this.setState({ fruit: 'orange' })
,会对之前的state和更新后的state进行合并。
而使用Hooks,会将state进行拆分为一个个value,更新后,直接使用新值替换,不会进行state的合并。[state,setState]的结构也让值的更新逻辑更加清晰。
React默认提供的常用Hooks
- useState()
- useContext()
- useReducer()
useContext()
配合
React.createContext({})
使用,在组件间的共享状态
示例:
const AppContext = React.createContext({});
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>
然后在Navbar组件内就可以直接使用AppContext
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}
useReducer()
用来简单替代redux做状态管理,但是没法提供中间件(middleware)和时间旅行(time travel)等复杂场景
示例:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
Effect Hooks
effectHooks让我们可以在函数组件内使用生命周期方法,我们可以在这里更新DOM,获取数据等具有'副作用'的行为。effect Hook会在组件每次render后执行,ruturn的函数会在组件卸载时执行,若要让effect hook只在组件首次加载时执行,可以传入一个空数组作为第二个参数,也可以在数组中指定依赖项,只有依赖项改变时,effectHooks才会执行。
import { useState, useEffect } from 'react';
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
}
Custom Hooks
自定义Hook是一个以'use'开头的javascript函数,可以调用其他的Hooks,从而进行逻辑封装,复用代码。例如:
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
其他函数组件就可以使用:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
*********************************
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Hooks必须在函数顶层使用,不能用于条件,循环,嵌套中。 Hooks会逐步完全替代class组件,目前还无法支持getSnapshotBeforeUpdate和componentDidCatch生命周期的功能。
Hooks的实现原理
首先我们需要整理下react的数据更新和视图渲染机制。之前都是通过调用setState
来更改数据,页面进行re-render,我们先来看看setState
是如何工作的。
react的基础架构
React的基础架构分为三个部分:react基础包、react-reconciler、renderer渲染模块
react基础模块: react 基础 API 及组件类,组件内定义 render 、setState 方法和生命周期相关的回调方法,相关 API 如下:
const React = {
Children: {},
createRef,
Component,
PureComponent,
createContext,
forwardRef,
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
unstable_Profiler: REACT_PROFILER_TYPE,
createElement: __DEV__ ? createElementWithValidation : createElement,
cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
isValidElement: isValidElement,
};
renderer渲染模块: 针对不同宿主环境采用不同的渲染方法实现,如 react-dom, react-webgl, react-native, react-art, 依赖 react-reconciler模块, 注入相应的渲染方法到 reconciler 中,react-dom 中相关的 API 如下:
const ReactDOM: Object = {
createPortal,
findDOMNode(
componentOrElement: Element | ?React$Component<any, any>,
): null | Element | Text {},
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {},
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {},
unstable_renderSubtreeIntoContainer() {},
unmountComponentAtNode(container: DOMContainer) {},
unstable_batchedUpdates: DOMRenderer.batchedUpdates,
unstable_deferredUpdates: DOMRenderer.deferredUpdates,
unstable_interactiveUpdates: DOMRenderer.interactiveUpdates,
flushSync: DOMRenderer.flushSync,
unstable_flushControlled: DOMRenderer.flushControlled,
}
react-reconciler核心模块:负责调度算法及 Fiber tree diff, 连接 react基础包 和 renderer 模块,注入 setState 方法到 component 实例中,在 diff 阶段执行 react 组件中 render 方法,在 patch 阶段执行 react 组件中生命周期回调并调用 renderer 中注入的相应的方法渲染真实视图结构。
setState的工作原理
setState
定义在React.Component中,但是React包中只是定义API,并没有具体实现逻辑。类似的还有createContext()
等大多数功能都是在‘渲染器’中实现的。react-dom、react-dom/server、 react-native、 react-test-renderer、 react-art都是常见的渲染器。所以我们在使用react新特性的时候,react和react-dom都需要更新。
setState
在React.Component中定义updater
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
在具体的渲染器中会自己实现updater:
// React DOM 内部
var classComponentUpdater = {
isMounted: isMounted,
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
var currentTime = requestCurrentTime();
var expirationTime = computeExpirationForFiber(currentTime, fiber);
var update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
{
warnOnInvalidCallback$1(callback, 'setState');
}
update.callback = callback;
}
flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
enqueueReplaceState: function (inst, payload, callback) {
//注释了
},
enqueueForceUpdate: function (inst, callback) {
//注释了
}
};
Hooks也是使用了相同的设计,使用了‘dispatcher’对象,来代替‘updater’。我们调用useState()
时,都被转发给当前的dispatcher。
updater字段和dispatcher对象都是使用依赖注入的通用编程原则的形式。在这两种情况下,渲染器将诸如setState之类的功能的实现“注入”到通用的React包中,以使组件更具声明性。
useState的工作原理
useState是如何让无状态的函数组件可以保存状态,更新视图,和this.setState的更新有啥区别?
React中有一个基础对象ReactElement,它由React.createElement()创建的
React.createElement(
type,
[props],
[...children]
)
//举个例子
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}
ReactDOM.render(
<Hello toWhat="World" />,
document.getElementById('root')
);
//完全等价于
class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
const element = {
$$typeof: REACT_ELEMENT_TYPE, // 是否是普通Element_Type
// Built-in properties that belong on the element
type: type, // 我们的组件,比如`class MyComponent`
key: key,
ref: ref,
props: props,
children: children,
// Record the component responsible for creating this element.
_owner: owner,
};
这是一个vdom节点,在React16之前,React会根据这个vdom节点生成真实的dom结构。React16之后,官方引入了Fiber结构,react的基本架构也变得更加复杂了。React会将vdom节点对应为一个Fiber节点,Fiber节点的结构:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null; // 就是ReactElement的`$$typeof`
this.type = null; // 就是ReactElement的type
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.memoizedState = null;
this.updateQueue = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.firstContextDependency = null;
// ...others
}
其中的this.updateQueue
用来存储setState的更新队列,this.memoizedState
来储存组件内的state状态,类组件中是用来存储state对象的,在Hooks中用来存储Hook对象。
//类组件中更新state的update对象
var update = {
expirationTime: expirationTime,
tag: UpdateState,
payload: null,
callback: null,
next: null,
nextEffect: null
};
//函数组件中的Hook对象
{
baseState,
next,
baseUpdate,
queue,
memoizedState
};
//类组件中的updateQueue的结构
var queue = {
baseState: baseState,
firstUpdate: null,
lastUpdate: null,
firstCapturedUpdate: null,
lastCapturedUpdate: null,
firstEffect: null,
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null
};
//每新增一个update就加入到队列中
function appendUpdateToQueue(queue, update) {
// Append the update to the end of the list.
if (queue.lastUpdate === null) {
// Queue is empty
queue.firstUpdate = queue.lastUpdate = update;
} else {
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}
memoizedState的更新机制
Hooks的更新分成两步,初始化时进行mount操作,更新时进行update操作。分别通过HooksDispatcherOnMountInDEV和HooksDispatcherOnUpdateInDEV两个对象来存储所有Hooks更新的函数。
HooksDispatcherOnMountInDEV = {
readContext: function (context, observedBits) {
},
useCallback: function (callback, deps) {
},
useContext: function (context, observedBits) {
},
useEffect: function (create, deps) {
},
useImperativeHandle: function (ref, create, deps) {
},
useLayoutEffect: function (create, deps) {
},
useMemo: function (create, deps) {
},
useReducer: function (reducer, initialArg, init) {
},
useRef: function (initialValue) {
},
useState: function (initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
last: null,
dispatch: null,
eagerReducer: basicStateReducer,
eagerState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null,
// Flow doesn't know this is non-null, but we do.
currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
},
useDebugValue: function (value, formatterFn) {
}
};
//其中的dispatch即为我们调用的‘setState’函数,核心代码为:
function dispatchAction(fiber, queue, action) {
//注释了*******
var update = {
expirationTime: renderExpirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
renderPhaseUpdates.set(queue, update);
}
HooksDispatcherOnUpdateInDEV = {
//注释了**********
useState: function (initialState) {
currentHookNameInDev = 'useState';
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
};
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
function updateReducer(reducer, initialArg, init) {
// 注释了**********
var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
var newState = hook.memoizedState;
var update = firstRenderPhaseUpdate;
do {
// Process this render phase update. We don't have to check the
// priority because it will always be the same as the current
// render's.
var _action = update.action;
newState = reducer(newState, _action);
update = update.next;
} while (update !== null);
}
update对象中的action就是使用setState的参数,update会被加入到更新queue中,在所有‘update’都收集完后,会触发react的更新。更新时,执行到函数组件中的useState,然后拿到Hook对象,取出其中的queue对象,依次进行更新,得到新的state保存到memoizedState上,并返回,更新视图。
其中memoizedState
是用来记录这个useState
应该返回的结果的,而next
指向的是下一次useState
对应的`Hook对象。
例:
function FunctionalComponent () {
const [state1, setState1] = useState(1)
const [state2, setState2] = useState(2)
const [state3, setState3] = useState(3)
}
执行的顺序为:
hook1 => Fiber.memoizedState
state1 === hoo1.memoizedState
hook1.next => hook2
state2 === hook2.memoizedState
hook2.next => hook3
state3 === hook2.memoizedState
next是依赖上一次的state的值,如果某个useState没有执行,这个对应关系就乱了。所以,react规定使用Hooks时,必须在根作用域下使用,不能用于条件语句,循环中。
模拟实现Hooks
整理下Hooks具有的特征:
- 调用useState(),返回一个Tuple([value,setValue])
- useState(),只能在顶层作用域使用,依赖于创建时的顺序。
- 只能在函数组件中使用,不能用于类组件
可以使用数组结构来模拟实现:
let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;
function createSetter(cursor) {
return function setterWithCursor(newVal) {
state[cursor] = newVal;
};
}
export function useState(initVal) {
if (firstRun) {
state.push(initVal);
setters.push(createSetter(cursor));
firstRun = false;
}
const setter = setters[cursor];
const value = state[cursor];
cursor++;
return [value, setter];
}
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
const [lastName, setLastName] = useState("Yardley"); // cursor: 1
return (
<div>
<Button onClick={() => setFirstName("Richard")}>Richard</Button>
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
</div>
);
}
function MyComponent() {
cursor = 0; // resetting the cursor
return <RenderFunctionComponent />; // render
}
console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']
// click the 'Fred' button
console.log(state); // After-click: ['Fred', 'Yardley']