render与bailout
React
创建fiber
的逻辑render
、bailout
。
render
:调用render
函数(组件),返回JSX
,与old fiber
进行diff
后创建fiber
。ClassComponent
执行render
方法。Function Component
执行自己。
bailout
:不执行render
,复用old fiber
。redenr
后bailout
:render
后发现无需更新,然后执行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的情况
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
。 Suspend
、Offscreen
组件可能不会执行bailout
。
oldProps === newProps
jsx的props每次都不一样?
只要执行render
,都会调用createElement
重新生成JSX
。
每次JSX
的props
都是一个新对象。
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。
复制代码
意味着,只要执行render
,oldProps !== newProps
必然为true
,无法bailout
。
旧版context值无变动
V16.3后都是使用新context
了,不用关注它,认为旧版context不会变动即可。
dev环境判断type
dev
时,type
全等才能bailout
,用于热重载。生产环境没有。
lanes无任务
fiber
的lanes
无任务,继续进行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;
}
复制代码
(workInProgress.flags & DidCapture) === NoFlags
fiber
没有Suspend
,error
的传递,执行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个逻辑顺序判断。
- 若有
shouldComponentUpdate
则由其控制。 - 若是
PureComponent
,则判断props
&state
是否equeal
。 - 无
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;
}
复制代码
举例
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
更新后,组件必定更新,不会受到Memo
,Pure
,shouComponentUpdate
约束。
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
。
总结
- 组件内
state
,context
的update
都会引起整个子组件render
。- 子组件可以通过
MemoComponent
减少render
。
- 子组件可以通过
- 减少
props
属性的变动,在PureComponent
,MemoComponent
,shouldComponentUpdate
中才能发挥作用。- 以组件
rendner
后,子组件默认是要redner
的,通过上诉的方法可以避免render
。
- 以组件
ContextProvider
会对比children
、value
,所以子组件尽量用children
传递保证不变。
如何减少render次数
Move State Down -- state下沉
将state
下沉到,单独的子组件中,而不是由父级控制。
举例说明:App
重新render
生成的StaticText
的props
是新对象。
导致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提升)
组件的子组件由props
的children
提供。
举例说明:
使用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
变化时下面的Read
、Write
都会被重新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>
);
}
复制代码
减少Props变动
函数
使用useCallback
来保证函数不变。
const handleClick = useCallback(() => {
/*...*/
}, []);
return <App onClick={handleClick} />;
复制代码
对象
避免使用对象字面量,改用useMemo
,ref
// 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
组件可以使用,对比state
和 props
, 减少重新渲染。