react 入门看这个就够了
发布日期:2022-02-17 02:39:49 浏览次数:18 分类:技术文章

本文共 28133 字,大约阅读时间需要 93 分钟。

1. React

1.1. 常识

React 是什么:用于构件用户界面的 JavaScript 库(将数据渲染为 HTML 视图,它不关注获取数据啥的,只给你渲染到 HTML 上面)

为什么要学:

  • 原生 JavaScript 操作 DOM (jQuery 也是操作 DOM,只是写起来简单) 繁琐、效率低(比如 document.getElementById() 一句话就是:DOM-API 操作 UI
  • 直接操作 DOM,浏览器会进行大量的重新绘制、重新排列,造成效率低下
  • 原生 JavaScript 没有组件化编码方案,代码复用率低

React 优点:

  • 采用组件化模式、声明式编码(命令式和声明式就像手动挡与自动挡的区别,命令式的步骤不能缺少,才能完成一个任务;而声明式可以自动帮你完成)
  • 在 React Native 中可以使用 React 语法进行移动端开发(利用 JavaScript 开发 app)
  • 使用虚拟 DOM + Diffing 算法,尽可能地减少了与真实 DOM 交互
    在这里插入图片描述

1.2. 旧版本使用

1.2.1. 库简介

  • babel.js

    • 之前的作用有:es6 转 es5 语法,让浏览器能够识别;模块化的时候 -> import 也需要靠它进行工作
    • 现在的新的一个作用:将 jsx 转为 js
  • react.development.js

    • react 的核心库
  • react-dom.development.js

    • react 用于操作 DOM 的核心库

1.2.2. 编写 hello-world

在这里插入图片描述

1.2.3. 为什么要用 jsx

需求:生成 <div id="root"><span>Hello React</span></div> 这样一个节点

原生 JavaScript:

// createElement 是创建真实 DOMReact.createElement('div', {    id: 'root' }, React.createElement('span', {   }, 'Hello World'))

jsx:

const VDOM = (    
Hello React
)

总结:这里就可以看出来 jsx 的优势了,它能够很轻松地创建虚拟 DOM (但实际上,它还是会翻译成上面的结果,只是我们写起来方便,就是个语法糖而已)

1.2.4. VDOM 是什么

  • 虚拟 DOM 实际上是 Object
  • 虚拟 DOM 比较,它只需要 react 内部使用,就不需要那么多真实 DOM 的属性
  • 虚拟 DOM 最终会被渲染成真实 DOM,放在页面上

在这里插入图片描述

1.2.5. jsx(JavaScript XML)

如下是 JSX 的一些语法规则:

  • 定义虚拟 DOM 时,不能有引号
  • 标签内有 表达式 时,得用 { username }
  • 样式要用 className = "username"
  • 内联样式用 {
    { color: 'red' }}
    ,可以看成 {} 传入了一个对象 { color: ‘#f00’ }
  • 只能有一个 根标签,如果要写多个兄弟节点,就只能在外层包一层
  • 标签 必须闭合,否则报错,如:<input type="text"/>
  • 尽量别乱写标签,如:<good>123</good>,小写开头的标签会自动转换成 HTML 元素,控制台会报错;大写开头就是 组件,如果没有定义的话,也会报错
    // 例子const id = 'username'const username = 'yuwan'const VDOM = (		

    { username.toLowerCase() }

    )

Tip:表达式代码 的区别:

1.表达式返回一个值,比如:username、a+b、add(1, 3)【左侧写一个变量,能够接收到的就是表达式】
2.比如这些就是代码而不是表达式:if() 、{}、 for(){}、 switch(){}等

1.2.6. 小案例 - 动态生成前端框架名称

const title = '前端 js 框架列表'const frameworks = ['Angular.js', 'React.js', 'Vue.js']const VDOM = (    

{ title}

    { // 这里是不能写 forEach 来遍历,因为它没有返回值;写 map 才能动态生成出来 frameworks.map((item, index) => { return
  • { item}
  • }) }

{ }

)ReactDOM.render(VDOM, document.getElementById('root'))

1.2.7. 模块、组件、模块化、组件化的理解

1.2.7.1. 模块

  • 理解:一般就是一个 js 文件,就是一个模块
  • 为什么分模块:按照业务逻辑增加,代码会增多且复杂
  • 作用:复用 js,简化 js 的编写,提高 js 运行效率

1.2.7.2. 组件

  • 理解:用来实现一些功能的 代码和资源 的集合(每个组件有自己的 HTML、CSS、JS、Video、image,简言之就是什么都拆了)
  • 为什么分组件:一个界面的功能更加复杂的时候,分组件就能更好地复用代码
  • 作用:复用代码,简化项目编码,提高运行效率

1.2.7.3. 模块化

当应用的 js 都是用模块来编写的,那么这个应用就是一个模块化的应用

1.2.7.4. 组件化

当应用都是以组件方式来进行开发,那么这个应用就是一个组件化的应用

1.3. react 面向组件编程

1.3.1. 定义组件

react 中,组件有两种:函数式组件类式组件

1.3.1.1. 函数式组件

渲染过程:

  • 1.React 解析组件标签,找到 CreateApp 组件(若找不到则会报错)
  • 2.发现 CreateApp 是使用函数定义的,因此就会调用该函数,将返回的虚拟 DOM 渲染为真实 DOM,随后呈现在页面中

注意:

  • babel 在转换过程中开启了严格模式,导致 this 不能指向 window,因此是 undefined

这里 render(CreateApp(), …) 也会呈现出内容,但是它不是个组件了,在 Console -> Components 下无法找到

1.3.1.2. 类式组件

类知识复习

总结:

  • constructor 可写可不写,具体看要求

  • class Son extends Father,且 Son 写了 constructor,就必须调用 super()【可以简化编码】

  • 类中定义的一般方法,放在了类的原型对象上,供所有实例使用

    在这里插入图片描述

  • 注意 this 的指向【构造器中,this 指向的是 new Person();一般方法的 this就要看具体情况了】

  • 注意原型链查找方式

在这里插入图片描述

渲染过程:

  • 1.react 解析标签,找到 MyApp 组件
  • 2.发现这个组件是类定义的,就会 new MyApp() 然后调用了 render 方法
  • 3.将 render() 返回的虚拟 DOM 渲染到页面的真实 DOM 中

1.3.2. 组件核心属性之 state

理解:简单理解就是组件自身所需要的数据 ;组件被称为状态机,通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

注意事项:

  • 组件中 render 的 this 指向了组件实例
  • 组件中自定义方法中 this 为 undefined,怎么解决?(bind、()=>{})
  • 状态中的数据不可直接修改,通过 setState 修改

1.3.2.1. 点击事件

原生 JavaScript 的点击事件分为三种写法,React 推荐第三种写法

const btn1 = document.getElementById('btn1')btn1.addEventListener('click',() => {       alert('hello world')})const btn2 = document.getElementById('btn2')btn2.onclick = () => {       alert('hello world')}// function test() {       alert('hello world')}

1.3.2.2. 类中方法 this 指向

类中的函数默认开启了严格模式,导致直接通过 varSpeak() 调用时,会输出 undefined

1.3.2.3. React 绑定事件

如下是演示,我们在不知道 react 怎么绑定事件的过程:首先想到的是传函数调用结果 -> 传表达式调用结果 -> 传函数名 -> 解决 this -> this.xxx 调用

绑定事件的正确用法

有两种方式:bind 来改变 this利用箭头函数的特点

在这里插入图片描述

总结:

  • 理解 react 为什么要通过上面方式来进行事件调用【主要是 this 指向问题】
  • 通过 setState 修改 state 里面的值,而且它是合并操作

1.3.3. 组件核心属性之 props

说明:props 是 只读 的,无法对里面的某个字段进行修改

1.3.3.1. 基本使用方法:

1.3.3.2. … 运算符简单用法

let arr1 = [1,2,3]let arr2 = [6,7,8]// 展开数组console.log(...arr1) // 1 2 3// 连接数组let arr3 = [...arr1, ...arr2] // [1,2,3,6,7,8]// 不定长参数function sum(...nums){       return nums.reduce((preV,curV) => preV+curV, 0)}sum(1,2,3) // 6sum(1,2) // 3// 使用字面量对象时用展开实现浅拷贝let p1 = {    name: 'yuwan', age: 23 }let p2 = {    ...p1 } // p2 --> { name: 'yuwan', age: 23 }let p3 = {    ...p1, name: 'Jerry', sex: '女'} // 其实就是合并操作,将 yuwan 替换为 Jerry,并追加 sex 属性

1.3.3.3. 给每个标签指定类别和默认值

解释:类似于 Vue 父子传值时,规定好一些默认值和类型

需要引入 prop-types.js,用于限定数据类型(16.0 以前都是通过 React.PropTypes.string 来进行限制的,导致 React 这个核心对象变大了而且也不一定需要这个,因此官方把它抽离出来形成了prop-types.js)

1.3.3.4. 简写方式

1.3.3.5. 构造器进一步作用

是否需要如下写法,取决于 是否需要在构造器中使用 this.props(很少用到,因此可以不写)

class Person extends React.Component{       constructor(props){           super(props)        console.log(this.props) // 这里调用了 super(props) 就能够在组件实例输出    }}

在这里插入图片描述

1.3.3.6. 函数式组件中使用 props

16.0之前的版本,函数式组件 仅可使用 props

function Person(props){    // 传入的参数都在这个 props 对象里了    const {   name, age, sex} = props    return 
{ name } - { age } - { sex }
}const p1 = { name: 'Tom', age: 24, sex: 'M'}ReactDOM.render(
, document.getElementById('app'))

1.3.4. 组件核心属性之 refs

解释:Vue 的 refs 和它是一样的,就是对某个元素打上标记,能够直接操作真实DOM

写法:

  • 字符串类型的 ref (效率存在问题,因此不建议再使用了
  • 回调形式 :ref={(curNode)=>this.rmsg = curNode}【公司常用】
  • React.createRef【官方推荐】

注意:不可过度使用 ref,在本例中,第二个输入框的失去焦点回调函数可以通过 handleRight(e){ e.target.value } 获取到它的文本值,也就是发生事件的元素正好是当前元素就可以用这种

1.3.4.1. 小案例 - 输入数据并弹框

class MyComponent extends React.Component{       state = {           leftVal:'',        rightVal:''    }    render() {           return (            
{ this.rmsg = curNode} }
) } handleLeft = () => { this.setState({ leftVal: this.refs.lmsg.value // 通过 this.refs 调用 }) } handleRight = () =>{ this.setState({ rightVal: this.rmsg.value // 直接通过 this.rmsg 调用 }) }}
class App extends React.Component{     lRef = React.createRef() // 这个容器只能装下当前节点,函数返回一个容器  render() {       return (            
) } handleLeft = () => { console.log(this.lRef.current.value) // 通过容器的 this.lRef.current 调用 }}

1.3.5. 事件处理

通过 onXxx 属性绑定事件的回调函数【注意大小写,跟原生相反】

  • 因为 react 使用的是自定义事件(如 onClick vs onclick ),它考虑了兼容性
  • react 把事件都委托给了最外层节点,因为事件冒泡原则,它就更高效(如 ul 添加点击事件就比给它的每个 li 添加事件效果好)

1.3.6. 收集表单数据

1.3.6.1. 小案例 - 输入账号和密码,弹窗显示

非受控组件:现用现取,如下面的 handleLogin() 调用的时候,采取获取 username 和 password

class Login extends React.Component{       idRef = React.createRef()    pwdRef = React.createRef()    render() {           // 默认表单提交 get 请求,并携带所有的 k-v 如:https://www.baidu.com/?username=admin&password=123        return (            

欢迎登录

账号:
密码:
) } handleLogin = (e) => { const username = this.idRef.current.value const password = this.pwdRef.current.value alert(`账号:${ username},密码:${ password}`) e.preventDefault() // 阻止表单默认提交行为 } clearInput = () => { this.idRef.current.value = '' this.pwdRef.current.value = '' }}

受控组件:随着用户操作,就把值绑定到 state 中 【推荐使用,毕竟不需要 ref 】

class Login extends React.Component {       state = {           username: '',        password: ''    }    render() {           // 默认表单提交 get 请求,并携带所有的 k-v 如:https://www.baidu.com/?username=admin&password=123        return (            

欢迎登录

账号:
密码:
) } inputUname = (e) => { this.setState({ username: e.target.value }) } inputPwd = (e) => { this.setState({ password: e.target.value }) } handleLogin = (e) => { e.preventDefault() // 阻止表单默认提交行为 const { username} = this.state const { password} = this.state }}

写通用输入方法(高阶函数写法)

func(callback) 或 func(){ return callback }则 func 是高阶函数(形参是函数或返回一个函数)

函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数,最后统一处理的函数编码形式

class Login {       render(){           return (        
账号:
) } inputData = (inputType) => { // 这个方法就可通用决定输入 return (e) => { // onChange 调用这个回调函数,自然会传入 event 形参 this.setState({ // 这里也算是函数柯里化 [inputType]: e.target.value // [inputType] 在设置对象键的时候,转成字符串,因为它是变量,否则直接存入inputType这个键 }) } }}

不用函数柯里化:onChange = { e => this.inputData(‘username’, e) } // 再重写一下 inputData 就好

1.3.7. 组件生命周期

三大框架都有生命周期回调函数,如下是原理图

1.初始化阶段:由 ReactDOM.render() 触发 — 初次渲染

  • constructor
  • componentWillMount
  • render【必用】
  • componentDidMount【常用于初始化,如开启定时器、发送请求、订阅消息】

2.更新阶段:由 this.setState() 或父组件重新 render() 触发

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

3.卸载阶段:由 ReactDOM.unmountComponentAtNode() 触发

  • componentWillUnmount【常用于收尾工作,如关闭定时器、取消订阅】
  • componentDidMount()【执行一次,Vue 的 mounted 与它类似】
  • componentWillUnmount()【执行一次,Vue 的 beforeDestroy 与它类似】
  • render 【组件初始化 + 修改状态触发】
    在这里插入图片描述

新版本的生命周期图

getDerivedStateFromProps(props):要求返回 null 或 { state的东西 },并将这个作为渲染,null 不影响 constructor 的 state 对象
在这里插入图片描述

1.3.8. diffing 算法

比较的最小粒度是:标签

  • xxx
  • // xxx等内容变了,但是如果 input 没变,input 仍会复用

    经典面试题:vue/react 中的 key 有什么作用?(或遍历列表时,index 为什么不适合当 key)

    diff 规则:
    1.新的虚拟 DOM 中找到与 旧的虚拟 DOM 相同的 key的话

    • 如果内容也相同 ==> 复用(即不重新生成真实 DOM)
    • 内容不同 ==> 重新生成真实 DOM

    2.没有找到的话

    • 直接生成真实 DOM
    /*    这里用 index 作为 key 的过程:    原始数据:       {id: 1, name: '张三', age: 19},       {id: 2, name: '李四', age: 21}    原始虚拟 DOM:        
  • 张三-19
  • 李四-21
  • 新的数据: {id: 3, name: '王五', age: 32}, {id: 1, name: '张三', age: 19}, {id: 2, name: '李四', age: 21} 新的虚拟 DOM:
  • 王五-32
  • 张三-19
  • 李四-21
  • diff 比较:三个
  • 都需要重新渲染;因此用 id 作为 key,要高效很多 index 作为 key: 对于逆序添加、删除操作会存在重新渲染的问题--效率低;如果只是展示数据,是可以使用的 */class Person extends React.Component { state = { personArr: [ { id: 1, name: '张三', age: 19}, { id: 2, name: '李四', age: 21} ] } render() { return (

    人员信息如下:

      { this.state.personArr.map((person, index) => { return
    • { person.name} - { person.age}
    • })}
    ) } addPerson = () => { const { personArr} = this.state this.setState({ personArr: [{ id: personArr.length + 1, name: '王五', age: 32}, ...personArr] // 注意这里是放在了开始位置 }) }}
  • Tip:如果每个

    • 含有 输入当前的 name,必须要用 id 作为 key了

    在这里插入图片描述

    1.4. 使用脚手架创建 APP

    1.4.1. 安装脚手架

    npm i -g create-react-appcreate-react-app my-app # 创建一个 react 项目cd my-appnpm startnpm build # 打包npm eject # 暴露 webpack 配置,此操作不可逆

    还可以用 npm init react-app my-app 构件 react 应用

    1.4.2. 目录结构

    my-app├─ package-lock.json├─ package.json├─ public│  ├─ favicon.ico│  ├─ index.html│  ├─ logo192.png│  ├─ logo512.png│  ├─ manifest.json         # 应用加壳的配置文件,也就是当成手机应用的一些常用配置│  └─ robots.txt            # 爬虫规则文件 ├─ README.md└─ src├─ App.css├─ App.js                   # App 组件├─ App.test.js├─ index.css├─ index.js                 # 入口文件├─ logo.svg├─ reportWebVitals.js       # 记录页面性能└─ setupTests.js            # 组件测试

    1.4.3. 小案例 - todoList

    总结:

    • 样式模块化:最好使用 less 等预处理,避免全局污染
    • rcc(快速生成类组件代码片段)rfc(快速生成函数组件代码片段)
    • 如何确定数据放在哪个组件的 state 中呢? 如果多个组件都要用到,就放在它们的父组件
    • 子组件要给父组件传值:
      // 父组件通过 props 传递一个函数给子组件,子组件通过合适的时机调用,就可以在父组件拿到子组件的值
      add = (a,b) => { console.log(a,b) // 3,4}// 子组件中class Son extends React.Component{ render(){ // 这里调用即可 this.props.addCallback(3,4) }}
    • 两个有用的包
      npm i nanoid # 生成 UUIDnpm i prop-types # 限定标签类型和属性,用法同之前
    • CheckBox 的一个坑:defaultChecked只渲染一次,后面 checked 更新后,它不会再发生变化(类似地还有 defaultValue 和 value)

    1.5. ajax

    安装:

    npm i axios

    跨域问题:浏览器能发送请求,但是被 ajax 引擎拦截了响应,导致获取不到结果

    在这里插入图片描述

    解决办法:

    • 修改 package.json,增加如下语句,然后发送 ajax 请求【此时 ajax 请求的端口号变成 3000】

      "proxy": "http://localhost:5000"

    注意:并不是所有请求都会转发给服务器,因请求 localhost:3000/index.html 在本地存在这个文件,就不会发送;反之才会转发到服务器

    • 当后台启动两个服务器,方式一就不适用了;src 下新建一个 setupProxy.js 内容如下:
    const proxy = require('http-proxy-middleware')module.exports = function (app){       app.use(        proxy('/api1',{    // 遇见 /api1 才会触发代理            target:'http://localhost:5000', // 请求转发给的服务器地址            changeOrigin: true, // 控制服务器收到的请求头中 Host 字段的值(request headers)不加的话,就是 3000,服务端还是可能会限制请求            pathRewrite:{    '^/api1': ''} // 重写请求路径:/api1/students -> students 才能访问到数据(必须要写)        }),        proxy('/api2',{               target:'http://localhost:5001',            changeOrigin: true,            pathRewrite:{    '^/api2': ''}        })    )}
    // 调用getStudentData = ()=>{     axios.get('http://localhost:3000/api1/students').then(res=>{    // 触发 /api1 代理    console.log(res)  }).catch(err=>{       console.log(err)  })}getCarData = ()=>{     axios.get('http://localhost:3000/api2/cars').then(res=>{    // 触发 /api2 代理    console.log(res)  }).catch(err=>{       console.log(err)  })}

    1.5.1. 小案例 - 搜索 github 用户

    总结:

    1.连续解构赋值并重命名:const {target:{value:keyWord}} = e // target 拿不到,它只是一个中间量

    2.await 只能获取到 promise 成功的回调,失败的情况要通过 try-catch 捕捉

    3.需要先订阅 (componentWillMount),再发布,组件销毁需要取消订阅 (componentWillUnmount)

    try{     const res = await reqxxx()   }catch (e) {     console.log(e)}

    1.5.2. 兄弟组件通信(消息订阅-发布)

    npm i pubsub-js

    1.5.3. fetch 发送请求(了解)

    它是 promise 风格的、关注分离的、windows 自带的能够发送异步请求

    1.6. 路由

    1.6.1. 一些常识

    1.SPA的理解

    • 整个应用是一个页面、多个组件组成
    • 不会刷新页面、只会局部刷新
    • 数据通过 ajax 请求,并在前端异步展现

    2.路由的理解

    工作原理:点击导航区特殊的路由链接,路由链接就改变了浏览器的地址,前端路由器监视到浏览器地址发生变化,就会展现相应的组件

    • 一个路由就是一个映射关系(key-value,比如 path = /xx,value = XX组件/函数,后端路由就是 function,前端路由就是 component)

    1.6.2. react-router-dom

    npm i react-router-dom

    1.6.2.1. 基本使用

    import {    BrowserRouter as Router, Link, Route } from 'react-router-dom'class App extends React.Component{       render() {           return (                
    Home
    About { /* 注册路由:路由组件 */}
    ) }}

    路由组件和一般组件的区别:

    • 写法不同

    • 存放位置不同(pages/components)

    • 是否默认接收了props

      在这里插入图片描述

    1.6.2.2. 高亮路由

    //  默认类名是 active;对于多个路由还可以进一步封装
    Home
    Home
    class MyNavLink extends React.Component{ render() {
    }}

    Tip:MyNavLink 中标签体内容 ‘Home’ 等价于 <MyNavLink to="/home" children="home"/> 于是它以 children 属性传给了 中,仍然是正常工作的

    1.6.2.3. 路由匹配问题

    // 访问 /home 默认是会向后匹配的,会展示 Home、Abc 两个组件的内容,效率低下import {    Switch } from "react-route-dom";
    // 引入 Switch,避免向后再匹配

    1.6.2.4. 多级路由问题

    如果是多级路由+刷新会引起 css 样式丢失

    比如 react 创建的 SPA 里的 index.html有如下引用

    组件:

    在这里插入图片描述

    解决办法:

    • link 下 href 路径写成 /css/bootstrap.css
    • link 下 href 路径写成 %PUBLIC_URL/css/bootstrap.css%
    • 将 BrowserRouter 改成 HashRouter(不常用)

    1.6.2.5. 精准匹配与模糊匹配

    默认是模糊匹配,匹配方式是:

    • 首先看 <Route>的 path 属性
    • 再把<NavLink> 的 to 属性按照 /分割,并按照顺序对比
    // 能匹配
    // 它的 path 是 /home,而 to 拆分后,第一个就是 home,因此能够匹配
    // 它的 path 是 /home/profile,而 to 拆分后,第一个是 /home,故无法匹配
    // 开启精准匹配

    Tip: 对于二级目录不要开启精准匹配,否则不会匹配到子路由

    1.6.2.6. 路由重定向

    import { Redirect } from "react-route-dom";
    // 重定向到 home 页,一般写在最下方

    1.6.2.7. 嵌套路由

    • 注册子路由时要写上父路由的 path 值
    • 由于路由按照注册路由的顺序来的,因此二级目录匹配成功会显示一级目录那个组件

    1.6.2.8. 路由传参

    1.params 传参

    { news.title }
    // Detail 如下方法获取:传过来了 {history,location,match} 对象const { id,title} = this.props.match.params

    2.search 传参

    import qs from 'querystring';
    { news.title }
    // Detail 得通过 props.location.search ==> '?id=1&title=新闻1' 解析出来const { id,title} = qs.parse(this.props.location.search.replace('?','')) // qs.stringify(obj) 把 obj 转成 urlencoded 编码

    3.state 传参(非组件内的 state)

    优势在于它不会在路由中体现传参,必须要传一个对象

    { news.title }
    // Detail 中可以直接通过 props.location.state 拿到数据

    Tip:直接刷新页面,state 不会丢失,因为通过 history.location 记录着呢(但是HashRouter一刷新就会丢失,因为它没有用history记录)

    1.6.2.9. replace 和 push

    // 开启 replace 模式
    { news.title }

    1.6.2.10. 编程式导航

    this.props.history.replace('xxx') // params、searchthis.props.history.replace('xxx',{   id,title}) // state 编程式导航

    1.6.2.11. withRouter

    把一般组件也用上 router 的 api

    import {    withRouter } from 'react-router-dom'// 直接给 withRouter 传递一个普通组件即可,返回一个新组建export default withRouter(Header);

    1.6.2.12. BrowserRouter 和 HashRouter 区别

    底层原理不一样:

    • BrowserRouter 底层调用H5的 history API,不兼容IE9及其一下
    • HashRouter 因为含有 #(#后面的参数是不会发送到后端的),所以调用的时候能形成历史记录

    1.7. redux

    和 vuex 一样,用于集中式状态管理;能不用就不用

    1.7.1. 原理图

    流程:

    1.组件通过store.getState()获取初始化状态;store 自动调用 reducer 返回初始化状态

    2.组件通过store.dispatch({ type: 'xxx', data: 'xxx'})转发给 store,store 调用 reducer 更新状态

    3.组件监听 store 里面的状态发生了变化,就重新 render 获取最新结果

    在这里插入图片描述

    1.7.2. 完整用法

    action 是 Object(称为同步 action) 或 function(异步 action:因为只有函数体能够开启异步任务,store 会调用这个函数,拿到执行结果)

    npm i redux-thunk # 用于支持异步 action

    异步 action 不是必须要用的,看是否自己想在组件里发送请求

    在 redux 目录 创建 store.js、count_action.js、constant.js、count_reducer.js

    // store.jsimport {    createStore, applyMiddleware, combineReducers } from "redux"; // 专用于创建 storeimport thunk from 'redux-thunk' // 用于支持异步 actionimport countReducer from './reducers/count' // 需要引入 reducer 来创建import personReducer from "./reducers/person";const reducer = combineReducers({     sum:countReducer,  personArr:personReducer}) // 合并 reducer,才能在 state 里存下它们各自的数据const store =  createStore(reducer, applyMiddleware(thunk)) // 创建 store 的时候,需要使用这个中间件export default store // 暴露 store
    // constant.js// 定义 redux 中所需要使用的常量,防止写错export const INCREMENT = 'increment'export const DECREMENT = 'decrement'export const ADD_USER = 'add_user'
    // count_reducer.jsimport {   DECREMENT, INCREMENT} from "./constant";const initState = 0export default function sumReducer(preState = initState, action){    // reducer 可以接参数,说明是个函数。主要用于初始化和更新 store  const {   type,data} = action  switch (type) {       case INCREMENT:      return preState+data    case DECREMENT:      return preState-data    default:      return preState  }}
    // count_action.jsimport {   DECREMENT, INCREMENT} from "./constant";export const createIncrementAction = data => ({   type: INCREMENT, data})export const createDecrementAction = data => ({   type: DECREMENT, data})export const createIncrementAsyncAction = (data, time) => {       // 异步 action,返回一个函数,默认是 store 来执行,因此会传入 dispatch  return (dispatch) => {       setTimeout(() => {         dispatch(createIncrementAction(data))    }, time)  }}

    组件中通过如下方式调用

    import React, {   Component} from 'react';import './index.css'import store from '../../redux/store'import {       createIncrementAction,    createIncrementAsyncAction} from '../../redux/count_action'class ReduxBasic extends Component {       selRef = React.createRef()    render() {           return (            
    { /* getState 能够获取到初始值或最新值(此时reducer未合并,getState就能拿到里面的值) */}

    sum: { store.getState()}

     
     
    ); } componentDidMount() { // 监听 store 数据发送改变,调用 render store.subscribe(() => { console.log('数据发生改变时触发') this.setState({ }) }) } add = () => { const val = parseInt(this.selRef.current.value) store.dispatch(createIncrementAction(val)) } addAsync = () => { const val = parseInt(this.selRef.current.value) store.dispatch(createIncrementAsyncAction(val, 3000)) }}export default ReduxBasic;

    1.7.3. react-redux

    在这里插入图片描述

    1.7.3.1. 基本使用

    • UI 组件不能有任何 redux 的 api,只负责页面的呈现、交互;容器组件负责和 redux 通信,将结果交给 UI 组件
    • 如何创建一个容器组件呢?==> connect(mapStateToProps,mapDispatchToProps)(UI组件),参数一是映射 state,返回一个对象;参数二是映射操作状态的方法,返回一个对象
    • 容器组件的 store 靠父组件通过 props 传给它
    • reducer/ 文件夹里还可新建 index.js,用于合并所有的 reducers,再暴露出去

    Tip:mapDispatchToProps 也可以是一个对象(公司常用)react-redux 自动调用了

    1.7.3.2. 优化版

    之前的 redux 目录中的文件不用变;新建 src/containers/Count/index.jsx,内容如下:

    import {    connect } from 'react-redux' // 用于连接 UI 和 reduximport {    createIncrementAction, createIncrementAsyncAction } from '../../redux/sum_action'import React, {   Component} from 'react';// 创建容器组件所连接的 UI 组件class CountUI extends Component {       selRef = React.createRef()    render() {           return (            

    sum: { this.props.sum}

    count 中读取到的人的个数:{ this.props.personArr.length}

     
     
    ); } add = () => { const val = parseInt(this.selRef.current.value) this.props.createIncrementAction(val) } addAsync = () => { const val = parseInt(this.selRef.current.value) this.props.createIncrementAsyncAction(val,3000) }}// 创建并暴露一个 CountContainer 组件// 因为 reducer 合并了,这里要想拿到值,就得通过 state.xxxexport default connect((state) => ({ sum: state.sum, personArr: state.personArr}),{ createIncrementAction, createIncrementAsyncAction})(CountUI)// 相当于
    这里是传值给 UI,因此要写一个对象才能有 key-value// 因为是 react-redux 调用这个函数,因此它把 const state = store.getState() 的结果 state作为参数传进去了// 参数二就是传一些方法的键值对给 CountUI// 因为 react-redux 能够自动调用 dispatch,因此这里我们传 action 就可以了
    import CountContainer from "./containers/Count";// app.js 引入这个 container
    import {    Provider } from "react-redux";import store from "./redux/store";// app 里所有用到 store 的组件都能拿到,就不需要每写一个组件传一个 store 进去了ReactDOM.render(        
    , document.getElementById('root'));

    1.7.3.3. 纯函数

    是一类函数:只要同样的输入(实参),就会得到同样的输出(返回值)

    1.不能改写参数数据

    function test(a){       a=10 // ERROR:改写了参数}

    2.不能产生任何副作用,如网络请求、文件读写啥的(反正不靠谱的都不行)

    3.不能调用 Date.now 或 Math.random 等不纯的方法(每次运行结果都不一样)

    1.7.3.4. redux 开发者工具

    1.谷歌网上应用商店 搜索 redux

    2.安装三方包

    npm i redux-devtools-extension

    3.修改 store.js

    import {    composeWithDevTools } from 'redux-devtools-extension'const store =  createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))

    1.7.3.5. 服务器运行

    npm i serve -gserve build # build 是打包后的文件夹

    1.8. 扩展知识

    1.8.1. setState

    • setState 是个同步方法,但是引起的后续动作是异步的

    • 一般来说,如果不依赖原来的状态,就用对象式;依赖的话,就用函数式

      在这里插入图片描述

      在这里插入图片描述

    // 函数式 setStateadd = () => {    // prop 可用可不用    this.setState((state,prop) => ({    sum: state.sum + 1 }))}

    1.8.2. 路由懒加载

    // Suspense 主要用于当请求第一个懒加载路由特别慢的时候,呈现一个默认的路由,以免白屏import {    lazy, Suspense} from 'react'const Home = lazy(()=>import('./Home'))class App extends React.Component{       render() {           return (                // ...            

    loading....

    }> // 注册路由
    ) }}

    1.8.3. hooks

    hooks 可以让用户在函数式组件使用 state,以及其他的 react 特性(如生命周期函数)

    function App() {    // 调用 1+N 次    // useState 返回两个值,一个 state,一个操作 state 的方法    const [state, setState] = React.useState({   sum: 0, name: 'default'}) // 第一次调,就缓存了state,不会因为再次调用而覆盖成0    function add() {           setState({   ...state, sum: state.sum + 1})    }    function changeName() {           setState({   ...state,name: 'Tom'})    }    return (        

    当前 sum: { state.sum}

    当前 name: { state.name}

    )}export default App;
    function App() {    // 调用 1+N 次    const [state, setState] = React.useState({    sum: 0 })    console.log('state:', state)    // 1.不传入第二个参数的话,就相当于 didUpdate,会执行 1+N 次(监测 state 所有状态)    // 2.传入第二个参数,就相当于给它说只监视某个值,一旦这个值发生改变就会触发这个回调函数    // 3.第二个参数是 []时,相当于 didMount,会执行 1 次    // 4.useEffect 返回值相当于 componentWillUnmount 函数    React.useEffect(() => {    // 让函数式组件有生命周期        const timer = setInterval(() => {               setState((state) => ({    sum: state.sum + 1 }))        }, 1000)        return () => {               window.clearInterval(timer)        }    }, [])      return (        

    当前 sum: { state.sum }

    ) function destroy() { // 卸载组件的回调 ReactDOM.unmountComponentAtNode(document.getElementById('root')) }}export default App;
    function App() {    // 调用 1+N 次    let inputRef = React.useRef() // React.createRef 也可以    return (        
    ) function showName() { alert(inputRef.current.value) }}export default App;

    1.8.4. Fragment

    用于包裹标签,这样项目就不会生成多余的 div 标签了,最多添加一个key属性,其他属性会丢失

    // Demo.jsximport {   Fragment} from "react";// 它起到一个包裹的作用,但是不会生成额外的 div 标签,不然就只有用 div 来进行包裹// 也可以用 <>空标签包裹,但是它不能传 key 属性了

    1.8.5. Context

    适用于祖组件和后代组件之间通信【一般用于封装插件】

    import React, {   Component} from 'react';const MyContext = React.createContext() // 1.首先创建一个 contextconst {    Consumer } = MyContextclass GrandFather extends Component {       state = {           username: 'Tom'    }    render() {           return (            

    我是 GrandFather 组件

    { /* 2.通过 Provider 提供 value,就可以让 Father 所有的后代组件都可使用 username */}
    ); }}class Father extends Component { render() { return (
    ); }}class Son extends Component { // 3.要想使用,就得声明,才能够从 this.context 获取 static contextType = MyContext render() { return (

    我是 son 组件,我接受到的 username:{ this.context.username}

    ); }}/*function Son(){ return (

    我是 son 组件,我接受到的 username:
    { value => value.username }

    );}*/export default GrandFather;

    1.8.6. 组件优化

    Component 存在两个问题:

    • 只要调用 setState(),即使不修改状态(传入空对象),还是会重新执行 render()
    • 只要当前组件 render(),那么子组件(即使没通过 props 使用父组件任何东西)就会重新 render(),效率低下
      原因:shouldComponentUpdate() 一直默认返回的是 true
      高效的做法是:只有 state 或 props 数据发生改变才调用 render()
      写法:
    import {   PureComponent} from 'react'class App extends PureComponent{       // 这个类重写了 shouldComponentUpdate(),react 又是进行浅对比,就会存在一个小问题  state = {         username: 'yuwan'  }  changeName = () => {         const obj = this.state      obj.username = 'xxx' // this.state === obj ==> true      this.setState(obj)  // 这里是无法更新的,因为 obj 地址还是和 this.state 相同  }}

    1.8.7. render props

    解释:这和 vue 的插槽作用差不多,预置某个位置,编写代码的时候,再填充进去

    import React, {   Component} from 'react';class App extends Component {       render() {           return (            
    { /* 2.编码的时候,确定是父子关系 */}
    }/>
    ); }}export default App;class Parent extends Component { state = { name: 'yuwan' } render() { return (

    我是父组件

    { /* 1.执行 render 那个函数,就能拿到 Child 组件【这就是 vue 插槽技术】 */} { this.props.render(this.state.name) }
    ) }}class Child extends Component { render() { return (

    我是子组件,收到的 name = { this.props.name}

    ) }}

    1.8.8. 错误边界

    解释:子组件出问题就在子组件中显示,而不要影响到整个应用的运行

    方式:在容易发送错误的组件的父组件进行处理

    注意点:

    • 在生产环境才会有效,开发环境效果看不出来
    • 只会捕获生命周期函数的逻辑,若果是自定义函数是不得行的
    import React, {   Component} from 'react';class App extends Component {       render() {           return (            
    ); }}export default App;class Parent extends Component { state = { hasError:false } static getDerivedStateFromError(err){ // 一旦子组件发生错误,就会在这里进行处理,会自动更新 state return { hasError:true } } componentDidCatch(error, errorInfo) { console.log(error) // 这里可以统计出错次数,反馈给服务器,以便于调试 } render() { return (

    我是父组件

    { this.state.hasError?

    糟糕,出问题了!

    :
    }
    ) }}class Child extends Component { state = { userList:'' } render() { return (

    我是子组件

    { this.state.userList.map(user=>{ return

    { user.name}

    })}
    ) }}

    1.8.9. 组建通信(总结)

    • props(适用于父子组件)
    • 消息订阅-发布:如 pub-sub(适用于兄弟组件、祖孙组件)
    • 集中式管理:如 redux(适用于兄弟组件、祖孙组件)
    • context:如 生产者-消费者(适用于插件封装)

    转载地址:https://blog.csdn.net/qq_39811414/article/details/115721088 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

    上一篇:Git知识总结
    下一篇:vue 官方文档 - 自定义事件

    发表评论

    最新留言

    路过按个爪印,很不错,赞一个!
    [***.219.124.196]2023年05月20日 06时20分59秒