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 传递
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>
);
};
再将 MovingComponent 和 ChildComponent 组合在一起
const SomeOutsideComponent = () => {
return (
<MovingComponent>
<ChildComponent />
</MovingComponent>
);
};
ChildComponent 现在属于 SomeOutsideComponent,SomeOutsideComponent 是 MovingComponent 的父组件
这样做之后发现移动鼠标就不会触发 ChildComponent 的 re-render 了,但是有几个令人迷惑的点,接下来一起来看下
1.1 迷惑点 1: 为什么作为 children 通过 props 传递,子组件没有 re-render
ChildComponent 是 MovingComponent 的 children,MovingComponent 触发了 re-render, children 却没有触发 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 传递
// 作为 props
<Parent children={() => <Child />} />
<Parent>
{() => <Child />}
</Parent>
const Parent = ({children}) => {
return <>{children()}</>
}
3. 探索 React Element
下面这行代码发生了什么
const child = <Child />;
可能有人会说这是组件 rendered,Child 组件的 渲染周期开始,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-render,child 常量将会从头开始重新创建,从 React 的角度看,child是一个新的 Element(re-recreated 了对象), 但是位置和类型完全相同,所以 React 只会用新数据更新现有已经存在的组件(re-render 已经存在的 Child)
这也正是缓存(memoization)可以发挥作用的原因:
const ChildMemo = React.memo(Child);
const Parent = () => {
const child = <ChildMemo />;
return <>{child}</>;
};
const Parent = () => {
const child = useMemo(() => <Child />, []);
return <>{child}</>;
};
定义的对象不会被 re-created,React 认为它不需要更新,Child 也不会 re-render
5. 解开迷惑
通过上面的探索过程,我们收集到了下述线索,现在用这些线索解开迷惑吧
const child = <Child/>只是创建了一个 Element,即定义组件,并不是 render,并且是定义了一个不可变对象- 定义的组件只有在实际的
render tree中出现时才会render,函数式组件在最后的return中 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 没有变化,所以来自 props 的 Element(定义的对象)没有被 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 prop,children 接受一个 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-render, children 函数会被触发,但是它的结果被缓存,所以最终 ChildComponent 不会 re-render
const SomeOutsideComponent = () => {
const [state, setState] = useState()
const child = useCallback(() => <ChildComponent />, [])
const ChildComponentMemo = React.memo(ChildComponent);
return (
<MovingComponent>
{() => <ChildComponentMemo />}
<MovingComponent/>
)
}