如何减少React render次数? 先了解fiber bailout逻辑!

2,572 阅读8分钟

render与bailout

React创建fiber的逻辑renderbailout

  • render:调用render函数(组件),返回JSX,与old fiber进行diff后创建fiber
    • ClassComponent执行render方法。Function Component执行自己。
  • bailout:不执行render,复用old fiber
  • redenrbailoutrender后发现无需更新,然后执行bailout的情况。例如shouComponentUpdate

我们想要减少render,就要了解执行bailout的逻辑。

bailout函数逻辑

尽量复用fiber,不进行render
fiber复用,判断fiber的子树(childLanes)是否有work

  • 有:返回child,继续遍历子树
  • 返回null跳过子树
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // 重用以前的context依赖关系
    workInProgress.dependencies = current.dependencies;
  }
  // 检测子树(childLanes)是否有work
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    //无,跳过子树
    return null;
  }
  // 说明子树有work,继续遍历子树
    
  // workInProgress.child 转化为 workInProgress
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

Fiber执行bailout时机

fiber遍历中,beginWork函数内判断。
前提:必须是update时。也就是必须有old fiber

bailout的情况

关键点:propslanes各类组件单独情况

render前判断

不执行render,执行bailout

//删除部分代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    // 是update阶段,
    if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
      // props不同 需要update
      oldProps !== newProps ||
      // 旧版上下文(现在不使用)
      hasLegacyContextChanged() ||
      // dev时,会判断type,用于热重载
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 需要更新。  (不代表必定更新,例如memo)
      didReceiveUpdate = true;
    } else {
      // 此fiber是否有lanes更新任务
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes);
      if (
        // 没有updateLane
        !hasScheduledUpdateOrContext &&
        // 没有Suspend,错误边界的传递.
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        didReceiveUpdate = false;
        // 无更新, 尝试bailout. 
        // 多数组件会直接进行baliout。
        // Suspend、Offscreen组件可能不会执行bailout。
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      // 有更新任务,但是props没改变,先设置需要更新为false  
      // 组件后续进行确定更新时,会将其设置为true
      didReceiveUpdate = false;
    }
  } else {
    // mount
  }
  // ...非bailout代码 
}

可以推出oldProps === newProps &没有updateLanes&没有Suspend,错误边界的flags尝试进行bailout

  • 多数组件会直接进行baliout
  • SuspendOffscreen组件可能不会执行bailout

oldProps === newProps

props全等时,才能进入bailout逻辑。

jsx的props每次都不一样?

只要执行render,都会调用createElement重新生成JSX
每次JSXprops都是一个新对象

function createElement(type, config, children) {
   // 每次都是新对象
  const props = {};
   // 删去了其余代码。
  return ReactElement( type, props, );
}

// jsx函数也是,jsx函数是v17用来替代createElement的。
function jsx(type, config, children) {
   // 每次都是新对象
  const props = {};
   // 删去了其余代码。
  return ReactElement( type, props, );
}
// 注意:rootFiber的props为null。 

意味着,只要执行renderoldProps !== newProps必然为true,无法bailout

旧版context值无变动

V16.3后都是使用新context了,不用关注它,认为旧版context不会变动即可。

dev环境判断type

dev时,type全等才能bailout,用于热重载。生产环境没有。

lanes无任务

fiberlanes无任务,继续进行baliout逻辑。

function checkScheduledUpdateOrContext(current: Fiber,renderLanes: Lanes,): boolean {
  // 检查fiber.lanes是否有任务
  const updateLanes = current.lanes;
  if (includesSomeLane(updateLanes, renderLanes)) {
    return true;
  }
  // ... lazy context 逻辑。 
  // ... lazy context还在测试,并没有启用。
  return false;
}

这里是不关心子树的lanes

(workInProgress.flags & DidCapture) === NoFlags

fiber没有Suspenderror的传递,执行bailout
到此bailout逻辑判断结束,可以进行bailout了。

render后判断

redner执行后,发现是无需更新的情况,尝试bailout,减少子树render
各组件更新单独判断。

ClassComponent

// 是否需要update,删除非update的代码
function updateClassInstance (){
  //...
  // 是否需要更新
   const shouldUpdate =
    // 是否有ForceUpdate
    checkHasForceUpdateAfterProcessing() ||
    // 检测组件是否要更新
    // 1. 若有shouldComponentUpdate则由其控制。
    // 2. 若是PureComponent,则判断props、state是否equeal。
    // 3. 无shouldComponentUpdate也不是PureComponent则需要更新。
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    )
  //... 随后执行finishClassComponent
  return shouldUpdate
}

// bailout逻辑,删除非bailout的代码
function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes,
) {
  // ...
  const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
   // 不需要更新,且 没有错误边界
  if (!shouldUpdate && !didCaptureError) {
    // bailout
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  // ... 
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

没有ForceUpdate& 组件无需更新时进行bailout

组件无需更新的情况
  • 按照3个逻辑顺序判断。
    1. 若有shouldComponentUpdate则由其控制。
    2. 若是PureComponent,则判断props&state是否equeal
    3. shouldComponentUpdate也不是PureComponent则需要更新。

FunctionComponent

// bailout逻辑,删除非bailout的代码
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  // ...  处理函数组件
  nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes,
  );
  // current存在, 且不需要更新. 
  // beginWroke中didReceiveUpdate赋值为false。
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
    
  // ...  
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

update时& 组件无需更新时进行bailout

举例
function A() {
    const [v, setV] = useState(1)
    console.log('A')
    return <div onClick={() => setV(2)}><AA/></div>
}
function AA() {
    console.log('AA')
    return 'AA'
}

当第一次点击 'A', 'AA'
当第二次点击 'A'。(bailout子树)。
当第三次点击无反应。(state相同,没添加更新)。

MemoComponent

分为SimpleMemo组件和Memo组件,默认使用都是SimpleMemo

updateMemoComponent
// 已删除与bailout逻辑不相干代码
function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
): null | Fiber {
  if (current === null) {
    const type = Component.type;
    // 简单memo组件,非class组件,没有compare,没有默认props,
    if (
      isSimpleFunctionComponent(type) &&
      Component.compare === null &&
      Component.defaultProps === undefined
    ) {
      let resolvedType = type;
      
      // fiber标记为SimpleMemoComponent
      // 此fiber后续,走updateSimpleMemoComponent函数,不在进入此函数。
      workInProgress.tag = SimpleMemoComponent;
      workInProgress.type = resolvedType;
         // ... 创建SimpleMemo组件
      return updateSimpleMemoComponent();
    }
      // ... mount逻辑
    return child;
  }
    
  const currentChild = ((current.child: any): Fiber); 
  // 检测是否有updateLanes任务,逻辑在上面有写。
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes);
  // 无更新任务
  if (!hasScheduledUpdateOrContext) {
    const prevProps = currentChild.memoizedProps;
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;
    // caompare对比相同 且 ref全等  
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      // 进行bailout
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }
  // ... 创建newChild
   const newChild = createWorkInProgress(currentChild, nextProps);
   return newChild;
}

updateSimpleMemoComponent

// SimpleMemoComponent逻辑
function updateSimpleMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
): null | Fiber {
  if (current !== null) {
    const prevProps = current.memoizedProps;
    if (
      // props 浅比较equal
      shallowEqual(prevProps, nextProps) &&
      // ref全等
      current.ref === workInProgress.ref &&
      // 用于热重载
      (__DEV__ ? workInProgress.type === current.type : true)
    ) {
      didReceiveUpdate = false;
      // fiber.lanes没有任务
      if (!checkScheduledUpdateOrContext(current, renderLanes)) {
        workInProgress.lanes = current.lanes;
        // bailout执行
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        );
      } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      }
    }
  }
  // 不能bailout,进入FC
  return updateFunctionComponent(current,workInProgress,Component,nextProps,renderLanes);
}

bailout逻辑

新旧props equal & ref全等 & lanes无任务时进行bailout

ContextProvider

注意:使用Context的组件,contex更新后,组件必定更新,不会受到MemoPureshouComponentUpdate约束。

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  const newValue = newProps.value;

  if (enableLazyContextPropagation) {
    // .. 无逻辑
  } else {
    // oldProps不为null,保证必须传入value 
    if (oldProps !== null) {
      const oldValue = oldProps.value;
      // 新旧value通过Object.is比较。 比全等严格。
      if (is(oldValue, newValue)) {
        if (
          // children全等
          oldProps.children === newProps.children &&
          // 旧版本的context没有变化
          // 旧版本的context不使用了,认为无变化即可。
          !hasLegacyContextChanged()
        ) {
          // bailout
          return bailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes);
        }
      } else {
        // 注意:会给所有使用此context的子组件,安排一个lanes任务
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }
    
  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

oldProps !== null& is(oldValue, newValue)&oldProps.children === newProps.children时进行balout

总结

  • 组件内statecontextupdate都会引起整个子组件render
    • 子组件可以通过MemoComponent减少render
  • 减少props属性的变动,在PureComponent,MemoComponentshouldComponentUpdate中才能发挥作用。
    • 以组件rendner后,子组件默认是要redner的,通过上诉的方法可以避免render
  • ContextProvider会对比childrenvalue,所以子组件尽量用children传递保证不变。

如何减少render次数

Move State Down -- state下沉

state下沉到,单独的子组件中,而不是由父级控制。
举例说明:
App重新render生成的StaticTextprops是新对象。
导致StaticText无法bailout必须进行render

// 组件每次修改state,
// 都会使StaticText重新render,打印log
function App() {
    let [v, setV] = useState('value');
    return (
        <div>
            <input value={v} onChange={(e) => setV(e.target.value)} />
            <p>{v}</p>
            <StaticText/>
        </div>
    );
}

function StaticText() {
    console.log('StaticText');
    return null
}

创建一个子组件From,将state下沉到子组件中。

function App() {
    return (
        <>
            <Form />
            <StaticText />
        </>
    )
}
// state只影响此子组件
function Form() {
    let [v, setV] = useState('value');
    return (
        <>
            <input value={v} onChange={(e) => setV(e.target.value)} />
            <p>{v}</p>
        </>
    );
}
// StaticText不在重新rendenr了
function StaticText() {
    console.log('StaticText');
    return null
}

state现在只和From有关,自然不会导致StaticText重新rendenr

Lift Content Up -- 内容提升(children提升)

组件的子组件由propschildren提供。
举例说明:
使用state下沉后,Form组件中还是有StaticText

function Form() {
    let [v, setV] = useState('value');
    return (
        <div>
            <input value={v} onChange={(e) => setV(e.target.value)} />
            <a href={v}>
                <p>{v}</p>
                <StaticText />
                <StaticText />
            </a>
        </div>
    );
}
// Form 引起 StaticText 重新rendenr。
function StaticText() {
    console.log('StaticText');
    return null
}

将不使用state的组件抽离出去,通过chidlren提供。

// 将不使用state的子组件 提升到父组件,通过children传递。
function FormCore() {
    return (
        <Form>
            <StaticText />
            <StaticText />
        </Form>
    )
}

function Form({children}) {
    let [v, setV] = useState('value');
    return (
        <div>
            <input value={v} onChange={(e) => setV(e.target.value)} />
            <a href={v}>
                <p>{v}</p>
                {children}
            </a>
        </div>
    );
}
// Form 不会在引起 StaticText 重新rendenr
function StaticText() {
    console.log('StaticText');
    return null
}

Context 读写分离

将读写分为2个Context,这样读写不会互相影响redner
或者说: 各自管理Context使用者的更新。
举例说明:
context变化时下面的ReadWrite都会被重新rendenr

const Context = React.createContext();

function Provider({children}) {
    const [v,setV] = useState('v')
    return (
        <Context.Provider value={{setV, v}}>
            {children}
        </Context.Provider>
    );
}
function Read() {
    console.log('read')
    const {v} = useContext(Context)
    return v
}
function Write() {
    console.log('write')
    const {setV} = useContext(Context)
    return <input type='text' onChange={(e)=>setV(e.target.value)}/>
}

function App() {
    return (
        <Provider>
            <Read/>
            <Write/>
        </Provider>
    );
}

现在将读写分离

// 读写2个context
const ReadContext = React.createContext();
const WriteContext = React.createContext();

function Provider({children}) {
    const [v, setV] = useState('')
    // 用useCallback包住,保证不变
    const write = useCallback((v) => setV(v.trim()), [])
    // 2个provide包住
    return (
        <WriteContext.Provider value={write}>
            <ReadContext.Provider value={v}>
                {children}
            </ReadContext.Provider>
        </WriteContext.Provider>
    );
}
// 使用ReadContext
function Read() {
    console.log('read')
    const v = useContext(ReadContext)
    return v
}
// 使用WriteContext
function Write() {
    console.log('write')
    const write = useContext(WriteContext)
    return <input type="text" onChange={(e) => write(e.target.value)} />
}
// 现在Wirte不会被重复render了
function App() {
    return (
        <Provider>
            <Read />
            <Write />
        </Provider>
    );
}

读写context分开,需要哪个用哪个。

减少Props变动

函数

使用useCallback来保证函数不变。

const handleClick = useCallback(() => {
  /*...*/
}, []);

return <App onClick={handleClick} />;

对象

避免使用对象字面量,改用useMemoref

// bad,对象字面量每次都是新对象
return <App value={{number:1}} />;

// good 使用useMemo
const obj = useMemo(()=>({number:1}),[])
return <App value={obj} />;

// good 使用ref
const objRef = useRef({number:1})
return <App value={objRef.current} />;

非必要state,不使用state

数据是源数据且需要渲染到视图,才应该放入state中。
因果关系可以推出的,不渲染的,不使用state,避免无关渲染。

PureComponent & shouldComponentUpdate

Class组件可以使用,对比stateprops, 减少重新渲染。

Reaact.memo

Function组件使用,只对props, 减少重新渲染。

参考