likes
comments
collection
share

React中的Hooks和Class

作者站长头像
站长
· 阅读数 15

从Javascript角度出发

经典面试题目,面向对象三大特性是什么?

答曰:继承、多态、封装

确实,在很多老牌语言中,面向对象都代表着Class这一写法,比如个人之前做的PHP,比如Java等等,其使得代码变得更加模块化、可维护、可扩展和易理解,确实还是蛮舒服的。但是JS却只在es6才出现Class,还只是个语法糖,这主要是因为JS是通过原型委托去进行代码复用的,在出现Class之前,JS可以通过构造函数、原型及原型链来实现面向对象的,所以Class的底层其实还是原型继承,只是提供了这一语法糖来使得JS的面向对象更符合大众。

React中的Hooks和Class

原型的话,写的再好可能还不如多去看看红宝书~

其实Class这块,也是多看红宝书就完事,我这里只是简要说一下:

类可以包含构造方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必须的。空的类定义照样有效。默认情况下,类定义中的代码都在严格模式下执行。

且与函数不同的是,函数受函数作用域限制,类受块作用域限制。

其实new一个类就是在调用构造函数。

从React角度出发

从React角度去思考这个问题的话,便是从组件的角度去开始摸索:

<Demo />

对于React组件而言,其定义方式主要分为两种:

1.Hooks

function Demo() {
    return <div>Hello World</div>
}

2.Class

class Demo extends React.Component {
    render() {
        return <div>Hello World</div>
    }
}

是的,对于用户而言,这两种的调用方式是一样的,但是React要做的远不止这些。 对比这两种方法: Class拥有非常清晰的生命周期,以及内部状态,使得一个组件的代码更为聚合,但是与之而来的缺点便是:

  1. 面向对象写法中复杂的this指向问题;
  2. 组件状态难以复用;
  3. 代码复杂度高,逻辑过于混合;

所以,为什么Hooks的优势便是:

  1. 函数式本来就是js的本命,函数式写法简单明了,且易于解耦合,易于复用;
  2. 不需要再在各个生命周期中放置大量不相关的逻辑,或者同一逻辑需要分散在各个生命周期中;
  3. 使用Hooks(内置/包/自定义)可以极大的简化很多逻辑处理以及组件层级,例如HOC(当然,HOC也可以作为优化方式)等等;

但是,Hooks也是有缺点的:

  1. 闭包问题,且不正确使用Hooks会造成性能问题;
  2. 对于副作用等操作,没有Class较为详细的生命周期,需要换成Hooks的方式去解决;
  3. 过分的原子化组件,可能会造成代码理解上的困难等等。

关于Hooks和Class的性能问题,其实官网是有相应的FQ,会有人说函数闭包的方法,会在每次重渲染更新的时候重新构建函数上下文,但是经过很多人在正常环境下的对比,其实二者差别不大,并且,下面的源码分析中也可以看出Hooks方法相较于Class少了很多例如创建实例等等的操作,且在核心方法上例如diff,都是调用相同的函数,且在Hooks中,很多避免重渲染的手段都开放给了开发者来决定。

常见的Class转换Hooks的方式有: 1.shouldComponentUpdate可以用React.memo代替进行浅比较; 2.componentDidMount和componentDidUpdate可以用useLayoutEffect或者useEffect代替 3.componentWillUnmount相当于useEffect的return等等。

ok,知道二者区别后,我们就要企图搞清楚React是怎么处理他们的。

当然大家都知道的答案是,React对其采用了两种不同的方式,有人会讲,为什么不统一使用new呢,确实,从js的角度出发,当然可以:

React中的Hooks和Class

但是,new并不能处理箭头函数(没有this),并且如图所示,new调用函数,无法返回原始值(new的特性)。 所以在React源码中有如下方法:

function isReactClass(type: any) {
  return type.prototype && type.prototype.isReactComponent;
}

当Class extends React.Component时会在其原型上找isReactComponent来判断是否为Class~

Class

在Hooks出现之前,react就像是原始js,也是两种写法:函数式和class,函数组件就是一个函数,没有this,并不能去组件实例化,只能单纯去渲染UI;而class拥有state、生命周期,可以使react组件具有UI的同时也拥有具体的事件处理能力。

class组件是以面向对象的方式进行编程,以类的方式拥有属性和方法,在组件开发过程中通过this去获取相应的数据和方法,同时对于class组件,react还内置了多种周期以供调用。

我们从render开始分析react是如何处理Class:

function renderRootSync(root: FiberRoot, lanes: Lanes) {
  // ...
  outer: do {
    try {
      if (
        workInProgressSuspendedReason !== NotSuspended &&
        workInProgress !== null
      ) { // ... }
      // 调用workLoopSync循环update
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleThrow(root, thrownValue);
    }
  } while (true)
  // ...
  // 循环更新结束,进入commit阶段
  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;
  // ...
}
function workLoopSync() {
  // Perform work without checking if we need to yield between fiber.
  // workInProgress指向当前正在处理的 fiber 节点
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    // 处理当前节点
    next = beginWork(current, unitOfWork, renderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderLanes);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    // 没有next,结束,向上
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...
  // 根据tag执行不同类型的节点函数
  switch (workInProgress.tag) {
    // ...
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
  }
}
function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  // ...
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  // 若组件并未实例化
  if (instance === null) {
    resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress);

    // In the initial pass we might need to construct the instance.
    // 执行构造函数,获取实例
    constructClassInstance(workInProgress, Component, nextProps);
    // 挂载类组件
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    // 标记
    shouldUpdate = true;
  } else if (current === null) {
    // 实例化后未渲染,当前为首次渲染
    // In a resume, we'll already have an instance we can reuse.
    // 复用实例
    shouldUpdate = resumeMountClassInstance(
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  } else {
    // 不是首次渲染
    // 更新
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  }
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderLanes,
  );
  // ...
  return nextUnitOfWork;
}
function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes,
) {
  // ...
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  // ...
}

以mountClassInstance方法举例:

function mountClassInstance(
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderLanes: Lanes,
): void {
  // ...
  if (
    typeof ctor.getDerivedStateFromProps !== 'function' &&
    typeof instance.getSnapshotBeforeUpdate !== 'function' &&
    (typeof instance.UNSAFE_componentWillMount === 'function' ||
      typeof instance.componentWillMount === 'function')
  ) {
    // 调用实例的生命周期方法
    callComponentWillMount(workInProgress, instance);
    // If we had additional state updates during this life-cycle, let's
    // process them now.
    processUpdateQueue(workInProgress, newProps, instance, renderLanes);
    instance.state = workInProgress.memoizedState;
  }
  // ...
}
function callComponentWillMount(workInProgress: Fiber, instance: any) {
  const oldState = instance.state;

  if (typeof instance.componentWillMount === 'function') {
    // 调用生命周期componentWillMount()
    instance.componentWillMount();
  }
  if (typeof instance.UNSAFE_componentWillMount === 'function') {
    instance.UNSAFE_componentWillMount();
  }

  if (oldState !== instance.state) {
    if (__DEV__) {
      console.error(
        '%s.componentWillMount(): Assigning directly to this.state is ' +
          "deprecated (except inside a component's " +
          'constructor). Use setState instead.',
        getComponentNameFromFiber(workInProgress) || 'Component',
      );
    }
    classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
  }
}

Hooks

Hooks使得函数组件拥有了函数内状态以及多种方法(代替生命周期),使得组件逻辑变得更加简单清晰,更容易复用。

让我们回到beginWork函数中,其中对于FunctionComponent类型,调用updateFunctionComponent方法:

function updateFunctionComponent(
  current: null | Fiber,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  // ...
  let nextChildren;
  let hasId;
  // 读取上下文
  prepareToReadContext(workInProgress, renderLanes);
  if (enableSchedulingProfiler) {
    markComponentRenderStarted(workInProgress);
  }
  if (__DEV__) {
    ReactCurrentOwner.current = workInProgress;
    setIsRendering(true);
    // 调用renderWithHooks执行函数组件更新
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
    hasId = checkDidRenderIdHook();
    setIsRendering(false);
  } else {
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
    hasId = checkDidRenderIdHook();
  }
  if (enableSchedulingProfiler) {
    markComponentRenderStopped();
  }
  // 判断是否可以直接复用fiber
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    // 调用该方法判断fiber的子树是否需要更新
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  if (getIsHydrating() && hasId) {
    pushMaterializedTreeId(workInProgress);
  }

  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  // diff 搞起来
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  if (__DEV__) {
    hookTypesDev =
      current !== null
        ? ((current._debugHookTypes: any): Array<HookType>)
        : null;
    hookTypesUpdateIndexDev = -1;
    // Used for hot reloading:
    ignorePreviousDependencies =
      current !== null && current.type !== workInProgress.type;
  }

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  // 是否为开发环境
  if (__DEV__) {
    if (current !== null && current.memoizedState !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    // 判断当前是否为null,也就是是否为首次挂载
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        // 众所周知,同一个hooks在mount和update阶段是调用不一样的方法
        // HooksDispatcherOnUpdateInDEV对应的是{useCallback: updateCallback,useEffect: updateEffect,...}
        ? HooksDispatcherOnMount
        // HooksDispatcherOnMountWithHookTypesInDEV对应的是{useCallback: mountCallback,useEffect: mountEffect,...}
        : HooksDispatcherOnUpdate;
  }

  const shouldDoubleRenderDEV =
    __DEV__ &&
    debugRenderPhaseSideEffectsForStrictMode &&
    (workInProgress.mode & StrictLegacyMode) !== NoMode;

  shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
  // 返回当前函数组件的return jsx...
  let children = Component(props, secondArg);
  shouldDoubleInvokeUserFnsInHooksDEV = false;

  // Check if there was a render phase update
  // 如 English所说,趋向稳定
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering until the component stabilizes (there are no more render
    // phase updates).
    children = renderWithHooksAgain(
      workInProgress,
      Component,
      props,
      secondArg,
    );
  }

  if (shouldDoubleRenderDEV) {
    // In development, components are invoked twice to help detect side effects.
    // 开发环境下,组件调用两次检查副作用(也就是常见的组件re-render问题之一)
    setIsStrictModeForDevtools(true);
    try {
      children = renderWithHooksAgain(
        workInProgress,
        Component,
        props,
        secondArg,
      );
    } finally {
      setIsStrictModeForDevtools(false);
    }
  }

  finishRenderingHooks(current, workInProgress, Component);

  return children;
}