React开发笔记
React.js
一.React依赖
1.开发依赖
- 开发React必须依赖三个库:
- react:包含react所必须的核心代码
- react-dom:react渲染在不同平台所需要的核心代码
- babel:将jsx转换成React代码的工具
- 第一次接触React会被它繁琐的依赖搞蒙,居然依赖这么多东西: (直接放弃?)
- 对于Vue来说,我们只是依赖一个vue.js文件即可,但是react居然要依赖三个包。
- 其实呢,这三个库是各司其职的,目的就是让每一个库只单纯做自己的事情;
- 在React的0.14版本之前是没有react-dom这个概念的,所有功能都包含在react里;
- 为什么要进行拆分呢?原因就是react-native。
- react包中包含了react web和react-native所共同拥有的核心代码。
- react-dom针对web和native所完成的事情不同:
- web端:react-dom会将jsx最终渲染成真实的DOM,显示在浏览器中
- native端:react-dom会将jsx最终渲染成原生的控件(比如Android中的Button,iOS中的UIButton)。
2.Babel和React的关系
- babel是什么呢?
- Babel ,又名 Babel.js。
- 是目前前端使用非常广泛的编译器、转移器。
- 比如当下很多浏览器并不支持ES6的语法,但是确实ES6的语法非常的简洁和方便,我们开发时希望使用它。
- 那么编写源码时我们就可以使用ES6来编写,之后通过Babel工具,将ES6转成大多数浏览器都支持的ES5的语法。
- 会启用严格模式!!!
- React和Babel的关系:
- 默认情况下开发React其实可以不使用babel。
- 但是前提是我们自己使用 React.createElement 来编写源代码,它编写的代码非常的繁琐和可读性差。
- 那么我们就可以直接编写jsx(JavaScript XML)的语法,并且让babel帮助我们转换成React.createElement。
3.依赖引入
-
三种方法
-
方式一:直接CDN引入
-
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
-
-
方式二:下载后,添加本地依赖
-
方式三:通过npm管理(后续脚手架再使用)
-
4.Demo
<body>
<div id="root"></div>
<!-- 添加依赖 -->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<!-- babel -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
// React 代码
// 渲染 hello world
// React18 之前
ReactDOM.render(<h2>Hello World</h2>, document.querySelector('#root'))
// React18 之后
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<h2> Hello World </h2>)
const app = ReactDOM.createRoot(document.querySelector('#app'))
app.render(<h2> 你好啊 </h2>)
</script>
<body>
<div id="root"></div>
<!-- 添加依赖 -->
<script src="../lib/react.js"></script>
<script src="../lib/react-dom.js"></script>
<!-- babel -->
<script src="../lib/babel.js"></script>
<script type="text/babel">
const root = ReactDOM.createRoot(document.querySelector('#root'))
// 1.文本
let message = "Hello World"
// 2.监听按钮点击
function btnClick() {
// 2.1 修改数据
message = 'Hello React!!!'
// 2.2 重新渲染
rootReander()
}
rootReander()
function rootReander() {
root.render((
<div>
<h2>{message}</h2>
<button onClick={btnClick}>修改文本</button>
</div>
))
}
</script>
</body>
5.组件化思想
-
在React中,如何封装一个组件呢?这里我们暂时使用类的方式封装组件:
- 1.定义一个类(类名大写,组件的名称是必须大写的,小写会被认为是HTML元素),继承自React.Component
- 2.实现当前组件的render函数
- render当中返回的jsx内容,就是之后React会帮助我们渲染的内容
-
数据依赖
-
两类
-
**参与界面更新的数据:**当数据变量时,需要更新组件渲染的内容;
参与界面更新的数据我们也可以称之为是参与数据流,这个数据是定义在当前对象的state中
// 1.类组件 class App extends React.Component { // 组件数据 constructor() { super() this.state = { message: 'Hello World' } } // 组件方法 // 渲染内容 render 方法() render() { return ( <div> <h2>{this.state.message}</h2> </div> ) } }
-
**不参与界面更新的数据:**当数据变量时,不需要更新将组建渲染的内容;
-
-
-
事件绑定
-
this绑定
-
this绑定错误
this绑定丢失了
class App extends React.Component { // 组件数据 constructor() { super() this.state = { message: 'Hello World' } } // 组件方法 (实例方法) btnCilck() { this.setState({ message: 'Hello React' }) } // 渲染内容 render 方法() render() { return ( <div> <h2>{this.state.message}</h2> <button onClick={this.btnCilck}>修改文本</button> </div> ) } }
-
this绑定正确
使用显示绑定(bind硬绑定)
class App extends React.Component { // 组件数据 constructor() { super() this.state = { message: 'Hello World' } } // 组件方法 (实例方法) btnCilck() { this.setState({ message: 'Hello React' }) } // 渲染内容 render 方法() render() { return ( <div> <h2>{this.state.message}</h2> <button onClick={this.btnCilck.bind(this)}>修改文本</button> </div> ) } }
使用箭头函数
class App extends React.Component { // 组件数据 constructor() { super() this.state = { message: 'Hello World' } } // 组件方法 (实例方法) btnCilck() { this.setState({ message: 'Hello React' }) } // 渲染内容 render 方法() render = () => { return ( <div> <h2>{this.state.message}</h2> <button onClick={this.btnCilck}>修改文本</button> </div> ) } }
-
-
6.VSCode代码片段
- 具体的步骤如下:
- 第一步,复制自己需要生成代码片段的代码;
- 第二步,https://snippet-generator.app/在该网站中生成代码片段;
- 第三步,在VSCode中配置代码片段;
二.JSX语法
1.关于JSX
-
JSX是什么?
- JSX是一种JavaScript的语法扩展(eXtension),也在很多地方称之为JavaScript XML,因为看起就是一段XML语法;
- 它用于描述我们的UI界面,并且其完成可以和JavaScript融合在一起使用;
- 它不同于Vue中的模块语法,你不需要专门学习模块语法中的一些指令(比如v-for、v-if、v-else、v-bind);
-
React认为渲染逻辑本质上与其他UI逻辑存在内在耦合
-
比如UI需要绑定事件(button、a原生等等);
-
比如UI中需要展示数据状态;
-
比如在某些状态发生改变时,又需要改变UI;
他们之间是密不可分,所以React没有将标记分离到不同的文件中,而是将它们组合到了一起,这个地方就是组件(Component);
-
2.JSX的基本使用
-
JSX书写规范
-
JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素(或者使用后面我们学习的Fragment);
return <div><div>1</div><div>2</div></div>
-
为了方便阅读,我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读,并且jsx可以进行换行书写;
return ( <div> <div>1</div> <div>2</div> </div> )
-
JSX中的标签可以是单标签,也可以是双标签;
注意:如果是单标签,必须以/>结尾;
return <img/>
-
-
注释
// JS的注释 return ( <div> { /* JSX注释写法 */ } <div>1</div> <div>2</div> </div> )
-
JSX嵌入变量作为子元素
-
情况一:当变量是Number、String、Array类型时,可以直接显示
return ( <div> {[1, 2, 3, 4, 5]} {'abc'} {12345} </div> )
-
情况二:当变量是null、undefined、Boolean类型时,内容为空;
-
如果希望可以显示null、undefined、Boolean,那么需要转成字符串;
return ( <div> {true} {undefined} {null} {String(true)} {String(undefined)} {String(null)} </div> )
-
转换的方式有很多,比如toString方法、和空字符串拼接,String(变量)等方式;
-
-
情况三:Object对象类型不能作为子元素(not valid as a React child)
return ( <div> {{ a: 'a', b: 'b' }} </div> )
-
情况四:插入JS表达式
- 三元运算符
- 运算表达式
- 执行一个函数
return ( <div> {true ? 'aaa' : 'bbb'} {'aaa' + 'bbb'} <ul> {(function () { return ['你的名字', '天气之子', '铃芽之旅'].map(item => <li>{item}</li>) })()} </ul> </div> )
-
-
JSX绑定属性
- 比如元素都会有title属性
- 比如img元素会有src属性
- 比如a元素会有href属性
- 比如元素可能需要绑定class
- 比如原生使用内联样式style
<div> {/* 1.基本属性绑定 */} <h2 title={'title'}>我是H2</h2> <img src={'https://ts2.cn.mm.bing.net/th?id=ABT41970E1FA57D5BEDCBDC69137981DC1A46641F152D0709D305CD52C561AF5250&w=120&h=120&c=1&rs=1&qlt=80&o=6&dpr=1.5&pid=SANGAM'} /> <a href={'baidu.com'}>百度一下</a> {/* 2.class属性 : 最好使用className*/} <div class="abc">1</div> <div className="abc">2</div> <div className={`abc ${'ss' ? 'active' : ''}`}></div> <div className={['abc', 'cba'].join(' ')}>3</div> {/* 3.使用第三方库 classnames*/} {/* 4.动态绑定style*/} <h2 style={{ color: "red", fontSize: '100px' }}> 动态style </h2> </div>
3.事件绑定
-
通过{}传入一个事件处理函数,这个函数会在事件发生时被执行;
命名采用小驼峰式(camelCase),而不是纯小写;
return <button onClick={this.btnCilck}>修改文本</button>
-
重点:注意this绑定问题
-
方案一:bind给btnClick显示绑定this
-
方案二:使用 ES6 class fields 语法
class App extends React.Component { constructor() { super() this.state = { } } foo = () => { console.log(this); } render() { return ( <div> <button onClick={this.foo}>惦记我</button> </div> ) } }
-
方案三:事件监听时传入箭头函数(个人推荐)
class App extends React.Component { constructor() { super() this.state = { } } foo() { console.log(this); } render() { return ( <div> <button onClick={() => this.foo()}>惦记我</button> </div> ) } }
-
-
-
参数传递
- 情况一:获取event对象
- 很多时候我们需要拿到event对象来做一些事情(比如阻止默认行为)
- 那么默认情况下,event对象有被直接传入,函数就可以获取到event对象;
- 情况二:获取更多参数
- 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数;
class App extends React.Component { constructor() { super() this.state = { } } foo() { console.log(this); } foo1(e, ...args) { console.log(e); console.log(args); } render() { return ( <div> <div> <button onClick={() => this.foo()}>惦记我</button> </div> <div> {/* 1.event传递 */} <button onClick={this.foo1.bind(this)}>bind_传递event</button> <button onClick={(e) => this.foo1(e)}>箭头函数_传递event</button> {/* 2.剩余参数传递,注意bind函数在函数柯里化上的应用! */} <button onClick={this.foo1.bind(this, 1)}>bind_剩余参数传递</button> <button onClick={(e) => this.foo1(e, 1)}>箭头函数_剩余参数传递</button> </div> </div > ) } }
- 情况一:获取event对象
4.条件渲染
在React中,所有的条件判断都和普通的JavaScript代码一致;
具体方法:
-
方式一:条件判断语句
- 适合逻辑较多的情况
-
方式二:三元运算符
- 适合逻辑比较简单
-
方式三:与运算符&&
- 适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染;
class App extends React.Component { constructor() { super() this.state = { status: false, userInfo: { name: 'ming', age: 19 } } } render() { const { status, userInfo } = this.state let showElement = null if (!status) { showElement = <h2>准备!</h2> } else showElement = <h2>开始!</h2> return ( <div> {/* 1.使用:条件判断 */} <div>{showElement}</div> {/* 2.使用:三元运算符 */} <div>{status ? <h2>开始!</h2> : <h2>准备!</h2>}</div> {/* 3.使用:逻辑于&& */} {/* 当某个值可能为空时,则用这个方法 */} <div>{userInfo && <div>{userInfo.name + '今年' + userInfo.age + '岁'}</div>}</div> </div> ) } }
-
方式四:
undefined?.undefined
可选链操作符 -
v-show的效果
- 主要是控制display属性是否为none
5.列表渲染
在React中并没有像Vue模块语法中的v-for指令,而且需要我们通过JavaScript代码的方式组织数据,转成JSX:
- React中的JSX正是因为和JavaScript无缝的衔接,让它可以更加的灵活;
- React是真正可以提高我们编写代码能力的一种方式;
如何展示:
-
用数组的map高阶函数
return ( <ul> {(function () { return ['你的名字', '天气之子', '铃芽之旅'].map((item,index) => <li key={index}>{item}</li>) })()} </ul> )
处理方法:
- 比如过滤掉一些内容:filter函数
- 比如截取数组中的一部分内容:slice函数
key值绑定:
提高tiff算法效率,微信小程序、Vue中均有该做法
6.JSX的原理和本质:createElement()
实际上,jsx 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖,所有的jsx最终都会被转换成React.createElement的函数调用。
6.1React.createElement(component, props, ...children)
参数:
- tyep
- 当前ReactElement的类型;
- 如果是标签元素,那么就使用字符串表示 “div”;
- 如果是组件元素,那么就直接使用组件的名称;
- config
- 所有jsx中的属性都在config中以对象的属性和值的形式存储;
- 比如传入className作为元素的class;
- children
- 存放在标签中的内容,以children数组的方式进行存储;
- 当然,如果是多个元素呢?React内部有对它们进行处理,处理的源码在下方
6.2关于Babel:
- 我们知道默认jsx是通过babel帮我们进行语法转换的,所以我们之前写的jsx代码都需要依赖babel。
- 可以在babel的官网中快速查看转换的过程:https://babeljs.io/repl/#?presets=react
6.3直接编写JSX代码:
<body>
<div id="root"></div>
<!-- 添加依赖 -->
<script src="../lib/react.js"></script>
<script src="../lib/react-dom.js"></script>
<!-- babel -->
<!-- <script src="../lib/babel.js"></script> -->
<script>
class App extends React.Component {
constructor() {
super()
this.state = {
}
}
render() {
const element = React.createElement(
"div",
null,
React.createElement(
"div",
{
className: "header"
},
"Header"
),
React.createElement(
"div",
{
className: "content"
},
React.createElement("h1", null, "Banner"),
React.createElement(
"ul",
null,
React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E1"
),
React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E2"
),
React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E3"
),
React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E4"
),
React.createElement("li", null, "\u5217\u8868\u6570\u636E5")
)
),
React.createElement(
"div",
{
className: "footer"
},
"Footer"
)
);
return element
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(React.createElement(App, null))
</script>
</body>
6.4虚拟DOM的创建过程
- 我们通过 React.createElement 最终创建出来一个 ReactElement对象
- React利用ReactElement对象组成了一个JavaScript的对象树
- JavaScript的对象树就是虚拟DOM(Virtual DOM)
- ReactElement的树结构
- 而ReactElement最终形成的树结构就是Virtual DOM;
6.5声明式编程
虚拟DOM帮助我们从命令式编程转到了声明式编程的模式。
- React官方的说法:Virtual DOM 是一种编程理念。
- 在这个理念中,UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象
- 我们可以通过ReactDOM.render让 虚拟DOM 和 真实DOM同步起来,这个过程中叫做协调(Reconciliation);
- 这种编程的方式赋予了React声明式的API:
- 你只需要告诉React希望让UI是什么状态;
- React来确保DOM和这些状态是匹配的;
- 你不需要直接进行DOM操作,就可以从手动更改DOM、属性操作、事件处理中解放出来;
三.React脚手架——creat-react-app
1.创建React项目
- node 环境的安装
npm i create-react-app -g
全局安装脚手架create-react-app 项目名称
创建项目- 项目名称不能包含大写字母
- 另外还有更多创建项目的方式,可以参考GitHub的readme
2.目录结构
- PWA概念
- PWA全称Progressive Web App,即渐进式WEB应用;
- 一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用;
- 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能;
- 这种Web存在的形式,我们也称之为是 Web App;
- PWA解决了哪些问题呢?
- 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏;
- 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能;
- 实现了消息推送;
- 等等一系列类似于Native App相关的功能;
- https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps
3.Webpack相关
React脚手架默认是基于Webpack来开发的,我们并没有在目录结构中看到任何webpack相关的内容,原因是React脚手架将webpack相关的配置隐藏起来了(其实从Vue CLI3开始,也是进行了隐藏)。
查看webpack配置信息:
-
执行
npm eject
package.json文件中有写到:"eject": "react-scripts eject",这个操作是不可逆的,所以在执行过程中会给与我们提示;
-
使用
create-react-app config
4.开始编写
-
将无用的文件进行删除
- 将src下的所有文件都删除
- 将public文件下出列favicon.ico和index.html之外的文件都删除掉
-
在src目录下,创建一个index.js文件,因为这是webpack打包的入口。
-
在index.js中开始编写React代码:
// 编写React代码,并且通过React渲染对应的内容 // 18 之前 // import { ReactDOM } from 'react' // react 18 引入 import ReactDOM from 'react-dom/client' const root = ReactDOM.createRoot(document.querySelector('#root')) root.render(<h2>哈哈</h2>)
-
如果我们不希望直接在 root.render 中编写过多的代码,就可以单独抽取一个组件App.js:
index.js
// 编写React代码,并且通过React渲染对应的内容 import ReactDOM from 'react-dom/client' import App from './App' const root = ReactDOM.createRoot(document.querySelector('#root')) root.render(<App />)
App.jsx
import React from "react" class App extends React.Component { constructor() { super() this.state = { } } render() { return ( <div> <div> { /* JSX注释写法 */} <div>1</div> <div>2</div> </div> <div> {[1, 2, 3, 4, 5]} {'abc'} {12345} </div> <div> {true} {undefined} {null} {String(true)} {String(undefined)} {String(null)} </div> <div> {true ? 'aaa' : 'bbb'} {'aaa' + 'bbb'} <ul> {(function () { return ['你的名字', '天气之子', '铃芽之旅'].map(item => <li>{item}</li>) })()} </ul> </div> <div> {/* 1.基本属性绑定 */} <h2 title={'title'}>我是H2</h2> <img src={'https://ts2.cn.mm.bing.net/th?id=ABT41970E1FA57D5BEDCBDC69137981DC1A46641F152D0709D305CD52C561AF5250&w=120&h=120&c=1&rs=1&qlt=80&o=6&dpr=1.5&pid=SANGAM'} /> <a href={'baidu.com'}>百度一下</a> {/* 2.class属性 : 最好使用className*/} <div class="abc">1</div> <div className="abc">2</div> <div className={`abc ${'ss' ? 'active' : ''}`}></div> <div className={['abc', 'cba'].join(' ')}>3</div> {/* 3.使用第三方库 classnames*/} {/* 4.动态绑定style*/} <h2 style={{ color: "red", fontSize: '100px' }}> 动态style </h2> </div> </div > ) } } export default App
四.组件化开发——基础
1.React中的组件化
组件的分类:
- 根据组件的定义方式
- 函数组件
- 关注UI的展示
- 类组件
- 关注数据逻辑
- 函数组件
- 根据组件内部是否有状态需要维护
- 无状态组件
- 关注UI的展示
- 有状态组件
- 关注数据逻辑
- 无状态组件
- 根据组件的不同职责
- 展示型组件
- 关注UI的展示
- 容器型组件
- 关注数据逻辑
- 展示型组件
1.1类组件
在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义。
-
定义要求
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component
- 类组件必须实现render函数
import { Component } from 'react' // 1.类组件 class App extends Component { constructor () { super() this.state ={ message: "App Component" } } render() { const {message } = this.state return <h2>{message}</h2> } } export default App
-
使用class定义一个组件
-
constructor是可选的,我们通常在constructor中初始化一些数据;
-
this.state中维护的就是我们组件内部的数据;
-
render() 方法是 class 组件中唯一必须实现的方法;
当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React元素
- 通常通过 JSX 创建。
- 例如,
<div />
会被 React 渲染为 DOM 节点,<MyComponent />
会被 React 渲染为自定义组件; - 无论是
<div />
还是<MyComponent />
均为 React 元素。
- 数组或 fragments:使得 render 方法可以返回多个元素。
- Portals:可以渲染子节点到不同的 DOM 子树中。
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型或 null:什么都不渲染
render() { // const {message } = this.state // 1.react元素:通过jsx编写的代码就会被编译成React.createElement,所以就是一个React元素 return <h2>{message}</h2> // 2.组件或者fragments return [ <h1>h1</h1>, <h2>h2</h2>, <h3>h3</h3> ] // 3.字符串/数值类型 return 123 return 'hello world' // 4.Boolean、undefined、null }
- React元素
-
1.2函数组件
函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。
-
函数组件的特点(先忽略hooks)
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数
- this关键字不能指向组件实例(因为没有组件实例)
- 没有内部状态(state)
-
定义一个函数组件
hooks另外说
// 函数式组件 function App() { // 返回值:和类组件中的render函数返回的一样 return <h1>App Function Components</h1> } export default App
2.生命周期
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
- 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;
- 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
- 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 比如卸载过程(Unmount),组件从DOM树中被移除的过程;
- React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
- 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;
- 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;
- 比如实现componentWillUnmount函数:组件即将被移除时,就会回调;
- 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
- 我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)
子组件
import { Component } from "react";
class HelloWorld extends Component {
constructor() {
super()
// 1.先执行构造函数
console.log("HelloWorld: 1.执行constructor构造函数");
this.state = {
message: "hello world"
}
}
changeTesxt() {
this.setState(
{message:"你好"}
)
}
render() {
const {message} = this.state
// 2.执行render函数
console.log("HelloWorld: 2.执行render函数");
return (
<div>
<h2>{message}</h2>
<button onClick={e=>this.changeTesxt()}>修改文本</button>
</div>
)
}
componentDidMount() {
// 3.执行componentDidMount生命周期函数
console.log("HelloWorld: 3.执行componentDidMount生命周期函数");
}
componentDidUpdate() {
// 4.执行componentDidUpdate生命周期函数
console.log("HelloWorld: 4.执行componentDidUpdate生命周期函数");
}
componentWillUnmount() {
// 5.执行componentWillUnmount生命周期函数
console.log("HelloWorld: 5.执行componentWillUnmount生命周期函数");
}
}
export default HelloWorld
父组件
import { Component } from "react";
import HelloWorld from "./Hello World";
class App extends Component {
constructor() {
super()
this.state = {
isShowHW: true
}
}
switchHWShow() {
this.setState({
isShowHW: false
})
}
render() {
return (
<div>
<h1>App</h1>
<button onClick={e=> this.switchHWShow() }>切换</button>
{this.state.isShowHW && <HelloWorld/>}
</div>
)
}
}
export default App
具体的生命周期函数:
-
Constructor
- 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
-
constructor中通常只做两件事情:
- 通过给 this.state 赋值对象来初始化内部的state;
- 为事件绑定实例(this);
-
componentDidMount
componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。
- componentDidMount中通常进行哪里操作呢?
- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求就最好的地方;(官方建议)
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);
- componentDidMount中通常进行哪里操作呢?
-
componentDidUpdate
componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
-
componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。
- 在此方法中执行必要的清理操作:
- 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
- 在此方法中执行必要的清理操作:
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
-
getDerivedStateFromProps:state 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state;
-
getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
-
class HelloWorld extends Component { constructor() { super() // 1.先执行构造函数 console.log("HelloWorld: 1.执行constructor构造函数"); this.state = { message: "hello world" } } changeTesxt() { this.setState( {message:"你好"} ) } render() { const {message} = this.state // 2.执行render函数 console.log("HelloWorld: 2.执行render函数"); return ( <div> <h2>{message}</h2> <button onClick={e=>this.changeTesxt()}>修改文本</button> </div> ) } componentDidMount() { // 3.执行componentDidMount生命周期函数 console.log("HelloWorld: 3.执行componentDidMount生命周期函数"); } componentDidUpdate(prevProps,prevState,snapshot) { // 4.执行componentDidUpdate生命周期函数 console.log("HelloWorld: 4.执行componentDidUpdate生命周期函数",prevProps,prevState,snapshot); } componentWillUnmount() { // 5.执行componentWillUnmount生命周期函数 console.log("HelloWorld: 5.执行componentWillUnmount生命周期函数"); } // 3.5 getSnapshotBeforeUpdate() { console.log("getSnapshotBeforeUpdate"); return { scrollPosition: 1000 } } }
-
-
shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;
-
另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。
-
更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html
3.组件通信
组件的嵌套使得组件间的通信尤为重要
3.1父传子
TS支持:
import React, { ReactNode, memo } from 'react'
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined
| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[
keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES
];
interface IProps {
children?: ReactNode
name: string
}
const Download: React.FC<IProps> = (props) => {
return (
<div>
<div>name: {props.name}</div>
</div>
)
}
export default memo(Download)
import { PureComponent, ReactNode } from 'react'
interface IProps {
name: string
age?: number
}
interface IState {
message: string
}
interface ISnapshot {
counter: number
}
class Demo extends PureComponent<IProps, IState, ISnapshot> {
state = {
message: 'hello'
}
// constructor(props: IProps) {
// super(props)
// // this.state = {
// // message: 'hello'
// // }
// }
getSnapshotBeforeUpdate(): ISnapshot | null {
return {
counter: 100
}
}
componentDidUpdate(
prevProps: Readonly<IProps>,
prevState: Readonly<IState>,
snapshot?: ISnapshot | undefined
): void {
console.log(prevProps, prevState, snapshot)
}
render(): ReactNode {
return (
<div>
<div>{this.props.name}</div>
<div>{this.state.message}</div>
</div>
)
}
}
export default Demo
示例:
-
父组件通过 属性 = 值 的形式来传递给子组件数据;
父组件
import React, { Component } from 'react' import MainBanner from './MainBanner' import MainProductList from './MainProductList' export class Main extends Component { constructor() { super() this.state = { banners: ["歌曲","MV","歌单"], productList: ["推荐","流行","热门"] } } render() { const {banners,productList} = this.state return ( <div> <MainBanner banners={banners} title="轮播图"/> <MainProductList productList={productList}/> </div> ) } } export default Main
-
子组件通过 props 参数获取父组件传递过来的数据;
子组件
import React, { Component } from 'react' export class MainProductList extends Component { render() { return ( <div> <h2>商品列表</h2> <ul> { this.props.productList.map(item=>{ return <li key={item}>{item}</li> }) } </ul> </div> ) } } export default MainProductList
import React, { Component } from 'react' export class MainBanner extends Component { constructor(props) { super(props) } render() { const {title,banners} = this.props return ( <div> <h2>轮播图名称:{title}</h2> <ul> { banners.map(item=>{ return <li key={item}>{item}</li> }) } </ul> </div> ) } } export default MainBanner
-
proTypes
- 对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:
- 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证;
- 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;
- 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
import React, { Component } from 'react' import PropTypes from 'prop-types' export class MainBanner extends Component { // es2022起可以这样写 static defaultProps ={ banners: ['1','2','3'], title: '默认标题' } render() { const {title,banners} = this.props return ( <div> <h2>轮播图名称:{title}</h2> <ul> { banners.map(item=>{ return <li key={item}>{item}</li> }) } </ul> </div> ) } } MainBanner.propTypes = { banners: PropTypes.array.isRequired, title: PropTypes.string } // 默认值的写法 MainBanner.defaultProps = { banners: ['1','2','3'], title: '默认标题' } export default MainBanner
- 对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:
-
更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
- 比如验证数组,并且数组中包含哪些元素;
- 比如验证对象,并且对象中包含哪些key以及value是什么类型;
- 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
3.2子传父
React中只存在单向数据流!
- vue中通过自定义事件来传递
- 在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
父组件
import React, { Component } from 'react'
import AddCounter from './AddCounter'
export class App extends Component {
constructor() {
super()
this.state = {
counter: 100
}
}
render() {
const {counter} = this.state
return (
<div>
<h2>当前计算: {counter}</h2>
<AddCounter addClick={(addNum)=>{this.setState({
counter: counter+ addNum
})}}></AddCounter>
</div>
)
}
}
export default App
子组件
import React, { Component } from 'react'
export class AddCounter extends Component {
addCount(count) {
this.props.addClick(count)
}
render() {
return (
<div>
<button onClick={e=>this.addCount(1)}>+1</button>
<button onClick={e=>this.addCount(5)}>+5</button>
<button onClick={e=>this.addCount(10)}>+10</button>
</div>
)
}
}
export default AddCounter
4.插槽
React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
-
组件的children子元素;
弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
父组件
import React, { Component } from 'react' import NavBar from './NavBar' export class App extends Component { render() { return ( <div> <NavBar> <button>按钮</button> <h2>我是标题</h2> <i>斜体文字</i> </NavBar> </div> ) } } export default App
子组件
import React, { Component } from 'react' import './style.css' 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> ) } } export default NavBar
这里传入多个的情况下就是数组,如果只有传入单个
children
的话,就直接是children
本身// 限制只能传入一个 NavBar.propTypes = { children: PropTypes.element } // 限制只能传入数组 NavBar.propTypes = { children: PropTypes.array }
-
props属性传递React元素;
父组件
import React, { Component } from 'react' import NavBar2 from './NavBar2' export class App extends Component { render() { return ( <div> <NavBar2 leftSlot={<button>按钮2</button>} centerSlot={<h2>我是标题2</h2>} rightSlot={<i>斜体文字2</i>}> </NavBar2> </div> ) } } export default App
子组件
import React, { Component } from 'react' export class NavBar2 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> ) } } export default NavBar2
-
作用域插槽
// 父组件 <div> <NavBar2 leftSlot={<button>按钮2</button>} centerSlot={<h2>我是标题2</h2>} rightSlot={item => <i>item</i>}> </NavBar2> </div> // 子组件 <div className='nav-bar'> <div className="left">{leftSlot}</div> <div className="center">{centerSlot}</div> <div className="right">{rightSlot(斜体文字2)}</div> </div>
5.非父子的通信
某些场景的数据需要共享,而使用一层层传递的方法非常麻烦,代码冗余
-
逐层props传递
可以使用
{...}
进行对象展开
export class App extends Component {
constructor() {
super()
this.state = {
info: {name:"kobe",age:30}
}
}
render() {
const {info} = this.state
return (
<div>
<Home name={this.state.info.name} age={this.state.info.age}></Home>
{/* 对象展开 */}
<Home {...info}></Home>
</div>
)
}
}
5.1Context
-
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
-
Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;
-
新起一个文件夹
context/theme-context/index.js
import React from 'react' // 1.创建一个Context const ThemeContext = React.createContext() export default ThemeContext
-
祖先元素进行处理
import ThemeContext from './theme-context.js' export class App extends Component { constructor() { super() this.state = { info: {name:"kobe",age:30} } } render() { const {info} = this.state return ( <div> <ThemeContext.Provider value={{color:'red',size:30}}> <Home {...info}/> </ThemeContext.Provider> </div> ) } } export default App
-
子组件进行接收
类组件
import React, { Component } from 'react' import ThemeContext from './theme-context.js' export class HomeInfo extends Component { render() { return ( <div>HomeInfo:{this.context.color}</div> ) } } // 3.对组件的contextType为某一个context HomeInfo.contextType = ThemeContext export default HomeInfo
函数组件
import ThemeContext from "./theme-context.js" function HomeBanner() { return <div> {/* 函数式组件中使用Context共享的数据 */} <ThemeContext.Consumer> { value => { return <h2>Bannertheme:{value.color}</h2> } } </ThemeContext.Consumer> </div> } export default HomeBanner
多个Context的用法:
-
新建文件
context/user-context/index.js
import React from 'react' const UserContext = React.createContext() export default UserContext
-
祖先组件
import React, { Component } from 'react' import Home from './Home' // // 1.创建一个Context // const ThemeContext = React.createContext() import HomeBanner from './HomeBanner' import ThemeContext from './theme-context.js' import UserContext from './user-context' export class App extends Component { constructor() { super() this.state = { info: {name:"kobe",age:30} } } render() { const {info} = this.state return ( <div> <UserContext.Provider value={{nickname:'kobe',age: 30}}> <ThemeContext.Provider value={{color:'red',size:30}}> <Home {...info}/> <HomeBanner/> </ThemeContext.Provider> </UserContext.Provider> </div> ) } } export default App
-
子组件
import React, { Component } from 'react' import ThemeContext from './theme-context.js' import UserContext from './user-context/index.js' export class HomeInfo extends Component { render() { return ( <div> <h2>HomeInfo:{this.context.color}</h2> <UserContext.Consumer> { value => { return <h2>Info User: {value.nickname}</h2> } } </UserContext.Consumer> </div> ) } } // 3.对组件的contextType为某一个context HomeInfo.contextType = ThemeContext export default HomeInfo
默认defaultValue:
-
user-context/index.js
import React from 'react' const UserContext = React.createContext({ nickname: 'blue' }) export default UserContext
-
祖先组件
import React, { Component } from 'react' import Home from './Home' // // 1.创建一个Context // const ThemeContext = React.createContext() import HomeBanner from './HomeBanner' import ThemeContext from './theme-context.js' import UserContext from './user-context' import Profile from './Profile' export class App extends Component { constructor() { super() this.state = { info: {name:"kobe",age:30} } } render() { const {info} = this.state return ( <div> <UserContext.Provider value={{nickname:'kobe',age: 30}}> <ThemeContext.Provider value={{color:'red',size:30}}> <Home {...info}/> <HomeBanner/> </ThemeContext.Provider> </UserContext.Provider> <Profile/> </div> ) } } export default App
-
子组件
Profile
import React, { Component } from 'react' import UserContext from './user-context' export class Profile extends Component { render() { console.log(this.context); return ( <div>Profile</div> ) } } Profile.contextType = UserContext export default Profile
-
API
React.createContext
- 创建一个需要共享的Context对象:
- 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
Context.Provider
- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
- Provider 接收一个 value 属性,传递给消费组件;
- 一个 Provider 可以和多个消费组件有对应关系;
- 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
- 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
Class.contextType
- 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
- 这能让你使用 this.context 来消费最近 Context 上的那个值;
- 你可以在任何生命周期中访问到它,包括 render 函数中;
Context.Consumer
- 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
- 这里需要 函数作为子元素(function as child)这种做法;
- 这个函数接收当前的 context 值,返回一个 React 节点;
5.2 Event Bus
可自己书写或者使用第三方的库
6.setState的使用详解
6.1本质
为什么使用setState?
- 开发中我们并不能直接通过修改 state的值 来让界面发生更新:
-
因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
-
React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
-
我们必须通过setState来告知React数据已经发生了变化;
setState 通过继承得来(类组件)
this.setState 的本质是使用
Object.assign(this.state,newState)
将原有对象进行合并的操作(并非替换),然后再合适的时候进行render()
-
6.2用法
- 三种不同的用法
export class App extends Component {
constructor(props) {
super(props)
this.state = {
message: 'Hello World',
counter: 0
}
}
changeText() {
// setState更多用法
// 1.基本使用
this.setState({
message: 'aaa'
})
// 2.setState可以传入一个回调函数
// 好处一:可以在回调函数中编写新的state的逻辑
// 好处二:当前的回调函数会将之前的state和props传递进来
this.setState((state,props) => {
// 1.编写一些对新的state处理逻辑
// 2.可以获取之前的state和props值
return {
message: 'aaa'
}
})
// 3.setState在React中的事件处理是一个异步调用
// 如果希望在数据更新之和(数据合并),获取到对于的结果执行一些逻辑代码
// 那么可以在setState中传入第二个参数: callback
this.setState({
message: 'bbb'
},()=>{
console.log("----",this.state.message); // 打印的是bbb
})
console.log("----",this.state.message); // 打印的依然是 hello world
}
increment() {
}
render() {
const {message,counter} = this.state
return (
<div>
<h2>message: {message}</h2>
<button onClick={e=>this.changeText()}>修改message</button>
<h2>当前计数:{counter}</h2>
<button onClick={e => this.increment()}>counter+1</button>
</div>
)
}
}
6.3 setState异步
setState异步更新:
-
为什么setState设计为异步呢?
- setState设计为异步其实之前在GitHub上也有很多的讨论;
- React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的同学可以参考一下;
- https://github.com/facebook/react/issues/11527#issuecomment-360199710;
-
setState设计为异步,可以显著的提升性能;
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
- 最好的办法应该是获取到多个更新,之后进行批量更新;
increment() { console.log('-------'); this.setState({ counter: this.state.counter + 1 // 实际上是 0 + 1 }) this.setState({ counter: this.state.counter + 1 // 实际上也是 0 + 1 }) this.setState({ counter: this.state.counter + 1 // 实际上还是 0 + 1 }) // --------------------------------------------------- this.setState((state) => { console.log(this.state.counter); // 0 return { counter: state.counter + 1 // 0+1 } }) this.setState((state) => { console.log(this.state.counter); // 也是 0 return { counter: state.counter + 1 // 1+1 } }) this.setState((state) => { console.log(this.state.counter); // 还是 0 return { counter: state.counter + 1 // 2+1 } }) }
-
如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
-
state和props不能保持一致性,会在开发中产生很多的问题;
-
如何获取异步后的结果:
-
方式一:setState的回调
-
setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
-
格式如下:setState(partialState, callback)
// 3.setState在React中的事件处理是一个异步调用 // 如果希望在数据更新之和(数据合并),获取到对于的结果执行一些逻辑代码 // 那么可以在setState中传入第二个参数: callback this.setState({ message: 'bbb' },()=>{ console.log("----",this.state.message); // 打印的是bbb }) console.log("----",this.state.message); // 打印的依然是 hello world }
-
-
当然,我们也可以在生命周期函数:
comonentDidUpdata(prevProps,provState,anapshot) { console.log(this.state.message) }
但是setState一定是异步的吗?(React18之前)
-
在组件生命周期或React合成事件中,setState是异步的;
-
在setTimeout或者原生dom事件中,setState是同步的;
changeText() { setTimeout(() => { // 在18之前,是同步操作 this.setState({message: '你好'}) console.log(this.state.message); // '你好' },0) } // 或者使用原生的方法添加事件,也会是同步的
setState默认是异步的(React18之后)
-
在React18之后,默认所有的操作都被放到了批处理中(异步处理)
-
如果希望代码可以同步拿到,则需要执行特殊的flushSync操作
import { flushSync } from 'react-dom' changeText() { flushSync(()=>{ this.setState({message: '你好'}); }) console.log(this.state.message); }
五.组件化开发——进阶
1.React性能优化SCU
-
渲染流程
-
更新流程
-
-
React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。
-
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
- 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n²),其中 n 是树中元素的数量;
- https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
- 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
- 这个开销太过昂贵了,React的更新性能会变得非常低效;
-
于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
-
同层节点之间相互比较,不会跨节点比较;
-
不同类型的节点,产生不同的树结构;
-
开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
-
记得这个警告不,就是提醒开发者加入一个 key 属性
- 在最后位置插入数据
- 这种情况,有无key意义并不大
- 在前面插入数据
- 这种做法,在没有key的情况下,所有的li都需要进行修改;
- 在最后位置插入数据
-
当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:
- 在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改;
- 将key为333的元素插入到最前面的位置即可;
-
key 的注意事项:
- key应该是唯一的;
- key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
- 使用index作为key,对性能是没有优化的;
-
-
-
render 函数被调用
注意:当App父组件的render函数被调用时,所有的子组件的render函数都将被查询调用
- 那么,我们可以思考一下,在以后的开发中,我们只要是修改了 App中的数据,所有的组件都需要重新render,进行diff算法, 性能必然是很低的:
- 事实上,很多的组件没有必须要重新render;
- 它们调用render应该有一个前提,就是依赖的数据(state、 props)发生改变时,再调用自己的render方法;
- 如何来控制render方法是否被调用呢?
- 通过
shouldComponentUpdate方法
即可;
- 通过
- 那么,我们可以思考一下,在以后的开发中,我们只要是修改了 App中的数据,所有的组件都需要重新render,进行diff算法, 性能必然是很低的:
-
shouldComponentUpdate
React给我们提供了一个生命周期方法
shouldComponentUpdate
(很多时候,我们简称为SCU),这个方法接受参数,并且需要有 返回值:- 接收两个参数
- 参数一:nextProps 修改之后,最新的props属性
- 参数二:nextState 修改之后,最新的state属性
- 返回值是一个boolean类型
- 返回值为true,那么就需要调用render方法;
- 返回值为false,那么就不需要调用render方法;
- 默认返回的是true,也就是只要state发生改变,就会调用render方法;
shouldComponentUpdata(newProps, nextState) { // 自己进行手动对比 if(this.props.message !== newProps.message || this.state.message !== nextState.message) { return true } return false }
- 接收两个参数
-
PureComponent 类
-
如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
-
我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;
-
-
事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了
- 将class继承自PureComponent。(不过是浅层比较)
shallowEqual方法
-
这个方法中,调用
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
,这个shallowEqual就是 进行浅层比较
-
-
-
-
高阶组件memo
-
目前我们是针对类组件可以使用PureComponent,那么函数式组件呢?
- 事实上函数式组件我们在props没有改变时,也是不希望其重新渲染其DOM树结构的
-
我们需要使用一个高阶组件memo:
- 我们将之前的Header、Banner、ProductList都通过memo函数进行一层包裹;
- Footer没有使用memo函数进行包裹;
- 最终的效果是,当counter发生改变时,Header、Banner、ProductList的函数不会重新执行;
- 而Footer的函数会被重新执行;
import { memo } from "react" const Profile = memo(function(props) { consolr.log("Profile render") return <h2>{props.message}</h2> }) export default Profile
- 我们将之前的Header、Banner、ProductList都通过memo函数进行一层包裹;
-
数据不可变的力量
-
数据不可变的力量
即this.state里的数据不得直接进行修改!!!
addNewFriends() { const newFriend = { name: 'zs', age: 20, height: 1.70}; // 在**PureComponent**中不会执行render() this.state.friends.push(newFriend) this.setState({ friends: this.state.friends}) }
在这种情况下(继承自PureComponent),会发现render函数不会调用,因为PureComponent对数据的对比是浅层对比,面对这种复杂数据类型,仅仅对比的是其在栈内存中的指针是否相同,深层的无法进行对比(案例里给friends只是进行了push操作,所以friends栈内存中的指针是没有改变的),故不会执行render函数。
addNewBook() { const newFriend = { name: 'zs', age: 20, height: 1.70}; const friends = {...this.state.friends} friends.push(newFriend) this.setState({ friends: friends}) }
在这种情况下,进行对比时
friends
和this.state.friends
的指针就不一样了,发现不一样,便能重新执行render函数
2.ref
在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:
- 管理焦点,文本选择或媒体播放;
- 触发强制动画;
- 集成第三方 DOM 库;
- 我们可以通过refs获取DOM;
如何获取DOM
-
方式一:传入字符串
- 使用时通过
this.refs.传入的字符串
格式获取对应的元素;
import React, { PureComponent,createRef,forwardRef } from 'react' export class App extends PureComponent { getNativeDOM() { // 1.在React元素上绑定一个ref字符串 console.log(this.refs.mc); } render() { return ( <div> <h2 ref="mc">hello world</h2> <button onClick={e => this.getNativeDOM()}>获取h2原生DOM</button> </div> ) } }
- 使用时通过
-
方式二:传入一个对象
- 对象是通过
React.createRef()
方式创建出来的; - 使用时获取到创建的对象其中有一个current属性就是对应的元素;
export class App extends PureComponent { constructor() { super() this.state= { } this.titleRef = createRef() } getNativeDOM() { // 1.在React元素上绑定一个ref字符串 // console.log(this.refs.mc); // 2.提前创建好ref对象,将我们创建好的对象绑定到元素 console.log(this.titleRef.current); } render() { return ( <div> <h2 ref="mc">hello world</h2> <h2 ref={this.titleRef}>hello world</h2> <button onClick={e => this.getNativeDOM()}>获取h2原生DOM</button> </div> ) } }
- 对象是通过
-
方式三:传入一个函数
- 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
- 使用时,直接拿到之前保存的元素对象即可;
export class App extends PureComponent { constructor() { super() this.state= { } this.titleRef = createRef() this.titleEl = undefined } getNativeDOM() { // 1.在React元素上绑定一个ref字符串 // console.log(this.refs.mc); // 2.提前创建好ref对象,将我们创建好的对象绑定到元素 // console.log(this.titleRef.current); // 3.传入一个回调函数,在对应的函数被渲染之后,回调函数被执行,且将元素传入 console.log(this.titleEl); } render() { return ( <div> <h2 ref="mc">hello world</h2> <h2 ref={this.titleRef}>hello world</h2> <h2 ref={el => { this.titleEl = el }}>hello world</h2> <button onClick={e => this.getNativeDOM()}>获取h2原生DOM</button> </div> ) } }
ref的类型
-
当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
获取原生 HTML 元素
示例:
如上
-
当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;
获取自定义 class 组件
示例:
import React, { PureComponent,createRef } from 'react' class HelloWorld extends PureComponent { test() { console.log('调用了子组件的方法'); } render() { return ( <h2>Hello World</h2> ) } } export class App extends PureComponent { constructor() { super() this.state= { } this.HelloWorldRef = createRef() } getComponent() { console.log(this.HelloWorldRef.current); this.HelloWorldRef.current.test() } render() { return ( <HelloWorld ref={this.HelloWorldRef}></HelloWorld> <button onClick={e => this.getComponent()}>获取自定义类组件</button> </div> ) } } export default App
-
你不能在函数组件上使用 ref 属性,因为他们没有实例;
- 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
- 这个时候我们可以通过
React.forwardRef
,后面我们也会学习 hooks 中如何使用ref;
import React, { PureComponent,createRef,forwardRef } from 'react' const HelloWorldFun = forwardRef(function(props, ref) { return ( <div> <h1 ref={ref}>函数HelloWorld</h1> <p>额</p> </div> ) }) export class App extends PureComponent { constructor() { super() this.state= { } this.FunHelloworld = createRef() } getFunComponent() { console.log(this.FunHelloworld.current); } render() { return ( <div> <HelloWorldFun ref={this.FunHelloworld}/> <button onClick={e => this.getFunComponent()}>获取自定义函数组件</button> </div> ) } } export default App
3.受控和非受控组件
在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state。
例如:
<form>
<label>
名字:
<input type="text" name="name" />
</label>
<input type="submit" value="提交" />
</form>
- 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
- 在React中,并没有禁止这个行为,它依然是有效的;
- 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
- 实现这种效果的标准方式是使用“受控组件”;
3.1 受控组件
在 HTML 中,表单元素(
如、<input> <textarea> 和 <select>
)之类的表单元素通常自己维护 state,并根据用户输入进 行更新。
示例:
有value默认值
export class App extends PureComponent {
constructor() {
super()
this.state = {
username : 'mingcomity'
}
}
inputChange(e) {
this.setState({
username: e.target.value
},() => {
console.log(this.state.username);
})
}
render() {
return (
<div>
<input type="text" value={this.state.username} />
<h2>{this.state.username}</h2>
</div>
)
}
}
-
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
- 我们将两者结合起来,使React的state成为“唯一数据源”;
- 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
- 被 React 以这种方式控制取值的表单输入元素就叫做**“受控组件”**;
-
由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。
render() { return ( <div> <input type="text" value={this.state.username} onChange={e => this.inputChange(e)}/> <h2>{this.state.username}</h2> </div> ) }
-
由于
handleUsernameChange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
演练:
-
textarea标签
- texteare标签和input比较相似:
-
select标签
- select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。
-
处理多个输入
- 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
- 这里我们可以使用ES6的一个语法:计算属性名(Computed property names)
import React, { PureComponent } from 'react' export class App extends PureComponent { constructor() { 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) } handleInputChange(event) { this.setState({ [event.target.name]: event.target.value }) } handleAgreeChange(event) { this.setState({ isAgree: event.target.checked }) } handleHobbiesChange(event, index) { const hobbies = [...this.state.hobbies] hobbies[index].isChecked = event.target.checked this.setState({ hobbies }) } handleFruitChange(event) { this.setState({ fruit: event.target.value }) } render() { const { username, password, isAgree, hobbies, fruit } = this.state return ( <div> <form onSubmit={e => this.handleSubmitClick(e)}> {/* 1.用户名和密码 */} <div> <label htmlFor="username"> 用户: <input id='username' type="text" name='username' value={username} onChange={e => this.handleInputChange(e)} /> </label> <label htmlFor="password"> 密码: <input id='password' type="password" name='password' value={password} onChange={e => this.handleInputChange(e)} /> </label> </div> {/* 2.checkbox单选 */} <label htmlFor="agree"> <input id='agree' type="checkbox" checked={isAgree} onChange={e => this.handleAgreeChange(e)} /> 同意协议 </label> {/* 3.checkbox多选 */} <div> 您的爱好: { hobbies.map((item, index) => { return ( <label htmlFor={item.value} key={item.value}> <input type="checkbox" id={item.value} checked={item.isChecked} onChange={e => this.handleHobbiesChange(e, index)} /> <span>{item.text}</span> </label> ) }) } </div> {/* 4.select */} <select value={fruit} onChange={e => this.handleFruitChange(e)}> <option value="apple">苹果</option> <option value="orange">橘子</option> <option value="banana">香蕉</option> </select> <div> <button type='submit'>注册</button> </div> </form> </div> ) } } export default App
import React, { PureComponent } from 'react' export class App extends PureComponent { constructor() { super() this.state = { 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) } handleFruitChange(event) { const options = Array.from(event.target.selectedOptions) const values = options.map(item => item.value) this.setState({ fruit: values }) // 扩展:Array.from() 将可迭代对象转为数组 // Array.from(arguments) const values2 = Array.from(event.target.selectedOptions,item => item.value) } render() { const { fruit } = this.state return ( <div> <form onSubmit={e => this.handleSubmitClick(e)}> {/* 4.select */} <select value={fruit} onChange={e => this.handleFruitChange(e)} multiple> <option value="apple">苹果</option> <option value="orange">橘子</option> <option value="banana">香蕉</option> </select> <div> <button type='submit'>注册</button> </div> </form> </div> ) } } export default App
3.2非受控组件
示例:
没有提供value
export class App extends PureComponent {
constructor() {
super()
this.state = {
username : ''
}
}
inputChange(e) {
this.setState({
username: e.target.value
},() => {
console.log(this.state.username);
})
}
render() {
return (
<div>
<input type="text" onChange={e => this.inputChange(e)}/>
<h2>{this.state.username}</h2>
</div>
)
}
}
-
React推荐大多数情况下使用 受控组件 来处理表单数据:
- 一个受控组件中,表单数据是由 React 组件来管理的;
- 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
-
如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据。
- 我们来进行一个简单的演练:
- 使用ref来获取input元素;
-
在非受控组件中通常使用defaultValue来设置默认值;
-
同样,
<input type="checkbox">
和<input type="radio">
支持 defaultChecked,<select>
和<textarea>
支 持 defaultValue。import React, { PureComponent, createRef } from 'react' export class App extends PureComponent { constructor() { super() this.state = { intro:'hh' } this.introRef = createRef() } // 在生命周期函数里进行监听表单的原生事件 componentDidMount() { this.introRef.current.addEventListener() } 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) console.log("获取非受控组件:", this.introRef.current.value) // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios) } render() { const { username, password, isAgree, hobbies, fruit ,intro} = this.state return ( <div> <form onSubmit={e => this.handleSubmitClick(e)}> {/* 5.非受控组件 */} <input ref={ this.introRef} type="text" defaultValue={intro}/> <div> <button type='submit'>注册</button> </div> </form> </div> ) } } export default App
4.高阶组件
大多数开发者都知道高阶函数,高阶组件和高阶函数非常相似
高阶函数:
-
接受一个或多个函数作为输入;
-
输出一个函数
JavaScript中比较常见的filter、map、reduce都是高阶函数
高阶组件:
- 高阶组件的英文是 Higher-Order Components,简称为 HOC
- 官方的定义:高阶组件是参数为组件,返回值为新组件的函数;
- 首先, 高阶组件 本身不是一个组件,而是一个函数;
- 其次,这个函数的参数是一个组件,返回值也是一个组件;
如何定义和使用?
import React, { PureComponent } from 'react'
// 定义一个高阶组件
function hoc(Cpn) {
// 1.定义类组件
class NewCpn extends PureComponent {
render() {
return <Cpn/>
}
}
return NewCpn
// 2.定义函数组件
// function NewCpn2(props) {
// }
// return NewCpn2
}
class HelloWorld extends PureComponent {
render() {
return <h2>Hello World</h2>
}
}
const HelloWorldHOC = hoc(HelloWorld)
export class App extends PureComponent {
render() {
return (
<div>
<HelloWorld/>
<HelloWorldHOC/>
</div>
)
}
}
export default App
对组件进行了拦截
-
高阶组件的调用过程类似于这样
const HelloWorldHOC = hoc(HelloWorld)
-
高阶函数的编写过程类似于这样
/ 定义一个高阶组件 function hoc(Cpn) { // 1.定义类组件 class NewCpn extends PureComponent { render() { return <Cpn/> } } return NewCpn // 2.定义函数组件 // function NewCpn2(props) { // } // return NewCpn2 }
-
高阶组件并不是React API的一部分,它是基于React的 组合特性而形成的设计模式;
-
高阶组件在一些React第三方库中非常常见:
- 比如redux中的connect;
- 比如react-router中的withRouter;
-
组件的名称问题:
- 在ES6中,类表达式中类名是可以省略的;
- 组件的名称都可以通过displayName来修改;
高阶组件的应用:
-
props的增强
import React, { PureComponent } from 'react' const userInfo = { name: 'ming', level: 99 } // 定义高阶组件:给一些需要特殊数据的组件,注入props function enhancedUserInfo(OriginComponent) { class NewComponent extends PureComponent { constructor(props) { super(props) this.state = { userInfo : { name: 'ming', level: 99 } } } render() { return <OriginComponent {...this.props} {...this.state.userInfo}/> } } return NewComponent } const Home = enhancedUserInfo( function Home(props) { return <h1>Home: {props.name} - {props.banners}</h1> } ) const Profile = enhancedUserInfo( function Profile(props) { return <h1>Profile: {props.level}</h1> } ) const HelloFriend = enhancedUserInfo( function enhancedUserInfo(props) { return <h1>HelloFriend {props.name}-{props.level}</h1> } ) export class App extends PureComponent { render() { return ( <div> <Home banners={["轮播1","轮播2","轮播3",]}/> <HelloFriend/> <Profile/> </div> ) } } export default App
-
渲染判断鉴权
import React, { PureComponent } from 'react' function loginAuth(OriginComponent) { return props => { // 从LoacalStorage中获取token const token = localStorage.getItem("token") if(token) { return <OriginComponent {...props}/> } else { return '请登录后再访问' } } } class Cart extends PureComponent { render() { return ( <div>Cart</div> ) } } const CartIs = loginAuth(Cart) export class App extends PureComponent { constructor() { super() } loginClick() { localStorage.setItem('token','kkkkkkk') // 希望强制刷新,在没有setState的情况下 this.forceUpdate() } render() { return ( <div> App <button onClick={e => this.loginClick()}>登录</button> <CartIs/> </div> ) } } export default App
-
生命周期劫持
import React, { PureComponent } from 'react' function logRenderTime(OriginComponent) { return class extends PureComponent { // componentWillMount() { // } // 即将挂载 UNSAFE_componentWillMount() { this.beginTime = new Date().getTime() } componentDidMount() { this.endTime = new Date().getTime() const interval = this.endTime - this.beginTime console.log(`当前页面花费了${interval}ms渲染完成`); } render() { return <OriginComponent {...this.props}/> } } } class Detail extends PureComponent { render() { return ( <div> <h2>Detail Page</h2> <ul> <li>数据列表1</li> <li>数据列表2</li> <li>数据列表3</li> <li>数据列表4</li> <li>数据列表5</li> <li>数据列表6</li> <li>数据列表7</li> <li>数据列表8</li> <li>数据列表9</li> <li>数据列表10</li> </ul> </div> ) } } const LogDetail = logRenderTime(Detail) export class App extends PureComponent { render() { return ( <div> <LogDetail/> </div> ) } } export default App
高阶函数的意义:
- 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理。
- 其实早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
- Mixin 可能会相互依赖,相互耦合,不利于代码维护;
- 不同的Mixin中的方法可能会相互冲突;
- Mixin非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;
- 当然,HOC也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
- HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;
- Hooks的出现,是开创性的,它解决了很多React之前的存在的问题
- 比如this指向问题、比如hoc的嵌套复杂度问题等等;
ref的转发
-
在前面我们学习ref时讲过,ref不能应用于函数式组件:
- 因为函数式组件没有实例,所以不能获取到对应的组件对象
-
但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?
- 方式一:直接传入ref属性(错误的做法)
- 方式二:通过forwardRef高阶函数;
import React, { PureComponent,createRef,forwardRef } from 'react' const HelloWorldFun = forwardRef(function(props, ref) { return ( <div> <h1 ref={ref}>函数HelloWorld</h1> <p>额</p> </div> ) })
5.Portals
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM 元素上的)。就是传送组件。
-
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM 元素上的)
-
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
-
第二个参数(container)是一个 DOM 元素;
-
-
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
-
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:
示例:
- 比如说,我们准备开发一个Modal组件,它可以将它的子组件渲染到屏幕的中间位置:
- 步骤一:修改index.html添加新的节点
- 步骤二:编写这个节点的样式
- 步骤三:编写组件代码
export class App extends PureComponent {
render() {
return (
<div>
<h1>App H1</h1>
{createPortal(<h2>App H2</h2>, document.querySelector('#mc'))}
<Modal>
<h2>我是标题</h2>
<p>我是内容</p>
</Modal>
</div>
)
}
}
export class Modal extends PureComponent {
render() {
return createPortal(this.props.children,document.querySelector('#modal'))
}
}
// -----------------
<style>
#modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
<body>
<div id="root"></div>
<div id="mc"></div>
<div id="modal"></div>
</body>
6.fragment
在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素:
对于我们希望不渲染一个div应该如何操作?
-
使用Fragment
-
Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
-
React还提供了Fragment的短语法:
-
它看起来像空标签 <> ;
import React, { PureComponent ,Fragment} from 'react' export class App extends PureComponent { render() { return ( <> <h2>我是App的标题</h2> <p>我是内容</p> </> ) } } export default App
-
但是,如果我们需要在Fragment中添加key,那么就不能使用短语法
{ this.state.sections.map(item =>{ return ( <Fragment key={item.title}> <h2>{item.title}</h2> <p>{item.context}</p> </Fragment> ) }) } // 报错 < key={item.title}> <h2>{item.title}</h2> <p>{item.context}</p> </>
-
7.StrictMode
StrictMode 是一个用来突出显示应用程序中潜在问题的工具
-
与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
-
它为其后代元素触发额外的检查和警告;
-
严格模式检查仅在开发模式下运行;它们不会影响生产构建;
-
可以为应用程序的任何部分启用严格模式:
- 不会对 Header 和 Footer 组件运行严格模式检查;
- 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查;
严格模式检查
-
识别不安全的生命周期:
UNSAFE_componentWillMount
UNSAFE_componentWillMount() { console.log( 'UNSAFE_componentWillMount() {}' ); }
-
使用过时的ref API
ref(string)
-
检查意外的副作用
-
这个组件的constructor会被调用两次;
export class Home extends PureComponent { constructor() { super() console.log('Home contructor'); // 打印两次 } componentDidMount() { console.log('Home componentDidMount'); // 打印两次 } render() { console.log('Profile render'); // 打印两次 return ( <div>Home</div> ) } }
-
这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
-
在生产环境中,是不会被调用两次的;
-
-
使用废弃的findDOMNode方法
- 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
import { findDOMNode } from 'react-dom';
-
检测过时的context API
- 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的;
- 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法;
六.其它扩展
1.过渡动画——react-transition-group
在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验。当然,我们可以通过原生的CSS来实现这些过渡动画,但是React社区为我们提供了react-transition-group用来完成过渡动画。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
1.1 Transition
该组件是一个和平台无关的组件(不一定要结合CSS);
在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
1.2 CSSTransition
在前端开发中,通常使用CSSTransition来完成过渡动画效果
import React, { PureComponent } from 'react'
import { CSSTransition } from 'react-transition-group'
import './style.css'
<style>
/* 首次进入 */
.mc-appear {
opacity: 0;
transform: translateX(-150px);
}
.mc-appear-active {
opacity: 1;
transform: translateX(0);
transition: all 2s ease;
}
/* 进入动画 */
.mc-enter {
opacity: 0;
}
/* 进入中 */
.mc-enter-active {
opacity: 1;
transition: opacity 2s ease;
}
/* 离开动画 */
.mc-exit {
opacity: 1;
}
/* 离开中 */
.mc-exit-active {
opacity: 0;
transition: opacity 2s ease;
}
</style>
export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
isShow: false
}
}
render() {
const {isShow} = this.state
return (
<div>
<button onClick={e => this.setState({isShow: !isShow})}>显示/隐藏</button>
{/* {isShow && <h2>嘿嘿嘿</h2>} */}
<CSSTransition appear unmountOnExit={true} in={isShow} classNames="mc" timeout={2000}>
<h2>嘿嘿嘿</h2>
</CSSTransition>
</div>
)
}
}
export default App
严格模式下:
import { createRef } from 'react'
export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
isShow: true,
}
this.sectionRef = createRef()
}
render() {
const {isShow} = this.state
return (
<div>
<button onClick={e => this.setState({isShow: !isShow})}>显示/隐藏</button>
{/* {isShow && <h2>嘿嘿嘿</h2>} */}
<CSSTransition nodeRef={this.sectionRef} onEnter={e => console.log('开始进入')} onEntering={ e => console.log('执行进入中')} onEntered={e => console.log('执行进入结束')} onExit={e => console.log('开始离开动画')} appear unmountOnExit={true} in={isShow} classNames="mc" timeout={2000}>
<h2 ref={this.sectionRef}>嘿嘿嘿</h2>
</CSSTransition>
</div>
)
}
}
-
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名称:比如card-enter、card-enter-active、card-enter-done;
- timeout:
- 过渡动画的时间
- appear:
- 是否在初次进入添加动画(需要和in同时为true)
- unmountOnExit:退出后卸载组件
- 其他属性可以参考官网来学习: https://reactcommunity.org/react-transition-group/transition
- in:触发进入或者退出状态
-
CSSTransition对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作
onEnter
:在进入动画之前被触发;onEntering
:在应用进入动画时被触发;onEntered
:在应用进入动画结束后被触发;
render() { const {isShow} = this.state return ( <div> <button onClick={e => this.setState({isShow: !isShow})}>显示/隐藏</button> {/* {isShow && <h2>嘿嘿嘿</h2>} */} <CSSTransition onEnter={e => console.log('开始进入')} onEntering={ e => console.log('执行进入中')} onEntered={e => console.log('执行进入结束')} onExit={e => console.log('开始离开动画')} appear unmountOnExit={true} in={isShow} classNames="mc" timeout={2000}> <h2>嘿嘿嘿</h2> </CSSTransition> </div> ) }
1.3 SwitchTransition
两个组件显示和隐藏切换时,使用该组件
import React, { PureComponent } from 'react'
import { SwitchTransition,CSSTransition } from 'react-transition-group'
import './style1.css'
<style>
.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;
}
</style>
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={ isLogin }
classNames="login"
timeout={1000}
>
<button onClick={e => this.setState({ isLogin: !isLogin })}>
{ isLogin ? '注销' : '登录'}
</button>
</CSSTransition>
</SwitchTransition>
</div>
)
}
}
export default App
- 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属性;
1.4 TransitionGroup
将多个动画组件包裹在其中,一般用于列表中元素的动画;
import React, { PureComponent } from 'react'
import { TransitionGroup ,CSSTransition} from 'react-transition-group'
import './style2.css'
<style>
.book-enter {
transform: translateX(100px);
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(100px);
opacity: 0;
transition: all 1s ease;
}
</style>
export class AppGroup extends PureComponent {
constructor() {
super()
this.state = {
books: [
{name: '你不知道的Javascript', price: 120,id:1},
{name: 'Javascript高级程序设计', price: 100,id:2},
{name: 'Vue.js设计与实现', price: 80,id:3}
]
}
}
addNewBook() {
const books = [...this.state.books]
books.push({
name: 'Javascript DOM 编程艺术',
price: 20,
id: Date.now()
})
this.setState({
books
})
}
removeNewBook(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>
<span>价格:{item.price}</span>
<button onClick={ e => this.removeNewBook(index)}>删除</button>
</li>
</CSSTransition>
)
})
}
</TransitionGroup>
<button onClick={ e => this.addNewBook()}>添加书籍</button>
</div>
)
}
}
export default AppGroup
-
当我们有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画
2.编写CSS
- 前面说过,整个前端已经是组件化的天下:
- 而CSS的设计就不是为组件化而生的,所以在目前组件化的框架中都在需要一种合适的CSS解决方案。
- 在组件化中选择合适的CSS解决方案应该符合以下条件:
- 可以编写局部css:css具备自己的具备作用域,不会随意污染其他组件内的元素;
- 可以编写动态的css:可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
- 支持所有的css特性:伪类、动画、媒体查询等;
- 编写起来简洁方便、最好符合一贯的css风格特点;
- 等等..
React中的CSS:
事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点。
- 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;
- 大家一致在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案;
Vue中的CSS:
Vue在CSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目 采用不一样的样式风格。
- Vue通过在.vue文件中编写
<style><style>
标签来编写自己的样式; - 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效;
- 通过 lang 属性来设置你喜欢的 less、sass等预处理器;
- 通过内联样式风格的方式来根据最新状态设置和改变css;
- 等等..
2.1 内联样式
- 内联样式是官方推荐的一种css样式的写法:
- style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
- 并且可以引用state中的状态来设置相关的样式;
- 内联样式的优点:
- 内联样式, 样式之间不会有冲突
- 可以动态获取当前state中的状态
- 内联样式的缺点:
- 写法上都需要使用驼峰标识
- 某些样式没有提示
- 大量的样式, 代码混乱
- 某些样式无法编写(比如伪类/伪元素)
- 所以官方依然是希望内联样式和普通的css来结合编写;
import React, {PureComponent} from 'react';
class App extends PureComponent {
constructor() {
super();
this.state = {
titleSize: 30
}
}
addTitleSize() {
this.setState({
titleSize: this.state.titleSize + 2
})
}
render() {
const {titleSize} = this.state
return (
<div>
<button onClick={e => this.addTitleSize()}>增加字体大小</button>
<h2 style={{color: 'red', fontSize: `${titleSize}px`}}>我是标题</h2>
<p style={{color: 'blue', fontSize: '20px'}}>我是内容哈哈哈</p>
</div>
);
}
}
export default App;
2.2 普通的CSS
- 普通的css我们通常会编写到一个单独的文件,之后再进行引入。
- 这样的编写方式和普通的网页开发中编写方式是一致的:
- 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
- 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
- 但是普通的css都属于全局的css,样式之间会相互影响;
- 这种编写方式最大的问题是样式之间会相互层叠掉;
App.jsx
import React, {PureComponent} from 'react';
import './App.css'
.title {
font-size: 30px;
color: red;
}
.content {
font-size: 20px;
color: aqua;
}
import Home from "./Home/home";
class App extends PureComponent {
render() {
return (
<div>
<h2 className='title'>APP</h2>
<p className='content'>我是内容咯</p>
<Home></Home>
</div>
);
}
}
export default App;
Home.jsx
import React, {PureComponent} from 'react';
import './home.css'
.title {
color: bisque;
}
class Home extends PureComponent {
render() {
return (
<div>
<h2 className='title'>Home</h2>
</div>
);
}
}
export default Home;
2.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中很受欢迎的一种方式。
import React, {PureComponent} from 'react';
import Home from "./Home/home";
import appStyle from './App.module.css'
<style>
.title {
font-size: 30px;
color: red;
}
.content {
font-size: 20px;
color: aqua;
}
</style>
class App extends PureComponent {
render() {
return (
<div>
<h2 className={appStyle.title}>APP</h2>
<p className={appStyle.content}>我是内容咯</p>
<Home></Home>
</div>
);
}
}
export default App;
2.4 CSS-in-JS
-
官方文档也有提到过CSS in JS这种方案:
- “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;
- 注意此功能并不是 React 的一部分,而是由第三方库提供;
- React 对样式如何定义并没有明确态度;
-
在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。
- 但是在前面的学习中,我们就提到过,React的思想中认为逻辑本身和UI是无法分离的,所以才会有了JSX的语法。
- 样式呢?样式也是属于UI的一部分;
- 事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态;
- 所以React有被人称之为 All in JS;
-
当然,这种开发的方式也受到了很多的批评:
- Stop using CSS in JavaScript for web development
- https://hackernoon.com/stop-using-css-in-javascript-for-web-development-fa32fb873dcc
-
批评声音虽然有,但是在我们看来很多优秀的CSS-in-JS的库依然非常强大、方便:
- CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修 改状态等等;
- 虽然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
- 所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案;
-
目前比较流行的CSS-in-JS的库有哪些呢?
- styled-components
- emotion
- glamorous
-
目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components的讲解为主;
2.5 使用styled-components
安装:yarn add styled-components
npm i styled-components
-
ES6标签模板字符串
-
ES6中增加了模板字符串的语法,这个对于很多人来说都会使用。
-
但是模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。
-
看这个普通的JavaScript的函数:
function foo(...args) { console.log(args); } foo("Hellos World");
- 正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外一种 调用方式:
foo`Hello World` const name = "Ming" foo`Hello ${name}`
-
如果我们在调用的时候插入其他的变量:
- 模板字符串被拆分了;
- 第一个元素是数组,是被模块字符串拆分的字符串组合;
- 后面的元素是一个个模块字符串传入的内容;
-
在styled component中,就是通过这种方式来解析模块字符串,最终生成我们想要 的样式的
-
基本使用:
- styled-components的本质是通过函数的调用,最终 创建出一个组件:
- 这个组件会被自动添加上一个不重复的class;
- styled-components会给该class添加相关的样式;
- 另外,它支持类似于CSS预处理器一样的样式嵌套:
- 支持直接子代选择器或后代选择器,并且直接编写 样式;
- 可以通过&符号获取当前元素;
- 直接伪类选择器、伪元素等;
示例:
import React, {PureComponent} from 'react';
import {AppWrapper, SectionWrapper} from "./style";
class App extends PureComponent {
render() {
return (
<AppWrapper>
<SectionWrapper>
<h2 className='title'>我是标题</h2>
<p className='content'>我是内容</p>
</SectionWrapper>
<div className='footer'>
<p>免责说明</p>
<p>版权说明</p>
</div>
</AppWrapper>
);
}
}
export default App;
import styled from "styled-components";
export const AppWrapper = styled.div`
.footer {
border: 1px solid orange;
}
`
export const SectionWrapper = styled.div`
border: 1px solid red;
.title {
font-size: 30px;
color: blue;
&:hover {
background-color: blue;
}
}
.content {
font-size: 20px;
color: green;
}
}
`
-
props、attrs属性
-
props可以传递
-
props可以被传递给styled组件
- 获取props需要通过${}传入一个插值函数,props会作为该函数的参数;
- 这种方式可以有效的解决动态样式的问题;
import React, {PureComponent} from 'react'; import {AppWrapper, SectionWrapper} from "./style"; class App extends PureComponent { constructor(props) { super(props); this.state = { size: 30, color: 'yellow' } } render() { const {size, color} = this.state return ( <AppWrapper> <SectionWrapper size={size} color={color}> <h2 className='title'>我是标题</h2> <p className='content'>我是内容</p> <button onClick={e => this.setState({color: 'red'})}>修改颜色</button> </SectionWrapper> <div className='footer'> <p>免责说明</p> <p>版权说明</p> </div> </AppWrapper> ); } } export default App;
import styled from "styled-components"; import {primaryColor,secondyColor,smallSize} from "./style/variables"; export const AppWrapper = styled.div` .footer { border: 1px solid orange; } ` // 可接收外部传来的props export const SectionWrapper = styled.div` border: 1px solid red; .title { font-size: ${ props => props.size }px; color: ${ props => props.color }; &:hover { background-color: blue; } } .content { font-size: ${smallSize}px; color: ${secondyColor}; } } `
export const primaryColor = '#ff8800' export const secondyColor = '#ff7788' export const smallSize = '12px' export const middleSize = '24px' export const largeSize = '36px'
-
添加attrs属性
import styled from "styled-components"; import {secondyColor,smallSize} from "./style/variables"; export const AppWrapper = styled.div` .footer { border: 1px solid orange; } ` // 可接收外部传来的props export const SectionWrapper = styled.div.attrs(props => { return { tColor: props.color || "blue" } })` border: 1px solid red; .title { font-size: ${ props => props.size }px; color: ${ props => props.tColor }; &:hover { background-color: blue; } } .content { font-size: ${smallSize}px; color: ${secondyColor}; } } `
-
-
混入mixin
import React from 'react'; import styled, { css } from 'styled-components'; // 定义样式混入对象 const mixin = css` color: #ffffff; font-size: 16px; `; // 创建组件样式 const Box = styled.div` ${mixin}; width: 200px; height: 200px; background-color: #ffffff; `; // 定义组件 const MyComponent = () => { return <Box>内容</Box>; }; export default MyComponent;
const theme = { color: { primary: "#ff385c", second: "#00848A", }, text: { primary: "#484848", second: "#222", }, mixin: { boxShadow: ` transition: box-shadow 0.2s ease; &:hover { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18); } `, }, }; export default theme;
使用混入
高级特性:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from "./04_CSS-in-JS/App";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ThemeProvider theme={{ color: "purple", size: "50px"}}>
<App />
</ThemeProvider>
</React.StrictMode>
);
import React, {PureComponent} from 'react';
import {HomeWrapper} from "./style";
class Index extends PureComponent {
render() {
return (
<HomeWrapper>
<div className='top'>
<div className='banner'>
Banner
</div>
</div>
<div className='bottom'>
<h2 className='header'>
商品列表
</h2>
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
</ul>
</div>
</HomeWrapper>
);
}
}
export default Index;
import styled from "styled-components";
export const HomeWrapper = styled.div`
.top {
.banner {
color: red;
}
}
.bottom {
.header {
color: ${ props => props.theme.color};
}
ul li {
font-size: ${props => props.theme.size};
color: red;
}
}
`
2.6 添加class
-
Vue
<div class="static" v-bind:class="{ active: isActive, 'text-danger': hasError}"> </div> <div :class="[activeClass,errorClass]"> </div> <div :class="[{ active: isActive }, errorClass]"> </div>
-
React
使用原生
class App extends PureComponent { constructor() { super(); this.state = { isbbb: true, isccc: false, } } 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> </div> ); } }
使用第三方库:classnames
npm i classnames
3.redux-devtools
浏览器插件
-
redux 可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?
- redux官网为我们提供了redux-devtools的工具;
- 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
-
安装该工具需要两步:
-
第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可);
-
第二步:在redux中继承devtools的中间件;
src/store/index.js
import {createStore,applyMiddleware,compose} from "redux"; import thunk from "redux-thunk"; import reducer from "./reducer"; // const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose; const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; // 想要 store.dispathch(function) const store = createStore(reducer,composeEnhancers(applyMiddleware(thunk))) export default store
-
-
开发环境和生产环境
开发环境下打开,生产环境下记得关闭
4.react-devtools
浏览器插件
七.Redux
1.纯函数
-
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
- 在react开发中纯函数是被多次提及的;
- 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求 必须是一个纯函数;
- 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
-
纯函数的维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出。
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如**“触发事件”,使输出设备输出,或更改输出值以外物件的内容**等。
- 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
-
总结一下:
-
确定的输入,一定会产生确定的输出;
-
函数在执行过程中,不能产生副作用;
副作用: 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响, 比如修改了全局变量,修改参数或者改变外部的存储;
副作用往往是产生bug的 “温床”。
-
-
案例:
slice
:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;- 纯函数
splice
:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;- 非纯函数
-
为什么纯函数在函数式编程中非常重要呢?
- 因为你可以安心的编写和安心的使用;
- 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的 外部变量是否已经发生了修改;
- 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
2.redux的意义
- JavaScript开发的应用程序,已经变得越来越复杂了:
- JavaScript需要管理的状态越来越多,越来越复杂;
- 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示 加载动效,当前分页;
- 管理不断变化的state是非常困难的:
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
- React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:
- 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;
- React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;
- Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;
- Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
3.redux的核心理念
-
Store
存放数据,里面的数据来自reducer
-
Action
Redux要求我们通过action来更新数据
- 所有数据的变化,必须通过派发(dispatch)action来更新;
- action是一个普通的JavaScript对象,用来描述这次更新的type和content;
-
Reducer
将state和action联系在一起
- reducer是一个纯函数;
- reducer做的事情就是将传入的state和action结合起来生成一个新的state;
注意原则:
- 单一数据源
- 整个应用程序的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都应该是纯函数,不能产生任何的副作用;
4.node实操redux
node 环境
-
安装redux
npm init -y
npm i redux --save / yarn add redux
-
创建一个新的项目文件夹:learn-redux
-
创建src目录,并且创建
store/index.js
文件const {createStore} = require("redux")
-
创建一个对象,作为我们要保存的状态:
// 初始化的数据 const initialState = { name: 'ming', age: '18' }
-
创建Store来存储这个state
-
创建store时必须创建reducer;
// 定义reducer函数:纯函数 function reducer() { return initialState; } // 创建的store const store = createStore(reducer) module.exports = store
-
我们可以通过
store.getState
来获取当前的state;const store = require("./store/index") console.log(store.getState())
-
-
通过action来修改state
-
通过dispatch来派发action;
修改store
const store = require("./store/index") // 修改store中的数据:必须使用action const nameAction = {type: 'change_name', name: 'comity'} // 派发 store.dispatch(nameAction) console.log(store.getState())
-
通常action中都会有type属性,也可以携带其他的数据;
const {createStore} = require("redux") // 初始化的数据 const initialState = { name: 'ming', age: 18 } // 定义reducer函数:纯函数 // 两个参数: // 参一:store中目前保存的state // 参二:本次需要更新的action(dispatch传入的action) // 返回值:它的返回值会作为本次store之后存储的state function reducer(state = initialState, action) { // 有数据更新时,那么返回新的state if(action.type === "change_name") { return {...state,name: action.name} } // 没有数据更新时,那么返回之前的state return state; } // 创建的store const store = createStore(reducer) module.exports = store
-
-
修改reducer中的处理代码
-
这里一定要记住,reducer是一个纯函数,不需要直接修改state;
// 定义reducer函数:纯函数 // 两个参数: // 参一:store中目前保存的state // 参二:本次需要更新的action(dispatch传入的action) // 返回值:它的返回值会作为本次store之后存储的state function reducer(state = initialState, action) { // 有数据更新时,那么返回新的state switch (action.type) { case "change_name": return {...state, name: action.name} case "add_age": return {...state, age: state.age + action.number} default: // 没有数据更新时,那么返回之前的state return state; } }
-
后面会讲到直接修改state带来的问题;
-
-
可以在派发action之前,监听store的变化:
const store = require("./store/index") // store发生改变时,执行该回调函数 store.subscribe(() => { console.log("订阅数据的变化",store.getState()) }) // 修改store中的数据:必须使用action store.dispatch({type: 'change_name', name: 'comity'}) store.dispatch({type: 'add_age', number: 1})
取消订阅
// store发生改变时,执行该回调函数 const unsubscribe = store.subscribe(() => { console.log("订阅数据的变化",store.getState()) }) // 取消订阅 unsubscribe()
-
优化action
const store = require("./store/index") // store发生改变时,执行该回调函数 const unsubscribe = store.subscribe(() => { console.log("订阅数据的变化", store.getState()) }) // actionCreators: 帮助我们创建action const changeNameAction = (name) => { return { type: 'change_name', name } } const addNumberAction = (number) => { return { type: 'add_age', number } } // 修改store中的数据:必须使用action store.dispatch(changeNameAction('comity')) store.dispatch(changeNameAction('mingcomity')) store.dispatch(addNumberAction(1)) store.dispatch(addNumberAction(10)) store.dispatch(addNumberAction(100))
-
结构划分:
-
接下来,我会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。
-
创建
store/index.js
文件:const {createStore} = require("redux") const reducer = require("./reducer") // 创建的store const store = createStore(reducer) module.exports = store
-
创建
store/reducer.js
文件:const {ADD_NUMBER,CHANGE_NAME} = require("./constants") // 初始化的数据 const initialState = { name: 'ming', age: 18 } function reducer(state = initialState, action) { // 有数据更新时,那么返回新的state switch (action.type) { case CHANGE_NAME: return {...state, name: action.name} case ADD_NUMBER: return {...state, age: state.age + action.number} default: // 没有数据更新时,那么返回之前的state return state; } } module.exports = reducer
-
创建
store/actionCreators.js
文件:const {ADD_NUMBER,CHANGE_NAME} = require("./constants") const changeNameAction = (name) => { return { type: CHANGE_NAME, name } } const addNumberAction = (number) => { return { type: ADD_NUMBER, number } } module.exports = { addNumberAction, changeNameAction }
-
创建
store/constants.js
文件:const ADD_NUMBER = "add_number" const CHANGE_NAME = "change_name" module.exports = { ADD_NUMBER, CHANGE_NAME }
-
5.react实操redux
示例:
App.jsx
可优化的点:
取消监听
用高阶组件包裹
import React, {PureComponent} from 'react';
import Home from "./pages/home";
import Profile from "./pages/profile";
import './styel.css'
import store from './store'
class App extends PureComponent {
constructor() {
super();
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
// store的变化时执行回调
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></Home>
<Profile></Profile>
</div>
</div>
);
}
}
export default App;
home.jsx
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import store from "../store";
import {addNumberAction} from "../store/actionCreators";
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 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(10)}>+10</button>
</div>
</div>
);
}
}
export default Home;
profile.jsx
import React, {PureComponent} from 'react';
import store from "../store";
import {subNumberAction} from "../store/actionCreators";
class Profile extends PureComponent {
constructor() {
super();
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({
counter: state.counter
})
})
}
subNUmber(num) {
store.dispatch(subNumberAction(num))
}
render() {
const {counter} = this.state
return (
<div>
<h2>PROFILE Counter: {counter}</h2>
<div>
<button onClick={e => this.subNUmber(1)}>-1</button>
<button onClick={e => this.subNUmber(5)}>-5</button>
<button onClick={e => this.subNUmber(10)}>-10</button>
</div>
</div>
);
}
}
export default Profile;
src/store/
import {createStore} from "redux";
import reducer from "./reducer";
const store = createStore(reducer)
export default store
import * as actionTypes from "./constants";
const initialState = {
counter: 100
}
function reducer(state = initialState,action) {
switch (action.type) {
case actionTypes.ADD_NUMBER:
return {...state,counter: state.counter + action.num}
case actionTypes.SUB_NUMBER:
return {...state,counter: state.counter - action.num}
default:
return state
}
}
export default reducer
import * as actionTypes from "./constants";
export const addNumberAction = (num) => ({
type: actionTypes.ADD_NUMBER,
num
})
export const subNumberAction = (num) => ({
type: actionTypes.SUB_NUMBER,
num
})
export const ADD_NUMBER = 'add_number'
export const SUB_NUMBER = 'sub_number'
6.react-redux
-
安装
npm i react-redux
-
src/index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import {Provider} from "react-redux"; import store from "./store"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> );
-
src/pages/about.jsx
import React, {PureComponent} from 'react'; import {connect} from "react-redux"; import {addNumberAction, subNumberAction} from "../store/actionCreators"; class About extends PureComponent { calcNumber(num,isAdd = false) { if(isAdd) { this.props.addNumber(num) } else { this.props.subNumber(num) } } render() { const {counter} = this.props return ( <div> <h2>ABOUT:counter:{counter}</h2> <button onClick={e=>this.calcNumber(12,true)}>+12</button> <button onClick={e=>this.calcNumber(12,false)}>-12</button> <button onClick={e=>this.calcNumber(24,true)}>+24</button> </div> ); } } // 接收到state,返回一个对象,表明需要用到哪些数据 const mapStateToProps = (state) =>({ counter: state.counter }) const mapDispatchToProps = (dispatch) => ({ addNumber: (num) => dispatch(addNumberAction(num)), subNumber: (num) => dispatch(subNumberAction(num)) }) // connect() 返回一个高阶组件 // 参数1:一个函数,将state映射到props // 参数2:一个函数,将dispatch映射到props export default connect(mapStateToProps,mapDispatchToProps)(About);
7.异步请求
-
在组件中进行
通过在组件的
componentDidMount
的钩子函数中请求到后,将数据存储在 redux 的state中。import React, {PureComponent} from 'react'; import {connect} from "react-redux"; import axios from "axios"; import {changeBannersAction} from "../store/actionCreators"; class Category extends PureComponent { componentDidMount() { axios.get("http://123.207.32.32:8000/home/multidata").then(res => { const banners = res.data.data.banner.list; this.props.changeBanners(banners) }) } render() { return ( <div> <h2>Category</h2> </div> ); } } const mapDispatchToProps= (dispatch) => ({ changeBanners(banners) { dispatch(changeBannersAction(banners)) } }) export default connect(null,mapDispatchToProps)(Category);
-
在redux中进行
事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;
-
下载 redux-thunk
npm i redux-thunk
-
src/store/index.js
import {createStore,applyMiddleware} from "redux"; import thunk from "redux-thunk"; import reducer from "./reducer"; // 想要 store.dispathch(function) const store = createStore(reducer,applyMiddleware(thunk)) export default store
-
src/store/actionCreators.js
import * as actionTypes from "./constants"; import axios from "axios"; export const addNumberAction = (num) => ({ type: actionTypes.ADD_NUMBER, num }) export const subNumberAction = (num) => ({ type: actionTypes.SUB_NUMBER, num }) export const changeBannersAction = (banners) => ({ type: actionTypes.CHANGE_BANNERS, banners }) export const fetchHomeMultidataAction = () => { // 正常情况下返回一个对象,但是由于是异步的,所以我们返回一个函数 // 但是redux是不支持的,所以想要注册中间件 return function (dispatch, getState) { // 会执行该函数 console.log(getState().counter) axios.get("http://123.207.32.32:8000/home/multidata").then(res => { const banners = res.data.data.banner.list; dispatch(changeBannersAction(banners)) }) } }
-
src/pages/category.jsx
import React, {PureComponent} from 'react'; import {connect} from "react-redux"; import { fetchHomeMultidataAction} from "../store/actionCreators"; class Category extends PureComponent { componentDidMount() { this.props.fetchHomeMultidata(); } render() { return ( <div> <h2>Category</h2> </div> ); } } const mapDispatchToProps= (dispatch) => ({ fetchHomeMultidata() { dispatch(fetchHomeMultidataAction()) } }) export default connect(null,mapDispatchToProps)(Category);
-
拓展:
- 中间件(Middleware):
- 这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;
- 比如日志记录、调用异步接口、添加代码调试功能等等;
- redux-thunk是如何做到让我们可以发送异步的请求呢?
- 我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象;
- redux-thunk可以让dispatch(action函数),action可以是一个函数;
- 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
- dispatch函数用于我们之后再次派发action;
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态
8.Reducer代码划分
如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
因此,我们可以对reducer进行拆分。
-
src/store/counter/constants.js
export const ADD_NUMBER = 'add_number' export const SUB_NUMBER = 'sub_number'
-
src/store/counter/actionCreators.js
import * as actionTypes from "./constants"; export const addNumberAction = (num) => ({ type: actionTypes.ADD_NUMBER, num }) export const subNumberAction = (num) => ({ type: actionTypes.SUB_NUMBER, num })
-
src/store/counter/reducer.js
import * as actionTypes from "./constants"; const initialState = { counter: 200 } function reducer(state = initialState, action) { switch (action.type) { case actionTypes.ADD_NUMBER: return {...state, counter: state.counter + action.num} case actionTypes.SUB_NUMBER: return {...state, counter: state.counter - action.num} default: return state } } export default reducer
-
src/store/counter/index.js
统一的出口
import reducer from "./reducer"; export default reducer export * from './actionCreators'
-
src/store/index.js
combineReducers函数可以方便的让我们对多个reducer进行合并
-
那么combineReducers是如何实现的呢?
- 事实上,它也是将我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函 数了);
- 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
- 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;
function reducer(state = {}, action) { // 返回一个对象,作为store的state return { counter: counterReducer(state.counter,action), home: homeReducer(state.home,action) } } // 源码进行了更多的优化,比如判断state到底有没变化
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__({trace: true}) || compose; const store = createStore(reducer,composeEnhancers(applyMiddleware(thunk))) export default store
-
-
src/App.jsx
class App extends PureComponent { constructor() { super(); this.state = { // 进行划分后 counter: store.getState().counter.counter } } componentDidMount() { store.subscribe(() => { const state = store.getState() this.setState({ counter: state.counter.counter }) }) } render() { const {counter} = this.state return ( <div> <h2>APP Counter: {counter}</h2> </div> </div> ); } }
9.Redux Toolkit
经过上面的体验,可以知道redux的编写逻辑过于的繁琐和麻烦。 Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;也称为**“RTK”**;
- Redux Toolkit的核心API主要是如下几个:
configureStore
:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。createSlice
:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。createAsyncThunk
: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分 派动作类型的 thunk
-
安装
npm install @reduxjs/toolkit react-redux
-
项目结构
-
src/index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import {Provider} from "react-redux"; import App from './App'; import store from "./store"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> );
-
src/App.jsx
import React, {PureComponent} from 'react'; import {connect} from "react-redux"; import Home from "./pages/Home"; import Profile from "./pages/Profile"; import './style.css' class App extends PureComponent { render() { const{ counter } = this.props return ( <div> <h2>App Counter: {counter}</h2> <div className='pages'> <Home/> <Profile/> </div> </div> ); } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) export default connect(mapStateToProps)(App);
-
src/pages/Home.jsx
import React, {PureComponent} from 'react'; import {connect} from "react-redux"; import {addNumber} from "../store/features/counter"; class Home extends PureComponent { addNumber(num) { this.props.addNumber(num) } render() { const{ counter } = this.props return ( <div> <h2>Home: {counter}</h2> <button onClick={ e => this.addNumber(5)}>+5</button> <button onClick={ e => this.addNumber(50)}>+50</button> <button onClick={ e => this.addNumber(250)}>+250</button> </div> ); } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) const mapDispatchToProps = (dispatch) => ({ addNumber(num) { dispatch(addNumber(num)) } }) export default connect(mapStateToProps,mapDispatchToProps)(Home);
-
src/pages/Profile.jsx
import React, {PureComponent} from 'react'; import {connect} from "react-redux"; import {subNumber} from "../store/features/counter"; class Profile extends PureComponent { subNumber(num) { this.props.subNumber(num) } render() { const{ counter } = this.props return ( <div> <h2>Profile:{counter}</h2> <button onClick={e => this.subNumber(5)}>-5</button> <button onClick={e => this.subNumber(50)}>-50</button> <button onClick={e => this.subNumber(250)}>-250</button> </div> ); } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) const mapDispatchToProps = (dispatch) => ({ subNumber(num) { dispatch(subNumber(num)) } }) export default connect(mapStateToProps,mapDispatchToProps)(Profile);
-
src/store/index.js
- configureStore用于创建store对象,常见参数如下:
- reducer:将slice中的reducer可以组成一个对象传入此处;
- middleware:可以使用参数,传入其他的中间件;
- devTools:是否配置devTools工具,默认为true;
import {configureStore} from "@reduxjs/toolkit"; import counterReducer from "./features/counter"; const store = configureStore({ reducer: { counter: counterReducer } }) export default store
- configureStore用于创建store对象,常见参数如下:
-
src/store/features/counter.js
- createSlice主要包含如下几个参数:
- name:用户标记slice的名词
- 在之后的redux-devtool中会显示对应的名词;
- initialState:初始化值
- 第一次初始化时的值;
- reducers:相当于之前的reducer函数
- 对象类型,并且可以添加很多的函数;
- 函数类似于redux原来reducer中的一个case语句;
- 函数的参数:
- 参数一:state
- 参数二:调用这个action时,传递的action参数;
- name:用户标记slice的名词
- createSlice返回值是一个对象,包含所有的actions;
import {createSlice} from "@reduxjs/toolkit"; const counterSlice = createSlice({ // 模块名 name: 'counter', // 数据的初始值 initialState: { counter: 888 }, // 相当于 reducer 函数 reducers: { // 相当于 case 语句 addNumber(state, {payload}) { state.counter += payload }, subNumber(state, action) { state.counter -= action.payload } } }) export const {addNumber, subNumber} = counterSlice.actions export default counterSlice.reducer
- createSlice主要包含如下几个参数:
10.RTK 异步请求
在组件的生命周期中请求:
-
目录
-
store/features/home.js
import {createSlice} from "@reduxjs/toolkit"; const homeSlice = createSlice({ name: 'home', initialState: { banners: [], recommends: [] }, reducers: { changeBanners(state, {payload}) { state.banners = payload }, changeRecommends(state, {payload}) { state.recommends = payload } } }) export const {changeBanners, changeRecommends} = homeSlice.actions export default homeSlice.reducer
-
store/index.js
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
-
pages/home.jsx
class Home extends PureComponent { componentDidMount() { axios.get("http://123.207.32.32:8000/home/multidata").then(res => { const banners = res.data.data.banner.list; const recommends = res.data.data.recommend.list; this.props.changeBanners(banners) this.props.changeRecommends(recommends) }) } // 略..... } const mapStateToProps = (state) => ({ counter: state.counter.counter }) const mapDispatchToProps = (dispatch) => ({ changeBanners(lists) { dispatch(changeBanners(lists)) }, changeRecommends(lists) { dispatch(changeRecommends(lists)) } }) export default connect(mapStateToProps,mapDispatchToProps)(Home);
-
pages/Profile.jsx
import React, {PureComponent} from 'react'; import {connect} from "react-redux"; import {subNumber} from "../store/features/counter"; class Profile extends PureComponent { subNumber(num) { this.props.subNumber(num) } render() { const {counter, banners, recommends} = this.props // ...... } const mapStateToProps = (state) => ({ counter: state.counter.counter, banners: state.home.banners, recommends: state.home.recommends }) const mapDispatchToProps = (dispatch) => ({ subNumber(num) { dispatch(subNumber(num)) } }) export default connect(mapStateToProps, mapDispatchToProps)(Profile);
在redux中请求:
Redux Toolkit默认已经给我们继承了Thunk相关的功能:createAsyncThunk
-
目录结构:
-
store/features/home.js
- 当createAsyncThunk创建出来的action被dispatch时,会存在三种状态:
- pending:action被发出,但是还没有最终的结果;
- fulfilled:获取到最终的结果(有返回值的结果);
- rejected:执行过程中有错误或者抛出了异常;
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit"; import axios from "axios"; 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 } }, // 类似于switch extraReducers: { [fetchHomeMultidataAction.pending](state,action) { console.log('pending') }, [fetchHomeMultidataAction.fulfilled](state,{payload}) { console.log('fulfilled') state.banners = payload.data.banner.list; state.recommends = payload.data.recommend.list; }, [fetchHomeMultidataAction.rejected](state,action) { console.log('rejected') }, } // 类似与promise链式调用 extraReducers:(builder) => { builder.addCase(fetchHomeMultidataAction.pending,(state,action) => { console.log('pending') }).addCase(fetchHomeMultidataAction.fulfilled,(state, {payload}) => { console.log('fulfilled') state.banners = payload.data.banner.list; state.recommends = payload.data.recommend.list; }).addCase(fetchHomeMultidataAction.rejected,(state,action) => { console.log('rejected') }) } }) export const {changeBanners, changeRecommends} = homeSlice.actions export default homeSlice.reducer
- 当createAsyncThunk创建出来的action被dispatch时,会存在三种状态:
-
pages/home.jsx
import { fetchHomeMultidataAction} from "../store/features/home"; class Home extends PureComponent { componentDidMount() { this.props.fetchHomeMultidata() } render() { const{ counter } = this.props return ( ); } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) const mapDispatchToProps = (dispatch) => ({ fetchHomeMultidata() { dispatch(fetchHomeMultidataAction()) } }) export default connect(mapStateToProps,mapDispatchToProps)(Home);
在redux中:
另一种写法
-
目录结构同上
-
store/features/home.js
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit"; import axios from "axios"; export const fetchHomeMultidataAction = createAsyncThunk("fetch/homemultidata",async (userInfo,{dispatch,getState}) => { const res = await axios.get("http://123.207.32.32:8000/home/multidata") console.log(userInfo) dispatch(homeSlice.actions.changeBanners(res.data.data.banner.list)) dispatch(homeSlice.actions.changeRecommends(res.data.data.recommend.list)) }) const homeSlice = createSlice({ name: 'home', initialState: { banners: [], recommends: [] }, reducers: { changeBanners(state, {payload}) { state.banners = payload }, changeRecommends(state, {payload}) { state.recommends = payload } } }) export const {changeBanners, changeRecommends} = homeSlice.actions export default homeSlice.reducer
-
pages/home.jsx
import { fetchHomeMultidataAction} from "../store/features/home"; class Home extends PureComponent { componentDidMount() { this.props.fetchHomeMultidata() } addNumber(num) { this.props.addNumber(num) } render() { const{ counter } = this.props return ( ); } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) const mapDispatchToProps = (dispatch) => ({ addNumber(num) { dispatch(addNumber(num)) }, fetchHomeMultidata() { dispatch(fetchHomeMultidataAction({name:'Ming'})) } }) export default connect(mapStateToProps,mapDispatchToProps)(Home);
11.RTK的数据不可变
-
在React开发中,我们总是会强调数据的不可变性:
- 无论是类组件中的state,还是redux中管理的state
- 事实上在整个JavaScript编码过程中,数据的不可变性都是非常重要的;
-
所以在前面我们经常会进行浅拷贝来完成某些操作,但是浅拷贝事实上也是存在问题的:
- 比如过大的对象,进行浅拷贝也会造成性能的浪费
- 比如浅拷贝后的对象,在深层改变时,依然会对之前的对象产生影响;
-
事实上Redux Toolkit底层使用了immerjs的一个库来保证数据的不可变性。
-
immutable-js库的底层原理和使用方法:
immutable-js 和 immerjs 差不多
-
为了节约内存,又出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性 数据结构);
- 用一种数据结构来保存数据;
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会 对内存造成浪费;
12.实现一个 react-redux
-
目录结构
-
hoc/connect.js
import {PureComponent} from "react"; import {StoreContext} from "./StoreContext"; /** * 参1:函数 * 参2:函数 * 返回值:函数(高阶组件) */ export function connect(mapStateToProps,mapDispatchToProps) { /** * 参1:接收一个组件 * 返回值:返回一个组件 */ return function (WrapperComponent) { class NewComponent extends PureComponent { constructor(props, context) { super(props); this.state = mapStateToProps(context.getState()) } componentDidMount() { this.unsubscribe = this.context.subscribe(() => { // 强制刷新 // this.forceUpdate() // 利用PureComponent进行优化更新 this.setState(mapStateToProps(this.context.getState())) }) } // 组件卸载时取消监听 componentWillUnmount() { this.unsubscribe() } render() { const stateObj = mapStateToProps(this.context.getState()) const dispatchObj = mapDispatchToProps(this.context.dispatch) return <WrapperComponent {...this.props} {...stateObj} {...dispatchObj}/> } } NewComponent.contextType = StoreContext return NewComponent } }
-
hoc/StoreContext.js
用于创建 context,方便用户传递 store,进行解耦
import {createContext} from "react"; export const StoreContext = createContext()
-
hoc/index.js
暴露统一的接口
export { StoreContext } from './StoreContext' export { connect } from './connect'
-
src/index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import {Provider} from "react-redux"; // 注意!!!!! import {StoreContext} from "./hoc"; import App from './App'; import store from "./store"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <StoreContext.Provider value={store}> <App /> </StoreContext.Provider> </Provider> </React.StrictMode> );
-
pages/About.jsx
import React, {PureComponent} from 'react'; import {connect} from "../hoc"; import {addNumber} from "../store/features/counter"; class About extends PureComponent { addNumber(num) { this.props.addNumber(num) } render() { const {counter} = this.props return ( <div> <h2>About Counter: {counter}</h2> <button onClick={ e => this.addNumber(5)}>+5</button> <button onClick={ e => this.addNumber(50)}>+20</button> <button onClick={ e => this.addNumber(250)}>+50</button> </div> ); } } const mapStateToProps = (state) => { return { counter: state.counter.counter } } const mapDispatchToProps = (dispatch) => ({ addNumber(num) { dispatch(addNumber(num)) } }) export default connect(mapStateToProps,mapDispatchToProps)(About);
13.redux中间件
日志打印:
在dispatch之前,打印一下本次的action对象,dispatch完成之后可以打印一下最新的store state,这样的需求对于调试来说是有一定的必要的。
function log(store) {
const next = store.dispatch
function logAndDispatch(action) {
console.log("当前派发的action:",action)
// 真正派发的代码
next(action);
console.log("派发之后的结果", store.getState())
}
// monkey patching: 猴补丁 => 篡改现有的代码,对现有的函数进行修改
store.dispatch = logAndDispatch
}
thunk函数:
我们知道redux中利用一个中间件redux-thunk可以让我们的dispatch不再只是处理对象,并且可以处理函数。
function thunk(store) {
const next = store.dispatch
function disparchThunk(action) {
if(typeof action === "function") {
action(store.dispatch, store.getState)
} else {
next(action)
}
}
store.dispatch = disparchThunk
}
export default thunk
合并中间件:
一个一个调用中间件过于麻烦,所以封装一个函数进行调用
function applyMiddleware(store,...args) {
args.forEach(fn => {
fn(store)
})
}
// 对每次派发的action进行拦截,进行日志打印
applyMiddleware(store,log, thunk)
14.React中state管理
目前主要三种:
- 方式一:组件中自己的state管理;
- 方式二:Context数据的共享状态;
- 方式三:Redux管理应用状态;
在开发中如何选择呢?
- 首先,这个没有一个标准的答案;
- 某些用户,选择将所有的状态放到redux中进行管理,因为这样方便追踪和共享;
- 有些用户,选择将某些组件自己的状态放到组件内部进行管理;
- 有些用户,将类似于主题、用户信息等数据放到Context中进行共享和管理;
- 做一个开发者,到底选择怎样的状态管理方式,是你的工作之一,可以一个最好的平衡方式(Find a balance that works for you, and go with it.);
推荐方案:
- UI相关的组件内部可以维护的状态,在组件内部自己来维护;
- 大部分需要共享的状态,都交给redux来管理和维护;
- 从服务器请求的数据(包括请求的操作),交给redux来维护
八.React-Router
1.Web发展历史
后端路由阶段:
- 早期的网站开发整个HTML页面是由服务器来渲染的.
- 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示.
- 但是, 一个网站, 这么多页面服务器如何处理呢?
- 一个页面有自己对应的网址, 也就是URL;
- URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理;
- Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.
- 上面的这种操作, 就是后端路由:
- 当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.
- 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
- 后端路由的缺点:
- 一种情况是整个页面的模块由后端人员来编写和维护的;
- 另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码;
- 而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情;
前后端分离:
- 前端渲染的理解:
- 每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染;
- 需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件;
- 同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了;
- 前后端分离阶段:
- 随着Ajax的出现, 有了前后端分离的开发模式;
- 后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中;
- 这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上;
- 并且当**移动端(iOS/Android)**出现后,后端不需要进行任何处理,依然使用之前的一套API即可;
- 目前比较少的网站采用这种模式开发;
- 单页面富应用阶段:
- 其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
- 也就是前端来维护一套路由规则.
2.路由原理
Hash路由:
- URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
- 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
- hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径
History路由:
- history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面
replaceState
:替换原来的路径;pushState
:使用新的路径;popState
:路径的回退;go
:向前或向后改变路径;forward
:向前改变路径;back
:向后改变路径;
3.React-Router基本使用
安装:
- 安装时,我们选择react-router-dom;
- react-router会包含一些react-native的内容,web开发并不需要;
npm install react-router-dom
示例:
3.1基础配置
src/index.js
- BrowserRouter使用history模式;
- HashRouter使用hash模式;
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import {HashRouter,BrowserRouter} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<HashRouter>
<App />
</HashRouter>
</React.StrictMode>
);
3.2路由映射配置
App.jsx
<Routes/>
:包裹所有的Route,在其中匹配一个路由
- Router5.x使用的是Switch组件
<Route/>
:Route用于路径的匹配
- path属性:用于设置匹配到的路径;
*
表示通配- element属性:设置匹配到路径后,渲染的组件;
- Router5.x使用的是component属性
- exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
- Router6.x不再支持该属性,自动进行
import React, {PureComponent} from 'react';
import {Link, Navigate, Route, Routes} from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import './style.css'
import Login from "./pages/Login";
import NotFound from "./pages/NotFound";
class App extends PureComponent {
render() {
return (
<div className='app'>
<header className='header'>
<h2> Header</h2>
<div className='nav'>
<Link to="/home" >
首页
</Link>
<Link to="/about">
关于
</Link>
<Link to="/login">
登录
</Link>
</div>
<hr/>
</header>
<main className='content'>
{
<Routes>
<Route path='/' element={<Navigate to='/home'/>}/>
<Route path='/home' element={<Home/>}/>
<Route path='/about' element={<About/>}/>
<Route path='/login' element={<Login/>}/>
<Route path='*' element={<NotFound/>}/>
</Routes>
}
</main>
<footer className='footer'>
<hr/>
Router
</footer>
</div>
);
}
}
export default App;
3.3路由配置和跳转
src/App.jsx
<Link/>
:通常路径的跳转是使用Link组件,最终会被渲染成a元素;
to属性:Link中最重要的属性,用于设置跳转到的路径;
replace属性:不添加路由记录的跳转
state属性:history路由用的
reloadDocument: 是否刷新操作
< NavLink/>
:在Link基础之上增加了一些样式属性
选中时会添加
active
的class名style:传入函数,函数接受一个对象,包含isActive属性
<NavLink to="/home" style={() => ({color: 'red'})} />
<NavLink to="/about" style={({isActive}) => ({color: isActive ? 'blue' : 'black'})}>
className:传入函数,函数接受一个对象,包含isActive属性
<NavLink to="/home" className={({isActive}) => isActive?"link-active":'' }>
<header className='header'>
<h2> Header</h2>
<div>
<Link to="/home">
首页
</Link>
<Link to="/about">
过于
</Link>
</div>
<hr/>
</header>
<header className='header'>
<h2> Header</h2>
<div className='nav'>
<NavLink to="/home">
首页
</NavLink>
<NavLink to="/about">
关于
</NavLink>
</div>
<hr/>
</header>
.nav .active {
color: red;
font-size: 30px;
}
3.4路由重定向
src/pages/Login.jsx
Navigate
:用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中。
<Route path='/' element={<Navigate to='/home'/>}/>
import React, {PureComponent} from 'react';
import {Navigate} from "react-router-dom";
class Login extends PureComponent {
constructor() {
super();
this.state = {
isLogin: false
}
}
render() {
const {isLogin} = this.state
return (
<div>
<h1>Login Pages</h1>
{!isLogin ? <button onClick={e =>this.setState({isLogin: !this.state.isLogin})}>登录</button> : <Navigate to='/home'/>}
</div>
);
}
}
export default Login;
3.5路由嵌套
路由嵌套是开发中常见的需求,在React-Router6中的配置和5中又有一定的区别,在5中是想要写到对应的组件里的。
src/App.jsx
import React, {PureComponent} from 'react';
import {Link, Navigate, Route, Routes} from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import './style.css'
import Login from "./pages/Login";
import NotFound from "./pages/NotFound";
import HomeRecommend from "./pages/HomeRecommend";
import HomeRanking from "./pages/HomeRanking";
class App extends PureComponent {
render() {
return (
<div className='app'>
<header className='header'>
<h2> Header</h2>
<div className='nav'>
<Link to="/home" >
首页
</Link>
<Link to="/about">
关于
</Link>
<Link to="/login">
登录
</Link>
</div>
<hr/>
</header>
<main className='content'>
{
<Routes>
<Route path='/' element={<Navigate to='/home'/>}/>
<Route path='/home' element={<Home/>}>
<Route path='/home' element={<Navigate to='/home/recommend'/>}/>
<Route path='/home/recommend' element={<HomeRecommend/>}></Route>
<Route path='/home/ranking' element={<HomeRanking/>}></Route>
</Route>
<Route path='/about' element={<About/>}/>
<Route path='/login' element={<Login/>}/>
<Route path='*' element={<NotFound/>}/>
</Routes>
}
</main>
<footer className='footer'>
<hr/>
Router
</footer>
</div>
);
}
}
export default App;
src/pages/home.jsx
<Outlet/>
:占位组件,用于在父路由元素中作为子路由的占位元素。
import React, {PureComponent} from 'react';
import {Link, Outlet} from "react-router-dom";
class Home extends PureComponent {
render() {
return (
<div>
<h2>Home</h2>
<div className='home-nav'>
<Link to='/home/recommend'>推荐</Link>
<Link to='/home/ranking'>排行</Link>
</div>
{/* 占位组件*/}
<Outlet/>
</div>
);
}
}
// 优化:二级路由的懒加载问题
<Suspense fallback="">
<Outlet />
</Suspense>
export default Home;
3.6手动路由跳转
在Router6.x版本之后,代码类的API都迁移到了hooks的写法
useNavigate
函数式组件:
import {Link, Navigate, Route, Routes, useNavigate} from "react-router-dom";
function App() {
const navigate = useNavigate()
function navigateTo(path) {
navigate(path)
}
return (
<div className='app'>
<header className='header'>
<h2> Header</h2>
<div className='nav'>
{/* HooK */}
<button onClick={e => navigateTo('/login') }>登录</button>
</div>
<hr/>
</header>
<main className='content'>
{
<Routes>
<Route path='/' element={<Navigate to='/home'/>}/>
<Route path='/home' element={<Home/>}>
<Route path='/home' element={<Navigate to='/home/recommend'/>}/>
<Route path='/home/recommend' element={<HomeRecommend/>}></Route>
<Route path='/home/ranking' element={<HomeRanking/>}></Route>
</Route>
<Route path='/about' element={<About/>}/>
<Route path='/login' element={<Login/>}/>
<Route path='*' element={<NotFound/>}/>
</Routes>
}
</main>
<footer className='footer'>
<hr/>
Router
</footer>
</div>
);
}
export default App;
类组件:
使用高阶组件进行处理
import React, {PureComponent} from 'react';
import {Link, Outlet, useNavigate} from "react-router-dom";
class Home extends PureComponent {
navigateTo(path) {
const {navigate} = this.props.router
navigate(path)
}
render() {
return (
<div>
<h2>Home</h2>
<div className='home-nav'>
<Link to='/home/recommend'>推荐</Link>
<Link to='/home/ranking'>排行</Link>
<button onClick={e => this.navigateTo('/home/ranking')}>排行</button>
</div>
{/* 占位组件*/}
<Outlet/>
</div>
);
}
}
function withRouter(WrapperComponent) {
return function NewComponent(props) {
const navigate = useNavigate()
const router = {
navigate
}
return <WrapperComponent {...props} router={router}/>
}
}
export default withRouter(Home);
3.7路由跳转传参
-
使用
params
传递就是动态路由,使用
useParams
获取params
对象function withRouter(WrapperComponent) { return function NewComponent(props) { const navigate = useNavigate() const params = useParams() const router = { navigate, params } return <WrapperComponent {...props} router={router}/> } }
<Route path='/detail/:id' element={<Detail/>}/>
navigateTo(id) { const {navigate} = this.props.router navigate(`/detail/${id}`) } <ul> { songMenu.map((item) => { return <li key={item.id} onClick={e => this.navigateTo(item.id)}>{item.name}</li> }) } </ul>
class Detail extends PureComponent { render() { const {params} = this.props.router return ( <div> <h1>Detail</h1> <h2>id:{params.id}</h2> </div> ); } } export default withRouter(Detail)
-
使用
query
传递<Link to="/user?name=ming&age=18">用户</Link>
function withRouter(WrapperComponent) { return function NewComponent(props) { const navigate = useNavigate() // params const params = useParams() // query const location = useLocation() /** * searchParams 是个异构对象 * 所以想要进行转变 */ const [searchParams] = useSearchParams() const query = Object.fromEntries(searchParams.entries()) // for(const item of searchParams.entries()) { // query[item[0]] = item[1] // } const router = { navigate, params, location, query } return <WrapperComponent {...props} router={router}/> } }
class User extends PureComponent { render() { const {name,age} = this.props.router.query return ( <div> <h1>UserInfo</h1> <h2>姓名:{name}</h2> <h2>年龄:{age}</h2> </div> ); } } export default withRouter(User);
3.8 路由配置文件
- 目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。
- 但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
- 在早期的时候,Router并且没有提供相关的API,我们需要借助于react-router-config完成;
- 在Router6.x中,为我们提供了useRoutes API可以完成相关的配置;
示例:
const Discover = React.lazy(() => import('@/view/discover'))
const Mine = React.lazy(() => import('@/view/mine'))
const Focus = React.lazy(() => import('@/view/focus'))
const Download = React.lazy(() => import('@/view/download'))
const routes: RouteObject[] = [
{
path: '/discover',
element: <Discover />
},
{
path: '/mine',
element: <Mine />
},
{
path: '/focus',
element: <Focus />
},
{
path: '/download',
element: <Download />
}
]
import {useRoutes} from "react-router-dom";
<main className='content'>
{useRoutes(routes)}
</main>
3.9路由懒加载
示例:
const Login = React.lazy(() => import("../pages/HomeRecommend"))
<React.StrictMode>
<Suspense fallback={<h1>Loading....</h1>}>
<HashRouter>
<App/>
</HashRouter>
</Suspense>
</React.StrictMode>
九.Hooks
1.关于Hooks
为什么需要Hooks?
-
Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
-
我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
-
class组件可以定义自己的state,用来保存组件自己内部的状态;
- 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
-
class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
- 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
- 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
-
class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
-
函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
-
-
所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
class组件存在的问题:
-
复杂组件变得难以理解:
- 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
- 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount中移除);
- 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
-
难以理解的class:
没有学过真正的面向对象语言,所以可能会存在理解不了的情况
- 很多人发现学习ES6的class是学习React的一个障碍。
- 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
- 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
-
组件复用状态很难:
- 在前面为了一些状态的复用我们需要通过高阶组件;
- 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
- 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
- 这些代码让我们不管是编写和设计上来说,都变得非常困难;
Hook的出现:
- Hook的出现,可以解决上面提到的这些问题;
- 简单总结一下hooks:
- 它可以让我们在不编写class的情况下使用state以及其他的React特性;
- 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
- Hook的使用场景:
- Hook的出现基本可以代替我们之前所有使用class组件的地方;
- 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
- Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
- 在我们继续之前,请记住 Hook 是:
- 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
- 100% 向后兼容的:Hook 不包含任何破坏性改动。
- 现在可用:Hook 已发布于 v16.8.0。
对比:
Hook 就是 JavaScript 函数,这个函数可以帮助你 钩入(hook into) React State以及生命周期等特性;
注意:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
- 自定义Hook中可以使用,以
use
开头的函数
2.useState
-
useState来自react,需要从react中导入,它是一个hook;
-
参数:初始化值,如果不设置为undefined;
-
返回值:数组,包含两个元素;
- 元素一:当前状态的值(第一调用为初始化值);
- 元素二:设置状态值的函数;
const App = memo((props) => {
const [counter,setCounter] =useState(0)
return (
<div>
<h2>当前计数:{counter}</h2>
<button onClick={e => setCounter(counter+1)}>+1</button>
<button onClick={e => setCounter(counter-1)}>-1</button>
</div>
);
})
点击button按钮后,会完成两件事情:
调用setCount,设置一个新的值;
组件重新渲染,并且根据新的值返回DOM结构;
-
State Hook的API就是 useState:
- useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。
- 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
- useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。
-
useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。
-
useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。
3.useEffect
什么是Effect Hook
- Effect Hook 可以让你来完成一些类似于class中生命周期的功能;
- 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
- 所以对于完成这些功能的Hook被称之为 Effect Hook;
useEffect的解析:
- 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
- useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
- 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
const App = memo((props) => {
const [counter,setCounter] =useState(0)
useEffect(()=>{
// 当前传入的回调函数会在组件被渲染完成后自动完成,第一次以及后面都会执行
// 网络请求DOM操作事件监听等 副作用函数
document.title = counter.toString()
})
return (
<div>
<h2>当前计数:{counter}</h2>
<button onClick={e => setCounter(counter+1)}>+1</button>
<button onClick={e => setCounter(counter-1)}>-1</button>
</div>
);
})
需要清除Effect:
- 在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除
- 比如我们之前的事件总线或Redux中手动调用subscribe;
- 都需要在componentWillUnmount有对应的取消订阅;
- Effect Hook通过什么方式来模拟componentWillUnmount呢?
- useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:
type EffectCallback = () => (void | (() => void | undefined));
如下,如果没有取消事件的监听,则会给
#root
重复添加多次的点击监听事件,导致一次会多次触发 fn 的打印,所以需要进行取消监听。
useEffect(() => {
const fn = () => {
console.log('bbbb')
}
document.querySelector('#root').addEventListener('click', fn);
return () => {
document.querySelector('#root').removeEventListener('click',fn)
}
})
- 为什么要在 effect 中返回一个函数?
- 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
- 如此可以将添加和移除订阅的逻辑放在一起;
- 它们都属于 effect 的一部分;
- React 何时清除 effect?
- React 会在组件更新和卸载的时候执行清除操作;
- 正如之前学到的,effect 在每次渲染的时候都会执行;
使用多个Effect:
单个Effect中,书写过多的逻辑会使得代码可读性很差。
- 使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:
- 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;
- Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:
- React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;
useEffect(() => {
console.log('bbbb')
return () => {
console.log('清除bbbb')
}
})
useEffect(() => {
console.log('cccc')
return () => {
console.log('清除ccccc')
}
})
Effect性能优化:
- 默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
- 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);
- 另外,多次执行也会导致一定的性能问题;
- useEffect实际上有两个参数:
- 参数一:执行的回调函数;
- 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
const App = memo((props) => {
const [counter, setCounter] = useState(0)
const [message,setMessage] = useState('Hello world')
useEffect(() => {
console.log('只要重新渲染就执行')
return () => {
console.log('只要重新渲染就执行')
}
})
useEffect(() => {
console.log('counter改变')
return () => {
console.log('组件卸载才执行')
}
},[counter])
useEffect(() => {
console.log('message改变')
return () => {
console.log('组件卸载才执行')
}
},[message])
return (
<div>
<h2>当前计数:{counter}</h2>
<h2>当前message:{message}</h2>
<button onClick={e => setCounter(counter + 1)}>+1</button>
<button onClick={e => setCounter(counter - 1)}>-1</button>
<button onClick={e => setMessage('Hello Mingcomity')}>修改message</button>
</div>
);
})
- 如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:
- 那么这里的两个回调函数分别对应的就是componentDidMount 和 componentWillUnmount生命周期函数了;
const App = memo((props) => {
const [counter, setCounter] = useState(0)
// 修改 counter 重复执行
useEffect(() => {
console.log('aaaa')
return () => {
console.log('清除aaaa')
}
})
// 修改 counter 不会重复执行
useEffect(() => {
console.log('bbbb')
return () => {
// 仅在卸载时才执行
console.log('清除bbbb')
}
},[])
return (
<div>
<h2>当前计数:{counter}</h2>
<button onClick={e => setCounter(counter + 1)}>+1</button>
<button onClick={e => setCounter(counter - 1)}>-1</button>
</div>
);
})
4.useContext
-
在之前的开发中,我们要在组件中使用共享的Context有两种方式:
- 类组件可以通过
类名.contextType = MyContext
方式,在类中获取context; - 多个Context或者在函数式组件中通过
MyContext.Consumer
方式共享context;
<UserContext.Provider value={{name: 'ming',age:19}}> <ThemeContext.Provider value={{color: 'red',fontSize: 30}}> <App /> </ThemeContext.Provider> </UserContext.Provider>
function App(props) { return ( <div> <UserContext.Consumer> { value => { return ( <h2> <div> name:{value.name} age:{value.age} </div> <ThemeContext.Consumer> { value1 => { return ( <div> color:{value1.color} fontSize: {value1.fontSize} </div> ) } } </ThemeContext.Consumer> </h2> ) } } </UserContext.Consumer> </div> ); }
- 类组件可以通过
-
但是多个Context共享时的方式会存在大量的嵌套:
- Context Hook允许我们通过Hook来直接获取某个Context的值;
import React, {memo, useContext} from 'react'; import {ThemeContext, UserContext} from "./context"; function App(props) { // 使用Context const user = useContext(UserContext) const theme = useContext(ThemeContext) return ( <div> <h2>User: {user.name}-{user.age}</h2> <h2 style={{color:theme.color}}>color: {theme.color}-{theme.fontSize}</h2> </div> ); }
-
注意事项:
-
当组件上层最近的
<MyContext.Provider>
更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。类似于Vue的双向绑定自动更新视图了
-
5.useReducer
和redux没啥关系
-
useReducer仅仅是useState的一种替代方案:
- 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
- 或者这次修改的state需要依赖之前的state时,也可以使用;
const App = memo((props) => { const [counter,setCounter] =useState(0) return ( <div> <h2>当前计数:{counter}</h2> <button onClick={e => setCounter(counter+1)}>+1</button> <button onClick={e => setCounter(counter-1)}>-1</button> </div> ); })
function reducer(state, action) { switch (action.type) { case 'increment': return {...state,counter: state.counter + 1} case 'decrement': return {...state,counter: state.counter - 1} case 'message': return {...state,message: 'message'} default: return state } } const App = memo((props) => { const [state,dispatch] = useReducer(reducer,{counter: 0,message:'?',user: {}}) return ( <div> <h2>当前计数:{state.counter}</h2> <h2>当前计数:{state.message}</h2> <button onClick={e => dispatch({type: 'increment'})}>+1</button> <button onClick={e => dispatch({type: 'decrement'})}>-1</button> <button onClick={e => dispatch({type: 'message'})}>修改message</button> </div> ); })
6.useCallback
useCallback实际的目的是为了进行性能的优化。
-
如何进行性能的优化呢?
- useCallback会返回一个函数的 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
const App = memo((props) => { const [counter,setCounter] =useState(0) // 重复执行时,函数会多次定义,有垃圾回收器使用也会被重新销毁 function increment() { setCounter(counter + 1) } // 使用useCallback,依赖不变的情况下,多次定义的时候,返回的值是相同,但其实在传参时依然重复定义了。。所以也没有优化 const increment = useCallback(function increment() { setCounter(counter + 1) }) return ( <div> <h2>当前计数:{counter}</h2> <button onClick={increment}>+1</button> <button onClick={e => setCounter(counter-1)}>-1</button> </div> ); })
const App = memo((props) => { const [counter,setCounter] =useState(0) const increment = useCallback(function increment() { console.log('sssss') setCounter(counter + 1) },[]) return ( <div> <h2>当前计数:{counter}</h2> <button onClick={increment}>+1</button> <button onClick={e => setCounter(counter-1)}>-1</button> </div> ); })
在这里传入一个依赖时,因为依赖没有改变,所以产生了闭包陷阱。
-
通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;
定义函数不会带来优化!!!
// 当props发生改变时,就会被重新渲染 const MCIncrement = memo(function (props) { const {increment} = props console.log('函数重新被渲染') return (<div> <button onClick={increment}>+1</button> </div>) }) const App = memo((props) => { const [counter, setCounter] = useState(0) const [message, setMessage] = useState('hello') // 重复执行时,函数会多次定义,有垃圾回收器使用也会被重新销毁 function increment() { setCounter(counter + 1) } return ( <div> <h2>当前计数:{counter}</h2> <h2>当前Message:{message}</h2> <button onClick={e => setMessage('你好')}>修改message</button> <button onClick={increment}>+1</button> <button onClick={e => setCounter(counter - 1)}>-1</button> <MCIncrement increment={increment}></MCIncrement> </div> ); })
在这种情况下,修改message也会导致子组件
<MCIncrement/>
的重新渲染(因为props传入的值发生了改变),造成性能浪费。const increment = useCallback(function increment() { console.log('sssss') setCounter(counter + 1) }, [counter])
使用 useCallback 时,因为 increment 依然是旧的值,所以子组件
<MCIncrement/>
不会被重新渲染(因为props传入的值没有改变)const App = memo((props) => { const [counter, setCounter] = useState(0) const [message, setMessage] = useState('hello') // 进一步的优化:当counter发生改变时,也使用同一个函数,可以使子组件不重新渲染 // 做法一,将counter依赖移除掉,缺点:闭包陷阱 // 做法二:使用 useRef,在父组件多次重新渲染时,返回的是同一个值 const countRef= useRef() countRef.current = counter const increment = useCallback(function increment() { console.log('sssss') setCounter(countRef.current + 1) }, []) return ( <div> <h2>当前计数:{counter}</h2> <h2>当前Message:{message}</h2> <button onClick={e => setMessage('你好')}>修改message</button> <button onClick={increment}>+1</button> <button onClick={e => setCounter(counter - 1)}>-1</button> <MCIncrement increment={increment}></MCIncrement> </div> ); })
进一步优化,使用useRef
7.useMemo
useMemo实际的目的也是为了进行性能的优化。
-
如何进行性能的优化呢?
-
useMemo返回的也是一个 memoized(记忆的) 值;
-
在依赖不变的情况下,多次定义的时候,返回的值是相同的;
useCallback(fn, []) = useMemo(() => fn,[])
- 进行大量的计算操作,是否有必须要每次渲染时都重新计算;
function clacNumTotal(num) {
let total = 0
console.log('计算被重新调用')
for(let i = 1; i<=num;i++) {
total +=1
}
return total
}
const App = memo((props) => {
const [count,setCount] =useState(0)
// useMemo 优化的是返回结果,也需要传入依赖
let result = useMemo(() => {
return clacNumTotal(50)
},[])
return (
<div>
<h2>计算结果:{result}</h2>
<h2>计算结果:{count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<button onClick={e => setCount(count-1)}>-1</button>
</div>
);
})
对于依赖不改变的情况下,参数一的函数是不会进行重新运算的,所以其返回的结果也是不变的
- 对子组件传递相同内容的对象时,使用useMemo进行性能的优化
const Hello = memo(function (props) {
console.log('子组件被重新渲染')
return <div>子组件</div>
})
const App = memo((props) => {
const [count,setCount] =useState(0)
const userInfo = {name:'zs',age:18}
return (
<div>
<h2>计算结果:{count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<button onClick={e => setCount(count-1)}>-1</button>
<Hello userInfo={userInfo}></Hello>
</div>
);
})
每次 count 变化时,子组件都要进行重新渲染
const userInfo = useMemo(() => ({name: 'zs', age: 18}), [])
由于传入的是同一个对象,所以子组件不会进行重新渲染。(memo包裹的情况下)
8.useRef
TS支持:
import { memo, useRef } from 'react'
import { Carousel } from 'antd'
import type { FC, ReactNode, ElementRef } from 'react'
import { BannerControl, BannerLeft, BannerRight, BannerWrapper } from './style'
import { useAppSelector } from '@/store'
import { shallowEqual } from 'react-redux'
interface IProps {
children?: ReactNode
}
const TopBanner: FC<IProps> = () => {
// 绑定组件
const bannnerRef = useRef<ElementRef<typeof Carousel>>(null)
const { banners } = useAppSelector(
(state) => ({
banners: state.recommend.banners
}),
shallowEqual
)
// 时间处理函数
function handlerPrevClick() { }
function handlerNextClick() { }
return (
<BannerWrapper>
<div className="banner wrap-v2">
<BannerLeft>
<Carousel autoplay ref={bannnerRef}>
{banners.map((item) => {
return (
<div className="banner-item" key={item.imageUrl}>
<img
className="image"
src={item.imageUrl}
alt={item.typeTitle}
/>
</div>
)
})}
</Carousel>
</BannerLeft>
<BannerRight></BannerRight>
<BannerControl>
<button className="btn left" onClick={handlerPrevClick}></button>
<button className="btn right" onClick={handlerNextClick}></button>
</BannerControl>
</div>
</BannerWrapper>
)
}
export default memo(TopBanner)
-
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
-
最常用的ref是两种用法:
-
用法一:引入DOM(或者组件,但是需要是class组件)元素;
const App = memo((props) => { const [counter,setCounter] =useState(0) const titleRef = useRef(null) const inputRef = useRef(null) return ( <div> <h2 ref={titleRef}>当前计数:{counter}</h2> <input type='text' ref={inputRef}/> <button onClick={e => inputRef.current.focus()}>获取input的焦点</button> <button onClick={e => console.log(titleRef)}>查看h2的DOM</button> </div> ); })
-
用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
也可以解决闭包陷阱
let obj = null const App = memo((props) => { const [counter,setCounter] =useState(0) const nameref = useRef() console.log(obj === nameref) // true obj = nameref return ( <div> <h2>当前计数:{counter}</h2> <button onClick={e => setCounter(counter+1)}>+1</button> </div> ); })
counter 改变时,始终打印 true
-
9.uselmperativeHandle
typescript: https://www.xstnet.com/article-160.html
-
回顾ref和foreardRef的结合使用:
- 通过forwardRef可以将ref转发到子组件;
- 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
const HelloWorld = memo(forwardRef((props,ref) => { return <input type='text' ref={ref}/> })) const App = memo((props) => { const titleRef = useRef() const inputRef = useRef() function handleDOM() { console.log(titleRef.current) } function handleDOM2() { console.log(inputRef.current) } return ( <div> <h2 ref={titleRef}>title</h2> <button onClick={handleDOM}>获取titleDOM</button> <HelloWorld ref={inputRef}/> <button onClick={handleDOM2}>获取inputDOM</button> </div> ); })
forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件
- 直接暴露给父组件带来的问题是某些情况的不可控;
- 父组件可以拿到DOM后进行任意的操作;
- 但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作;
function handleDOM2() { console.log(inputRef.current) inputRef.current.focus() inputRef.current.value = 'aaaaa' }
第三个操作要禁止掉呢
-
通过useImperativeHandle可以值暴露固定的操作:
- 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;
- 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;
- 比如我调用了 focus函数,甚至可以调用 printHello函数;
const HelloWorld = memo(forwardRef((props, ref) => { const inputRef = useRef() useImperativeHandle(ref, () => { return { focus() { console.log('获取焦点') inputRef.current.focus() }, setValue(value) { inputRef.current.value = value } } }) return <input type='text' ref={inputRef}/> })) const App = memo((props) => { const titleRef = useRef() const inputRef = useRef() function handleDOM2() { console.log(inputRef.current) inputRef.current.focus() inputRef.current.value = 'aaaaa' } return ( <div> <HelloWorld ref={inputRef}/> <button onClick={handleDOM2}>获取inputDOM</button> </div> ); })
指定暴露了两个方法,变得更加安全
10.useLayoutEffect
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已
-
useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
-
useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
const App = memo((props) => { const [counter, setCounter] = useState(0) useEffect(() => { console.log('3.useEffect') }) useLayoutEffect(() => { console.log('2.useLayoutEffect') }) console.log('1.App render') return ( <div> <h2>当前计数:{counter}</h2> <button onClick={e => setCounter(counter + 1)}>+1</button> </div> ); })
-
如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
11.自定义Hook
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。
注意:use开头!!!
12.Redux 中的 Hook
TS支持:
import { configureStore } from '@reduxjs/toolkit'
import {
useSelector,
TypedUseSelectorHook,
useDispatch,
shallowEqual
} from 'react-redux'
import counterReducer from './modules/counter'
// 获取返回值类型
type IRootStore = ReturnType<typeof store.getState>
type DispatchType = typeof store.dispatch
// 自动类型推导
export const useAppSelector: TypedUseSelectorHook<IRootStore> = useSelector
export const useAppDispatch: () => DispatchType = useDispatch
export const shallowEqualApp = shallowEqual
const store = configureStore({
reducer: {
counter: counterReducer
}
})
export default store
useSelector:
将state映射到组件
-
参数:
-
参数一:将state映射到需要的数据中;
-
参数二:可以进行比较来决定是否组件重新渲染;
浅层比较
-
-
useSelector默认会比较我们返回的两个对象是否相等;
- 如何比较呢? const refEquality = (a, b) => a === b;
- 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;
useDispatch:
直接获取dispatch函数
useStore:
直接获取store对象
示例:
简单使用示例
import React, {memo, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {addNumber, subNumber} from "./store/features/counter";
const App = memo((props) => {
// 使用useSelector将redux中store的数据映射出来
const {counter} = useSelector((state) => ({
counter: state.counter.counter
}))
// 使用dispatch直接派发
const dispatch = useDispatch()
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumber(num))
} else {
dispatch(subNumber(num))
}
}
return (
<div>
<h2>当前计数:{counter}</h2>
<button onClick={e => addNumberHandle(1)}>+1</button>
<button onClick={e => addNumberHandle( 1,false)}>-1</button>
</div>
);
})
export default App;
进行性能优化后的示例:
import React, {memo} from 'react';
import {useDispatch, useSelector,shallowEqual} from "react-redux";
import {addNumber, changeMessageAction, subNumber} from "./store/features/counter";
const Home = memo((props) => {
// 默认情况下,useSelect 监听的是整个state,只要state发生了改变就会重新渲染,使用 shallowEqual 就能
const {message} = useSelector((state)=>({
message:state.counter.message
}),shallowEqual)
const dispatch = useDispatch()
function changeMessage(str) {
dispatch(changeMessageAction(str))
}
console.log('Home-Render')
return (
<div>
<h2>Home: {message}</h2>
<button onClick={e=>changeMessage('你好哇!')}>修改message</button>
</div>
)
})
const App = memo((props) => {
// 使用useSelector将redux中store的数据映射出来
const {counter} = useSelector((state) => ({
counter: state.counter.counter
}),shallowEqual)
// 使用dispatch直接派发
const dispatch = useDispatch()
console.log('App-Render')
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumber(num))
} else {
dispatch(subNumber(num))
}
}
return (
<div>
<h2>当前计数:{counter}</h2>
<button onClick={e => addNumberHandle(1)}>+1</button>
<button onClick={e => addNumberHandle( 1,false)}>-1</button>
<Home/>
</div>
);
})
export default App;
13.了解SSR
- 什么是SSR?
- SSR(Server Side Rendering,服务端渲染),指的是页 面在服务器端已经生成了完成的HTML页面结构,不需要浏 览器解析;
- 对应的是CSR(Client Side Rendering,客户端渲染), 我们开发的SPA页面通常依赖的就是客户端渲染;
- 早期的服务端渲染包括PHP、JSP、ASP等方式,但是在目前前 后端分离的开发模式下,前端开发人员不太可能再去学习PHP、 JSP等技术来开发网页;
- 不过我们可以借助于Node来帮助我们执行JavaScript代码,提 前完成页面的渲染
- 什么是同构?
- 一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。
- 同构是一种SSR的形态,是现代SSR的一种表现形式。
- 当用户发出请求时,先在服务器通过SSR渲染出首页的内容。
- 但是对应的代码同样可以在客户端被执行。 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染;
什么是Hydration?
-
在进行 SSR 时,我们的页面会呈现为 HTML。
-
但仅 HTML 不足以使页面具有交互性。例如,浏览器端 JavaScript 为零的页面不能是交互式的(没有 JavaScript 事件处理程序来响应用 户操作,例如单击按钮)。
-
为了使我们的页面具有交互性,除了在 Node.js 中将页面呈现为 HTML 之外,我们的 UI 框架(Vue/React/...)还在浏览器中加载和呈现 页面。(它创建页面的内部表示,然后将内部表示映射到我们在 Node.js 中呈现的 HTML 的 DOM 元素。)
这个过程称为hydration。
-
14.useId
useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。
所以我们可以得出如下结论:
- useId是用于react的同构应用开发的,前端的SPA页面并不需要使用它;
- useId可以保证应用程序在客户端和服务器端生成唯一的ID,这样可以有效的避免通过一些手段生成的id不一致,造成 hydration mismatch;
15.useTransition
返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。
- 在告诉react对于某部分任务的更新优先级较低,可以稍后进行更新。
import React, {memo, useState, useTransition} from 'react';
import namesArray from "./namesArray";
const App = memo((props) => {
const [showNames, setShowNames] = useState(namesArray)
const [pending,setTransition] = useTransition()
function saiXuan(e) {
setTransition(() => {
const str = e.target.value
const newArr = namesArray.filter(item => {
return item.includes(str)
})
setShowNames(newArr)
})
}
return (
<div>
<input type='text' onInput={e => saiXuan(e)}/>
<h2>用户名列表:{pending && <span>数据正在加载!!!!!</span>}</h2>
<ul>
{
showNames.map((item,index) => {
return <li key={index}>{item}</li>
})
}
</ul>
</div>
);
})
export default App;
16.useDeferredValue
接收一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。
- 在明白了useTransition之后,我们就会发现useDeferredValue的作用是一样的效果,可以让我们的更新延迟。
import React, {memo, useDeferredValue, useState, useTransition} from 'react';
import namesArray from "./namesArray";
const App = memo((props) => {
const [showNames, setShowNames] = useState(namesArray)
const deferedShowNames = useDeferredValue(showNames)
function saiXuan(e) {
const str = e.target.value
const newArr = namesArray.filter(item => {
return item.includes(str)
})
setShowNames(newArr)
}
return (
<div>
<input type='text' onInput={e => saiXuan(e)}/>
<h2>用户名列表:</h2>
<ul>
{
deferedShowNames.map((item,index) => {
return <li key={index}>{item}</li>
})
}
</ul>
</div>
);
})
export default App;
十.项目搭建
1.普通项目搭建
-
创建项目
上述已说
-
项目结构
-
jsconfig.json
{ "compilerOptions": { "target": "es5", "module": "esnext", "baseUrl": "./", "moduleResolution": "node", "paths": { "@/*": [ "src/*" ] }, "jsx": "preserve", "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] } }
-
-
配置别名
// @ => src: webpack // 问题:react 脚手架隐藏 webpack // 解决一: npm run eject // 解决二: craco => create-react-app config
-
npm i @craco/craco —D
提示不支持可安装
npm i @craco/craco@alpha -D
-
根目录创建
craco.config.js
文件const path = require('path') const resolve = pathname => path.resolve(__dirname,pathname) module.exports = { // less // webpack webpack: { alias: { "@": resolve("src"), "components": resolve("src/components"), "utils": resolve("src/utils") } } }
-
修改启动配置
"scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" },
-
-
less 配置
-
npm i craco-less
如遇版本问题可下载
craco-less@2.1.0-alpha.0
-
配置
craco.config.js
const path = require('path') const CracoLessPlugin = require('craco-less'); const resolve = pathname => path.resolve(__dirname,pathname) module.exports = { // less plugins: [ { plugin: CracoLessPlugin, // antd 里的配置 // options: { // lessLoaderOptions: { // lessOptions: { // modifyVars: { '@primary-color': '#1DA57A' }, // javascriptEnabled: true, // }, // }, // }, }, ], // webpack webpack: { alias: { "@": resolve("src"), "components": resolve("src/components"), "utils": resolve("src/utils") } } }
-
-
css 样式重置
-
normalize.css
npm i normalize.css
-
reset.css
用于自己的样式表重置css
@import "./variables.less"; body, button, dd, dl, dt, fieldset, form, h1, h2, h3, h4, h5, h6, hr, input, legend, li, ol, p, pre, td, textarea, th, ul { padding: 0; margin: 0; } a { color: @textColor; text-decoration: none; } img { vertical-align: top; } ul,li { list-style: none; }
-
-
关于本地图片引入
import { styled } from 'styled-components' import coverImg from '@/assets/img/cover_01.jpeg' export const HomeBannerWrapper = styled.div` height: 529px; background: url(${require('@/assets/img/cover_01.jpeg')}) center/cover; background: url(${require('@/assets/img/cover_01.jpeg').default}) center/cover; background: url(${coverImg}) center/cover; `
import React, { memo } from 'react' import { HomeBannerWrapper } from './style' import coverImg from '@/assets/img/cover_01.jpeg' const HomeBanner = memo(() => { return ( <HomeBannerWrapper> <img src={coverImg} alt=''></img> </HomeBannerWrapper> ) }) export default HomeBanner
2.TS项目搭建
-
创建项目
create-react-app xxxxx --template typescript
-
craco 修改webpack配置
如上
-
集成editorconfig配置
EditorConfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。
- VSCode需要安装一个插件:EditorConfig for VS Code
# http://editorconfig.org root = true [*] # 表示所有文件适用 charset = utf-8 # 设置文件字符集为 utf-8 indent_style = space # 缩进风格(tab | space) indent_size = 2 # 缩进大小 end_of_line = lf # 控制换行类型(lf | cr | crlf) trim_trailing_whitespace = true # 去除行尾的任意空白字符 insert_final_newline = true # 始终在文件末尾插入一个新行 [*.md] # 表示仅 md 文件适用以下规则 max_line_length = off trim_trailing_whitespace = false
-
使用prettier工具
Prettier 是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。
1.安装prettier
npm install prettier -D
2.配置.prettierrc文件:
- useTabs:使用tab缩进还是空格缩进,选择false;
- tabWidth:tab是空格的情况下,是几个空格,选择2个;
- printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
- singleQuote:使用单引号还是双引号,选择true,使用单引号;
- trailingComma:在多行输入的尾逗号是否添加,设置为
none
,比如对象类型的最后一个属性后面是否加一个,; - semi:语句末尾是否要加分号,默认值true,选择false表示不加;
{ "useTabs": false, "tabWidth": 2, "printWidth": 80, "singleQuote": true, "trailingComma": "none", "semi": false }
3.创建.prettierignore忽略文件
/dist/* .local .output.js /node_modules/** **/*.svg **/*.sh /public/*
3.VSCode需要安装prettier的插件
4.VSCod中的配置
- settings =>format on save => 勾选上
- settings => editor default format => 选择 prettier
5.测试prettier是否生效
- 测试一:在代码中保存代码;
- 测试二:配置一次性修改的命令;
在package.json中配置一个scripts:
"prettier": "prettier --write ."
-
安装ESLint
-
npm i eslint -D npx eslint --init
- 仅仅检查语法
- 检查语法进行报错(一般用这个)
- 检查语法找到错误且强制格式化
- ES规范
- cjs规范
-
.eslintrc.js
module.exports = { env: { browser: true, es2021: true, // 解决cjs模块问题 node: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', // 在React@17以后,是不需要再手动去引入React的。因为该版本之后加入了react/jsx-runtime,会自动对JSX进行解析。 'plugin:react/jsx-runtime', 'plugin:react/recommended', // 添加与prettier 'plugin:prettier/recommended' ], overrides: [ { env: { node: true }, files: ['.eslintrc.{js,cjs}'], parserOptions: { sourceType: 'script' } } ], parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, plugins: ['@typescript-eslint', 'react'], rules: { '@typescript-eslint/no-var-requires': 0 } }
settings.json
"eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ],
-
eslint与prettier一致
安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)
extends: [ ......, 'plugin:prettier/recommended' ],
-
-
样式重置以及less配置
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
述文本描述文本描述文本描述文本描述文本描述文本
述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
查看全部3条回复
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本
描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本描述文本