关于javascript:当我们在用Hooks时我们到底在用什么

3次阅读

共计 8292 个字符,预计需要花费 21 分钟才能阅读完成。

开篇有奖

如果你最近一年进来面过试,很可能面临这些问题:

  • react 16 到底做了哪些更新;
  • react hooks 用过么,晓得其原理么;

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

分三步:

  • 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 生成报错时,这时也不会导致页面渲染解体,而只是更新失败, 页面依然还在。

以上就是我上半年面试本人一直总结迭代出的答案,心愿能对你有所启发。

接着来答复第二个问题,hooks 实质是什么?

hooks 为什么呈现

当咱们在议论 React 这个 UI 库时,最先想到的是,数据驱动视图,简略来讲就是上面这个公式:

view = fn(state)

咱们开发的整个利用,都是很多组件组合而成,这些组件是纯正,不具备扩大的。因为 React 不能像一般类一样间接继承,从而达到性能扩大的目标。

呈现前的逻辑复用

在用 react 实现业务时,咱们复用一些组件逻辑去扩大另一个组件,最常见比方 Connect,Form.create, Modal。这类组件通常是一个容器,容器外部封装了一些通用的性能(非视觉的占多数),容器外面的内容由被包装的组件本人定制,从而达到肯定水平的逻辑复用。

在 hooks 呈现之前,解决这类需要最罕用的就两种模式:HOC 高阶组件 Render Props

高阶组件相似于 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);
}, []);

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

所以在下一次更新时,再次执行 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 后,会造成一个状态待执行工作链表:

如果仔细观察,会发现这个链表还是一个 (会在 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 时,咱们到底在用什么?

正文完
 0