写在开头: 前端
因为项目全面技术转型,项目里会大量启用到hooks,因而有了此次写做react
做为一个class组件的重度爱好者,被迫走向了hooks,阅读hook的源码(惨) 算法
这些都是我以前的文章性能优化
正式开始,今天要写什么呢,本来我对react原理很是清楚,本身写过简单的react,带diff算法和异步更新队列的,可是对hooks源码只知其一;不知其二,因而就要深究他的性能相关问题了 - 重复渲染的逻辑微信
因为项目环境比较复杂,若是是纯class组件,那么就是component、pureComponent、shouldComponentUpdate之类的控制一下是否从新渲染,可是hooks彷佛更多场景,接下来一一攻破。dom
父组件异步
export default function Test() { const [state, setState] = useState({ a: 1, b: 1, c: 1 }); const [value, setValue] = useState(11); return ( <div> <div> state{state.a},{state.b} </div> <Button type="default" onClick={() => { //@ts-ignore setState({ a: 2, b: 1 }); //@ts-ignore setState({ a: 2, b: 2 }); console.log(state, 'state'); }} > 测试 </Button> <hr /> <div>value{value}</div> <Button type="default" onClick={() => { setValue(value + 1); }} > 测试 </Button> <Demo value={state} /> </div> ); }
子组件函数
export default class App extends React.Component<Props> { render() { const { props } = this; console.log('demo render'); return ( <div> {props.value.a},{props.value.b} </div> ); } }
结果每次点击图中的测试按钮,子组件Demo都会从新render:
总结:父组件(hook)每次更新,都会导出一个新的state和value对象,子组件确定会更新(若是不作特殊处理)
父组件代码跟上面同样,子组件使用PureComponent:
export default function Test() { const [state, setState] = useState({ a: 1, b: 1, c: 1 }); const [value, setValue] = useState(11); return ( <div> <div> state{state.a},{state.b} </div> <Button type="default" onClick={() => { //@ts-ignore setState({ a: 2, b: 1 }); //@ts-ignore setState({ a: 2, b: 2 }); console.log(state, 'state'); }} > 测试 </Button> <hr /> <div>value{value}</div> <Button type="default" onClick={() => { setValue(value + 1); }} > 测试 </Button> <Demo value={state} /> </div> ); }
子组件使用PureComponent:
export default class App extends React.PureComponent<Props> { render() { const { props } = this; console.log('demo render'); return ( <div> {props.value.a},{props.value.b} </div> ); } }
结果子组件依旧会每次都从新render:
总结:结论同上,确实是依赖的props改变了,由于父组件是hook模式,每次更新都是直接导出新的value和state.
理论:class的setState,若是你传入的是对象,那么就会被异步合并,若是传入的是函数,那么就会立马执行替换,而hook的setState是直接替换,那么setState在hook中是异步仍是同步呢?
**实践:
**
组件A:
export default function Test() { const [state, setState] = useState({ a: 1, b: 1, c: 1 }); const [value, setValue] = useState(11); return ( <div> <div> state{state.a},{state.b},{state.c} </div> <Button type="default" onClick={() => { //@ts-ignore setState({ a: 2 }); //@ts-ignore setState({ b: 2 }); console.log(state, 'state'); }} > 测试 </Button> <hr /> <div>value{value}</div> <Button type="default" onClick={() => { setValue(value + 1); }} > 测试 </Button> <Demo value={state} /> </div> ); }
**我将setState里两次分别设置了state的值为{a:2},{b:2},那么是合并,那么我最终获得state应该是{a:2,b:2,c:1},若是是替换,那么最后获得的state是{b:2}
**
**结果:
**
点击测试按钮后,state变成了{b:2},整个value被替换成了{b:2}
结论:hook的setState是直接替换,而不是合并
父组件:
export default class App extends React.PureComponent { state = { count: 1, }; onClick = () => { const { count } = this.state; this.setState({ count: count + 1, }); }; render() { const { count } = this.state; console.log('father render'); return ( <div> <Demo count={count} /> <Button onClick={this.onClick}>测试</Button> </div> ); } }
子组件:
interface Props { count: number; } export default function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; }
逻辑:父组件(class组件)调用setState,刷新自身,而后传递给hooks子组件,而后自组件从新调用,更新
可是我此时须要想实现一个class 组件的 PureComponent同样的效果,须要用到React.memo
修改父组件代码为:
export default class App extends React.PureComponent { state = { count: 1, value: 1, }; onClick = () => { const { value } = this.state; this.setState({ count: value + 1, }); }; render() { const { count, value } = this.state; console.log('father render'); return ( <div> <Demo count={count} /> {value} <Button onClick={this.onClick}>测试</Button> </div> ); } }
子组件加入memo,代码修改成:
import React, { useState, memo } from 'react'; interface Props { count: number; } function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; } export default memo(App);
此时逻辑:class组件改变了自身的state,本身刷新本身,由上而下,传递了一个没有变化的props给hooks组件,hooks组件使用了memo包裹本身。
结果:
咱们使用了memo实现了PureComponent的效果,浅比较了一次
export default class App extends React.PureComponent { state = { count: 1, value: 1, }; onClick = () => { const { value } = this.state; this.setState({ value: 1, }); }; render() { const { count, value } = this.state; console.log('father render'); return ( <div> <Demo count={count} /> {value} <Button onClick={this.onClick}>测试</Button> </div> ); } }
结果:因为每次设置的值都是同样的(都是1),hooks不会更新,同class
父组件传入count给子组件
export default function Father() { const [count, setCount] = useState(1); const [value, setValue] = useState(1); console.log('father render') return ( <div> <Demo count={count} /> <div>value{value}</div> <Button onClick={() => { setValue(value + 1); }} > 测试 </Button> </div> ); }
子组件使用count
export default function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; }
结果:每次点击测试,都会致使子组件从新render
子组件加入memo
function App(props: Props) { console.log(props, 'props'); return <div>{props.count}</div>; } export default memo(App);
结果:
子组件并无触发更新
⚠️:这里跟第一个案例class的PureComponent不同,第一个案例class的PureComponent子组件此时会从新render,是由于父组件hooks确实每次更新都会导出新的value和state。这里是调用了一次,设置的都是相同的state.因此此时不更新
父组件:
export default function App() { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); const handleClickButton1 = () => { setCount1(count1 + 1); }; const handleClickButton2 = useCallback(() => { setCount2(count2 + 1); }, [count2]); return ( <div> <div> <Button onClickButton={handleClickButton1}>Button1</Button> </div> <div> <Button onClickButton={handleClickButton2}>Button2</Button> </div> </div> ); }
子组件:
import React from 'react'; const Button = (props: any) => { const { onClickButton, children } = props; return ( <> <button onClick={onClickButton}>{children}</button> <span>{Math.random()}</span> </> ); }; export default React.memo(Button)
结果:虽然咱们使用了memo.可是点击demo1,只有demo1后面的数字改变了,demo2没有改变,点击demo2,两个数字都改变了。
那么咱们不使用useCallback看看
父组件修改代码,去掉useCallback
export default function App() { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); const handleClickButton1 = () => { setCount1(count1 + 1); }; const handleClickButton2 = () => { setCount2(count2+ 1); }; return ( <div> <div> <Demo onClickButton={handleClickButton1}>Demo1</Demo> </div> <div> <Demo onClickButton={handleClickButton2}>Demo</Demo> </div> </div> ); }
**子组件代码不变,结果此时每次都会两个数字都会跟着变。
**
官方对useCallback的解释:
就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)
结论:
咱们声明的handleClickButton1是直接定义了一个方法,这也就致使只要是父组件从新渲染(状态或者props更新)就会致使这里声明出一个新的方法,新的方法和旧的方法尽管长的同样,可是依旧是两个不一样的对象,React.memo 对比后发现对象 props 改变,就从新渲染了。
const a =()=>{} const b =()=>{} a===b //false
**这个道理你们都懂,不解释了
**
import React, { useState, useCallback } from 'react'; import Demo from './Demo'; export default function App() { const [count2, setCount2] = useState(0); const handleClickButton2 = useCallback(() => { setCount2(count2 + 1); }, []); return ( <Demo count={count2} onClickButton={handleClickButton2} >测试</Demo> ); }
这样count2的值永远都是0,那么这个组件就不会重导出setCount2这个方法,handleClickButton2这个函数永远不会变化,Button只会更新一次,就是Demo组件接受到的props从0到1到的时候.继续点击,count2也是0,可是props有一次从0-1的过程致使Demo子组件被更新,不过count2始终是0,这很是关键
使用前
export default function App() { const [count, setCount] = useState(0); const [value, setValue] = useState(0); const userInfo = { age: count, name: 'Jace', }; return ( <div> <div> <Demo userInfo={userInfo} /> </div> <div> {value} <Button onClick={() => { setValue(value + 1); }} ></Button> </div> </div> ); }
子组件使用了memo,没有依赖value,只是依赖了count.
可是结果每次父组件修改了value的值后,虽然子组件没有依赖value,并且使用了memo包裹,仍是每次都从新渲染了
import React from 'react'; const Button = (props: any) => { const { userInfo } = props; console.log('sub render'); return ( <> <span>{userInfo.count}</span> </> ); }; export default React.memo(Button);
使用后useMemo
const [count, setCount] = useState(0); const obj = useMemo(() => { return { name: "Peter", age: count }; }, [count]); return <Demo obj={obj}>
*很明显,第一种方式,若是每次hook组件更新,那么hook就会导出一个新的count,const 就会声明一个新的obj对象,即便用了memo包裹,也会被认为是一个新的对象。。*
看看第二种的结果:
父组件更新,没有再影响到子组件了。
写在最后:
为何花了将近4000字来说React hooks的渲染逻辑,React的核心思想,就是拆分到极致的组件化。拆得越细致,性能越好,避免没必要要的更新,就是性能优化的基础,但愿此文能真正帮助到你了解hook的渲染逻辑
CALASFxiaotan
),拉你进技术群,长期交流学习...前端巅峰
」公众号,认真学前端,作个有专业的技术人...点个在看支持我吧,转发就更好了
好文我在看👇