文章目录
React特点
React是一个将数据渲染为 HTML 视图 的 js 库
简单看来,React 框架主要功能体现在前端 UI 页面的渲染,包括性能优化以及操作简化等等方面。站在 mvc 框架的角度来看,React 操作 view 层的功能实现。
- 采用组件化模式、声明式编码、函数式编程,提高开发效率和组件复用性
- 它遵循从高阶组件到低阶组件的单向数据流。
- 在 React Native 中可以用 react 预发进行安卓、ios 移动端开发
- 使用虚拟 dom 和 diff 算法,尽量减少与真实 dom 的交互,提高性能
React 与 Vue 对比
相同点:
- 都使用 Virtural DOM
- 都使用组件化思想,流程基本一致
- 都是响应式,推崇单向数据流
- 都有配套框架,Vue 有
Vue-router
和Vuex
,而 React 有React-router
和React-Redux
- 都有成熟的社区,都支持服务端渲染
不同点:
模版语法不同
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)):
- 不同的组件产生不同的 DOM 结构。当 type 不相同时,对应 DOM 操作就是直接销毁老的 DOM,创建新的 DOM。
- 同一层次的一组子节点,可以通过唯一的 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 元素。
-
定义虚拟 dom 时不要用引号
-
标签中引入 js 表达式要用 {}
-
如果在 jsx 要写行内样式需要使用 style={{coler:red}} 形式
-
样式的类名指定不能写 class,要写 className;
-
只有一个根标签
-
标签必须闭合
-
标签首字母
①若小写字母开头,则会将该标签转为 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>
}
}
}
事件处理
- 通过 onXxxx 属性指定事件处理函数(小驼峰形式)
- 通过 event.target 可以得到发生事件的 dom 元素
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
在原生 DOM 中,我们可以通过返回 false
来阻止默认行为,但是这在 React 中是行不通的,在 React 中需要明确使用 preventDefault()
来阻止默认行为。
事件回调函数里的 event
是经过 React 特殊处理过的(遵循 W3C 标准),所以可以放心地使用它,而不用担心跨浏览器的兼容性问题。
注意:在使用事件回调函数的时候,需要特别注意 this
的指向问题。
因为在 React 里,除了构造函数和生命周期钩子函数里会自动绑定 this 为当前组件外,其他的都不会自动绑定 this
的指向为当前组件,因此需要我们自己注意好 this 的绑定问题。
通常而言,在一个类方式声明的组件里事件回调,需要在组件的 constructor
里绑定回调方法的 this 指向。
render函数
当组件的 state 或者 props 发生改变的时候,render 函数就会重新执行
当父组件的 render 函数重新执行时,子组件的 render 函数也会重新执行
虚拟DOM
- 本质上其实就是一个 object 对象;
- 虚拟 dom 上的属性比较少,真实 dom 属性多,因为虚拟 dom 只在 react 内部使用,用不到那么多的属性
- 虚拟 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
不相同则直接删掉重建,不再深度比较;tag
和 key
两者都相同,则认为是相同节点,也不再深度比较
虽然如此,如果组件树过大,diff
其实还是会有一部分的开销
因此react
内部通过 fiber
优化 diff
算法,外部建议开发者使用 SCU
和pureComponent
生命周期
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
语法比较 action
中 type
。 匹配时,更新对应的内容返回新的 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 开发者工具
-
下载开发者工具 Redux DevTools
-
下载完后右上方的插件图标还是不会亮的,因为它还识别不了你写的 redux,所以还需要下载一个库(redux-devtools-extension)
-
然后在 store 文件中引入该库文件
import {composeWithDevTools} from redux-devtools-extension
-
然后在
createStore()
第二个参数位置调用composeWithDevTools()
,将之前的中间件传到该方法中export default createStore(allReducer,composeWithDevTools(applyMiddleware(thunk)))
Redux-saga中间件
redux-saga 提供了一些辅助函数,用来在一些特定的 action 被发起到 Store 时派生任务,先来看一下两个辅助函数:takeEvery
和 takeLatest
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 提供两个路由器 BrowserRouter
和 HashRoauter
。
前者是浏览器的路由方式,也就是使用 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
类的一个例子。如果类实现了 getDerivedStateFromError
或 componentDidCatch
这两个生命周期方法的任何一个,那么这个类就会成为 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 类包裹 ToDoForm
和 ToDoList
。 如果这些组件中发生任何错误,我们会记录错误并显示回退 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
组件并将其附加 id 为 someid
的 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 以及生命周期
我们可以使用一些钩子,例如 useState
,useEffect
,useContext
,useMemo()
,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
会在初始时生成一次。在后面的过程中只有它的依赖 a
或 b
变化了才会重新生成。
传递给组件事件时用 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
useReducer
是 useState
的代替方案,用于 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
。不可变性是提高性能的关键,不要对数据进行修改,而是始终在现有集合的基础上创建新的集合,以保持尽可能少的复制,从而提高性能。 - 使用
PureComponent
和memo
进行浅比较,类组件用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 改为异步执行清理工作
同时还纠正了清理函数的执行顺序,按组件树上的顺序来执行(之前并不严格保证顺序)