开篇有奖

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

  • 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.jsfunction 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 只是造成了状态待执行工作链表,真正失去最终状态,其实是在下一次更新(获取状态)时,即:

// 读取最新ageconst [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时,咱们到底在用什么?