React 基础
初步创建
React特点
- 采用组件化模式、声明式编码,提高开发效率及组件复用率
- 在React Native中可以使用React语法进行移动端开发
- 使用虚拟DOM + 优秀的Diffing算法,经量减少与真实DOM的交互
Hello World
<body>
<div id="root"></div>
<!-- react核心库 -->
<script src="./react.development.js"></script>
<!-- 支持react操作DOM -->
<script src="./react-dom.development.js"></script>
<!-- 将jsx转为js -->
<script src="./babel.min.js"></script>
<script type="text/babel">
// 创建虚拟DOM
const VDOM = <div>Hello World</div>
// 渲染虚拟DOM
ReactDOM.render(VDOM, document.getElementById('root'));
</script>
</body>
创建虚拟DOM的两种方法
- jsx语法
<script type="text/babel">
// 创建虚拟DOM
const VDOM = <div>Hello World</div>
// 渲染虚拟DOM
ReactDOM.render(VDOM, document.getElementById('root'));
</script>
- js语法
<script type="text/javascript">
// 创建虚拟DOM
const VDOM = React.createElement('div', {id: 'title'}, 'Hello World')
// 渲染虚拟DOM
ReactDOM.render(VDOM, document.getElementById('root'));
</script>
关于虚拟DOM
- 本质是Object类型对象
- 虚拟DOM比较“轻”,真实DOM比较“重”。虚拟DOM上的属性较真实DOM要少
- 虚拟DOM会被React转化为真实DOM,在页面上渲染
JSX语法规则
- 定义虚拟DOM时,不能用引号包括
- 标签中混入js表达式时,要用{}
- 样式的类名指定,不用class而用className
- 内联样式写法:style={{key: value}}
- 虚拟DOM只能有一个跟标签
- 标签必须闭合
- 标签首字母
- 若小写字母开头,则将标签转为html同名元素。若html中无该对应同名标签,则报错
- 若大写字母开头,react会将标签作为组件渲染
ES6结构赋值
const obj = {a: {b: 1}};
const {a} = obj; // 传统结构赋值
const {a: {b}} = obj; // 连续结构赋值
const {a: {b: newName}} = obj; // 连续结构赋值 + 重命名
组件
函数式组件
- React解析组件标签,找到了MyComponent组件。
- 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真是DOM,并渲染到页面
<script type="text/babel">
// 创建函数式组件
function MyComponent() {
console.log(this); // 因为bable编译开启了严格模式,此处的this是undefined
return <h2>我是函数式组件</h2>
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
</script>
类组件
- React解析组件标签,找到了MyComponent组件。
- 发现组件是使用类定义的,随后new出该类的实例对象,并通过该实例调用到原型上的render方法
- 将render返回的虚拟DOM转为真实DOM,并渲染到页面
<script type="text/babel">
// 创建类式组件
class MyComponent extends React.Component {
// render方法是放在MyComponent的原型对象上的,供实例使用
render() {
// render中的this是MyComponent的实例对象
console.log(this);
return <div>这是一个类组件</div>
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
</script>
组件实例的三大核心
状态state
- state是组件对象的一个重要属性,他的值是对象。
- 组件被称为“状态机”,通过更新组件的state来更新对应的页面显示
- 注意:
- 组件中render方法中的this为组件实例对象
- 组件自定义方法中的this为undefined
- 解决方法1:在构造函数中bind函数this指向
- 解决方法2:赋值语句定义箭头函数
- state数据不能直接修改,直接修改不会触发render方法更新页面。需要调用内置API修改:setState()
class Weather extends React.Component {
state = {
isHot: false
}
constructor(props) {
super(props);
this.demo = this.demo.bind(this); // 修改demo方法中的this指向
}
render() {
const {isHot} = this.state;
// 此处changeWeather方法只是赋值给了onClick,并未调用
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>;
}
// 箭头函数没有自己的this, 它的this是继承而来; 默认指向在定义它时所处的对象(宿主对象)
changeWeather = () => {
// 内置API修改state
this.setState({isHot: !this.state.isHot});
}
// 事件中调用自定义函数时,this为undefined。
// 原因1:demo方法并非由Weather实例对象调用
// 原因2:类中的方法,局部开启了严格模式,导致this为undefined
demo() {
console.log(this);
}
}
组件传值props
- props属性为只读
- 类组件
class Person extends React.Component { // 限定标签属性类型、是否必传 static propTypes = { // PropTypes需要引入prop-types.js文件 name: PropTypes.string.isRequired, // name必传,且为字符串 age: PropTypes.number, // 限制为数值 sex: PropTypes.string, // 限制为字符串 speak: PropTypes.func // 限制为函数 } // 指定标签属性默认值 static defaultProps = { sex: 'man', age: 18 } // react基本不用构造函数 constructor(props) { super(props); // 是否接受props和是否给super传递props,取决于constructor中石否用到this.props。不传则取不到 console.log(this.props); } render() { const {name, age, sex} = this.state; return ( <ul> <li>{name}</li> <li>{age}</li> <li>{sex}</li> </ul> ); } } const data = { name: '张三', age: 18, sex: '男' } function speak() { console.log('speak'); } // 渲染组件到页面 ReactDOM.render(<Person name={data.name} age={data.age} sex={data.sex} speak={speak}/>, document.getElementById('root1')); // 此处的{...data}并非构造字面量对象时使用的展开语法。{}表示嵌入js表达式,jsx标签中可以直接使用...obj展开一个对象 ReactDOM.render(<Person {...data}/>, document.getElementById('root2'));
- 函数组件
function Person(props) { const {name, age, sex} = props; return ( <ul> <li>{name}</li> <li>{age}</li> <li>{sex}</li> </ul> ); } // 限定标签属性类型、是否必传 Person.propTypes = { name: PropTypes.string.isRequired, // name必传,且为字符串 age: PropTypes.number, // 限制为数值 sex: PropTypes.string, // 限制为字符串 speak: PropTypes.func // 限制为函数 } // 指定标签属性默认值 Person.defaultProps = { sex: 'man', age: 18 } const data = { name: '张三', age: 18, sex: '男' } function speak() { console.log('speak'); } // 渲染组件到页面 ReactDOM.render(<Person name={data.name} age={data.age} sex={data.sex} speak={speak}/>, document.getElementById('root1')); ReactDOM.render(<Person {...data}/>, document.getElementById('root2'));
refs获取节点
- 字符串形式(较老版本中的写法,不推荐使用)
class Demo extends React.Component { render() { return ( <div> <div> <input ref="input" type="text"/> <button onClick={this.getValue}>按钮</button> </div> </div> ); } getValue = () => { const { input } = this.refs; console.log(input.value); } }
- 回调形式
// 内联回调函数 和 绑定回调函数 实际使用中并没有什么影响,推荐使用内联回调 class Demo extends React.Component { render() { return ( <div> <div> { /* 内联回调函数:在state更新的时候,内联回调会被调用两次,第一次c为null,第二次c为节点 */ } <input ref={element => this.input = element} type="text"/> <button onClick={this.getValue}>按钮</button> </div> <div> { /* 绑定回调函数:在state更新的时候,绑定的函数不会被再次调用 */ } <input ref={this.getInput} type="text"/> <button onClick={this.getValue}>按钮</button> </div> </div> ); } getInput = (element) => { this.input = element; } getValue = () => { const { input } = this; console.log(input.value); } }
- createRef API形式 (React最推荐的形式)
- React.createRef()调用后返回一个容器,该容器可以存储被ref标识的节点
- 每一个容器只能存一个
class Demo extends React.Component { myRef = React.createRef(); render() { return ( <div> <div> <input ref={this.myRef} type="text"/> <button onClick={this.getValue}>按钮</button> </div> </div> ); } getValue = () => { console.log(this.myRef.current.value); } }
React事件处理
- 通过onXxx属性指定事件处理函数(注意大小写)
- React使用的是自定义事件,而不是使用原生的DOM事件。—为了更好的兼容性
- React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) —为了高效
- 通过event.target得到发生事件的DOM元素对象 — 勿过度使用ref
收集表单数据
- 非受控组件:现用现取(使用ref)
// 在提交的时候才取input中的值 class Demo extends React.Component { handleSubmit = (e) => { e.preventDefault(); console.log(`用户名:${this.userName.value}---密码:${this.password.value}`) }; render() { return ( <form onSubmit={this.handleSubmit}> 用户名:<input type="text" ref={c => this.userName = c} /> 密码:<input type="password" ref={c => this.password = c} /> <button>提交</button> </form> ); } }
- 受控组件:通过状态进行数据存储(避免使用ref)
// 页面输入类的DOM,随着输入将数据维护到state中。用的时候从状态中取 class Demo extends React.Component { state = { userName: '', password: '' } handleSubmit = (e) => { e.preventDefault(); const {userName, password} = this.state; console.log(`用户名:${userName}---密码:${password}`) }; saveUserName = (e) => { this.setState({ userName: e.target.value }); }; savePassword = (e) => { this.setState({ password: e.target.value }); }; render() { return ( <form onSubmit={this.handleSubmit}> 用户名:<input type="text" onChange={this.saveUserName}/> 密码:<input type="password" onChange={this.savePassword}/> <button>提交</button> </form> ); } }
高阶函数
- 满足一下两个条件之一,即是高阶函数
- 函数A,接收的参数是一个函数,那么A是高阶函数:Promise,setTimeout,setInterval,map,forEach…
- 函数A,调用后的返回值是一个函数,那么A是高阶函数:
函数柯里化
- 通过函数的调用继续返回一个函数,实现多次接收参数最后统一处理的函数编码形式
class Demo extends React.Component { state = { userName: '', password: '' } // 高阶函数(返回值是一个函数) saveFormData1 = (type) => { return event => { this.setState({[type]: event.target.value}); } }; saveFormData2(type, event) { this.setState({[type]: event.target.value}); } render() { return ( <div> { /*用函数柯里化实现*/ } <form> 用户名:<input type="text" onChange={this.saveFormData1('userName')}/> 密码:<input type="password" onChange={this.saveFormData1('password')}/> <button>提交</button> </form> { /*不用函数柯里化实现*/ } <form onSubmit={this.handleSubmit}> 用户名:<input type="text" onChange={event => this.saveFormData2('userName', event)}/> 密码:<input type="password" onChange={event => this.saveFormData2('password', event)}/> <button>提交</button> </form> </div> ); } }
生命周期
旧版本react生命周期钩子函数
- 初始化阶段
- constructor() {}:构造函数
- componentWillMount() {}:组件将要挂载
- render() {}:组件挂载 (必用)
- componentDidMount() {}:组件挂载完毕 (常用)
- 更新阶段:组件内部this.setState 或 父组件render触发
- componentWillReceiveProps() {}:父组件render更新。子组件将要接受新的props时触发(第一次接受props不触发)
- shouldComponentUpdate() {}:setState()调用后,控制是否更新组件的阀门。必须要有一个返回值,返回值类型为布尔值
- componentWillUpdate() {}:组件将要更新
- render() {}:组件更新
- componentDidUpdate() {}:组件更新完毕
- 卸载组件:ReactDOM.unmountComponentAtNode(节点)
- componentWillUnmount() {}:组件将要卸载 (常用)
- componentWillUnmount() {}:组件将要卸载 (常用)
新版本react生命周期钩子函数
- 区别:
- 废弃(或即将废弃)三个钩子函数:componentWillMount、componentWillUpdate、componentWillReceiveProps。需要使用时,需要加前缀UNSAFE_
- 新增两个钩子函数(并不常用):getDerivedStateFormProps、getSnapsshotBeforeUpdate
- getDerivedStateFormProps:状态更新时,在render之前调用
// 此方法调用,state的值在任何时候都取决于props。state将无法进行修改 // props为组件传入的参数,state为组件定义的state static getDerivedStateFromProps(props, state) { console.log('new-----getDerivedStateFormProps'); return props; } ReactDOM.render(<NewReact count={123}/>, document.getElementById('root'))
- getSnapsshotBeforeUpdate:在最近一次渲染输出(提交到DOM节点之前)调用,它使得组件能在发生更改之前从DOM中捕获一些信息。此声明周期的任何返回值将最为参数传递给componentDidUpdate
getSnapshotBeforeUpdate(prevProps, prevState) { console.log(prevProps, prevState) return '123'; } componentDidUpdate(prevProps, prevState, snapshot) { console.log(prevProps, prevState, snapshot); // snapshot = '123' }
重要的钩子函数
- render
- componentDidMount
- componentWillUnmount
即将废弃的钩子函数
- componentWillMount
- componentWillUpdate
- componentWillReceiveProps
Diffing算法和Key的作用
Diffing算法
- React在将虚拟DOM转成真实DOM的时候进行一个比较,已经渲染过一次的节点,再次渲染的时候,会对相同的key值的节点进行比较,如果内容相同,则复用原来的DOM
key的作用
-
key是虚拟DOM对象的一个标识
-
当状态中的数据发生变化,React会根据新数据生成新的虚拟DOM。随后React将进行新的虚拟DOM与旧的虚拟DOM的Diffing算法比较。规则如下
- 旧虚拟DOM中找到了与新虚拟DOM相同的key:
- 若虚拟DOM中内容没变,直接使用之前的真实DOM;
- 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
- 旧虚拟DOM中未找到与新虚拟DOM相同的key:根据数据创建新的真实DOM,随后渲染到到页面
- 旧虚拟DOM中找到了与新虚拟DOM相同的key:
-
index作为key可能引发的问题:
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作: 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
- 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
- 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
-
开发中如何选择key?:
- 最好使用每条数据的唯一标识作为key, 比如id
- 如果确定只是简单的展示数据,用index也是可以的。
React脚手架
- 用于快速创建React库的模板项目
- 项目整体技术架构:react + webpack + es6 + eslint
- 使用脚手架开发项目特点:模块化,组件化,工程化
创建项目
- 全局安装脚手架:npm install -g create-react-app
- 创建项目:create-react-app my-project
- 启动项目:npm start
项目结构
- public ---- 静态资源文件夹
- favicon.icon ------ 网站页签图标
- index.html -------- 主页面
<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>
- manifest.json ----- 应用加壳的配置文件
- robots.txt -------- 爬虫协议文件
- src ---- 源码文件夹
- App.css -------- App组件的样式
- App.js --------- App组件
- App.test.js ---- 用于给App做测试
- index.css ------ 样式
- index.js ------- 入口文件
ReactDOM.render( // 用于检测内部包裹的所有子组件中,react使用是否合理(例如字符串形式的ref) <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // 用于记录页面性能,需要在reportWebVitals.js文件中进行相应配置 reportWebVitals();
- logo.svg ------- logo图
- reportWebVitals.js — 页面性能分析文件(需要web-vitals库的支持)
- setupTests.js ---- 组件单元测试的文件(需要jest-dom库的支持)
React 请求数据
- react本身只关注界面,并不包含发送ajax请求的代码
- react中需要继承第三方ajax库(或自己封装)
axios
- 安装axios:yarn add axios
- 使用
import axios from 'axios'; axios.get('http://localhost:5000/students').then( res => {}, err => {} )
跨域处理办法
- package.json配置代理
缺点:只能指定一个代理// package.json { "proxy": "http://localhost:5000" } // 使用注意 // 浏览器不再直接给5000端口发送请求,而是像3000端口请求 // 3000服务器收到地址,先在当前端口找是否有匹配的地址,如果没有再向5000端口进行请求 axios.get('http://localhost:3000/students').then();
- 配置跨域文件:setupProxy.js
// 1.在src目录下新增setupProxy.js文件 // 2.配置setupProxy.js文件 const proxy = require('http-proxy-middleware'); module.exports = function (app) { app.use( proxy('/api', { // 匹配到'/api'前缀的请求,就会触发代理配置 target: 'http://localhost:5000', // 跨域目标服务器 // changeOrigin 控制服务器收到的请求头中HOST的值 // true => 与服务区地址一致:http://localhost:5000 // false => 真实的当前地址:http://localhost:3000 changeOrigin: true, pathRewrite: {'^/api': ''} // 重写请求路径,将'/api'替换为'' }), proxy('/master', { // 可以传多个参数 target: 'http://localhost:5001', changeOrigin: true, pathRewrite: {'^/master': ''} }) ) }
fetch
- window内置方法,不同于XMLHttpRequest的ajax请求
- 采用了关注分离的方式:连接服务器正常与否,与获取数据分离
// 关注分离
fetch(`https://api.github.com/search/users?q=${this.state.keyword}`)
.then( // 联系服务器成功与否
response => {
console.log('联系服务器成功', response);
return response.json();
}
)
.then(data => console.log(data)) // 获取数据
.catch(err => console.log(err))
// 代码优化
getData = async () => {
try {
const response = await fetch(`https://api.github.com/search/users?q=${this.state.keyword}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.log(error);
}
}
组件通信
父子组件通信:Props
- 父传子:父组件给子组件标签添加属性传值,子组件通过props接收
- 子传父:父组件给子组件传递一个函数,子组件调用父组件函数,同时想函数内进行传参
// 父组件
export default class App extends Component {
state = {
list: [],
isFirst: true,
isLoading: false,
err: ''
}
render() {
return (
<div className="container">
{ /* 传递函数 */ }
<Search updateState={this.updateState}/>
{ /* 批量传参 */ }
<List {...this.state}/>
</div>
)
}
updateState = (state) => {
this.setState(state)
}
}
// Search子组件
export default class Search extends Component {
getData = () => {
const {updateState} = this.props;
updateState({isFirst: false, isLoading: true, err: ''})
axios.get(`https://api.github.com/search/users?q=${this.state.keyword}`).then(
res => {
updateState({isLoading: false, list: res.data.items});
},
err => {
updateState({isLoading: false, err: err.message})
}
)
}
}
// List 子组件
export default class List extends Component {
render() {
const { list, isFirst, isLoading, err } = this.props;
return (
<div className="row">
{
isFirst ? <h2>第一次进入</h2> :
isLoading ? <h2>Loading......</h2> :
err ? <h2>err</h2> :
list.map(item => {
return (
<div className="card" key={item.id}>
<a href={item.html_url} target="_blank" rel="noreferrer">
<img alt="header_pic" src={item.avatar_url} style={{ width: '100px' }} />
</a>
<p className="card-text">{item.login}</p>
</div>
)
})
}
</div>
)
}
}
任意组件通信:PubSubJS
- 消息订阅与发布模式
// 订阅消息 import PubSub from 'pubsub-js'; export default class List extends Component { state = { list: [], isFirst: true, isLoading: false, err: '' } componentDidMount() { // 订阅 this.token = PubSub.subscribe('updateList', (msg, data) => this.setState(data)) } componentWillUnmount() { // 取消订阅 PubSub.unsubscribe(this.token); } } // 发布消息 export default class Search extends Component { getData = () => { // 发布 PubSub.publish('updateList', {isFirst: false, isLoading: true, err: ''}); axios.get(`https://api.github.com/search/users?q=${this.state.keyword}`).then( res => { PubSub.publish('updateList', {isLoading: false, list: res.data.items}); }, err => { PubSub.publish('updateList', {isLoading: false, err: err.message}); } ) } }
React 路由
SPA 的理解
- 单页面Web应用(single page web application, SPA)
- 整个应用只有一个完整的页面
- 点击页面中的跳转连接不会刷新页面,只会做页面的局部更新
- 数据都需要通过ajax请求获取,并在前端异步展现
路由的理解
- 路由是一个映射关系(key: value)
- key为路径,value是function或component
react-router-dom
- 基本使用:
- 导航区的标签:
<Link to="/home">Home</Link> <!-- NavLink默认会在匹配到路径的时候给标签加一个active类(可以不写) 可通过activeClassName自定义类名。 --> <NavLink activeClassName="active" to="/about"></NavLink>
- 展示区写Route标签进行路径的匹配:
<!-- V5版本 --> <Route path="/about" component={ About }/> <!-- V6版本 --> <Routes> <Route path="/home" element={ <Home /> }></Route> </Routes>
- 所有的路由组件都需要包裹在<BrowserRouter>或者<HashRouter/>标签内
- 导航区的标签:
路由组件与一般组件(V5)
- 写法不同
- 一般组件:
- 路由组件:<Link path="/demo" component={ Demo } />
- 存放位置不同:
- 一般组件:components
- 路由组件:pages
- 接收到的props不同:
- 一般组件:写组件标签时,传递什么就能收到什么
- 路由组件:收到三个固定属性
- history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
listen: ƒ listen(listener)
push: ƒ push(path, state)
replace: ƒ replace(path, state) - location:
pathname: “/home”
search: “”
state: undefined - match:
params: {}
path: “/home”
url: “/home”
- history:
NavLink二次封装
props接收标签传递的属性参数,同时接收标签体内容在props.children中。
使用children时可以直接赋值给标签属性children,即展示标签体内容
// 封装
export default class MyNavLink extends Component {
render() {
return (
<NavLink activeClassName="active-class" className="list-group-item" {...this.props} />
// 等同于
<NavLink activeClassName="active-class" className="list-group-item" to={this.props.to} children={this.props.children} />
// 等同于
<NavLink activeClassName="active-class" className="list-group-item" to={this.props.to}>{this.props.children}</NavLink>
)
}
}
// 使用
<MyNavLink to="/about">About</MyNavLink>
BrowserRouter下使用相对路径问题
BrowserRouter下使用多层级路由,会导致使用相对路径的文件丢失(找不到文件,默认返回index.html)。
<!-- index.html -->
<link rel="stylesheet" href="./css/bootstrap.css">
<!-- app.jsx -->
<Route path="/api/home" component={ Home }/>
解决方法:
- 相对路径改为绝对路径
<link rel="stylesheet" href="/css/bootstrap.css">
- 使用%PUBLIC_URL%作为根路径
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
- 使用HashRouter
ReactDOM.render(<HashRouter><App /></HashRouter>, document.getElementById('root'));
精准匹配exact
- 模糊匹配:起始路径包含Route需要的路径,即可匹配。url可以多写
<!-- 起始路径可以匹配到Route,则home组件就可以展示。 同时Route需要的路径一个也不能少 --> <MyNavLink to="/home/a/b">home</MyNavLink><!-- 可以匹配 --> <!-- 起始路径为/a。无法匹配到Route,不会再匹配二段路径/home --> <MyNavLink to="/a/home/b">home</MyNavLink><!-- 无法匹配 --> <Switch> <Route path="/home" component={ Home }/> </Switch>
- 精准匹配:路径需要完全匹配才能使用
<MyNavLink to="/home/a/b">home</MyNavLink> <Switch> <Route exact path="/home" component={ Home }/> </Switch>
- 精准匹配不要随意开启,需要的时候再开。有时候开启精准匹配会导致无法匹配二级路由
Redirect重定向
- 一般写在所有路由的最下方,当所有路由无法匹配时,跳转到Redirect指定的路由
- 属性:
- to:需要重定向到哪个路径
- from:当匹配到from中的路径时,重定向到to后的路径。不写from默认为"/"
- exact:开启精准匹配
- push:将新路径push进history中,而不是替换
<Switch> <Route path="/about" component={ About }/> <Route exact path="/home" component={ Home }/> <Redirect push from="/test" to="/home"></Redirect> <Redirect exact to="/about"></Redirect> </Switch>
嵌套路由
- "/home/news"路径匹配过程:
- /home/news 分为 home 和 news。在app.js中进行路由匹配。匹配到Home组件,进行渲染
- Home组件加载,home中注册了新的路由。路径/home/news继续进行匹配
- 匹配到/home/news路径,加载News组件
- 注册子路由时,要写上父路由的完整路径
- 路由的匹配时按照注册路由的顺序进行的
<!-- app.js --> <!-- app.js中的路由最先注册 --> <Switch> <Route path="/about" component={ About }/> <Route path="/home" component={ Home }/> <Redirect to="/about"></Redirect> </Switch> <!-- home.js --> <!-- home组件加载后,继续注册home中的路由,再进行匹配 --> <Switch> <Route path="/home/news" component={ News } /> <Route path="/home/message" component={ Message } /> <Redirect to="/home/news"></Redirect> </Switch>
路由传参(三种方式)
- params参数
- 路由链接(携带参数):<Link to="/home/message/detail/1/title">链接</Link>
- 注册路由(声明接收):<Route path="/home/message/detail/:id/:title" component={ Detail } />
- 接收参数:const {id, title} = this.props.match.params;
- search传参
- 路由链接(携带参数):<Link to="/home/message/detail?id=1&title=title">连接</Link>
- 注册路由(声明接收):<Route path="/home/message/detail" component={ Detail } />
- 接收参数:
import qs from 'querystring'; // 转换查询字符串的库,react自带。qs.parse <=> qs.stringify const search = this.props.location.search; const {id, title} = qs.parse(search.slice(1));
- 备注:获取到的search是urleccoded编码字符串,需要借助querystring解析
- state传参
- 路由链接(携带参数):<Link to={{pathName: ‘/home/message/detail’, state: {id:1, title: ‘title’}}}>连接</Link>
- 注册路由(声明接收):<Route path="/home/message/detail" component={ Detail } />
- 接收参数:const {id, title} = this.props.location.state;
- 备注:刷新也能保留住参数,原理是state在history中有存储
push和replace
- 默认为push.是一个压栈操作,显示的是栈顶的路由
<Route push path="/home/message/detail" component={ Detail } />
- replace是替换当前路由,history中不再保留当前路由
<Route replace path="/home/message/detail" component={ Detail } />
编程式路由导航
- history中的方法:
- go(num):参数为num,正数表示前进几步,负数表示后退几步
- goForward():前进一步
- goBack():后退一步
- replace(url, state):替换路由
- push(url, state):压栈路由
// 携带params参数 this.props.history.push(`/home/message/detail/${id}/${title}`); // 携带search参数 this.props.history.push(`/home/message/detail?id=${id}&title=${title}`); // 携带state参数 this.props.history.push(`/home/message/detail`, {id, title});
withRouter
- 可以让非路由组件的props拥有history/location/match三个对象
- 通过history实现一般组件的编程式路由导航
import {withRouter} from 'react-router-dom';
class Header extends Component {
forward = () => {
this.props.history.goForward();
}
render() {
return (
<div>
<h2>React Router Demo</h2>
<button onClick={this.forward}>forward</button>
</div>
)
}
}
export default withRouter(Header);
BrowserRouter 和 HashRouter的区别
- 底层原理不一样
- BrowserRouter使用的是H5的history api,不兼容IE9一下版本浏览器
- HashRouter使用的是URL的哈希值
- url表现不一样
- BrowserRouter的路径中没有#:localhost:3000/home/news
- HashRouter中包含#:localhost:3000/#/home/news
- 刷新后对state参数的影响
- BrowserRouter没有任何影响,因为state存储在history中
- HashRouter刷新后会导致state数据丢失
- 备注:HashRouter可以解决一些路径错误相关的问题(即上方提及的,引入文件的相对路径问题)
Redux状态管理
- 什么是redux:redux是专门用于做状态管理的JS库。可以集中式管理react应用中多个组件共享的状态
- 适用场景:
- 某个组件的状态,需要与其它组件共享
- 一个组件需要改变另一个组件的状态
- 总体原则:能不用就不用,不用的情况下开发吃力才考虑使用
Redux三大核心
action
- 动作的对象
- 包含2个属性~
- type:标识属性, 值为字符串, 唯一, 必要属性
- data:数据属性, 值类型任意, 可选属性
reducer
- 用于初始化状态、加工状态。
- 加工时,根据旧的state和action, 产生新的state的纯函数。
store
- 将state、action、reducer联系在一起的对象
- 如何得到此对象?
- import {createStore} from ‘redux’
- import reducer from ‘./reducers’
- const store = createStore(reducer)
- 此对象的功能?
- getState(): 得到state
- dispatch(action): 分发action, 触发reducer调用, 产生新的state
- subscribe(listener): 注册监听, 当产生了新的state时, 自动调用
Redux应用
-
src下建立:
- redux文件夹
- store.js
- count_reducer.js
- count_action.js
-
store.js:
- 引入redux中的createStore函数,创建一个store
- createStore调用时要传入一个为其服务的reducer
- 第二个参数为引入中间件,来自第三方库,用于支持异步action
- 记得暴露store对象
import { createStore, applyMiddleware } from 'redux'; import { countReducer } from './counter_reducer'; import thunk from 'redux-thunk'; export default createStore(countReducer, applyMiddleware(thunk))
-
count_reducer.js:
- reducer的本质是一个函数,接收:preState,action,返回加工后的状态
- reducer有两个作用:初始化状态,加工状态
- reducer被第一次调用时,是store自动触发的,
- 传递的preState是undefined,
- 传递的action是:{type:’@@REDUX/INIT_a.2.b.4}
const countInit = 0; export default function countReducer(preState = countInit, action) { const { type, data } = action; switch(type) { case 'increment': return preState + data; case 'decrement': return preState - data; default: return preState; } }
-
count_action.js
- 用于提供创建action对象的方法
- 异步action需要创建一个方法作为参数传给dispatch.同时store需要通过applyMiddleware方法引入thunk
// 同步action export function createIncrementAction(data) { return {type: 'increment', data}; } // 异步action export function createIncrementAsyncAction(data, time) { // store.dispatch调用此方法时,会传入dispatch方法 return (dispatch) => { setTimeout(() => { dispatch(createIncrementAction(data)) }, time); } }
-
在index.js中监测store中状态的改变,一旦发生改变重新渲染
- 备注:redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。
import store from './store'; store.subscribe(ReactDOM.render(<App />, document.getElementById('root')));
react-redux
- react-redux是react官方提供的库
- 它将所有组件分成两大类:
- 容器组件:
- 负责管理数据和业务逻辑,不负责UI的呈现
- 可以使用 Redux 的 API
- 一般保存在containers文件夹下
// 容器组件完整写法 import Count from '../../components/Count'; // UI组件 import { connect } from 'react-redux'; // 连接UI组件与redux,并返回一个容器组件 import { createDecrementAction, createIncrementAction, createIncrementAsyncAction } from '../../redux/count/count_action' // 给UI组件传递props属性 const mapStateToProps = (state) => { return {count: state}; } // 给UI组件传递方法 const mapDispatchToProps = (dispatch) => { return { increment: data => dispatch(createIncrementAction(data)), decrement: data => dispatch(createDecrementAction(data)), asyncIncrement: data => dispatch(createIncrementAsyncAction(data)) } } // 连接UI组件与redux,并返回一个容器组件 export default connect(mapStateToProps, mapDispatchToProps)(Count);
// 容器组件简写 export default connect( state => ({count: state}), { increment: createIncrementAction, decrement: createDecrementAction, asyncIncrement: createIncrementAsyncAction } )(Count);
- UI组件:
- 只负责 UI 的呈现,不带有任何业务逻辑
- 通过props接收数据(一般数据和函数)
- 不使用任何 Redux 的 API
- 一般保存在components文件夹下
- 容器组件:
- 使用react-redux后,无需再检测状态更新
- Provider给所有容器组件提供store
import ReactDOM from 'react-dom'
import App from './App';
import store from './redux/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>
, document.getElementById('root'));
- 容器组件和UI组件可以合并到一个文件中
多个reducer处理
- store.js文件合并多个reducer
import { createStore, combineReducers } from 'redux'; import countReducer from './reducers/count'; import personReducer from './reducers/person'; const reducers = combineReducers({ count: countReducer, persons: personReducer }); export default createStore(reducers);
- react-redux容器组件引用
export default connect( state => ({count: state.count, persons: state.persons}), {increment: createIncrementAction} )(Count)
redux开发者工具
- chrome商店:Redux DevTools
- 项目安装库:yarn add redux-devtools-extension
- store.js引入库
import { createStore, combineReducers, applyMiddleware } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import countReducer from './reducers/count' import personReducer from './reducers/person'; import thunk from 'redux-thunk'; const allReducer = combineReducers({ count: countReducer, persons: personReducer }) export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
拓展
setState更新状态的两种写法
- 对象式setState:setState(stateChange, [callback])
- stateChange为状态改变对象
- callback为状态更新完毕的回调函数,setState操作是异步的,该回调会在render调用后才会被调用
- 函数式setState:setState(updater, [callback])
- updater为返回stateChange对象的函数
- updater可以接收到state和props
- callback同上
- 注意:
- setState()方法有时候是同步的,有时候是异步的
- 异步:由React控制的事件处理程序(比如onClick/onChange等),以及生命周期函数调用setState不会同步更新state。
- 同步:React控制之外的事件中调用setState是同步更新的。比如原生js绑定的事件,setTimeout/setInterval等。
- 原因:假如setState为同步执行,则每次调用都会完整的执行complementShouldUpdate/componentWillUpdate/render/componentDidUpdate这些生命周期函数,虽然有diff算法,但也会一定程度影响性能。通过异步执行,等待多个setState都执行完毕,将最终的state状态进行渲染,则各个生命周期函数都只要执行一次。
- 原理:在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates将isBatchingUpdates修改为true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。
- 多个setState调用会合并处理。下面程序调用了两次setState,但render只会调用一次
render() { console.log('render') } hanldeClick() { this.setState({ name: 'jack' }) this.setState({ age: 12 }) }
- 对象式setState与函数式setState区别
hanldeClick() { // 连续调用三次对象式setState,结果是count只+1。 // 因为setState异步调用,每次调用完,this.state.count状态并未更新,三次调用取到的count值是一样的 this.setState({ count: this.state.count + 1 }) this.setState({ count: this.state.count + 1 }) this.setState({ count: this.state.count + 1 }) // 连续调用三次函数式setState,结果是count+3 // 多次调用函数式setState的情况,React会保证调用每次函数,state都已经合并了之前的状态修改结果。 this.setState(state => ({count: state.count + 1})); this.setState(state => ({count: state.count + 1})); this.setState(state => ({count: state.count + 1})); }
- 在同一个事件处理程序中不要混用对象式setState和函数式setState
- setState()方法有时候是同步的,有时候是异步的
Hooks
- 支持在函数组建中使用state和其它一些特性
- 三个常用的Hook
State Hook
React.useState()
// state更新,Hooks函数会重新调用。React在第一次调用useState后,会对创建的state进行缓存,后续执行不会覆盖原有结果
export default function Hooks () {
// 数组结构赋值,第一个为定义的state属性,第二个为修改属性的方法
const [count, setCount] = React.useState(0);
function add() {
// 同setState方法
setCount(count + 1);
setCount(count => ++count);
}
return (
<div>
<div>Count: { count }</div>
<button onClick={add}>+1</button>
</div>
)
}
Effect Hook
React.useEffect()
export default function Demo () {
const [count, setCount] = React.useState(0);
// 参数一(回调函数):初始化会执行一次,后续更新state是否调用,取决于参数二
// 参数二(数组):不传第二参数时,相当于检测所有state,任何state更新,参数一回调都会调用。传空数组,相当于不检测任何state。数组中指定检测count,则在初始化加载和count更新时,调用参数一的回调函数。
React.useEffect(() => {
const timer = setInterval(() => {
setCount(count => ++count);
}, 1000);
// 参数一的回调函数返回值,相当于componentWillUnmount钩子函数
return () => {
clearInterval(timer);
}
}, [count])
function add() {
setCount(count => ++count);
}
function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
return (
<div>
<div>Count: { count }</div>
<button onClick={add}>+1</button>
<button onClick={unmount}>卸载</button>
</div>
)
}
Ref Hook
React.useRef()
export default function Demo () {
const input = React.useRef();
function getRef() {
console.log(input.current.value);
}
return (
<div>
<input type="text" ref={input} />
<button onClick={getRef}>getRef</button>
</div>
)
}
Fragment
- 在react渲染时会被忽略的一个标签。相当于Angular中的<ng-container>标签
- 也可以写<>空标签,但空标签无法带key值
import React, { Component, Fragment } from 'react'
export default class Demo extends Component {
render() {
return (
<Fragment key={1}>
<div>demo</div>
</Fragment>
)
}
}
Context
- 一种组件通讯方式,常用语祖组件与后代组件之间
import React, { Component } from 'react';
// 创建Context容器对象。祖组件与后代组件公用
const MyContext = React.createContext();
// 提取Provider,方便祖组件包裹后代组件
const {Provider} = MyContext;
export default class A extends Component {
state = {name: '张三'}
render() {
return (
<div>
<h3>A组件</h3>
<h4>state: { this.state.name }</h4>
<hr />
{ /*
<Provider>也可以用<MyContext.Provider>
value中的传值,后代组件都可以使用。value可以传值,也可以传对象
*/ }
<Provider value={ this.state.name }>
<B />
</Provider>
</div>
)
}
}
// 子组件传值一般不用Context,因为provider更便捷
// 取值方式一:
class B extends Component {
// 使用context中的数据之前,需要进行申明
// 声明后,this中的context才会有值
static contextType = MyContext;
render() {
return (
<div>
<h3>B组件</h3>
<h4>state:{this.context}</h4>
<hr />
<C />
</div>
)
}
}
// 取值方式二:
// 函数组件:只能通过方式二取值
class C extends Component {
render() {
return (
<div>
<h3>C组件</h3>
<MyContext.Consumer>
{ value => (<h4>state:{value}</h4>) }
</MyContext.Consumer>
<hr />
</div>
)
}
}
组件优化
- component 存在两个问题:
- 只要执行setState(),即使不改变状态数据,组件也会重新render()
- 只要当前组件重新render(),就会自动重新render子组件
- 解决思路:只有当组件state或者props数据发生变化,才重新render
- 解决方法:
- 手动编写shouldComponentUpdate钩子函数
shouldComponentUpdate(nextProps, nextState) { return nextState.name !== this.state.name } shouldComponentUpdate(nextProps, nextState) { return nextProps.name !== this.props.name; } }
- 将组件继承的Compoent改为PureComponent
import React, { PureComponent } from 'react' export default class Parent extends PureComponent {}
- 手动编写shouldComponentUpdate钩子函数
render props
- 向组件内动态传入组件
- 组件标签嵌套使用
export default class A extends Component { render() { return ( <div> <h3>A组件</h3> <B><C /></B> </div> ) } } class B extends Component { render() { return ( <div> <h3>B组件</h3> {this.props.children} </div> ) } }
- 通过props传参:可以给嵌入的组件进行传参
export default class A extends Component { render() { return ( <div> <h3>A组件</h3> <B render={name => <C name={name}/>}/> </div> ) } } class B extends Component { render() { return ( <div> <h3>B组件</h3> {this.props.render('张三')} </div> ) } }
- 组件标签嵌套使用
错误边界
- 用于捕获后代组件错误,渲染出备用页面。将错误控制在一定范围内
- 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其它组件在合成事件、定时器中产生的错误
- 错误边界的控制,只会在生产环境生效
- 使用方式
state = { hasError: '' } // 生命周期函数,一旦后代组件包错,就会出发 static getDerivedStateFromError(error) { return {hasError: error} } // 统计页面的错误,发送请求的后台 componentDidCatch(error, info) { console.log(error, info) } render() { return ( <div>{this.state.hasError ? '页面出错' : <Child />}</div> ) }
组件通信方式总结
- 组件关系:
- 父子组件
- 兄弟组件
- 跨级组件
- 几种通信方式:
- props:
- children props
- render props - 消息订阅发布
- pubsub、event - 状态集中式管理
- redux、dva等 - Context
- 生产者-消费者模式
- props:
- 搭配方式:
- 父子组件:props
- 兄弟组件:消息订阅-发布、集中式管理
- 跨级组件:消息订阅-发布、集中式管理、Context(开发用的少,封装插件用的多)