“React学习之旅:从入门到精通的点滴感悟“

在探索和学习React的过程中,我逐渐领悟到了前端开发的魅力与挑战。React,作为Facebook推出的开源JavaScript库,以其独特的思维方式和强大的功能,引领着前端开发的潮流。在这篇文章中,我将分享我的React学习心得,总结笔记,以期帮助那些同样在前端领域摸索的开发者们。

1.Hello React 案例

<div id="root"></div>;// 类组件和函数式组件
class App extends React.Component {// 组件数据constructor() {super();this.state = {message: "Hello World",};// 对需要绑定的方法, 提前绑定好this 或者方法写成箭头函数上下文指向React组件this.btnClick = this.btnClick.bind(this);}// 组件方法(实例方法)btnClick() {// 1.将state中message值修改掉 2.自动重新执行render函数函数this.setState({message: "Hello React",});}// 渲染内容 render方法render() {return (<div><h2>{this.state.message}</h2><button onClick={this.btnClick}>修改文本</button></div>);}
}// 将组件渲染到界面上
const root = ReactDOM.createRoot(document.querySelector("#root"));
// App根组件
root.render(<App />);

如果上下文中没有定义this,那么this默认会指向全局对象,在浏览器中这个全局对象通常是window。在 React 的类组件中,果没有显示绑定方法的this,那么调用方法时this指向会失去组件实例的上下文,进而变成undefined,从而导致运行时错误。

为了避免这种问题,可以使用.bind()或箭头函数显式绑定方法的this,或者在构造函数中使用箭头函数定义方法,从而确保方法的this指向是组件实例。

2. 列表渲染

....// 封装App组件class App extends React.Component {constructor() {super()this.state = {data: ["Vue", "React", "Angular"]}}render() {// 1.对data进行for循环// const liEls = []// for (let i = 0; i < this.state.data.length; i++) {//   const item = this.state.movies[i]//   const liEl = <li>{item}</li>//   liEls.push(liEl)// }// 2.data数组 => liEls数组// const liEls = this.state.data.map(movie => <li>{movie}</li>)return (<div><h2>电影列表</h2><ul>{this.state.movies.map(movie => <li>{movie}</li>)}</ul></div>)}}
......

3.JSX 语法规则

3.1 插入内容

  • jsx 中的注释
  • JSX 嵌入变量作为子元素
    • 情况一:当变量是 Number、String、Array 类型时,可以直接显示
    • 情况二:当变量是 null、undefined、Boolean 类型时,内容为空
      • 如果希望可以显示 null、undefined、Boolean,那么需要转成字符串;
      • 转换的方式有很多,比如 toString 方法、和空字符串拼接,String(变量)等方式;
    • 情况三:Object 对象类型不能作为子元素(not valid as a React child)
  • JSX 嵌入表达式
  • 运算表达式
  • 三元运算符
  • 行一个函数**
// jsx语法规则
class App extends React.Component {constructor() {super();this.state = {message: "Hello World",count: 1000,names: ["紫陌", "张三", "王五"],aaa: undefined,bbb: null,ccc: true,friend: { name: "zimo" },firstName: "zimo",lastName: "紫陌",age: 20,list: ["aaa", "bbb", "ccc"],};}render() {// 解构拿到值const { message, names, count, aaa, bbb, ccc, friend, firstName, lastName, age } = this.state;// 对内容进行运算后显示(插入表示)const ageText = age >= 18 ? "成年人" : "未成年人";const liEls = this.state.list.map((item) => <li>{item}</li>);return (<div>{/* 1.Number / String / Array直接显示出来*/}<h2>{message}</h2><h2>{count}</h2><h2>{names}</h2>{/* 2.undefined/null/Boolean  默认不展示,三种方法都可以显示出来*/}<h2>{String(aaa)}</h2><h2>{bbb + ""}</h2><h2>{ccc.toString()}</h2>{/* 3.Object类型不能作为子元素展示 */}<h2>{friend.name}</h2> {/* zimo */}<h2>{Object.keys(friend)[0]}</h2> {/* name */}{/* 4.可以插入对应表达式 */}<h2>{10 + 20}</h2><h2>{firstName + "" + lastName}</h2>{/* 5.插入三元表达式 */}<h2>{ageText}</h2><h2>{age >= 18 ? "成年人" : "未成年人"}</h2>{/* 6.可以调用方法获取结果 */}<ul>{liEls}</ul><ul>{this.state.list.map((item) => (<li>{item}</li>))}</ul><ul>{this.getList()}</ul></div>);}getList() {const liEls = this.state.list.map((item) => <li>{item}</li>);return liEls;}
}

3.2 绑定属性

// 1.定义App根组件
class App extends React.Component {constructor() {super();this.state = {title: "紫陌",imgURL: "https:*****",href: "https://*****",isActive: true,objStyle: { color: "red", fontSize: "30px" },};}render() {const { title, imgURL, href, isActive, objStyle } = this.state;// 需求: isActive: true -> active// 1.class绑定的写法一: 字符串的拼接const className = `abc cba ${isActive ? "active" : ""}`;// 2.class绑定的写法二: 将所有的class放到数组中const classList = ["abc", "cba"];if (isActive) classList.push("active");// 3.class绑定的写法三: 第三方库classnames -> npm install classnamesreturn (<div>{/* 1.基本属性绑定 */}<h2 title={title}>我是h2元素</h2><img src={imgURL} alt="" /><a href={href}>百度一下</a>{/* 2.绑定class属性: 最好使用className */}<h2 className={className}>哈哈哈哈</h2><h2 className={classList.join(" ")}>哈哈哈哈</h2>{/* 3.绑定style属性: 绑定对象类型 */}<h2 style={{ color: "red", fontSize: "30px" }}>紫陌YYDS</h2><h2 style={objStyle}>紫陌</h2></div>);}
}

jsx 语法规则:

1.定义虚拟 DOM 时,不要写引号。

2.标签中混入 JS 表达式时要用{}。

3.样式的类名指定不要用 class,要用 className。

5.只有一个根标签

6.标签必须闭合

7.标签首字母

(1).若小写字母开头,则将该标签转为 html 中同名元素,若 html 中无该标签对应的同名元素,则报错。

(2).若大写字母开头,react 就去渲染对应的组件,若组件没有定义,则报错。

3.3 事件绑定

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写;
  • 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行;
class App extends React.Component {constructor() {super();this.state = {count: 10,};this.btn1Click = this.btn1Click.bind(this);}btn1Click() {console.log("btn1", this);this.setState({ count: this.state.count + 1 });}btn2Click = () => {console.log("btn2", this);this.setState({ count: this.state.count + 1 });};btn2Click = () => {console.log("btn2", this);this.setState({ count: this.state.count + 1 });};btn3Click() {console.log("btn2", this);this.setState({ count: this.state.count + 1 });}render() {const { count } = this.state;return (<div><h2>当前计数:{count}</h2>{/* 1.this绑定方式一:bind绑定 */}<button onClick={this.btn1Click}>按钮1</button>{/* 2.this.绑定方式二:ES6 class fields */}<button onClick={this.btn2Click}>按钮2</button>{/* 3.this绑定方式三:直接传入一个箭头函数(重要) */}<button onClick={() => console.log("btn3Click")}>按钮3</button><button onClick={() => this.btn3Click()}>按钮4</button></div>);}
}

3.4 事件参数传递

  • 在执行事件函数时,有可能我们需要获取一些参数信息:比如 event 对象、其他参数
  • 情况一:获取 event 对象
    • 很多时候我们需要拿到 event 对象来做一些事情(比如阻止默认行为)
    • 那么默认情况下,event 对象有被直接传入,函数就可以获取到 event 对象;
  • 情况二:获取更多参数
    • 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数;
class App extends React.Component {constructor() {super();this.state = {message: "Hello World",};}btnClick(event, name, age) {console.log("btnClick", event, this);console.log("name,age", name, age);}render() {return (<div>{/* 1.event参数传递 */}<button onClick={this.btnClick.bind(this)}>按钮1</button><buttononClick={(event) => {this.btnClick(event);}}>按钮2</button>{/* 额外参数传递 */}<button onClick={this.btnClick.bind(this, "zimo", 18)}>按钮3(不推荐)</button><button onClick={(event) => this.btnClick(event, "zimo", 18)}>按钮4</button></div>);}
}

3.5 条件渲染

// 1.定义App根组件
class App extends React.Component {constructor() {super();this.state = {isShow: false,flag: undefined,};}render() {const { isShow, flag } = this.state;// 1.条件判断方式一: 使用if进行条件判断let showElement = null;if (isShow) {showElement = <h2>显示紫陌</h2>;} else {showElement = <h1>隐藏紫陌</h1>;}return (<div>{/* 1.方式一: 根据条件给变量赋值不同的内容 */}<div>{showElement}</div>{/* 2.方式二: 三元运算符 */}<div>{isShow ? <h2>显示</h2> : <h2>隐藏</h2>}</div>{/* 3.方式三: &&逻辑与运算 */}{/* 场景: 当某一个值, 有可能为undefined时, 使用&&进行条件判断 */}<div> {flag && <h2>{flag}</h2>} </div></div>);}
}

3.6 条件列表渲染

  • 在 React 中,展示列表最多的方式就是使用数组的map 高阶函数
  • 很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理 比如过滤掉一些内容:filter 函数
  • 比如截取数组中的一部分内容:slice 函数
class App extends React.Component {constructor() {super();this.state = {students: [{ id: 111, name: "zimo", score: 199 },{ id: 112, name: "aniy", score: 98 },{ id: 113, name: "james", score: 199 },{ id: 114, name: "curry", score: 188 },],};}render() {const { students } = this.state;/* 第一种分别写 *///分数大于100学生进行展示const filterStudent = students.filter((item) => {return item.score > 100;});//分数大于100前两名信息const sliceStudents = filterStudent.slice(0, 2);return (<div><h2>列表数据</h2><div>{/* 第二种方式链式调用 */}{students.filter((item) => item.score > 100).slice(0, 2).map((item) => {return (<div key={item.id}><h2>学号: {item.id}</h2><h3>姓名: {item.name}</h3><h4>分数:{item.score}</h4></div>);})}</div></div>);}
}

3.7 JSX 的本质

  • JSX 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。 所有的 jsx 最终都会被转换成 React.createElement 的函数调用。

  • createElement 需要传递三个参数:

  • 参数一:type

    • 当前 ReactElement 的类型;
    • 如果是标签元素,那么就使用字符串表示 “div”;
    • 如果是组件元素,那么就直接使用组件的名称;
  • 参数二:config
    ./images

  • 所有 jsx 中的属性都在 config 中以对象的属性和值的形式存储

  • 比如传入 className 作为元素的 class;

  • 参数三:children

    • 存放在标签中的内容,以 children 数组的方式进行存储;
  1. 源码分析

在这里插入图片描述

  1. babel 转换
  • JSX 是通过 babel 帮我们进行语法转换的,写的 jsx 代码都需要依赖 babel。
  • 可以在 babel 的官网中快速查看转换的过程:https://babeljs.io/repl/#?presets=react

在这里插入图片描述

babel 转换过来的代码可以直接运行。界面依然是可以正常的渲染

3.组件化开发

3.1 render 函数的返回值

  • render 被调用时,它会检查 this.props this.state 的变化并返回以下类型之一:
  • React 元素
    • 通常通过 JSX 创建。
    • 例如,
      会被 React 渲染为 DOM 节点, <MyComponent /> 会被 React 渲染为自定义组件;
    • 无论是
      还是 <MyComponent /> 均为 React 元素。
  • 数组或 fragments:使得 render 方法可以返回多个元素。
  • Portals:可以渲染子节点到不同的 DOM 子树中。
  • 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
  • 布尔类型或 null:什么都不渲染。

3.2 函数组件

  • 函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容。
  • 函数组件有自己的特点(hooks 就不一样了):
    • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
    • this 关键字不能指向组件实例(因为没有组件实例);
    • 没有内部状态(state);
// 函数式组件
function App(props) {// 返回值: 和类组件中render函数返回的是一致return <h1>Hello zimo</h1>;
}export default App;

4.React 生命周期

4.1 常用生命周期函数图

在这里插入图片描述

  1. 全部生命周期函数

在这里插入图片描述

4.2 生命周期函数例子

父组件:APP 组件

import React, { Component } from "react";
import Hello from "./Hello";class App extends Component {constructor() {super();this.state = {isShow: true,};}isShowFun() {this.setState({isShow: !this.state.isShow,});}render() {const { isShow } = this.state;return (<div>我是父组件{isShow && <Hello />}<button onClick={() => this.isShowFun()}>显示隐藏子组件</button></div>);}
}export default App;

子组件:Hello

import React, { Component } from "react";class Hello extends Component {constructor() {// 1.构造方法: constructorconsole.log("1. Hello constructor");super();this.state = {message: "你好紫陌",};}updateText() {this.setState({message: "你好啊,zimo~",});}// 2.执行render函数render() {console.log("2. Hello render");const { message } = this.state;return (<div><h2>{message}</h2><p>{message}这是子组件</p><button onClick={() => this.updateText()}>修改文本</button></div>);}// 3.组件被渲染到DOM:被挂载到DOMcomponentDidMount() {console.log("Hello组件挂载完成");}// 4. 组件的DOM被更新完成:DOM发生更新componentDidUpdate() {console.log("Hello组件更新完成");}// 5.组件从DOM中卸载掉:从DOM移除掉componentWillUnmount() {console.log("Hello组件卸载完成");}// 不常用的生命周期函数shouldComponentUpdate() {return true; //false 不会更新数据}// 等等
}export default Hello;

运行效果

在这里插入图片描述

4.3 不常用生命周期函数

  • pgetSnapshotBeforeUpdate:在 React 更新 DOM 之前回调的一个函数,可以获取 DOM 更新前的一些信息(比如说滚动位置);
  • pgetDerivedStateFromProps:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新 state;
  • 等等

4.4 总结:新旧版生命周期函数

旧版生命周期函数:

  1. 初始化阶段: 由 ReactDOM.render()触发—初次渲染 1. constructor() 2. componentWillMount() 3. render() 4. componentDidMount() =====> 常用
    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
  2. 更新阶段: 由组件内部 this.setSate()或父组件 render 触发 1. shouldComponentUpdate() 2. componentWillUpdate() 3. render() =====> 必须使用的一个 4. componentDidUpdate()
  3. 卸载组件: 由 ReactDOM.unmountComponentAtNode()触发 1. componentWillUnmount() =====> 常用
    一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

新版生命周期函数:

  1. 初始化阶段: 由 ReactDOM.render()触发—初次渲染 1. constructor() 2. getDerivedStateFromProps 3. render() 4. componentDidMount() =====> 常用
    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
  2. 更新阶段: 由组件内部 this.setSate()或父组件重新 render 触发 1. getDerivedStateFromProps 2. shouldComponentUpdate() 3. render() 4. getSnapshotBeforeUpdate 5. componentDidUpdate()
  3. 卸载组件: 由 ReactDOM.unmountComponentAtNode()触发 1. componentWillUnmount() =====> 常用
    一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

5.组件通信

5.1 父传子(props)

  • 父组件通过 属性**=值** 的形式来传递给子组件数据;
  • 子组件通过 props 参数获取父组件传递过来的数据;

父组件:Main:

export class Main extends Component {constructor() {super();this.state = {banners: [],productList: [],};}componentDidMount() {axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {const banners = res.data.data.banner.list;const recommend = res.data.data.recommend.list;this.setState({banners,productList: recommend,});});}render() {const { banners, productList } = this.state;return (<div className="main"><div>Main</div><MainBanner banners={banners} title="紫陌" /><MainBanner /><MainProductList productList={productList} /></div>);}
}

子组件:MainBanner

import React, { Component } from "react";
import PropTypes from "prop-types";export class MainBanner extends Component {/* es2022的语法等同于下面 MainBanner.defaultProps */// static defaultProps = {//   banners:[],//   title:"默认标题"// }render() {// console.log(this.props)const { title, banners } = this.props;return (<div className="banner"><h2>封装一个轮播图: {title}</h2><ul>{banners?.map((item) => {return <li key={item.acm}>{item.title}</li>;})}</ul></div>);}
}//MainBanner传入props类型进行验证
MainBanner.propTypes = {banners: PropTypes.array,title: PropTypes.string,
};//MainBanner传入的props的默认值
MainBanner.defaultProps = {banners: [],title: "默认标题",
};export default MainBanner;

props 类型验证

  • 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
  • 更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
  • 比如验证数组,并且数组中包含哪些元素;
  • 比如验证对象,并且对象中包含哪些 key 以及 value 是什么类型;
  • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
  • 如果没有传递,我们希望有默认值呢?
    • 我们使用 defaultProps 就可以了

禹神案例:

1. props限制案例:
----------------------------------------------------------------------------------------------------------//创建组件class Person extends React.Component{render(){// console.log(this);const {name,age,sex} = this.props//props是只读的//this.props.name = 'jack' //此行代码会报错,因为props是只读的return (<ul><li>姓名:{name}</li><li>性别:{sex}</li><li>年龄:{age+1}</li></ul>)}}//对标签属性进行类型、必要性的限制Person.propTypes = {name:PropTypes.string.isRequired, //限制name必传,且为字符串sex:PropTypes.string,//限制sex为字符串age:PropTypes.number,//限制age为数值speak:PropTypes.func,//限制speak为函数}//指定默认标签属性值Person.defaultProps = {sex:'男',//sex默认值为男age:18 //age默认值为18}.............function speak(){console.log('我说话了');}2.props的简写方式也就是利用es2022语法
---------------------------------------------------------------------------------------------------//创建组件class Person extends React.Component{constructor(props){//构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props// console.log(props);super(props)console.log('constructor',this.props);}//对标签属性进行类型、必要性的限制static propTypes = {name:PropTypes.string.isRequired, //限制name必传,且为字符串sex:PropTypes.string,//限制sex为字符串age:PropTypes.number,//限制age为数值}//指定默认标签属性值static defaultProps = {sex:'男',//sex默认值为男age:18 //age默认值为18}render(){// console.log(this);const {name,age,sex} = this.props//props是只读的//this.props.name = 'jack' //此行代码会报错,因为props是只读的return (<ul><li>姓名:{name}</li><li>性别:{sex}</li><li>年龄:{age+1}</li></ul>)}}-----------------------------------------------------------------------------------------------------3. 函数式组件传递props//创建组件function Person (props){const {name,age,sex} = propsreturn (<ul><li>姓名:{name}</li><li>性别:{sex}</li><li>年龄:{age}</li></ul>)}Person.propTypes = {name:PropTypes.string.isRequired, //限制name必传,且为字符串sex:PropTypes.string,//限制sex为字符串age:PropTypes.number,//限制age为数值}//指定默认标签属性值Person.defaultProps = {sex:'男',//sex默认值为男age:18 //age默认值为18}//渲染组件到页面ReactDOM.render(<Person name="jerry"/>,document.getElementById('test1'))

5.2 子传父

  • 在 vue 中是通过自定义事件来完成的;
  • 在 React 中是通过 props 传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;

案例:

父组件:APP

export class App extends Component {constructor() {super();this.state = {counter: 100,};}//调用修改变量changeCounter(count) {this.setState({counter: this.state.counter + count,});}render() {const { counter } = this.state;return (<div><h2>当前计数为:{counter}</h2><AddCounter addClick={(count) => this.changeCounter(count);}/></div>);}
}

子组件:AddCounter

export class AddCounter extends Component {addCount(count) {//拿到父组件定义的函数传变量this.props.addClick(count);}render() {return (<div><buttononClick={(e) => {this.addCount(10);}}>点我加10</button></div>);}
}

图解:

在这里插入图片描述

6.React 中的插槽

  • Vue 中有一个固定的做法是通过 slot 来完成插槽效果
  • React 对于这种需要插槽的情况非常灵活,有两种方案可以实现:
    • 组件的 children 子元素;
    • props 属性传递 React 元素;
  1. 第一种方式:每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。

    children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

父组件:APP

export class App extends Component {render() {return (<div>{/* 1.使用children实现插槽 */}<NavBar><button>按钮</button><h2>zimo</h2><span>紫陌</span></NavBar></div>);}
}

子组件:NavBar

export class NavBar extends Component {render() {const { children } = this.props;return (<div className="nav-bar"><div className="left">{children[0]}</div><div className="center">{children[1]}</div><div className="right">{children[2]}</div></div>);}
}

2.第二种方式:另外一个种方案就是使用 props 实现:通过具体的属性名,可以让我们在传入和获取时更加的精准;

父组件:APP

export class App extends Component {render() {const spanEl = <span>紫陌</span>;return (<div>{/* 2.使用props实现插槽 */}<NavBar leftSlot={<button>按钮2</button>} centerSlot={<h2>zimo</h2>} rightSlot={spanEl} /></div>);}
}

子组件:NavBar

export class NavBar extends Component {render() {const { leftSlot, centerSlot, rightSlot } = this.props;return (<div className="nav-bar"><div className="left">{leftSlot}</div><div className="center">{centerSlot}</div><div className="right">{rightSlot}</div></div>);}
}

react 作用域插槽效果实现

tabs 案例演示:

在这里插入图片描述

7. 非父子通信(context)

  • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;

7.1context 相关 API

  1. React.createContext

    • 创建一个需要共享的 Context 对象:

    • 如果一个组件订阅了 Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的 context 值;

    • defaultValue 是组件在顶层查找过程中没有找到对应的 Provider,那么就使用默认值

    • // 1.创建一个Context
      const userContext = React.createContext(defaultValue);
      
  2. Context.Provider

    • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:

    • Provider 接收一个 value 属性,传递给消费组件;

    • 一个 Provider 可以和多个消费组件有对应关系;

    • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;

    • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;

    • <MyContext.Provider value={/* 某个值*/}
      
  3. Class.contextType

    • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:

    • 这能让你使用 this.context 来消费最近 Context 上的那个值;

    • 你可以在任何生命周期中访问到它,包括 render 函数中;

    • //设置组件的contextType为某一个Context
      //class.contextType = ThemeContext
      HomeInfo.contextType = ThemeContext;
      
    1. Context.Consumer
    • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。

    • 这里需要 函数作为子元素(function as child)这种做法;

    • 这个函数接收当前的 context 值,返回一个 React 节点;

    • <UserContext.Consumer>{(value) => {return <h2>Info User: {value.nickname}</h2>;}}
      </UserContext.Consumer>
      

什么时候使用Context.Consumer 呢?

  1. 当使用 value 的组件是一个函数式组件时;

  2. 当组件中需要使用多个 Context 时;

案例一:

APP 父组件:

theme-context.jsx
--------------------------------------------------------------------------------------------------------
import React from "react"// 1.创建一个Context
const ThemeContext = React.createContext({ color: "blue", size: 10 })export default ThemeContextuser-context.jsx
---------------------------------------------------------------------------------------------------------
import React from "react"// 1.创建一个Context
const UserContext = React.createContext()export default UserContextAPP.jsx
----------------------------------------------------------------------------------------------------------import React, { Component } from 'react'
import Home from './Home'import ThemeContext from "./context/theme-context"
import UserContext from './context/user-context'
import Profile from './Profile'export class App extends Component {render() {return (<div><h2>App</h2>{/*  通过ThemeContext中Provider中value属性为后代提供数据 */}<UserContext.Provider value={{nickname: "kobe", age: 30}}><ThemeContext.Provider value={{color: "red", size: "30"}}><A/></ThemeContext.Provider></UserContext.Provider><Profile/></div>)}
}

A 组件:

export class Home extends Component {render() {return (<div><B /><C /></div>);}
}

B 组件:类组件

import ThemeContext from "./context/theme-context";
import UserContext from "./context/user-context";export class B extends Component {render() {// 4.第四步操作: 获取数据, 并且使用数据console.log(this.context);return (<div>{/*获取最近的context  */}<h2>HomeInfo: {this.context.color}</h2>{/*获取指定的context  */}<UserContext.Consumer>{(value) => {return <h2>Info User: {value.nickname}</h2>;}}</UserContext.Consumer></div>);}
}// 3.第三步操作: 设置组件的contextType为某一个Context
HomeInfo.contextType = ThemeContext;

C 组件:函数组件

import ThemeContext from "./context/theme-context";function C() {return (<div>{/* 函数式组件中使用Context共享的数据 */}<ThemeContext.Consumer>{(value) => {return <h2> Banner theme:{value.color}</h2>;}}</ThemeContext.Consumer></div>);
}export default HomeBanner;

案例二:

import React, { Component } from "react";//创建Context对象
const MyContext = React.createContext();
const { Provider, Consumer } = MyContext;
export default class A extends Component {state = { username: "tom", age: 18 };render() {const { username, age } = this.state;return (<div className="parent"><h3>我是A组件</h3><h4>我的用户名是:{username}</h4><Provider value={{ username, age }}><B /></Provider></div>);}
}class B extends Component {render() {return (<div className="child"><h3>我是B组件</h3><C /></div>);}
}class C extends Component {//声明接收contextstatic contextType = MyContext;render() {const { username, age } = this.context;return (<div className="grand"><h3>我是C组件</h3><h4>我从A组件接收到的用户名:{username},年龄是{age}</h4></div>);}
}function C() {return (<div className="grand"><h3>我是C组件</h3><h4>我从A组件接收到的用户名:<Consumer>{(value) => `${value.username},年龄是${value.age}`}</Consumer></h4></div>);
}
在应用开发中一般不用context, 一般都它的封装react插件;

8.setState 地使用

  • 开发中我们并不能直接通过修改 state 的值来让界面发生更新:
  • 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改 React 并不知道数据发生了变化;
  • React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的 Proxy 的方式来监听数据的变化;
  • 我们必须通过 setState 来告知 React 数据已经发生了变化;

8.1 setState 三种用法

案例一:

export class App extends Component {constructor(props) {super(props);this.state = {message: "Hello Zimo",};}changeText() {// 1.基本使用this.setState({message: "你好啊, 紫陌",});// 2.setState可以传入一个回调函数// 好处一: 可以在回调函数中编写新的state的逻辑// 好处二: 当前的回调函数会将之前的state和props传递进来this.setState((state, props) => {// 1.编写一些对新的state处理逻辑// 2.可以获取之前的state和props值console.log(this.state.message, this.props);return {message: "你好啊, 紫陌",};});// 3.setState在React的事件处理中是一个异步调用// 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码// 那么可以在setState中传入第二个参数: callbackthis.setState({ message: "你好啊, 紫陌" }, () => {console.log("++++++:", this.state.message); //后打印,setState异步});console.log("------:", this.state.message); //先打印}render() {const { message } = this.state;return (<div><h2>message: {message}</h2><button onClick={(e) => this.changeText()}>修改文本</button></div>);}
}

案例二:宇哥

import React, { Component } from "react";export default class Demo extends Component {state = { count: 0 };add = () => {//对象式的setState//1.获取原来的count值const { count } = this.state;//2.更新状态this.setState({ count: count + 1 }, () => {console.log(this.state.count);});//console.log('12行的输出',this.state.count); //0//函数式的setStatethis.setState((state) => ({ count: state.count + 1 }));};render() {return (<div><h1>当前求和为:{this.state.count}</h1><button onClick={this.add}>点我+1</button></div>);}
}

8.2 为什么 setState 设计为什么异步?

  • setState 设计为异步,可以显著的提升性能
    • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步;
    • state 和 props 不能保持一致性,会在开发中产生很多的问题;

案例:

import React, { Component } from "react";function Hello(props) {return <h2>{props.message}</h2>;
}export class App extends Component {constructor(props) {super(props);this.state = {message: "Hello Zimo",counter: 0,};}changeText() {this.setState({ message: "你好啊,紫陌" });console.log(this.state.message);}increment() {console.log("------");//以下三个最终是counter:1// this.setState({//   counter: this.state.counter + 1// })// this.setState({//   counter: this.state.counter + 1// })// this.setState({//   counter: this.state.counter + 1// })//以下结果是counter:3// this.setState((state) => {//   return {//     counter: state.counter + 1//   }// })// this.setState((state) => {//   return {//     counter: state.counter + 1//   }// })// this.setState((state) => {//   return {//     counter: state.counter + 1//   }// })}render() {const { message, counter } = this.state;console.log("render被执行");return (<div><h2>message: {message}</h2><button onClick={(e) => this.changeText()}>修改文本</button><h2>当前计数: {counter}</h2><button onClick={(e) => this.increment()}>counter+1</button><Hello message={message} /></div>);}
}

8.3setState 一定是异步吗

  1. React18 之前
    • 分成两种情况:
    • 在组件生命周期或 React 合成事件中,setState 是异步;
    • 在 setTimeout 或者原生 dom 事件中,setState 是同步;
    • 在这里插入图片描述

  1. React18 之后

setState 默认是异步的

在*React18 之后,默认所有的操作都被放到了批处理中(异步处理)

如果希望代码可以同步会拿到,则需要执行特殊的 flushSync 操作

import { flushSync } from 'react-dom'
changeText() {setTimeout(() => {// 在react18之前, setTimeout中setState操作, 是同步操作// 在react18之后, setTimeout中setState异步操作(批处理)flushSync(() => {this.setState({ message: "你好啊, 李银河" })})console.log(this.state.message)}, 0);
}

9. render 函数优化(PureComponent,memo)

  • 修改了 App 中的数据,所有的组件都需要重新 render,进行 diff 算法, 性能必然是很低的,很多的组件没有必须要重新 render。它们调用 render 应该有一个前提,就是依赖的数据(state、 props)发生改变时,再调用自己的 render 方法;
  • 如何来控制 render 方法是否被调用呢?
  • 通过 shouldComponentUpdate 方法即可;

9.1shouldComponentUpdate

  • React 给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为 SCU),这个方法接受参数,并且需要有 返回值:

  • 该方法有两个参数:

  • 参数一:nextProps 修改之后,最新的 props 属性

  • 参数二:nextState 修改之后,最新的 state 属性

  • 该方法返回值是一个 boolean 类型:

  • 返回值为 true,那么就需要调用 render 方法;

  • 返回值为 false,那么久不需要调用 render 方法

  • 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;

9.2PureComponent

  • 如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
  • 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?
  • props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false;
  • 事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了
  •  将 class 继承自 PureComponent。

9.3 高阶组件 memo

  • 函数式组件我们在 props 没有改变时,也是不希望其重新渲染其 DOM 树结构的
  • 我们需要使用一个高阶组件 memo:

案例

APP组件------------------------------------------------------------
import React, { PureComponent } from 'react'
import Home from './Home'
import Recommend from './Recommend'
import Profile from './Profile'export class App extends PureComponent {constructor() {super()this.state = {message: "Hello World",counter: 0}}// shouldComponentUpdate(nextProps, newState) {//   // App进行性能优化的点//   if (this.state.message !== newState.message || this.state.counter !== newState.counter) {//     return true//   }//   return false// }changeText() {this.setState({ message: "你好啊,紫陌!" })// this.setState({ message: "Hello World" })}increment() {this.setState({ counter: this.state.counter + 1 })}render() {console.log("App render")const { message, counter } = this.statereturn (<div><h2>App-{message}-{counter}</h2><button onClick={e => this.changeText()}>修改文本</button><button onClick={e => this.increment()}>counter+1</button><Home message={message}/><Recommend counter={counter}/><Profile message={message}/></div>)}
}export default AppHome组件----------------------------------------------------------------------------------------
import React, { PureComponent } from 'react'export class Home extends PureComponent {constructor(props) {super(props)this.state = {friends: []}}// shouldComponentUpdate(newProps, nextState) {//   // 自己对比state是否发生改变: this.state和nextState//   if (this.props.message !== newProps.message) {//     return true//   }//   return false// }render() {console.log("Home render")return (<div><h2>Home Page: {this.props.message}</h2></div>)}
}export default HomeProfile组件----------------------------------------------------------------------------------------------------
import { memo } from "react"const Profile = memo(function(props) {console.log("profile render")return <h2>Profile: {props.message}</h2>
})export default Profile

10.数据不可变(state)

修改 state 数据必须把 state 数据拷贝出来替换掉原来的数据,PureComponent 才可以调用 render 函数。

性能优化 – React (reactjs.org) 数据不可变

import React, { PureComponent } from "react";export class App extends PureComponent {constructor() {super();this.state = {books: [{ name: "你不知道JS", price: 99, count: 1 },{ name: "JS高级程序设计", price: 88, count: 1 },{ name: "React高级设计", price: 78, count: 2 },{ name: "Vue高级设计", price: 95, count: 3 },],friend: {name: "kobe",},message: "Hello World",};}addNewBook() {const newBook = { name: "Angular高级设计", price: 88, count: 1 };// 1.直接修改原有的state, 重新设置一遍// 在PureComponent是不能重新渲染(render)/*    this.state.books.push(newBook)this.setState({ books: this.state.books }) */// 2.赋值一份books, 在新的books中修改, 设置新的books保证不是同一份books才会刷新renderconst books = [...this.state.books];books.push(newBook);this.setState({ books: books });}addBookCount(index) {// this.state.books[index].count++   这个也不能重新渲染renderconst books = [...this.state.books];books[index].count++;this.setState({ books: books });}render() {const { books } = this.state;return (<div><h2>数据列表</h2><ul>{books.map((item, index) => {return (<li key={index}><span>name:{item.name}-price:{item.price}-count:{item.count}</span><button onClick={(e) => this.addBookCount(index)}>+1</button></li>);})}</ul><button onClick={(e) => this.addNewBook()}>添加新书籍</button></div>);}
}export default App;

11.ref 获取 DOM

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例;可以通过 React.forwardRef

11.1ref 获取 DOM

  • 方式一:传入字符串
    • 使用时通过 this.refs.传入的字符串格式获取对应的元素;
  • 方式二:传入一个对象
    • 对象是通过 React.createRef() 方式创建出来的;
    • 使用时获取到创建的对象其中有一个 current 属性就是对应的元素;
  • 方式三:传入一个函数
  • 该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
  • 使用时,直接拿到之前保存的元素对象即可;
import React, { PureComponent, createRef } from "react";export class App extends PureComponent {constructor() {this.titleRef = createRef();this.titleEl = null;}getNativeDOM() {// 1.方式一: 在React元素上绑定一个ref字符串// console.log(this.refs.why)// 2.方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素// console.log(this.titleRef.current)// 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入console.log(this.titleEl);}render() {return (<div><h2 ref="why">Hello 紫陌</h2><h2 ref={this.titleRef}>你好zimo</h2><h2 ref={(el) => (this.titleEl = el)}>你好啊紫陌</h2><button onClick={(e) => this.getNativeDOM()}>获取DOM</button></div>);}
}export default App;

11.2 ref 获取类组件实例

import React, { PureComponent, createRef } from "react";class HelloWorld extends PureComponent {test() {console.log("test------");}render() {return <h1>Hello World</h1>;}
}export class App extends PureComponent {constructor() {super();this.hwRef = createRef();}getComponent() {console.log(this.hwRef.current);this.hwRef.current.test();}render() {return (<div><HelloWorld ref={this.hwRef} /><button onClick={(e) => this.getComponent()}>获取组件实例</button></div>);}
}export default App;

11.3 ref 获取函数组实例

import React, { PureComponent, createRef, forwardRef } from "react";const HelloWorld = forwardRef(function (props, ref) {return (<div><h1 ref={ref}>Hello World</h1><p>哈哈哈</p></div>);
});export class App extends PureComponent {constructor() {super();this.hwRef = createRef();}getComponent() {console.log(this.hwRef.current);}render() {return (<div><HelloWorld ref={this.hwRef} /><button onClick={(e) => this.getComponent()}>获取组件实例</button></div>);}
}export default App;

12.受控和非受控组件

  • 表单元素通常自己维护 state,并根据用户输入进 行更新。

  • 在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

  • 我们将两者结合起来,使 React 的 state 成为“唯一数据源”;  渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;

  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;

  • 由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。

  • 类似 vue 的双向绑定

    12.1 受控组件

class App extends PureComponent {constructor(props) {super();this.state = {name: "zimo",};}inputChange(e) {console.log(e.target.value);this.setState({name: e.target.value,});}render() {const { name } = this.state;return (<div>{/* 受控组件 */}<input type="text" value={name} onChange={(e) => this.inputChange(e)} /><h2>{name}</h2>{/* 非受控组件 */}<input type="text" /></div>);}
}

各种表单收集数据例子

class App extends PureComponent {constructor(props) {super();this.state = {username: "",password: "",isAGree: false,hobbies: [{ value: "sing", text: "唱", isChecked: false },{ value: "dance", text: "跳", isChecked: false },{ value: "rap", text: "rap", isChecked: false },],fruit: ["orange"],};}handleSubmitClick(event) {// 1.阻止默认行为event.preventDefault();// 2.获取到所有的表单数据, 对数据进行组织console.log("获取所有的输入内容");console.log(this.state.username, this.state.password);const hobbies = this.state.hobbies.filter((item) => item.isChecked).map((item) => item.value);console.log("获取爱好: ", hobbies);// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)}handleInputChage(event) {console.log(event);this.setState({[event.target.name]: event.target.value,});}handleAgreeChange(event) {this.setState({isAGree: event.target.checked,});}handleHobbiesChange(evnet, index) {const hobbies = [...this.state.hobbies];hobbies[index].isChecked = evnet.target.checked;this.setState({ hobbies });}handleFruitChange(event) {const options = Array.from(event.target.selectedOptions);const values = options.map((item) => item.value);this.setState({ fruit: values });// 效果同上const values2 = Array.from(event.target.selectedOptions, (item) => item.value);console.log(values2);}render() {const { username, password, isAGree, hobbies, fruit } = this.state;return (<div>{/* 表单 */}<form onSubmit={(e) => this.handleSubmitClick(e)}>{/* 1.用户名密码 */}<div><label htmlFor="username">用户名:<inputtype="text"id="username"value={username}name="username"onChange={(e) => this.handleInputChage(e)}/></label><label htmlFor="password">密码:<inputtype="password"id="password"value={password}name="password"onChange={(e) => this.handleInputChage(e)}/></label></div>{/* 2.checkbox 单选*/}<label htmlFor="agree"><input type="checkbox" id="agree" checked={isAGree} onChange={(e) => this.handleAgreeChange(e)} />同意协议</label>{/* 3.checkbox */}<div>你的爱好:{hobbies.map((item, index) => {return (<label htmlFor={item.value} key={item.value}><inputtype="checkbox"value={item.value}checked={item.isChecked}id={item.value}onChange={(e) => this.handleHobbiesChange(e, index)}/><span>{item.text}</span></label>);})}</div>{/* 4.select  多选*/}<select value={fruit} onChange={(e) => this.handleFruitChange(e)} multiple><option value="apple">苹果</option><option value="orange">橘子</option><option value="banana">香蕉</option></select>{/* 提交按钮 */}<button type="submit">注册</button></form></div>);}
}export default App;

12.2 非受控组件

  • React 推荐大多数情况下使用 受控组件 来处理表单数据: 一个受控组件中,表单数据是由 React 组件来管理的;
  • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
  • 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从 DOM 节点中获取表单数据。
  • 使用 ref 来获取 input 元素, 在非受控组件中通常使用来设置默认
  • 开发非受控组件用的比较少
import React, { PureComponent, createRef } from "react";class App extends PureComponent {constructor(props) {super();this.state = {intro: "紫陌yyds",};this.introRef = createRef();}handleSubmitClick(event) {// 1.阻止默认行为event.preventDefault();// 2.获取到所有的表单数据, 对数据进行组织console.log("ref", this.introRef.current.value);// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)}render() {const { intro } = this.state;return (<div>{/* 非受控组件 */}<input type="text" defaultValue={intro} ref={this.introRef} />{/* 提交按钮 */}<button type="submit">注册</button></div>);}
}

13.高阶组件

  • 高阶函数的定义至少满足以下条件之一:
    • 接受一个或多个函数作为输入;
    • 输出一个函数;
  • JavaScript 中比较常见的 filter、map、reduce 都是高阶函数。
  • 高阶组件的英文是 Higher-Order Components,简称为 HOC;
  • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数;

**高阶组件 本身不是一个组件,而是一个函数,这个函数的参数是一个组件,返回值也是一个组件 **

13.1 高阶组件的定义和作用

import React, { PureComponent } from "react";// 定义一个高阶组件
function hoc(Cpn) {//  定义类组件class NewCpn extends PureComponent {render() {return <Cpn name="zimo" />;}}return NewCpn;
}class Hello extends PureComponent {render() {return <div>你好啊,紫陌</div>;}
}const HelloHOC = hoc(Hello);export class App extends PureComponent {render() {return (<div><HelloHOC /></div>);}
}export default App;

13.2 高阶组件应用-props 增强

APP 组件

import React, { PureComponent } from "react";
import enhancedUserInfo from "./hoc/enhanced_props";
import About from "./pages/About";const Home = enhancedUserInfo(function (props) {return (<h1>Home: {props.name}-{props.level}-{props.banners}</h1>);
});const Profile = enhancedUserInfo(function (props) {return (<h1>Profile: {props.name}-{props.level}</h1>);
});const HelloFriend = enhancedUserInfo(function (props) {return (<h1>HelloFriend: {props.name}-{props.level}</h1>);
});export class App extends PureComponent {render() {return (<div><Home banners={["轮播图1", "轮播图2"]} /><Profile /><HelloFriend /><About /></div>);}
}export default App;

enhancedUserInfo 高阶组件:

import { PureComponent } from "react";// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {class NewComponent extends PureComponent {constructor(props) {super(props);this.state = {userInfo: {name: "zimo",level: 99,},};}render() {return <OriginComponent {...this.props} {...this.state.userInfo} />;}}return NewComponent;
}export default enhancedUserInfo;

About 组件

import React, { PureComponent } from "react";
import enhancedUserInfo from "../hoc/enhanced_props";export class About extends PureComponent {render() {return <div>About: {this.props.name}</div>;}
}export default enhancedUserInfo(About);

13.3 高阶组件应用-Context 共享

定义 context theme_context.js

import { createContext } from "react";const ThemeContext = createContext();export default ThemeContext;

定义 HOC 函数 with_theme.js

import ThemeContext from "../context/theme_context";function withTheme(OriginComponent) {return (props) => {return (<ThemeContext.Consumer>{(value) => {return <OriginComponent {...value} {...props} />;}}</ThemeContext.Consumer>);};
}
export default withTheme;

Product 组件:

import React, { PureComponent } from "react";
import withTheme from "../hoc/with_theme";export class Product extends PureComponent {render() {const { color, size } = this.props;return (<div><h2>Product:{color} - {size}</h2></div>);}
}export default withTheme(Product);

APP 组件:

import React, { PureComponent } from "react";
import ThemeContext from "./context/theme_context";
import Product from "./pages/Product";export class App extends PureComponent {render() {return (<div><ThemeContext.Provider value={{ color: "red", size: 50 }}><Product /></ThemeContext.Provider></div>);}
}export default App;

13.4 渲染判断鉴权

  • 在开发中,我们可能遇到这样的场景:
  • 某些页面是必须用户登录成功才能进行进入;
  • 如果用户没有登录成功,那么直接跳转到登录页面;
  • 就可以使用高阶组件来完成鉴权操作:
function loginAuth(OriginComponent) {return (props) => {// 从localStorage中获取tokenconst token = localStorage.getItem("token");if (token) {return <OriginComponent {...props} />;} else {return <h2>请先登录, 再进行跳转到对应的页面中</h2>;}};
}export default loginAuth;

13.5 生命周期劫持

可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

下面是高阶组件是计算渲染时间:

import { PureComponent } from "react";function logRenderTime(OriginComponent) {return class extends PureComponent {UNSAFE_componentWillMount() {this.beginTime = new Date().getTime();}componentDidMount() {this.endTime = new Date().getTime();const interval = this.endTime - this.beginTime;console.log(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`);}render() {return <OriginComponent {...this.props} />;}};
}export default logRenderTime;

13.6 高阶组件的意义

  • 高阶组件可以针对某些 React 代码进行更加优雅的处理。
  • 早期的 React 有提供组件之间的一种复用方式是 mixin,目前已经不再建议使用:
  • Mixin 可能会相互依赖,相互耦合,不利于代码维护;
  • 不同的 Mixin 中的方法可能会相互冲突;
  • Mixin 非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;
  • HOC 也有缺陷:
  • HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;
  • HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突;
  • Hooks 的出现,是开创性的,它解决了很多 React 之前的存在的问题
  • 比如 this 指向问题、比如 hoc 的嵌套复杂度问题等等;

14. Portals 的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的 DOM 元素上的)。
  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
  • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
  • 第二个参数(container)是一个 DOM 元素;

createPortal(

App H2

, document.querySelector(“#zimo”))

index.html-------------------------------------------------------------------------------------------------
<div id="root"></div>
<div id="zimo"></div>App组件:---------------------------------------------------------------------------------------------------import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"export class App extends PureComponent {render() {return (<div className='app'><h1>App H1</h1>{/*元素放到zimo容器 */}{createPortal(<h2>App H2</h2>, document.querySelector("#zimo"))}</div>)}
}export default App

15.Fragment

  • 开发中,我们总是在一个组件中返回内容时包裹一个 div 元素:
  • 我们又希望可以不渲染这样一个 div,使用 Fragment (类似于 Vue 中的 template)
  • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点
  • React 还提供了 Fragment 的短语法:
  • 它看起来像空标签 <></>
  • 如果我们需要在 Fragment 中添加 key,那么就不能使用短语法
import React, { PureComponent, Fragment } from 'react'export class App extends PureComponent {constructor() {super()this.state = {sections: [{ title: "哈哈哈", content: "我是内容, 哈哈哈" },{ title: "呵呵呵", content: "我是内容, 呵呵呵" },{ title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },{ title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },]}}render() {const { sections } = this.statereturn (<><h2>我是App的标题</h2><p>我是App的内容, 哈哈哈哈</p><hr />{sections.map(item => {return ({/*这里不能使用简写 因为key*/}<Fragment key={item.title}><h2>{item.title}</h2><p>{item.content}</p></Fragment>)})}</>)}
}export default App

16. StrictMode

  • StrictMode 是一个用来突出显示应用程序中潜在问题的工具:
  • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
  • 它为其后代元素触发额外的检查和警告
  • 严格模式检查仅在开发模式下运行;它们不会影响生产构建;

例子:

  • 可以为应用程序的任何部分启用严格模式
  • 不会对 Profile 组件运行严格模式检查;
  • 但是,Home 以及它们的所有后代元素都将进行检查;
import React, { PureComponent, StrictMode } from "react";
import Home from "./pages/Home";
import Profile from "./pages/Profile";export class App extends PureComponent {render() {return (<div><StrictMode><Home /></StrictMode><Profile /></div>);}
}export default App;

16.1 严格模式检查的是什么

  1. 识别不安全的生命周期:
  2. 使用过时的 ref API
  3. .检查意外的副作用
    1. 这个组件的 constructor 会被调用两次;
    2. 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
    3. 在生产环境中,是不会被调用两次的;
  4. 使用废弃的 findDOMNode 方法
    1. 在之前的 React API 中,可以通过 findDOMNode 来获取 DOM,
  5. 检测过时的 context API
    1. 早期的 Context 是通过 static 属性声明 Context 对象属性,通过 getChildContext 返回 Context 对象等方式来使用 Context 的; 目前这种方式已经不推荐使用,

17. React 过渡动画实现

  • React 曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。
  • 这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装
  • react-transition-group 本身非常小,不会为我们应用程序增加过多的负担。

npm

npm install react-transition-group --save

yarn

yarn add react-transition-group

17.1 react-transition-group 主要组件

  • react-transition-group 主要包含四个组件
    • Transition
    • 该组件是一个和平台无关的组件(不一定要结合 CSS);
    • 在前端开发中,我们一般是结合 CSS 来完成样式,所以比较常用的是 CSSTransition;
    • CSSTransition
    • 在前端开发中,通常使用 CSSTransition 来完成过渡动画效果
    • SwitchTransition
    • 两个组件显示和隐藏切换时,使用该组件
    • TransitionGroup
    • 将多个动画组件包裹在其中,一般用于列表中元素的动画;

17.2 CSSTransition 动画

  • CSSTransition 是基于 Transition 组件构建的:
  • CSSTransition 执行过程中,有三个状态:appear、enter、exit;
  • 它们有三种状态,需要定义对应的 CSS 样式:
  • 第一类,开始状态:对于的类是-appear、-enter、exit;
  • 第二类:执行动画:对应的类是-appear-active、-enter-active、-exit-active;
  • 第三类:**执行结束:**对应的类是-appear-done、-enter-done、-exit-done;
  • CSSTransition 常见对应的属性
    • in:触发进入或者退出状态
    • 如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉
    • in 为 true 时,触发进入状态,会添加-enter、-enter-acitve 的 class 开始执行动画,当动画执行结束后,会移除两个 class, 并且添加-enter-done 的 class;
    • 当 in 为 false 时,触发退出状态,会添加-exit、-exit-active 的 class 开始执行动画,当动画执行结束后,会移除两个 class,并 且添加-enter-done 的 class;
    • classNames:动画 class 的名称
    • 决定了在编写 css 时,对应的 class 名称:比如 zimoenter、zimo-enter-active、zimo-enter-done;
    • timeout:
    • 过渡动画的时间
    • appear:
    • 是否在初次进入添加动画(需要和 in 同时为 true)
    • unmountOnExit:退出后卸载组件
    • 其他属性可以参考官网来学习:  https://reactcommunity.org/react-transition-group/transition
    • CSSTransition 对应的钩子函数:主要为了检测动画的执行过程,来完成一些 JavaScript 的操作
    • onEnter:在进入动画之前被触发;
    • onEntering:在应用进入动画时被触发;
    • onEntered:在应用进入动画结束后被触发;

APP 组件:

import React, { PureComponent } from "react";
import { CSSTransition } from "react-transition-group";
import "./style.css";export class App extends PureComponent {constructor(props) {super(props);this.state = {isShow: true,};}render() {const { isShow } = this.state;return (<div><button onClick={(e) => this.setState({ isShow: !isShow })}>切换</button>{/* 无动画 */}{/* {isShow && <h2>紫陌YYDS</h2>} */}{/* 有动画 */}<CSSTransition// 以下为属性in={isShow}unmountOnExit={true}timeout={2000}classNames={"zimo"}appear// 以下为钩子函数onEnter={(e) => console.log("开始进入动画")}onEntering={(e) => console.log("执行进入动画")}onEntered={(e) => console.log("执行进入结束")}onExit={(e) => console.log("开始离开动画")}onExiting={(e) => console.log("执行离开动画")}onExited={(e) => console.log("执行离开结束")}><div className="section"><h2>紫陌YYDS</h2><p>zimo学前端</p></div></CSSTransition></div>);}
}export default App;

CSS:

.zimo-appear,
.zimo-enter {opacity: 0;
}.zimo-appear-active,
.zimo-enter-active {opacity: 1;transition: opacity 2s ease;
}/* 离开动画 */
.zimo-exit {opacity: 1;
}.zimo-exit-active {opacity: 0;transition: opacity 2s ease;
}

17.3 SwitchTransition 动画

  • SwitchTransition 可以完成两个组件之间切换的炫酷动画:

    • 比如我们有一个按钮需要在 on 和 off 之间切换,我们希望看到 on 先从左侧退出,off 再从右侧进入;
    • 这个动画在 vue 中被称之为 vue transition modes;
    • react-transition-group 中使用 SwitchTransition 来实现该动画;
  • SwitchTransition 中主要有一个属性:mode,有两个值

  • in-out:表示新组件先进入,旧组件再移除;

  • out-in:表示就组件先移除,新组建再进入;

  • 如何使用 SwitchTransition 呢?

  • SwitchTransition 组件里面要有 CSSTransition 或者 Transition 组件,不能直接包裹你想要切换的组件;

  • SwitchTransition 里面的 CSSTransition 或 Transition 组件不再像以前那样接受 in 属性来判断元素是何种状态,取而代之的是 key 属性;

  • 当 key 值改变时,CSSTransition 组件会重新渲染,也就会触发动画

APP 组件:

import React, { PureComponent } from "react";
import { SwitchTransition, CSSTransition } from "react-transition-group";
import "./style.css";export class App extends PureComponent {constructor(props) {super(props);this.state = {isLogin: true,};}render() {const { isLogin } = this.state;return (<div><SwitchTransition mode="out-in"><CSSTransition// 当key值改变时,CSSTransition组件会重新渲染,也就会触发动画key={isLogin ? "login" : "exit"}classNames={"login"}timeout={1000}><button onClick={(e) => this.setState({ isLogin: !isLogin })}>{isLogin ? "退出" : "登录"}</button></CSSTransition></SwitchTransition></div>);}
}export default App;

CSS :

.login-enter {transform: translateX(100px);opacity: 0;
}.login-enter-active {transform: translateX(0);opacity: 1;transition: all 1s ease;
}.login-exit {transform: translateX(0);opacity: 1;
}.login-exit-active {transform: translateX(-100px);opacity: 0;transition: all 1s ease;
}

17.4 TransitionGroup 动画

当我们有一组动画时,需要将这些 CSSTransition 放入到一个 TransitionGroup 中来完成动画

APP 组件:

import React, { PureComponent } from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import "./style.css";export class App extends PureComponent {constructor() {super();this.state = {books: [{ id: 111, name: "你不知道JS", price: 99 },{ id: 222, name: "JS高级程序设计", price: 88 },{ id: 333, name: "Vuejs高级设计", price: 77 },],};}addNewBook() {const books = [...this.state.books];books.push({id: new Date().getTime(),name: "React高级程序设计",price: 99,});this.setState({ books });}removeBook(index) {const books = [...this.state.books];books.splice(index, 1);this.setState({ books });}render() {const { books } = this.state;return (<div><h2>书籍列表:</h2><TransitionGroup component="ul">{books.map((item, index) => {return (<CSSTransition key={item.id} classNames="book" timeout={1000}><li><span>{item.name}</span><button onClick={(e) => this.removeBook(index)}>删除</button></li></CSSTransition>);})}</TransitionGroup><button onClick={(e) => this.addNewBook()}>添加新书籍</button></div>);}
}export default App;

CSS:

.book-enter {transform: translateX(150px);opacity: 0;
}.book-enter-active {transform: translateX(0);opacity: 1;transition: all 1s ease;
}.book-exit {transform: translateX(0);opacity: 1;
}.book-exit-active {transform: translateX(150px);opacity: 0;transition: all 1s ease;
}

18. React 中的 CSS

  • css 一直是 React 的痛点,也是被很多开发者吐槽、诟病的一个点。
  • 在这一点上,Vue 做的要好于 React
  • React 官方并没有给出在 React 中统一的样式风格: 普通的 css,到 css modules,再到 css in js,有几十种不同的解决方案,上百个不同的库; 大家一致在寻找最好的或者说最适合自己的 CSS 方案,但是到目前为止也没有统一的方案;

18.1 内联样式

  • 内联样式是官方推荐的一种 css 样式的写法:
  • style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
  • 并且可以引用 state 中的状态来设置相关的样式;
  • 内联样式的优点:
  • 内联样式, 样式之间不会有冲突
  • 可以动态获取当前 state 中的状态
  • 内联样式的缺点:
  • 写法上都需要使用驼峰标识
  • 某些样式没有提示
  • 大量的样式, 代码混乱
  • 某些样式无法编写(比如伪类/伪元素)
export class App extends PureComponent {constructor(props) {super(props);this.state = {size: 12,};}addTitleSize() {this.setState({size: this.state.size + 2,});}render() {const { size } = this.state;return (<div><button onClick={(e) => this.addTitleSize()}>字体变大</button><h2 style={{ fontSize: `${size}px` }}>我是紫陌</h2><h2 style={{ color: "yellowGreen" }}>我是紫陌</h2></div>);}
}

18.2 普通的 css

  • 普通的 css 我们通常会编写到一个单独的文件,之后再进行引入。
  • 这样的编写方式和普通的网页开发中编写方式是一致的:
  • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
  • 但是普通的 css都属于全局的 css,样式之间会相互影响
  • 这种编写方式最大的问题是样式之间会相互层叠掉;

18.3 css modules

  • css modules 并不是 React 特有的解决方案,而是所有使用了**类似于 webpack****配置的环境下都可以使用的**。
    • 如果在其他项目中使用它,那么我们需要自己来进行配置,比如配置 webpack.config.js 中的 modules: true 等。
  • React 的脚手架已经内置了 css modules 的配置:
  • .css/.less/.scss 等样式文件都需要修改成 .module.css/.module.less/.module.scss 等; 之后就可以引用并且进行使用了;
  • css modules 确实解决了局部作用域的问题,也是很多人喜欢在 React 中使用的一种方案。
  • 但是这种方案也有自己的缺陷:
  • 引用的类名,不能使用连接符(.home-title),在 JavaScript 中是不识别的;
  • 所有的className 都必须使用{style.className} 的形式来编写
  • 不方便动态来修改某些样式,依然需要使用内联样式的方式
  • 如果你觉得上面的缺陷还算 OK,那么你在开发中完全可以选择使用 css modules 来编写,并且也是在 React 中很受欢迎的一种方式。

Home.module.css

.section {border: 1px solid skyblue;
}.title {color: purple;
}

Home.jsx

import React, { PureComponent } from "react";
import homeStyle from "./Home.module.css";export class Home extends PureComponent {render() {return (<div className={homeStyle.section}><div className={homeStyle.title}>Home的标题</div></div>);}
}export default Home;

18.4 CSS in JS

  • 官方文档也有提到过 CSS in JS 这种方案:

    • CSS-in-JS” 是指一种模式其中 CSS 由 JavaScript 生成而不是在外部文件中定义
    • 注意此功能并不是 React 的一部分,而是由第三方库提供
    • React 对样式如何定义并没有明确态度;
  • 在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。

    • React 的思想中认为逻辑本身和 UI 是无法分离的,所以才会有了 JSX 的语法
    • CSS-in-JS 的模式就是一种将样式(CSS)也写入到 JavaScript 中的方式,并且可以方便的使用 JavaScript 的状态;
    • 所以 React 有被人称之为 All in JS;
  1. 认识 styled-components

    • CSS-in-JS 通过 JavaScript 来为 CSS 赋予一些能力,包括类似于CSS 预处理器一样的样式嵌套、函数定义、逻辑复用、动态修 改状态等等; 虽然 CSS 预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点
    • 所以,目前可以说 CSS-in-JS 是 React 编写 CSS 最为受欢迎的一种解决方案;
  2. 目前比较流行的 CSS-in-JS 的库

    • styled-components
    • emotion
    • glamorous

目前可以说 styled-components 依然是社区最流行的 CSS-in-JS 库

  1. props、attrs 属性

    • props 可以被传递给 styled 组件  获取 props 需要通过${}传入一个插值函数,props 会作为该函数的参数;

    • 这种方式可以有效的解决动态样式的问题;

    • 添加 attrs 属性

    • export const SectionWrapper = styled.div.attrs((props) => ({// 可以通过attrs给标签模板字符串中提供属性tColor: props.color || "blue",
      }))` xxx  xxx `;
      
  2. 案例

APP 组件:

import React, { PureComponent } from "react";
import { AppWrapepr, SectionWrapper } from "./style";export class App extends PureComponent {constructor() {super();this.state = {size: 30,color: "yellow",};}render() {const { size, color } = this.state;return (<AppWrapepr><SectionWrapper size={size} color={color}><h2 className="title">我是紫陌</h2><p className="content">我是zimo</p><button onClick={(e) => this.setState({ color: "skyblue" })}>修改颜色</button></SectionWrapper><div className="footer"><h2>这是底部</h2><p>zimo@yyds</p></div></AppWrapepr>);}
}export default App;

style.js 文件

import styled from "styled-components";
import { primaryColor, largeSize } from "./style/variables";// 基本使用
export const AppWrapepr = styled.div`.footer {border: solid 1px blue;}
`;// 子元素单独抽取到一个样式组件
export const SectionWrapper = styled.div.attrs((props) => ({// 可以通过attrs给标签模板字符串中提供属性tColor: props.color || "blue",
}))`border: 1px solid red;.title {/* 可以接受外部传入的props */font-size: ${(props) => props.size}px;color: ${(props) => props.tColor};&:hover {background-color: purple;}}/* 从一个单独文件中引入变量 */.content {font-size: ${largeSize}px;color: ${primaryColor};}
`;

variables.js 文件 (变量文件)

export const primaryColor = "#ff8822";
export const secondColor = "#ff7788";export const smallSize = "12px";
export const middleSize = "14px";
export const largeSize = "18px";
  1. styled 高级特性

styled 设置主题,支持样式的继承

index.jsx(根组件传主题变量)

<ThemeProvider theme={{ color: "purple", size: "50px" }}><App />
</ThemeProvider>

APP.jsx

import React, { PureComponent } from "react";
import { HomeWrapper, ButtonWrapper } from "./style";export class Home extends PureComponent {render() {return (<HomeWrapper><h2 className="header">商品列表</h2><ButtonWrapper>哈哈哈</ButtonWrapper></HomeWrapper>);}
}export default Home;

style.js

import styled from "styled-components";const Button = styled.button`border: 1px solid red;border-radius: 5px;
`;// 继承
export const ButtonWrapper = styled(Button)`background-color: #0f0;color: #fff;
`;// 主题
export const HomeWrapper = styled.div`.header {color: ${(props) => props.theme.color};font-size: ${(props) => props.theme.size};}
`;

18.5 React 中添加 class(classnames 库)

React 在 JSX 给了我们开发者足够多的灵活性,你可以像编写 JavaScript 代码一样,通过一些逻辑来决定是否添加某些 class

用于动态添加 classnames 的一个库

yarn add classnames

import React, { PureComponent } from "react";
import classNames from "classnames";export class App extends PureComponent {constructor() {super();this.state = {isbbb: true,isccc: true,};}render() {const { isbbb, isccc } = this.state;const classList = ["aaa"];if (isbbb) classList.push("bbb");if (isccc) classList.push("ccc");const classname = classList.join(" ");return (<div>{/* 不推荐  */}<h2 className={`aaa ${isbbb ? "bbb" : ""} ${isccc ? "ccc" : ""}`}>我是紫陌</h2>{/* 不推荐 */}<h2 className={classname}>我是紫陌</h2>{/* 推荐 第三方库 */}<h2 className={classNames("aaa", { bbb: isbbb, ccc: isccc })}>哈哈哈哈</h2><h2 className={classNames(["aaa", { bbb: isbbb, ccc: isccc }])}>呜呜呜</h2></div>);}
}

语法:

classNames(' foo','bar'); //=>'foo  bar'
classNames('foo'{bar: true });//=>·'foo bar'
classNames({'foo-bar': false });//=>''
classNames({ foo: true },{ bar: true });//=>'foo bar'
classNames({ foo: true, bar: true }); //=>.'foo bar'
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); //=>'foo bar baz quux'
classNames(null,false,'bar',undefined,0,1,{baz:null}, '') // 'bar 1'

19.纯函数理解

  • 函数式编程中有一个非常重要的概念叫纯函数,JavaScript 符合函数式编程的范式,所以也有纯函数的概念;

  • 在 react 开发中纯函数是被多次提及的,比如 react 中组件就被要求像是一个纯函数(为什么是像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求 必须是一个纯函数;

  • 简单概要纯函数:

    • 确定的输入,一定会产生确定的输出;
    • 函数在执行过程中,不能产生副作用;
  • 纯函数副作用概念:

    • 表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响, 比如修改了全局变量,修改参数或者改变外部的存储;
  • 纯函数的作用和优势

    • 可以安心的编写和安心的使用; 保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的 外部变量是否已经发生了修改; 用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
    • React 中就要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改,reducer也被要求是一个纯函数

20. Redux

20.1 Redux 的三大原则

  • 单一数据源
    • 整个应用程序的state 被存储在一颗 object tree 中,并且这个 object tree只存储在一个 store 中
    • Redux 并没有强制让我们不能创建多个 Store,但是那样做并不利于数据的维护;
    • 单一的数据源可以让整个应用程序的 state 变得方便维护、追踪、修改
  • State 是只读的
  • 唯一修改 State 的方法一定是触发 action,不要试图在其他地方通过任何的方式来修改 State:
  • 这样就确保了 View 或网络请求都不能直接修改 state,它们只能通过 action 来描述自己想要如何修改 state;
  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竟态)的问题;
  • 使用纯函数来执行修改
    • 通过 reducer 将 旧 state 和 actions 联系在一起,并且返回一个新的 State:
    • 随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同 state tree 的一部分;
    • 但是所有的 reducer 都应该是纯函数,不能产生任何的副作用;

20.2 Redux 的使用过程

  • 创建 Store 来存储这个 state
    • 创建 store 时必须创建 reducer; -
    • 通过 store.getState 来获取当前的 state;
  • 通过 action 来修改 state
    • 通过 dispatch 来派发 action;
    • 通常 action 中都会有 type 属性,也可以携带其他的数据;
  • 修改 reducer 中的处理代码
    • reducer 是一个纯函数,不需要直接修改 state
  • 可以在派发 action 之前,监听 store 的变化

20.3Redux 结构划分

  • 创建 store/index.js 文件:
  • 创建 store/reducer.js 文件:
  • 创建 store/actionCreators.js 文件:
  • 创建 store/constants.js 文件:

在这里插入图片描述
在这里插入图片描述

20.4 脱离 React 的 redux 代码

redux 和 react 没有直接的关系,完全可以在 React, Angular, Ember, jQuery, or vanilla JavaScript 中使用 Redux

store/index.js

const { createStore } = require("redux");
const reducer = require("./reducer.js");// 创建的store
const store = createStore(reducer);module.exports = store;

reducer.js

const { ADD_NUMBER, CHANGE_NAME } = require("./constants");// 初始化数据
const initialState = {name: "张三",counter: 100,
};function reducer(state = initialState, action) {switch (action.type) {case CHANGE_NAME:return { ...state, name: action.name };case ADD_NUMBER:return { ...state, counter: state.counter + action.num };}
}module.exports = reducer;

constants.js

const ADD_NUMBER = "add_number";
const CHANGE_NAME = "change_name";module.exports = {ADD_NUMBER,CHANGE_NAME,
};

actionCreators.js

const { CHANGE_NAME, ADD_NUMBER } = require("./constants.js");const changeNameAction = (name) => ({type: CHANGE_NAME,name,
});const addNumberAction = (num) => ({type: ADD_NUMBER,num,
});module.exports = {changeNameAction,addNumberAction,
};

上面四模块就是 store 的结构

组件派发 Action

const store = require("./store1/index.js");
const { addNumberAction, changeNameAction } = require("./actionCreator.js");const unsubscribe = store.subscribe(() => {console.log("订阅数据的变化:", store.getSatte());
});store.dispatch(changeNameAction("zimo"));
store.dispatch(changeNameAction("紫陌"));
store.dispatch(changeNameAction("yy"));store.dispatch(addNumberAction(13));
store.dispatch(addNumberAction(167));
store.dispatch(addNumberAction(137));

20.5React 中的 Redux

Store 目录结构代码略过。。。。同上。直接看组件

APP 组件:

import React, { Component } from "react";
import Home from "./pages/home";
import Profile from "./pages/profile";
import "./style.css";
import store from "./store";class App extends Component {constructor() {super();this.state = {counter: store.getState().counter,};}componentDidMount() {store.subscribe(() => {const state = store.getState();this.setState({ counter: state.counter });});}render() {const { counter } = this.state;return (<div><h2>App counter: {counter}</h2><div className="pages"><Home /><Profile /></div></div>);}
}export default App;

Home 组件:

import React, { PureComponent } from "react";
import store from "../store";
import { addNumberAction } from "../store/actionCreators";export class Home extends PureComponent {constructor() {super();this.state = {counter: store.getState().counter,};}componentDidMount() {store.subscribe(() => {const state = store.getState();this.setState({ counter: state.counter });});}addNumber(num) {store.dispatch(addNumberAction(num));}render() {const { counter } = this.state;return (<div><h2>Home counterer: {counter}</h2><div><button onClick={(e) => this.addNumber(1)}>+1</button><button onClick={(e) => this.addNumber(5)}>+5</button><button onClick={(e) => this.addNumber(8)}>+8</button></div></div>);}
}export default Home;

Profile 组件

import React, { PureComponent } from "react";
import store from "../store";
import { addNumberAction } from "../store/actionCreators";export class profile extends PureComponent {constructor() {super();this.state = {counter: store.getState().counter,};}componentDidMount() {store.subscribe(() => {const state = store.getState();this.setState({ counter: state.counter });});}addNumber(num) {store.dispatch(addNumberAction(num));}render() {const { counter } = this.state;return (<div><h2>Profile Counter: {counter}</h2><div><button onClick={(e) => this.addNumber(1)}>+1</button><button onClick={(e) => this.addNumber(5)}>+5</button><button onClick={(e) => this.addNumber(8)}>+8</button></div></div>);}
}export default profile;

上面就是基本使用。但是有个问题。每次写一个组件就要写一个一套相似的逻辑如图:

在这里插入图片描述

20.6 Redux–connect 高阶函数

  • connect 高阶函数 实际上 redux 官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。

    安装 react-redux:yarn add react-redux

  1. 在 index.js 文件中:context 的全局注入一个 store

    import App from "./App";
    import { Provider } from "react-redux";
    import store from "./store";const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<Provider store={store}><App /></Provider>
    );
    
  2. About 组件:有加减逻辑

import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumberAction, subNumberAction } from "../store/actionCreators";export class About extends PureComponent {calcNumber(num, isAdd) {if (isAdd) {console.log("加", num);this.props.addNumber(num);} else {console.log("减", num);this.props.subNumber(num);}}render() {const { counter } = this.props;return (<div><h2>About Page: {counter}</h2><div><button onClick={(e) => this.calcNumber(6, true)}>+6</button><button onClick={(e) => this.calcNumber(88, true)}>+88</button><button onClick={(e) => this.calcNumber(6, false)}>-6</button><button onClick={(e) => this.calcNumber(88, false)}>-88</button></div></div>);}
}const mapStateToProps = (state) => {return {counter: state.counter,};
};const mapDispatchToProps = (dispatch) => {return {addNumber: (num) => {dispatch(addNumberAction(num));},subNumber(num) {dispatch(subNumberAction(num));},};
};export default connect(mapStateToProps, mapDispatchToProps)(About);
  1. connect()函数是一个高阶函数接收两个参数,connect 函数返回一个高阶组件。最后一个括号是放组件的
  2. 使用了 connect 函数。mapStateToProps 把 store 上的数据映射到了 props。组件通过 props 拿到。action 也是如此。类似 vue 中的 mapState 辅助函数,mapAction 辅助函数

20.7 派发异步的 action

  • redux-thunk 是可以发送异步的请求**,默认情况下的 dispatch(action),action 需要是一个 JavaScript 的对象**;

  • redux-thunk 可以让 dispatch(action 函数),action 可以是一个函数;

  • 该函数会被调用,并且会传给这个函数一个 dispatch 函数和 getState 函数

  • dispatch 函数用于我们之后再次派发 action;

  • getState 函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

  • 使用 redux-thunk

    yarn add redux-thunk

    import { createStore, applyMiddleware } from "redux";
    import thunk from "redux-thunk";
    import reducer from "./reducer.js";const store = createStore(reducer, applyMiddleware(thunk));export default store;
    

**案例代码:异步 action:**组件中通过 props 调用 action 发情网络请求

import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { fetchHomeMultidataAction } from "../store/actionCreators";export class Category extends PureComponent {componentDidMount() {this.props.fetchHomeMultidata();}render() {return <h2>Category Page: {this.props.counter}</h2>;}
}const mapStateToProps = (state) => {return {counter: state.counter,};
};const mapDispatchToProps = (dispatch) => {return {fetchHomeMultidata: () => {dispatch(fetchHomeMultidataAction());},};
};export default connect(mapStateToProps, mapDispatchToProps)(Category);

actionCreators.js 中:

export const changeBannersAction = (banners) => ({type: actionTypes.CHANGE_BANNERS,banners,
});export const changeRecommendsAction = (recommends) => ({type: actionTypes.CHANGE_RECOMMENDS,recommends,
});export const fetchHomeMultidataAction = () => {// 普通action/* 如果是一个普通action,那么我们这里需要返回一个action对象问题:对象是不能直接拿到服务器请求的异步数据的return {}*/// 异步action处理返回函数/* 如果返回一个函数那么redux是不支持的。需要借助插件redux-thunk*/return (dispatch, getState) => {//异步操作axios.get("http://xxxxxx/home/multidata").then((res) => {const banners = res.data.data.banner.list;const recommends = res.data.data.recommend.list;// 派发actiondispatch(changeBannersAction(banners));dispatch(changeRecommendsAction(recommends));});};
};

组件中 mapStateToProps 函数直接获取即可。

20.8 redux-devtools

  • 第一步:在对应的浏览器中安装相关的插件(比如 Chrome 浏览器扩展商店中搜索 Redux DevTools 即可)
  • 第二步:在 redux 中继承 devtools 的中间件;
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer.js";const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));export default store;

20.9 Redux 模块化

**combineReducers****函数实现模块化**

  • combineReducers 是如何实现?
  • 事实上,它也是将我们传入的 reducers 合并到一个对象中,最终返回一个 combination 的函数(相当于我们之前的 reducer 函 数了);
  • 执行 combination 函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的 state 还是新的 state
  • 新的 state 会触发订阅者发生对应的刷新,而旧的 state 可以有效的组织订阅者发生刷新

store/index.js

import { createStore, applyMiddleware, compose, combineReducers } from "redux";
import thunk from "redux-thunk";import counterReducer from "./counter";
import homeReducer from "./home";// 将两个reducer合并在一起
const reducer = combineReducers({counter: counterReducer,home: homeReducer,
});const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));export default store;

在这里插入图片描述
在这里插入图片描述

combineReducers实现原理

import counterReducer from "./counter";
import homeReducer from "./home";// combineReducers实现原理(了解)
function reducer(state = {}, action) {// 返回一个对象, store的statereturn {//state.counter 第一次undefined 就拿到默认值counter: counterReducer(state.counter, action),home: homeReducer(state.home, action),};
}

21.ReduxToolkit

  • Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
  • 在前面我们学习 Redux 的时候应该已经发现,redux 的编写逻辑过于的繁琐和麻烦。
  • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
  • Redux Toolkit 包旨在成为编写 Redux 逻辑的标准方式,从而解决上面提到的问题;
  • 在很多地方为了称呼方便,也将之称为“RTK”;

安装 Redux Toolkit:

npm install @reduxjs/toolkit react-redux

  • Redux Toolkit 的核心 API 主要是如下几个:
    • configureStore:包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk 默认包含,并启用 Redux DevTools Extension。
    • createSlice:接受 reducer 函数的对象、切片名称和初始状态值,并自动生成切片 reducer,并带有相应的 actions。
    • createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个 pending/fulfilled/rejected 基于该承诺分 派动作类型的 thunk

21.Redux Toolkit 使用流程

  1. store 的创建

    • configureStore 用于创建 store 对象,常见参数如下:
    • reducer,将 slice 中的 reducer 可以组成一个对象传入此处;
    • middleware:可以使用参数,传入其他的中间件(自行了解);
    • devTools:是否配置 devTools 工具,默认为 true;
    import { configureStore } from "@reduxjs/toolkit";import counterReducer from "./features/counter";
    import homeReducer from "./features/home";const store = configureStore({reducer: {counter: counterReducer,home: homeReducer,},
    });export default store;
    
  2. 通过 createSlice 创建一个 slice。

  • createSlice 主要包含如下几个参数:
    • name:用户标记 slice 的名词, 在之后的 redux-devtool 中会显示对应的名词;
    • initialState:初始化值 ,第一次初始化时的值;
    • reducers:相当于之前的 reducer 函数
    • 对象类型,并且可以添加很多的函数;
    • 函数类似于 redux 原来 reducer 中的一个 case 语句;
    • 函数的参数:
    • 参数一:state
    • 参数二:调用这个 action 时,传递的 action 参数;
    • createSlice 返回值是一个对象,包含所有的 actions;

store/features/counter.js(counter 的 reducer)

import { createSlice } from "@reduxjs/toolkit";const counterSlice = createSlice({name: "counter",initialState: {counter: 999,},reducers: {addNumber(state, { payload }) {console.log(payload);state.counter = state.counter + payload;},subNumber(state, { payload }) {state.counter = state.counter - payload;},},
});export const { addNumber, subNumber } = counterSlice.actions;export default counterSlice.reducer;

组件中展示数据:(这部分热 redux 一样)

import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumber } from "../store/features/counter";export class About extends PureComponent {render() {const { counter } = this.props;return (<div><h2>About Page: {counter}</h2></div>);}
}const mapStateToProps = (state) => {return {counter: state.counter.counter,};
};const mapDispatchToProps = (dispatch) => {return {addNumber: (num) => {dispatch(addNumber(num));},};
};export default connect(mapStateToProps, mapDispatchToProps)(About);

21.2 Toolkit 的异步操作

  • 在之前 redux-thunk 中间件让 dispatch 中可以进行异步操作
  • Redux Toolkit 默认已经给我们继承了 Thunk 相关的功能:createAsyncThunk

当 createAsyncThunk 创建出来的 action 被 dispatch 时,会存在三种状态:

  1. pending:action 被发出,但是还没有最终的结果;
  2. fulfilled:获取到最终的结果(有返回值的结果);
  3. rejected:执行过程中有错误或者抛出了异常;
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
// 1.
export const fetchHomeMultidataAction = createAsyncThunk("fetch/homemultidata", async () => {const res = await axios.get("http://123.207.32.32:8000/home/multidata");return res.data;
});const homeSlice = createSlice({name: "home",initialState: {banners: [],recommends: [],},reducers: {changeBanners(state, { payload }) {state.banners = payload;},changeRecommends(state, { payload }) {state.recommends = payload;},},//2.extraReducers: {[fetchHomeMultidataAction.pending](state, action) {console.log("fetchHomeMultidataAction pending");},[fetchHomeMultidataAction.fulfilled](state, { payload }) {state.banners = payload.data.banner.list;state.recommends = payload.data.recommend.list;},[fetchHomeMultidataAction.rejected](state, action) {console.log("fetchHomeMultidataAction rejected");},},
});export const { changeBanners, changeRecommends } = homeSlice.actions;
export default homeSlice.reducer;

extraReducers第二种写法(函数式写法)

可以向 builder 中添加 case 来监听异步操作的结果

extraReducers: (builder) => {builder.addCase(fetchHomeMultidataAction.pending, (state, action) => {console.log("fetchHomeMultidataAction-pending");}).addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {state.banners = payload.data.banner.list;state.recommends = payload.data.recommend.list;});
};

函数式写法还有一个用法

在这里插入图片描述

22.React Router

  • 安装时,我们选择 react-router-dom;

  • react-router 会包含一些 react-native 的内容,web 开发并不需要;

    npm install react-router-dom

22.1 BrowserRouter 或 HashRouter

  • BrowserRouter 使用 history 模式;
  • HashRouter 使用 hash 模式;
root.render(<HashRouter><App /></HashRouter>
);

22.2 路由映射配置

  • Routes:包裹所有的 Route,在其中匹配一个路由
    • Router5.x 使用的是 Switch 组件
  • Route:Route 用于路径的匹配;
  • path 属性:用于设置匹配到的路径;
  • element 属性:设置匹配到路径后,渲染的组件;
    • Router5.x 使用的是 component 属性
  • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
  • Router6.x 不再支持该属性
<Routes><Route path="/home" element={<Home />}><Route path="/home/recommend" element={<HomeRecommend />} /></Route>
</Routes>

22.3 路由跳转

  1. Link

to 属性:Link 中最重要的属性,用于设置跳转到的路径

<Link to="/home">首页</Link>
<Link to="/about">关于</Link>
  1. NavLink

需求:路径选中时,对应的 a 元素变为红色

  • 我们要使用 NavLink 组件来替代 Link 组件:
  • style:传入函数,函数接受一个对象,包含 isActive 属性
  • className:传入函数,函数接受一个对象,包含 isActive 属性
<NavLink to="/home" style={({isActive}) => ({color: isActive ? "red": ""})}>首页</NavLink>
<NavLink to="/about" style={({isActive}) => ({color:  isActive ? "red": ""})}>关于</NavLink>

默认的 activeClassName: 事实上在默认匹配成功时,NavLink 就会添加上一个动态的 active class; 所以我们也可以直接编写样式

如果你担心这个 class 在其他地方被使用了,出现样式的层叠,也可以自定义 class

 <NavLink to="/home" className={({isActive}) => isActive?"link-active":""}>首页</NavLink><NavLink to="/about" className={({isActive}) => isActive?"link-active":""}>关于</NavLink>
  1. Navigate

    Navigate 用于路由的重定向,当这个组件出现时,就会执行跳转到对应的 to 路径中:

<Route path="/" element={<Navigate to="/home" />} />

23.4Not Found 页面配置

  • 开发一个 Not Found 页面;
  • 配置对应的 Route,并且设置 path 为*即可;
<Route path="*" element={<NotFound />} />

23.5 路由的嵌套

<Routes><Route path="/home" element={<Home />}><Route path="/home" element={<Navigate to="/home/recommend" />} /><Route path="/home/recommend" element={<HomeRecommend />} /><Route path="/home/ranking" element={<HomeRanking />} /></Route>
</Routes>

23.6 手动路由的跳转

  • 跳转主要是通过 Link 或者 NavLink 进行跳转的,实际上我们也可以通过 JavaScript 代码进行跳转。
  • Navigate 组件是可以进行路由的跳转的,但是依然是组件的方式。
  • 如果我们希望通过 JavaScript 代码逻辑进行跳转(比如点击了一个 button),那么就需要获取到 navigate 对象。
  1. 函数式使用 hooks 跳转

    export function App(props) {const navigate = useNavigate();function navigateTo(path) {navigate(path);}return (<div className="app"><div className="nav"><button onClick={(e) => navigateTo("/category")}>分类</button><span onClick={(e) => navigateTo("/order")}>订单</span></div><div className="content"><Routes><Route path="/category" element={<Category />} /><Route path="/order" element={<Order />} /></Routes></div></div>);
    }
    
  2. 类路由跳转(必须使用高阶组件封装才行,第一点实现)

    import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";// 高阶组件: 函数
    function withRouter(WrapperComponent) {return function (props) {// 1.导航const navigate = useNavigate();// 2.动态路由的参数: /detail/:idconst params = useParams();// 3.查询字符串的参数: /user?name=why&age=18const location = useLocation();const [searchParams] = useSearchParams();const query = Object.fromEntries(searchParams);const router = { navigate, params, location, query };return <WrapperComponent {...props} router={router} />;};
    }export default withRouter;
    

    使用时候:

    import { withRouter } from "../hoc";export class Home extends PureComponent {navigateTo(path) {const { navigate } = this.props.router;navigate(path);}render() {return (<div><button onClick={(e) => this.navigateTo("/home/songmenu")}>歌单</button>{/* 占位的组件 */}<Outlet /></div>);}
    }export default withRouter(Home);
    

23.7 路由参数传递

传递参数有二种方式:

  1. 动态路由的方式;
  2. search 传递参数;
  • 比如/detail 的 path 对应一个组件 Detail;
  • 如果我们将 path 在 Route 匹配时写成/detail/:id,那么 /detail/abc、/detail/123 都可以匹配到该 Route,并且进行显示;
  • 这个匹配规则,我们就称之为动态路由;
  • 使用动态路由可以为路由传递参数
  1. search 传递参数

在这里插入图片描述

高阶组件:

import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";// 高阶组件: 函数
function withRouter(WrapperComponent) {return function (props) {// 1.导航const navigate = useNavigate();// 2.动态路由的参数: /detail/:idconst params = useParams();// 3.查询字符串的参数: /user?name=why&age=18const location = useLocation();const [searchParams] = useSearchParams();const query = Object.fromEntries(searchParams);const router = { navigate, params, location, query };return <WrapperComponent {...props} router={router} />;};
}export default withRouter;
  1. search 传递参数

在这里插入图片描述

23.8 路由的配置文件(包含路由懒加载)

  • 目前路由定义都是直接使用 Route 组件,并且添加属性来完成的。
  • 但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
  • 在早期的时候,Router 并且没有提供相关的 API,我们需要借助于 react-router-config 完成;
  • 在 Router6.x 中,为我们提供了 useRoutes API 可以完成相关的配置;
<div className="content">{useRoutes(routes)}</div>

类似 vue

路由表:

import Home from "../pages/Home";
import HomeRecommend from "../pages/HomeRecommend";
import HomeRanking from "../pages/HomeRanking";
import HomeSongMenu from "../pages/HomeSongMenu";
// import About from "../pages/About"
// import Login from "../pages/Login"
import Category from "../pages/Category";
import Order from "../pages/Order";
import NotFound from "../pages/NotFound";
import Detail from "../pages/Detail";
import User from "../pages/User";
import { Navigate } from "react-router-dom";
import React from "react";const About = React.lazy(() => import("../pages/About"));
const Login = React.lazy(() => import("../pages/Login"));const routes = [{path: "/",element: <Navigate to="/home" />,},{path: "/home",element: <Home />,children: [{path: "/home",element: <Navigate to="/home/recommend" />,},{path: "/home/recommend",element: <HomeRecommend />,},{path: "/home/ranking",element: <HomeRanking />,},{path: "/home/songmenu",element: <HomeSongMenu />,},],},{path: "/about",element: <About />,},{path: "/login",element: <Login />,},{path: "/category",element: <Category />,},{path: "/order",element: <Order />,},{path: "/detail/:id",element: <Detail />,},{path: "/user",element: <User />,},{path: "*",element: <NotFound />,},
];export default routes;

路由懒加载:

在这里插入图片描述

23.Hooks

  • 总结一下 hooks:
    • 它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性;
    • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决
  • Hook 的使用场景:
  • Hook 的出现基本可以代替我们之前所有使用 class 组件的地方;
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
  • 请记住 Hook 是:
    • 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
    • 100% 向后兼容的:Hook 不包含任何破坏性改动。
    • 现在可用:Hook 已发布于 v16.8.0。

23.1 useState Hook

  • 参数:初始化值,如果不设置为 undefined;

  • 返回值:数组,包含两个元素;

    • 元素一:当前状态的值(第一调用为初始化值);
    • 元素二:设置状态值的函数;
  • useState 的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便

  • 也可以在一个组件中定义多个变量和复杂变量(数组、对象)

  • 使用它们会有两个额外的规则:

    • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
    • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
import React, { memo, useState } from "react";const App = memo(() => {const [message, setMessage] = useState("zimo");function changeMessage() {setMessage("紫陌");}return (<div><h2>App:{message}</h2><button onClick={changeMessage}>修改文本</button></div>);
});export default App;

23.1 useEffect Hook

  • useEffect Hook 可以让你来完成一些类似于 class 中生命周期的功能;
  • 类似于网络请求、手动更新 DOM、一些事件的监听,都是 React 更新 DOM 的一些副作用(Side Effects);
  • 所以对于完成这些功能的 Hook 被称之为 Effect Hook;
  1. useEffect 的解析
  • 通过 useEffect 的 Hook,可以告诉 React 需要在渲染后执行某些操作;
  • useEffect 要求我们传入一个回调函数,在React 执行完更新 DOM 操作之后,就会回调这个函数;
  • 默认情况下,**无论是第一次渲染之后,**还是每次更新之后,都会执行这个 回调函数
  1. 需要清除 Effect
  • 在 class 组件的编写过程中,某些副作用的代码,我们需要在 componentWillUnmount 中进行清除:
  • 比如我们之前的事件总线或 Redux 中手动调用 subscribe;
  • 都需要在 componentWillUnmount 有对应的取消订阅;

为什么要在 Effect 中返回一个函数?

  • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
  • 如此可以将添加和移除订阅的逻辑放在一起;
  • 它们都属于 effect 的一部分;

React 何时清除 effect?

  • React 会在组件更新和卸载的时候执行清除操作;
  • Effect 在每次渲染的时候都会执行;
import React, { memo, useEffect, useState } from "react";const App = memo(() => {const [count, setCount] = useState(0);// 负责告知React,在执行完当前组件渲染之后要执行的副作用代码useEffect(() => {console.log("监听redux中数据变化, 监听eventBus中的zimo事件");// 返回值:回调函数 => 组件被重新渲染或者组件卸载的时候执行return () => {console.log("取消监听redux中数据变化, 取消监听eventBus中的zimo事件");};});return (<div><button onClick={(e) => setCount(count + 1)}>+1({count})</button></div>);
});export default App;
  1. 多个 Effect
  • 使用 Effect Hook,我们可以将它们分离到不同的 useEffect 中:
  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
import React, { memo, useEffect } from "react";
import { useState } from "react";const App = memo(() => {const [count, setCount] = useState(0);// 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码useEffect(() => {// 1.修改document的title(1行)console.log("修改title");});// 一个函数式组件中, 可以存在多个useEffectuseEffect(() => {// 2.对redux中数据变化监听(10行)console.log("监听redux中的数据");return () => {// 取消redux中数据的监听};});useEffect(() => {// 3.监听eventBus中的why事件(15行)console.log("监听eventBus的zimo事件");return () => {// 取消eventBus中的zimo事件监听};});return (<div><button onClick={(e) => setCount(count + 1)}>+1({count})</button></div>);
});export default App;
  1. Effect 性能优化
  • 默认情况下,useEffect 的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
  • 某些代码我们只是希望执行一次即可,类似于 componentDidMount 和 componentWillUnmount 中完成的事情;(比如网 络请求、订阅和取消订阅);
  • 多次执行也会导致一定的性能问题;

useEffect实际上有两个参数:

  • 参数一:执行的回调函数
  • 参数二:该 useEffect 在哪些 state 发生变化时,才重新执行;(受谁的影响)

如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:

  • 这里的两个回调函数分别对应的就是 componentDidMount 和 componentWillUnmount 生命周期函数了;
import React, { memo, useEffect } from "react";
import { useState } from "react";const App = memo(() => {const [count, setCount] = useState(0);const [message, setMessage] = useState("Hello World");useEffect(() => {console.log("修改title:", count);}, [count]);useEffect(() => {console.log("监听redux中的数据");return () => {};}, []);useEffect(() => {console.log("监听eventBus的why事件");return () => {};}, []);useEffect(() => {console.log("发送网络请求, 从服务器获取数据");return () => {console.log("会在组件被卸载时, 才会执行一次");};}, []);return (<div><button onClick={(e) => setCount(count + 1)}>+1({count})</button><button onClick={(e) => setMessage("你好啊")}>修改message({message})</button></div>);
});export default App;

23.3 useContext

在组件中使用共享的 Context 有两种方式

  1. 类组件可以通过 类名.contextType = MyContext 方式,在类中获取 context;
  2. 多个 Context 或者在函数式组件中通过 MyContext.Consumer 方式共享 context;

但是多个 Context 共享时的方式会存在大量的嵌套:

  • Context Hook 允许我们通过 Hook 来直接获取某个 Context 的值;
index.js-------------------------------------------------------------------------------root.render(<UserContext.Provider value={{name: "zimo", level: 99}}><ThemeContext.Provider value={{color:"red",size:18}}><App /></ThemeContext.Provider></UserContext.Provider>
)函数组件中---------------------------------------------------------------------------import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"const App = memo(() => {// 使用Contextconst user = useContext(UserContext)const theme = useContext(ThemeContext)return (<div><h2>User: {user.name}-{user.level}</h2><h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2></div>)
})export default App

当组件上层最近的 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

23.4 useReducer

  • 很多人看到 useReducer 的第一反应应该是 redux 的某个替代品,其实并不是。
  • useReducer 仅仅是 useState 的一种替代方案:
  • 在某些场景下,如果 state 的处理逻辑比较复杂,我们可以通过 useReducer 来对其进行拆分;
  • 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;

注释是 useState 写法

import React, { memo, useReducer } from "react";
// import { useState } from 'react'function reducer(state, action) {switch (action.type) {case "increment":return { ...state, counter: state.counter + 1 };case "decrement":return { ...state, counter: state.counter - 1 };case "add_number":return { ...state, counter: state.counter + action.num };case "sub_number":return { ...state, counter: state.counter - action.num };default:return state;}
}// useReducer+Context => reduxconst App = memo(() => {// const [count, setCount] = useState(0)const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} });// const [counter, setCounter] = useState()// const [friends, setFriends] = useState()// const [user, setUser] = useState()return (<div>{/* <h2>当前计数: {count}</h2><button onClick={e => setCount(count+1)}>+1</button><button onClick={e => setCount(count-1)}>-1</button><button onClick={e => setCount(count+5)}>+5</button><button onClick={e => setCount(count-5)}>-5</button><button onClick={e => setCount(count+100)}>+100</button> */}<h2>当前计数: {state.counter}</h2><button onClick={(e) => dispatch({ type: "increment" })}>+1</button><button onClick={(e) => dispatch({ type: "decrement" })}>-1</button><button onClick={(e) => dispatch({ type: "add_number", num: 5 })}>+5</button><button onClick={(e) => dispatch({ type: "sub_number", num: 5 })}>-5</button><button onClick={(e) => dispatch({ type: "add_number", num: 100 })}>+100</button></div>);
});export default App;

23.5 useCallback

useCallback 实际的目的是为了进行性能的优化

  • useCallback 会返回一个函数的 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • 通常使用 useCallback 的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;
import React, { memo, useState, useCallback, useRef } from "react";// useCallback性能优化的点:
// 1.当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件// props中的属性发生改变时, 组件本身就会被重新渲染
const Home = memo(function (props) {const { increment } = props;console.log("Home被渲染");return (<div><button onClick={increment}>increment+1</button>{/* 100个子组件 */}</div>);
});const App = memo(function () {const [count, setCount] = useState(0);const [message, setMessage] = useState("hello");// 闭包陷阱: useCallback// const increment = useCallback(function foo() {//   console.log("increment")//   setCount(count+1)// }, [count])// 进一步的优化: 当count发生改变时, 也使用同一个函数(了解)// 做法一: 将count依赖移除掉, 缺点: 闭包陷阱// 做法二: useRef, 在组件多次渲染时, 返回的是同一个值const countRef = useRef();countRef.current = count;const increment = useCallback(function foo() {console.log("increment");setCount(countRef.current + 1);}, []);// 普通的函数// const increment = () => {//   setCount(count+1)// }return (<div><h2>计数: {count}</h2><button onClick={increment}>+1</button><Home increment={increment} /><h2>message:{message}</h2><button onClick={(e) => setMessage(Math.random())}>修改message</button></div>);
});export default App;

当需要将一个函数传递给子组件时, 最好使用 useCallback 进行优化, 将优化之后的函数, 传递给子组件!

优化体现在传递子组件时。 父组件重新渲染都会重新定义函数体现不到优化。所以体现在子组件。函数传给子组件,子组件重新渲染不会重新定义父组件的函数。

23.6 useMemo

  • useMemo 实际的目的也是为了进行性能的优化。
    • useMemo 返回的也是一个 memoized(记忆的) 值;
    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { memo } from "react";
import { useMemo, useState } from "react";const HelloWorld = memo(function (props) {console.log("HelloWorld被渲染~");return <h2>Hello World</h2>;
});function calcNumTotal(num) {// console.log("calcNumTotal的计算过程被调用~")let total = 0;for (let i = 1; i <= num; i++) {total += i;}return total;
}const App = memo(() => {const [count, setCount] = useState(0);// const result = calcNumTotal(50)// 1.不依赖任何的值, 进行计算const result = useMemo(() => {return calcNumTotal(50);}, []);// 2.依赖count// const result = useMemo(() => {//   return calcNumTotal(count*2)// }, [count])// 3.使用useMemo对子组件渲染进行优化const info = useMemo(() => ({ name: "why", age: 18 }), []);return (<div><h2>计算结果: {result}</h2><h2>计数器: {count}</h2><button onClick={(e) => setCount(count + 1)}>+1</button><HelloWorld result={result} info={info} /></div>);
});export default App;

useMemo 是给子组件传一个值的。传引用型才有优化 {} []。普通的值没有优化。因为对象和数组会重新声明。

23.7 useRef

  1. 用法一:引入 DOM(或者组件,但是需要是 class 组件)元素
import React, { memo, useRef } from "react";const App = memo(() => {const titleRef = useRef();const inputRef = useRef();function showTitleDom() {console.log(titleRef.current);inputRef.current.focus();}return (<div><h2 ref={titleRef}>Hello,zimo</h2><input type="text" ref={inputRef} /><button onClick={showTitleDom}>查看title的DOM</button></div>);
});export default App;
  1. 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变
import React, { memo, useRef, useState, useCallback } from "react";let obj = null;const App = memo(() => {const [count, setCount] = useState(0);const nameRef = useRef();console.log(obj === nameRef); //trueobj = nameRef;// 通过useRef解决闭包陷阱const countRef = useRef();countRef.current = count;const increment = useCallback(() => {setCount(countRef.current + 1);}, []);return (<div><h2>Hello World: {count}</h2><button onClick={(e) => setCount(count + 1)}>+1</button><button onClick={increment}>+1</button></div>);
});export default App;

useRef 返回一个 ref 对象,返回的 ref 对象再组件的整个生命周期保持不变。

28.8 useImperativeHandle

  • 回顾一下 ref 和 forwardRef 结合使用:
    • 通过 forwardRef 可以将 ref 转发到子组件;
    • 子组件拿到父组件中创建的 ref,绑定到自己的某一个元素中;
  • forwardRef 的做法本身没有什么问题,但是我们是将子组件的 DOM 直接暴露给了父组件:
  • 直接暴露给父组件带来的问题是某些情况的不可控;
  • 父组件可以拿到 DOM 后进行任意的操作;
import React, { memo, useRef, forwardRef, useImperativeHandle } from "react";const HelloWorld = memo(forwardRef((props, ref) => {const inputRef = useRef();// 子组件对父组件传入的ref进行处理useImperativeHandle(ref, () => {return {focus() {console.log("focus");inputRef.current.focus();},setValue(value) {inputRef.current.value = value;},};});return <input type="text" ref={inputRef} />;})
);const App = memo(() => {const titleRef = useRef();const inputRef = useRef();function handleDOM() {// console.log(inputRef.current)inputRef.current.focus();// inputRef.current.value = ""inputRef.current.setValue("哈哈哈");}return (<div><h2 ref={titleRef}>哈哈哈</h2><HelloWorld ref={inputRef} /><button onClick={handleDOM}>DOM操作</button></div>);
});export default App;
  • 上面的案例中,我们只是希望父组件可以操作的 focus,其他并不希望它随意操作;
    • 通过 useImperativeHandle 可以值暴露固定的操作:
    • 通过 useImperativeHandle 的 Hook,将传入的 ref 和 useImperativeHandle 第二个参数返回的对象绑定到了一起;
    • 所以在父组件中,使用 inputRef.current 时,实际上使用的是返回的对象;
    • 比如我调用了 focus 函数,甚至可以调用 printHello 函数;

23.9 useLayoutEffect

  • useLayoutEffect 看起来和 useEffect 非常的相似,事实上他们也只有一点区别而已:
    • useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新;
    • useLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新;
  • 如果我们希望在某些操作发生之后再更新 DOM,那么应该将这个操作放到 useLayoutEffect。
import React, { memo, useEffect, useLayoutEffect, useState } from "react";const App = memo(() => {const [count, setCount] = useState(100);useLayoutEffect(() => {console.log("useLayoutEffect");if (count === 0) {setCount(Math.random() + 99);}});console.log("App render");return (<div><h2>count: {count}</h2><button onClick={(e) => setCount(0)}>设置为0</button></div>);
});export default App;

官方更推荐使用 useEffect 而不是 useLayoutEffect。

23.10 自定义 Hook

获取滚动位置

import { useState, useEffect } from "react";function useScrollPosition() {const [scrollX, setScrollX] = useState(0);const [scrollY, setScrollY] = useState(0);useEffect(() => {function handleScroll() {// console.log(window.scrollX, window.scrollY)setScrollX(window.scrollX);setScrollY(window.scrollY);}window.addEventListener("scroll", handleScroll);return () => {window.removeEventListener("scroll", handleScroll);};}, []);return [scrollX, scrollY];
}export default useScrollPosition;

localStorage 数据存储

import { useEffect, useState } from "react";function useLocalStorage(key) {// 1.从localStorage中获取数据, 并且数据数据创建组件的stateconst [data, setData] = useState(() => {const item = localStorage.getItem(key);if (!item) return "";return JSON.parse(item);});// 2.监听data改变, 一旦发生改变就存储data最新值useEffect(() => {localStorage.setItem(key, JSON.stringify(data));}, [data]);// 3.将data/setData的操作返回给组件, 让组件可以使用和修改值return [data, setData];
}export default useLocalStorage;

23.11 redux hooks(useDispatch useSelector)

  • 在之前的 redux 开发中,为了让组件和 redux 结合起来,我们使用了 react-redux 中的 connect:

    • 但是这种方式必须使用高阶函数结合返回的高阶组件;
    • 并且必须编写:mapStateToProps 和 mapDispatchToProps 映射的函数;
  • 在 Redux7.1 开始,提供了 Hook 的方式,我们再也不需要编写 connect 以及对应的映射函数了

  • useSelector 的作用是将 state 映射到组件中:

  • 参数一:将 state 映射到需要的数据中;

  • 参数二:可以进行比较来决定是否组件重新渲染;

  • useDispatch 非常简单,就是直接获取 dispatch 函数,之后在组件中直接使用即可;

  • 我们还可以通过 useStore 来获取当前的 store 对象;

import React, { memo } from "react";
import { useSelector, useDispatch, shallowEqual } from "react-redux";
import { addNumberAction, changeMessageAction, subNumberAction } from "./store/modules/counter";// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
const Home = memo((props) => {// 1.使用useSelector将redux中store的数据映射到组件内const { message } = useSelector((state) => ({message: state.counter.message,}),shallowEqual);const dispatch = useDispatch();function changeMessageHandle() {// 2.使用dispatch直接派发actiondispatch(changeMessageAction("你好啊, zimo!"));}console.log("Home render");return (<div><h2>Home: {message}</h2><button onClick={(e) => changeMessageHandle()}>修改message</button></div>);
});const App = memo((props) => {// 1.使用useSelector将redux中store的数据映射到组件内const { count } = useSelector((state) => ({count: state.counter.count,}),shallowEqual);// 2.使用dispatch直接派发actionconst dispatch = useDispatch();function addNumberHandle(num, isAdd = true) {if (isAdd) {dispatch(addNumberAction(num));} else {dispatch(subNumberAction(num));}}console.log("App render");return (<div><h2>当前计数: {count}</h2><button onClick={(e) => addNumberHandle(1)}>+1</button><button onClick={(e) => addNumberHandle(6)}>+6</button><button onClick={(e) => addNumberHandle(6, false)}>-6</button><Home /></div>);
});export default App;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/577294.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

test assert-01-Google Truth 断言

Truth Truth 是用于Java测试的断言框架&#xff0c;灵感来自于FEST&#xff0c;并受到一些可扩展性需求的驱动&#xff0c;几乎完全由谷歌员工在业余时间编写&#xff0c;或者作为Java核心图书馆管理员的身份做出贡献。 作用 作为工程师&#xff0c;我们花费大部分的时间来阅…

《C++避坑神器·二十四》简单搞懂json文件的读写之根据键值对读写Json

c11 json解析库nlohmann/json.hpp文件整个代码由一个头文件组成 json.hpp&#xff0c;没有子项目&#xff0c;没有依赖关系&#xff0c;没有复杂的构建系统&#xff0c;使用起来非常方便。 json.hpp库在文章末尾下载 读写主要有两种方式&#xff0c;第一种根据键值对读写&…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Progress进度条组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Progress进度条组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、Progress组件 进度条也是UI开发最常用的组件之一&#xff0c;进度条组件…

OpenAI换血大震动始末:“ChatGPT之父”奥特曼,缘何被“扫地出门”?

近期&#xff0c;AI业界发生了一场“大地震”。作为聊天机器人ChatGPT的开发者&#xff0c;OpenAI无疑是最受关注的人工智能公司之一。就是这家公司的联合创始人、CEO、有“ChatGPT之父”之称的阿尔特曼在“疯狂的5天”里&#xff0c;经历了被闪电免职、加入微软、最终又官复原…

影响嵌入式项目成功的一些要素

选择了嵌入式开发这个技术领域&#xff0c;就相当于选择了一条充满挑战的技术开发道路。 对于工程师而言&#xff0c;项目的成功和失败对他们十分重要。因为一行行代码他们不知道熬了多少个通宵&#xff0c;脑细胞死了多少而写出来的。 如果项目失败了&#xff0c;就意味着辛辛…

移动开发新的风口?Harmony4.0鸿蒙应用开发基础+实践案例

前段时间鸿蒙4.0引发了很多讨论&#xff0c;不少业内人士认为&#xff0c;鸿蒙将与iOS、安卓鼎足而三了。 事实上&#xff0c;从如今手机操作系统竞赛中不难看出&#xff0c;安卓与iOS的形态、功能逐渐趋同化&#xff0c;两大系统互相取长补短&#xff0c;综合性能等差距越来越…

【Element】el-select下拉框实现选中图标并回显图标

一、背景 需求&#xff1a;在下拉框中选择图标&#xff0c;并同时显示图标和文字&#xff0c;以便用户可以直观地选择所需的图标。 二、功能实现 <template><div><el-table ref"table" :data"featureCustom2List" height"200"…

分享71个Java源码总有一个是你想要的

分享71个Java源码总有一个是你想要的 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 链接&#xff1a;https://pan.baidu.com/s/1frK-W3GT8WrydSlQ-E3o6A?pwd6666 提取码&#xff1a;6666 UI代码 def __init__(self):import …

Text2SQL学习整理(五)将Text-to-SQL任务与基本语言模型结合

导语 上篇博客&#xff1a;Text2SQL学习整理&#xff08;四&#xff09;将预训练语言模型引入WikiSQL任务简要介绍了两个借助预训练语言模型BERT来解决WIkiSQL数据集挑战的方法&#xff1a;SQLOVA和X-SQL模型。其中&#xff0c;借助预训练语言模型的强大表示能力&#xff0c;S…

【Gitlab】CICD流水线自动化部署教程

第一步&#xff0c;准备 GitLab 仓库 这个不用多说&#xff0c;得先保证你的项目已经托管在一个 GitLab 仓库中。 第二步&#xff0c;定义 .gitlab-ci.yml 文件 在你的项目根目录中创建一个 .gitlab-ci.yml 文件。这个文件将定义所有 CI/CD 的工作流程&#xff0c;包括构建、测…

C++中的内存锁定

内存锁定(memory locking)是确保进程保留在主内存中并且免于分页的一种方法。在实时环境中&#xff0c;系统必须能够保证将进程锁定在内存中&#xff0c;以减少数据访问、指令获取、进程之间的缓冲区传递等的延迟。锁定内存中进程的地址空间有助于确保应用程序的响应时间满足实…

OCP NVME SSD规范解读-1

OCP&#xff08;Open Compute Project&#xff09;是一个由Facebook于2011年发起的开源项目。其目标是重新设计和优化数据中心的硬件&#xff0c;包括服务器、存储、网络设备等&#xff0c;以提高效率&#xff0c;降低运营成本&#xff0c;并推动技术的创新和标准化。 在OCP中&…

C++ Qt开发:Charts绘制各类图表详解

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍TreeWidget与QCharts的常用方法及灵活运用。 …

部署谷歌的Gemini大模型

前言 本文将介绍如何使用Docker、Docker-Compose私有化部署谷歌的Gemini大模型&#xff0c;以及没有服务器的情况下如何使用Vercel来部署。 Demo: 使用新加坡云服务器部署&#xff1a;Gemini Pro Chat (snowice.eu.org) 使用Vercel部署&#xff1a;Gemini Pro Chat (snowice.eu…

[足式机器人]Part2 Dr. CAN学习笔记-自动控制原理Ch1-5比例积分控制器Proportional-Intefral Controller

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-自动控制原理Ch1-5比例积分控制器Proportional-Intefral Controller 消除稳态误差——设计新的控制器

AspectJ入门(一)

AspectJ是一个面向切面的框架&#xff0c;扩展了Java语言。有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。Spring的AOP底层也是用了这个框架。 AOP可以拦截指定的方法并对方法增强&#xff0c;而且无需侵入到业务代码中&#xff0c;使业务与非业务处理逻辑分离…

机器学习之实验过程01

import pandas as pd import numpy as np import matplotlib.pyplot as plt data_path /home/py/Work/labs/data/SD.csv # 请确保您的数据文件路径是正确的 df pd.read_csv(data_path) df.head() # 创建散点图 # 创建散点图 plt.figure(figsize(10, 6)) plt.scatter…

MySQL 数据库系列课程 05:MySQL命令行工具的配置

一、Windows启动命令行工具 &#xff08;1&#xff09;打开 Windows 的开始菜单&#xff0c;找到安装好的 MySQL&#xff0c;点击MySQL 8.0 Command Line Client - Unicode&#xff0c;这个带有 Unicode 的&#xff0c;是支持中文的&#xff0c;允许在命令行中敲中文。 &…

三网合一建设方案

一、什么是三网融合&#xff1f; 三网合一&#xff08;即三网融合&#xff09;&#xff0c;是指电信网、广播电视网和互联网的相互渗透、互相兼容、并逐步整合成为统一的信息通信网络&#xff0c;其中互联网是核心。只需要引入三个网络中的一个&#xff0c;就能实现电视、互联…

Java架构师系统架构需求分析实战

目录 1 导语2 需求分析实战3 核心方法论-架构立方体4 功能性模型-模块定义5 功能性模型-模块关系图6 功能性模型-模块细化 想学习架构师构建流程请跳转&#xff1a;Java架构师系统架构设计 1 导语 架构设计的实战和思维方法的讨论&#xff0c;主要聚焦于需求分析的重要性和方…