耗时一个月,React 知识点万字大总结(超全超基础)

React特点

React是一个将数据渲染为 HTML 视图 的 js 库

简单看来,React 框架主要功能体现在前端 UI 页面的渲染,包括性能优化以及操作简化等等方面。站在 mvc 框架的角度来看,React 操作 view 层的功能实现。

  • 采用组件化模式、声明式编码、函数式编程,提高开发效率和组件复用性
  • 它遵循从高阶组件到低阶组件的单向数据流。
  • 在 React Native 中可以用 react 预发进行安卓、ios 移动端开发
  • 使用虚拟 dom 和 diff 算法,尽量减少与真实 dom 的交互,提高性能

React 与 Vue 对比

相同点:

  1. 都使用 Virtural DOM
  2. 都使用组件化思想,流程基本一致
  3. 都是响应式,推崇单向数据流
  4. 都有配套框架,Vue 有 Vue-routerVuex,而 React 有 React-routerReact-Redux
  5. 都有成熟的社区,都支持服务端渲染

不同点:

模版语法不同

Vue 推荐编写近似常规 HTML 的模板进行渲染,而 React 推荐 JSX 的书写方式。

核心思想不同

Vue推崇灵活易用(渐进式开发体验),数据可变,双向数据绑定(依赖收集)。

React推崇函数式编程(纯组件),数据不可变以及单向数据流。

组件实现不同

Vue源码实现是把options挂载到Vue核心类上,然后再new Vue({options})拿到实例。

React内部使用了四大组件类包装VNode,不同类型的 VNode 使用相应的组件类处理,职责划分清晰明了。

React 类组件都是继承自 React.Component 类,其 this 指向用户自定义的类,对用户来说是透明的。

响应式原理不同

Vue依赖收集,自动优化,数据可变。递归监听 data 的所有属性,直接修改。当数据改变时,自动找到引用组件重新渲染。

React基于状态机,手动优化,数据不可变,需要 setState 驱动新的 State 替换老的 State。当数据改变时,以组件为根目录,默认全部重新渲染。

diff 算法不同

两者流程思维上是类似的,都是基于两个假设(使得算法复杂度降为 O (n)):

  1. 不同的组件产生不同的 DOM 结构。当 type 不相同时,对应 DOM 操作就是直接销毁老的 DOM,创建新的 DOM。
  2. 同一层次的一组子节点,可以通过唯一的 key 区分。

但两者源码实现上有区别:

Vue 基于 snabbdom 库,它有较好的速度以及模块机制。Vue Diff使用双向链表,边对比,边更新DOM。

React主要使用 diff 队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。

事件机制不同

Vue原生事件使用标准Web事件。Vue 组件自定义事件机制,是父子组件通信基础。

React原生事件被包装,所有事件都冒泡到顶层 document 监听,然后在这里合成事件下发。基于这套,可以跨端使用事件机制,而不是和 Web DOM 强绑定。

React 组件上无事件,父子组件通信使用 props。

React 与 Angular 对比

Angular 是一个成熟的 MVC 框架,带有很多特定的特性,比如服务、指令、模板、模块、解析器等等。

React 是一个非常轻量级的库,它只关注 MVC 的视图部分。

Angular 遵循两个方向的数据流,而 React 遵循从上到下的单向数据流。

React 在开发特性时给了开发人员很大的自由,例如,调用 API 的方式、路由等等。

我们不需要包括路由器库,除非我们需要它在我们的项目。

JSX语法

JSX 是 javascript 的语法扩展。它就像一个拥有 javascript 全部功能的模板语言。它生成 React 元素,这些元素将在 DOM 中呈现。React 建议在组件使用 JSX。在 JSX 中,我们结合了 javascript 和 HTML,并生成了可以在 DOM 中呈现的 react 元素。

  1. 定义虚拟 dom 时不要用引号

  2. 标签中引入 js 表达式要用 {}

  3. 如果在 jsx 要写行内样式需要使用 style={ {coler:red}} 形式

  4. 样式的类名指定不能写 class,要写 className;

  5. 只有一个根标签

  6. 标签必须闭合

  7. 标签首字母
    ①若小写字母开头,则会将该标签转为 html 同名标签,如果没找到,则会报错;
    ②若大写字母开头,则会认为是组件,它就会去找对应的组件,如果没找到,就会报组件未定义的错误;

实质:JSX 通过 babel 编译,而 babel 实际上把 JSX 编译给 React.createElement() 调用

  • React.createElement()即 h 函数,返回 vnode
  • 第一个参数可能是组件也可能是 html tag ,如果是组件,首字母必须大写
const imgElem = <div>
   <p>some text</p>
	 <img src={
    
    imgUrl} />
 </div>
      
// 经过 babel 编译后

var imgElem = 
React.createElement("div",null,
React.createElement("p",null,"some text"),React.createElement("img",{
    
    src:imgUrl}));

state状态

state 是组件实例对象最重要的属性,必须是对象的形式

组件被称为状态机,通过更改 state 的值来达到更新页面显示(重新渲染组件)

组件 render 中的 this 指的是组件实例对象

组件自定义方法中的 this 为 undefined,怎么解决?

①将自定义函数改为表达式 + 箭头函数的形式(推荐)

②在构造器中用 bind()强制绑定 this

状态数据不能直接赋值,需要用 setState()

setState()有同步有异步,基本上都是异步更新,自己定义的DOM事件里setState()是同步的

同步异步原理:看是否能命中 batchUpdate 机制,就是判断 isBatchingUpdates,true 为同步,false 为异步

class ListDemo extends React.component{
    
    
  constructor(props){
    
    ...}
  render(){
    
    ...}
  increase = () =>{
    
    
    // 开始:处于 batchUpdate
    // isBatchingUpdates = true
    this.setState({
    
    
      count : this.state.count + 1
    })
    // 结束
    // isBatchingUpdates = false
  }
}
                     
class ListDemo extends React.component{
    
    
  constructor(props){
    
    ...}
  render(){
    
    ...}
  increase = () =>{
    
    
    // 开始:处于 batchUpdate
    // isBatchingUpdates = true
    setTimeout(() => {
    
    
      // 由于异步,所以此时 isBatchingUpdates 是 false
      this.setState({
    
    
      	count : this.state.count + 1
    	})
  	})
    // 结束
    // isBatchingUpdates = false
  }
}

能命中 batchUpdate 机制:生命周期(和它调用的函数)、React 中注册的事件(和它调用的函数),其实就是 React 可以“管理”的入口

不能命中 batchUpdate 机制:setTimeout/setInterval等(和它调用的函数)、自定义 DOM 事件(和它调用的函数),其实就是 React “管不到”的入口,因为不是在 React 中注册的

state 异步更新的话,更新前会被合并:setState()传入对象会被合并(类似于Object.assgin),传入函数不会被合并

// 传入对象会被合并,每次+1
this.setState({
    
    
  count:this.state.count + 1
})
this.setState({
    
    
  count:this.state.count + 1
})
this.setState({
    
    
  count:this.state.count + 1
})

// 传入函数不会被合并,每次+3
this.setState((prevState,proprs) => {
    
    
  return{
    
    
    count:prevState.count + 1
  }
})
this.setState((prevState,proprs) => {
    
    
  return{
    
    
    count:prevState.count + 1
  }
})
this.setState((prevState,proprs) => {
    
    
  return{
    
    
    count:prevState.count + 1
  }
})

props属性

props 就是在调用组件的时候在组件中添加属性传到组件内部去使用

  • 每个组件都会有 props 属性
  • 组件标签的所有属性都保存在 props
  • 组件内部不能改变外部传进来的 props 属性值

接下来如果想对传入的 props 属性进行一些必传、默认值、类型的校验,就需要用到一个 prop-types 库

下载:npm i prop-types --save
引入:import PropTypes from ‘prop-types’

class Person extends React.Component{
    
    
  //对标签属性进行类型、必要性的限制
  static propTypes = {
    
    
    name:PropTypes.string.isRequired,//限制name必传,且为字符串
    sex:PropTypes.string,//限制sex为字符串
    age:PropTypes.number,//限制age为数字
    speak:PropTypes.func,//限制speak为函数
  }
  //指定默认标签属性值
  static defaultProps = {
    
    
    sex:'男', //sex默认值为男
    age:18 //age默认值为18
  }
}

refs 属性

  • 字符串形式的 ref(这种方式已过时,不推荐使用,因为效率低)

refs 是组件实例对象中的属性,它专门用来收集那些打了 ref 标签的 dom 元素

比方说,组件中的 input 添加了一个 ref=“input1”

那么组件实例中的 refs 就 ={input1:input(真实 dom)}

这样就可以通过 this.refs.input1 拿到 input 标签 dom 了

就不需要想原生 js 那样通过添加属性 id,

然后通过 document.getElementById (“id”) 的方式拿

  • 回调函数
class Demo extends React.Component{
    
    
  showData = () => {
    
    
    const {
    
    input1} = this
    alert(input1.value)
  }
  render(){
    
    
    return{
    
    
      <input ref={
    
    c => this.input1 = c}  type="text" />
      <button onClick={
    
    this.showData}>点我展示数据</button>
    }
  }
}

直接让 ref 属性 = 一个回调函数,为什么这里说是回调函数呢?

因为这个函数是我们定义的,但不是我们调用的

是 react 在执行 render 的时候,看到 ref 属性后跟的是函数,他帮我们调用了

然后把当前 dom 标签当成形参传入

所以就相当于把当前节点 dom 赋值给了 this.input1,那这个 this 指的是谁呢?

不难理解,这里是箭头函数,本身没有 this 指向,所以这个 this 得看外层的

该函数外层是 render 函数体内,所以 this 就是组件实例对象

所以 ref={c=>this.input1=c} 意思就是给组件实例对象添加一个 input1

最后要取对应节点 dom 也直接从 this(组件实例中取)

  • createRef

createRef() 方法是 React 中的 API,它会返回一个容器,存放被 ref 标记的节点,但该容器是专人专用的,就是一个容器只能存放一个节点;

当 react 执行到 div 中第一行时,发现 input 节点写了一个 ref 属性,又发线在上面创建了 myRef 容器,所以它就会把当前的节点存到组件实例的 myRef 容器中

注意:如果你只创建了一个 ref 容器,但多个节点使用了同一个 ref 容器,则最后的会覆盖掉前面的节点,所以,你通过 this.ref 容器.current 拿到的那个节点是最后一个节点。

class Demo extends React.Component{
    
    
  //React.createRef调用后可以返回一个容器,该容器可以存储被ref所标示的节点,该容器是专人专用的
  myRef = React.createRef()
  //展示输入框的数据
  showData = () => {
    
    
    alert(this.myRef.current.value)
  }
  render(){
    
    
    return{
    
    
      <div>
      	<input ref={
    
    this.myRef}  type="text" />
        <button onClick={
    
    this.showData}>点我展示数据</button>
      </div>
    }
  }
}

事件处理

  1. 通过 onXxxx 属性指定事件处理函数(小驼峰形式)
  2. 通过 event.target 可以得到发生事件的 dom 元素
  3. 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

在原生 DOM 中,我们可以通过返回 false 来阻止默认行为,但是这在 React 中是行不通的,在 React 中需要明确使用 preventDefault() 来阻止默认行为。

事件回调函数里的 event 是经过 React 特殊处理过的(遵循 W3C 标准),所以可以放心地使用它,而不用担心跨浏览器的兼容性问题。

注意:在使用事件回调函数的时候,需要特别注意 this 的指向问题。

因为在 React 里,除了构造函数和生命周期钩子函数里会自动绑定 this 为当前组件外,其他的都不会自动绑定 this 的指向为当前组件,因此需要我们自己注意好 this 的绑定问题。

通常而言,在一个类方式声明的组件里事件回调,需要在组件的 constructor 里绑定回调方法的 this 指向。

render函数

当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行

当父组件的 render 函数重新执行时,子组件的 render 函数也会重新执行

虚拟DOM

  1. 本质上其实就是一个 object 对象;
  2. 虚拟 dom 上的属性比较少,真实 dom 属性多,因为虚拟 dom 只在 react 内部使用,用不到那么多的属性
  3. 虚拟 dom 最终会被 react 转换成真实 dom,呈现再页面上

大致过程:

state数据

jsx模版

数据+模版 结合,生成虚拟DOM

(虚拟DOM就是一个JS对象,用它来描述真实的DOM)(损耗了性能)

用虚拟DOM的结构生成真实的DOM来显示

state发生改变

数据+模版 生成新的虚拟DOM(极大提升了性能)

比较原始虚拟DOM和新的虚拟DOM的区别,找差异(极大提升了性能)

直接操作DOM,改变内容

优点:

性能提升

使得跨端应用得以实现

虚拟 DOM 中的 key 的作用:

当状态中的数据发生改变时,react 会根据新数据生成新虚拟 DOM

随后 react 会进行新虚拟 DOM和旧虚拟 DOM的 diff 算法比较

若旧 DOM中找到了与新 DOM相同的 key,则会进一步判断两者的内容是否相同

如果也一样,则直接使用之前的真实 DOM,如果内容不一样,则会生成新的真实 DOM,替换掉原先的真实 DOM

若旧 DOM中没找到与新 DOM相同的 key,则直接生成新的真实 DOM,然后渲染到页面

不用 index 作为 key 的原因:

若对数据进行逆序添加、逆序删除等破坏顺序的操作时会产生不必要的真实 DOM 更新,造成效率低下

如果结构中还包含输入类的 dom,会产生错误 dom 更新,出现界面异常

diff算法

react 中如果某个组件的状态发生改变,react 会把此组件以及此组件的所有后代组件重新渲染

不过重新渲染并不代表会全部丢弃上一次的渲染结果,react 还是会通过 diff 去比较两次的虚拟 dom 最后 patch 到真实的 dom

diff 算法只比较同一层级,不跨级比较

tag 不相同则直接删掉重建,不再深度比较;tagkey 两者都相同,则认为是相同节点,也不再深度比较

虽然如此,如果组件树过大,diff 其实还是会有一部分的开销

因此react 内部通过 fiber 优化 diff 算法,外部建议开发者使用 SCUpureComponent

生命周期

componentWillMount() //在组件即将被挂载到页面的时候自动执行

componentDidMount() //在组件被挂载到页面之后,自动被执行

shouldComponentUpdate(nextProps,nextState){
    
      //组件被更新之前自动被执行,默认返回true
  if(nextState.count !== this.state.count){
    
    
    return true //可以渲染
  }
  return false // 不可以渲染
}

// shouldComponentUpdate 返回true则执行,返回false不执行
componentWillUpdate() //在组件被更新之前,自动执行

componentDidUpdate() //在组件更新完成之后,自动执行

componentWillReceiveProps() //一个组件要冲父组件接受参数

//只要父组件的render函数被重新执行了,子组件的这个生命周期函数就会被执行

componentWillUnmount() //当这个组件即将被从页面中剔除的时候会被执行

在这里插入图片描述

挂载时

先执行构造器(constructor)

组件将要挂载(componentWillMount)

组件挂载渲染(render)

组件挂载完成(componentDidMount)

组件销毁(componentWillUnmount)

组件内部状态更新:

组件是否应该更新(shouldComponentUpdate)

组件将要更新(componentWillUpdate)

组件更新渲染(render)

组件更新完成(componentDidUpdate)

父组件重新 render:

调用组件将要接收新 props(componentWillReceiveProps)

组件是否应该更新(shouldComponentUpdate)

组件将要更新(componentWillUpdate)

组件更新渲染(render)

组件更新完成(componentDidUpdate)

注:只有在父组件状态发生改变了,重新调用 render 时才会调用子组件的 componentWillReceiveProps 函数,父组件第一次引用子组件不会调用

脚手架工具

使用 create-react-app(脚手架工具)创建一个初始化项目

1、下载脚手架工具:npm i -g create-react-app

2、创建引用:create-react-app my-app

3、运行应用:cd my-app(进入应用文件夹),npm start(启动应用)

如果控制台报了You are running create-react-app 5.0.0, which is behind the latest release (5.0.1).

那么先查一下npm 的版本,如果大于 5.2 ,就使用 npx create-react-app@latest train-ticket

然后是运行 npm run eject

因为在 package.json 中,只有三个依赖,分别是 react,react-dom,react-scripts

依赖为什么这么少?是因为像 webpack,babel 等等都是被 creat react app 封装到了 react-scripts 这个项目当中,包括基本启动命令,都是通过调用 react-scripts 这个依赖下面的命令进行启动的。

creat react app 搭建出来的项目默认支持这 4 种命令:start 以开发模式启动项目,build 将整个项目进行构建,test 进行测试,eject会将原本 creat react app 对 webpack,babel 等相关配置的封装弹射出来。

如果我们要将 creat react app 配置文件进行修改,现有目录下是没有地方修改的,此时,我们就可以通过 eject 命令将原本被封装到脚手架当中的命令弹射出来,然后就可以在项目的目录下看到很多配置文件。

npm run eject 是个单向的操作,一旦 eject ,npm run eject 的操作是不可逆的。

Redux

Redux = Reducer + Flux

Redux 是 React 的一个状态管理库,它基于 flux。

Redux 简化了 React 中的单向数据流。 Redux 将状态管理完全从 React 中抽象出来。

它是专门做状态管理的 js 库,不是 react 插件库

作用:集中式管理 react 应用中多个组件共享的状态

需求场景:

某个组件的状态需要让其他组件也能拿到

一个组件需要改变另一个组件的状态(通信)

在这里插入图片描述

Redux设计和使用的三项原则:

store是唯一的、

只有store能够改变自己的内容、

Reducer必须是纯函数。

纯函数指的是,给定固定的输入,就一定会有固定的输出,而且不会有任何副作用。

redux流程原理

在这里插入图片描述

在 React 中,组件连接到 redux ,如果要访问 redux,需要派出一个包含 id 和负载 (payload) 的 action。action 中的 payload 是可选的,action 将其转发给 Reducer。

reducer 收到 action 时,通过 switch...case 语法比较 actiontype。 匹配时,更新对应的内容返回新的 state

Redux 状态更改时,连接到 Redux 的组件将接收新的状态作为 props。当组件接收到这些 props 时,它将进入更新阶段并重新渲染 UI。

reducer可以接受state,但是绝不能修改state。

ActionTypes拆分:

通过constants创建常量,用于检测定位bug位置。

如果是直接使用字符串,如果写错不报异常,没有办法定位错误位置。

使用ActionCreator统一创建action:

提升代码可读性和方便前端自动化测试。

无状态组件

当定义一个UI组件,只负责渲染,没有任何逻辑操作的时候,

建议使用无状态函数,性能提升,因为不用有生命周期函数这种。

React-redux

react-redux 将 react 组件划分为容器组件展示组件

  • 展示组件:只是负责展示 UI,不涉及到逻辑的处理,数据来自父组件的 props;
  • 容器组件:负责逻辑、数据交互,将 state 里面的数据传递给展示组件进行 UI 呈现

Provider连接store ,它内部的组件都可以获取到store

<Provider store={
    
    store}>
	<TodoList />
</Provider>

connect方法让子组件和store连接

TodoList是UI组件,connect将业务逻辑和UI组件结合,返回一个容器组件

//在TodoList.js里
export default connect(mapStateToProps,null)(TodoList);

mapStateToProps:此函数将 state 映射到 props 上,因此只要 state 发生变化,新 state 会重新映射到 props。 这是订阅 store 的方式。

mapDispatchToProps:此函数用于将 actionCreators 绑定 props

怎么做连接,就是用mapStateToProps做映射

//在TodoList.js里
const mapStateToProps = (state) => {
    
    
  return {
    
    
    inputValue: state.inputValue
  }
}

mapDispatchToProps,解决组件如何对store里的数据做修改

可以让props里的方法调用store.dispatch去操作store里的数据

//store.dispatch , props
const mapDispatchToProps = (dispatch) => {
    
    
  return {
    
    
    changeInputValue(e){
    
    
      const action = {
    
    
        type:'change_input_value',
        value:e.target.value
      }
      dispatch(action);
    }
  }
}
export default connect(mapStateToProps,mapDispatchToProps)(TodoList);

Redux-thunk中间件

redux 里,action 仅仅是携带了数据的普通 js 对象。

actionCreator 返回的值是这个 action 类型的对象。

然后通过 store.dispatch 进行分发,同步的情况下一切都很完美,但是 reducer 无法处理异步的情况。

那么我们就需要在 action 和 reducer 中间架起一座桥梁来处理异步。

这就是 middleware 中间件,就是指在 action 和 store 之间。

使得可以在action里面写异步的代码。

其实就是对store的dispatch方法升级,本来只能接受一个对象,现在也可以接受一个函数。

在这里插入图片描述

redux 开发者工具

  1. 下载开发者工具 Redux DevTools

  2. 下载完后右上方的插件图标还是不会亮的,因为它还识别不了你写的 redux,所以还需要下载一个库(redux-devtools-extension)

  3. 然后在 store 文件中引入该库文件 import {composeWithDevTools} from redux-devtools-extension

  4. 然后在createStore()第二个参数位置调用 composeWithDevTools(),将之前的中间件传到该方法中export default createStore(allReducer,composeWithDevTools(applyMiddleware(thunk)))

Redux-saga中间件

redux-saga 提供了一些辅助函数,用来在一些特定的 action 被发起到 Store 时派生任务,先来看一下两个辅助函数:takeEverytakeLatest

import {
    
     takeEvery } from 'redux-saga'
// Generator生成器函数
function* watchFetchData() {
    
    
  yield takeEvery("FETCH_REQUESTED", fetchData)
}

takeEvery 函数可以使用下面的写法替换

function* watchFetchData() {
    
    
   while(true){
    
    
     yield take('FETCH_REQUESTED');
     yield fork(fetchData);
   }
}

takeEvery 允许多个 fetchData 实例同时启动。在某个特定时刻,我们可以启动一个新的 fetchData 任务, 尽管之前还有一个或多个 fetchData 尚未结束。

如果我们只想得到最新请求的响应(例如,始终显示最新版本的数据),我们可以使用 takeLatest 辅助函数

import {
    
     takeLatest } from 'redux-saga'

function* watchFetchData() {
    
    
  yield takeLatest('FETCH_REQUESTED', fetchData)
}

和 takeEvery 不同,在任何时刻 takeLatest 只允许执行一个 fetchData 任务,并且这个任务是最后被启动的那个,如果之前已经有一个任务在执行,那之前的这个任务会自动被取消。

redux-saga 框架提供了一些创建 effect 的函数,大概介绍几个常用的:

  • take(pattern)
  • put(action)
  • call(fn, …args)
  • fork(fn, …args)

take 函数可以理解为监听未来的 action,它创建了一个命令对象,告诉 middleware 等待一个特定的 action, Generator 会暂停,直到一个与 pattern 匹配的 action 被发起,才会继续执行下面的语句,也就是说,take 是一个阻塞的 effect

put 函数是用来发送 action 的 effect,可以简单的把它理解成为 redux 框架中的 dispatch 函数,当 put 一个 action 后,reducer 中就会计算新的 state 并返回。 注意:put 也是阻塞 effect

call 函数就是可以调用其他函数的函数,它命令 middleware 来调用 fn 函数, args 为函数的参数,注意:fn 函数可以是一个 Generator 函数,也可以是一个返回 Promise 的普通函数,call 函数也是阻塞 effect

fork 函数和 call 函数很像,都是用来调用其他函数的,但是 fork 函数是非阻塞函数。也就是说,程序执行完 yield fork(fn, args) 这一行代码后,会立即接着执行下一行代码语句,而不会等待 fn 函数返回结果后,在执行下面的语句。

// sages.js

import {
    
     put, call, take,fork } from 'redux-saga/effects';
import {
    
     takeEvery, takeLatest } from 'redux-saga'

export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

function* incrementAsync() {
    
    
  // 延迟 1s 在执行 + 1操作
  yield call(delay, 1000);
  yield put({
    
     type: 'INCREMENT' });
}

export default function* rootSaga() {
    
    
  // while(true){
    
    
  //   yield take('INCREMENT_ASYNC');
  //   yield fork(incrementAsync);
  // }
  // 下面的写法与上面的写法上等效
  yield takeEvery("INCREMENT_ASYNC", incrementAsync)
}

基本用法总结:

使用 createSagaMiddleware 方法创建 saga 的 Middleware

然后在创建的 redux 的 store 时,使用 applyMiddleware 函数将创建的 saga Middleware 实例绑定到 store 上

最后可以调用 saga Middleware 的 run 函数来执行某个或者某些 Middleware

在 saga 的 Middleware 中,可以使用 takeEvery 或者 takeLatest 等 API 来监听某个 action

当某个 action 触发后, saga 可以使用 call 发起异步操作

操作完成后使用 put 函数触发 action ,同步更新 state ,从而完成整个 State 的更新。

React Router Dom

react-router-dom 是应用程序中路由的库。

React 库中没有路由功能,需要单独安装 react-router-dom

react-router-dom 提供两个路由器 BrowserRouterHashRoauter

前者是浏览器的路由方式,也就是使用 HTML5 提供的 history API,这种方式在 react 开发中是经常使用的路由方式。但是在打包后,打开会发现访问不了页面,所以需要通过配置 nginx 解决或者后台配置代理。

后者在路径前加入 #号成为一个哈希值。Hash 模式的好处是:再也不会因为我们刷新而找不到我们的对应路径,但是链接上面会有#/

Route 用于路由匹配。

Link 组件用于在应用程序中创建链接。 它将在 HTML 中渲染为锚标记。

NavLink 是突出显示当前活动链接的特殊链接。

Switch 不是必需的,但在组合路由时很有用。

Redirect 用于强制路由重定向。

Fragments

在 React 中,需要有一个父元素,同时从组件返回 React 元素。有时在 DOM 中添加额外的节点会很烦人。

使用 Fragments,就可以不需要在 DOM 中添加额外的节点。

  return (
    <React.Fragment>
       <Compoent A />
       <Compoent B />
       <Compoent C />
    </React.Fragment>
  )

ErrorBoundary

在 React 中,我们通常有一个组件树。如果任何一个组件发生错误,它将破坏整个组件树。没有办法捕捉这些错误,我们可以用错误边界优雅地处理这些错误。

错误边界有两个作用

  • 如果发生错误,显示回退 UI
  • 记录错误

下面是 ErrorBoundary 类的一个例子。如果类实现了 getDerivedStateFromErrorcomponentDidCatch 这两个生命周期方法的任何一个,那么这个类就会成为 ErrorBoundary。前者返回 {hasError: true} 来呈现回退 UI,后者用于记录错误。

import React from 'react'

export class ErrorBoundary extends React.Component {
    
    
    constructor(props) {
    
    
      super(props);
      this.state = {
    
     hasError: false };
    }
  
    static getDerivedStateFromError(error) {
    
    
      // Update state so the next render will show the fallback UI.
      return {
    
     hasError: true };
    }
  
    componentDidCatch(error, info) {
    
    
      // You can also log the error to an error reporting service
      console.log('Error::::', error);
    }
  
    render() {
    
    
      if (this.state.hasError) {
    
    
        // You can render any custom fallback UI
        return <h1>OOPS!. WE ARE LOOKING INTO IT.</h1>;
      }
  
      return this.props.children; 
    }
  }

以下是如何在其中一个组件中使用 ErrorBoundary。使用 ErrorBoundary 类包裹 ToDoFormToDoList。 如果这些组件中发生任何错误,我们会记录错误并显示回退 UI。

import React from 'react';
import '../App.css';
import {
    
     ToDoForm } from './todoform';
import {
    
     ToDolist } from './todolist';
import {
    
     ErrorBoundary } from '../errorboundary';

export class Dashboard extends React.Component {
    
    

  render() {
    
    
    return (
      <div className="dashboard"> 
        <ErrorBoundary>
          <ToDoForm />
          <ToDolist />
        </ErrorBoundary>
      </div>
    );
  }
}

Portals

默认情况下,所有子组件都在 UI 上呈现,具体取决于组件层次结构。

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

我们可以将 children 组件移出 parent 组件并将其附加 idsomeid 的 Dom 节点下。

首先,先获取 id 为 someid DOM 元素,接着在构造函数中创建一个元素 div,在 componentDidMount 方法中将 someRoot 放到 div 中 。 最后,通过 ReactDOM.createPortal(this.props.childen), domnode)children 传递到对应的节点下。

render(){
    
    
  // 使用 Portals 渲染到 body 上
  return ReactDOM.createPortal(
  	<div className="modal">{
    
    this.props.children}</div>,
    document.body // DOM 节点
  )
}

Context

一种组件间通信方式,常用于祖组件与后代组件间通信

Context提供了一种方式,能够让数据在组件树中传递而不必一级一级手动传递

在父组件创建Context容器对象:
    const XxxContext = React.createContext()  

在父组件中渲染子组件时,外面包裹XxxContext.Provider, 通过value属性给后代组件传递数据:
    <XxxContext.Provider value={
    
    数据}>
    子组件
    </XxxContext.Provider>

后代组件读取数据:
//第一种方式:仅适用于类组件 
  static contextType = XxxContext  // 声明接收context
	{
    
    this.context} // 读取context中的value数据

//第二种方式: 函数组件与类组件都可以
  <XxxContext.Consumer>
    {
    
    
      value => ( // value就是context中的value数据
        {
    
    value.username}
      )
    }
  </XxxContext.Consumer>

LazyLoad

import React, {
    
     Component,lazy,Suspense } from 'react'

// 通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包
const Login = lazy(()=>import('@/pages/Login')) // 路由组件

// 通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面
// fallback里面可以放一个只显示加载中的一个通用组件
<Suspense fallback={
    
    <h1>loading.....</h1>}>
    <Switch>
        <Route path="/xxx" component={
    
    Xxxx}/>
        <Redirect to="/login"/>
    </Switch>
</Suspense>

Memo

多数情况下我们不需要对函数组件的渲染进行特殊优化,即使有些重复渲染也不会对体验造成太大影响,但有些情况下优化就显得很必要。在 class 组件中,我们可以通过 shouldComponentUpdate 阻止不必要的 rerender

shouldComponentUpdate(nextProps) {
    
    
   return nextProps.demoUrl !== this.props.demoUrl;
}

但在函数组件中,没有 shouldComponentUpdate

为了解决函数组件中的优化问题,React 在 16.6 版本增加了 React.memo

React.memo 是一个高阶组件,类似于 React.PureComponent,只不过用于函数组件而非 class 组件。 如果函数组件在相同 props 下渲染出相同结果,可以把它包裹在 React.memo 中来通过缓存渲染结果来实现性能优化。这意味着 React 会跳过组件渲染,而使用上次渲染结果。

React.memo 默认只会浅比较 props,如果需要定制比较,可以给第二个参数传入自定义比较函数。

和 class 组件中的 shouldComponentUpdate 不同,如果 props 相同则应返回 true,否则返回 false。这点二者正好相反。

const DemoLoader = React.memo(props => {
    
    
  const {
    
     demoUrl } = props;
  return <div className="demoloader">
    <iframe src={
    
    demoUrl} />
  </div>;
}, (prevProps, nextProps) => {
    
    
  return prevProps.demoUrl === nextProps.demoUrl;
});

React Hooks

Hooks 是 React 版本 16.8 中的新功能

Hooks 是消息处理的一种方法,用来监视指定程序

函数组件中需要处理副作用,可以用钩子把外部代码“钩”进来

就是系统运行到某一时期时,会调用被注册到该时机的回调函数

Hooks 让我们在函数组件中可以使用 state 和其他功能

一个假设两个存在:

假设任何以 use 开头并紧跟着一个大写字母的函数就是一个 Hook

只在 React 函数组件中调用 Hook,而不在普通函数中调用 Hook。(Eslint 通过判断一个方法是不是大坨峰命名来判断它是否是 React 函数)

只在最顶层使用 Hook,而不要在循环,条件或嵌套函数中调用 Hook

为什么要使用 Hooks ?

类组件的不足:

  • 状态逻辑复用难:缺少复用机制、渲染属性和高阶组件导致层级冗余
  • 趋于复杂难以维护:生命周期函数混杂不相关逻辑、相关逻辑分散在不同生命周期中
  • this 指向困扰:内联函数过度创建新句柄、类成员函数不能保证 this

Hooks 的优势:

  • 函数组件无 this 问题
  • 自定义 Hooks 方便复用组件状态逻辑
  • 副作用的关注点分离

Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期

我们可以使用一些钩子,例如 useStateuseEffectuseContextuseMemo()useReducer

状态钩子useState()

const [count,setCount] = useState<number>(0);
  • 声明组件状态,让函数组件也可以有 state 状态,并进行状态数据的读写操作
  • 参数可以设置 state 的初始值,第一次初始化指定的值在内部作了缓存
  • 返回值是一个只有两个元素的数组:[ 状态,状态更新函数 ]
  • setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
  • setXxx(value => newValue): 参数为函数, 接收原本状态值, 返回新的状态值, 内部用其覆盖原来的状态值
  • React.useState()是单人单用的,想创建多个初始化state,就要创建多个 React.useState()

副作用钩子useEffect()

useEffect(() => {
    
    
  document.title = `点击${
      
      count}`;
},[count]);
  • 可以取代生命周期函数 componentDidMount,componentDidUpdate,componentWillUnmount
  • 可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
  • 第二个参数相当于一个依赖,根据第二个参数传入的值来确定是否执行 DidUpdate
  • 如果不指定useEffect的第二个参数,页面初始化和更新后都会执行,相当于componentDidMount()componentDidUpdate(),监测所有状态的改变每次渲染结束都会被调用,会导致无限循环。所以为了避免这种循环,可以在第二个参数加上一个空数组,回调函数只会在第一次render()后执行

模拟生命周期函数:

// 模拟 class 组件的 DidMount
useEffect(() => {
    
    
  console.log('加载完了')
},[])  // 第二个参数为[]

// 模拟 class 组件的 DidUpdate
useEffect(() => {
    
    
  console.log('更新了')
},[count]) // 第二个参数就是依赖的 state ,只有 count 改变了才会执行该钩子

// 模拟 class 组件的 DidMount
useEffect(() => {
    
    
  let timeId = window.setInterval(() => {
    
    
    console.log(Date.now())
  },1000)
  // 返回一个函数
  // 模拟 WillUnMount
  return () => {
    
    
    window.clearInterval(timeId)
  }
},[])

在 useEffect 中使用 async/await :

useEffect(() => {
    
    
  const fetchData = async () => {
    
    
    const responses = await fetch("url");
    const data = await responses.json();
    setRobotGallery(data);
  }
  fetchData();
},[]);

上下文钩子useContext()

先看一下原始的方法:创建上下文对象,使用 React.createContext(默认值)

const defaultContextValue = {
    
    
  username:'rmm',
}
export const appContext = React.createContext(defaultContextValue);

ReactDOM.render(
  <appContext.Provider value={
    
    defaultContextValue}>
  	<App />
  </appContext.Provider>,
  document.getElementById('root')
)

然后在子组件中使用

<appContext.Consumer>{value.username}</appContext.Consumer>

<appContext.Consumer>{username => <h1>{username}</h1>}</appContext.Consumer>

来获取到值。

接下来使用一下useContext钩子函数,非常简单。

const value = useContext(appContext)
直接使用{
    
    value.username}即可,不需要 appContext.Consumer

性能优化钩子useMemo()

相当于 shouldComponentUpdate

useMemo 是一个函数,他的返回值取决于第一个回调函数的 return 值

接受两个参数,第一个参数是回调函数,返回一个和原本一样的对象

第二个参数是一个数组,监听数据。当第二个参数发生改变的时候,才产生一个新的对象

如果是空数组的话,数据发生变化将不会引起组件变化

性能优化钩子useCallback()

本身也是一个函数,可以直接执行。有两个参数,第一个也是回调函数,第二个是数组,类似于 useMemo , 就是渲染数据需要执行返回值,也就是函数。

const memoizedCallback = useCallback(
  () => {
    
    
    doSomething(a, b);
  },
  [a, b],
);

memoizedCallback 会在初始时生成一次。在后面的过程中只有它的依赖 ab 变化了才会重新生成。

传递给组件事件时用 useCallback 是它的正确使用方法

useCallback包裹的函数比一般的函数内存开销会大一点,因为为了能判断 useCallback 要不要更新结果,我们要在内存保存上一次的依赖,而且如果useCallback返回的函数依赖了组件其他的值,由于 JS 中闭包的特性,他们也会一直存在而不被销毁。

DOM元素钩子useRef()

返回一个可变的 ref 对象,其 .current 属性,被初始化为传入参数

返回的 ref 对象在整个生命周期内保持不变,改变引用不会触发重新渲染

const valueRef = useRef(initialValue)

作用:可以获取 DOM 元素以及 DOM 元素的属性、保存变量(每次返回相同引用)、生成对 DOM 对象的引用

  • React.createRef 能够在类组件函数组件中使用
  • useRef 仅能够在函数组件中使用

原因:createRef 每次渲染都会返回一个新的引用,useRef 每次都会返回相同的引用。函数组件会随着函数的不断执行而刷新,React.createRef 所得到的 Ref 对象因重复初始化而无法得到保存,而使用类组件对生命周期进行了分离,不受影响

获取一个 dom 元素的值,在组件上加上一个ref=useRef 的返回值, 获取 value 值就是 useRef.current.value

useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问组件或真实的 DOM 节点,从而可以对 DOM 进行一些操作,比如监听事件等等。

这个利用 useRef 获取 DOM 元素的操作可以类比 vue 中利用 ref 属性进行相应的获取 DOM 元素或组件进行理解

import React, {
    
     useState, useRef } from "react";
function App() {
    
    
  let [name, setName] = useState("Nate");
  let nameRef = useRef(); // 这里就是获取ref绑定的那个DOM元素值
  const submitButton = () => {
    
    
    setName(nameRef.current.value);  // 这里用setName设置name值时,是把input框里的输入值传入进去
  };
  return (
    <div className="App">
      <p>{
    
    name}</p>
      <div>
        <input ref={
    
    nameRef} type="text" />   // 这里的ref就是获取了这个input输入框,利用ref.current就可以获取这个DOM节点
        <button type="button" onClick={
    
    submitButton}>
          Submit
        </button>
      </div>
    </div>
  );
}

useReducer

useReduceruseState 的代替方案,用于 state 复杂变化,useState 就是使用 useReducer 构建的

useReducer(reducer, initialState) 接受 2 个参数,分别为 reducer 函数 和 初始状态

接收一个 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法

useReducer 与 Redux 的区别:

useReducer 是单个组件状态管理,组件通讯还需要 props

Redux 是全局的状态管理,多组件共享数据

import React, {
    
     useReducer } from 'react'

const initialState = 0
const reducer = (state: number, action: string) => {
    
    
  // reducer function 的 2 个参数分别为当前 state 和 action, 并根据不同的 action 返回不同的新的 state
  switch (action) {
    
    
    case 'increment':
      return state + 1
    case 'decrement':
      return state - 1
    case 'reset':
      return initialState
    default:
      return state
  }
}

function CounterOne() {
    
    
  const [count, dispatch] = useReducer(reducer, initialState)
  return (
    <div>
      <div>Count - {
    
    count}</div>
      <button
        // dispatch 方法接受一个参数,执行对应的 action
        onClick={
    
    () => dispatch('increment')}
      >Increment</button>
      <button
        onClick={
    
    () => dispatch('decrement')}
      >Decrement</button>
      <button
        onClick={
    
    () => dispatch('reset')}
      >Reset</button>
    </div>
  )
}

export default CounterOne

自定义Hook

所谓的自定义 Hook,实际上就是把很多重复的逻辑都放在一个函数里面,通过闭包的方式给 return 出来。

import React, {
    
     useState, useEffect } from 'react';

export default function App() {
    
     
    // 正常我们这样子实现
    const [title, setTitle] = useState('默认标题')
    
    useEffect(() => {
    
    
        document.title = title;
    }, [title]);
    
    const onTitleChange = (event) => {
    
    
        setTitle(event.target.value);
    }
    
    return (<div>
        <h2>{
    
    title}</h2>
        <input type="text"  onInput={
    
    onTitleChange}/>
    </div>)
}

抽取共用逻辑,封装成自定义 Hook

export function useSetPageTitle (initTitle) {
    
    
  const [title, setTitle] = useState(initTitle || '默认标题');

  useEffect(() => {
    
    
    document.title = title;
  }, [title]);

  return [title, setTitle]
}

在其他组件中使用刚刚写的 useSetPageTitle

import {
    
     useSetPageTitle } from '../App';

export default function Chirld() {
    
     
    // 这里使用刚才写自定义Hook
    const [title, setTitle] = useSetPageTitle();
    
    return (<div>
        <h2>{
    
    title}</h2>
        <input type="text"  onInput={
    
    onTitleChange}/>
    </div>>)
}

HOC高阶组件

const hoc = higherOrde(wrappedComponent);
  • 高阶组件就是一个返回了组件的函数
  • 通过组件嵌套的方法给子组件添加更多的功能
  • 接收一个组件作为参数并返回一个经过改造的新组件

为什么要使用高阶组件?

  • 抽取重复代码,实现组件复用
  • 条件渲染,控制组件的渲染逻辑(渲染劫持)
  • 捕获/劫持被处理组件的生命周期

命名规范:withXXX( )

性能优化

可以通过多种方式提高应用性能:

  • 适当地使用 shouldComponentUpdate 生命周期方法。 它避免了子组件的不必要的渲染。 如果树中有 100 个组件,则不重新渲染整个组件树来提高应用程序性能。
  • 使用 create-react-app 来构建项目,这会创建整个项目结构,并进行大量优化。
  • 使用immutable.js。不可变性是提高性能的关键,不要对数据进行修改,而是始终在现有集合的基础上创建新的集合,以保持尽可能少的复制,从而提高性能。
  • 使用PureComponentmemo进行浅比较,类组件用PureComponent,函数组件用memo
  • 减少函数 bind this 的次数
  • 在显示列表或表格时始终使用 Keys,这会让 React 的更新速度更快。
  • 懒加载。React 可以通过 react-lazyload 这种成熟组件来进行懒加载的支持。
  • 切分代码。通过 Code Splitting 来懒加载代码,提高用户的加载体验。例如通过 React Loadable 来将组件改写成支持动态 import 的形式。
  • 页面占位。有时候图片或者文字没有加载完毕,对应位置空白,然后加载完毕会突然撑开页面导致闪屏。这时候使用第三方组件 react-placeholder 可以解决这种情况。
  • 减少业务代码体积。通过 Tree Shaking 来减少一些代码

React16 的架构以及 React17改动

React 16 架构分为三部分:

Scheduler(调度器):调度任务优先级,使优先级高的任务进入 Reconciler。

Reconciler(协调器):负责找出变化的组件。通过 diff 算法找出变化的组件交给 Renderer 渲染器。

Renderer(渲染器):负责将变化的组件重新渲染。

Diff 机制:首先由 Scheduler(调度器)去调度任务的优先级,将优先级比较高的任务加入到 Reconciler(协调器)中。Reconciler(协调器)通过 diff 算法计算出需要更新的组件,并标记更新状态。等整个组件更新完成之后,再通过 Renderer(渲染器)去执行更新并渲染组件。

React17:

改动一:事件委托不再挂到 document 上

React 17 不再往 document 上挂事件委托,而是挂到 DOM 容器上,即事件绑定到 root 组件中

另一方面,将事件系统从 document 缩回来,也让 React 更容易与其它技术栈共存(至少在事件机制上少了一些差异)

改动二:向浏览器原生事件靠拢

onScroll 不再冒泡

onFocus/onBlur 直接采用原生 focusin/focusout 事件

捕获阶段的事件监听直接采用原生 DOM 事件监听机制

注意,onFocus/onBlur 的下层实现方案切换并不影响冒泡,也就是说,React 里的 onFocus 仍然会冒泡

改动三:DOM 事件复用池被废弃

之前出于性能考虑,为了复用 SyntheticEvent,维护了一个事件池,导致 React 事件只在传播过程中可用,之后会立即被回收释放。

传播过程之外的事件对象上的所有状态会被置为 null,除非手动 e.persist()(或者直接做值缓存)

React 17 去掉了事件复用机制,因为在现代浏览器下这种性能优化没有意义,反而给开发者带来了困扰。

改动四:Effect Hook 清理操作改为异步执行

useEffect 本身是异步执行的,但其清理工作却是同步执行的(就像 Class 组件的 componentWillUnmount 同步执行一样),可能会拖慢切 Tab 之类的场景,因此 React 17 改为异步执行清理工作

同时还纠正了清理函数的执行顺序,按组件树上的顺序来执行(之前并不严格保证顺序)

猜你喜欢

转载自blog.csdn.net/weixin_46232841/article/details/124505088