React入门
React——用于动态构建用户界面的 JavaScript 库(声明式编码、组件化模式)
React Native开发移动端原生应用
JSX—— JavaScript XML
1、基本使用:
1.1:相关库
- react.js:React核心库。
- react-dom.js:提供操作DOM的react扩展库。
- babel.min.js:解析JSX语法代码转为JS代码的库。
1.2:JS创建基本语法:React.createElement(component, props, ...children)
1.3:JSX创建基本语法:直接写html结构即可,babel转义成js。其就是 js 创建语法的语法糖
1.4:渲染基本语法:ReactDOM.render(virtualDOM, containerDOM)
1.5:语法规则: 1、定义虚拟DOM无引号;
2、标签混入js表达式(不是js语句),用{};
这里要区分一下什么是表达式什么是语句(代码):
1、语句:if、for等,不产生值的代码,控制代码逻辑的
2、表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,比如a、a+b、demo(1),其实就是等号右边
3、标签类名用className,不用class,因为ES6有关键字class
4、内联样式,要用{{ }},最外层相当于jsx的执行,如2,里层为对象,所以为双花括号
5、JSX虚拟DOM只能有一个根标签
6、JSX标签必须闭合,<input type="text"/>或者<input type="text"></input>
7、标签首字母:
(1)若小写开头,则将该标签转为html中的同名元素;若无该标签,则报错
(2)若大写,则认为是组件
1.6:模块、组件、模块化与组件化
模块(JS模块):向外提供特定功能的js程序, 一般就是一个js文件,复用js, 简化js的编写, 提高js运行效率。
组件(大于模块,因为其包含JS,还有html、css、video等等):用来实现局部功能效果的代码和资源的集合(html/css/js/image等等),复用编码, 简化项目编码, 提高运行效率
模块化的:当应用的js都以模块来编写的, 这个应用就是一个模块化的应用
组件化的:当应用是以多组件的方式实现, 这个应用就是一个组件化的应用
2、面向组件编程(函数式、类式)
函数式组件:
<script type="text/babel"> //函数式组件 function Demo(){ console.log(this); //此处this为undefined,因为babel属于严格模式 return <h2>函数式组件</h2> } //渲染组件到页面,组件首字母大写,标签闭合 ReactDOM.render(<Demo/>,document.getElementById('test')) /* 1、react解析组件标签,发现是函数定义 2、随后执行函数返回VDom 3、再解析为真实Dom,插入页面 */ </script>
类式组件:
<script type="text/babel"> //类式组件 class MyComponent extends React.Component{ //这里的render是放在了原型上,供实例使用 //render中的this是MyComponent组件实例对象,包括React和MyComponent的内容 render(){ return <h2>类式组件</h2> } } //渲染组件 ReactDOM.render(<MyComponent/>,document.getElementById('test')) /* 1、react解析组件标签,发现是类定义 2、随后react内部new创建实例,调用原型render得到VDom 3、再解析为真实Dom,插入页面 */
</script>
3、组件实例的三大核心属性(这里我们研究class,因为最初是只有class才有状态,才有this,才是复杂组件,才有这三大核心属性。新版react给函数式定义了hooks,暂且按下不表):
1、state(因为学过vue,且都是数据驱动,这里完全可以看做是vue中的data,其也不能直接修改,需要用setState):
值是对象(可以包含多个key-value的组合),通过更新组件的state来更新对应的页面显示(重新渲染组件)
state的使用要注意一个点就是this的指向问题:函数的this,使用bind或者箭头函数解决
学习版的react,根据顺序来捋清逻辑:
<script type="text/babel"> //1、创建class组件 class Weather extends React.Component{ constructor(props){ //1.重写构造器 super(props) //2.给实例对象上的state赋值,初始化状态。state其实在React上 this.state = {isHot:true,wind:'大风'} //7.为了改变changeHot的this指向,将原型上的changeHot放到构造函数本身,这是一种解决方法 //this.changeHot = this.changeHot.bind(this) } render(){ //3.解构,读取状态 const {isHot,wind} = this.state //4.尽量以原生事件来写,{}绑定事件,不要立即执行 return <h1 onClick={this.changeHot}> 今天{isHot?'炎热':'凉爽'},而且很{wind}</h1> } //5.方法都写到组件内,清晰 //8.还可以使用赋值+箭头函数,箭头函数的this为上下文 changeHot=()=>{ //6.这里的this是undefined,render的this指向实例是因为2渲染的时候调用了他 //6.这里的changeHot没有this,因为一开始的时候onClick并没有执行,只是赋值,所以其应该指向window,但是class默认开启严格模式,所以为undefined //9.状态数据state,不能直接修改或更新 //this.state.isHot = !this.state.isHot //10.使用setState,合并修改对象 let isHot = this.state.isHot this.setState({isHot:!isHot}) } } //2、渲染 ReactDOM.render(<Weather/>,document.getElementById('test')) </script>
稍微简化:
<script type="text/babel"> class Weather extends React.Component{ //初始化原型的state state = {isHot:true,wind:'大风'} render(){ const {isHot,wind} = this.state return <h1 onClick={this.changeHot}> 今天{isHot?'炎热':'凉爽'},而且很{wind}</h1> } //自定义函数,赋值语句+箭头函数 changeHot=()=>{ let isHot = this.state.isHot this.setState({isHot:!isHot}) } } ReactDOM.render(<Weather/>,document.getElementById('test')) </script>
2、props(也和Vue的props传输类似,外部传进组件内部的值)
每个组件对象都会有props(properties的简写)属性,组件标签的所有属性都保存在props中
组件内部不要修改props数据
<script type="text/babel"> class Person extends React.Component{ render(){ const {name,gender,age} = this.props return ( <ul> <li>姓名:{name}</li> <li>性别:{gender}</li> <li>年龄:{age}</li> </ul> ) } } ReactDOM.render(<Person name='tom' gender='男' age='18'/>,document.getElementById("test1")) ReactDOM.render(<Person name='jacky' gender='男' age='18'/>,document.getElementById("test2")) </script>
1、展开运算符...:一次传递多个属性,但是要注意传进去的属性名要和组件内的对齐。
这里的展开运算符需要注意:展开运算符可以展开数组,但是不能直接展开一个对象,ES6中使用{...p}是表示深拷贝一个对象。但是这里的花括号没有意义,只是单纯的做一个分隔符,花括号内部是在执行JS,react和babel解析了...,所以可以使用展开运算符展开对象
ReactDOM.render(<Person name='tom' gender='男' age='18'/>,document.getElementById("test1")) const p = {"name":'tom' ,"gender":'男' ,"age":'18'} ReactDOM.render(<Person {...p}/>,document.getElementById("test2"))
2、props类型限制:propTypes。其实有点像是TS了,对属性进行各种限制
3、props默认值:defaultProps。也可以把下面的属性直接放进Person类里面
Person.propTypes={ //P小写是因为react的限制,内部需要遍历到这个属性,才可以有限制 name: PropTypes.string.isRequired, //name必须有,是字符。这里的P大写是因为调用了库 gender: PropTypes.string, //gender可以没有,是字符 speak: PropTypes.func //speak限制为方法,为了不和关键字冲突,库默认方法属性叫func } Person.defaultProps={ gender: '男', age: 16 }
//直接写类里,创建属于类的属性或方法需要使用静态,可以不通过实例进行访问。这里要重点关注实例属性和类属性的区别 class Person extends React.Component{ static propTypes={ name: PropTypes.string.isRequired, //name必须有,是字符 gender: PropTypes.string, //gender可以没有,是字符 speak: PropTypes.func //speak限制为方法,为了不和关键字冲突,库默认方法属性叫func } static defaultProps={ gender: '男', age: 16 } }
4、props是只读的,不可修改(和vue也一样了,防污染,不应该修改外面传进来的值)
5、构造器:可以不写,因为要写的时候所考虑的情况我们都处理掉了。
官网描述:通常,在 React 中,构造函数仅用于以下两种情况:
1、通过给this.state赋值对象来初始化内部state。我们外部赋值了state
2、为事件处理函数绑定实例。我们通过箭头函数解决了
constructor(props){ super(props) this.state = {isHot:true,wind:'大风'} //已经外部赋值 this.changeHot = this.changeHot.bind(this) //箭头函数已解决this问题 }
6、函数式组件的props:一般来说函数因为在严格模式下没有所谓的this,所以就不会有state和refs,但因为可以传参,所以可以使用props(后面降到hooks可以用),其限制只能像上述的写在函数外,而不能写在函数内部。
function Person(props){ return ( <ul> <li>姓名:{props.name}</li> <li>性别:{props.gender}</li> <li>年龄:{+props.age+1}</li> </ul> ) } ReactDOM.render(<Person name='tom' gender='男' age='18'/>,document.getElementById("test1"))
3、refs:组件内的标签可以定义ref属性来标识自己
1、基本形式:但是这个不推荐
- 当 ref 定义为string 时,需要React实时追踪当前正在渲染的组件,在 reconciliation 阶段, React Element 创建和更新的过程中, ref 会被包装为一个闭包函数, 等待 commit 阶段被执行,这会对React 的性能产生一些影响。
- 当使用 render callback 模式的时候,使用 string ref 会造成 ref 挂载位置产生歧义。
- string ref 无法被组合,例如一个第三方库的父组件已经给子组件传递了 ref,那么我们就无法再在子组件上添加 ref 了,而 callback ref 可完美解决此问题。
class Person extends React.Component{ showData=()=>{ // const input1 = document.getElementById('right') alert(this.refs.left.value) } render(){ return ( <div> <input type="text" ref='left'/> <button onClick={this.showData}>点击弹框</button> </div> ) } } ReactDOM.render(<Person/>,document.getElementById("test1"))
2、回调函数形式:如果使用内联函数,组件更新的时候其回调函数会执行两次。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题。但是执行两次也并不影响
class Person extends React.Component{ state = {isHot:true} // 类绑定,更新时只会执行一次 changeState=(c)=>{ const {isHot} = this.state this.setState({isHot:!isHot}) console.log(c); } showData=()=>{ alert(this.left.value) } render(){ const {isHot} = this.state return ( <div> {/*内联形式的回调在更新时会执行两次*/} {/*<p onClick={this.changeState} ref={(c)=>c}>今天很{isHot?'热':'凉爽'}</p>*/} <p onClick={this.changeState} ref={this.changeState}>今天很{isHot?'热':'凉爽'}</p> <input type="text" ref={c=>this.left = c}/> <button onClick={this.showData}>点击弹框</button> </div> ) } }
3、creatRef:React.creatRef()调用后会返回一个储存被ref标识的节点的容器。但该函数只能包含一个节点,如果有多个节点需要使用ref,则要创建多个
class Person extends React.Component{ myRef = React.createRef() myRef2 = React.createRef() showData=()=>{ alert(this.myRef.current.value) } render(){ return ( <div> <input type="text" ref={this.myRef}/> <button ref={this.myRef2} onClick={this.showData}>点击弹框</button> </div> ) } }
4、ref中的事件处理
1. 通过onXxx属性指定事件处理函数(注意大小写)
1) React使用的是自定义(合成)事件, 而不是使用的原生DOM事件——兼容性
2) React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)——性能
2. 通过event.target得到发生事件的DOM元素对象——不要过渡使用ref,发生事件的节点正好是要操作的节点的时候可以用event.target拿到
class Person extends React.Component{ showData=(event)=>{ alert(event.target.value) } render(){ return ( <div> <input type="text" onBlur={this.showData}/> </div> ) } }
4、表单数据:表单提交会默认触发页面跳转。Vue的双向绑定,React没有实现,所以可以手动用onChange实现
非受控组件:现用现取,等值输入完了再去拿。只有输入完提交了才能校验
class Login extends React.Component{ collect = (e)=>{ e.preventDefault() const {user, psw} = this alert(`账号为:${user.value};密码为:${psw.value}`) } render(){ return ( <form action="" onSubmit={this.collect}> 用户名:<input ref={c=>this.user=c} type="text"/> <br/> 密码:<input ref={c=>this.psw=c} type="password"/> <br/> <button>登录</button> </form> ) } }
受控组件:随用随取,state随时维护输入的值,可随时进行数据校验。少用ref
class Login extends React.Component{ state ={name:'',psw:''} dataName=(e)=>{ this.setState({name:e.target.value}) } dataPsw=(e)=>{ this.setState({psw:e.target.value}) } collect = (e)=>{ e.preventDefault() const {name, psw} = this.state alert(`账号为:${name};密码为:${psw}`) } render(){ return ( <form action="" onSubmit={this.collect}> 用户名:<input onChange={this.dataName} type="text"/> <br/> 密码:<input onChange={this.dataPsw} type="password"/> <br/> <button>登录</button> </form> ) } }
5、高阶函数:如果参数或者返回值是函数,那就是高阶函数,比如闭包、Promise、setTimeout、数组的操作等
class Login extends React.Component{ state ={name:'',psw:''} data=(dataType)=>{ //因为回调函数加了括号会立即执行,会没有返回值。这里使用闭包,将函数作为返回值
//e是onChange给的,所以要在返回的函数里传进来 return (e)=>{ //这里的dataType在赋值对象的时候,不能直接引用,对象会默认解析为字符串dataType //逻辑就是a=name,对象的Obj.a和Obj[a]的区别 this.setState({[dataType]:e.target.value}) } } collect = (e)=>{ e.preventDefault() const {name, psw} = this.state alert(`账号为:${name};密码为:${psw}`) } render(){ return ( <form action="" onSubmit={this.collect}> 用户名:<input onChange={this.data('name')} type="text"/> <br/> 密码:<input onChange={this.data('psw')} type="password"/> <br/> <button>登录</button> </form> ) } }
函数柯里化:多态。根据参数,处理不同参数
纯函数:1. 一类特别的函数: 只要是同样的输入(实参),必定得到同样的输出(返回)
2. 必须遵守以下一些约束
1) 不得改写参数数据
2) 不会产生任何副作用,不能做例如网络请求(失败),输入和输出设备(丢失)
3) 不能调用Date.now()或者Math.random()等不纯,会改变的方法
3. redux的reducer函数必须是一个纯函数
6、组件的生命周期
1、 组件从创建到死亡它会经历一些特定的阶段。React组件中包含一系列勾子函数(生命周期回调函数), 会在特定的时刻调用。我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。
2、旧生命周期
- 初始化阶段: 由ReactDOM.render()触发---初次渲染:1. constructor();2. componentWillMount();3. render();4. componentDidMount()
-
更新阶段:由组件内部this.setSate()或父组件重新render触发:1. shouldComponentUpdate();2. componentWillUpdate();3. render();4. componentDidUpdate();
- 卸载阶段:由ReactDOM.unmountComponentAtNode()触发:componentWillUnmount()
3、新生命周期
- 初始化阶段: 由ReactDOM.render()触发---初次渲染:1. constructor();2. getDerivedStateFromProps();3. render();4. componentDidMount()
-
更新阶段:由组件内部this.setSate()或父组件重新render触发:1. getDerivedStateFromProps(); 2. shouldComponentUpdate();3. render();4. getSnapshotBeforeUpdate(); 5. componentDidUpdate();
- 卸载阶段:由ReactDOM.unmountComponentAtNode()触发:componentWillUnmount()
新React考虑使用异步渲染,为了防止滥用误用出现未知bug,所以有三个旧钩子可能被删除:componentWillMount();componentWillUpdate();componentWillReceiveProps();
添加了两个新钩子:getDerivedStateFromProps(); getSnapshotBeforeUpdate();
1、getDerivedStateFromProps(props,state); 静态方法,参数接收props/state,控制state的渲染,state的状态完全取决于所返回的是props还是state。如果需要state的值一直等于props的值,就去用。一般不会去控制。
static getDerivedStateFromProps(props,state){ console.log('getDerivedStateFromProps'); // return null // return props return state }
2、getSnapshotBeforeUpdate();在componentDidUpdate的时候会传入三个参数,preProps、preState和snapshot。一般用来捕获在渲染前的一些信息,因为再往下就渲染完成,获取不到之前的一些数值了。比如滚动,一般也不用.
getSnapshotBeforeUpdate(prevProps, prevState) { // 我们是否在 list 中添加新的 items ? // 捕获滚动位置以便我们稍后调整滚动位置。 if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; return list.scrollHeight - list.scrollTop; } return null; } componentDidUpdate(prevProps, prevState, snapshot) { // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items, // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。 //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值) if (snapshot !== null) { const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; } }
4、主要考虑render、componentDidMount、componentWillUnmount三个钩子
7、react项目目录:
public ---- 静态资源文件夹
- favicon.icon ------ 网站页签图标
- index.html -------- 主页面
- logo192.png ------- logo图
- logo512.png ------- logo图
- manifest.json ----- 应用加壳的配置文件
- robots.txt -------- 爬虫协议文件
src ---- 源码文件夹
- App.css -------- App组件的样式
- App.js --------- App组件
- App.test.js ---- 用于给App做测试
- index.css ------ 样式
- index.js ------- 入口文件
- logo.svg ------- logo图
- reportWebVitals.js--- 页面性能分析文件(需要web-vitals库的支持)
- setupTests.js---- 组件单元测试的文件(需要jest-dom库的支持)
8、样式要注意,因为其不像Vue有Scope,所以要注意样式的模块化:1、最简单的其实还是class不同名;2、使用模块化引入样式index.module.cs,一般不用;3、less;4、style-loader css-loader等等
9、组件开发流程:
- 拆分组件: 拆分界面,抽取组件
- 实现静态组件: 使用组件实现静态页面效果
- 实现动态组件
- 动态显示初始化数据
- 数据类型
- 数据名称
- 保存在哪个组件?
- 交互(从绑定事件监听开始)
- 动态显示初始化数据
10、组件信息传递:
首先要从架构上考虑状态应该放在哪个组件最合适。操作状态的函数也将会放在该组件内(状态提升)。React是单向数据流
父传子:props
子传父:也可以用props,父组件给子组件传递函数,子组件执行父组件的函数,通过传参把新的props传回给父组件操作
兄弟互传:子传父传子
任意组件之间:Context,消息订阅与发布机制PubSubJS:一个组件发布消息与参数('topic', data),另一个组件订阅某一消息并响应执行回调获取参数('topic', responFn)
// 订阅者收到消息后的回调函数 var mySubscriber = function (msg, data) { console.log( msg, data ); }; // 订阅者,可以把它挂载在某一个钩子上,一般可以放在componentDidMount var token = PubSub.subscribe('MY TOPIC', mySubscriber); // 发布者 PubSub.publish('MY TOPIC', 'hello world!');
//取消订阅
PubSub.unsubscribe(token) //异步发布 PubSub.publishSync('MY TOPIC', 'hello world!');
11、React路由
1、单页面SPA:整个应用只有一个完整的页面。点击页面中的链接不会刷新页面,只会做页面的局部更新。数据都需要通过ajax请求获取, 并在前端异步展现。
2、路由:一个路由就是一个映射关系(key:value)。其基础就是栈结构
具体规则:1、hash,Hash 方法是在路由中带有一个#锚点,通过监听 # 后的哈希值 URL 路径标识符的更改而触发的浏览器 hashchange 事件,然后通过获取 location.hash 得到当前的路径标识符,再进行一些路由跳转的操作,其不会页面跳转。localhost -> localhost/#/test1 ->l ocalhost/#/test2,只监听#后的变化。不美观
2、history API,纯history操作路径通过push、replace将路径入栈出栈,go、goBack和goForward实现路径的浏览。history 提供了pushState(新增)和 replaceState(替换) 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新。但是是H5的,兼容性没hash好
前端路由的核心就是以上两点,其他的功能类似动态路由、路由参数、路由动画,都是添加上的功能。
3、常用组件:
1. <BrowserRouter> history Api,一般来说整个应用是用一个路由器管理,所以该标签一般定义在index.js上。其刷新网页,维护history导致state不会消失
2. <HashRouter> hash,同上。HashRouter刷新后,因为其底层原理并不是使用history,所以会导致路由state参数的丢失
3. <Route> path对应路由,component对应路由组件。<Route path="/about" component={About} />(Router6中<Route>可配合useRoutes()配置路由表)
4. <Redirect> 重定向,类似自动跳转,开屏默认跳转的路由,登录成功后自动跳转之前的页面等。原理就是当其他都没匹配上,就再去找Redirect。(Router6改成<Navigate to=' ' replace={false}/>)
5. <Link> 类似<a></a>,to到指定路由。<Link to="/home">Home</Link>
6. <NavLink> 如果点击,即追加类名activeClassName,动态激活。和Vue的RouterLink类似,依据当前路由路径和to判断是否一致来添加class。(Router6需要className的值改成函数,点击都会调用,根据参数isActive,更改类名。home子组件匹配成功,默认home高亮;若添加end属性,home不高亮)
7. <Switch>路由是按定义顺序匹配,为了提高路由匹配效率,只要匹配到,立马结束,不往下匹配。单一匹配,一般一个路由一个组件。(Router6改成<Routes/>,且必须)
4、一般组件:引用渲染<Header/>这类,props默认为空
路由组件:路由渲染<Route/>这类,props默认传和路由相关的东西,比如location、history和match等,类似Vue的route。这一些其实基本就是原生的BOM内容了。
以下展示的是主要的信息,有些未展示:
location:当前路由信息。hash/pathname/search/state
history:管理历史路由,控制路由前进后退等。go/goBack/goForward/push/replace
match:匹配模式,参数。isExact/params/path/url
5、封装路由组件:其实也是当一个一般组件rcc/rfc编写,因为是用标签引入的。import路由组件的时候,根据需要,看是引入Link还是NavLink。注意标签体的传递,也是在props里,属于children,可以直接写入标签内
//封装组件 import React, { Component } from 'react' import { NavLink, } from "react-router-dom"; export default class myNavLink extends Component { render() { return ( <NavLink className="list-group-item" {...this.props}></NavLink> ) } } //引用 <MyNavLink to='/home' show='Home'>Home</MyNavLink> <MyNavLink to='/about' show='About'>About</MyNavLink>
6、样式路径问题:如果在路由路径中使用了多级路由,在index.html引入样式时,刷新重定向后,再次请求的css文件路径可能会出现问题。可以处理一下引用路径。比如引入css时,./换成/;或者用%PUBLIC_URL%路径,这个在react用得比较多,且只能在react有效;也可以用锚点从而请求路径不带#号后的内容
7、路径匹配:精准匹配:路由开启exact。路由和link一一对应。一般不要开启,不然会无法匹配二级路由
//路由 <Route exact={true} path="/about/a" component={About} /> //router6: element={<About/>} //Link <MyNavLink to="/about/a" show="About"/>
模糊匹配:默认模式,路由去匹配Link,按照Link的顺序去匹配
//路由 <Route path="/about" component={About} /> //Link <MyNavLink to="/about/a" show="About"/> //错误 <MyNavLink to="/a/about" show="About"/>
8、嵌套路由(多级路由)Vue的子路由:多级下的子组件的Link,需要包含父级的Link。先走的父级的Link,再走多级的Link。否则会匹配不到导致执行父级上的重定向回About。
同理,Link加了路径,Route也需要。这个不太像Vue一样专门一个route文件去编辑
//Route <Route path="/home/news" component={News}/> <Route path="/home/message" component={Message}/> <Redirect to="/home/news"/> //Link <MyNavLink to="/home/news">News</MyNavLink> <MyNavLink to="/home/message">Message</MyNavLink>
9、路由传参:
1、params,动态路由。路由用/:id类型匹配,Link用模板字符串传。子组件用props.match.params接收
// 父组件 <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> <Route path="/home/message/detail/:id/:title" component={Detail}/> // 子组件 const {id,title} = this.props.match.params
//所拿到的对象为{id:msgObj.id的值, title: msgObj.title的值}
2、search,search参数通过?判断,路由并不需要匹配。获取到的search是urlencoded编码字符串,需要借助querystring解析,slice去掉?号
//父组件 <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> <Route path="/home/message/detail" component={Detail}/> //子组件 const {search} = this.props.location const {id,title} = qs.parse(search.slice(1))
3、state,地址栏不会显示参数。因为link上要加state,而state不是显示在url上,所以这里的to是写成一个对象的形式。history维护location,即使页面刷新,url不刷新,参数也不会丢失,导致刷新其也不会跳转查到初始页面
//父组件 <Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link> <Route path="/home/message/detail" component={Detail}/> //子组件 const {id,title} = this.props.location.state || {}
10、路由跳转(编程式路由导航):默认push模式,使用props.history.下的各类操作函数,push()、replace()、go()等
//比如这个例子里,各个消息都是同级的,就没必要返回的时候返回上一个消息,可以直接返回到home下,而不是3,2,1 <Link replace to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
11、withRouter。一般组件是没有路由组件的一些功能的,比如history,所以按道理是不能直接操控前进后退等。在一般组件内引入withRouter,将一般组件包装成路由组件,内部按照路由组件,调用this.props.history.push()等方法即可。
import {withRouter} from 'react-router-dom' class Header extends Component{ ... } // 返回一个新组件 export default withRouter(Header)
12、redux。第三方出品
redux是一个专门用于做状态管理的JS库,可以用在react、angular、vue下。现在还有很多别的,本质上都是发布订阅模式
- 没有状态管理工具:直接用 props 或者 context
- 单项数据流:
redux
、zustand
- 双向绑定:
mobx
、valtio
- 状态原子化:
jotai
、recoil
- 有限状态机:
xstate
何时要用到状态管理工具?1. 某个组件的状态,需要让其他组件可以随时拿到(共享)。2. 一个组件需要改变另一个组件的状态(通信)。
1、原理
2、三大核心(Vue四大或者说五大,状态管理都类似)
1、action:
1、动作对象。同步(Vue的mutation的定义)
type:标识属性, 值为字符串, 唯一, 必要属性。操作
data:数据属性, 值类型任意, 可选属性。数据
如:{ type: 'ADD_STUDENT',data:{name: 'tom',age:18} }
export const createIncrementAction = data => ({type:INCREMENT,data}) //箭头函数直接返回一个对象需要用括号包着,否则会和函数体的花括号冲突
2、函数。异步(Vue的action)
//这里常规的会写成store.dispatch调用分发,但是在store拦截action的时候,会查看action,其里面要用一般对象,而不能用函数,所以
action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的。//action内
export const createIncrementAsyncAction = (data,time) => {
//这里store调用的dispatch,所以也会默认的传一个dispatch过来。当然,在action再引用store,再store.dispacth也可以 return (dispatch)=>{ setTimeout(()=>{ //这里通常会理解写成store.dispatch...,但是在store拦截action的时候其检查内部不是对象(这里我们写成了函数)会报错 //所以我们使用中间件去封装dispatch:redux-thunk,有了这个才能异步 dispatch(createIncrementAction(data)) },time) } }
//store内引入redux-thunk
2、reducer(Vue的mutation):初始化状态(undefined,初始化),加工状态(旧state,动作)。其是根据旧的state和action, 产生新的state的纯函数,preState不能改变。
const initState = 0 //初始化状态 export default function countReducer(preState=initState,action){ //从action对象中获取:type、data const {type,data} = action //根据type决定如何加工数据 switch (type) { case INCREMENT: //如果是加
//这里要注意,如果preState是一个对象,比如数组,若直接返回preState则不生效,因为redux会做对比,不同才return新值。
//所以可一个解构返回新数组 return preState + data case DECREMENT: //若果是减 return preState - data default: return preState } }
3、store(Vue的state):将state、action、reducer联系在一起的对象
创建store:
1) import {createStore} from 'redux'
2) import reducer from './reducers' --> 纯操作函数
3) const store = createStore(reducer)
使用store的步骤:
1) store.getState(): 得到state
2) store.dispatch(action): 分发action, 触发reducer调用, 产生新的state
3) store.subscribe(listener): 注册监听, 当产生了新的state时, 自动调用,执行代码,如render更新视图:我们不会去直接this.render()去调用渲染,而是会setState({}),只要调用setState,都会去执行render。改方法可以写在总的index里,只要写一次就行,整体diff
import store from './redux/store' store.subscribe(()=>{ ReactDOM.render(<App/>,document.getElementById('root')) })
tips:可以把action的一些操作名称统一通constant.js去维护,防止写错。这不单只是在redux里,算是编程的一个知识点:
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
13、react-redux。官方
概念:
UI:
- 只负责 UI 的呈现,不带有任何业务逻辑
-
通过props接收数据(一般数据和函数)
-
不使用任何 Redux 的 API
-
一般保存在components文件夹下
容器(一般就是react-redux生成):
-
负责管理数据和业务逻辑,不负责UI的呈现
-
使用 Redux 的 API
- 可以自动检测store变化,并自动render,不需要subscribe再render
-
一般保存在containers文件夹下,也可以把UI和容器写一起
1、连接:
//引入connect用于连接UI组件与redux import {connect} from 'react-redux'
mapStateToprops:将外部的数据(即state对象)转换为UI组件的标签属性,传状态
mapDispatchToProps:将分发action的函数转换为UI组件的标签属性,传方法
简写,把mapDispatch函数简写成对象:
connect( state => ({key:value}), //映射状态,数据 {key:xxxxxAction} //映射操作状态的方法,操作。react-redux能dispatch自动分发,不用再写成函数的形式 )(UI组件)
2、store的定义:而且因为容器包裹UI,store之和容器打交道,所以store只要在index.js里,在大封装APP,也就是app.jsx的上一层使用即可。使用Provider
import store from './redux/store' import {Provider} from 'react-redux' ReactDOM.render( <Provider store={store}> <App/> </Provider>, document.getElementById('root') )
如果要给个别的容器使用,也可以在app.jsx里使用
import React, { Component } from 'react' import Count from './containers/Count' import store from './redux/store' export default class App extends Component { render() { return ( <div> {/* 给容器组件传递store */} <Count1 store={store} /> <Count2 /> <Count3 store={store} /> </div> ) } }
3、数据共享:store共享、acitons文件共用、reducers文件公用(combineReducers合并生成对象,否则只会执行一个reducer,只能保存一种数据结构)
//汇总所有的reducer变为一个总的reducer const allReducer = combineReducers({ he:countReducer, rens:personReducer }) //暴露store, 现阶段createStore已经弃用,现用legacy_createStore export default createStore(allReducer,applyMiddleware(thunk))
4、使用:定义好store,创建action。store内配置相应的 data:actionindex.js引入store。components内使用useSelector引入data、useDispatch调用action。
14、补充
1、
(1). setState(stateChange, [callback])------对象式的setState
1.stateChange为状态改变对象(该对象可以体现出状态的更改),就是setState({count: count+1})
2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用。nextTick
this.setState({ count: count+1 },()=>{ console.log(count); });
(2). setState(updater, [callback])------函数式的setState
1.updater为返回stateChange对象的函数。
2.updater可以接收到state和props。
3.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。
this.setState((state, props) => ({ count: state.count + props }));
总结:
1.对象式的setState是函数式的setState的简写方式(语法糖)
2.使用原则:
(1).如果新状态不依赖于原状态 ===> 使用对象方式
(2).如果新状态依赖于原状态 ===> 使用函数方式,当然,对象式其实也可以。{count: this.state.count+1}
(3).如果需要在setState()执行后获取最新的状态数据,要在第二个callback函数中读取
2、路由组件lazyload:
//1.通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包 const Login = lazy(()=>import('@/pages/Login')) //2.通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面 <Suspense fallback={<h1>loading.....</h1>}> <Switch> <Route path="/xxx" component={Xxxx}/> <Redirect to="/login"/> </Switch> </Suspense>
3、Hooks:可以在函数组件中使用 state 以及其他的 React 特性。函数式组件没有this
(1). State Hook: React.useState()
(2). Effect Hook: React.useEffect()
(3). Ref Hook: React.useRef()
1、
(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作
(2). 语法: const [xxx, setXxx] = React.useState(initValue)
(3). useState()说明:
参数: 第一次初始化指定的值在内部作缓存
返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
(4). setXxx()2种写法:
setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值
export default function Index(props) { const [count, setCount] = React.useState(0); const [person] = React.useState({ name: "tom", age: 18 }); function add() { // setCount(count=>count+1) setCount((count) => count + 1); } return ( <div> <h2>当前求和:{count}</h2> <h2>当前姓名:{person.name}</h2> <h2>当前年龄:{person.age}</h2> <button onClick={add}>+1</button> </div> ); }
2、Effect Hook
(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用、使用生命周期操作:
发ajax请求数据获取
设置订阅 / 启动定时器
手动更改真实DOM
(3). 语法和说明:
useEffect(() => {
// 在此可以执行任何带副作用操作
return () => { // 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等
}
}, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行。如果不传,则监测所有state;如果传空数组,则谁也不监测
//effect hook 相当于三个生命钩子 React.useEffect(() => { //DidMount let timer = setTimeout(() => { setCount((count) => count + 1); }, 1000); //WillUnmount return ()=>{ clearInterval(timer) } //只检测count,DidUpdate }, [count]);
(4). 可以把 useEffect Hook 看做如下三个函数的组合
componentDidMount()
componentDidUpdate()
componentWillUnmount()
3、
(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据
(2). 语法: const refContainer = useRef()
(3). 作用:保存标签对象,功能与React.createRef()一样
4、新版Reactrouter新增多个hook
useRoutes()配置路由表。类似Vue的路由表,嵌套路由也是和vue一个套路;配合<Outlet/>指定路由组件在别的组件内的位置
useParams()获取路由参数。由于函数式组件没有this,拿不到history里的参数,所以要使用hook,返回qs分割好的对象
useMatch()获取路由match参数,需要传参路径
useSearchParams()获取路由Search参数,返回数组,第一个是参数,第二个是setSearch
useLocation()获取location参数
useNavigate()使用history,因为函数式没有this,拿不到history,有时候需要在函数内跳转页面,需要用到history,可以replace,传参等
useInRouterContext()判断是否在路由上下文环境内,返回判断
useNavigationType()返回当前页面导航类型:pop刷新页面、push、replace
useOutlet()用来呈现当前组件中渲染的嵌套路由,也就是当前组件内的子路由,该子路由需要挂载才能返回
useResolvedPath()解析路径,传入路径,类似qs
4、<Fragment><Fragment>。相当于Vue的template
5、Context。一种组件间通信方式, 常用于【祖孙组件】与【后代组件】间通信。一般应用开发不用,开发插件用。有些类似Vue的 provide 和 inject
1) 创建Context容器对象:写在所有组建的外侧,类似redux共享
const XxxContext = React.createContext() 首字母大写
2) 父组件渲染子组时,在子外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<xxxContext.Provider value={数据}> 子组件 </xxxContext.Provider>
3) 后代组件读取数据:
//第一种方式:仅适用于类组件 static contextType = xxxContext // 先声明接收context this.context // 然后,读取context中的value数据 //第二种方式: 函数组件与类组件都可以 <xxxContext.Consumer> {value => ( // 函数接收参数value,就是context中的value数据要显示的内容)} </xxxContext.Consumer>
6、PureComponent
其实主要是优化shouldComponentUpdate。当父组件更新时,控制子组件的更新,比如子组件没用到父组件的数据时,子组件不应该更新。
办法1:重写shouldComponentUpdate()方法比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false
shouldComponentUpdate(nextProps,nextState){ // console.log(this.props,this.state); //目前的props和state // console.log(nextProps,nextState); //接下要变化的目标props,目标state return !this.props.carName === nextProps.carName //一般来说子组件的判断用props,因为是获取的props;父组件判断是state }
办法2:使用PureComponent,PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
class Child extends PureComponent{}
注意:
只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false。可以说不能监听Object改变。为了避免,就应该去返回新对象,彻底改变,而不是在原有的基础上修改,返回修改后的老对象。
不要直接修改state数据, 而是要产生新数据
项目中一般使用PureComponent来优化,shouldComponentUpdate需要手动指定
7、render props(Vue的插槽)向组件内动态传入带内容的标签。就是第三方组件插入插槽
//传统嵌套,children props <A> <B>xxxx</B> </A> {this.props.children} 问题: 如果B组件需要A组件内的数据, ==> 做不到
//render props,通过render函数传递 //slot class A extends Component { state = {name:'tom'} render() { console.log(this.props); const {name} = this.state return ( <div className="a"> <h3>我是A组件</h3> {this.props.render(name)} //slot插槽预留的位置 </div> ) } } //引用 <A render={(name)=><B name={name}/>}/> //要插入的B class B extends Component { render() {return ( <div className="b"> <h3>我是B组件,{this.props.name}</h3> </div> ) } }
8、错误边界:用来捕获后代组件错误,渲染出备用页面,防止整个页面崩溃。类似自定义局域的404页面
只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用:getDerivedStateFromError配合componentDidCatch。不过一般用在生产环境 ,因为显示的时间较短。
// 生命周期函数,一旦后台组件报错,就会触发 static getDerivedStateFromError(error) { console.log(error); // 在render之前触发 // 返回一个新的state状态,将之前定义的state修改。state修改就可以渲染自定义的404或别的错误页面 return { hasError: true, }; } componentDidCatch(error, info) { // 统计页面的错误。发送请求发送到后台去 console.log(error, info); }
9、组件间通信
组件间的关系:1、父子组件;2、兄弟组件(非嵌套组件)3、祖孙组件(跨级组件)
几种通信方式:
1.props:
(1).children props
(2).render props
2.消息订阅-发布:
pubs-sub、event等等
3.集中式管理:
redux、dva等等
4.conText:
生产者-消费者模式,provider
常见搭配:
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)
官网和其他文档的一些补充:
1、React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。
2、Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。JSX是其语法糖
// 注意:这是简化过的结构 const element = { type: 'h1', props: { className: 'greeting', children: 'Hello, world!' } };
这个称为react元素。虚拟dom
3、State 的更新可能是异步的
因为 this.props
和 this.state
可能会异步更新,所以不要依赖他们的值来更新下一个状态。要解决这个问题,可以让 setState()
接收一个函数而不是一个对象。
// Wrong this.setState({ counter: this.state.counter + this.props.increment, }); // Correct this.setState((state, props) => ({ counter: state.counter + props.increment }));
4、Ref 转发是一项将ref自动地通过组件传递到其一子组件的技巧。一般也不会用
//子组件
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children} </button> )); // 你可以直接获取 DOM button 的 ref:指定ref为jsx的props const ref = React.createRef(); <FancyButton ref={ref}>Click me!</FancyButton>;
第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
5、Fragments。React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。其实类似vue的template生成代码片段,渲染的时候标签会自动消失
//父组件 class Table extends React.Component { render() { return ( <table> <tr> <Columns /> </tr> </table> ); } } //子组件 class Columns extends React.Component { render() { return ( <React.Fragment key等props属性可以写> <td>Hello</td> <td>World</td> </React.Fragment> ); } } //唯一根,包裹div会不生效 class Columns extends React.Component { render() { return ( <div> <td>Hello</td> <td>World</td> </div> ); } }
6、高阶组件。高阶组件是参数为组件,返回值为新组件的函数。比如withRouter封装的组件
const NavbarWithRouter = withRouter(Navbar); // React Redux 的 `connect` 函数 const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
不要在render中使用HOC,不单只是性能问题,还有可能造成数据丢失
render() { // 每次调用 render 函数都会创建一个新的 EnhancedComponent // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作! return <EnhancedComponent />; }
7、可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,例如,如果 MyComponents.DatePicker
是一个组件,可以在 JSX 中直接使用:
const MyComponents = { DatePicker: function DatePicker(props) { return <div>Imagine a {props.color} datepicker here.</div>; } } function BlueDatePicker() { return <MyComponents.DatePicker color="blue" />; }
8、由于react的diff并不是很精细,父组件一旦更新,其下子组件也将更新。为了减少性能损失的影响,可以使用shouldComponentUpdate去控制子组件的更新。现阶段一般使用PureComponent
9、Protals。
ReactDOM.createPortal(child, container)
一个 portal 的典型用例是当父组件有 overflow: hidden
或 z-index
样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。使用时要注意事件冒泡等的设置
10、Hook使用规则:
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。react是根据hook的调用顺序来判断哪个state对应哪个setState的,如果放在函数内,比如if,很有可能导致hook丢失,从而导致对应出错。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)
11、自定义Hook:其实可以理解为公用的方法,接收参数、返回值,内部可以调用别的hook来完成逻辑
// 自定义
import React, { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; }
// 调用 const isOnline = useFriendStatus(props.friend.id);
组件被调用其完全独立,复用状态逻辑,即使是同时调用同一个自定义hook的组件,其state也不会相同。自定义hook必须以use开头
12、React采用Object.is(value1, value2)来判断是否为同一个值
Object.is()
方法判断两个值是否为同一个值,如果满足以下任意条件则两个值相等:
- 都是
undefined
- 都是
null
- 都是
true
或都是false
- 都是相同长度、相同字符、按相同顺序排列的字符串
- 都是相同对象(意味着都是同一个对象的值引用)
- 都是数字且
- 都是
+0
- 都是
-0
- 都是
NaN
- 都是同一个值,非零且都不是
NaN
- 都是
Object.is()
与 ==
不同。==
运算符在判断相等前对两边的变量(如果它们不是同一类型)进行强制转换(这种行为将 "" == false
判断为 true
),而 Object.is
不会强制转换两边的值。
Object.is()
与 ===
也不相同。差别是它们对待有符号的零和 NaN 不同,例如,===
运算符(也包括 ==
运算符)将数字 -0
和 +0
视为相等,而将 Number.NaN
与 NaN
视为不相等。