文章目录
React 入门
React 简介
React 为何物
React:用于构建用户界面的 JavaScript 库。由 Facebook
开发且开源。
为何学习 React
原生 JavaScript 的痛点:
- 操作 DOM 繁琐、效率低
- 使用 JavaScript 直接操作 DOM,浏览器进行大量重绘重排
- 原生 JavaScript 没有组件化编码方案,代码复用率低
React 的特点:
- 采用组件化模式、声明式编码,提高开发效率和组件复用率
- 在
React Native
中可用 React 语法进行移动端开发 - 使用虚拟 DOM 和 Diffing 算法,减少与真实 DOM 的交互
React 初体验
来一发 Hello React
相关 JS 库:
react.development.js
:React 核心库react-dom.development.js
:提供 DOM 操作的 React 扩展库babel.min.js
:解析 JSX 语法,转换为 JS 代码
<!-- 准备好一个“容器” -->
<div id="test"></div>
<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>
<!-- 此处一定要写babel,表示写的不是 JS,而是 JSX,并且靠 babel 翻译 -->
<script type="text/babel">
//1.创建虚拟DOM
// 不要写引号,因为不是字符串
const VDOM = <h1>Hello,React</h1>
//2.渲染虚拟DOM到页面
// 导入核心库和扩展库后,会有 React 和 ReactDOM 两个对象
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
创建虚拟 DOM 的两种方式:JS 和 JSX
- 使用 JS 创建虚拟 DOM 比 JSX 繁琐
- JSX 可以让程序员更加简单地创建虚拟 DOM,相当于语法糖
- 最终 babel 会把 JSX 语法转换为 JS
JS创建
<script type="text/javascript">
//1.使用 React 提供的 API 创建虚拟DOM
const VDOM = React.createElement('h1', { id: 'title' }, React.createElement('span', {}, 'Hello,React'))
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
JSX创建(最终babel转为js)
<script type="text/babel">
//1.创建虚拟DOM
const VDOM = (
<h1 id="title">
<span>Hello,React</span>
</h1>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
虚拟 DOM && 真实 DOM
关于虚拟 DOM:
- 本质是 Object 类型的对象(一般对象)
- 虚拟 DOM 比较“轻”,真实 DOM 比较“重”,因为虚拟 DOM 是 React 内部在用,无需真实 DOM 上那么多的属性。
- 虚拟 DOM 最终会被 React 转化为真实 DOM,呈现在页面上。
<script >
const VDOM = (
<h1 id="title">
<span>Hello,React</span>
</h1>
)
ReactDOM.render(VDOM, document.getElementById('test'))
const TDOM = document.getElementById('demo')
console.log('虚拟DOM', VDOM)
console.log('真实DOM', [TDOM])
</script>
JSX
JSX 简介
-
全称:JavaScript XML
-
React 定义的类似于 XML 的 JS 扩展语法;本质是
React.createElement()
方法的语法糖XML :
<note> <to>George</to> <from>John</from> <heading>Reminder</heading> <body>Don't forget the meeting!</body> </note>
-
作用:简化创建虚拟 DOM
JSX 语法规则
- 定义虚拟 DOM 时,不要写引号
- 标签中混入 JS 表达式需要使用
{}
- 指定类名不用
class
,使用className
- 内联样式,使用
style={ { key: value } }
的形式 - 只能有一个根标签
- 标签必须闭合,单标签结尾必须添加
/
:<input type="text" />
- 标签首字母小写,则把标签转换为 HTML 对应的标签,若没有,则报错
- 标签首字母大写,则渲染对应组件,若没有定义组件,则报错
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>jsx语法规则</title>
<style>
.title {
background-color: orange;
width: 200px;
}
</style>
</head>
<body>
<div id="test"></div>
...
<script >
const myId = 'aTgUiGu'
const myData = 'HeLlo,rEaCt'
const VDOM = (
<div>
<h2 className="title" id={myId.toLowerCase()}>
<span style={{ color: 'white', fontSize: '19px' }}>{myData.toLowerCase()}</span>
</h2>
<input type="text" />
// <good>very good</good>
// <Child></Child>
</div>
)
ReactDOM.render(VDOM, document.getElementById('test'))
</script>
</body>
</html>
JSX 例子
注意区分:JS 语句(代码) 与 JS 表达式:
- 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方
a
a + b
demo(1)
arr.map()
function test() {}
2. 语句(代码):
if(){}
for(){}
switch(){case:xxxx}
3.循环遍历数组
<script >
let list = ['Angular', 'React', 'Vue']
const VDOM = (
<div>
<h1>前端js框架列表</h1>
<ul>
// React 会自动遍历数组
{list.map((item, index) => {
// Each child in a list should have a unique "key" prop.
return <li key={index}>{item}</li>
})}
</ul>
</div>
) ReactDOM.render(VDOM, document.getElementById('test'))
</script>
React 面向组件编程
函数式组件
<script >
//1.创建函数式组件
function MyComponent() {
//此处的 this 是 undefined,因为 babel 编译后开启了严格模式
console.log(this)
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('test'))
/*
渲染组件的过程:
- React 解析标签,寻找对应组件
- 发现组件是函数式组件,则调用函数,将返回的虚拟 DOM 转换为真实 DOM ,并渲染到页面中
*/
</script>
要点:
- 组件名称首字母必须大写,否则会解析成普通标签导致报错,详见 JSX 语法规则
- 函数需返回一个虚拟 DOM
- 渲染组件时需要使用标签形式,同时标签必须闭合
渲染组件的过程:
- React 解析标签,寻找对应组件
- 发现组件是函数式组件,则调用函数,将返回的虚拟 DOM 转换为真实 DOM ,并渲染到页面中
类式组件
<script>
// 创建类式组件
// 不同点 要class 类并且继承React.Component
class MyComponent extends React.Component {
render() {
console.log('render中的this:', this) // this 指向实例对象
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('test'))
</script>
组件渲染过程:
- React 解析组件标签,寻找组件
- 发现是类式组件,则
new
该类的实例对象,通过实例调用原型上的render
方法 - 将
render
返回的虚拟 DOM 转为真实 DOM ,渲染到页面上
三大组件实例核心属性 --state
state
是组件实例对象最重要的属性,值为对象。又称为状态机,通过更新组件的 state
来更新对应的页面显示。
要点:
- 初始化
state
- React 中事件绑定
this
指向问题setState
修改state
状态constructor
、render
、自定义方法的调用次数
<script >
class Weather extends React.Component {
// 调用一次
constructor(props) {
super(props)
// 初始化 state
this.state = { isHot: true, wind: '微风' }
// 解决 this 指向问题 不加这个的话,函数是开启严格模式下的直接调用this会输出undefined
this.changeWeather = this.changeWeather.bind(this)
}
// 调用 1+N 次
render() {
// 读取状态
const { isHot } = this.state
// 事件绑定
return <h1 onClick={this.changeWeather}>今天天气 {isHot ? '炎热' : '凉爽'}</h1>
/* onClick={this.changeWeather} 不加this的话调用不到函数
等于play(){
这样直接调用调用不到原型上的study一样,得加this才能调用到
study()
}
*/
}
// 点一次调一次
changeWeather() {
const isHot = this.state.isHot
// 对 state 的修改是一种合并而非替换,即 wind 依然存在
this.setState({ isHot: !isHot })
}
}
ReactDOM.render(<Weather />, document.getElementById('test'))
</script>
简化版:
<script>
class Weather extends React.Component {
state = { isHot: true, wind: '微风' }
render() {
const { isHot } = this.state
return <h2 onClick={this.changeWeather}>天气{isHot ? '炎热' : '凉爽'}</h2>
}
// 采用箭头函数 + 赋值语句形式
changeWeather = () => {
const isHot = this.state.isHot
this.setState = { isHot: !isHot }
}
}
ReactDOM.render(<Weather />, document.getElementById('test'))
</script>
组件实例核心属性 props
每个组件对象都有 props
属性,组件标签的属性都保存在 props
中。
props
是只读的,不能修改。
props 基本使用
<script>
class Person extends React.Component {
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
// 类似于标签属性传值
ReactDOM.render(<Person name="Lily" age={19} sex="男" />, document.getElementById('test'))
</script>
批量传递 props
<script >
class Person extends React.Component {
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
const obj = { name: 'Ben', age: 21, sex: '女' }
ReactDOM.render(<Person {...obj} />, document.getElementById('test'))
</script>
限制 传递的props
在 React 15.5
以前,React
身上有一个 PropTypes
属性可直接使用,即 name: React.PropTypes.string.isRequired
,没有把 PropTypes
单独封装为一个模块。
从 React 15.5
开始,把 PropTypes
单独封装为一个模块,需要额外导入使用。
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script >
class Person extends React.Component {
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
// 类型和必要性限制
// 注意Person.propTypes 和 PropTypes
Person.propTypes = {
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
// 限制 speak 为函数
speak: PropTypes.func,
}
// 指定默认值
Person.defaultProps = {
sex: 'male',
age: 19,
}
ReactDOM.render(<Person name="Vue" sex="male" age={11} speak={speak} />, document.getElementById('test'))
function speak() {
console.log('speaking...')
}
</script>
props 的简写形式
Person.propTypes
和 Person.defaultProps
可以看作在类身上添加属性,利用 static
关键词就能在类内部进行声明。因此所谓简写只是从类外部移到类内部。
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script >
class Person extends React.Component {
static propTypes = {
// 必修为字符串且必填
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
// 限制 speak 为函数
speak: PropTypes.func,
}
// static 是添加到Person上的属性
static defaultProps = {
sex: 'male',
age: 19,
}
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
ReactDOM.render(<Person name="Vue" sex="male" age={11} speak={speak} />, document.getElementById('test'))
function speak() {
console.log('speaking...')
}
</script>
类式组件的构造器与 props
构造函数一般用在两种情况:
- 通过给
this.state
赋值对象来初始化内部state
- 为事件处理函数绑定实例
constructor(props) {
super(props)
// 初始化 state
this.state = { isHot: true, wind: '微风' }
// 解决 this 指向问题
this.changeWeather = this.changeWeather.bind(this)
}
因此构造器一般都不需要写。如果要在构造器内使用 this.props
才声明构造器,并且需要在最开始调用 super(props)
:
否则this.props 就会变为undefined ,不过一般也可以调用props
constructor(props) {
super(props)
console.log(this.props)
}
函数式组件使用 props
由于函数可以传递参数,因此函数式组件可以使用 props
。
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script>
function Person(props) {
const { name, age, sex } = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
// 限制就必须写在函数外面了,因为函数里面this丢失(开启了严格模式)
Person.propTypes = {
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
}
Person.defaultProps = {
sex: '男',
age: 18,
}
ReactDOM.render(<Person name="jerry" />, document.getElementById('test'))
</script>
组件实例核心属性 refs
通过定义 ref
属性可以给标签添加标识。即获取该节点
字符串形式的 ref
这种形式已过时,效率不高,官方 (opens new window)不建议使用。
<script>
class Demo extends React.Component {
showData = () => {
const { input1 } = this.refs
alert(input1.value)
}
render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据" />
<button onClick={this.showData}>点我提示左侧的数据</button>
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
回调形式的 ref
要点:
c => this.input1 = c
就是给组件实例添加input1
属性,值为节点(currentNode)- 由于是箭头函数,因此
this
是render
函数里的this
,即组件实例
<script>
class Demo extends React.Component {
showData = () => {
// 直接从this 里取出input1 属性
const { input1 } = this
alert(input1.value)
}
render() {
return (
<div>
<input ref={(c) => {this.input1 = c }}type="text" placeholder="点击按钮提示数据" />
<button onClick={this.showData}>点我提示左侧的数据</button>
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
关于回调 ref
执行次数的问题,官网 (opens new window)描述:
TIP
如果
ref
回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null
,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的ref
并且设置新的。通过将ref
的回调函数定义成class
的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
即内联函数形式,在更新过程重新调用render中 ref
回调会被执行两次,第一次传入 null
,第二次传入 DOM 元素。若是下述形式,则只执行一次。但是对功能实现没有影响,因此一般也是用内联函数形式。
函数定义成 class
的绑定函数的方式
<script>
//创建组件
class Demo extends React.Component {
state = { isHot: false }
changeWeather = () => {
const { isHot } = this.state
this.setState({ isHot: !isHot })
}
saveInput = (c) => {
this.input1 = c
console.log('@', c)
}
render() {
const { isHot } = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
<input ref={this.saveInput} type="text" />
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
createRef API
该方式通过调用 React.createRef
返回一个容器用于存储节点,且一个容器只能存储一个节点。(官方推荐的用法)
<script>
class Demo extends React.Component {
myRef = React.createRef()
myRef2 = React.createRef()
showData = () => {
alert(this.myRef.current.value)
}
showData2 = () => {
alert(this.myRef2.current.value)
}
render() {
return (
<div>
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据" />
<button onClick={this.showData}>点我提示左侧的数据</button>
<input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById('test'))
</script>
事件处理
- React 使用自定义事件,而非原生 DOM 事件,即
onClick、onBlur
:为了更好的兼容性 - React 的事件通过事件委托方式进行处理:为了高效
- 通过
event.target
可获取触发事件的 DOM 元素:勿过度使用ref
当触发事件的元素和需要操作的元素为同一个时,可以不使用 ref
:
class Demo extends React.Component {
showData2 = (event) => {
alert(event.target.value)
}
render() {
return (
<div>
<input onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}
受控 & 非受控组件
包含表单的组件分类:
- 非受控组件:现用现取。即需要使用时,再获取节点得到数据
- 受控组件:类似于 Vue 双向绑定的从视图层绑定到数据层
尽量使用受控组件,因为非受控组件需要使用大量的 ref
。
// 非受控组件
class Login extends React.Component {
handleSubmit = (event) => {
// 阻止表单提交的默认事件
event.preventDefault()
const { username, password } = this
alert(`用户名是:${username.value}, 密码是:${password.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input ref={(c) => (this.username = c)} type="text" name="username" />
密码:
<input ref={(c) => (this.password = c)} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
// 受控组件
class Login extends React.Component {
state = {
username: '',
password: '',
}
saveUsername = (event) => {
this.setState({ username: event.target.value })
}
savePassword = (event) => {
this.setState({ password: event.target.value })
}
handleSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
alert(`用户名是:${username}, 密码是:${password}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input onChange={this.saveUsername} type="text" name="username" />
密码:// onChange input改变一次触发一次
<input onChange={this.savePassword} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
对上述受控组件的代码进行优化,希望把 saveUsername
和 savePassword
合并为一个函数。
要点:
- 高阶函数:参数为函数或者返回一个函数的函数,如
Promise、setTimeout、Array.map()
- 函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
// 函数柯里化
function sum(a) {
return (b) => {
return (c) => {
return a + b + c
}
}
}
// 使用高阶函数和柯里化写法
class Login extends React.Component {
state = {
username: '',
password: '',
}
saveFormData = (dataType) => {
return (event) => {
// 对象里传入属性
this.setState({ [dataType]: event.target.value })
}
}
handleSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
alert(`用户名是:${username}, 密码是:${password}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input onChange={this.saveFormData('username')} type="text" name="username" />
密码:
<input onChange={this.saveFormData('password')} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
/* 不使用柯里化写法
onChange={(event) => this.saveFormData('username', event)} 主要改变在这既可以接受传入的Str和事件获取里面的值
*/
class Login extends React.Component {
state = {
username: '',
password: '',
}
// 简化不用高阶函数
saveFormData = (dataType, event) => {
this.setState({ [dataType]: event.target.value })
}
handleSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
alert(`用户名是:${username}, 密码是:${password}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input onChange={(event) => this.saveFormData('username', event)} type="text" name="username" />
密码:
<input onChange={(event) => this.saveFormData('password', event)} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
生命周期
生命周期旧版
初始化阶段:ReactDOM.render()
触发的初次渲染
constructor
componentWillMount
(组件将要挂载时调用)render
componentDidMount
(组件挂载完毕调用)
更新阶段
- 父组件重新
render
触发的更新
-
componentWillReceiveProps
( 父组件更新数据传值时调用,并不是一开始就会调用可以在里面接受到props值) -
shouldComponentUpdate
:控制组件是否更新的阀门,返回值为布尔值,默认为true
。若返回false
,则后续流程不会进行。 -
componentWillUpdate
-
render
-
componentDidUpdate
(组件完成数据更新)2.组件内部调用
this.setState()
修改状态 -
shouldComponentUpdate
( 阀门,必修return 一个布尔值,true 就往下执行,false 就不执行即不更新页面 ) -
componentWillUpdate
-
render
-
componentDidUpdate
3.组件内部调用 this.forceUpdate()
强制更新 (数据不更新页面也重新渲染时调用)
componentWillUpdate
render
componentDidUpdate
卸载阶段:ReactDOM.unmountComponentAtNode()
触发
componentWillUnmount
生命周期新版
- 废弃三个钩子:
componentWillMount
、componentWillReceiveProps
、componentWillUpdate
。在新版本中这三个钩子需要加UNSAFE_
前缀才能使用,后续可能会废弃。 - 新增两个钩子(实际场景用得很少):
getDerivedStateFromProps
、getSnapshotBeforeUpdate
static getDerivedStateFromProps(props, state) (opens new window):
- 需使用
static
修饰 - 需返回一个对象更新
state
或返回null
- 适用于如下情况:
state
的值任何时候都取决于props
getSnapshotBeforeUpdate(prevProps, prevState) (opens new window):
- 在组件更新之前获取快照
- 得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置)
- 返回值将作为参数传递给
componentDidUpdate()
class {
static getDerivedStateFromProps(props,state){
console.log('getDerivedStateFromProps',props,state);
return null
}
getSnapshotBeforeUpdate(){
console.log('getSnapshotBeforeUpdate');
return 'atguigu'
}
componentDidUpdate(preProps,preState,snapshotValue){
console.log('componentDidUpdate',preProps,preState,snapshotValue);
}
}
// getSnapshotBeforeUpdate 案例
class NewsList extends React.Component {
state = { newsArr: [] }
componentDidMount() {
setInterval(() => {
//获取原状态
const { newsArr } = this.state
//模拟一条新闻
const news = '新闻' + (newsArr.length + 1)
//更新状态
this.setState({ newsArr: [news, ...newsArr] })
}, 1000)
}
getSnapshotBeforeUpdate() {
return this.refs.list.scrollHeight
}
componentDidUpdate(preProps, preState, height) {
// 每次都 往上滚 对应添加的节点距离
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
}
render() {
return (
<div className="list" ref="list">
{this.state.newsArr.map((n, index) => {
return (
<div key={index} className="news">
{n}
</div>
)
})}
</div>
)
}
}
ReactDOM.render(<NewsList />, document.getElementById('test'))
最重要的三个钩子
render
:初始化渲染和更新渲染componentDidMount
:进行初始化,如开启定时器、发送网络请求、订阅消息componentWillUnmount
:进行收尾,如关闭定时器、取消订阅消息
虚拟 DOM 与 Diff 算法
key
的作用:
key
是虚拟 DOM 对象的标识,可提高页面更新渲染的效率。
当状态中的数据发生变化时,React 会根据新数据生成新的虚拟 DOM ,接着对新旧虚拟 DOM 进行 Diff 比较,规则如下:
- 旧虚拟 DOM 找到和新虚拟 DOM 相同的 key:
- 若内容没变,直接复用真实 DOM
- 若内容改变,则生成新的真实 DOM ,替换页面中之前的真实 DOM
- 旧虚拟 DOM 未找到和新虚拟 DOM 相同的 key:根据数据创建新的真实 DOM ,渲染到页面
使用 index
作为 key
可能引发的问题:
- 若对数据进行逆序添加、逆序删除等破坏顺序的操作,会进行没有必要的真实 DOM 更新。界面效果没问题,但效率低下。
- 如果结构中包含输入类的 DOM(如 input 输入框) ,则会产生错误的 DOM 更新。
- 若不存在对数据逆序添加、逆序删除等破坏顺序的操作,则没有问题。
// 使用 index 作为 key 引发的问题
class Person extends React.Component {
state = {
persons: [
{ id: 1, name: '小张', age: 18 },
{ id: 2, name: '小李', age: 19 },
],
}
add = () => {
const { persons } = this.state
const p = { id: persons.length + 1, name: '小王', age: 20 }
this.setState({ persons: [p, ...persons] })
}
render() {
return (
<div>
<h2>展示人员信息</h2>
<button onClick={this.add}>添加小王</button>
<h3>使用index作为key</h3>
<ul>
{this.state.persons.map((personObj, index) => {
return (
// 输入类的 DOM(如 input 输入框) ,则会产生错误的 DOM 更新。并且渲染效率低
<li key={index}>
{personObj.name}---{personObj.age}
<input type="text" />
</li>
)
})}
</div>
)
}
}
React 脚手架
创建 React 项目
- 全局安装 React 脚手架:
npm i -g create-react-app
- 创建项目:
create-react-app 项目名称
- 进入文件夹:
cd 项目名称
- 启动项目:
npm start
React 脚手架项目结构
public
:静态资源文件
manifest.json
:应用加壳(把网页变成安卓/IOS 软件)的配置文件robots.txt
:爬虫协议文件
src
:源码文件
App.test.js
:用于给App
组件做测试,一般不用index.js
:入口文件reportWebVitals.js
:页面性能分析文件,需要web-vitals
库支持setupTests.js
:组件单元测试文件,需要jest-dom
库支持
index.html
代码分析:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- %PUBLIC_URL% 代表 public 文件夹的路径 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 开启理想视口,用于做移动端网页的适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 用于配置浏览器页签+地址栏的颜色(仅支持安卓手机浏览器) -->
<meta name="theme-color" content="red" />
<!-- 网站描述 -->
<meta name="description" content="Web site created using create-react-app" />
<!-- 用于指定网页添加到手机主屏幕后的图标 -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用加壳时的配置文件 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<!-- 若浏览器不支持 js 则展示标签中的内容 -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
样式的模块化
样式的模块化可用于解决样式冲突的问题。该方法比较麻烦,实际开发用的比较少。用 less
就能解决了。
component/Hello
文件下的 index.css
改名为 index.module.css
。
.title {
background-color: orange;
}
Hello
组件导入样式:
import { Component } from 'react'
import hello from './index.module.css'
export default class Hello extends Component {
render() {
return <h2 className={hello.title}>Hello,React!</h2>
}
}
组件化开发思想
- 拆分组件、实现静态组件,注意:
className
、style
的写法 - 动态初始化列表,如何确定将数据放在哪个组件的
state
中?
- 某个组件使用:放在其自身的
state
中 - 某些组件使用:放在他们共同的父组件
state
中,即状态提升
- 关于父子之间通信:
-
父传子:直接通过
props
传递 -
子传父:父组件通过
props
给子组件传递一个函数,子组件调用该函数//子组件 this.props.XXX(函数) //父组件 <子组件 xxx(函数名)={对应的处理函数}/>
- 结构命名
写成这种index形式,引入时只需要引入对应文件夹,react脚手架会自动找到里面的index.js / jsx
导入时 .js 跟 .jsx 后缀可以省略
TodoList 案例总结
// 父组件
class Father extends Component {
state: {
todos: [{ id: '001', name: '吃饭', done: true }],
flag: true,
}
addTodo = (todo) => {
const { todos } = this.state
const newTodos = [todo, ...todos]
this.setState({ todos: newTodos })
}
render() {
return <List todos={this.state.todos} addTodo={this.addTodo} />
}
}
// 子组件
class Son extends Component {
// 由于 addTodo 是箭头函数,this 指向父组件实例对象,因此子组件调用它相当于父组件实例在调用
handleClick = () => {
this.props.addTodo({ id: '002', name: '敲代码', done: false })
}
render() {
return <button onClick={this.handleClick}>添加</button>
}
}
// 键盘按起事件
onKeyUp = (event)=>{
const {target,keyCode} = event
// 判断用户是否敲下回车
if(keyCode != 13) return
// 判断用户输入的是否为空
if(target.value.trim() == '') {
alert("不能输入空")
return
}
// 敲下回车将值传去父组件
this.props.addList(target.value)
// 随后清空input值
target.value = ''
}
// 弹出对话框询问,需要添加window
if(window.confirm('是否删除'))
- 注意
defaultChecked
和checked
的区别,类似的还有:defaultValue
和value
- 拿复选框的值需要掉event.target.checked
- 状态在哪里,操作状态的方法就在哪里
React 网络请求
React 脚手架配置代理
方法一:
在 package.json
文件中进行配置:
"proxy": "http://localhost:5000"
- 优点:配置简单,前端请求资源可不加前缀
- 缺点:不能配置多个代理
- 工作方式:当请求了 3000 端口号(本机)不存在的资源时,就会把请求转发给 5000 端口号服务器
方法二:
在 src
目录下创建代理配置文件 setupProxy.js
,进行配置:
const proxy = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
//api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
proxy('/api1', {
//配置转发目标地址(能返回数据的服务器地址)
target: 'http://localhost:5000',
//控制服务器接收到的请求头中host字段的值
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
changeOrigin默认值为false,但一般将changeOrigin改为true
*/
changeOrigin: true,
//去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
pathRewrite: { '^/api1': '' },
}),
proxy('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: { '^/api2': '' },
})
)
}
消息订阅-发布机制
即 React 中兄弟组件或任意组件之间的通信方式。
使用的工具库:PubSubJS(opens new window)
下载安装 PubSubJS
:npm install pubsub-js --save
基础用法:
import PubSub from 'pubsub-js'
// 订阅消息
var token = PubSub.subscribe('topic', (msg, data) => {
console.log(msg, data)
// msg 是订阅的名称,data才是发送的数据
})
// 发布消息
PubSub.publish('topic', 'hello react')
// 取消订阅
PubSub.unsubscribe(token)
Github 搜索框案例知识点总结
import axios from 'axios'
axios.get('url').then((res,err)=>{即可获得数据})// 只需在componentDidMount 掉用即可
- 设计状态时要考虑全面,例如带有网络请求的组件,要考虑请求失败怎么办。
- ES6 知识点:解构赋值 + 重命名
let obj = { a: { b: 1 } }
//传统解构赋值
const { a } = obj
//连续解构赋值
const {
a: { b },
} = obj
//连续解构赋值 + 重命名
const {
a: { b: value },
} = obj
- 消息订阅与发布机制
- 先订阅,再发布(隔空对话)
- 适用于任意组件间通信
- 要在
componentWillUnmount
钩子中取消订阅
fetch
发送请求(关注分离的设计思想)
try {
// 先看服务器是否联系得上
const response = await fetch(`/api1/search/users2?q=${keyWord}`)
// 再获取数据
const data = await response.json()
console.log(data)
} catch (error) {
console.log('请求出错', error)
}
React 路由
路由的理解
何为路由?
- 一个路由是一个映射关系
key
为路径,value
可能是function
或 组件
后端路由:
value
是function
,用于处理客户端的请求- 注册路由:
router.get(path, function(req, res))
- 工作过程:Node 接收到请求,根据路径匹配路由,调用对应函数处理请求,返回响应数据
前端路由:
value
是组件- 注册路由:
<Route path="/test" component={Test}>
- 工作过程:浏览器路径变为
/test
,展示Test
组件
路由基本使用
安装 react-router-dom
:
// 安装 5.X 版本路由
npm install react-router-dom@5.2.0 -S
// 最新已经 6.X 版本,用法和 5.X 有所不同
npm install react-router-dom -S
- 导航区a标签改为Link 标签
<Link to='/about'>about</Link>
- 展示区写
<Routes>
<Route path="/about" element={<About />} />
</Routes>
<App>
的最外侧包裹<BrowserRouter>
或<HashRouter>
:
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
6.x
版本的用法参考文章(opens new window)
以 5.x
版本为例展示基本使用:
// App.jsx
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'
export default class App extends Component {
render() {
return (
<div>
<div className="list-group">
<Link className="list-group-item" to="/about">
About
</Link>
<Link className="list-group-item" to="/home">
Home
</Link>
</div>
<div className="panel-body">
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
</div>
</div>
)
}
}
<App>
的最外侧包裹 <BrowserRouter>
或 <HashRouter>
:
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
6.x 版本的用法
// App.jsx
import React, { Component } from 'react'
import { Routes,Route,Link } from 'react-router-dom';
import Home from './components/Home'
import About from './components/About'
export default class App extends Component {
render() {
return (
<div>
// 控制路由的转换
<Link className="list-group-item active" to='/home'>home</Link>
<Link className="list-group-item" to='/about'>about</Link>
// 展示对应路由的内容
// 与5.x 不同于 需要用Routes 包裹并更换component 为 element
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
)
}
}
<App>
的最外侧包裹 <BrowserRouter>
或 <HashRouter>
:
import React from "react";
import ReactDOM from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App'
ReactDOM.render(
<Router>
<App/>
</Router>,document.getElementById('root'))
路由组件和一般组件
写法不同:
- 一般组件:
<Demo/>
- 路由组件:
<Route path="/demo" component={Demo}/>
存放位置不同:
- 一般组件:
components
- 路由组件:
pages
接收到的 props
不同:
- 一般组件:标签属性传递
- 路由组件:接收到三个固定的属性
// 从this.props 里接受到的三个属性的主要参数
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/home/message/detail/2/hello"
search: ""
state: undefined
match:
params: {}
path: "/home/message/detail/:id/:title"
url: "/home/message/detail/2/hello"
NavLink 的使用
NavLink
可以实现路由链接的高亮,通过 activeClassName
指定样式名,默认追加类名为 active
。
<NavLink activeClassName="demo" to="/about">About</NavLink>
<NavLink activeClassName="demo" to="/home">Home</NavLink>
封装 NavLink
组件:由于 NavLink
组件中重复的代码太多,因此进行二次封装。
※ 细节点:组件标签的内容会传递到 this.props.children
属性中,反过来通过指定标签的 children
属性可以修改组件标签内容
// MyNavLink 组件
import React, { Component } from 'react'
import { NavLink } from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
// this.props.children 可以取到标签内容,如 About, Home
// 反过来通过指定标签的 children 属性可以修改标签内容
return <NavLink activeClassName="demo" className="list-group-item" {...this.props} />
}
}
封装完后使用
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
Switch 的使用
Switch
可以提高路由匹配效率,如果匹配成功,则不再继续匹配后面的路由,即单一匹配。
<!-- 只会展示 Home 组件 -->
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Route path="/home" component={Test} />
</Switch>
6.x 中switch 已更改用法(为Routes)
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
解决多级路径刷新页面样式丢失的问题
public/index.html
中 引入样式时不写./
写/
(常用)public/index.html
中 引入样式时不写./
写%PUBLIC_URL%
(常用)- 使用
HashRouter
<link rel="stylesheet" href="/css/bootstrap.css" />
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css" />
路由的严格匹配与模糊匹配
- 默认使用模糊匹配(输入的路径必须包含要匹配的路径,且顺序一致)
- 开启严格匹配:
<Route exact path="/about" component={About}/>
- 严格匹配需要再开,开启可能会导致无法继续匹配二级路由,因为路由时按注册顺序去寻找的,当第一层开了严格匹配第一层的匹配失败就不会找第二层了
Redirect 的使用
- 一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到 Redirect 指定的路由
import {Redirect} from 'react-router-dom'
<Switch>
<Route path="/about" component="{About}" />
<Route path="/home" component="{Home}" />
// 放在最后面
<Redirect to="/about" />
</Switch>
6.x 中已移除Redirect
import { Routes,Route,Navigate } from 'react-router-dom';
<Routes>
<Route path="/" element={<Navigate replace to="/home" />} />
</Routes>
嵌套路由
- 注册子路由需写上父路由的
path
- 路由的匹配是按照注册路由的顺序进行的
<!-- 父组件 -->
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Redirect to="/about" />
</Switch>
<!-- 子组件 -->
<ul className="nav nav-tabs">
<li><MyNavLink to="/home/news">News</MyNavLink> </li>
<li><MyNavLink to="/home/message">Message</MyNavLink></li>
</ul>
<!-- 路由展示区域 -->
<Switch>
<Route path="/home/news" component={News} />
<Route path="/home/message" component={Message} />
<Redirect to="/home/news" />
</Switch>
动态路由传参
三种方式:params, search, state
参数
三种方式对比:
state
方式当前页面刷新可保留参数,但在新页面打开不能保留。前两种方式由于参数保存在 URL 地址上,因此都能保留参数。params
和search
参数都会变成字符串
第一种 params 传参
<!-- 路由链接 -->
<Link to={`/home/mess/details/${item.id}/${item.name}`}>{item.name}</Link>
<!-- 注册路由 -->
<Route path='/home/mess/details/:id/:title' component={Details}></Route>
//接收参数
const { id, title } = this.props.match.params
第二种 search 传参
<!-- 路由链接 声明search 参数传递参数-->
<Link to={`/home/mess/details?id=${item.id}&title=${item.name}`}>{item.name}</Link>
<!-- 注册路由 声明search 参数传递参数 无需特殊声明-->
<Route path='/home/mess/details' component={Details}></Route>
//接收参数
// 解析 urlenencode的函数
const app = (str)=>{
const search = this.props.location.search
var query = search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == str){return pair[1];}
}
return (false)
}
const id = app('id') // 得到id 的value
const title = app('title') // 得到 title 的value
// 可以获得对应id 的item对象
const findRes = data.find(item=>{
return item.id == id
})
// 或者引入 import qs from 'querystring'
import qs from 'querystring' // qs.parse 可以转urlencoded 为对象形式 qs.stringfy() 可以转为urlencoded形式
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1))
第三种 state 传参
<!-- 路由链接 声明 state 参数传递参数-->
<Link to={{pathname:"/home/mess/details",state:{id:item.id,title:item.name}}}>{item.name}</Link>
<!-- 注册路由 声明search 参数传递参数 无需特殊声明-->
<Route path='/home/mess/details' component={Details}></Route>
//state接收参数
// 接受state 参数
const {id,title} = this.props.location.state
编程式导航
编程式导航是使用路由组件 this.props.history
提供的 API 进行路由跳转:(只有路由组件才有该方法)
this.props.history.push(path, state)
this.props.history.replace(path, state)
this.props.history.goForward()
this.props.history.goBack()
this.props.history.go(n)
// 调用 路由组件 porps 的方法
pushShow(id,title){
this.props.history.push(`/home/mess/details/${id}/${title}`)
}
// 绑定 编程式导航的方法
<button onClick={()=>this.pushShow(item.id,item.name)}>push</button>
// 编程式导航传参
this.props.history.push(`/home/message/detail/${id}/${title}`)
this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)
this.props.history.push(`/home/message/detail`, { id: id, title: title })
withRouter 的使用
withRouter
的作用:加工一般组件,让其拥有路由组件的 API ,如 this.props.history.push
等。
import React, {Component} from 'react'
import {withRouter} from 'react-router-dom'
class Header extends Component {
...
}
// 导出时用withRouter加工后的组件
export default withRouter(Header)
BrowserRouter 和 HashRouter
底层原理不一样:
BrowserRouter
使用的是 H5 的 history API,不兼容 IE9 及以下版本。HashRouter
使用的是 URL 的哈希值。
路径表现形式不一样
BrowserRouter
的路径中没有#
,如:localhost:3000/demo/test
HashRouter
的路径包含#,如:localhost:3000/#/demo/test
刷新后对路由 state
参数的影响
BrowserRouter
没有影响,因为state
保存在history
对象中。HashRouter
刷新后会导致路由state
参数的丢失!
备注:HashRouter
可以用于解决一些路径错误相关的问题。
React UI 组件库
Ant Design 配置按需引入和自定义主题
以下配置是
3.x
版本,4.x
版本见官网(opens new window)
1、安装依赖:
npm install react-app-rewired customize-cra babel-plugin-import // 处理按需导入
npm install less less-loader // 自定义主题文件
2、修改 package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}
3、根目录下创建 config-overrides.js
//配置具体的修改规则
const { override, fixBabelImports, addLessLoader } = require('customize-cra')
module.exports = override(
fixBabelImports('import', { // 组件按需导入无需再导入整个包
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
lessOptions: { // 从less5.x以上需要套上lessOptions才能配置文件
javascriptEnabled: true,
modifyVars: { '@primary-color': 'green' },// 自定义全局主题颜色为绿色
},
})
)
详细用法前去官方文档
Redux
Redux 概述
Redux 为何物
- Redux 是用于做 状态管理 的 JS 库
- 可用于 React、Angular、Vue 等项目中,常用于 React
- 集中式管理 React 应用多个组件共享的状态
何时用 Redux
- 某个组件的状态,需要让其他组件拿到(状态共享)
- 一个组件需要改变另一个组件的状态(通信)
- 使用原则:不到万不得已不要轻易动用
Redux 工作流程
- 组件想操作 Redux 中的状态:把动作类型和数据告诉
Action Creators
Action Creators
创建action
:同步action
是一个普通对象,异步action
是一个函数Store
调用dispatch()
分发action
给Reducers
执行Reducers
接收previousState
、action
两个参数,对状态进行加工后返回新状态Store
调用getState()
把状态传给组件
完整版 redux 的使用
1、 创建 redux 文件夹下的 store.js
// store.js
// 导入创建store方法,跟处理action异步函数的方法
import {createStore,applyMiddleware} from 'redux'
// 导入需要管理的组件
import count from './count_reducer'
// 引入处理异步函数的中间件
import thunk from 'redux-thunk'
// 导出 store 第一个参数为需要管理的组件,第二个参数为中间件
export default createStore(count,applyMiddleware(thunk))
2、 创建 redux 文件夹下的 count_reducer.js (用于指定操作状态值)
// 赋初始值时为0
const InitState = 0
// 第一次action里的 type 为 undefined ,avalue 为空 调用函数进行初始化状态 返回初始状态为0
// 向外暴露一个方法
export default function count (preState=InitState,action){
// 从action 里解构赋值出要用的type跟state
const {type,state} = action
switch (type) {
case 'increment': // 加法
return preState + state*1
case 'decrement': // 减法
return preState - state*1
default:
return preState
}
}
3、 创建 redux 文件夹下的 count_action.js (用于创建action对象 / 异步函数)
// 用来给count 创建action对象
// 返回的是一个对象
export const actionIncrement = (state)=>({type:'increment',state})
export const actionDecrement = (state)=>({type:'decrement',state})
// 创建异步action对象
export const IncrementSync = (state,time)=>{
// 异步api 不同在于返回一个函数给store 然后会传一个dispatch参数
return (dispatch)=>{
setTimeout(() => {
dispatch(actionIncrement(state))
}, time);
}
}
4、 在需要使用的地方引入
import React, { Component } from 'react'
// 引入stroe
import store from './store/store'
// 引入action创建对象
import {actionIncrement,actionDecrement,IncrementSync} from './store/count_action'
export default class App extends Component {
// 监听store里的属性变化后重新调用render渲染画面
componentDidMount(){
store.subscribe(()=>{
this.setState({})
})
}
// 加+1 的处理函数
AddNum = ()=>{
const {value} = this.allNum
// const {count} = this.state
// this.setState({count:count+value*1}) 原生写法
// store.dispatch({type:'increment',state:value}) 简写版不用action 直接dispatch去调用reducer
// 用action 完整版写法
store.dispatch(actionIncrement(value))
}
AddNumSync = ()=>{
const {value} = this.allNum
// const {count} = this.state
// setTimeout(() => {
// store.dispatch({type:'increment',state:value})
// 调用action异步方法
store.dispatch(IncrementSync(value,500))
// }, 500);
}
render() {
return (
<div>
// store.getState() 获取到当前的count值
<h1>当前求和数:{store.getState()}</h1>
<select ref = {c => this.allNum = c}>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
</select>
<button onClick={this.AddNum}>+</button>
<button onClick={this.AddNumSync}>异步加</button>
</div>
)
}
}
核心概念(api)
action
:
- 表示动作的对象,包含 2 个属性
type
:标识属性,值为字符串,唯一,必须属性data
:数据属性,类型任意,可选属性{type: 'increment', data: 2}
reducer
:
- 用于初始化状态、加工状态
- 根据旧状态和
action
产生新状态 - 是纯函数
纯函数:输入同样的实参,必定得到同样的输出
- 不能改写参数数据
- 不产生副作用,如网络请求、输入输出设备(网络请求不稳定)
- 不能调用
Date.now()
、Math.random()
等不纯方法
store
:
- Redux 核心对象,内部维护着
state
和reducer
- 核心 API
store.getState()
:获取状态store.dispatch(action)
:分发任务,触发reducer
调用,产生新状态store.subscribe(func)
:注册监听函数,当状态改变自动调用
combineReducers()
作用:合并多个reducer函数
//代码示例 ------------------ redux/reducers/index.js ------------------------------------ /** * 该文件用于汇总所有的reducer为一个总的reducer */ //引入combineReducers,用于汇总多个reducer import {combineReducers} from 'redux' //引入为Count组件服务的reducer import count from './count' import persons from './person' //汇总所有的reducer变为一个总的reducer export default combineReducers({ count,persons })
注意事项
- redux 只负责管理状态,状态改变驱动页面展示要自己写
- 可以在
index.js
中统一监听状态变化,也可以在组件中单独监听。注意不能直接this.render()
调用render
函数,要通过this.setState({})
间接调用 reducer
由store
自动触发首次调用,传递的preState
为undefined
,action
为{type: '@@REDUX/ININT_a.5.v.9'}
类似的东东,只有type
Redux 异步编程
安装异步中间件:
npm install redux-thunk -S
要点:
- 延迟的动作不想交给组件,而是
action
- 当操作状态所需数据要靠异步任务返回时,可用异步
action
- 创建
action
的函数返回一个函数,该函数中写异步任务 - 异步任务完成后,分发一个同步
action
操作状态 - 异步
action
不是必要的,完全可以在组件中等待异步任务结果返回在分发同步action
// store.js
/**
* 该文件撰文用于暴露一个store对象,整个应用只有一个store对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入汇总后的reducer
import reducer from './reducers'
//引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'
//引入redux-devtools-extension
import {composeWithDevTools} from 'redux-devtools-extension'
//暴露store
export default createStore(reducer,composeWithDevTools(applyMiddleware(thunk)))
// count_action.js
import { INCREMENT, DECREMENT } from './constant.js'
export const createIncrementAction = (data) => ({ type: INCREMENT, data })
export const createDecrementAction = (data) => ({ type: DECREMENT, data })
// 异步 action 返回一个函数
export const createIncrementAsyncAction = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
}, time)
}
}
// Count.jsx
incrementAsync = () => {
const { value } = this.selectNumber
store.dispatch(createIncrementAsyncAction(value * 1))
}
整个过程简单理解:store
在分发 action
时,发现返回一个函数,那它知道这是个异步 action
。因此 store
勉为其难地帮忙执行这个函数,同时给这个函数传递 dispatch
方法,等待异步任务完成取到数据后,直接调用 dispatch
方法分发同步 action
。
React-Redux
React-Redux 是一个插件库,用于简化 React 中使用 Redux 。
React-Redux 将组件分为两类:
- UI 组件
- 只负责 UI 呈现,不带有业务逻辑
- 通过
props
接收数据 - 不能使用 Redux 的 API
- 保存在
components
文件夹下
- 容器组件
- 负责管理数据和业务逻辑,和 Redux 通信,将结果交给 UI 组件
- 可使用 Redux 的 API
- 保存在
containers
文件夹下
相关API
① Provider
作用: 让所有组件都可以得到state数据
import React from 'react' import ReactDOM from "react-dom" import App from './App' import store from './redux/store' import {Provider} from 'react-redux' ReactDOM.render( /* 此处需要用Provider包裹App,目的是让App所有的后代容器组件都能接收到store */ <Provider store={store}> <App/> </Provider>, document.getElementById('root') )
② connect()()
- 作用: 用于包装 UI 组件生成容器组件
- 使用connect(
mapDispatchToProps
,mapDispatchToProps
)(UI组件)注意点:
- 该方法默认传入
state
与dispatch
- 可以省略
dispatch
直接传入action
方法,该api会自动帮你调用dispatch
Ⅰ-mapStateToProps
作用:将外部的数据(即
state对象
)转换为UI组件的标签属性1.mapStateToProps函数返回的是一个对象;
2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
3.mapStateToProps
用于传递状态
function mapStateToProps(state){ return {count:state} }
Ⅱ-mapDispatchToProps
作用:将
分发action的函数
转换为UI组件的标签属性
- mapDispatchToProps函数返回的是一个对象;
- 返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
- mapDispatchToProps
用于传递操作状态的方法
- 可以省略
dispatch
,直接传入action
,api将会自动调用
dispatch
Ⅲ-代码示例
------------------------------不简化代码----------------------------------------------- /* 1.mapStateToProps函数返回的是一个对象; 2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value 3.mapStateToProps用于传递状态 */ function mapStateToProps(state){ return {count:state} } /* 1.mapDispatchToProps函数返回的是一个对象; 2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value 3.mapDispatchToProps用于传递操作状态的方法 */ function mapDispatchToProps(dispatch){ return { jia:number => dispatch(createIncrementAction(number)), jian:number => dispatch(createDecrementAction(number)), jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)), } } //使用connect()()创建并暴露一个Count的容器组件 export default connect(mapStateToProps,mapDispatchToProps)(CountUI) ----------------下面是简化代码----------------------------- //使用connect()()创建并暴露一个Count的容器组件 //使用connect(传入状态,操作状态方法)(UI组件) export default connect( state => ({ count: state.count, personCount: state.persons.length }), {increment, decrement, incrementAsync} )(Count)
多个组件数据共享
首先规范化文件结构,容器组件和 UI 组件合为一体后放在 containers
文件夹。redux
文件夹新建 actions
和 reducers
文件夹分别用于存放每个组件对应的 action
和 reducer
。
添加案列
新建 Person
组件对应的 action
和 reducer
:
--------------------------------------person.js-------------------------------
import {ADD_PERSON} from '../constant'
//创建增加一个人的action动作对象
export const addPerson=personObj=>({
type:ADD_PERSON,
data:personObj
})
--------------------------------------person.js-------------------------------
import {ADD_PERSON} from '../constant'
//初始化人的列表
const initState = [{id:'001',name:'tom',age:18}]
export default function personReducer(preState=initState,action){
// console.log('personReducer@#@#@#');
const {type,data} = action
switch (type) {
case ADD_PERSON: //若是添加一个人
//preState.unshift(data) //此处不可以这样写,这样会导致preState被改写了,personReducer就不是纯函数了。
return [data,...preState]
default:
return preState
}
}
关键步骤:在 store.js
中使用 combineReducers()
整合多个 reducer
来创建 store
对象。
这样 Redux 中就以对象的形式存储着每个组件的数据。
// redux/store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import countReducer from './reducers/count'
import personReducer from './reducers/person'
import thunk from 'redux-thunk'
// 这样就可以同时管理多个组件的状态
const Reducers = combineReducers({
total: countReducer,
personList: personReducer,
})
export default createStore(Reducers, applyMiddleware(thunk))
Person
组件中获取 Redux 保存的状态,包括其他组件的数据。
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createAddPersonAction } from '../../redux/actions/person'
import { nanoid } from 'nanoid'
class Person extends Component {
addPerson = () => {
const name = this.nameInput.value
const age = this.ageInput.value
const personObj = { id: nanoid(), name, age }
this.props.addPerson(personObj)
this.nameInput.value = ''
this.ageInput.value = ''
}
render() {
return (
<div>
<h2>在Person组件拿到Count组件的数据:{this.props.count}</h2>
<input type="text" ref={(c) => (this.nameInput = c)} placeholder="Please input name" />
<input type="text" ref={(c) => (this.ageInput = c)} placeholder="Please input age" />
<button onClick={this.addPerson}>添加</button>
<ul>
{this.props.personList.map((item) => {
return (
<li key={item.id}>
{item.name} -- {item.age}
</li>
)
})}
</ul>
</div>
)
}
}
export default connect(
// state 是 Redux 保存的状态对象
// 容器组件从 Redux 中取出需要的状态,并传递给 UI 组件
state => ({personList: state.personList, count: state.total}),
{
addPerson: createAddPersonAction
// 这一行凑数的,为了保持代码格式
addPerson2: createAddPersonAction
}
)(Person)
项目打包运行
运行命令:npm run build
进行项目打包,生成 build
文件夹存放着打包完成的文件。
运行命令:npm i serve -g
全局安装 serve
,它能够以当前目录为根目录开启一台服务器,进入 build
文件夹所在目录,运行 serve
命令即可开启服务器查看项目效果。
或者 serve build 运行指定文件夹 + port 可以指定端口等
React 扩展内容
setState 更新状态的两种写法
对象式:setState(stateChange, [callback])
stateChange
为状态改变对象(该对象可以体现出状态的更改)callback
是可选的回调函数, 它在状态更新完毕、界面也更新后才被调用
函数式:setState(updater, [callback])
- updater 为返回 stateChange 对象的函数。
- updater 可以接收到 state 和 props。
说明:
- React 状态更新是异步的。下述代码打印的
count
值是上一次的值,而非更新后的。可在第二个参数回调中获取更新后的状态。
add = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count)
}
add = () => {
this.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count)
})
}
callback
回调在componentDidMount
钩子之后执行- 对象式写法可以看做函数式写法的语法糖
add = () => {
this.setState((state, props) => {
return { count: state.count + props.step }
})
}
this.setState({ count: this.state.count + 1 })
路由组件懒加载 lazyLoad
import React, { Component, lazy, Suspense } from 'react'
import Loading from './Loading'
// 通过 lazy 函数配合 import() 函数动态加载路由组件
// 路由组件代码会被分开打包
const Home = lazy(() => import('./Home'))
const About = lazy(() => import('./About'))
export default Demo extends Component {
render() {
return (
<div>
<h1>Demo 组件</h1>
<Link to="/home">Home</Link>
<Link to="/about">About</Link>
// 通过 <Suspense> 指定在加载得到路由打包文件前显示一个自定义 Loading 界面
<Suspense fallback={Loading}>
<Switch>
<Route path="/home" component={Home}>
<Route path="/about" component={About}>
</Switch>
</Suspense>
</div>
)
}
}
React Hook
Hook 是 React 16.8.0 增加的新特性,让我们能在函数式组件中使用
state
和其他特性
State Hook
State Hook
让函数式组件也可拥有state
状态。- 语法:
const [Xxx, setXxx] = React.useState(initValue)
useState()
参数:状态初始化值;返回值:包含 2 个元素的数组,分别为状态值和状态更新函数- setXxx() 的 2 种用法:
setXxx(newValue)
setXxx(value => newValue)
- 注意!新状态值会覆盖原状态值!因此若有多个状态,只能多次调用
React.useState
,不能使用对象!
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState('Tom')
function add() {
setCount(count + 1)
setCount((count) => count + 1)
}
Effect Hook
Effect Hook
让我们能在函数式组件中执行副作用操作(就是模拟生命周期钩子)- 副作用操作:发送 Ajax 请求、定时器、手动更改真实 DOM
Effect Hook
可以模拟三个钩子:componentDidMount
、componentDidUpdate
、componentWillUnmount
React.useEffect
第一个参数return
的函数相当于componentWillUnmount
,若有多个会按顺序执行
// 语法
React.useEffect(() => {
...
return () => {
// 组件卸载前执行,即 componentWillUnmount 钩子
...
}
}, [stateValue]) // [stateValue] 为监视state的某个属性
// 模拟 componentDidMount
// 第二个参数数组为空,表示不监听任何状态的更新
// 因此只有页面首次渲染会执行输出
React.useEffect(() => {
console.log('DidMount')
return () => {
console.log('WillUnmount 1')
}
}, [])
// 模拟全部状态 componentDidUpdate
// 若第二个参数不写,表示监听所有状态的更新
React.useEffect(() => {
console.log('All DidUpdate')
return () => {
console.log('WillUnmount 2')
}
})
// 模拟部分状态 componentDidUpdate
// 第二个参数数组写上状态,表示只监听这些状态的更新
React.useEffect(() => {
console.log('Part DidUpdate')
return () => {
console.log('WillUnmount 3')
}
}, [count, name])
// 若调用 ReactDOM.unmountComponentAtNode(document.getElementById('root'))
// 会输出 WillUnmount 1、2、3
Ref Hook
Ref Hook
可以在函数式组件存储或查找组件内的标签或其他数据- 语法:
const refContainer = React.useRef()
- 保存标签对象的容器,和
React.createRef()
类似,也是专人专用
function Demo() {
const myRef = React.useRef()
function show() {
console.log(myRef.current.value)
}
return (
<div>
<input type="text" ref={myRef} />
<button onClick={show}>展示数据</button>
</div>
)
}
Fragment
Fragment
标签本身不会被渲染成一个真实 DOM 标签,有点像 Vue 的template
。- 用空标签也有相同效果,但是空标签不能传递任何属性,
Fragment
标签可以传递key
属性,遍历时候可用。
import React, { Component, Fragment } from 'react'
export default class Demo extends Component {
render() {
return (
<Fragment key={1}>
<input type="text" />
<input type="text" />
</Fragment>
)
// 或
return (
<>
<input type="text" />
<input type="text" />
</>
)
}
}
Context
Context 是一种组件间通信方式,常用于祖父组件与子孙组件。实际开发一般不用,一般用 React-Redux
用法说明:
1) 创建Context容器对象:
const XxxContext = React.createContext()
2) 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<XxxContext.Provider value={数据}>
子组件
</XxxContext.Provider>
3) 后代组件读取数据:
// 第一种方式:仅适用于类组件
// 声明接收context
static contextType = xxxContext
// 读取context中的value数据
this.context
//第二种方式: 可用于函数组件与类组件
<XxxContext.Consumer>
{
// value就是context中的value数据
value => (
...
)
}
</XxxContext.Consumer>
举个栗子:
// context.js
import React from 'react'
export const MyContext = React.createContext()
export const { Provider, Consumer } = MyContext
// A.jsx
class A extends Component {
state = { username: 'tom', age: 18 }
render() {
const { username, age } = this.state
return (
<div>
<h3>A组件</h3>
<h4>用户名是:{username}</h4>
<Provider value={{ username, age }}>
<B />
</Provider>
</div>
)
class B extends Component {
render() {
return (
<div>
<h3>B组件</h3>
<C />
</div>
)
// 后代c 要使用祖组件传来的value
class C extends Component {
// 先声明接受
static contextType = MyContext
render() {
// 然后就可以从this。context上读取传来的值,不声明就没有值
const { username, age } = this.context
return (
<div>
<h3>C组件</h3>
<h4>
从A组件接收到的用户名:{username},年龄:{age}
</h4>
</div>
)
// 函数式组件使用
function C() {
return (
<div>
<h3>我是C组件</h3>
<h4>
从A组件接收到的用户名:
// 在要使用的地方 用Consumer 包裹住,里面传一个value value里既有传来的属性
<Consumer>
{(value) => `${value.username},年龄是${value.age}`}
</Consumer>
</h4>
</div>
)
组件渲染优化
问题:
- 只要调用
setState()
,即使没有修改状态,组件也会重新render()
- 只要父组件重新渲染,即使子组件没有使用父组件的状态,也会重新
render()
原因:
shouldComponentUpdate()
钩子默认总是返回true
改进:
- 只有组件的
state
或props
的数据发生改变时才重新渲染
方式:
- 手动重写
shouldComponentUpdate(nextProps, nextState)
的逻辑,只有数据发生改变才返回true
- 使用
PureComponent
,它重写了shouldComponentUpdate()
, 只有state
或props
数据有变化才返回true
TIP
- 它只是进行
state
和props
数据的浅比较, 如果只是数据对象内部数据变了, 返回false
。即对于引用数据类型,比较的是地址引用- 不要直接修改
state
数据, 而是要产生新数据
import React, { PureComponent } from 'react'
class Demo extends PureComponent {
...
addStu = () => {
// 不会渲染
const { stus } = this.state
stus.unshift('小刘')
this.setState({ stus })
// 重新渲染
const { stus } = this.state
this.setState({ stus: ['小刘', ...stus] })
}
...
}
render props (插槽)
类似于 Vue 中的插槽技术
如何向组件内部动态传入带内容的结构(即标签或组件)?
- Vue:插槽技术
- React:
- 使用
children props
:通过组件标签体传入结构 - 使用
render props
:通过组件标签属性传入结构,可携带数据
- 使用
children props
方式:
- 组件标签体内容会存储到
this.props.children
中 - 缺点:A 组件无法向 B 组件传递数据
使用 children props
:
import React, { Component } from 'react'
export default class Parent extends Component {
render() {
return (
<div>
<h3>Parent组件</h3>
<A>
<B />
</A>
</div>
)
}
}
class A extends Component {
state = { name: 'tom' }
render() {
return (
<div>
<h3>A组件</h3>
{this.props.children}
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
<h3>B组件</h3>
</div>
)
}
}
render props
方式:
<A render={(name) => <B name={name} />} />
{this.props.render(name)}
import React, { Component } from 'react'
export default class Parent extends Component {
render() {
return (
<div>
<h3>Parent组件</h3>
// 调用一个render 内联一个函数携带数据参数,并返回一个组件标签 传递参数
// 可以将B换成任意想展示的组件或内容
<A render={(name) => <B name={name} />} />
</div>
)
}
}
class A extends Component {
state = { name: 'tom' }
render() {
const { name } = this.state
return (
<div>
<h3>A组件</h3>
// 在指定位置用了 this。props。render(xxx)既可以指定传来的组件展示的地方
// 并将属性传递给B组件
{this.props.render(name)}
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
<h3>B组件,{this.props.name}</h3>
</div>
)
}
}
错误边界
TIP
错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面。
注意:只在生产环境(项目上线)起效
特点:
- 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
- 简单理解就是只能捕获后代组件生命周期钩子里面代码的错误
import React, { Component } from 'react'
import Child from './Child'
export default class Parent extends Component {
state = {
//用于标识子组件是否产生错误
hasError: '',
}
// 当子组件出现错误,会触发调用,并携带错误信息
static getDerivedStateFromError(error) {
// render 之前触发
// 返回新的 state
return { hasError: error }
}
componentDidCatch(error, info) {
console.log(error, info)
console.log('此处统计错误,反馈给服务器')
}
render() {
return (
<div>
<h2>Parent组件</h2>
{this.state.hasError ? <h2>网络不稳定,稍后再试</h2> : <Child />}
</div>
)
}
}
组件通信方式总结
props
- 消息订阅发布:
pubs-sub
- 集中管理:Redux、dva 等
- conText(祖孙之间,后代组件)
推荐搭配:
- 父子组件:
props
- 兄弟组件:消息订阅-发布、集中式管理
- 祖孙组件(跨级组件):消息订阅-发布、集中式管理、
conText
(开发用的少,封装插件用的多即 React-Redux)
自定义 Hook
注意事项
- Hook 不处理的话规定只能在函数式组件中使用
你想在项目中使用基于类的 Hook 逻辑,并且目前无法将这些类组件重写为 Hooks。类可能太复杂了,或者如果你更改它,可能会破坏项目中的许多其他内容。这种方法的商业价值也值得怀疑。如果你转至 React 文档,会看到一个有趣的声明:
解决不能在类式组件里使用hook的方法
Using Hooks as HOC
HOC 是重用组件逻辑的高级 React 技术,其使我们能够在现有类组件中使用 Hook 逻辑。因为 HOC 是使一个组件作为输入,并通过一些额外的 props 返回相同的组件。在我们的情况下,我们将传递 Hook 函数作为 props。
import React from 'react';
import { useScreenWidth } from '../hooks/useScreenWidth';
export const withHooksHOC = (Component: any) => {
return (props: any) => {
const screenWidth = useScreenWidth();
return <Component width={screenWidth} {...props} />;
};
};
复制代码
最后一步是用该HOC简单包装我们现有的类组件。然后,我们仅使用width属性作为传递给组件的其他属性。
import React from 'react';
import { withHooksHOC } from './withHooksHOC';
interface IHooksHOCProps {
width: number;
}
class HooksHOC extends React.Component<IHooksHOCProps> {
render() {
return <p style={{ fontSize: '48px' }}>width: {this.props.width}</p>;
}
}
export default withHooksHOC(HooksHOC);
获取窗口视口大小
import React,{ useState , useEffect , useCallback ,Component} from 'react'
function useWinsize(){
//返回值:包含 2 个元素的数组,分别为状态值和状态更新函数
const [size,SetSize] = useState({
// 参数为初始值
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
})
// useCallback 用于缓存方法 useMemo 用于缓存属性 状态
const onResize = useCallback(()=>{
// 在此调用方法可以将方法缓存起来提高效率
SetSize({
// 更新数据,直接传newValue 值第一种更新用法
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight
})
},[]) // 第二个参数传空数组,表示didMount 只执行一次
useEffect(()=>{
// 当didmount 时执行一遍添加一个窗口监听事件,当 触发resize 事件时,执行 onResize 的回调函数更新窗口
window.addEventListener('resize',onResize)
// 调用unMount 组件卸载时移除组件监听事件,防止在其他组件继续监听
return ()=>{
window.removeEventListener('resize',onResize)
}
},[])
// 调用该函数返回size 给使用者
return size
}
//组件中使用
export default function MyHooks (){
const size = useWinsize()
console.log(size);
return <div>size:{size.width}x{size.height}</div>
}
// App.jsx ----------------------------------------
import React, { Component } from 'react'
// 使用Hook
import Win from './react_hook/UseClientHeigth'
export default class App extends Component {
render() {
return (
<div>
<Win/>
</div>
)
}
}
点击按钮更改名字,并展示延迟等待
import React, { useState, useEffect } from 'react'
const usePerson = (name) => {
const [loading, setLoading] = useState(true)
const [person, setPerson] = useState({})
useEffect(() => {
setLoading(true)
setTimeout(()=> {
setLoading(false)
setPerson({name})
},2000)
},[name])
return [loading,person]
}
const AsyncPage = ({name}) => {
const [loading, person] = usePerson(name)
return (
<>
{loading?<p>Loading...</p>:<p>{person.name}</p>}
</>
)
}
const PersonPage = () =>{
const [state, setState]=useState('')
const changeName = (name) => {
setState(name)
}
return (
<>
<AsyncPage name={state}/>
<button onClick={() => {changeName('名字1')}}>名字1</button>
<button onClick={() => {changeName('名字2')}}>名字2</button>
</>
)
}
export default PersonPage
获取鼠标视口
import React, { useState, useEffect } from 'react'
const useMousePosition = () => {
const [position, setPosition] = useState({x: 0, y: 0 })
useEffect(() => {
const updateMouse = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
document.addEventListener('mousemove', updateMouse)
return () => {
document.removeEventListener('mousemove', updateMouse)
}
})
return position
}
export default useMousePosition
// 需要引入时
import React, { Component } from 'react'
// 使用Hook
import useMousePosition from './react_hook/useMouse'
// 注意只能在函数里使用
export default function App(){
const position = useMousePosition()
return (
<div>
{position.x},{position.y}
</div>
)
}