在react中,通常用函数组件和类组件这两种方式构建组件。
函数组件
function Example(props){
return (
<div>{ props.name }</div>
)
}
本质上就是函数,函数最后返回DOM结构,通过接收外界prop,渲染界面,不执行与UI无关的逻辑处理。函数组件中没有this,没有生命周期,是无状态组件。
什么是无状态?
function Example(){
let number = 0;
function add(){
number = number + 1;
console.log('执行+');
}
function minus(){
number = number - 1;
console.log('执行-');
}
return (
<div>
<span>{number}</span>
<button onClick={add}>+</button>
<button onClick={minus}>-</button>
</div>
)
}
?上面例子中,如果点击“+”按钮,span中的number会是几?答案是0,所以无状态代表无法保存和更新状态。因为函数组件的这个问题,React还有一种保存状态的方法(类组件)。
类组件
class Example extends React.Component{
constructor() {
super()
this.state = {
number:0
}
}
}
类组件是有状态组件,他通过继承React.component获得一些方法供内部使用,可以通state定义状态,通过setState更改状态。
用类组件改写上面例子:
class Example extends React.Component{
constructor() {
super()
this.state = {
number:0
}
}
add(){
this.setState({
number: this.state.number+1
})
}
minus(){
this.setState({
number: this.state.number-1
})
}
render() {
return (
<div>
<span>{this.state.number}</span>
<button onClick={this.add.bind(this)}>+</button>
<button onClick={this.minus.bind(this)}>-</button>
</div>
)
}
}
此时再点击“+”按钮,span中展示的值就为1了。
类组件的缺点
| 问题 | 解决方案 |
|---|---|
| 生命周期中代码重复,耦合度高 | 无 |
| 逻辑难以复用 | 高阶组件,render props(导致组件树层级很深,形成嵌套地域) |
| this指向 | 匿名函数,bind |
类组件相比于函数组件,性能也更差一点,因为他需要先实例化。
? 而hooks的出现能较好的解决这些问题!
React Hooks
什么是hooks?
Hooks 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。简单来说:函数组件也可以有状态了!
useState
function Example(){
const [number, setNumber] = useState(0)
function add(){
setNumber(number+1)
}
function minus(){
setNumber(number-1)
}
return (
<div>
<span>{number}</span>
<button onClick={add}>+</button>
<button onClick={minus}>-</button>
</div>
)
}
还是用相同的例子进行hooks改写。useState与类组件中的this.state提供的功能完全相同。变量会在重复渲染时被React记住,并且提供最新的值给函数,还可以通过调用 setNumber来更新当前的number。
useEffect
useEffect Hook 相当于类组件中 componentDidMount,componentDidUpdate和componentWillUnmount三个生命周期函数的组合。useEffect告诉react组件在渲染结束后进行一些操作,并且在每次渲染后都重新执行。
如果在挂载时和更新时需要调用相同的逻辑代码
// 运用hook
useEffect(() => {
document.title = `number是${number}`;
})
?
// 类组件
componentDidMount() {
document.title = `number是{number}`;
}
componentDidUpdate() {
document.title = `number是{number}`;
}
这就是之前说的类组件中相同代码逻辑重复!
可选清除effect
内部返回一个函数相当于componentWillUnmount函数(组件卸载)
useEffect(() => {
return () => {
};
});
例子:
useEffect(() => {
let timerId = setInterval(() => {
// ...
}, 100);
return () => {
clearInterval(timerId);
};
});
选择性执行effect
可以选择性传递第二个参数
useEffect(() => {
document.title = `number是${number}`;
}, [number])
- 不传:每次重新渲染都执行
- 传[]:只有在挂载时执行一次
- 传state变量:只有在state变量变化时执行
Hooks原理(?以useState为例)
问题:
– React内部是怎么区分状态?
– 每次重新渲染,如何记住最新的状态?
– 为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?
带着这三个问题来看一下源码~~
if (currentDispatcher !== null) {
currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
currentDispatcher = HooksDispatcherOnMountInDEV;
}
根据currentDispatcher是否为null,给其赋不同的内容。看一下HooksDispatcherOnUpdateInDEV和HooksDispatcherOnMountInDEV里是什么?
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
…
// 其他Hooks
};
// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
…
useState: updateState,
// 其他Hooks
};
所以它不同时机执行不同的dispatcher方法,这个时机是调用通过renderWithHooks知道当前渲染的节点判断使用哪个dispatcher,也就是说在mount和update执行的是不同的useState方法。所以需要分为两个阶段。
初次渲染
看一下mountState()中做了什么
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook(); // 创建新的hook对象并加到链上,返回workInProgressHook
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState; // 获取初始值并初始化hook对象
const queue = (hook.queue = { // 创建更新队列(update quene),并初始化
last: null,
dispatch: null, // 更新函数
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
mounState()中mountWorkInProgressHook()函数是怎么创建hook的?
function mountWorkInProgressHook(): Hook {
const hook: Hook = { // hook数据结构
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
概括来说:mountState所做的就是进行hook对象的创建,每遇到一个hook就通过next将它连接起来,形成链表。并存放在到fiber节点(当前组件)中memorizedState的属性上。将初始状态存入hook.memorizedState和baseState中,创建queue对象赋值给hook.queue(hook对象中的queue存放的是更新队列),创建dispatch更新方法并初始化,方法最后返回初始状态和修改状态的方法。
?Fiber中存放的hook链表大概是这个样子:

更新阶段
根据之前的例子,setNumber执行的就是dispatchAction方法。
function dispatchAction (fiber,queue,action) {
const update = {
action,
next: null,
};
// 将update对象添加到循环链表中
const pending = queue.pending;
if (pending === null) {
// 链表为空,将当前更新作为第一个,并保持循环
update.next = update;
} else {
const first = pending.next;
if (first !== null) {
// 在最新的update对象后面插入新的update对象
update.next = first;
}
pending.next = update;
}
// 将表头保持在最新的update对象上
queue.pending = update;
…
// 进行调度工作
scheduleWork();
}
该方法做了什么呢?
创建一个update,存放起来形成一个更新队列,然后发起调度更新。在调度前还会做一些优化,他会判断是否新旧state一样,如果一样就return,不进行调度。
更新队列是如何存放的是一个难点,更新是以单向环状链表存放的。这样做的好处是可以保证update链表都循环一遍,并能取到最后一个节点,queue.pending永远存放的是最新的update,并且.next能拿到第一个update。

什么是调度呢?
调度就是将这一轮的更新任务按优先级排列,遍历整个fiberTree执行更新操作,就又回到初次渲染的过程,这次是把HooksDispatcherOnUpdateInDEV赋值给currentDispatcher,也就是updateState。看一下updateState~
function updateState (initialState) {
return updateReducer(basicStateReducer);
}
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 拿到更新列表的表头
const pending = queue.pending;
// 获取最早的那个update对象
first = pending !== null ? pending.next : null;
if (first !== null) {
let newState;
let update = first;
do {
// 执行每一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
通过获取到当前要更新的hook对象,从中拿到更新队列进行循环计算和赋值操作,直到update不为第一个放入链表的action,得到最新的state返回出去。

整个流程总结为一张图(省略了调度阶段)?
