当咱们在用Hooks时,咱们到底在用什么?

开篇有奖

若是你最近一年出去面过试,极可能面临这些问题:html

  • react 16到底作了哪些更新;
  • react hooks用过么,知道其原理么;

第一个问题若是你提到了Fiber reconciler,fiber,链表,新的什么周期,可能在面试官眼里这仅仅是一个及格的回答。如下是我整理的,自我感受还良好的回答:前端

分三步:react

  • react做为一个ui库,将前端编程由传统的命令式编程转变为声明式编程,即所谓的数据驱动视图,但若是简单粗暴的操做,好比讲生成的html直接采用innerHtml替换,会带来重绘重排之类的性能问题。为了尽可能提升性能,React团队引入了虚拟dom,即采用js对象来描述dom树,经过对比先后两次的虚拟对象,来找到最小的dom操做(vdom diff),以此提升性能。
  • 上面提到的vDom diff,在react 16以前,这个过程咱们称之为stack reconciler,它是一个递归的过程,在树很深的时候,单次diff时间过长会形成JS线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿,这在现代前端是一个致命的问题。因此为了解决这种问题,react 团队对整个架构进行了调整,引入了fiber架构,将之前的stack reconciler替换为fiber reconciler。采用增量式渲染。引入了任务优先级(expiration)requestIdleCallback的循环调度算法,简单来讲就是将之前的一根筋diff更新,首先拆分红两个阶段:reconciliationcommit;第一个reconciliation阶段是可打断的,被拆分红一个个的小任务(fiber),在每一侦的渲染空闲期作小任务diff。而后是commit阶段,这个阶段是不拆分且不能打断的,将diff节点的effectTag一口气更新到页面上。
  • 因为reconciliation是能够被打断的,且存在任务优先级的问题,因此会致使commit前的一些生命周期函数屡次被执行, 如componentWillMount、componentWillReceiveProps 和 componetWillUpdate,但react官方已申明这些问题,并将其标记为unsafe,在React17中将会移除
  • 因为每次唤起更新是从根节点(RootFiber)开始,为了更好的节点复用与性能优化。在react中始终存workInprogressTree(future vdom) 与 oldTree(current vdom)两个链表,两个链表相互引用。这无形中又解决了另外一个问题,当workInprogressTree生成报错时,这时也不会致使页面渲染崩溃,而只是更新失败,页面仍然还在。

以上就是我上半年面试本身不断总结迭代出的答案,但愿能对你有所启发。git

接着来回答第二个问题,hooks本质是什么?github

hooks 为何出现

当咱们在谈论React这个UI库时,最早想到的是,数据驱动视图,简单来说就是下面这个公式:面试

view = fn(state)

咱们开发的整个应用,都是不少组件组合而成,这些组件是纯粹,不具有扩展的。由于React不能像普通类同样直接继承,从而达到功能扩展的目的。算法

出现前的逻辑复用

在用react实现业务时,咱们复用一些组件逻辑去扩展另外一个组件,最多见好比Connect,Form.create, Modal。这类组件一般是一个容器,容器内部封装了一些通用的功能(非视觉的占多数),容器里面的内容由被包装的组件本身定制,从而达到必定程度的逻辑复用。编程

在hooks 出现以前,解决这类需求最经常使用的就两种模式:HOC高阶组件Render Propsredux

高阶组件相似于JS中的高阶函数,即输入一个函数,返回一个新的函数, 好比React-Redux中的Connect:性能优化

class Home extends React.Component {
  // UI
}

export default Connect()(Home);

高阶组件因为每次都会返回一个新的组件,对于react来讲,这是不利于diff和状态复用的,因此高阶组件的包装不能在render 方法中进行,而只能像上面那样在组件声明时包裹,这样也就不利于动态传参。而Render Props模式的出现就完美解决了这个问题,其原理就是将要包裹的组件做为props属性传入,而后容器组件调用这个属性,并向其传参, 最多见的用props.children来作这个属性。举个🌰:

class Home extends React.Component {
  // UI
}

<Route path = "/home" render= {(props) => <Home {...props} } />

更多关于render 与 Hoc,能够参见之前写的一片弱文:React进阶,写中后台也能写出花

已存方案的问题

嵌套地狱

上面提到的高阶组件和RenderProps, 看似解决了逻辑复用的问题,但面对复杂需求时,即一个组件须要使用多个复用包裹时,两种方案都会让咱们的代码陷入常见的嵌套地狱, 好比:

class Home extends React.Component {
  // UI
}

export default Connect()(Form.create()(Home));

除了嵌套地狱的写法让人困惑,但更致命的深度会直接影响react组件更新时的diff性能。

函数式编程的普及

Hooks 出现前的函数式组件只是以模板函数存在,而前面两种方案,某种程度都是依赖类组件来完成。而提到了类,就不得不想到下面这些痛点:

  • JS中的this是一个神仙级的存在, 是不少入门开发趟不过的坑;
  • 生命周期的复杂性,不少时候咱们须要在多个生命周期同时编写同一个逻辑
  • 写法臃肿,什么constructor,super,render

因此React团队回归view = fn(state)的初心,但愿函数式组件也能拥有状态管理的能力,让逻辑复用变得更简单,更纯粹。

架构的更新

为何在React 16前,函数式组件不能拥有状态管理?其本质是由于16之前只有类组件在更新时存在实例,而16之后Fiber 架构的出现,让每个节点都拥有对应的实例,也就拥有了保存状态的能力,下面会详讲。

hooks 的本质

有可能,你听到过Hooks的本质就是闭包。可是,若是满分100的话,这个说法最多只能得60分。

哪满分答案是什么呢?闭包 + 两级链表

下面就来一一分解, 下面都以useState来举例剖析。

闭包

JS 中闭包是难点,也是必考点,归纳的讲就是:

闭包是指有权访问另外一个 函数做用域中变量或方法的函数,建立闭包的方式就是在一个函数内建立闭包函数,经过闭包函数访问这个函数的局部变量, 利用闭包能够突破做用链域的特性,将函数 内部的变量和方法传递到外部。
export default function Hooks() {
  const [count, setCount] = useState(0);
  const [age, setAge] = useState(18);

  const self = useRef(0);

  const onClick = useCallback(() => {
    setAge(19);
    setAge(20);
    setAge(21);
  }, []);

  console.log('self', self.current);
  return (
    <div>
      <h2>年龄: {age} <a onClick={onClick}>增长</a></h2>
      <h3>轮次: {count} <a onClick={() => setCount(count => count + 1)}>增长</a></h3>
    </div>
  );
}

以上面的示例来说,闭包就是setAge这个函数,何以见得呢,看组件挂载阶段hook执行的源码:

// packages/react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init) {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = initialArg;
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: initialState,
  });
  // 重点
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
  return [hook.memoizedState, dispatch];
}

因此这个函数就是mountReducer,而产生的闭包就是dispatch函数(对应上面的setAge),被闭包引用的变量就是currentlyRenderingFiberqueue

  • currentlyRenderingFiber: 其实就是workInProgressTree, 即更新时链表当前正在遍历的fiber节点(源码注释:The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook);
  • queue: 指向hook.queue,保存当前hook操做相关的reducer 和 状态的对象,其来源于mountWorkInProgressHook这个函数,下面重点讲;

这个闭包将 fiber节点与action, action 与 state很好的串联起来了,举上面的例子就是:

  • 当点击增长执行setAge, 执行后,新的state更新任务就储存在fiber节点的hook.queue上,并触发更新;
  • 当节点更新时,会遍历queue上的state任务链表,计算最终的state,并进行渲染;

ok,到这,闭包就讲完了。

第一个链表:hooks

在ReactFiberHooks文件开头声明currentHook变量的源码有这样一段注释。

/*
Hooks are stored as a linked list on the fiber's memoizedState field.  
hooks 以链表的形式存储在fiber节点的memoizedState属性上
The current hook list is the list that belongs to the current fiber.
当前的hook链表就是当前正在遍历的fiber节点上的
The work-in-progress hook list is a new list that will be added to the work-in-progress fiber.
work-in-progress hook 就是即将被添加到正在遍历fiber节点的hooks新链表
*/
let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;

从上面的源码注释能够看出hooks链表与fiber链表是极其类似的;也得知hooks 链表是保存在fiber节点的memoizedState属性的, 而赋值是在renderWithHooks函数具体实现的;

export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
  // 获取当前节点的hooks 链表;
  nextCurrentHook = current !== null ? current.memoizedState : null;
  // ...省略一万行
}

有可能代码贴了这么多,你还没反应过来这个hooks 链表具体指什么?

其实就是指一个组件包含的hooks, 好比上面示例中的:

const [count, setCount] = useState(0);
const [age, setAge] = useState(18);
const self = useRef(0);
const onClick = useCallback(() => {
  setAge(19);
  setAge(20);
  setAge(21);
}, []);

造成的链表就是下面这样的:

20200717112830

因此在下一次更新时,再次执行hook,就会去获取当前运行节点的hooks链表;

const hook = updateWorkInProgressHook();
// updateWorkInProgressHook 就是一个纯链表的操做:指向下一个 hook节点

到这 hooks 链表是什么,应该就明白了;这时你可能会更明白,为何hooks不能在循环,判断语句中调用,而只能在函数最外层使用,由于挂载或则更新时,这个队列须要是一致的,才能保证hooks的结果正确。

第二个链表:state

其实state 链表不是hooks独有的,类操做的setState也存在,正是因为这个链表存在,因此有一个经(sa)典(bi)React 面试题:

setState为何默认是异步,何时是同步?

结合实例来看,当点击增长会执行三次setAge

const onClick = useCallback(() => {
  setAge(19);
  setAge(20);
  setAge(21);
}, []);

第一次执行完dispatch后,会造成一个状态待执行任务链表:
20200720111316

若是仔细观察,会发现这个链表仍是一个(会在updateReducer后断开), 这一块设计至关有意思,我如今也还没搞明白为何须要环,值得细品,而创建这个链表的逻辑就在dispatchAction函数中。

function dispatchAction(fiber, queue, action) {
  // 只贴了相关代码
  const update = {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  };
  // 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);
}

上面已经说了,执行setAge 只是造成了状态待执行任务链表,真正获得最终状态,实际上是在下一次更新(获取状态)时,即:

// 读取最新age
const [age, setAge] = useState(18);

而获取最新状态的相关代码逻辑存在于updateReducer中:

function updateReducer(reducer, initialArg,init?) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  // ...隐藏一百行
  // 找出第一个未被执行的任务;
  let first;
  // baseUpdate 只有在updateReducer执行一次后才会有值
  if (baseUpdate !== null) {
    // 在baseUpdate有值后,会有一次解环的操做;
    if (last !== null) {
      last.next = null;
    }
    first = baseUpdate.next;
  } else {
    first = last !== null ? last.next : null;
  }

  if (first !== null) {
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    // do while 遍历待执行任务的状态链表
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        // 优先级不足,先标记,后面再更新
      } else {
        markRenderEventTimeAndConfig(
          updateExpirationTime,
          update.suspenseConfig,
        );

        // Process this update.
        if (update.eagerReducer === reducer) {
          // 简单的说就是状态已经计算过,那就直接用
          newState = update.eagerState;
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
      // 终止条件是指针为空 或 环已遍历完
    } while (update !== null && update !== first);  
    // ...省略100行
    return [newState, dispatch];
  }
}

最后来看,状态更新的逻辑彷佛是最绕的。但若是看过setState,这一块可能就比较容易。至此,第二个链表state就理清楚了。

读到这里,你就应该明白hooks 究竟是怎么实现的:

闭包加两级链表

虽然我这里只站在useState这个hooks作了剖析,但其余hooks的实现基本相似。

另外分享一下在我眼中的hooks,与类组件到底究竟是什么联系:

  • useState: 状态的存储及更新,状态更新会触发组件更新,和类的state相似,只不过setState更新时是采用Object.assign(oldstate, newstate); 而useState的set是直接替代式的
  • useEffect: 相似于之前的componentDidMount 和 componentDidUpdate生命周期钩子(即render 执行后,再执行Effect, 因此当组件与子组件都有Effect时,子组件的Effect先执行), Update须要deps依赖来唤起;
  • useRefs: 用法相似于之前直接挂在类的this上,像this.selfCount 这种,用于变量的临时存储,而又不至于受函数更新,而被重定义;与useState的区别就是,refs的更新不会致使Rerender
  • useMemo: 用法同之前的componentWillReceiveProps与getDerivedStateFromProps中,根据state和props计算出一个新的属性值:计算属性
  • useCallback: 相似于类组件中constructor的bind,但比bind更强大,避免回调函数每次render形成回调函数重复声明,进而形成没必要要的diff;但须要注意deps,否则会掉进闭包的坑
  • useReducer: 和redux中的Reducer相像,和useState同样,执行后能够唤起Rerender

第一次写源码解析,出发点主要两点:

  • 最近半年本身在react确实下了一些功夫,有一个输出也是为了本身之后更好的回忆;
  • 网上太多的人用一个闭包来归纳hooks,我以为这是对技术的亵渎(我的意见);

文章中如有不详或不对之处,欢迎斧正;

推荐阅读: 源码解析React Hook构建过程:没有设计就是最好的设计

首发连接:当咱们在用Hooks时,咱们到底在用什么?

相关文章
相关标签/搜索