react基础05-setState的两种方式以及回调函数、路由组件懒加载、hooks、Fragment、context、shouldComponentUpdate和PureComponent进行组件优化、children props和render props、错误边界、react组件通信总结
setState()
1、setState有两种写法
第一种:对象形式
this.setState({ count: count + 1 }, () => console.log(this.state.count))
第二种:函数形式,返回一个对象。函数的第一个参数是state,第二个参数是props
this.setState( (state, props) => ({ count: state.count + 1 }), () => console.log(this.state.count) )
2、setState还有第二个参数,callback,是一个回调函数,可选,它在状态更新完后、视图也更新(render)完后才被调用
如果需要在setState执行后获取最新的状态,就在callback中获取,有点像this.$nextTick()
路由组件懒加载
1、通过lazy方法和import动态加载路由组件
2、通过<Suspense></Suspense>指定在加载得到路由打包文件前显示一个loading界面
import React, { Component, lazy, Suspense } from 'react' import { NavLink, Route, Switch, Redirect } from 'react-router-dom' import Loading from './Loading' // import About from './About' // import Home from './Home' // import Home1 from './Home1' const About = lazy(() => import('./About')) const Home = lazy(() => import('./Home')) const Home1 = lazy(() => import('./Home1')) export default class LazyLoad extends Component { render() { return ( <> <NavLink to="/about" tag="p"> About </NavLink> <NavLink to="/home">Home</NavLink> <Suspense fallback={<Loading />}> <Switch> <Route path="/about" component={About} /> <Route path="/home" component={Home} /> <Route path="/home" component={Home1} /> </Switch> <Redirect from="/" to="/about" /> </Suspense> <style> {` .active { background-color: greenyellow; } `} </style> </> ) } }
Hooks
Hook是react16.8.0新增的语法,可以在函数式组件中使用state及其他的react特性
三个常用的hook
1、useState
const [count, setCount] = useState(0)
第一次初始化指定的值会在内部做缓存
setCount有两种写法:
setCount(count + 1) // 或 setCount(count => count + 1)
2、useEffect
useEffect(() => { const timer = setInterval(() => { setCount(count => count + 1) }, 1000) // 返回的函数相当于componentWillUnmount钩子,组件卸载前执行,在这里清除定时器、取消订阅等 return () => { clearInterval(timer) } }, [])
(1)第一个参数:回调函数,可以执行ajax请求,开启定时器等操作
(2)第二个参数:
第一种情况:不传参,执行1+n次,初始化调用一次,监听所有的state,有任何一个发生改变,都会再次执行一次(有点像render(1+n);也有人说像componentDidUpdate,但是这个钩子初始化不执行,状态发生变化时才会执行(n)) 不传参的情况在实际开发中较少见,要么传个[],要么传个数组里面监听状态
第二种情况:传 [],不监测任何状态,回调函数只会在第一次render后执行,相当于componentDidMount钩子,可以在这里进行ajax请求、开启定时器、订阅消息
第三种情况:传 [count, name],初始化执行一次,并且监听count和name的状态,如果发生改变,执行回调函数(有点像vue中的computed)
(3)返回值:返回一个函数,该函数相当于componentWillUnmount钩子,组件卸载前执行,在这里清除定时器、取消订阅等
3、useRef
const inputRef = useRef() return ( <div> <input type="text" ref={inputRef} /><button onClick={() => console.log(inputRef.current.value)}> 点击打印input框内容 </button> <button onClick={() => (inputRef.current.value = '123')}> 设置input框内容 </button> </div> )
Ref Hook可以在函数式组件中存储/查找组件内的标签或其他数据。功能和类式组件中的createRef()一样
Fragment标签
<Fragment key={1}></Fragment>和<></> 都是不渲染出真实的根标签,减少一层dom结构,为了满足jsx的语法规范
Fragment标签只能接收一个参数key,如果当前组件不参与遍历,使用空标签即可,如果参与遍历,可以使用Fragment标签,传入key值
context
概念:
用于【祖组件】和【后代组件】间通信,类似于vue中的provide / inject
实际开发中一般不用context,使用react-redux中的Provider组件,在入口文件中将App组件包裹起来可以将state传到每个组件中
ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.querySelector('#root') )
使用:
1、通过Provider的value属性传递数据
2、类组件可以通过static关键字声明接收context,也可以通过Consumer组件获取context
3、函数式组件可以通过useContext钩子接收context,也可以通过Consumer组件去获取context
import React, { Component } from 'react' const Context = React.createContext() const { Provider, Consumer } = Context export default class A extends Component { state = { name: '小明', age: 18 } render() { const { name, age } = this.state return ( <div className="box a"> <h2>A组件</h2> <p>用户名:{name}</p> {/* 1、通过Provider包裹子组件,子组件及子组件中的组件都具有context */} <Provider value={{ name, age }}> <B /> </Provider> <style> {` .box{padding:8px;} .a {background-color: greenyellow;} .b {background-color: yellowgreen;} .c {background-color: deeppink;} .d {background-color: pink;} `} </style> </div> ) } } class B extends Component { render() { return ( <div className="box b"> <h3>B组件</h3> <C /> </div> ) } } class C extends Component { static contextType = Context // 2、类式组件的context中如果需要有值,必须写上context声明 render() { return ( <div className="box c"> <h4>C组件</h4> <p>类式组件接收用户名(第一种方式):{this.context.name}</p> <p> <Consumer> {({ name }) => `类式组件接收用户名(第二种方式):${name}`} </Consumer> </p> <D /> </div> ) } } function D() { return ( <div className="d"> <h5>D组件</h5> <p> {/* 3、函数式通过Consumer组件来获取context */} <Consumer>{({ name }) => `函数式组件接收用户名:${name}`}</Consumer> </p> </div> ) }
效果:
组件优化(解决render无效调用)
Component的2个问题:
1、只要执行setState(),即使不改变数据,组件也会重新render,导致效率低
2、只要当前组件重新render,就会触发子组件重新render,即使子组件中没有使用父组件的任何数据,这也会导致效率低
期望的做法:
只有当组件的state或props发生变化时才调用render
原因:
shouldComponentUpdate钩子默认返回true,这导致this.setState({})会调用render函数,但是这啥也没改,调用render就是浪费性能
解决:
第一种:
// 重写shouldComponentUpdate钩子,比较新旧state或props,有变化返回true,没有返回false。这样写的好处:this.setState({})将不会触发render shouldComponentUpdate(nextProps, nextState) { // 该钩子是在render前调用的,但是可以拿到render后的props和state,所以参数名叫nextProps、nextState return this.state.count !== nextState.count }
缺点:当state和props是多个时,需要写的判断太多了
第二种:
使用PureComponent替代Component,PureComponent重写了shouldComponentUpdate钩子,只有当state或props数据有变化才会触发render
注意:不要直接对state进行修改,如下代码react不支持
<button onClick={() => { // this.setState({ count: count + 1 }) const state1 = this.state // 浅复制会导致 // const state1 = JSON.parse(JSON.stringify(this.state)) state1.count += 1 this.setState(state1) }} > 点击+1 </button>
为什么:这里要求回调函数是一个纯函数。如果对this.state进行深复制就可以这么用,新对象和state间就没有关系了
特别注意纯函数中容易踩得坑:使用数组的push、unshift、splice等方法,这都是直接改变数组的,然后this.setState(arr),这不是改了原来的state了么
render props
如何将组件标签中的内容展示出来?
vue:vue插槽
react:
第一种:children props(标签中的内容是一个特殊的标签属性(props传参方式就是写标签属性),属性名叫children) 缺点:如果B组件需要A组件内的数据,做不到
import React, { PureComponent } from 'react' import SetState from '../01setState' export default class index extends PureComponent { render() { return ( <> <h2>父组件</h2><A> <B name={'props小明'} /> </A> </> ) } } class A extends PureComponent { state = { name: '小明', age: 18 } render() { return ( <> <h3>A组件</h3> {this.props.children}</> ) } } class B extends PureComponent { render() { return ( <> <h3>B组件</h3> <p>接收到props的name:{this.props.name}</p> </> ) } }
第二种:render props(类似vue的slot) A组件中通过标签属性render(可以自由命名,但一般都是命名为render),值是一个函数,返回B组件,函数的参数就是A组件中调用render时传的值。
import React, { PureComponent } from 'react' import SetState from '../01setState' export default class index extends PureComponent { render() { return ( <> <h2>父组件</h2> <A render={values => <B name={values.name} />} /></> ) } } class A extends PureComponent { state = { name: '小明', age: 18 } render() { return ( <> <h3>A组件</h3> {this.props.render(this.state)} </> ) } } class B extends PureComponent { render() { return ( <> <h3>B组件</h3> <p>接收到A组件的name:{this.props.name}</p> </> ) } }
效果:
错误边界
组件中的子组件和后代组件,一旦发生错误会导致当前组件的渲染被阻塞,错误边界就是让拦截错误的扩散,哪个子组件出错了就展示备用文案或组件,从而不影响到自身的渲染。
在当前组件中使用getDerivedStateFromError生命周期拦截错误,并且只能捕获子组件/后代组件生命周期(主要是应用于render)产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
import React, { Component } from 'react' import Child from './Child' export default class Parent extends Component { state = { hasError: '', name: '小明' } // 当子组件或后代组件发生错误时触发 static getDerivedStateFromError(err) { console.log(err) return { hasError: err } // render之前触发,需要返回一个对象,对state中的hasError重新赋值 } componentDidCatch(error, info) { console.log('统计错误,反馈给服务器') } render() { console.log(this) return ( <> <h2>Parent组件</h2> {this.state.hasError ? 'Child组件中有错误' : <Child />} <h3>名字:{this.state.name}</h3> </> ) } }
组件间通信方式总结
1、props:
children props
render props(相当于children props的升级,类似vue的slot)
2、消息订阅-发布:
pubsub-js、event等
3、集中式管理:
redux、dva等
4、context:
生产者-消费者模式
搭配方式:
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件:消息订阅-发布、集中式管理、context(多用于插件封装)
x