介绍
React是一个用于构建用户界面的javascript库,简化DOM操作。
声明式、虚拟DOM、Diffing算法
更新页面前比较虚拟DOM只更新有差异的真实DOM
jsx(JavaScript XML)
是React.createElement的语法糖
在原生js里面通过React.createElement也可以创建虚拟daom但太繁琐,通过jsx简化了对虚拟DOM的操作。jsx对虚拟DOM的操作通过babel翻译后都会转成原生虚拟DOM的操作(React.createElement)
语法规则
1.定于虚拟DOM时,不要写引号
2.标签中混入JS表达式要用{}
{表达式}
3.样式的类名改用className(class是es6类关键字)
4.style以对象形式书写,遵循小驼峰风格
const VDOM = (
<div style={{color:"red",fontSize:"30px"}}></div>
)
5.虚拟DOM只能有一个根标签
6.标签必须闭合
7.标签首字母
1.首字母小写,转同名html标签,若没有同名html标签,则报错
2.首字母大写,渲染对应的组件,若组件没有定义,则报错
8.数组表达式会被自动遍历
脚手架
全局安装
npm install -g create-react-app
创建
create-react-app hello-react
创建17版本
create-react-app hello-react
npm install react@17.x react-dom@17.x --save //降低到17版本
修改src>index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<App />,document.getElementById('root')
)
启动
npm start
编译
npm run build
虚拟dom
1.本质是object类型的对象(一般对象)
2.虚拟DOM比较轻(只有一部分必要的属性),真实DOM比较重
组件
1.函数、类名就是组件名
2.渲染时必须以闭合标签形式
React.render(<Dome/>,document.getElementById('main'))
3.渲染时React会自动调用函数、实例化类
函数式组件
1.函数必须有返回值
2.函数名必须大写
3.this为undefined,babel默认开启严格模式
4.函数式组件默认只有props,但通过其他方式也可以模拟类式组件的特性
props
组件的属性会被收集传给函数,函数第一个参数接收
只读(对象只读,但可以修改props引用其他对象)
批量传入
注意原生不允许直接 ...ob(对象展开)
只能在标签传入时使用
React.render(<Dome {...ob} />,document.getElementById('main'))
限制
export default (props) => {
for (const key in props) { // 只有通过遍历props对象来完成判断限制
if (Object.hasOwnProperty.call(props, key)) {
const element = props[key];
console.log(element)
}
}
return <div>
</div>
}
默认值
export default (props) => {
console.log(props)
props = { ...{ age: 13 }, ...props } // 通过对象展开运算符展开props对象,又因为重复属性会被后面的属性覆盖所以达到默认值的效果。 不能使用Object.assign,因为会修改一个参数对象的属性,props只读
console.log(props)
return <div>
<button onClick={() => {
}}>点击修改s</button>
</div>
}
Hooks
让函数式组件可以使用state以及其他react特性。只在最顶层使用 Hook
一般情况下最后一个参数为Hooks更新条件,不传:每次被执行都更新,空数组:只执行一次,数组:数组元素更新(变化)时执行
useState
React.useState:让函数式组件模拟实现类式组件的state,调用一次state修改函数就会重新执行函数,该组件所有的视图就会更新。
在 v18 之前只在事件处理函数中实现了批处理,React将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setstate 事件合并)
setTimeout(() => { // 在异步函数里面setState也是异步的
setState(1)
console.log("first") // first会被打印输出,但是在 v18 之前 只在事件处理函数 中实现了批处理,所以setTimeout执行完毕后函数会被重新执行两次
setState(2)
}, 1000);
但是该操作是直接替换
import React from 'react'
const [data,setData] = React.useState(0) // 返回一个数组,第一个是值,第二个是修改值的函数(只能通过修改函数修改)
setData(2) // 直接传值,修改data的值为0
setData((data)=> data+1) // 传入函数(接收一个参数data),修改data的值为data+1
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
第一个参数 reducer 是函数 (state, action) => newState
,接受当前的 state 和操作行为。第二个参数 initialArg 是状态初始值。第三个参数 init 是懒惰初始化函数。
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useEffect
React.useState:让函数式组件模拟类式组件三个生命周期(componentDidmount、componentDidUpdate、componentWillUnmount)
模拟componentDidmount
组件挂载后(路由不变的情况下,无论函数执行多少次,该钩子执行一次)
import React from 'react'
React.useEffect(()=>{
console.log("+")
},[]) // 第二个参数传入 空数组 表示不监听任何数据所以只在初始化执行一次
模拟componentDidUpdate
组件更新后(不要在该生命周期里更新视图会导致循环更新)
import React from 'react'
React.useEffect(()=>{
console.log("+")
}) // 第二个参数传入 不传 表示监听所有useState的数据
模拟componentWillUnmount
函数组件创建完成后就销毁(不要在该生命周期里更新视图会导致循环更新),初始化时不会被执行
import React from 'react'
React.useEffect(()=>{
console.log("+")
return ()=>{ // 返回的函数会在组件卸载前执行
console.log("-")
}
}) // 第二个参数传入 不传 表示监听所有useState的数据
总结
本质上来说useEffect应该是数组参数为key创建多个队列,当条件满足就将对应的函数入队然后将队列里的函数全部执行,如果队列里函数返回了一个函数,那么会按返回顺序入队最后在将回调函数入队。并且该队列长度为2,对于超出的部分在队头抛弃。即在每个useEffect回调函数里面只能返回一个函数,并且该返回的函数里面不能再返回函数,因为这样就会导致队列长度超出,再返回的函数会被抛弃
useRef
React.useRef:让函数式组件模拟类式组件的refs。不能直接对react组件使用,需要组件指定暴露的内容
const MyRef = React.useRef()
<input ref={MyRef} />
console.log(MyRef.current.value) // 和React.createRef()功能一样
useMemo
import { useMemo, useState, memo } from "react";
interface IProps {
value: number
}
let Con = memo((props:IProps) => {
console.log("render Con")
return (<div>{props.value}</div>)
})
export default function Home() {
const [count, setCount] = useState(0);
const [value, setValue] = useState(0);
const cachedValue = useMemo(function() {
return count + 1
}, [count]) // 空数组只执行一次,不传每次执行
return (
<>
<div>
{count}
</div>
<div>
{value}
</div>
<Con value={cachedValue} />
<button onClick={() => setCount(v => v + 1)}>Add Count</button>
<button onClick={() => setValue(v => v + 1)}>Add Value</button>
</>
);
}
useMemo 接收2个参数,第一个参数接收一个 factory 函数 ,函数的返回值即是 useMemo的返回值,第二参数是依赖列表,如果依赖列表中的值发生变化,就会引发组件的重渲染,并重新执行函数拿到新的值。
在上面的例子中 cachedValue 变量的求值依赖于count,如果 count 的不发生变化, useMemo 的 factory 便不会重新执行,cachedValue 的值也不会发生变化。
useMemo 作为一种性能优化的手段,可用于一些较为耗时耗资源的计算值的缓存, 避免因为这些计算影响渲染的效率。
useCallback
组件重新渲染时,useCallback不会被重新执行(依然是原来的函数),当参数2的依赖项变化时,重新执行(生成新的函数)
function Form() {
const [text, updateText] = useState('');
const handleSubmit = useCallback(() => {
console.log(text);
}, [text]); // 每次 text 变化时 handleSubmit 都会变,如果不写依赖数组意味着每次都会重新创建,空数组的话只创建一次
return (
<>
<input value={text} onChange={(e) => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} /> // 很重的组件,不优化会死的那种
</>
);
}
useContext
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext); // 和函数组件的context用法差不获取值的时候不通过<Consumer> 加函数的方式获取值,而是通过useContext传入一个Context对象获取值
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useImperativeHandle
自定义react组件被ref时返回的值
useImperativeHandle(ref, createHandle, [deps]) useImperativeHandle自定义在使用 时向父组件公开的实例值。与往常一样,在大多数情况下应避免使用 refs 的命令性代码。 应与 forwardRef 一起使用
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({ // 函数返回值作为外部ref的值,一般向外暴露操作内部状态的方法
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
useLayoutEffect
使用方式和useEffect基本一致,只不过在useEffect前执行,也能获取ref。一般在此动态设置样式操作DOM,所以会导致多此渲染性能会比较差一点。
usedebugvalue
useDebugValue 用于在 React 开发者工具(如果已安装,在浏览器控制台 React 选项查看)中显示 自定义 Hook 的标签。
useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
import React, { useState, useDebugValue } from 'react';
import ReactDOM from 'react-dom';
// 自定义 Hook
function useMyCount(num) {
const [ count, setCount ] = useState(0);
// 延迟格式化
useDebugValue(count > num ? '溢出' : '不足', status => {
return status === '溢出' ? 1 : 0;
});
const myCount = () => {
setCount(count + 2);
}
return [ count, myCount ];
}
function App() {
const [ count, seCount ] = useMyCount(10);
return (
<div>
{count}
<button onClick={() => seCount()}>setCount</button>
</div>
)
}
ReactDOM.render(<App />, root);
提示:我们不推荐你向每个自定义 Hook 使用 useDebugValue,只有自定义 Hook 被复用时才最有意义。
———————————————— 版权声明:本小结为CSDN博主「_Boboy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/weixin_4372…
context
this.context:指向父组件创建的公共数据区域(子组件必须被Provider包裹),一般用于给多层子组件传值
由于函数式组件没有this,所以采取context的第二种写法(也适用于类式组件)。
import React from 'react'
// 父组件创建Context
const MyContext = React.createContext() // 创建Context
const {Provider,Consumer} = MyContext // 从创建Context实例上解构Provider、Consumer标签
function A(){
return(
<div>
<Provider value={{name:'张三',age:18}}> {// 子组件必须包裹在Context.Provider标签中,并通过value指定需要共享的值 }
<B />
</Provider>
</div>
)
}
function B(){
return(
<div>
{/*通过Consumer获取Provider传入的数据,Consumer标签内回调函数接收Provider的value*/}
{// 只能包含函数,函数返回值作为该区域的内容,可以修改传入的数据。本质上Consumer就是一个引用}
<Consumer>
{value => value.name}
</Consumer>
</div>
)
}
context
this.context:指向父组件创建的公共数据区域(子组件必须被Provider包裹),一般用于给多层子组件传值
由于函数式组件没有this,所以采取context的第二种写法(也适用于类式组件)。
import React from 'react'
// 父组件创建Context
const MyContext = React.createContext() // 创建Context
const {Provider,Consumer} = MyContext // 从创建Context实例上解构Provider、Consumer标签
function A(){
return(
<div>
<Provider value={{name:'张三',age:18}}> {// 子组件必须包裹在Context.Provider标签中,并通过value指定需要共享的值 }
<B />
</Provider>
</div>
)
}
function B(){
return(
<div>
{/*通过Consumer获取Provider传入的数据,Consumer标签内回调函数接收Provider的value*/}
{// 只能包含函数,函数返回值作为该区域的内容,可以修改传入的数据。本质上Consumer就是一个引用}
<Consumer>
{value => value.name}
</Consumer>
</div>
)
}
类式组件
1.必须继承React.component
class MyComponent extends React.ComPonent{ // 类式组件必须继承React.ComPonent
state = {}
show = ()=>{ //show属性会被添加到实例对象上,指向箭头函数。 所以在绑定事件时不存在 this 指向问题 注意不允许 show()=>{}
}
}
2.实例(由React自动new)必须有render方法(渲染时调用render方法),并且必须有返回值
3.由于继承React.ComPonent所以实例对象会多三个属性props、refs、state
constructor
只在初始化时调用一次
传入一个props调用super(props)则在构造函数里可以通过this.props访问到props否则不能(React18自定义constructor必须调用super(props))
三大属性
state
和原对象合并
该属性只能是对象,用于存放响应式数据,直接修改该数据不会同步视图
this.setState({},()=>{}):回调函数会在视图更新完成后执行
setState
对象式
新状态不依赖原状态
this.setState({},()=>{}) // 传入一个对象作为state,该对象会和原state合并。回调函数会在界面更新后执行
函数式
对象式是函数式的语法糖,新状态依赖原状态
this.setState((state,props)=>{
return {
}
},()=>{}) // 传入一个函数(接收state、props),该函数返回一个对象作为state,该对象会和原state合并。回调函数会在界面更新后执行
refs
render返回时指定了ref属性的标签,标签(虚拟DOM转成真实DOM)会被收集到refs属性中。不能直接对react组件使用,需要组件指定暴露的内容
字符串(17以弃用)
字符串类型的ref不被推荐,字符串类型的ref存在效率问题
render(){
return (
<div ref="one">---</div>
)
}
函数回调
Reacrt会把当前真实节点传给回调函数
挂载时执行一次,更新时对比前后两次回调函数是否为同一函数对象,如果是更新时不执行回调,如果不是更新时执行两次回调函数第一次传入null用以恢复状态,第二次传入真实DOM用以修改
内联函数
使用量更多
每组件更新时ref内联函数回调会被执行两次,第一次传入null,第二次传入真实节点。因为内联函数每次都是新函数,前后两个函数对象不相同,所以新函数第一次传入null用以恢复状态,第二次传入真实DOM用以修改
render(){
return (
<div ref={(node)=>{}}>---</div>
)
}
实例方法
每组件更新时ref实例方法回调不会被执行,只有在组件加载时执行一次,因为函数被实例指向,函数执行后函数对象依然存在,通过对比前后两次函数对象是相同对象所以只在加载时执行一次。
render(){
return (
<div ref={this.getNode}>---</div>
)
}
getNode = (node)=>{
}
createRef容器
通过React.createRef()可以创建一个容器该容器只能存放一个DOM节点,通过容器.current可以访问到容器里的DOM节点
myRef = React.createRef()
show = ()=>{
console.log(this.myRef.current)
}
render(){
return(
<div ref={this.myRef}></div>
)
}
props
用于接收外部传入的值、只读
React.render(<Dome name='张三' age={19} />,document.getElementById('main')) // 注意传入的类型不同
this.propos.name获取传入的值
批量传入
注意原生不允许直接 ...ob
只能在标签传入时使用
React.render(<Dome {...ob} />,document.getElementById('main'))
限制
import PropTypes from 'prop-types'
Demo.proTypes = {
// 注意ProTypes必须先引入
name:PropTypes.string.isRequired, // name属性必须是字符串 必填。为了避免和原生String冲突,所以采取小写
show:PropTypes.func //注意为了避免function 所以是 func
}
或
class Demo extends React.ComPonent{
static proTypes = {
// 注意ProTypes必须先引入
name:PropTypes.string.isRequired, // name属性必须是字符串 必填。为了避免和原生String冲突,所以采取小写
}
}
默认值
Demo.defaultProps = {
name:"张三"
}
或
class Demo extends React.ComPonent{
static defaultProps = {
name:"张三"
}
}
render
每调用setState,render都会被执行(异步),render执行1+n次
setState
this.setState({name:"张三"}) // 通过this.setState传入一个新对象(与原对象合并),才会触发视图更新 React通过 实例.render() 调用
context
this.context:指向父组件创建的公共数据区域,一般用于给多层子组件传值,子组件必须被Provider包裹
import React from 'react'
// 父组件创建Context
const MyContext = React.createContext() // 创建Context
const {Provider} = MyContext // 从创建Context实例上解构Provider标签
export default class A extends Component{
render(){
return(
<div>
<Provider value={{name:'张三',age:18}}> // 子组件必须包裹在Context.Provider标签中,并通过value指定需要共享的值
<B />
</Provider>
</div>
)
}
}
class B extends Component{
static contextType = MyContext // 必须把 Context赋值给类的 静态属性contextType
render(){
return(
<div>
{this.context.name}
</div>
)
}
}
第二种写法参考函数组件的context
事件this指向
当类组件定义为
class MyComponent extends React.ComPonent{
show(){ // shwo属性会被定义在实例对象的原型上,并且指向一个非箭头函数
}
}
所以
<h1 onClick={this.show}>***</h1> // 注意绑定的函数是原型上的函数,该函数this为undefined 回调时调用直接函数不是 实例.函数 调用 有react框架调用
1.生成新函数。修改this指向
constructor(props){
super(props)
this.show = this.show.bind(this) // 通过原型函数bind生成一个新函数(this指向实例对象),并被实例属性指向
}
2.传入立即调用函数,在函数里面返回一个函数通过闭包也可以修正this指向问题
类式和函数式组件区别
在极端情况下函数式比类式性能更好
类式组件的this能在渲染方法以及生命周期方法中得到最新的实例
事件
React会将事件对象传给事件回调函数,通过event.target获取触发事件的DOM元素
React事件是自定义(合成)事件,为了更好的兼容性
事件会被添加到最外层的元素,采取事件委托处理,为了更高效
传值
父传子
props
子传父
回调函数
父组件通过props给子组件传递一个回调函数,该函数用于修改值
状态在哪里定义,修改状态的方法就在哪里
add = (v)=>{
}
render(){
return (
<Demo add={this.add}></Demo>
)
}
互传
pubsub-js(消息订阅与发布)
安装
npm i pubsub-js -S
导入
import PubSub from 'pubsub-js'
订阅与取消
let token PubSub.subscribe('num1',(msg,v)=>{}) // 订阅名为num1的消息,当有名为num1的消息发布时会执行回调函数,第一个参数为消息名
PubSub.unsubscribe(token) // 取消订阅
发布
PubSub.publish('num1',0) // 发布名为num1的消息,并给回调函数传值
标签体
标签体的内容被收集到 props.children中,相同设置children属性就可以指定标签体内容
<Deome children="666"></Deome>
样式模块化
原生css实现
css文件名中添加.module
src>components>Hellow>index.module.css
.title{
}
导入时需用对象接收
src>components>Hellow>index.jsx
import React,{Component} from 'react'
import hello from './index.module.css'
export default class Hello extends Component{
render(){
return <h2 className={hellow.title}></h2>
}
}
less实现
受控组件
推荐使用,非受控组件存在过度使用ref问题
通过事件绑定,将用户输入的数据维护到state中,读取时通过state读取
非受控组件
通过ref,读取用户输入的数据,不维护到stata中
简单组件
state没值
复杂组件
state有值
一般组件
由程序员调用渲染的组件,props不会被传入其他的值
<Demo></Demo>
路由化组件
通过withRouer函数可以让一般组件拥有路由组件的特性如history、localtion、math
import {withRouter} from 'react-router-dom'
export default withRouter(Demo)
路由组件
路由组件由Route渲染的组件,一般把路由组件放在src>pages文件夹下
不传props的话props有默认值
history
go、goBack、push、replace
localtion
pathname、ssearch(hash路由参数)、state
math
params(history路由参数)、path、url
import Demo from './pages'
<Route path='/test' component={Demo} />
<React.StrictMode>
用于检查代码不合理处
高阶函数
函数接收函数或返回函数称该函数为高阶函数
函数柯里化
通过函数多次调用连续返回函数的方式,实现多次接收参数统一处理
function sum(a){
return (b)=>{
return (c)=>{
return a+b+c
}
}
}
sum(1)(2)(3)
组件生命周期(16)
注意
在高版本可以用16版本的生命周期钩子,但由于componentWillMount、componentWillReceiveProps、componentDidUpdate在未来版本中可能出现bug,所以在使用以上方法时要加上前缀UNSAFE下划线用以提示如
因为生命周期是函数是react操作类实例调用的,所以在生命周期函数里面this是类实例对象
UNSAFE_componentWillMount(){}
UNSAFE_componentWillReceiveProps(){}
UNSAFE_componentDidUpdate(){}
react18版本,dev模式下render使用的是strict mode,strict mode的通过两次调用constructor和render函数来更好的检测不符合预期的副作用
下列函数会执行两次
- 类组件的constructor,render和shouldComponentUpdate方法
- 类组建的静态方法getDerivedStateFromProps
- 函数组件方法体
- 状态更新函数(setState的第一个参数)
- 传入useState,useMemo或useReducer的函数
在production环境下不会这样,所以不用担心
挂载时
会按照以下顺序依次执行
1.constructor
类构造器
必须调用super,可以访问属性
2.componentWillMount(UNSAFE_)
组件将要挂载
3.render
返回虚拟DOM
4.更新DOM和refs
5.componentDidMout(初始化)
组件挂载完毕
一般在该回调做一些初始化的事、开启定时器、发送网络请求、订阅消息
更新时
会按照以下顺序依次执行
1.componentWillReceiveProps(UNSAFE_)
接收一个props参数
只有在父组件更新(父组件加载时不执行) 时从这里开始执行,不管是否传入了props
2.shouldComponentUpdate
接收两个参数nextProps(最新的props)、nextState(最新的state)
只有该函数返回true时,后续函数才能执行
在调用setState时从这里开始向下执行
可以对state修改,在这里对state做一些处理
3.componentWillUpdate(UNSAFE_)
在调用forceUpdate时从这里开始向下执行
4.render
返回虚拟DOM
5.更新DOM和refs
6.componentDidUpdate
组件更新完毕,接收更新前的props和state
componentDidUpdate(preProps,preState){
}
卸载前
1.componentWillUnmount(收尾)
一般在该回调函数做一些收尾的事,例如关闭回调函数、取消订阅
组件将要卸载
通过React.unmountComponentAtNode可以卸载组件
React.unmountComponentAtNode(docu,emt.getElementById('demo'))
组件生命周期(17)
注意
react18版本,dev模式下render使用的是strict mode,strict mode的通过两次调用constructor和render函数来更好的检测不符合预期的副作用
下列函数会执行两次
- 类组件的constructor,render和shouldComponentUpdate方法
- 类组建的静态方法getDerivedStateFromProps
- 函数组件方法体
- 状态更新函数(setState的第一个参数)
- 传入useState,useMemo或useReducer的函数
在production环境下不会这样,所以不用担心
挂载时
会按照以下顺序依次执行
1.constructor
类构造器
2.getDerivedStateFromProps(新增类方法)
从props得到一个派生的状态,使用较少
接收两个参数props、state
必须定义为类方法,必须返回一个对象(null),该对象会和state对象合并
应用于state的值任何时候都取决与props
static getDerivedStateFromProps(props,state){
return null
}
3.render
返回虚拟DOM
4.更新DOM和refs
5.componentDidMout(初始化)
组件挂载完毕
一般在该回调做一些初始化的事、开启定时器、发送网络请求、订阅消息
更新时
会按照以下顺序依次执行
1.getDerivedStateFromProps(新增类方法)
从props得到一个派生的状态,使用较少
接收两个参数props、state
必须定义为类方法,必须返回一个对象(null),该对象会和state对象合并
应用于state的值任何时候都取决与props
static getDerivedStateFromProps(props,state){
return null
}
2.shouldComponentUpdate
New props、setState
该回调只有通过以上函数才会调用
只有该函数返回true时,后续函数才能执行
在调用setState时从这里开始向下执行
4.render
返回虚拟DOM
5.getSnapshotBeforeUpdate(新增)
获取快照
注意getSnapshotBeforeUpdate和componentDidUpdate的参数一二都是旧值
getSnapshotBeforeUpdate(preProps,preState){
return {}
}
返回一个非undefined,该返回值会传给componentDidUpdate回调的第三个参数。
主要用于在更新真实DOM前收集一些数据
6.更新DOM和refs
7.componentDidUpdate
组件更新完毕,该函数接收三个参数 更新前的props、state、getSnapshotBeforeUpdate返回值
componentDidUpdate(preProps,preState,snapshotValue){
}
卸载前
1.componentWillUnmount(收尾)
一般在该回调函数做一些收尾的事,例如关闭回调函数、取消订阅
组件将要卸载
通过React.unmountComponentAtNode可以卸载组件
React.unmountComponentAtNode(docu,emt.getElementById('demo'))
16、17总结
16准备废弃componentWillMount、componentWillReceiveProps、componentWillUpdate三个回调
17新增getDerivedStateFromProps、getSnapshotBeforeUpdate两个回调
diffing算法
最小粒度是标签
如果新旧虚拟树的DOM节点(key)相同(遍历查询),那就比较标签其他内容(包括子节点,只比较虚拟DOM定义时的部分),如果不同就用新的虚拟DOM生成真实DOM替换(只替换不同的标签)相同的部分会保留,如果相同那就不做变动
如果新旧虚拟DOM的key不同,那就根据新的虚拟DOM生成真实DOM
nanoid
生成唯一字符串
import {nanoid} from 'nanoid'
let s = nanoid()
跨域
路由5
react路由有三种实现,分别给不同的平台使用(web、native、any)
react-router-dom
react-router-dom是react路由的web实现
开启路由
需要路由管理的区域必须写到根路由里面,所以一般直接将写在根路由里面
BrowserRouter
BrowserRouter底层调用了history,路由组件的state参数保存在其中,刷新页面不会导致路由组件的stata参数丢失
history路由
import {BrowserRouter} from 'react-router-dom'
React.render(
<BrowserRouter>
<App/>
</BrowserRouter>,
document.getElementById('root')
)
样式丢失
由于使用的是请求路径所以在刷新页面时会向服务器发起请求,导致一些个相对资源出现问题。
不用./xxx改用/xxx
不用./xxx改用%PUBLIC_URL%/xxx // %PUBLIC_URL%:public根路径
改用HashRouter
HashRouter
HashRouter仅仅对hash地址的操作没有缓存数据,所以在页面刷新时会导致路由组件的stata丢失
hash路由
import {HashRouter} from 'react-router-dom'
React.render(
<HashRouter>
<App/>
</HashRouter>,
document.getElementById('root')
)
触发路由
Link
import {Link} from 'react-router-dom'
export default class Demo extends Component {
render() {
return (
<div>
// BrowserRouter:/test
// HashRouter:#/test
<Link to="/test">test</Link>
</div>
)
}
}
NavLink
NavLink和Link功能上大体相同,主要有个activeClassName可以在单击时追加指定的样式
import {NavLink} from 'react-router-dom'
export default class Demo extends Component {
render() {
return (
<div>
// BrowserRouter:/test
// HashRouter:#/test
<NavLink activeClassName="demo" to="/test">test</NavLink>
</div>
)
}
}
注册路由
Route
Route组件在匹配成功后会向下接着匹配
import {Link} from 'react-router-dom'
import {Route} from 'react-router-dom'
export default class Demo extends Component {
render() {
return (
<div>
// BrowserRouter:/test
// HashRouter:#/test
<Link to="/test">test</Link>
<Route path="/test" component={Demo_one} />
</div>
)
}
}
Swith
将Routr组件放在Switch组件中匹配成功就不再向下匹配
<Swtitch>
<Route>
</Route>
</Swtitch>
Redirect
重定向组件,匹配到该组件时就跳转到指定的路由
import {Redirect} from 'react-router-dom'
<Redirect to='/home' /> // 当匹配到该Redirect就跳转路由到 /home
匹配规则
默认是遍历匹配,只要匹配的组件就会显示。可以包裹Swtitch标签实现一个路径只匹配一个组件就停止。
exact
默认是模糊匹配当发生路由改变时,Router会从左向右去匹配路由规则,满足条件就展示组件如/home/a会被to='/home'匹配。通过开启精准匹配,如/home/a不会被to='/home'匹配,只有完全一样时才会匹配
嵌套路由
嵌套路由使用路由的模糊匹配,注意不要开启精确匹配。
嵌套路由在匹配规则带上父路由即可
<Swtitch>
<Route path="/home/demo" component={demo}>
</Route>
</Swtitch>
路由组件传参
params
路径参数
发送
在Link to后添加额外的信息
<Link to='/home/1'></Link>
接收
在Route path后定义变量
<ROute path='/home/:id' component={Demo} ></Link>
使用
this.props.match.params.id
search
查询参数
发送
在Link to后添加额外的信息
<Link to='/home?id=1&name=zs'></Link>
接收
search无需声明接收
使用
通过props.location.search可以拿到查询参数字符串如 ?id=1&name=zs
query-string
通过query-string可以将上面的字符串转换成对象
安装
npm i -save-dev query-string
引入
import QueryString from 'query-string'
使用
querystring.decode(this.props.location.search.slice(1))
路由组件state
本地刷新URL,路由组件的statede走缓存
参数不会在地址栏中显示,也正是因为这样导致通过URL获取信息的组件会接收不到参数
需要将to写成一个对象
发送
<Link to={{pathname:'/home',state:{id:1,name:'zs'}}}></Link>
接收
路由组件state无需声明接收
使用
通过props.location.state即可拿到对象
push
默认就是push模式
replace
replace={true}开启replace模式
<Link replace={true} to={{pathname:'/home',state:{id:1,name:'zs'}}}></Link>
编程式路由
路径参数直接修改即可,要携带路由组件state参数,在路径传入一个对象即可
history
push
this.props.history.push('/home',state)
replace
this.props.history.replace('/home',state)
go
实现路由的前进后退
this.props.history.go(num)
ant-design
安装
npm install antd --save
注意
在ant-design需要单独引入css
import 'antd/dist/antd.css'; // 也可以根据官网进行按需导出
redux
不是react插件库,是一个第三方库,采取构造请求、管理数据、执行计算分离的原则。
所有redux相关文件都放在src>redux中
安装
npm i redux -S
reduce
执行器
每个组件都有一个reduce
reduce是一个函数,和storo绑定后,接收两个参数,第一次参数是之前函数的返回值,第二个参数是可自定义的指令对象。
store会初始化(执行两次返回值无效) 与其绑定的reduce,
{type: '@@redux/INIT9.6.m.q.v.a'}
{type: '@@redux/PROBE_UNKNOWN_ACTION6.0.t.5.l'}
redux>count_reducer.js
function countReducer(preState=0,action){
// preState:函数之前的返回值 初始化:undefined,通过函数默认值将初始化时的值改为0
// action:可自定义的指令对象 初始化:{type:"@@redux/INITx.x.x.x.x"} x:随机字符
}
视图同步
redux数据的变化不会引起视图的变化,redux的数据变化时(应该是===比较)调用store.subscribe回调
所以一般在index.js中给store.subscribe绑定reader执行
App.js
import store from '.redux/store'
ReactDOM.render(<App/>,document.getElementById('root'))
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
store
管理数据
注意4.2的import {createStore} from 'redux'会警告弃用,import {legacy_createStore as createStore} from 'redux'
每个应用只有一个store,store是redux的核心。
redux>store.js
import {createStore} from 'redux'
import {countReducer} from './count_reducer' // 导入计算器
export default createStore(countReducer) // 和计算器绑定并暴露store对象
store.getState():获取保管的值
store.dispatch({type:'add',data:1}):发送自定义指令,只能传一个对象(异步action时除外)
action
每个组件都有action,store.dispatch(action)
同步action
生成一个对象({type:'',data:''})传入store.dispatch(action())
异步action
应用场景:异步等待不想交给组件,想交给store。
给store.dispatch传入一个函数,那么该函数会被store调用。并且该函数会接收到store对象
因为是由store对象调用的,所有需要对store对象修改配置
安装redux-thunk
npm i redux-thunk -S
store.js
import {applyMiddleware} from 'redux' // 导入中间件
import thunk from 'redux-thunk' // 导入形实转换程序
export default createStore(countReducer,applyMiddleware(thunk)) // 配置store
多状态
由于redux实现状态是通过reduce调用得到的,store只是初始化调用和保存其调用的返回值而已,所以要完成多状态管理就有多个reduce存在。通过combineReducers可以实现多个reduce合并。
import {combineReducers} from 'redux'
const allReducer = combineReducers({
// 指令会按照添加的顺序依次传递给reduce
getData:dataReducer,
getSum:sumReducer
})
export default createStore(allReducer,applyMiddleware(thunk))
通过store.getState()就会返回一个对象,得到对应reduce的值
store.dispatch({type:"test"})指令会按序在所有的reduce传递,reduce的返回值只能修改其状态,不能终止指令传递
react-redux
react-redux是react官方库。
react-redux把组件分为UI组件和容器组件。react-redux规定UI组件必须放在容器组件中。
实现了数据视图同步
安装
npm install react-redux -S
UI组件
只专注于UI的操作,不直接操作redux相关。接收(通过props)容器组件传入的数据,间接操作redux。
一般将UI组件放在components中
容器组件
只专注与redux的操作,对redux操作由容器组件完成。给UI组件传入(通过props)redux相关数据
一般将容器组件放在container中,必要时将ui组件也整合到容器组件中,但只向外暴露容器组件
创建
import CountUI from '../../components/Count' // 引入UI组件
import {connect} from 'react-redux' // 引入容器组件构造函数
// 容器组件绑定store后,store.state会被传入。该函数的返回值会被传入给UI组件的props(属性)
function mapStateToProps(state){
return {
name;state.name
}
}
// 容器组件绑定store后,store.dispatch会被传入。该函数的返回值会被传入给UI组件的props(方法)
function mapDispatchToProps(dispatch){
return {
add:number => dispatch(createIncrementAction(number))
}
}
// mapDispatchToProps也可以是一个对象
// 参数一是函数,react-redux调用该函数并传入由redux保存的state,将其返回值作为props传递给UI组件
// 参数二是函数,react-redux调用该函数并传入dispatch触发器,将其返回值作为props传递给UI组件
export default connect(mapStateToProps,mapDispatchToProps)(CountUI) // 绑定UI组件
注意,mapDispatchToProps可以是一个对象(值为action)
mapDispatchToProps = {
add:createIncrementAction(number) // createIncrementAction会被react-redux通过store.dispatch()调用
}
//
export default connect(mapStateToProps,mapDispatchToProps)(CountUI)
绑定store
<Count store={store}/> // 容器组件绑定store
Provider
Provider里面的容器组件会绑定和Provider一样的store
import {Provider} from 'react-redux'
<Provider store={store}>
<App/>
</Provider>
UI容器组件整合
一般将UI组件和容器组件整合到一个文件中。
scr>containers>Count>index.jsx
class Count extends Component{
}
mapStateToProps = ()=>{
return {}
}
mapDispatchToProps = {}
export default connect(mapStateToProps,mapDispatchToProps)(Count)
踩坑日记
在容器组件里面修改了值后,再对值的访问为undefined
总结
src>redux // 存放redux相关文件
actions // 存放action文件
count.js //
index.js // 汇总所有action再导出,也可以不汇总导出
demo.js //
reduces // 存放reduce文件
count.js //
index.js // 汇总所有reduce再导出,也可以不汇总导出
demo.js //
constant.js // action常量
store.js // store文件
src>containers // 存放容器组件和UI组件的整合文件
调试工具
Redux DevTools,该工具需要代码的支持
安装
npm install redux-devtools-extension
配置
src>store
import {composeWithDevTolls} from 'redux-devtools-extension'
const allReducer = combineReducers({
// 指令会按照添加的顺序依次传递给reduce
getData:dataReducer,
getSum:sumReducer
})
export default createStore(allReducer,composeWithDevTolls(applyMiddleware(thunk)) // 异步action传给composeWithDevTolls
React扩展
forwardRef
由于ref不能直接对react组件,通过forwardRef包装的组件,并且指定ref(该ref与外部ref为同一对象)后,对react组件使用ref时会得到指定的真实DOM
const FancyButton = React.forwardRef((props, ref) => ( // 注意参数2 ref 用于指定外部获取的ref
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
memo
父组件渲染会导致子组件的渲染,通过memo可以避免不必要的子组件渲染。它的 props 发生改变的时候进行重新渲染
let Test = ()=>{
return (<div>Test</div>)
}
export default React.mome(Test)
lazy
懒加载。
一般情况下在网页第一次打开时所有路由组件就请求完成,通过lazy函数可以实现当组件需要展示时才去下载路由组件
在导入一般组件时要通过lazyLoad函数导入
import {lazy,Suspense} from 'react'
const Demo = lazy(()=>{import('./Demo')})
<Suspense fallback={<div>再等等了</div>}> // 懒加载的组件必须放在Suspense中,fallback当懒加载的组件还没有加载完成时就显示fallback后面的组件(不能是懒加载的组件)
<Route path='/demo' component={Demo}/>
</Suspense>
Fragment
由于react规定组件必须被包裹,Fragment编译后丢弃不产生标签,只允许有一个key属性
import {Fragment} from 'react'
<Fragment key={id}>
<buttom>*</buttom>
</Fragment>
空标签
空标签也可以用于包裹组件,空标签不允许有任何属性
<>
<buttom>*</buttom>
</>
PureComponent
import {PureComponent} from 'react'
// 继承PureComponent的组件会在 this.setState({}) state、props对象没有改变的情况下不render组件 原理是在shouldComponentUpdate进行了逻辑判断
export default class Demo extends PureComponent {
render() {
return (
<div>
</div>
)
}
}
props插槽
通过把子组件传递给父组件,达到把子组件的控制权交给父组件。
父组件不显示子组件,子组件不会开始生命周期。
props.children标签体
import React, { Component } from 'react'
import A from './components/A'
import B from './components/B'
import MyData from './components/MyData'
export default class App extends Component {
render() {
return (
<div>
<div >
<A>
<B></B>
<MyData></MyData>
{
<B></B>
}
</A>
</div>
</div>
)
}
}
import React, { Component } from 'react'
export default class A extends Component {
render() {
return (
<div>A
// 当标签体有多个不同类型的数据时,children是个数组,如果数组里面没有函数(函数不显示但会警告),那么react自动将里面数据显示。
// 当标签体是相同类型数据时,children就是该数据,该数据不是函数(函数不显示但会警告)的话就直接显示
{(this.props.children)}
</div>
)
}
}
props.render标签属性
约定当把子组件通过父组件属性传递时通过render属性
import React, { Component } from 'react'
import A from './components/A'
import B from './components/B'
export default class App extends Component {
render() {
return (
<div>
<A render={[<B></B>,<B></B>,(age)=> <B age={age}></B>]}></A>
</div>
)
}
}
import React, { Component } from 'react'
export default class A extends Component {
render() {
return (
<div>A
{this.props.render}
{(this.props.render[2](18))}
</div>
)
}
}
import React, { Component } from 'react'
export default class B extends Component {
render() {
console.log("+")
return (
<div>B
{this.props.age}
</div>
)
}
}
总结
以上两种方式的原理都是一样,只是传递的方式不一样。并且任何合法的数据都传递,包括传递一个函数再由父组件调用函数传参,达到把子组件的控制权交给父组件
错误边界
只能在生产环境使用
把子组件的错误限制在一个区域内,并给出友好的提示
统计错误出现的次数。
父组件处理
export default class Demoe extends Component {
state = {
hasError:''
}
static getDerivedStateFromError(error){ // 子组件报错。触发该回调携带错误信息
return {hasError,error} // 返回一个对象该对象会和state对象合并
}
render() {
console.log("+")
return (
<div>
// 判断是否拥有错误信息
{this.state.hasError ? <h2>网络背锅</h2> : <Child></Child>}
</div>
)
}
}
componentDidCatch(error,info){ // 当前组件render() 函数抛出错误,则会触发该函数
}
通信方式
props
消息阅读-发布
redux
context
路由6
安装react-router-dom
npm install react-router-dom
开启路由
需要路由管理的区域必须写到根路由里面,所以一般直接将写在根路由里面。
src->index.js
import {HashRouter} from "react-router-dom"
ReactDOM.render(
<HashRouter><App /></HashRouter>
, document.getElementById('root')
)
HashRouter、BrowserRouter
Routes、Route
<Routes>
// Swtitch组件被Routes替换,路由组件必须被Routes包裹功能和Swtitch一样
// Route的component改为element,并且属性值以标签形式。caseSensitive在匹配路由时区分大小写
<Route caseSensitive path='/demo' element={<Demo />} />
</Routes>
Navigate
Navigate标签用于替换Redirect标签。
<Routes>
<Route caseSensitive path='/demo' element={<Demo />}
// 当路由匹配到/时就跳转到指定的/about路由
// 默认replace={false}:push模式
<Route path='/' element={<Navigate to='/about' replace={false} />} />
</Routes>
Navigate组件只要被渲染就会跳转到指定路的路由
{num == 2 ? <Navigate to='/about' replace={true} />:<h4>demo</h4>} // replace={true}:replace模式
// 当num等于2时,会导致Navigate被渲染,而Navigate只要被渲染路由就会跳转到指定的/about
NavLink
在路由5中NavLink被点击会给标签追加一个active类
在6中NavLink的className等于一个函数
// className函数接收一个参数,被点击时传入{isActive:true}否者{isActive:false}函数的返回值作为类名
// 初始化时自动调用一次
<NavLink className={({isActive}=>{return isActive ? 'active show':'demo'})} end to='/home/news'></NavLink> // end只有子路由链接有单击效果
Link
没有高亮
useRoutes(路由表)
通过路由表可以把注册路由抽离出来集中管理(参考vue-router),注意触发路由的组件不用集中管理
<Routes>
<Route path='/about' element={<About />}/>
<Route path='/about' element={<About />}/>
</Routes>
以上可以被抽离成
src>routes>index.js
import About from '../pages/About'
import Home from '../pages/Home'
export default[
{path:'/about',element:<About />},
{path:'/home',element:<Home />}
]
src>App.jsx
import {useRoutes} from 'react-router-dom'
import routes from './routes'
export default function App(){
const element = useRoutes(routes) // 传入路由表(数组)
return (
// 路由注册被集中成了element,注意element是变量形式 不是标签形式
<div>
{element}
</div>
)
}
Outlet(嵌套路由)
src>routes>index.js
import About from '../pages/About'
import Home from '../pages/Home'
export default[
{path:'/about',element:<About />},
{path:'/home',element:<Home />,
children:[
// 配置嵌套路由的路由表
{path;'news',element:<News />}, // 子路由不加/
{path;'message',element:<Message />} // 子路由不加/
]}
]
src>pages>Home.jsx
import {Outlet} from 'react-router-dom'
export default function Home(){
return (
<div>
<H5>Home</H5>
<Outlet /> {// 指定嵌套路由的展示位置
}
</div>
)
}
useRoutes、Outlet注意点
useRoutes用于非嵌套路由,确定路由组件在App中展示的位置
Outlet用于嵌套路由,父路由组件被匹配时确定子路由组件在父路由组件中的展示位置
to简写
NavLink和Link的to多种简写(在路由5中必须把路由完全写完不允许简写)
< to='./demo'>、< to='demo'>:在当前路由下触发一个子路由demo
< to='/demo‘>:修改顶层路由为/demo
参数传递
useParams
只能BrowserRouter路由模式使用
Params参数
import {useParams} from 'react-router-dom'
const params = useParams() // 获取路由组件传入的params参数
useSearchParams
只能HashRouter路由模式使用
Search参数
import {useSearchParams} from 'react-router-dom'
const [search,setSearch] = useSearchParams() // 结构数组
console.log(search,get('id')) // 调用get获取传入的Search参数
useLocation
state参数
注意路由6的state参数定义和路由5不一样
<Link state={{name:'张三',age:18}} > // 直接写在组件的state属性上
</Link>
import {useLocation} from 'react-router-dom'
const {state} = useLocation() // 解构state
console.log(state)
useNavigate(编程式导航)
import {useNavigate} from 'react-router-dom'
const usenavigate = useNavigate()
usenavigate()('/demo',{ // url支持简写
replace:true,// 默认为false
state:{ // 传递stata参数 其他参数直接写在路径中
name:'zs'
}
})
usenavigate(-1) // 后退
usenavigate(1) // 前进
useInRouterContext
import {useInRouterContext} from 'react-router-dom'
console.log(useInRouterContext()) // 判断是否被路由的上下包裹(BrowserRouter、HashRouter)
useNavigationType
import {useNavigationType} from 'react-router-dom'
console.log(useInRouterContext()) // 返回当前的导航类型(用户是如何来到当前页面的) POP:刷新 PUSH REPLACE
useOutlet
import {useOutlet} from 'react-router-dom'
console.log(useOutlet()) // 返回当前组件中被渲染的嵌套路由组件,嵌套路由没有挂载返回null
useResolvedPath
import {useResolvedPath} from 'react-router-dom'
console.log(useResolvedPath('/user?name=张三&age=18')) // 解析给定的路由信息
踩坑日记
在函数组件执行时触发的组件更新会立即执行,以回调方式触发的组件更新会在回调函数执行完成后更新,并且在回调函数中无法获取到最新state(直接使用state对象)因为回调函数指向的是旧的state对象,通过如下方式才能访问最新的state。监听state、变量
setState((p)=>{
console.log(p);
return p;
})
尽量使用生命周期hook,尽量不要通过触发更新组件重新执行函数的方式来做逻辑判断(hook上下文属于当前函数,重新执行函数是新的上下文)
通过数组添加类时要通过join(' ')做连接,不然再展开时有逗号