React 性能优化

2024/09/22 React 共 18299 字,约 53 分钟

熟悉 React 的同学们都知道,每次数据更新都会重新渲染 fiber 树,匹配渲染优先级组件及其所有子组件都会重新渲染,存在心智负担,可能开发中很多同学会掏出 useMemouseCallback, memo, pureComponent, sholdComponentUpdate 组合拳来优化避免组件重复渲染。

组件为什么会被重复渲染?

首先来段示例,当 App 组件触发会影响 Child 重新渲染。

function Child() {
  console.log('Child::render');
  return <div>child</div>
}

funcction App() {
  const [data, setData] = useState(0);
  return (
    <>
      <p>{ data }</p>
      <button onClick={handleClick}>click</button>
      <Child/>
    </>
  );

  function handleClick() {
    setData(data => data + 1);
  }
}

有哪些方法可以避免Child渲染呢?

const Child1 = () => {
  console.log('Child1::render');
  return <div>child</div>
}

const child1 = <Child1/>;

const Child6 = memo(() => {
  console.log('Child6::render');
  return <div>child6</div>
})

funcction App({ children, render }) {
  const [data, setData] = useState(0);
  return (
    <>
      <p>{ data }</p>
      <button onClick={handleClick}>click</button>
      { child1 }  
      { 
        useMemo(() => {
          console.log('Child2::render');
          return <div>child2</div>
        }, [])
      }
      {
        useState(() => {
          console.log('Child3::render');
          return <div>child3</div>
        })[0]
      }
      { children }
      { render }
      <Child6/>
    </>
  );

  function handleClick() {
    setData(data => data + 1);
  }
}

let child5 = () => {
    console.log('Child5::render');
    return <div>child5</div>
}

<App render={child5}>
  { React.createElement(() => {
    console.log('Chil4::render');
    return <div>child4</div>
  }) }
</App>

// 首次渲染
print -> star
  Child2::render
  Child3::render
  Child1::render
  Child4::render
  Child6::render
print -> end

通过点击 button 可以观察到控制台并无 Child 打印内容输出, 子组件并没有被重复渲染; 也就说除了常规包裹 useMemomemo 外,还有很多其他的方式也可以达到类似效果; 可能会有同学疑惑(后面会有解释)

  • children 不是子节点吗,为什么没有重新渲染
  • useState 为什么也可以避免 Child 渲染
  • 打印顺序是什么鬼?

React 组件会根据 state(useState|setState), props, context 来判断当前 fiber 是否可以复用 找到源码中 beginWork 方法, beginWork 主要用于生成子 filber 以及打上对应 flags

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;
  // current 存在 update更新
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    // 判断新旧props是否全等 或者 上下文是否有变化
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged()
    ) {
      didReceiveUpdate = true;
      // 当渲染优先级不包含workInProgress的优先级,复用旧fiber
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      ...
      // 复用fiber
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        ...
        didReceiveUpdate = true;
      } else {
       ...
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

乍一看不是很合理吗,那么为何App组件更新会导致Child组件刷新呢?

首先我们得知道workInProgress.pendingProps到底是啥?

// 源码中
const pendingProps = element.props;

// 那么这个element又是啥呢?
// 格式如下
{
  _owner:null
  _store:{validated: true}
  $$typeof:Symbol(react.element)
  key:null
  props:{}
  ref:null
  type:() => {\n  console.log('Child::RENDER');\n  return /*#__PURE...
}
// 是不是很熟悉,这就是常写得Jsx数据
// 可以打印看看
const Child1 = () => {
  console.log('Child1::render');
  return <div>child</div>;
};

const child1 = <Child1 />;
console.log(child1);

// 换个方式可以证明
let props1 = child1.props;
let props2 = child1.props;
console.log(props1 === props2);
print -> true

// props3 与 props4 生成得Jsx 不是同一引用因此不相等
let props3 = (<Child1 />).props;
let props4 = (<Child1 />).props;
console.log(props3 === props4);
print -> false

// 这个概念类似
const a = { "test": 1 };
const b = { "test": 1'};
console.log(a === b);
print -> false
const c = a; // "c" 仅仅是 "a" 的引用
console.log(a === c); 
print -> true

因此当 App 组件更新时 重新生成 JSXChild 组件对应得 props 内存地址也发生了变化,故会重新渲染.

这也就解释了

  • 同一引用得 Child1 组件不会受到影响。
  • childrenrender 属性也是一样的, 引用未变。
    • Child5Child6 组件是在 App 外部生成的 JSX 并赋值给 App 中的 childrenrender 属性。
    • 所以 App 组件再次渲染子组件便会重新生成 JSX(换句话说只会影响内部的子元素)
    • 因为 App 外层没有变更,因此 App 不会重新生成 JSXchildrenrender 内存地址固然不变咯。

useMemo, memo为何可以避免组件重新渲染?

可能大伙都很熟悉这几个 API, 对于 useMemo 缓存变量,memo 包裹的组件浅比较 props 来判断组件是否需要重新渲染。

memo


// 项目中很多这么使用的(可能有问题,下面会说)
const Test = memo(() => <div>???</div>)
// memo还有第二个参数“手动档”进行新旧props比较
const Test = memo(() => <div>???</div>, (prev, next) => prev !== next)

// 那么 memo 是怎么保障组件渲染的呢
// 源码 updateMemoComponent 中
const prevProps = currentChild.memoizedProps;
// compare 为 “手动档” 回调
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
  // 复用 fiber
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 也就是存在定义的比较函数时使用,否则使用默认比较shallowEqual
// 命中则复用不会重新渲染

所以这也就解释了上面为什么说 memo 包裹的 Child6 不会被重新渲染。 再看看默认比较规则 shallowEqual:

  • 比较内存
  • 不是对象
  • 比较参数长度
  • 遍历参数比较

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}
问: 给所有组件都包裹memo合适吗?

上面大家都可以看到 memo 的好处,但是凡是都有代价的,对比正常的组件多了一层浅比较逻辑不说。 对于参数比较少的组件来说,使用默认规则可能开销不那么大。

// 举个极端的例子,这要走默认比较不得老要命了😉
// 下次看到建议直接打死
const Demo = memo(() => <div>demo</div>)

let records = {
  ...100 arguments
}
<Demo {...records}/>

useMemo

// useMemo 用来缓存原始变量
// 举个栗子
let originData = {
  test: '???',
};

const Test = () => {
  const [data, setData] = useState(0);
  let cacheData = useMemo(() => originData, []);
  console.log(cacheData === originData);

  return <div onClick={() => {
      setData(data => data + 1)
  }}>click</div>
}

// Test 组件每次渲染 都会打印 true
// 上面说了<Child/> 就是JSX对象
// 所以useMemo也能缓存JSX对象引用,  保障新旧props地址相同
问:为什么useMemo可以缓存数据?

其实可以根据上面的示例猜测一下,(执行回调,缓存回调变量??) 细说的话这个问题其实和 Hooks 原理是差不多的。

首次渲染时的 useMemo:

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}
// dispatcher.useMemo 最终会调用 mountMemo

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
// 可以看到 nextValue (也就是需要缓存的变量或者JSX对象)被存入hook.memoizedState中



function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };
  // workInProgressHook 表示组件所创建的hooks链表
  // 为null的话表示当前hook是该组件的第一个hook
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
// mountWorkInProgressHook 
// 创建一个hook
// 是第一个hook, 赋值memoizedState = workInProgressHook = hook
// 不是的话就next连接起来
  // 举个栗子
  useA(); // workInProgressHook = hookA
  useB(); // workInProgressHook = hookA.next -> hookB


//上面的 currentlyRenderingFiber 在 renderWithHooks 中赋值 workInProgress(当前组件fiber)
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;
  ...
  let children = Component(props, secondArg);
  ...
  }

再看看update阶段的useMemo

// dispatcher.useMemo 最终会调用 updateMemo
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 首次渲染缓存的值
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 比较依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 复用旧值
        return prevState[0];
      }
    }
  }
  // 计算新值
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
// updateMemo 整个逻辑还是很简单的



// 再看看比较依赖areHookInputsEqual 
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    return false;
  }

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

大家也可以下面例子验证 useMemo 存储位置,控制台查看 memoizedStatememo 缓存的数据。

function App() {
  return (
    <>
      {useMemo(() => {
        console.log('Child2::render');
        return <div>child2</div>;
      }, [])}
    </>
  );
}
let dom = document.getElementById('root')
let current = dom.__reactContainer$g605vp1tnct
console.log(current.child.memoizedState.memoizedState);
print -> [JSX对象, 依赖]

所以当 App 组件再次渲染时,倘若 useMemo 依赖项没有变更,便会复用(使用上次内存地址,进而新旧 props 也会全等)。

那么useMemo 相同效果的好兄弟 “pureComponents” & “sholdComponentUpdate”; 他们的作用真的相等吗?

pureComponents

类组件只需要继承 pureComponents 便可以避免 render 渲染,是不是神似 memo , 那你知道他们的区别吗?

class Test extends PureComponent {

    render() {
        console.log('Testsssssssssss');
        return "Test"
    }
}

// 源码中 PureComponent 实现
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// 继承Component
Object.assign(pureComponentPrototype, Component.prototype);
// 这里留意一下 isPureReactComponent 标识
pureComponentPrototype.isPureReactComponent = true;

可以在 updateClassInstance 中看到全貌,省略了部分代码 看上去代码量很多,其实每环逻辑很清晰, 主要为 componentDidUpdategetSnapshotBeforeUpdate 添加标记 返回是否需要重新渲染布尔标识(true -> 需要渲染 | false -> 复用)

  1. 判断新旧 propsdata 内存地址是否一致,上下文是否变化
    • 没有变化复用放回 false, 为相关生命周期打添加对应标识
  2. 计算 shouldUpdate
    • shouldUpdatefalse 时代表复用,为相关生命周期打添加对应标识(与上面 1-a 一致)
    • shouldUpdatetrue 代表需要重新 render,存在 componentWillUpdate or UNSAFE_componentWillUpdate 执行。
function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderLanes: Lanes,
): boolean {
  const instance = workInProgress.stateNode;

  const unresolvedOldProps = workInProgress.memoizedProps;
  const oldProps =
    workInProgress.type === workInProgress.elementType
      ? unresolvedOldProps
      : resolveDefaultProps(workInProgress.type, unresolvedOldProps);
  instance.props = oldProps;
  const unresolvedNewProps = workInProgress.pendingProps;

  const oldContext = instance.context;
  const contextType = ctor.contextType;
  let nextContext = emptyContextObject;
  ...

  const oldState = workInProgress.memoizedState;
  let newState = (instance.state = oldState);
  newState = workInProgress.memoizedState;

  if (
    unresolvedOldProps === unresolvedNewProps &&
    oldState === newState &&
    !hasContextChanged() &&
    !checkHasForceUpdateAfterProcessing()
  ) {
    if (typeof instance.componentDidUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Update;
      }
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Snapshot;
      }
    }
    return false;
  }
  ...
  // 重新计算 shouldUpdate
  // 需要留意 checkShouldComponentUpdate
  const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    );
  // 重新渲染 render 
  // 存在 componentWillUpdate or UNSAFE_componentWillUpdate 执行
  // 为 componentDidUpdate and getSnapshotBeforeUpdate 添加标记
  if (shouldUpdate) {
    if (
      !hasNewLifecycles &&
      (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
        typeof instance.componentWillUpdate === 'function')
    ) {
      if (typeof instance.componentWillUpdate === 'function') {
        instance.componentWillUpdate(newProps, newState, nextContext);
      }
      if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
        instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
      }
    }
    if (typeof instance.componentDidUpdate === 'function') {
      workInProgress.flags |= Update;
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
      workInProgress.flags |= Snapshot;
    }
  } else {
    // 复用
    // 为 componentDidUpdate and getSnapshotBeforeUpdate 添加标记
    if (typeof instance.componentDidUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Update;
      }
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Snapshot;
      }
    }

    workInProgress.memoizedProps = newProps;
    workInProgress.memoizedState = newState;
  }

  instance.props = newProps;
  instance.state = newState;
  instance.context = nextContext;

  return shouldUpdate;
}

checkShouldComponentUpdate 中逻辑非常简单:

  1. 存在 shouldComponentUpdate 调用(shouldComponentUpdate 优先级高于 pureComponents
  2. 否则判断是否带有 isPureReactComponent 标识
    • 使用默认比较 shallowEqual (老演员了与上面 memo 一致)
    • 除了比较 props 外还会比较 state 满足一个条件即可
  3. 返回是否渲染状态标识。
function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;
  if (typeof instance.shouldComponentUpdate === 'function') {
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );

    return shouldUpdate;
  }

  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

上面也验证了 PureComponentsmemo 的差异性:

  • 比较规则的差异性
    • 除了比较 props 还会比较 state
    • shouldComponentUpdate 传参与 memo 回调也不相同
  • shouldComponentUpdate 更像是 memo 的第二个回调
    • 优先级高于默认 shallowEqual 比较

所以这么一看 memo 像是 PureComponents + shouldComponentUpdate 的浓缩版😄。

shouldUpdate 渲染标识是怎么影响渲染的?

finishClassComponent 中会根据 shouldUpdate 标识来判断复用 这个 bailoutOnAlreadyFinishedWord (也是老演员了,上面复用都有出现,,大家感兴趣可以自行了解,此处不展开)复用 Filber

// 异常
const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;

if (!shouldUpdate && !didCaptureError) {
  if (hasContext) {
    invalidateContextProvider(workInProgress, Component, false);
  }
  // 复用
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 存在异常
if (
    didCaptureError &&
    typeof Component.getDerivedStateFromError !== 'function'
  ) {
    nextChildren = null;

    if (enableProfilerTimer) {
      stopProfilerTimerIfRunning(workInProgress);
    }
  } else {
      // 重新 render
      nextChildren = instance.render();
  }
  ...

好现在在回溯之前的问题: 给所有的 Class 组件包裹 PureComponents 合适吗?

其实与 上面给所有组件包裹 memo 的问题是一样的,甚至 PureComponents 默认比较某些情况性能还是比 memo 默认比较更差,比如100+的新旧 props 都相等时, 会遍历比较一次后又会遍历比较新旧state, 倘若 state 也是100+ 呢?🤪

其他优化

项目中经常可以看到组件里写组件的方式,这里举例并不是说这样写不行,对于某些场景会有性能差异。

const Item2 = () => {
    useEffect(() => {
        console.log('Item2 is Mount')
    }, []);
    console.log('Item2 is Render')
    return (
        <p>Item------</p>
    );
  }

function App() {
  const [data, setData] = useState(0);
  const Item = () => {
    useEffect(() => {
        console.log('Item is Mount')
    }, []);
    console.log('Item is Render')
    return (
        <p>Item------</p>
    );
  }
  
  return (
      <div>
        {
            Array.from({length: 10}).map((_, index) => <Item key={index}/>)
        }
        {
            Array.from({length: 10}).map((_, index) => <Item2 key={index}/>)
        }
        <button onClick={hanldeClick}>莫碍老子</button>
      </div>
  );
}

--------------------
每当点击触发App更新时, 打印输出如下
click 1
print ->
  Item is Render
  Item2 is Render
  Item2 is Mount
click 2
print ->
  Item is Render
  Item2 is Render
  Item2 is Mount
  Item is Render
  Item2 is Render
  Item2 is Mount

每当点击时。你会发现每次 Item 组件内的 useEffect 都会重新执行, 而 Item2 组件不会这是为何?

  • 唉最开始不是说,App 组件更新,子组件的 JSX 都会重新创建,导致内存地址不同,进而渲染子组件
    • 按照这个逻辑来说,ItemItem2 组件不应该会有差异才对呀
  • 看上去 Item 组件每次都初始化了一般,这是为何?

这个解释起来有点麻烦,需要了解 DiffHooks 原理以及 commit渲染流

reconcileChildrenArray (也叫 Array Diff)-> updateSlot -> updateElement中 可以找到想要的答案。

function updateElement(
    returnFiber: Fiber,
    current: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    if (current !== null) {
      if (
        current.elementType === element.type
      ) {
        // 克隆复用 Fiber
        const existing = useFiber(current, element.props);
        existing.ref = coerceRef(returnFiber, current, element);
        existing.return = returnFiber;
        return existing;
      } else if (enableBlocksAPI && current.tag === Block) {
        ...
      }
    }
    // 重新创建 Fiber
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, current, element);
    created.return = returnFiber;
    return created;
  }

上面只需要留意 current.elementType === element.type

  • current 表示旧的 Fiber 单元, element 也就是最开始介绍过的 JSX 对象,那么这个逻辑就表示判断 Item 组件引用地址,如果相同克隆复用 Fiber, 不同便会新增 Fiber;

  • 而对于 hooks 分为俩个阶段,首次渲染初始化创建 hooks 链表,以及 update 阶段移动链表获取当前每个对应的 hook 计算结果。而 hooks 链表被挂载在组件的 Fiber 上。 因此当重新创建 Fiber 时,上次初始化的 hooks 链表并没有得到保留,进而 renderWithHooks 再次进入执行 Item 组件时,任然是创建 Hooks链表commit 阶段调度 (这里后续文章会说明)。

  • 这也就解释了为什么 Item 组件会每次都会执行 useEffect 回调,这里差异化除了创建 Fiber 的开销外,如果存在 Hooks 还有 Hooks 链表每次创建的开销。

把逻辑全套在setData会有什么问题?

可能会觉得不会有影响,因为执行点击回调时 从点击到视图改变时间维度不会因为你将逻辑抽离而改变,不过是逻辑执行时机的改变:

  • 一个是点击回调执行逻辑后设置值。
  • 一个是传入回调后到 renderWithHooks, 调用 App component, hooks update 计算值的差别。 emmm, 你这说确实没错,倘若你开启了并发模式,那便会有差异了(可以想想)
// 举个栗子
// 如果一个hooks 有多个update 单元
// 这里的lang 表示优先级, action 表示对应需要计算的值
hooks -> queue -> 
              update1 -> update2 ->             update3
              lang=3     lang=1                 lang=3
              action=1   action=(d) => d + 1    action=(d) => 100+more

// 可以看到 update3 为逻辑回调 
// 假设当前渲染优先级为3, 那么本次满足条件应该执行的是 update1 + update3

遍历update链表
update1 -> update2 -> update3
  1
  ^
update1满足渲染条件
newState = 1;
newBaseState = 1;

update1 -> update2 -> update3
            d => d + 1
              ^
update2不满足渲染条件
baseFirst = update2
newState = 1;
newBaseState = 1;


update1 -> update2 -> update3
                      d => 100more
                        ^
update3满足渲染条件
baseFirst = update2 -> update3
newState = (1) => 100more 的结果;
newBaseState = 1;

那么页面上data会暂时展示 newState的结果
下次更新会执行上次跳过的链表以newBaseState为基准, baseFirstupdate2 -> update3
因此你会发现update3 这个逻辑回调单元被执行计算了俩次

至于hooks链表计算详细流程有机会后续文章补充😎

useContent 不正当使用

这里给个结论,具体参考 useContent 章节; 当上下文更新时会额外深度优先遍历 Fiber 匹配消费者的 Context, 因此当 Provider 嵌套的内容越多(比如根元素上),遍历的成本就越高;组件内耦合使用更为合适。

那么有什么合适的方式大范围传值呢?

用过 Vue 的同学都知道 MVVM 的原理,对属性进行劫持,在 VNode 创建阶段获取属性,为其添加对应组件的 updateComponent,属性更新 setter 调度任务执行 updateComponent 创建 VNode Tree 比较更新视图。

通过结合 Proxy, 就可以再状态变更时收集所有的消费者,批量更新 (市面上也有相关的库自行了解)。

小demo

function Store(data) {
    this.data = data;
    let id = 1;
    let promise = Promise.resolve();
    let lineComponent = null;
    this.forceUpdate = null;
    let updateMap = null;
    let proxy = new Proxy(data, {
      get(obj, prop) {
        if (componentMap) {
            if (componentMap.has(lineComponent)) {
                let list = componentMap.get(lineComponent);
                list.add(prop);
            } else {
                componentMap.set(lineComponent, new Set([prop]));
            }
        }
        
        return obj[prop];
      },
      set: (target, key, value) => {
        if (target[key] != value && updateMap) {
            updateMap.set(key, [target[key], value])
            target[key] = value;
        }
        return true;
        }
    });
    let componentMap = new Map();

    this.useUpdate = function useUpdate() {
        let [_, set] = React.useState(0);
        this.forceUpdate = () => set(a => a + 1);
        return (key, val) => {
            updateMap = new Map();
            proxy[key] = val;
            promise.then(this.forceUpdate);
        };
    }
  
    this.useStore = function(cb) {
      return cb(proxy);
    }
    this.Provider = function Provider({ value, children }) {
      return (
        <>
          { children }
        </>
      );
    }
    
    let defaultEqual = function(equal, prev, next) {
        let list = componentMap.get(lineComponent) || [];
        
        for (let i of list) {
            if (updateMap && updateMap.has(i)) {
                return false;
            }
        }
        if (equal) {
            return equal(prev, next);
        }

        for (let i in next) {
            if (Object.is(next[i], prev[i])) {
                continue;
            }
            return false
        }
        return true;
    }
    this.Component = (callback, equal) => {
      id+=1;
      lineComponent = null;
      let JSX = React.memo(callback, defaultEqual.bind(null, equal));
      lineComponent = JSX.type;
      return JSX;
    }
  }

使用

let store = new Store({
  name: 'wujie',
  age: '2-'
})

const Demo = store.Component(() => {
    let { name, age } = store.useStore((data) => ({
        name: data.name,
        age: data.age
    }));
    let set = store.useUpdate();
    return (
        <div onClick={() => {
            set('name',11)
            set('name',33)
            set('age',50)
        }}>data --- { name }--{age}</div>
    );
})

const root = (
    <store.Provider>
        {
            React.createElement(() => {
                let [a, b] = React.useState(0);
                console.log('render.....')
                return (
                    <div>
                        <p onClick={() => b(a + 1)}>a---{a}</p>
                        <Demo/>
                    </div>
                );
            })
        }
    </store.Provider>
)

文档信息

Search

    Table of Contents