Site Overlay

React Hooks

在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 相当于类组件中 componentDidMountcomponentDidUpdatecomponentWillUnmount三个生命周期函数的组合。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])
  1. 不传:每次重新渲染都执行
  2. 传[]:只有在挂载时执行一次
  3. 传state变量:只有在state变量变化时执行

Hooks原理(?以useState为例)

问题:
– React内部是怎么区分状态?
– 每次重新渲染,如何记住最新的状态?
– 为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?

带着这三个问题来看一下源码~~

if (currentDispatcher !== null) {
  currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
  currentDispatcher = HooksDispatcherOnMountInDEV;
}

根据currentDispatcher是否为null,给其赋不同的内容。看一下HooksDispatcherOnUpdateInDEVHooksDispatcherOnMountInDEV里是什么?

// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
 useState: mountState,
…
 // 其他Hooks
};

// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
…
 useState: updateState,
 // 其他Hooks
};

所以它不同时机执行不同的dispatcher方法,这个时机是调用通过renderWithHooks知道当前渲染的节点判断使用哪个dispatcher,也就是说在mountupdate执行的是不同的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.memorizedStatebaseState中,创建queue对象赋值给hook.queue(hook对象中的queue存放的是更新队列),创建dispatch更新方法并初始化,方法最后返回初始状态和修改状态的方法。

?Fiber中存放的hook链表大概是这个样子:
997f34de-b31a-4a66-8782-ab1bc9b4f91c

更新阶段

根据之前的例子,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

0fa5e1dd-aa4c-4868-8205-7ffe533d754f

什么是调度呢?
调度就是将这一轮的更新任务按优先级排列,遍历整个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返回出去。

b79a3987-d7de-4b20-891e-260c504113f1

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

216c1c71-6c9a-4af5-a359-1453d2dff79b