本文共 28335 字,大约阅读时间需要 94 分钟。
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 = ()ReactDOM.render(VDOM, document.getElementById('root')){ title}
{ // 这里是不能写 forEach 来遍历,因为它没有返回值;写 map 才能动态生成出来 frameworks.map((item, index) => { return
- { item}
}) }{
}
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 算法
比较的最小粒度是:标签
经典面试题: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:
人员信息如下:
- { this.state.personArr.map((person, index) => { return
- { person.name} - { person.age} })}
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 样式丢失
组件:
解决办法:
- 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就能拿到里面的值) */}); } 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;sum: { store.getState()}
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 (); } 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)// 相当于sum: { this.props.sum}
count 中读取到的人的个数:{
this.props.personArr.length}这里是传值给 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 ()}export default App;当前 sum: { state.sum}
当前 name: { state.name}
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 () function destroy() { // 卸载组件的回调 ReactDOM.unmountComponentAtNode(document.getElementById('root')) }}export default App;当前 sum: { state.sum }
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 (); }}class Father extends Component { render() { return (我是 GrandFather 组件
{ /* 2.通过 Provider 提供 value,就可以让 Father 所有的后代组件都可使用 username */} ); }}class Son extends Component { // 3.要想使用,就得声明,才能够从 this.context 获取 static contextType = MyContext render() { return ( ); }}/*function Son(){ return (我是 son 组件,我接受到的 username:{ this.context.username}
);}*/export default GrandFather;我是 son 组件,我接受到的 username:
{ value => value.username }
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 (}/> ) }}class Child extends Component { render() { return (我是父组件
{ /* 1.执行 render 那个函数,就能拿到 Child 组件【这就是 vue 插槽技术】 */} { this.props.render(this.state.name) }我是子组件,收到的 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 ( ) }}class Child extends Component { state = { userList:'' } render() { return (我是父组件
{ this.state.hasError?糟糕,出问题了!
:} ) }}我是子组件
{ 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 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!