React学习笔记(四)—— 组件通信与状态管理、Hooks、Redux、Mobx
react管理状态的工具:
1、利用hooks进行状态管理;
2、利用Redux进行状态管理,这种方式的配套工具比较齐全,可以自定义各种中间件;
3、利用Mobx进行状态管理,它通过透明的函数响应式编程使得状态管理变得简单和可扩展。
2013 年 5 月 React 诞生。但 2015 年之前,大概都是 jQuery 的天下。2015 年 3 月 React 0.13.0 发布,带来了 class 组件写法。
在 React class 组件时代,状态就是 this.state,使用 this.setState 更新。
为避免一团乱麻,React 引入了 "组件" 和 "单向数据流" 的理念。有了状态与组件,自然就有了状态在组件间的传递,一般称为 "通信"。
父子通信较简单,而深层级、远距离组件的通信,则依赖于 "状态提升" + props 层层传递。
于是,React 引入了 Context,一个用于解决组件 "跨级" 通信的官方方案。
但 Context 其实相当于 "状态提升",并没有额外的性能优化,且写起来比较啰嗦。
为优化性能,一般会添加多个 Context,写起来就更啰嗦。在项目没那么复杂时,还不如层层传递简单。
Context 没那么好用,React 官方也没什么最佳实践,于是一个个社区库就诞生了。
目前比较常用的状态管理方式有hooks、redux、mobx三种。
一、组件通信
(1).组件的特点
组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据
在组件化过程中,通常会将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能
(2).知道组件通讯意义
而在这个过程中,多个组件之间不可避免的要共享某些数据
为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通、这个过程就是组件通讯
1.1、父传子
父组件向子组件通信是通过父组件的props传递数据完成。
UserList.jsx接收父组件的数据,展示用户信息,子组件:
import React, { Component } from 'react' export default class UserList extends Component { render() { return ( <div> <ul> {this.props.users.map(user =><li key={user.id}> {user.name} </li>)} </ul> </div> ) } }
UserListContainer.jsx向子组件传递数据,父组件:
import React, { Component } from 'react' import UserList from './UserList' export default class UserListContainer extends Component { state={users:[]} componentDidMount(){ const users=[ {id:"1001",name:"Jone"}, {id:"1002",name:"Mali"}, {id:"1003",name:"Locy"}, {id:"1004",name:"Rose"}, {id:"1005",name:"Jack"} ] this.setState({users:users}); } render() { return ( <div><div>用户信息列表</div> <UserList users={this.state.users}/> </div> ) } }
运行结果:
解释:数据users在父组件中通过属性传递给子组件UserList,在UserList中通过props接收父组件传入的数据,完成父传子,这是最简单,最基本的一个状态的传递方法,推荐常用。
1.2、子传父
子传父依然使用props,父组件先给子组件传递一个回调函数,子组件调用父组件的回调函数传入数据,父组件处理数据即可。
在UserList中添加新增加功能:
import React, { Component } from 'react' export default class UserList extends Component { state={newUser:""} handleChange=e=>{ this.setState({newUser:e.target.value}); } handleClick=e=>{ if(this.state.newUser&&this.state.newUser.length>0){ this.props.onAddUser(this.state.newUser); } } render() { return ( <div> <ul> {this.props.users.map(user =><li key={user.id}> {user.name} </li>)} </ul> <div> 姓名:<input type="text" onChange={this.handleChange} value={this.state.newUser}></input> <button onClick={this.handleClick} type="submit">新增</button> </div> </div> ) } }
在UserListContainer中添加onAddUser参数与函数:
import React, { Component } from 'react' import UserList from './UserList' export default class UserListContainer extends Component { state={users:[]} componentDidMount(){ const users=[ {id:"1001",name:"Jone"}, {id:"1002",name:"Mali"}, {id:"1003",name:"Locy"}, {id:"1004",name:"Rose"}, {id:"1005",name:"Jack"} ] this.setState({users:users}); } onAddUser(newUser){ let users=this.state.users; this.setState({users:users.concat({ id:parseInt((users[users.length-1].id)+1)+"", name:newUser })}); } render() { return ( <div><div>用户信息列表</div> <UserList users={this.state.users} onAddUser={this.onAddUser.bind(this)}/> </div> ) } }
运行:
解释:在子组件中用户输入了一个新的姓名,调用props.addUser方法将新添加的用户信息发送给父组件完成添加功能,所以这里实现了子传父功能。
1.3、兄弟组件间通信
兄弟组件不能直接相互传送数据,需要通过状态提升的方式实现兄弟组件的通信,即把组件之间需要共享的状态保存到距离它们最近的共同父组件内,任意一个兄弟组件都可以通过父组件传递的回调函数来修改共享状态,父组件中共享状态的变化也会通过props向下传递给所有兄弟组件,从而完成兄弟组件之间的通信。
我们在UserListContainer中新增一个子组件UserDetail,用于显示当前选中用户的详细信息,比如用户的年龄、联系方式、家庭地址等。这时,UserList 和 UserDetail 就成了兄弟组件,UserListContainer是它们的共同父组件。当用户在 UserList中点击一条用户信息时,UserDetail需要同步显示该用户的详细信息,因此,可以把当前选中的用户 currentUser保存到UserListContainer的状态中。
UserList.jsx
import React, { Component } from 'react' import "./css/userList.css" /** * 用户列表组件 */ export default class UserList extends Component { /** * 构造函数 * @param {*} props */ constructor(props) { super(props); this.state = { newUser: ""}; } /** * 输入框内容变化事件 * @param {*} e */ handleChange = e => { this.setState({ newUser: e.target.value }); } /** * 新增用户按钮点击事件 * @param {*} e */ handleClick = e => { if (this.state.newUser && this.state.newUser.length > 0) { this.props.onAddUser(this.state.newUser); } } /** * 用户列表项点击事件 * @param {*} userId */ handleSelect(userId) { this.props.onSetCurrentUser(userId); } /** * 渲染函数 */ render() { return ( <div> <ul> {this.props.users.map(user => <li key={user.id} className={user.id === this.props.currentUserId ? "active" : ""} onClick={this.handleSelect.bind(this, user.id)}> {user.name} </li>)} </ul> <div> 姓名:<input type="text" onChange={this.handleChange} value={this.state.newUser}></input> <button onClick={this.handleClick} type="submit">新增</button> </div> </div> ) } }
UserDetails.jsx
import React, { Component } from 'react' export default class UserDetails extends Component { render() { return ( this.props.currentUser? <div> <h2>详细信息</h2> <fieldset> <legend>用户</legend> <p> {/* 用户编号 */} 编号:{this.props.currentUser.id} </p> <p> {/* 用户姓名 */} 姓名:{this.props.currentUser.name} </p> </fieldset> </div>:"" ) } }
UserListContainer.jsx
import React, { Component } from 'react' import UserList from './UserList' import UserDetails from './UserDetails' export default class UserListContainer extends Component { state={users:[],currentUserId:null} componentDidMount(){ const users=[ {id:"1001",name:"Jone"}, {id:"1002",name:"Mali"}, {id:"1003",name:"Locy"}, {id:"1004",name:"Rose"}, {id:"1005",name:"Jack"} ] this.setState({users:users}); } // 添加用户 onAddUser(username){ let users=this.state.users; let newUser={ id:(parseInt(users[users.length-1].id)+1)+"", name:username }; this.setState({users:users.concat(newUser),currentUserId:newUser.id}); } // 设置当前用户 onSetCurrentUser(userId){ this.setState({currentUserId:userId}); } render() { const users=this.state.users.filter(user=>user.id===this.state.currentUserId); const currentUser=users&&users.length>0&&users[0]; return ( <div><div>用户信息列表</div> <UserList users={this.state.users} onAddUser={this.onAddUser.bind(this)} onSetCurrentUser={this.onSetCurrentUser.bind(this)} currentUserId={this.state.currentUserId}/> <UserDetails currentUser={currentUser} /> </div> ) } }
运行效果:
解释:在子组件UserList中添加一个username,通过onAddUser将username传入父组件UserListContainer中,这里完成了状态提升,在UserListContainer中再将新添加的用户传入给UserDetail组件,实现从父传给子组件,整个过程实现了兄弟之间的数据传递功能。
UserListPro.jsx
import React, { Component } from 'react' import "./css/userListPro.css" export default class UserListPro extends Component { state={username:""} usernameChange=(e)=>{ this.setState({username:e.target.value}) } handleSubmit=e=>{ const {username} = this.state; if(username&&username.length>0){ //调用父组件的onAddUser方法将值username传递给父组件 this.props.onAddUser(this.state.username); } e.preventDefault(); }; selectHandle=(e)=>{ //将当前选择的用户编号传给父组件 this.props.onSetCurrentId(e.target.id); }; render() { console.log(this); return ( <div> <ul className='userDetail'> {this.props.users.map(user=><li key={user.id} id={user.id} onClick={this.selectHandle} className={user.id===this.props.currentId?"active":""} >{user.id} - {user.name}</li>)} </ul> <div> <form onSubmit={this.handleSubmit}> <p> <label>姓名:</label> <input type="text" value={this.state.username} onChange={this.usernameChange}/> <button>添加</button> </p> </form> </div> </div> ) } }
UserListContainer.jsx
import React, { Component } from 'react' import UserListPro from './UserListPro'; import UserDetail from './UserDetail'; /** * UserListContainer中包含UserList组件,所以UserListContainer是父组件,而UserList是子组件 * 子组件通过调用父组件中的onAddUser方法将输入的用户添加到集合中,完成子传父功能 */ export default class UserListContainer extends Component { //currentId用于记录当前用户的编号 state={users:[],currentId:null} componentDidMount(){ const users=[ {id:"1001",name:"Jone"}, {id:"1002",name:"Mali"}, {id:"1003",name:"Locy"}, {id:"1004",name:"Rose"}, {id:"1005",name:"Jack"} ]; this.setState({users:users}); } //新添加用户 onAddUser(username){ //生成新的编号 //let id=parseInt(this.state.users[this.state.users.length-1].id)+1; let id=this.state.users[this.state.users.length-1].id*1+1; //将添加的新用户 const user={id,name:username}; //将新用户添加到users状态中 this.setState({users:this.state.users.concat(user),currentId:id}); } //子组件通过该方法设置当前用户的编号 onSetCurrentId=(id)=>{ this.setState({currentId:id}); } render() { //根据用户编号从用户集合中获取用户集合 const users=this.state.users.filter(user => user.id === this.state.currentId); //当前用户 let currentUser=null; //如果查找了 if(users&&users.length>0){ //设置当前用户 currentUser=users[0]; } return ( <div> <h2>用户列表</h2> <UserListPro users={this.state.users} onAddUser={this.onAddUser.bind(this)} currentId={this.state.currentId} onSetCurrentId={this.onSetCurrentId} /> <div> <UserDetail currentUser={currentUser}></UserDetail> </div> </div> ) } }
UserDetail.jsx
import React, { Component } from 'react' export default class UserDetail extends Component { render() { return ( this.props.currentUser? <div> <h2>用户详情</h2> <fieldset> <legend>详细</legend> <p> 编号:{this.props.currentUser.id} </p> <p> 姓名:{this.props.currentUser.name} </p> </fieldset> </div> :"" ) } }
css/userListPro.css
.userDetail li{ cursor: pointer; } .userDetail li:hover{ background: lightyellow; } .active{ background: lightyellow; }
运行结果:
1.4、多级组件通信
当组件所处层级太深时,往往需要经过很层的props传递才能将所需的数据或者回调函数传递给使用组件,所以props作为桥梁通信便会显得很麻烦。React提供了一个context上下文,让任意层级的子组件都可以获取父组件中的状态和方法。
Parent.jsx 父
import React, { Component } from 'react' import Sub1 from './Sub1'; export default class Parent extends Component { state={ n:100 } setN(n){ this.setState({n}); } render() { return ( <div style={{backgroundColor:"lightblue"}}> <ul> <li> <div> <h2>父组件 n={this.state.n}</h2> </div> <Sub1 onSetN={this.setN.bind(this)}></Sub1> </li> </ul> </div> ) } }
Sub1.jsx 子
import React, { Component } from 'react' import Sub11 from './Sub11'; export default class Sub1 extends Component { setNumber() { let n=parseInt(this.txtInput.value); this.props.onSetN(n); } render() { return ( <div style={{background:"lightred"}}> <ul> <li> <h2>子组件:Sub1</h2> <p> <input ref={input=>this.txtInput=input} type="text" /> <button onClick={this.setNumber.bind(this)}>设置N的值</button> </p> <Sub11 onSetN={this.props.onSetN}/> </li> </ul> </div> ) } }
Sub11.jsx 孙
import React, { Component } from 'react' export default class Sub11 extends Component { setNumber() { let n=parseInt(this.txtInput.value); this.props.onSetN(n); } render() { return ( <div style={{background:"lightred"}}> <ul> <li> <h2>孙组件:Sub11</h2> <p> <select ref={input=>this.txtInput=input} type="text"> <option value={300}>300</option> <option value={600}>600</option> <option value={900}>900</option> </select> <button onClick={this.setNumber.bind(this)}>设置N的值</button> </p> </li> </ul> </div> ) } }
结果:
解释:Parent组件将setN传递给Sub1,Sub1再传递给Sub11,Sub11调用SetN设置父组件的值,这样做可以实现组件间的多级传递,但非常麻烦,如果层级较多时。
1.5、Context
当组件所处层级太深时,往往需要经过很层的props传递才能将所需的数据或者回调函数传递给使用组件,所以props作为桥梁通信便会显得很麻烦。React提供了一个context上下文,让任意层级的子组件都可以获取父组件中的状态和方法。
每个组件都拥有context属性,可以查看到:
getChildContext:与访问context属性需要通过contextTypes指定可访问的属性一样,getChildContext指定的传递给子组件的属性需要先通过childContextTypes来执行,不然会报错。
使用context改进后的示例如下:
Parent.jsx 父
import React, { Component } from 'react' import {PropTypes} from 'prop-types' import Sub1 from './Sub1'; class Parent extends Component { state={ n:100 } setN=(n)=>{ this.setState({n}); } getChildContext(){ return {onSetN: this.setN} } render() { return ( <div style={{backgroundColor:"lightblue"}}> <ul> <li> <div> <h2>父组件 n={this.state.n}</h2> </div> <Sub1></Sub1> </li> </ul> </div> ) } } //声明context的属性的类型信息 Parent.childContextTypes = { onSetN:PropTypes.func } export default Parent;
Sub1.jsx 子
import React, { Component } from 'react' import Sub11 from './Sub11'; import {PropTypes} from 'prop-types' class Sub1 extends Component { setNumber() { let n=parseInt(this.txtInput.value); this.context.onSetN(n); } render() { return ( <div style={{background:"lightred"}}> <ul> <li> <h2>子组件:Sub1</h2> <p> <input ref={input=>this.txtInput=input} type="text" /> <button onClick={this.setNumber.bind(this)}>设置N的值</button> </p> <Sub11/> </li> </ul> </div> ) } } //声明要使用的context属性的类型信息 Sub1.contextTypes={ onSetN: PropTypes.func } export default Sub1;
Sub11.jsx 孙
import React, { Component } from 'react' import {PropTypes} from 'prop-types' class Sub11 extends Component { setNumber() { let n=parseInt(this.txtInput.value); this.context.onSetN(n); console.log(this); } render() { return ( <div style={{background:"lightred"}}> <ul> <li> <h2>孙组件:Sub11</h2> <p> <select ref={input=>this.txtInput=input} type="text"> <option value={300}>300</option> <option value={600}>600</option> <option value={900}>900</option> </select> <button onClick={this.setNumber.bind(this)}>设置N的值</button> </p> </li> </ul> </div> ) } } //声明要使用的context属性的类型信息 Sub11.contextTypes={ onSetN: PropTypes.func } export default Sub11;
结果:
解释:父组件通过getChildContext向自己的后代组件传递一个对象,使用childContextTypes声明属性的类型,在后代组件中使用this.context接收父组件传入的对象,不过所有的要使用父组件中传入对象的子组件都要声明contextTypes对象的属性类型信息。这样就实现了一次下发,多处使用,完成了组件间数据的传递。
二、Hooks
2.1、解构
1、数组解构就是能快速提取数组中的指定成员(数组的某一项值或所有的值)
例如:
解构赋值都是一一对应的,按照顺序。
const arr = [200,300,400] const [a,b,c] = arr console.log(a,b,c) // 200,300,400
也可以取数组的某一项值(结构必须保持一致)
const arr = [200,300,400] const [, , c] = arr console.log(c) // 400
还可在用“...”的方式提取所有的成员(注意的是这种...的写法只能在解构成员的最后一个成员使用)代码如下
const arr = [200,300,400] const [a,...all] = arr console.log(all) // [300,400] 会返回得到一个最后所有的数组
如果提取的解构成员小于数组的长度,就会从前到后的顺序来提取,代码如
const arr = [200,300,400] const [a] = arr console.log(a) // 200 按顺序提取第一个
如果提取成员大于数组长度,那么最后的提取的最后是undefined,代码如下
const arr = [200,300,400] const [a,b,c,d] = arr console.log(d) // undefined
2、对象解构和数组解构基本类似,只不过对象解构的取值方式是根据对象的属性名来取值
例如:
const obj = {name:'100',age:'30',size:'M'} const { name } = obj console.log(name) // 100
顺便说一下,对象里面的属性名和其他自定义的变量名称如果重名的时候要怎么解决,一旦重名就会报错,看代码:
const obj = {name:'100',age:'30',size:'M'} const name = 'lucy' const {name} = obj console.log(name) // 会报错
// 要么重新命名,要么可以按照下面的写法来避免
const obj = {name:'100',age:'30',size:'M'} const name = 'lucy' const {name:nameObj} = obj // 对象属性名称的重新指定 console.log(nameObj) // 100
思考题:
请问下面的代码是什么意思?控制台输出什么?
<script> function useBook() { return ["ES6高级编程", (bookname) => console.log(bookname + "!")]; } const [name, showBook] = useBook(); showBook(name); </script>
答案:

ES6高级编程!
2.2、Hooks基础
Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state,一般搭配函数式组件使用。
在React 16.8之前,函数式组件只能作为无状态组件(只读组件),即不能进行状态管理。
函数式创建组件通常是无状态组件,这种方式没有办法在内部对状态统一管理,如果我们非要添加状态管理呢,那就只能借助redux啦~或者我们自己利用观察者模式实现一个发布订阅。
那么如果我们非要这么做,React版本在16.8.X以后增添了一个新特性就是hooks。
hooks涉及API有useState、 useEffect、 useCallback、 useRef、 useMemo、 React.memo、 useReducer等,具体可以参考官方文档。
2.2.1、useState() hooks状态钩子
搭配函数式组件,主要是可以进行组件的状态管理,好处是不像传统state需要注意this指向(函数式组件中没有this)。
一般用法 const [ a , setA ] = useState(初始值)
a表示组件需要声明的变量a,setA允许你在组件其它的位置对a的数据进行改变setA(2),即a的值将为2
一个组件中可以声明多个useState()
Counter3.jsx
import React,{useState} from 'react'; export default function Counter3(props){ let [count,setCount]=useState(0); return (<div> <h2>{props.name}</h2> <p> <button onClick={()=>setCount(count+1)}>{count}</button> </p> </div>); }
调用
const vnode = (
<div>
<Counter3 name="函数式组件使用状态的计数器" />
</div>
);
结果:
2.2.2、useEffect 副作用钩子
import React,{useState,useEffect} from 'react'; export default function Counter3(props){ let [count,setCount]=useState(0); useEffect(()=>{ console.log(`当前count的值为${count}`); }) return (<div> <h2>{props.name}</h2> <p> <button onClick={()=>setCount(count+1)}>{count}</button> </p> </div>); }
运行时的初始状态:
解释:
点击时的效果:
解释:
卸载时执行,在userEffect回调函数中返回一个函数将在组件卸载与更新阶段执行:
Counter3.jsx
import React,{useState,useEffect} from 'react'; export default function Counter3(props){ let [count,setCount]=useState(0); useEffect(()=>{ console.log(`当前count的值为${count}`); return ()=>{ console.log("组件被卸载了"); } }) return (<div> <h2>{props.name}</h2> <p> <button onClick={()=>setCount(count+1)}>{count}</button> </p> </div>); }
Box.jsx
import React, { Component } from "react"; import Counter3 from "./Counter3"; export default class Box extends Component { state = { isShow: true }; render() { return ( <div> <h2>父组件容器</h2> {this.state.isShow ? <Counter3 /> : <span>子组件不显示</span>} <p> <button type="button" onClick={() => this.setState({ isShow: !this.state.isShow })} > {this.isShow ? "隐藏" : "显示"} </button> </p> </div> ); } }
初始效果:
点击时的效果
如果想仅在卸载时执行则可以指定第二个参数,声明要监视的成员,如:
useEffect(()=>{ console.log(`当前count的值为${count}`); return ()=>{ console.log("组件被卸载了"); } },[]);
这里使用的是一个空数组表明没有任何成员的变化会引起useEffect执行,再看看卸载时的效果:
点击时count的更新并没有触发useEffect执行,卸载将执行
如果挂载、更新、卸载都需要处理,则可以这样:
import React,{useState,useEffect} from 'react'; export default function Counter3(props){ let [count,setCount]=useState(0); useEffect(()=>{ console.log(`组件挂载或更新了${count}`); },[count]); useEffect(()=>{ return ()=>{ console.log("组件被卸载了"); } },[]); return (<div> <h2>{props.name}</h2> <p> <button onClick={()=>setCount(count+1)}>{count}</button> </p> </div>); }
效果:
我们使用 useEffect 重写了上面的例子,useEffect 第一个参数接收一个函数,可以用来做一些副作用比如异步请求,修改外部参数等行为,而第二个参数称之为dependencies,是一个数组,如果数组中的值变化才会触发 执行useEffect 第一个参数中的函数。返回值(如果有)则在组件销毁或者调用函数前调用。
- 1.比如第一个 useEffect 中,理解起来就是一旦 count 值发生改变,则修改 documen.title 值;
- 2.而第二个 useEffect 中传递了一个空数组[],这种情况下只有在组件初始化或销毁的时候才会触发,用来代替 componentDidMount 和 componentWillUnmount,慎用;
- 3.还有另外一个情况,就是不传递第二个参数,也就是useEffect只接收了第一个函数参数,代表不监听任何参数变化。每次渲染DOM之后,都会执行useEffect中的函数。
基于这个强大 Hooks,我们可以模拟封装出其他生命周期函数,比如 componentDidUpdate 代码十分简单
function useUpdate(fn) { // useRef 创建一个引用 const mounting = useRef(true); useEffect(() => { if (mounting.current) { mounting.current = false; } else { fn(); } }); }
现在我们有了 useState 管理状态,useEffect 处理副作用,异步逻辑,学会这两招足以应对大部分类组件的使用场景。
import React,{useState,useEffect} from 'react' export default function Counter8(){ let [count,setCount]=useState(0); let [n,setN]=useState(0); let [isInit,setIsInit]=useState(true); //副作用钩子 useEffect(()=>{ return ()=>{ console.log("组件被卸载了!componentWillUnmount "); }; },[]); useEffect(()=>{ console.log("组件挂载成功了!componentDidMount"); },[]); useEffect(()=>{ if(!isInit){ console.log("组件更新成功了!componentDidUpdate"); } else{ setIsInit(false); } },[count,n]); return (<div> <h2>计数器 count={count} n={n}</h2> <button onClick={()=>{setCount(count+1)}} >count++</button> <button onClick={()=>{setN(n+1)}} >n++</button> </div>) }
2.2.3、useContext()获取上下文
获取context 对象 用法:const value = useContext(MyContext);
上面介绍了 useState、useEffect 这两个最基本的 API,接下来介绍的 useContext 是 React 帮你封装好的,用来处理多层级传递数据的方式,在以前组件树种,跨层级祖先组件想要给孙子组件传递数据的时候,除了一层层 props 往下透传之外,我们还可以使用 React Context API 来帮我们做这件事,举个简单的例子:
App.js
import React from "react"; const { Provider, Consumer } = React.createContext(null); function Bar() { return <Consumer>{(color) => <h2>{color}</h2>}</Consumer>; } function Foo() { return <Bar />; } function App() { return ( <Provider value={"blue"}> <Foo /> </Provider> ); } export default App;
通过 React createContext 的语法,在 APP 组件中可以跨过 Foo 组件给 Bar 传递数据。而在 React Hooks 中,我们可以使用 useContext 进行改造。
import React, { useContext } from "react"; const colorContext = React.createContext("red"); function Bar() { const color = useContext(colorContext); return <h2>{color}</h2>; } function Foo() { return <Bar />; } function App() { return ( <colorContext.Provider value="blue"> <Foo /> </colorContext.Provider> ); } export default App;
传递给 useContext 的是 context 而不是 consumer,返回值即是想要透传的数据了。用法很简单,使用 useContext 可以解决 Consumer 多状态嵌套的问题。
function HeaderBar() { return ( <CurrentUser.Consumer> {user => <Notifications.Consumer> {notifications => <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> } } </CurrentUser.Consumer> ); }
而使用 useContext 则变得十分简洁,可读性更强且不会增加组件树深度。
function HeaderBar() { const user = useContext(CurrentUser); const notifications = useContext(Notifications); return ( <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> ); }
示例:
import React, { useState, createContext, useContext } from "react"; const CountContext = createContext(0); const Example = () => { const [count, setCount] = useState<number>(0); return ( <div> <p>父组件点击数量:{count}</p> <button onClick={() => setCount(count + 1)}>{"点击+1"}</button> <CountContext.Provider value={count}> <Counter /> </CountContext.Provider> </div> ); }; const Counter = () => { const count = useContext(CountContext); return <p>子组件获得的点击数量:{count}</p>; }; export default Example;
运行效果:
2.2.5、useReducer () 复杂状态管理
useReducer 这个 Hooks 在使用上几乎跟 Redux/React-Redux 一模一样,唯一缺少的就是无法使用 redux 提供的中间件。我们将上述的计时器组件改写为 useReducer,
作用:复杂状态管理,跟redux本质上是一样的
函数式组件如果涉及到状态管理,我们需要借助redux,那么hooks需要吗,答案也是一样的,简单的状态管理我们可以通过useState来进行管理,如果比较复杂的状态管理呢,react hook给我们提供了方法 useReducer
使用useReducer分四步
- 创建初始值
initialState
const reducer = (state, action) => {switch(action.type) {case:}}传入旧的数据
state
和创建所有操作action
const [state,dispatch] = useReducer(reducer,initialState)传给useReducer,得到读和写API,必须写在函数里面
dispatch({type:'add '})
调用写的类型
总的来说,useReducer是useState的复杂版,所有useState的规则,useReducer都适用
useReducer(dispatch,state,init)
import React, { useState } from "react"; export default function Counter9() { let [count, setCount] = useState(0); return ( <div> <h2>计数器 count={count}</h2> <button onClick={() => { setCount(count + 5); }} > +5 </button> <button onClick={() => { setCount(count - 3); }} > -3 </button> <button onClick={() => { setCount(0); }} > 重置 </button> <button onClick={() => { setCount(count * 10); }} > *10 </button> </div> ); }
import React, { useReducer } from 'react' function reducer(state,action){ switch(action.type){ case 'increment': return {count:state.count+action.payload}; case 'decrement': return {count:state.count-action.payload}; case 'reset': return {count:action.payload}; default: throw new Error('未知的action'); } } function Counter4(props){ const [state,dispatch]=useReducer(reducer,{count:0}); return ( <div> <h2>计数器 - count={state.count}</h2> <button onClick={()=>{dispatch({type:"increment",payload:5})}}>+5</button> <button onClick={()=>{dispatch({type:"decrement",payload:3})}}>-3</button> <button onClick={()=>{dispatch({type:"reset",payload:0})}}>重置</button> <button onClick={()=>{dispatch({type:"multiplication",payload:10})}}>乘10</button> </div> ) } export default Counter4;
运行结果:
用法跟 Redux 基本上是一致的,用法也很简单,算是提供一个 mini 的 Redux 版本。
用法与 useState 类似,从 useReducer 中得到读接口 state,写接口 dispatch。最后操作时传参给 dispatch 写接口。操作灵活多变,比 useState 好处就是能聚集所有的操作和各种状态量。着重理解这几行代码,读懂。
useReducer 算是 useState 的复杂版
使用 useReducer 分以下步骤:
创建初始值的状态initialState
创建所有对状态的操作reducer(state,action)
传给useReducer,得到读和写的接口
调用写({'type':'操作类型'})
2.2.6、useCallback()缓存回调函数
https://zh-hans.reactjs.org/reference/react/useCallback
避免传入的回调每次都是新的函数实例化而导致依赖组件重新渲染;用法:const memoizedCallback = useCallback(()=>{doSomething(a,b);},[a,b],);把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新;
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)。
英文文档
作用:提升性能,缓存事件,减少没必要的渲染
当我们使用类组件创建时,我们会怎么绑定事件呢
class App extends React.Component { constructor(props) { super(props); } render() { return <div> <p>{`hello~${name}`}</p> <button onClick={() => { console.log('click') }}>Click</button> </div> } }
这样写会导致什么结果呢,就是当渲染的时候react会认为每一次绑定的事件都是新的,从而从新进行计算
改进如下
class App extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log('click') } render() { return <div> <p>{`hello~${name}`}</p> <button onClick={this.handleClick}>Click</button> </div> } }
我们讲触发函数绑定在this上,来缓存这个方法
hooks
function App() { let [count, setCount] = useState(0); return <div> <button onClick={() => setCount(1)} ></button> </div> }
同样的问题这么写也是存在的,改进如下
function App() { let [count, setCount] = useState(0); let handleSetCount = useCallback(() => { setCount(1); }) return <div> <button onClick={handleSetCount} ></button> </div> }
我们通过useCallback来缓存这个事件达到优化效果
2.2.7、useMemo()
缓存每次传入的props,避免依赖的组件每次都重新渲染;用法:const memoizedValue = useMemo (()=>computeExpensiveValue(a,b),[a,b]);把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。
useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。
(1)、useCallback(fn, inputs)
(2)、useMemo(() => fn, inputs)
1与2等价。所以上面的示例可以使用useMemo修改。
唯一的区别是:useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你。所以在前面的例子中,可以返回 handleClick 来达到存储函数的目的。
所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。
function Parent({ a, b }) { // Only re-rendered if `a` changes: const child1 = useMemo(() => <Child1 a={a} />, [a]); // Only re-rendered if `b` changes: const child2 = useMemo(() => <Child2 b={b} />, [b]); return ( <> {child1} {child2} </> ) }
当 a/b 改变时,child1/child2 才会重新渲染。从例子可以看出来,只有在第二个参数数组的值发生变化时,才会触发子组件的更新。
Child1.jsx
export default function Child1(props){ return <div> <h2>子组件1 - {props.msg}</h2> </div> }
Child2.jsx
export default function Child2(props){ return <div> <h2>子组件2 - {props.msg}</h2> </div> }
Parent.jsx
import Child1 from './Child1'; import Child2 from './Child1'; function Parent({a,b}){ const part1=()=>{ console.log("part1渲染了"); return <Child1 msg={`a的值发生了变化,现在是:${a}`}/> } const part2=()=>{ console.log("part2渲染了"); return <Child2 msg={`b的值发生了变化,现在是:${b}`}/> } return ( <> {part1()} {part2()} </> ) } export default Parent;
ParentBox.jsx
import React,{useState,useCallback} from 'react' import Parent from './Parent' export default function ParentBox(props){ let [a,setA]=useState(0); let [b,setB]=useState(0); let handleChangeA=useCallback(e=>{ setA(e.target.value); },[]) let handleChangeB=useCallback(e=>{ setB(e.target.value); },[]) return (<div> <h1>ParentBox组件 - a={a} b={b}</h1> a:<input type="text" onChange={handleChangeA} /> b:<input type="text" onChange={handleChangeB} /> <div> <Parent a={a} b={b} /> </div> </div>) }
运行效果:
初始状态
反复修改a的值
解释:。。。
改进后的Parent.jsx
import React,{useMemo} from 'react'; import Child1 from './Child1'; import Child2 from './Child1'; function Parent({a,b}){ const part1=useMemo(()=>{ console.log("part1渲染了"); return <Child1 msg={`a的值发生了变化,现在是:${a}`}/> },[a]); const part2=useMemo(()=>{ console.log("part2渲染了"); return <Child2 msg={`b的值发生了变化,现在是:${b}`}/> },[b]); return ( <> {part1} {part2} </> ) } export default Parent;
反复修改a的值:
2.2.8、useRef() 返回一个可变的 ref 对象
获取组件的真实节点;用法:const refContainer = useRef(initialValue);
useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用,看个简单的例子:
import React, { useState, useRef } from "react"; function App() { let [name, setName] = useState("Nate"); let nameRef = useRef(); const submitButton = () => { setName(nameRef.current.value); }; return ( <div className="App"> <p>{name}</p> <div> <input ref={nameRef} type="text" /> <button type="button" onClick={submitButton}> Submit </button> </div> </div> ); }
useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问组件或真实的 DOM 节点,重点是组件也是可以访问到的,从而可以对 DOM 进行一些操作,比如监听事件等等。
三、useReducer代替Redux实现状态管理
五、使用 useReducer 代替 Redux
使用 createContext/useContext 模拟 Redux 的全局数据状态管理的作用域,useReducer 来表示全局数据状态管理中的所有读写操作。
在创建的上下文对象中能够及时更新数据,就类似于一个局部的 Redux。在以下代码中,案例
:我将模拟一个 state 有三个变量 n,m,p。一个组件更新,其他组件的值也会连带着更新。
state/npmCtx.js
// 导入 React 模块 import React from "react"; // 创建一个 counterContext 上下文对象 const counterContext = React.createContext(); // 自定义 hook,返回 counterContext 上下文对象 export function useCounterContext() { return React.useContext(counterContext); } // 将 counterContext 导出默认对象 export default counterContext;
state/counterReducer.js
// 初始化状态变量 export const initState = { n: 0, // 变量 n 的值为 0 p: 0, // 变量 p 的值为 0 m: 0, // 变量 m 的值为 0 }; // reducer 函数 export default function reducer(state, action) { switch (action.type) { case "incN": // action 的 type 为 "incN" 时 return { ...state, n: state.n + action.payload }; // 返回一个新的 state,其中变量 n 为原值加上 action.payload case "incP": // 如果 action 的 type 为 "incP" return { ...state, p: state.p + action.payload }; // 返回一个新的 state,其中变量 p 为原值加上 action.payload case "incM": // 如果 action 的 type 为 "incM" return { ...state, m: state.m + action.payload }; // 返回一个新的 state,其中变量 m 为原值加上 action.payload case "reset": // 如果 action 的 type 为 "reset" return initState; // 返回初始的 state default: throw new Error("Unknown action"); // 如果 action 的 type 不被识别,则抛出错误 } }
NCom.jsx
// 导入名为 useCounterContext 的来自文件 './state/npmCtx.js' 的上下文 import {useCounterContext} from './state/npmCtx.js' export default function NCom(){ // 使用 useCounterContext() 提供的状态和分发函数进行状态管理 const {state,dispatch}=useCounterContext(); // 返回组件 JSX return ( <div> <h2>组件 NCom</h2> <h3>n={state.n}</h3> <h3>p={state.p}</h3> <h3>m={state.m}</h3> <button onClick={()=>{ //当点击时触发 dispatch 函数,发送类型为 "incN" 带有增量为 10 dispatch({type:"incN",payload:10}) }}>n+10</button> </div> ); }
PCom.jsx
import {useCounterContext} from './state/npmCtx.js' export default function PCom(){ const {state,dispatch}=useCounterContext(); return ( <div> <h2>组件 PCom</h2> <h3>n={state.n}</h3> <h3>p={state.p}</h3> <h3>m={state.m}</h3> <button onClick={()=>{dispatch({type:"incP",payload:5})}}>p+5</button> </div> ); }
MCom.jsx
import {useCounterContext} from './state/npmCtx.js' export default function MCom(){ const {state,dispatch}=useCounterContext(); return ( <div> <h2>组件 MCom</h2> <h3>n={state.n}</h3> <h3>p={state.p}</h3> <h3>m={state.m}</h3> <button onClick={()=>{dispatch({type:"incM",payload:-3})}}>p-3</button> </div> ); }
// 引入React库 import React from 'react' // 引入计数器上下文 import counterContext from './state/npmCtx.js' // 引入计数器Reducer和初始化状态 import reducer,{initState} from './state/counterReducer.js' // 引入三个组件 import NCom from './NCom.jsx'; import PCom from './PCom.jsx'; import MCom from './MCom.jsx'; // 定义计数器父组件 export default function CounterParent(){ // 使用计数器Reducer和初始化状态,创建状态和dispatch函数 const [state,dispatch] = React.useReducer(reducer,initState); return ( // 渲染计数器父组件 <div> <h1>计数器</h1> {/* 使用计数器上下文,提供状态和dispatch函数 */} <counterContext.Provider value={{state,dispatch}}> {/* 渲染三个计数器子组件 */} <NCom/> <PCom/> <MCom/> </counterContext.Provider> </div> ); }
运行效果:
四、Redux
3.1、概要
1、redux是一个专门用于做状态管理的JS库,并非react插件库,但基本和react配合开发使用
2.其作用是集中式管理react应用中多个组件共享的状态,及负责管理状态
什么情况下需要使用redux?
1、某个组件的状态,需要让其他组件随时拿到共享
2、一个组件需要改变另一个组件的状态(或是通信)
3.2、第一个Redux程序
3.2.1、安装
安装稳定版:
npm install --save redux
以上基于使用 npm 来做包管理工具的情况下。
否则你可以直接在 unpkg 上访问这些文件,下载下来,或者把让你的包管理工具指向它。
一般情况下人们认为 Redux 就是一些 CommonJS 模块的集合。这些模块就是你在使用 Webpack、Browserify、或者 Node 环境时引入的。如果你想追求时髦并使用 Rollup,也是支持的。
你也可以不使用模块打包工具。redux
的 npm 包里 dist
目录包含了预编译好的生产环境和开发环境下的 UMD 文件。可以直接使用,而且支持大部分流行的 JavaScript 包加载器和环境。比如,你可以直接在页面上的 <script> 标签 中引入 UMD 文件,也可以
让
Bower
来安装。UMD 文件可以让你使用 window.Redux
全局变量来访问 Redux。
Redux 源文件由 ES2015 编写,但是会预编译到 CommonJS 和 UMD 规范的 ES5,所以它可以支持 任何现代浏览器。你不必非得使用 Babel 或模块打包器来使用 Redux。
附加包
多数情况下,你还需要使用 React 绑定库和开发者工具。
npm install --save react-redux
npm install --save-dev redux-devtools
需要提醒的是,和 Redux 不同,很多 Redux 生态下的包并不提供 UMD 文件,所以为了提升开发体验,我们建议使用像 Webpack 和 Browserify 这样的 CommonJS 模块打包器。
3.2.2、原理图
3.3.3、三大原则
Redux 可以用这三个基本原则来描述:
单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
这让同构应用开发变得非常容易。来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中。由于是单一的 state tree ,调试也变得非常容易。在开发中,你可以把应用的 state 保存在本地,从而加快开发速度。此外,受益于单一的 state tree ,以前难以实现的如“撤销/重做”这类功能也变得轻而易举。
console.log(store.getState())
/* 输出
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。
Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)
就是这样,现在你应该明白 Redux 是怎么回事了。
3.3.4、示例
应用程序的整个全局状态存储在单个存储中的对象树中。更改状态树的唯一方法是创建一个操作,一个描述发生了什么的对象,并将其分派到存储区。为了指定如何更新状态以响应某个操作,需要编写纯reducer函数,该函数根据旧状态和操作计算新状态。
store/counterReducer.js
export const initState = { count: 0, }; export default function reducer(state = initState, { type, data }) { switch (type) { case "increment": return { count: state.count + data }; case "decrement": return { count: state.count - data }; case "reset": return initState; default: return initState; } }
store/store.js
//import { createStore } from "redux"; import { legacy_createStore as createStore } from "redux"; import reducer, { initState } from "./counterReducer"; const store = createStore(reducer, initState); export default store;
reduxCounter.jsx
import React, { Component } from 'react' import store from './reduxState/store' export default class ReduxCounter extends Component { incrementHandle=e=>{ console.log("incrementHandle"); store.dispatch({type: 'increment',data:5}); console.log(store.getState().count); } decrementHandle=e=>{ store.dispatch({type: 'decrement',data:3}); } resetHandle=e=>{ store.dispatch({type: 'reset'}); } componentDidMount() { store.subscribe(() => { this.setState({}) }) } render() { return ( <div style={{backgroundColor:"#00ccff"}}> <h2>ReduxCounter</h2> <h3>当前Count={store.getState().count}</h3> <button onClick={this.incrementHandle}>+5</button> <button onClick={this.decrementHandle}>-3</button> <button onClick={this.resetHandle}>重置</button> </div> ) } }
运行结果:
测试多组件数据共享的情况
当前求和为奇数再加
异步加
redux中,当store数据更新后,界面数据并不会直接更新,需要手动更新
如果是函数式组件,手动更新方式如下
// redux中,当store数据更新后,界面数据并不会直接更新,需要手动更新
// 初始化一个update数据使用useState(),主要是为了通过setUpdate()来更新组件
// 模拟render()生命周期,实现组件重新加载,以更新界面的store数据
// 大坑:redux中,当store数据更新后,界面数据并不会直接更新,需要手动更新 // 初始化一个update数据使用useState(),主要是为了通过setUpdate()来更新组件 // 模拟render()生命周期,实现组件重新加载,以更新界面的store数据 const [update,setUpdate] = useState({}) // useEffect模拟componentDidMount()生命周期 useEffect(() => { // store.subscribe()是redux提供的,监测store更新的函数 store.subscribe(() => { // 当store数据更新后执行 setUpdate() ,组件重新加载,实现界面store数据更新 setUpdate({}) }) })
类式组件,使用生命周期直接在store.subscribe()中调用setState()即可实现更新数据
componentDidMount() { store.subscribe(() => { this.setState({}) }) }
1、store.js:
(1)引入redux中的legacy_createStore(备注:createStore已废弃),创建一个store
(2)legacy_createStore调用时要传入一个为其服务的reducer
(3)暴露store对象
2、reducer.js:
(1)该文件用于创建一个为Count组件服务的reducer、reducer本质就是一个函数,接收preState,action、返回加工后的状态
(2)reducer有2个作用:初始化函数、加工状态
(3)reducer被第一次调用时,是store触发的,
传递的preState是undefined,
传递的action是类似如下格式数据:
{type:
“@@redux/INITr.y.u.u.5.o”}
3、action.js:该文件专门为所服务的组件生成action对象
4、constant.js:统一管理action中相关的常量,同时也避免写错常量字段
5、备注:可在index.js入口文件中监测store中状态的改变,一旦发生改变重新渲染
五、Mobx
React的数据管理,除了redux之外,一个新的状态管理方案mobx
传统React使用的数据管理库为Redux。Redux要解决的问题是统一数据流,数据流完全可控并可追踪。要实现该目标,便需要进行相关的约束。Redux由此引出了dispatch action reducer等概念,对state的概念进行强约束。然而对于一些项目来说,太过强,便失去了灵活性。Mobx便是来填补此空缺的。
六、ref基础知识
不管在Vue中还是React,如果我们想使用一个元素的DOM,不需要通过JS中操纵DOM的方法,它们提供了一个专属的API就是ref。
而Vue中的ref可能比较简单,这一篇主要讲一下如何在React中使用ref,以及使用ref的场景。
5.1、ref的挂载
在React中,ref可以挂载到html元素上,同时也可以挂载在React元素上,看下面的代码:
import React, { Component } from 'react' // import { findDOMNode } from 'react-dom' import Child from './Child' export default class Father extends Component { componentDidMount(){ console.log(this.refs.refElement); console.log(this.refs.child); } render() { return ( <div> <input ref={ 'refElement' }></input> <Child ref={ 'child' }/> <button onClick={this.fn}>123</button> </div> ) } }
控制台的打印为:
可以看到,在React中,ref是可以挂载到HTML元素和React元素上的。
(1)挂载HTML元素,返回真实的DOM
(2)挂载React元素,返回render后的实例对象
同时React也提供了一个方法findDOMNode可以将React元素的ref返回变成真实的DOM元素。
import { findDOMNode } from 'react-dom' console.log(findDOMNode(this.refs.child));
同时在上面的代码我们也可以看出来,ref的挂载是在componentDidMount等生命周期之前执行的。
5.2、使用ref的三种方式
5.2.1、字符串的方式
import React, { Component } from 'react' export default class Father extends Component { componentDidMount(){ console.log(this.refs.refElement); } render() { return ( <div> <input ref={ 'refElement' }></input> <button onClick={this.fn}>123</button> </div> ) } }
这种方式和Vue的ref比较相似,但是官方目前已经不推荐使用该方式,后续可能还会废弃。
5.2.2、函数的方式
import React, { Component } from 'react' export default class Father extends Component { componentDidMount(){ console.log(this.refElement); } render() { return ( <div> <input ref={ ref => this.refElement = ref }></input> <button onClick={this.fn}>获取元素</button> </div> ) } }
5.2.3、react.CreateRef的方式
import React, { Component } from 'react' export default class Father extends Component { refElement = React.createRef(); componentDidMount(){ console.log(this.refElement.current); } render() { return ( <div> <input ref={this.refElement}></input> <button onClick={this.fn}>123</button> </div> ) } }
记住这里面通过refElement中的current,获取真实的DOM元素。
七、打包一个项目
八、作业
8.1、使用多种方法实现页面加载完成时让搜索文本框获取焦点,侧重练习ref的使用。
8.2、完成所有的上课示例。