前言
useCallback、useMemo、memo、shouldUpdate,性能优化的API,不出性能问题你是不该使用的,盲目的使用会造成负优化,甚至还会造成延迟闭包的bug。
在易用性、开发速度和性能上,我会优先考虑易用性和开发速度。
用 useCallback 和 useMemo 这个只是一方面, 除了这些API,React 减少rerender的思路
React的app级粒度更新 —— jsx的灵活让props难以区分变与不变
App是一个组件,Box是一个子组件
const App = () => {
return <Box h="200px" w="200px" {/*.....*/}>hello</Box>
}
// jsx 编译后
const App = () => {
return /*#__PURE__*/ React.createElement(Box, { h: "200px", w: "200px" }, "hello");
};
const App = () => {
return <Box h="200px" w="200px" {/*.....*/}>hello</Box>
}
// jsx 编译后
const App = () => {
return /*#__PURE__*/ React.createElement(Box, { h: "200px", w: "200px" }, "hello");
};
React采用的策略是全等比较,props比了第一回的{ h: "200px", w: "200px" }
和第二回的{ h: "200px", w: "200px" }
,虽然看似相等,但是function每次都创建了新的 object,地址不同,照样渲染,即使是空对象也没辙,也如泄洪一般,从脑袋顶一直渲染到脚底板。Box是会被渲染的
const App = () => {
return <Box>hello</Box> //Box无memo,照样render
}
// jsx 编译后
const App = () => {
//此处props是空对象
return /*#__PURE__*/ React.createElement(Box, {}, "hello");
};
const App = () => {
return <Box>hello</Box> //Box无memo,照样render
}
// jsx 编译后
const App = () => {
//此处props是空对象
return /*#__PURE__*/ React.createElement(Box, {}, "hello");
};
这是React的App级的更新策略 为什么采用这样的策略呢?因为props太多啦,假设是一个复杂的props
{ h: "200px", w: "200px" 此处省略200多个key-value}
我如果浅比较diff半天,结果发现只变了一个prop,还是要让自组件rerender,所以干脆,直接rerender得了。
这样虽然保证万无一失,但是这样的粗粒度对
props不常改变或传参较少,rerender性能昂贵的组件
不太友好。
毕竟是一个大框架,这样的边角问题虽然存在,那我还是要处理的,于是就把shouldUpdate一系列api暴露出来,把问题抛给开发者呗。
反观Vue这边的template,从写法上就不难看出,可以直接找到变和不变。
<Box w="50px" h="50px" :color="color" />
<Box w="50px" h="50px" :color="color" />
如何解决?
在该例子中,得益于JSX语法的灵活和 React App 级的更新粒度。一旦App根部触发更新,泄洪便开始了。
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((a) => a + 1)}>Update</button>
<A /> {/* 或是 <A width="50px"/>,都是不变的props */}
<B count={count}/>
</>
);
};
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((a) => a + 1)}>Update</button>
<A /> {/* 或是 <A width="50px"/>,都是不变的props */}
<B count={count}/>
</>
);
};
解决方法1 状态下沉
既然你知道了状态提升,那么我想聊一聊状态下沉。
很简单,和状态提升反着来,我们只有A用到了count,那么我们就该把const [count, setCount] = useState(0);移到B去,能动小树,就不要动大树。
const App = () => {
return (
<>
<A />
<B />
</>
);
};
const App = () => {
return (
<>
<A />
<B />
</>
);
};
这样做的理由就是,
组件围绕状态进行拆分,达到State 和 View的对应关系
(PS:这在React基本上是不可能完美完成,即使对应不是很精准,大差不差也行)
Vue和Solid这样利用proxy以state为中心,依赖追踪,State和View的对应关系粒度更细,性能也更好。
解决方法2 shouldUpdate剪枝
使用组件优化的api,shouldComponentUpdate、memo、PureComponent等, 原理呢,就是一个高阶组件,相当于给A造了一个父节点,隔离层。
该例里,A组件未传props,不可能由父级改变,套memo,正收益,收益的大小取决于A组件的复杂程度。
解决方法3 Context
React 的 Context 很好地解决了状态跨组件流通的问题。
Redux等React状态管理库就是这一个原理,往往要在App根部加装context.Provider就是证明(PS: 也能让第三方库的 state 与 App 在同一个生命周期,参见 Solid 中的 createRoot)。
const App = () => {
return {...}
}
export default ()=>(
<某某XProvider>
<App/>
</某某XProvider>
)
const App = () => {
return {...}
}
export default ()=>(
<某某XProvider>
<App/>
</某某XProvider>
)
const Leaf = memo(() => {
const title = useContext(myContext);
return <span>{title}</span>
})
const Leaf = memo(() => {
const title = useContext(myContext);
return <span>{title}</span>
})
一个足够小的叶节点,挖空了树干,减少了中间的rerender。
解决方法4 依赖追踪 Solidjs
拿到 state 和对应的 view ,在虚拟 dom + 运行时 diff 的方案显然过于漫长,如果能在编译时就拿到这部分信息就会快很多。
Solidjs 在编译时将一个个小的 div 变成函数
便于理解,类似于
const [count, setCount] = createSignal(1);
div = () => {
return {
innerHTML: count(); // <------ 调用时,currentFn 是 div
}
}
const [count, setCount] = createSignal(1);
div = () => {
return {
innerHTML: count(); // <------ 调用时,currentFn 是 div
}
}
在 Signal 调用时,可以得知是具体在哪个函数中调用,是哪一个叶节点

