vue
<template>
<div :style="{ opacity: number / 300 }">
<div>{{ heavy() }}</div>
</div>
</template>
<script>
export default {
props: ['number'],
methods: {
heavy () {
const n = 100000
let result = 0
for (let i = 0; i < n; i++) {
result += Math.sqrt(Math.cos(Math.sin(42)))
}
return result
}
}
}
</script>
优化后的组件代码如下:
<template>
<div :style="{ opacity: number / 300 }">
<ChildComp/>
</div>
</template>
<script>
export default {
components: {
ChildComp: {
methods: {
heavy () {
const n = 100000
let result = 0
for (let i = 0; i < n; i++) {
result += Math.sqrt(Math.cos(Math.sin(42)))
}
return result
},
},
render (h) {
return h('div', this.heavy())
}
}
},
props: ['number']
}
</script>
通过一个 heavy 函数模拟了一个耗时的任务,且这个函数在每次渲染的时候都会执行一次,所以每次组件的渲染都会消耗较长的时间执行 JavaScript。
而优化后的方式是把这个耗时任务 heavy 函数的执行逻辑用子组件 ChildComp 封装了,由于 Vue 的更新是组件粒度的,虽然每一帧都通过数据修改导致了父组件的重新渲染,但是 ChildComp 却不会重新渲染,因为它的内部也没有任何响应式数据的变化。所以优化后的组件不会在每次渲染都执行耗时任务,自然执行的 JavaScript 时间就变少了。
不过这个场景下的优化用计算属性要比子组件拆分要好。得益于计算属性自身缓存特性,耗时的逻辑也只会在第一次渲染的时候执行,而且使用计算属性也没有额外渲染子组件的开销。
react
看这样一段代码:
import React, { useContext, useState } from "react";
const ThemeContext = React.createContext();
export function ChildNonTheme() {
console.log("不关心皮肤的子组件渲染了");
return <div>我不关心皮肤,皮肤改变的时候别让我重新渲染!</div>;
}
export function ChildWithTheme() {
const theme = useContext(ThemeContext);
return <div>我是有皮肤的哦~ {theme}</div>;
}
export default function App() {
const [theme, setTheme] = useState("light");
const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={theme}>
<button onClick={onChangeTheme}>改变皮肤</button>
<ChildWithTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
</ThemeContext.Provider>
);
}
这段代码看起来没啥问题,也很符合撸起袖子就干的直觉,但是却会让 ChildNonTheme 这个不关心皮肤的子组件,在皮肤状态更改的时候也进行无效的重新渲染。
这本质上是由于 React 是自上而下递归更新, 这样的代码会被 babel 翻译成 React.createElement(ChildNonTheme) 这样的函数调用,React官方经常强调 props 是immutable 的,所以在每次调用函数式组件的时候,都会生成一份新的 props 引用。
来看下 createElement 的返回结构:
const childNonThemeElement = {
type: 'ChildNonTheme',
props: {} // <- 这个引用更新了
}
正是由于这个新的 props 引用,导致 ChildNonTheme 这个组件也重新渲染了。
那么如何避免这个无效的重新渲染呢?关键词是「巧妙利用 children」。
import React, { useContext, useState } from "react";
const ThemeContext = React.createContext();
function ChildNonTheme() {
console.log("不关心皮肤的子组件渲染了");
return <div>我不关心皮肤,皮肤改变的时候别让我重新渲染!</div>;
}
function ChildWithTheme() {
const theme = useContext(ThemeContext);
return <div>我是有皮肤的哦~ {theme}</div>;
}
function ThemeApp({ children }) {
const [theme, setTheme] = useState("light");
const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={theme}>
<button onClick={onChangeTheme}>改变皮肤</button>
{children}
</ThemeContext.Provider>
);
}
export default function App() {
return (
<ThemeApp>
<ChildWithTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
<ChildNonTheme />
</ThemeApp>
);
}
没错,唯一的区别就是我把控制状态的组件和负责展示的子组件给抽离开了,通过 children 传入后直接渲染,由于 children 从外部传入的,也就是说 ThemeApp 这个组件内部不会再有 React.createElement 这样的代码,那么在 setTheme 触发重新渲染后,children 完全没有改变,所以可以直接复用。
让我们再看一下被 ThemeApp 包裹下的 ,它会作为 children 传递给 ThemeApp,ThemeApp 内部的更新完全不会触发外部的 React.createElement,所以会直接复用之前的 element 结果:
// 完全复用,props 也不会改变。
const childNonThemeElement = {
type: ChildNonTheme,
props: {}
}