React17全家桶精简备忘录

153 阅读34分钟

介绍

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

useEffect执行时机.png

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

useLayoutEffect运行时机.png

使用方式和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所以实例对象会多三个属性propsrefsstate

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实现

react配置less.jpeg

受控组件

推荐使用,非受控组件存在过度使用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生命周期.png

注意

在高版本可以用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)

17生命周期.png

注意

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

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

redux原理图.png

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(' ')做连接,不然再展开时有逗号