Skip to main content

React elements, children, parents 和 re-render

1. 令人迷惑的 children 使用方式

想象一个场景,里面有频繁的 state 变化,如在 onMouseMove 回调中更新 state

const MovingComponent = () => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
<ChildComponent />
</div>
)
}

React 组件会在 state 更新时 re-render 自身及所有子组件,上面例子中,每次移动鼠标都会更新 state ,进而触发 re-render,这样子组件 ChildComponent 也会 re-render,如果 ChildComponent 比较复杂沉重,这频繁的 re-render 会带来性能问题

除了使用 React.memo, 可以在 ChildComponent 外部提取, 将其作为 children 传递

MovingComponent
const MovingComponent = ({children}) => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
{/* children 不会 re-render */}
{children}
</div>
)
}

再将 MovingComponentChildComponent 组合在一起

SomeOutsideComponent
const SomeOutsideComponent = () => {
return (
<MovingComponent>
<ChildComponent />
</MovingComponent>
)
}

ChildComponent 现在属于 SomeOutsideComponentSomeOutsideComponentMovingComponent 的父组件

这样做之后发现移动鼠标就不会触发 ChildComponentre-render 了,但是有几个令人迷惑的点,接下来一起来看下

1.1 迷惑点1: 为什么作为 children 通过 props 传递,子组件没有 re-render

ChildComponentMovingComponentchildrenMovingComponent 触发了 re-renderchildren 却没有触发 re-render

1.2 迷惑点2:当 children 作为 render function 时,始终 re-render

children 作为一个 render function 时,ChildComponent 就会触发 re-render, 即使它并不依赖于已经改变的 state

const MovingComponent = ({children}) => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
{children({data: 'something'})}
</div>
)
}

// SomeOutsideComponent 不会 re-render
const SomeOutsideComponent = () => {
return (
<MovingComponent>
{/* ChildComponent 会在 MovingComponent state 改变时 触发 re-render,即使没有传递props */}
{() => <ChildComponent />}
</MovingComponent>
)
}

上面这个例子中,SomeOutsideComponent 没有 re-render,但是 ChildComponent 作为 render function 使用时,没有 props 传递,也会触发 re-render

1.3 迷惑点3:React.memo 仅缓存父组件,子组件仍 re-render,包裹了子组件不需要包裹父组件

React.memo,如果对 SomeOutsideComponent 引入一些 state,并尝试用 React.memo 阻止 children re-render

缓存父组件,不缓存子组件
const MovingComponent = ({children}) => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
{children({data: 'something'})}
</div>
)
}

const MovingComponentMemo = React.memo(MovingComponent)

const SomeOutsideComponent = () => {
const [state, setState] = useState()

return (
<MovingComponentMemo>
{/* 父组件 state 变化时 re-render,子组件也会触发 re-render */}
<ChildComponent/>
</MovingComponentMemo>
)
}
只缓存子组件,不缓存父组件
const MovingComponent = ({children}) => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
{children({data: 'something'})}
</div>
)
}

const ChildComponentMemo = React.memo(ChildComponent)

const SomeOutsideComponent = () => {
const [state, setState] = useState()

return (
<MovingComponent>
{/* 父组件 state 变化时 re-render,子组件 不会 触发 re-render */}
<ChildComponentMemo/>
</MovingComponent>
)
}

1.4 迷惑点4:使用 useCallback 包裹 render function,仍 re-render

const SomeOutsideComponent = () => {
const [state, setState] = useState()

const child = useCallback(() => <ChildComponent />, [])

return (
<MovingComponent>
{/* 尽管用 useCallback 包裹缓存了,但是还是 re-render */}
{child}
</MovingComponent>
)
}

2. 探索 React 的 children 是什么

const Parent = ({children}) => {
return <>{children}</>
}

<parent>
<Child />
</parent>

从上面代码看,这个 children 就是 props,我们使用时要么解构,要么就 props.children

甚至可以直接这么写, 效果一样一样的

<Parent children={<Child />} />

和其他的 prop 一样,也可以将组件作为 Element, Function, Component 传递

render function in children
// 作为 props
<Parent children={() => <Child />} />

<Parent>
{() => <Child />}
</Parent>

const Parent = ({children}) => {
return <>{children()}</>
}

3. 探索 React Element

下面这行代码发生了什么

const child = <Child />

可能有人会说这是组件 renderedChild 组件的 渲染周期开始,but,不太对

<Child /> 被称为元素,是 React.createElement() 的语法糖, 返回一个对象

这个对象描述了 实际在 render tree 中出现时,希望在屏幕上看到的内容

React.createElement(
type,
[props],
[...children]
)
// 创建并返回给定类型的新 React 元素。
// 类型参数可以是标签名称字符串(如 'div' 或 'span' )、React 组件类型(类或函数)或 React 片段类型
const child = <Child />;
// 等同于
const child = React.createElement(Child, null, null);

只有在返回结果(函数式组件中相当于“渲染内容”),并且只有Parent 组件渲染自己,才会触发 Child 组件的实际渲染

const Parent = () => {
const child = <Child/>

return <>{child}</>
}

4. 探索更新 Element

Element不可变对象,更新 Element 并触发其相应组件 re-render 的唯一方法是 re-create 一个自身对象,re-render 过程发生的就是这个

const Parent = () => {
// child definition object will be re-created.
// so Child component will be re-rendered when Parent re-renders
const child = <Child />;

return <div>{child}</div>;
};

如果 Parent 组件 re-renderchild 常量将会从头开始重新创建,从 React 的角度看,child是一个新的 Element(re-recreated 了对象), 但是位置和类型完全相同,所以 React 只会用新数据更新现有已经存在的组件(re-render 已经存在的 Child

这也正是缓存(memoization)可以发挥作用的原因:

使用 React.memo 包裹 Child
const ChildMemo = React.memo(Child)

const Parent = () => {
const child = <ChildMemo />

return <>{child}</>
}
使用 useMemo
const Parent = () => {
const child = useMemo(() => <Child/>, [])

return <>{child}</>
}

定义的对象不会被 re-created,React 认为它不需要更新,Child 也不会 re-render

5. 解开迷惑

通过上面的探索过程,我们收集到了下述线索,现在用这些线索解开迷惑吧

  1. const child = <Child/> 只是创建了一个 Element,即定义组件,并不是 render,并且是定义了一个不可变对象
  2. 定义的组件只有在实际的 render tree 中出现时才会 render,函数式组件在最后的return
  3. re-create 定义对象会触发相关组件的 re-render

5.1 迷惑点1: 为什么作为 children 通过 props 传递,子组件没有 re-render

const MovingComponent = ({children}) => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
{/* children 不会 re-render */}
{children}
</div>
)
}

const SomeOutsideComponent = () => {
return (
<MovingComponent>
<ChildComponent />
</MovingComponent>
)
}

children 是在 SomeOutsideComponent 组件中创建的 <ChildComponent/> 元素,MovingComponent state 改变时,MovingComponent re-render,但是 props 没有变化,所以来自 propsElement(定义的对象)没有被 re-create,所以最终不会触发 re-render

5.2 迷惑点2: 当 children 作为 render function 时,始终 re-render

const MovingComponent = ({children}) => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
{children({data: 'something'})}
</div>
)
}

// SomeOutsideComponent 不会 re-render
const SomeOutsideComponent = () => {
return (
<MovingComponent>
{/* ChildComponent 会在 MovingComponent state 改变时 触发 re-render,即使没有传递props */}
{() => <ChildComponent />}
</MovingComponent>
)
}

在这种情况下,children 是一个函数,Element(定义的对象)是调用函数返回的结果,在 MovingComponent 内部调用该函数,也就是 MovingComponent 每次 re-render 时都会调用,即 re-create 定义的对象 <ChildComponent/>,这就是触发其 re-render 的原因

5.3 迷惑点3: React.memo 仅缓存父组件,子组件仍 re-render, 包裹了子组件不需要包裹父组件

缓存父组件,不缓存子组件
const MovingComponent = ({children}) => {
const [state, setState] = useState({x: 100, y: 100})

return (
<div
onMouseMove={(e) => setState({x: e.clientX - 20, y: e.clientY - 20})}
style={{left: state.x, top: state.y}}
>
{children({data: 'something'})}
</div>
)
}

const MovingComponentMemo = React.memo(MovingComponent)

const SomeOutsideComponent = () => {
const [state, setState] = useState()

return (
<MovingComponentMemo>
{/* 父组件 state 变化时 re-render,子组件也会触发 re-render */}
<ChildComponent/>
</MovingComponentMemo>
)
}

这里子组件是 props,和下面这种写法相等

const SomeOutsideComponent = () => {
// ...
return <MovingComponentMemo children={<ChildComponent />} />;
};

在这里我们只对 MovingComponentMemo 进行了缓存,但是他仍然有 children propchildren 接受一个 Element (object)。每次 re-render 时都会 re-create 这个对象,memoized 组件会尝试进行 props 检查,检查到 children prop 改变,就会触发 MovingComponentMemo re-render, 这里 ChildComponent 的定义被 re-create,所以也会触发 re-render

5.4 迷惑点4: 使用 useCallback 包裹 render function,仍 re-render

const SomeOutsideComponent = () => {
const [state, setState] = useState()

const child = useCallback(() => <ChildComponent />, [])

return (
<MovingComponent>
{/* 尽管用 useCallback 包裹缓存了,但是还是 re-render */}
{child}
</MovingComponent>
)
}

上面这段代码 child 作为函数进行传递,且被缓存,但是仍然 re-render,和下面这段代码等同

const SomeOutsideComponent = () => {
const [state, setState] = useState()

const child = useCallback(() => <ChildComponent />, [])

return (
<MovingComponent children={child} />
)
}

如果想阻止 ChildComponent re-render,需要不在这里使用 useCallback 缓存,只是将 ChildComponent 包裹在 React.memo 中,MovingComponent re-renderchildren 函数会被触发,但是它的结果被缓存,所以最终 ChildComponent 不会 re-render

const SomeOutsideComponent = () => {
const [state, setState] = useState()

const child = useCallback(() => <ChildComponent />, [])
const ChildComponentMemo = React.memo(ChildComponent);

return (
<MovingComponent>
{() => <ChildComponentMemo />}
<MovingComponent/>
)
}