React
虽然你知道可以优化,但不代表你应该优化
# 为啥用React
答React的好处就是了
合成事件
VDOM
JSX
生命周期
单向数据流
……
# React、Vue
引出mvc和mvvm
react和vue底层思想
优缺点
实践:技术选型
得出结论
2
3
4
5
# 相同
- 组件化开发和VDOM
- 支持props
- 数据驱动视图,不直接操作ADOM
- 支持SSR
- 都借鉴MVVM的思想
- 将注意力集中在 保持核心库,将其他功能 如 路由 和全局状态管理 交给相关的库
- 都有自己的构建工具
# 不同
vue实现数据双向绑定,react单向数据流
react的jsx功能强大,扩展性强
vue的dom操作方便,for/if指令
react思想很棒,代码更美观
Vue标签更方便,也是封装好的语法糖
VDOM
Vue比React好? (opens new window)
# 区别?
大体相同,都使用VDOM高效 更新视图, 提倡组件化, 实现 数据驱动视图, 使用diff算法, 对diff算法 优化, router库实现url到组件的映射, 状态管理
具体实现 不尽相同
# 组件化
组件是独立和可复用的代码组织单元,它使开发者使用小型、独立和通常可复用的组件构建大型应用
提高开发效率**、测试性、**复用性
降低耦合度,在保持接口不变可以替换不同组件快速完成需求,输入框,可替换日历、时间、范围等组件作具体的实现
调试方便,整个系统通过组件组合,出现问题时,排除法移除组件,根据报错组件快速定位问题,因为每个组件低耦合,职责单一,逻辑会比分析整个系统简单
提高可维护性,组件职责单一,被复用,对代码优化可获得系统整体升级
都推崇组件化,将页面拆分成小的可复用单元提高 复用率和开发效率。 开发时react和vue有相同的套路,父子组件传参,数据状态管理,前端路由
React推荐 JSX + inline style, 把 HTML 和 CSS 写进 JavaScript
Vue 推荐 template 的单文件组件格式(简单易懂,从传统前端转过来易于理解),即 html,css,JS 写在同一个文件(vue也支持JSX写法)
# VDOM
表现为描述 DOM 结构及其属性信息的 对象
- 存储在内存
- 描述ADOM
- 数据变化 生成新的DOM,对比新旧VDOM将差异更新到ADOM
优点
- 减少 DOM 操作:虚拟 DOM 可以将多次 DOM 操作合并为一次操作
- 研发效率的问题:虚拟 DOM 的出现,为数据驱动视图 思想提供 高度可用 载体, 前端开发 基于函数式 UI 编程方式实现高效的声明式编程
- 跨平台的问题: 同一套虚拟 DOM,可 对接不同平台 渲染逻辑,从而实现“一次编码,多端运行”
# react、vue VDOM相同点
都使用 Virtual DOM + Diff算法,最后都是生成render函数,render函数执行返回VNode(VDOM的数据结构,本质是树)
每次UI更新,会根据render重新生成最新VNode,跟以前缓存起来老的VNode比对,使用Diff算法 更新ADOM(VDOM是 对象 , 在JS引擎中, ADOM在浏览器渲染引擎中,所以操作VDOM比操作ADOM开销要小 )
# diff 优化基本上思路相同
- tag不同认为是不同节点
- 只比较同一层级,不跨级比较
- 同一层级的节点用key唯一标识,tag和key都相同则认为是同一节点
# diff 源码 相同之处
处理老节点部分,都需要把节点处理 key - value 的 Map 结构,方便在往后的比对中 快速通过节点的 key 取到对应 节点。同样在比对两个新老节点是否相同时,key 是否相同也是 判断标准。 设置一个唯一值 key 在 diff 处理 时 性能才最大化
# react和vue中VDOM差别
都是用js对象 模拟ADOM,VDOM的diff 最小化更新ADOM, 减小 性能损耗,按颗粒度分为不同的类型比较同层级dom节点,进行增、删、移
按颗粒度分为tree diff, component diff, element diff. tree diff 比较同层级dom节点,进行增、删、移操作。如果遇到component, 就会重新tree diff流程
# dom的更新策略不同
react 自顶向下全diff。vue 跟踪每 个组件 依赖关系,不需要re-rendr
react 状态 改变时, 重新render 生成新的VDOM tree, 新旧dom tree 比较, patch打补丁方式,局部更新 。 react为 避免父组件更新 引起不必要 子组件更新, 在shouldComponentUpdate 逻辑判断,减少没必要 render, 重新生成VDOM,做差量对比
vue通过Object.defineProperty 把 data 属性全部转为 getter/setter。 watcher实例对象 在组件渲染时,将属性记录为dep, 当dep 项中的 setter被调用时,通知watch重新计算,使得关联组件更新
# 总结
Vue2 核心Diff算法采用 双端比较 ,同时从新旧children的两端 比较,借助key值找到可复用的节点,再 操作。相比React的Diff算法,同样情况 可以减少移动节点次数,减少 性能损耗,更加 优雅
# 数据驱动视图
数据变化 相应 视图 更新。开发者只需要关注数据 变化而不用 手动 操作DOM
# vue 数据驱动视图
MVVM 框架实现。MVVM 包含3个部分:model、view和 viewModel
- Model 数据
- View 视图部分, dom
- ViewModel 连接视图与数据的中间件
ViewModel是实现数据驱动视图的核心, 数据变化 ViewModel能够监听到 , 及时 通知view 修改。同样当页面有事件触发 ,ViewModel也能够监听 ,并通知model响应。ViewModel相当于观察者,监控着双方的动作,及时通知对方操作
首先,vuejs实例化过程中,会对遍历传给实例化对象选项中的data 选项,遍历其属性使用 Object.defineProperty 全部转为 getter/setter
同时每个实例对象都有watcher实例对象,在模板编译的过程中,用getter访问data的属性,watcher此时把用到的data属性记为依赖,建立视图与数据联系。之后渲染视图的数据依赖改变,watcher对比前后两个的数值是否变化,确定是否通知视图re-render。这样实现所谓的数据对于视图驱动
# react的数据驱动视图
- pending 当前所有等待更新的state队列
- isBatchingUpdates 标识当前是否处理批量更新状态,默认false
- dirtyComponent 当前所有待更新state的组件队列
setState实现数据驱动视图,引发一次组件的更新过程从而实现页面的re-render
- setState() 将接收的第一个参数state存储在pending队列
- 判断是否处于批量更新,是 就将需要更新state的组件添加到dirtyComponents
- 不是 会遍历dirtyComponents所有组件,调用updateComponent更新每个dirty组件
# 核心思想
# React设计实现
是一个简单的JS UI库,用于构建高效、快速的用户界面
是一个轻量级库
遵循组件设计模式、声明式编程范式和函数式编程
使用VDOM操作ADOM
遵循从高阶组件到低阶组件的单向数据流
# 优点
提高性能
JSX 可读性增加
易集成使用
代码直观
组件简单可复用
VDOM
函数式编程
React 老矣,我建议大家用用别的框架 (opens new window)
# 不习惯之处
React 我爱你,但你太让我失望了 (opens new window)
# JSX
const ele=<h1>hello world</h1>
既不是字符串也不是HTML,是jsx,是JS的语法扩展,生成React元素
React认为渲染逻辑本质与其他UI逻辑内在耦合,将标签和逻辑共同存放在组件中,实现关注点分离
编译之后,JSX被转换为JS普通函数调用,取值后得到JS对象
允许在条件或循环语句中使用JSX,将其赋值给变量,传入JSX作为参数,以及从函数中返回JSX
防止注入攻击
- 可以安全地在JSX中插入用户内容
- React的DOM在渲染所有内容后,会进行转义,确保应用中永不会注入那些不是自己写的内容
JSX表示对象
Babel将JSX转译为一个名为React.createElement()函数调用
React读取这些React元素对象并使用它们构建DOM以保持随时更新
# 状态提升
将多个组件需要共享的state 向上 移动到它们最近的公共组件 中,实现state 的共享
让数据自顶向下单向流动,所有组件数据来自父辈组件,由父辈组件来统一存储和修改,再传入子组件
**为了组件之间的数据更加单向性,**数据的传输上始终出现一对一的情况,方便我们只需要在向子组件传递数据的父组件上操作,并传回子组件,更新数据,体现了React单向数据流的设计思想,复用组件时,组件数据不会相互干扰,代码逻辑上便于管理
# key
Warning:Each child in an array or iterator should have a unique "key" prop
为啥使用
key在DOM中元素增删改查时帮助识别哪些元素发生变化,所以需要给每一个元素赋予唯一标志,想要访问key属性值,需要使用其他属性名 显性 访问
diff算法将key作为唯一id对比组件value确定是否更新
不传key也能用是因为react检测子组件没有key,默认将index作为key
原则
- key同,组件属性有变化,react只会更新组件变化的属性
- key不同,销毁之前的组件,导致re-render
使用index存在啥问题
span单纯展示组件,是受控组件,值由我们定好
若子组件只是受控组件,index作为key,表面可能不会出问题,实际性能可能受影响
列表数据源 顺序 改变,若适用 index 作为key,对应key为0 1 2的组件都变,子组件全部会re-render
注意:未添加key,兄弟节点更新位置前后错位一个,后续全部的比较都会 错误,导致找不到对比目标,性能大打折扣
若使用id作为key,根据更新原则,子组件和key均未改变,顺序改变,我们只移动而不是re-render
input是非受控组件,用户可任意改变value
正确赋值key
若纯粹展示没有其他变更,使用index或其他值作为key没有问题,因为没有diff,就不会用key
- 和具体元素一一对应
- 尽力不使用 index 作为key
- 不使用随机数等 加上不稳定的key,否则性能开销比不加key更糟糕
使用场景
由数组动态创建的子组件的情况
为一个有复杂繁琐逻辑的组件添加key后,后续操作可改变key值,从而 达到 先销毁之前的组件,再重新创建该组件

# ⭐️ 生命周期
- 挂载 mount,组件第一次在 DOM 树中被渲染
- 更新过程 update,re-render
- 卸载 unmount,组件从 DOM树中被移除过程
React15生命周期
constructor()
componentWillReceiveProps() // 父组件状态属性更新触发
shouldComponentUpdate() // 组件更新时调用,在此可拦截更新
componentWillMount() // 初始化渲染时调用(挂载前调用)
componentWillUpdate() // 组件更新时调用
componentDidUpdate() /
componentDidMount() // 初始化渲染时调用(挂载后调用)
render()
componentWillUnmount()
2
3
4
5
6
7
8
9
React16
constructor()
getDerivedStateFromProps() // 组件初始化和更新时调用
shouldComponentUpdate() // 组件更新时调用,在此可拦截更新
render()
getSnapshotBeforeUpdate() // 组件更新时调用
componentDidMount() // 组件初始化时调用(挂载后调用)
componentDidUpdate(prevProps, prevState) // 组件更新后调用
componentWillUnmount()
2
3
4
5
6
7
8
# React 16前
# React 16后

首次挂载 getDefaultProps、getInitialState、componentWillMount、render 和 componentDidMount
卸载组件
重新挂载组件 getInitialState、componentWillMount、render 和 componentDidMount,但并不执行 getDefaultProps
更新组件 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate
# constructor
实例过程中自动调用,方法内部通过super关键字获取父组件的props
- 初始化state
- this上挂载方法
# 16新增2个
# getDerivedStateFromProps
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 当传入的type发生变化的时候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}
2
3
4
5
6
7
8
9
10
11
- 静态方法(纯函数)
- 只能通过preState 而不是 prevProps 作对比,保证了state 和 props 间的简单关系及不需要处理第一次渲染时 prevProps 为空的情况
执行时机:组件创建和更新阶段,不论是props变化还是state变化,也会调用
在每次render方法前调用,第一个参数为即将更新的props,第二个参数为当前组件的state,可以比较props 和 state来加一些限制条件,防止无用的state更新
该方法返回新对象更新state,不更新则返回 null
静态方法阻止操作instance,阻止多次操作setState,没有instance就不能操作DOM
逻辑应该会很简单,就不会出错,不出错就不会打断DFS过程
getDerivedStateFromProps() contains the following legacy lifecycles: componentWillMount componentWillReceiveProps componentWillUpdate
React 16.4后对getDerivedStateFromProps做了微调。在>=16.4以后的版本中,组件任何的更新流程都会触发getDerivedStateFromProps,而在16.4以前,只有父组件的更新会触发该生命周期
不咋使用
# getSnapshotBeforeUpdate
返值Snapshot值(默认为 null ),作为componentDidUpdate第三个参数
运行在render后,表示ADOM构建完成,但还没有渲染到页面,可理解为更新前的快照,用来做一些附加的DOM操作
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('#enter getSnapshotBeforeUpdate');
return 'foo';
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
2
3
4
5
6
7
8
获取组件更新前的一些信息,比如组件的滚动位置之类
# componentDidMount
组件挂载到ADOM节点后执行,render之后执行
- 执行数据获取,事件监听等操作
此处 调用 setState 触发一次额外的渲染,它 在浏览器刷新屏幕前执行!所以用户没有感知,但会带来一定性能问题
和 componentWillMount 一样,有且仅有一次 调用!
# shouldComponentDidMount
告知组件本身基于当前的props和state是否需要 re-render,默认返回true
执行时机:到新的props/state 调用,返回true/false告知组件更新与否
一般情况,不建议在该周期方法中深层比较,影响效率
同时也不能调用setState,会导致无限循环更新
# componentDidUpdate
ADOM挂载到页面后运行
可根据前后的props和state变化操作,如获取数据,修改DOM等
componentDidUpdate(prevProps, prevState, snapshot){}
# componentWillUnmount
组件卸载前,清理副作用
一旦一个组件实例被卸载,不会被再次挂载,只可能被重新创建
# 准备废弃
- 被废弃的3个方法都在render之前,因为fiber出现,可能因为高优先级任务 打断现有任务导致它们被执行多次
- React想约束使用者,好的框架能让人写出易维护和扩展的代码
# componentWillMount
风险很高,鸡肋
此函数可以使用 componentDidMount 和 constructor 代替
为了约束开发者,直接干掉了此 API
# componentWillReceiveProps(nextProps)
一个API并非越复杂才越优秀
props 变化执行,初始化render 时不执行
老版本React中,若组件自身的某个state跟其props密切相关,没有一种优雅的方式处理state,而是需要在 componentWillReceiveProps 中判断前后2个props是否相同,若不同再将新的props更新到相应的state上
通过 setState更新组件状态,旧属性通过this.props 获得,此处调用更新状态是安全的,不会触发额外render
优点
- 可以在子组件的render函数执行前获取新的props,进而更新子组件自己的state
- 可以将数据请求放在这里执行,需要的参数中nextProps获取,不必将所有请求都放在父组件中
缺点
- 破坏state数据的单一数据源,导致组件状态不可预测
- 增加组件的重绘次数
# componentWillUpdate
componentWillUpdate( nextProps, nextState )
挡了新的Fiber架构的路
- 一次更新可能被调用多次,因为一次更新中 componentDidUpdate 只会被调用一次,所以将原先写在 componentWillUpdate 中的回调移至componentDidUpdate 就可以解决这个问题
- 获取 DOM 元素状态,fiber中render可被打断,可能在WillMount中获取到的元素状态很可能与实际需要的不同,这个通常可以使用getSnapshotBeforeUpdate解决
由getSnapshotBeforeUpdate(prevProps,prevState) 代替,在最终的render之前调用
为何要废弃它们?
在Fiber机制下,render阶段允许被暂停、终止和重启
**导致render阶段的生命周期都可能重复执行。**这几个方法常年被滥用,执行过程中存在风险。比如setState fetch发起请求 操作ADOM等
这些操作完全可以转移到其他方法中做
即使没有开启异步渲染,Recat15中也可能导致一些严重的问题,比如componentWillReceiveProps和componentWillUpdate里滥用setState导致重复渲染,死循环
首先确保了Fiber机制下数据安全性,同时也确保生命周期方法的行为更纯粹
# 🔥 setState
componentWillMount中setState
无意义,应当将setState放在constructor中(初始化state)
组件只挂载一次,componentWillMount中的setState回合constructor中state合并执行
componentDidMount中setState
导致组件初始化时触发更新,渲染2次
componentWillUnmount中setState
不会更新,无意义
shouldComponentUpdate和componentWillUpdate中setState
禁止
造成死循环
setState后会再次触发这2个函数,然后又触发setState
componentDidUpdate中setState
可以
组件刚更新完成,又要更新一次,连续render两次
和componentDidMount类似
componentWillReceiveProps中setState
可以
不会re-render,因为只有 props变化才会触发,setState不会造成死循环
# 💚 组件通信
- 父组件向子组件传递
- 子组件向父组件传递
- 兄弟组件之间的通信
- 父组件向后代组件传递
- 非关系组件传递
父组件向子组件传递
React的数据流是单向的,这是最常见的方式,props
父子通信底层如何实现?
子组件向父组件传递
父组件向子组件**传一个函数,然后通过这个函数的回调拿到子组件传递的值**
兄弟组件之间的通信
父组件作为**中间层实现数据互通**
父组件向后代组件传递
最普通的事情,像全局数据一样
使用context可共享数据,其他数据都能读取对应的数据
非关系组件传递
组件间关系类型复杂,可以将数据进行一个全局资源管理,从而实现通信,例如redux dva
# 💙有/无状态组件
# 有状态组件
特点
- 是类组件
- 有继承
- 有this
- 有生命周期
- 使用较多,易触发生命周期钩子函数
- 内部使用state,根据外部组件传入的props和自身state渲染
使用场景
- 需要使用状态的
- 需要状态操作组成的
总结
可维护自己的state,可以对组件做更多的控制
# 无状态组件
特点
- 不依赖自身state
- 可以是类组件或函数组件
- 可避免使用this
- 组件内部不维护state,props改变,组件re-render
使用场景
组件不需要管理state
优点
- 简化代码 专注render
- 组件不需要实例化,无生命周期
- 视图和数据解耦
缺点
- 无法使用ref
- 无生命周期
- 无法控制组件re-render
当一个组件不需要管理自身状态时,就是无状组件,应该优先设计为函数组件,比如定义的
# 受/非受控组件
# 受控组件
表单状态变化,触发onChange,更新state
受控组件中,组件渲染出的状态和value/checked属性相对应,react通过这种方式消除组件的局部状态,使组件变得可控
缺点
多个输入框需要获取到全部值时,需要每个都编写事件处理函数,代码臃肿
后来,出现了非受控组件
# 非受控组件
Input组件有内部value,没有任何属性——非受控组件,组件状态不受外部环境控制,而是封闭在组件内部
若把state的value放到props中——变成 受控组件(此时input值取决于外部传递的props)
表单组件没有value props
可使用ref 从 DOM 中获取表单值,而不是编写事件处理函数
非受控组件可以减少代码量

# 类/函数组件
相同点
基本可认为两者完全一致
不同点
函数组件 是 无状态组件 的思想
函数组件 无法使用 state,没有 生命周期方法,接收 props,渲染 DOM
函数组件没有 this
函数组件 更易理解
函数组件性能优化依靠 React.memo缓存渲染结果实现
类组件 基于面向对象编程,主打 没有副作用,引用透明等特点
类组件性能优化依靠shouldC omponentUpdate阻断渲染实现
# class、hooks
函数式编程?
class占内存啊,写了class写hooks可以快速切换,但是写了hooks再写class就不得行,因为所有最终回归的是C++ C 和操作系统,class相当于提供了一个框框,约束我们代码的编写
hooks就有点放飞自我,因为是函数式编程不像class是一个类,实例 有静态方法,占用内存,而函数式就是运行完内存即释放,需要使用的变量得不到一个合适的保存,于是只有使用闭包方式,但是闭包存在挺多的问题,涉及到内存泄漏等,而且存在很多闭包,
链式编程?
如果有bug怎么调试,大家都是学生,首先是为了提高自己发现问题并解决问题的能力
React存在兼容性,之所以不升级antd版本,为了兼容id浏览器
# 💚 性能优化
React 性能优化的那些事儿 (opens new window)
# 卡顿原因
render时会根据现有render产生新的jsx数据和现有fiberRoot对比,找到不同地方,生成新的workInProgress,进而在挂载阶段把新的workInProgress交给服务器渲染
这个过程中,为了让底层机制更加高效快速,react做了大量优化,设立任务优先级、异步调度、diff算法和时间分片等
整个链路为了快速高效完成从数据更新到页面渲染的整体流程
为了不让递归遍历寻找所有更新节点太大而占用浏览器资源,React升级了fiber架构,时间分片,实现增量更新
diff——高效查找所有更新节点
任务调度优先级——高效更新
优化——控制刷新渲染的波及范围,只让该更新的更新,更新链路尽快走完
# 优化场景
- 父组件更新,子组件不更新
PureComponent、shouldComponentUpdate、父组件对子组件缓冲、memo
- 组件自己控制是否刷新
- 减少波及范围,无关刷新不存在state
- 合并state,减少setState执行次数
- 更快完成diff比较过程
# 思路
- 减少render次数
- 减少渲染的节点
- key
- 降低计算量
- 缓存
- VDOM
- 使用工具分析性能瓶颈
- 使用不可突变数据结构,数组使用concat,对象使用Object.assign()
- 组件尽可能拆分
- 列表类组件优化
- bind函数优化
- 不滥用props
- reactDOMServer进行服务端渲染组件
# React.memo
React16.6新增,缓存组件
和PureComponent相似,但memo只用于函数组件
使用其对航班信息可视化系统做了优化
React17+antvL7+antd
记忆组件渲染结果,提高组件性能
只检查props是否变化
做浅比较
第二个参数可传入自定义比较函数
areEqual方法和shouldComponentUpdate返回值相反
const App=React.memo(
function myApp(props){
//使用props渲染
}
function areEqual(prevProps, nextProps){
//如果把prevProps传入render方法的返回结果和将nextProps传入render的返回结果一样,则返回true,否则返回false
}
);
2
3
4
5
6
7
8
# React.useMemo
缓存大量计算
函数组件中,匿名函数,箭头函数和普通函数会重新声明
useMemo 第一个参数是函数,这个函数返回的值会被缓存,同时作为 useMemo 的返回值,第二个参数是一个数组依赖,如果数组值变化,会重新执行第一个参数的函数,并将函数返回的值缓存起来作为 useMemo 的返回值
// 避免这样做
function Component(props) {
const someProp = heavyCalculation(props.item);
return <AnotherComponent someProp={someProp} />
}
// 只有 `props.item` 改变时someProp的值才会被重新计算
function Component(props) {
const someProp = useMemo(() => heavyCalculation(props.item), [props.item]);
return <AnotherComponent someProp={someProp} />
}
2
3
4
5
6
7
8
9
10
11
# PureComponent
避免重复渲染
React.Component并未实现 shouldComponentUpdate(),而 React.PureComponent以浅层对比 Prop 和 State 的方式实现了该函数
shouldComponentUpdate做的是“浅层比较”。若是“深层比较”,某个特定组件的行为,需要我们自己编写
父组件状态的每次更新,都会导致子组件re-render,即使传入相同props。但这里的re-render 不是说会更新DOM,而是每次都会调用diif算法判断是否需要更新DOM。这对大型组件例如组件树来说 非常消耗性能
shouldComponentUpdate确保只有当组件props状态改变时才会re-render,返回false表示不希望组件re-render
PureComponent进行浅比较判断组件是否应该re-render,对于传入的基本类型props,只要值相同,浅比较就会认为相同,对于传入的引用类型props,浅比较只会认为传入的props是不是同一个引用,如果不是,哪怕这两个对象中的内容完全一样,也会被认为是不同的props。
PureComponent,因为进行浅比较也会花费时间,这种优化更适用于大型的展示组件上。大型组件也可以拆分成多个小组件,并使用memo来包裹小组件,也可以提升性能。
- 确保数据类型是值类型
- 如果是引用类型,不应当有深层次的数据变化(解构)
判断步骤:
- 直接比较新老
props/新老state是否相等。相等不更新组件 - 判断新老
state/props,有不是对象/为null的,返回 false ,更新组件 Object.keys将新老props/新老state属性名key变成数组,判断数组长度是否相等,不相等证明有属性增加/减少,更新组件- 遍历老
props/老state,判断对应新props/新state,有没有与之对应且相等的(这个相等是浅比较),有一个不对应/不相等,直接返回false,更新组件
# 高阶组件
函数组件没有 shouldComponentUpdate 方法,可利用 高阶组件 封装一个类似PureComponent 方法
# 避免内联对象
使用内联对象时,react会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象,因此,该组件对于prop的浅层比较始终返回false,导致组件一直re-render。
// Don't do this!
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />
}
// Do this instead :)
const styles = { margin: 0 };
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={styles} {...aProp} />
}
2
3
4
5
6
7
8
9
10
11
12
# 避免匿名函数
// 避免这样做
function Component(props) {
return <AnotherComponent onChange={() => props.callback(props.id)} />
}
// 优化方法一
function Component(props) {
const handleChange = useCallback(() => props.callback(props.id), [props.id]);
return <AnotherComponent onChange={handleChange} />
}
// 优化方法二
class Component extends React.Component {
handleChange = () => {
this.props.callback(this.props.id)
}
render() {
return <AnotherComponent onChange={this.handleChange} />
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 组件懒加载
使用新的React.Lazy和React.Suspense完成
React.lazy
定义动态加载的组件,可直接缩减打包后 bundle 体积,延迟加载在初次渲染时不需要渲染的组件
React.Suspense
悬挂 终止 暂停
配合渲染 lazy 组件,在等待加载 lazy组件时展示 loading 元素,不至于直接空白,提升用户体验;
网球后台用到
只有在生产环境下展示SettingDrawer
<Suspense fallback={<PageLoading />}>{this.renderSettingDrawer()}</Suspense>
const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
2
3
4
5
# 用 CSS
而不是强制加/卸载组件
渲染成本很高,尤其是在需要更改DOM时。此操作可能非常消耗性能并可能导致延迟
// 避免对大型的组件频繁对加载和卸载
function Component(props) {
const [view, setView] = useState('view1');
return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
}
// 使用该方式提升性能和速度
const visibleStyles = { opacity: 1 };
const hiddenStyles = { opacity: 0 };
function Component(props) {
const [view, setView] = useState('view1');
return (
<React.Fragment>
<SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
<AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
</React.Fragment>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# React.Fragment
无需 在DOM中添加额外标签,不会渲染任何元素
function Component() {
return (
<React.Fragment>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</React.Fragment>
)
}
2
3
4
5
6
7
8
9
# key
- 保证key具有唯一性
- DIff算法根据key判断元素时新创建还是被移动的元素,从而减少不必要渲染
虽然key是一个prop,但是接受key的组件并不能读取到key的值,因为key和ref是React保留的两个特殊prop
# 合理使用Context
无需为每层组件手动添加 Props,通过provider接口在组件树间进行数据传递
原则
Context 中只定义被大多数组件所共用的属性
- 使用createContext创建一个上下文
- 设置provider并通过value接口传递state数据
- 局部组件从value接口中传递的数据对象中获取读写接口
网球系统后台使用了这个技术
toggle——切换
import { createContext } from 'react';
const LoginContext = createContext();
export default LoginContext;
2
3
4
# 虚拟列表
只渲染当前视口可见元素
- 无限滚动列表,table
- 无限切换日历
# 合理设计组件
简化Props
简化State
减少组件嵌套
渲染函数内不该放太多副作用
不滥用context,因为context可穿透React.memo或shouldComponentUpdate的对比,一旦context变化,所以依赖该context的组件都会刷新
context不能精细化渲染
# PureComponent、Component
Component需要实现shouldComponentDidMount,而PureComponent通过浅对比默认实现了shouldComponentDidMount,当组件更新时,若组件的props 和 state都没有改变,不会触发render函数,省去VDOM的生成和对比过程
PureComponent其最佳情况是展示组件,因为其shouldComponentUpdate()进行浅比较,如果是引用数据类型,只会比较是不是同一个地址,不会比较这个地址的数据是否改变,浅比较会忽略属性或状态突变情况
如果确保 state和 props 经常在变,不用 PureComponent反而效率可能更好,因为浅比较也会耗性能
# ❤️ setState
告诉组件数据有更新,可能需要re-render,react通常会集齐一批需更新的组件,再一次性保证渲染性能
setState改变之后,立即用this.state拿不到最新状态
# 同步or异步
根据变量isBatchingUpdates(是否 是批量更新) 判断是直接更新this.state还是放到队列中回头再说,isBatchUpdates默认false(同步更新 this.state),函数batchUpdates 会将isBatchingUpdates修改为true,当React调用事件处理/生命周期函数之前 调用batchedUpdates,后果就是 setState异步更新
若isBatchingUpdates为true,命中batchUpdate机制,进行 "异步更新",反之为 "同步更新"
API层面,setState是普通的调用执行的函数,自然是同步API
此 同步 非 彼 同步
同步还是异步 指的是调用API后更新DOM是异步还是同步?——取决于 被调用的环境
- React能控制的范围调用,如 合成事件、生命周期,会批量更新,状态合并后再更新DOM,为异步
- 原生JS控制的范围调用,如 原生事件,定时器回调,Ajax回调,setState调用后立即更新DOM,为同步
异步 就是 批量更新,减少ADOM渲染次数,在React能控制范围内,一定是批量更新(为了性能着想),先合并 状态,再一次性 更新DOM
setState意味着一个完整的渲染流程,包括
shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
setState 批量更新(异步) 是为了避免 频繁 re-render,消耗性能
批量更新 过程 和 事件循环 类似,来一个setState将其加入 队列,待时机成熟, 队列中 state 结果合并,最后只对最新的state 进行更新
在 react17 中,setState 批量执行,但如果在 setTimeout、事件监听器等函数里,setState 会同步执行。可以在外面包一层 batchUpdates 函数,手动设置 excutionContext 切换成批量执行
react18 中所有的 setState 都是异步批量执行了
# 第二个参数
是一个可选的回调函数,这个函数 在 组件re-render后执行,等价于 componentDidUpdate 中执行
可拿到更新后的state
# state如何注入到组件
通过connect 和 mapStateToProps 将state注入 到组件
mapStateToProps(state,ownProps)中带有两个参数,含义是
- state-store管理的 全局状态对象,所有组件状态都存储在该对象中
- ownProps 组件通过 props 传入的参数
# 🍊 事件机制
react为了解决跨平台、兼容性问题,自己封装了一套事件机制,代理了原生事件,例如JSX的onClick和onChange都是合成事件
充当浏览器原生事件的跨浏览器包装器 的对象
将不同浏览器行为组合到一个 API中,确保事件显示一致的 属性
<div onClick={this.handleClick.bind(this)}>点我</div>
React并不是将click事件绑定到了div的ADOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样减少内存消耗,还能在组件挂在销毁时统一订阅和移除事件
除此之外,冒泡到document上的事件也不是原生浏览器事件,而是由react自己实现的合成事件(SyntheticEvent)。如果不想要事件冒泡的话应调用event.preventDefault()方法,而不是调用event.stopProppagation()方法

合成事件 是react 模拟原生 DOM 事件 所有能力的事件对象,优点如下
- 兼容 所有浏览器,更好跨平台
- 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果有很多的事件监听,就需要分配很多事件对象,造成高额内存分配。对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,便于下次复用
- 事件统一 存放在一个数组,避免频繁新增和删除(垃圾回收)
- 便于 react 统一管理
原生事件 先执行,合成事件 后执行,合成事件 冒泡绑定到 document 上,所以 尽量避免 原生事件和合成事件 混用,(为啥呢??)
因为 如果原生事件 阻止冒泡,可能导致合成事件无法执行,因为合成事件需要冒泡到 document 上才会执行
# 💚 Hooks
React16.8新增特性,主要解决了函数式组件无状态的问题
类组件内部逻辑难以拆分和复用
函数组件,内部无法定义和维护state,也叫 无状态 组件
轻量 灵活 易于维护和组织,学习成本低
组件功能清晰很小巧!
- 类组件需继承 class
- 类组件可访问生命周期方法
- 类组件可获取实例化后 的 this
- 类组件可定义并维护state
hooks前,类组件边界强于函数组件
UI=render(data)
或
UI=func(date)
2
3
# 解决
- 组件 逻辑复用,render props/HOC也为了复用,hooks作为官方底层API,最轻量且改造成本小,不影响组件层次结构和嵌套地狱
- 复杂组件理解
- 难以理解的class,不同生命周期使逻辑分散混乱,不易维护管理,关注this指向问题,代码复用代价高,HOC使整个组件变得臃肿
- 状态与UI隔离,状态逻辑变成更小的粒度,易抽象为自定义hooks,组件状态和UI更清晰
# 限制
- 不在循环、条件和嵌套函数中调用(链表 实现Hook,可能导致 数组取值错位)
- 仅在函数组件中调用hook(因为没有 this)
- useEffect中避免使用useState
# hooks和生命周期
| class 组件 | Hooks 组件 |
|---|---|
| constructor | useState |
| getDerivedStateFromProps | useState 里面 update 函数 |
| shouldComponentUpdate | useMemo |
| render | 函数本身 |
| componentDidMount | useEffect |
| componentDidUpdate | useEffect |
| componentWillUnmount | useEffect 里面返回的函数 |
| componentDidCatch | 无 |
| getDerivedStateFromError | 无 |
# useState
和class组件的state差不多
返回一个state和更新state的函数
初始渲染期间,返回状态和传入第一个参数相同
setState用于state的更新,接收新的state并将组件的一次re-render加入 队列
为啥useState使用数组
解构赋值!!
使用数组,我们可以对数组中元素命名,代码比较干净
使用对象,解构时必须要和useState内部实现返回对象同名,多次使用只能设置别名
// 第一次使用
const { state, setState } = useState(false);
// 第二次使用
const { state: counter, setState: setCounter } = useState(0)
2
3
4
# 模拟componentDidMount
useEffect(() => {
console.log('componentDidMount')
}, [])
2
3
# useLayoutEffect、useEffect
useEffect相当于是一个副作用的函数
渲染阶段 可改变DOM、添加订阅、设置定时器、记录日志,执行包含副作用的操作都不被允许,可能产生bug 破坏UI一致性
使用useEffect完成副作用操作
- 共同点
都是处理副作用,包括 DOM改变,订阅设置,定时器操作
- 不同点
useEffect 渲染之前 异步调用,不会等待DOM真正渲染后执行,可能会闪烁
useLayoutEffect 渲染之前 同步调用,不会闪烁,**总是比 useEffect先执行!**可获取更新的state
useLayoutEffect 和 componentDidMount,componentDidUpdate 执行时机一样,在浏览器将所有变化渲染到屏幕之前执行
建议使用 useEffect!建议使用 useEffect!建议使用 useEffect! 避免阻塞视觉更新
页面有异常就再替换为 useLayoutEffect
项目中用于发送请求
# 第二个参数
航班系统中使用useEffect第二参数对系统发送请求进行优化,仅在组件挂载和卸载时执行,传递一个[],作为第二参数,告诉React我的effect不依赖于props或state中的任何值,不需要重复执行
控制不同航班信息的展示,使用useEffect的第二个参数传递一个state,控制drawer的变化,只有state变化了,effect才会执行,async-await执行结束,数据更新,打开drawer就有效果了
收集子组件的值,将参数和函数一起传入子组件,子组件调用函数,父组件就能接收子组件的值了,根据传回来的数据,以及判断URL,重新获取数据,根据state更新对应的值,渲染界面
2
3
4
5
第一个参数 是回调函数
第二个参数 是依赖
当第二个参数为null或undefined时,回调每次 render 都会执行,参数为数组时,只有依赖变了才会执行
VDOM转fiber的过程叫做reconcile,更新到DOM的过程叫做 commit,reconcile的过程可打断,需要schedule
hooks也是基于fiber实现,它在fiber节点上维护一个链表(memorizedState属性)来保存数据,每个hook都是从对应的链表元素上存取各自的数据
这个链表建立的过程叫做——mount,以后只需要update,所以每个hook的实现都分为mount和update两个阶段
useEffect第二个参数对应的是 deps,如何判断是否要更新??
deps是新传入的参数,若是undefined会作为null,hook.memorizedState.deps 取到的是之前的 deps
如果 prevDeps 是 null,直接返回 false,所以第二个参数传 undefined 或 null 函数都会执行
否则,才会新旧的 deps 数组中每个元素对比,有一个不一样就返回 false
# useMemo
当父组件中调用子组件时,父组件的state变化,会导致父组件更新,子组件即使没有变化,也会更新
函数式组件从头更新到尾,只要一处改变,所有模块都会刷新——没必要!
理想状态是 各个模块只进行 自己的更新,不相互影响
useMemo为了防止一种情况——父组件更新,无论是否最子组件操作,子组件都会更新
memo 结合了 PureComponent 纯组件 和 componentShouldUpdate 功能,会对传入的props进行一次对比,根据第二个函数返回值判断哪些props需要更新
useMemo和memo类似,都是 判断 是否满足当前限定条件决定是否执行 callback ,useMemo的第二个参数是一个数组,通过这个数组判断是否更新回调函数
好处
- 减少不必要 循环 和 渲染
- 减少 子组件 渲染次数
- 避免不必要 的开销
# useCallback
和useMemo 可以说是一模一样,唯一不同的是useMemo返回函数运行结果,而useCallback返回函数
缓存回调,避免传入的回调每次都是新的函数实例导致依赖组件re-render
是父组件传递子组件的 一个函数,防止无关 刷新,必须配合 memo,否则可能 还会降低 性能
# useRef
获取组件真实节点,返回一个可变的ref对象,且这个对象 只有 current 属性,可设置 initialValue
缓存数据
react-redux 源码中,hooks推出后,react-redux用大量的useMemo重做了 Provide 等核心模块,运用了useRef 缓存数据,所运用的useRef() 没有一个是绑定在dom元素上的,都是用于数据缓存
react-redux 利用 重新赋值,改变了缓存的数据,减少不必要更新,如果使用userState势必会re-render
# 为啥ref获取不到hooks?
# 总结
- 一个优秀的hooks一定具备 useMemo useCallback 等api 优化
- 制作自定义hooks遇到传递过来的值,优先考虑使用useRef,再考虑使用useState
- 封装时,应该将存放的值放入useRef中
# Hooks原理
hooks的本质是链表,保存在组件对应fiber的memoizedState中
# 🙋 ref
ref --> reference:引用
React中 引用简写,是一个 属性,助于 存储 对React元素 或 组件的引用,引用由组件渲染配置函数返回
ref使用有三种方式:字符串(不推荐使用),对象,函数
1、字符串 React16 之前用的最多的,如
<p ref="info">text</p>
2、函数格式,ref对应一个方法,该方法有一个参数,即,对应的节点实例
<p ref={ele=>this.info=ele}>test</p>
3、createRef方法,React16 提供的API,使用React.createRef()实现
# 使用场景
- input标签聚焦
- 希望直接使用DOM元素中的某个方法,或者直接使用自定义组件中的某一个方法
- ref作用于内置的HTML组件,得到的将是真实的DOM元素
- ref作用于类组件,得到的将是类的实例
- ref不能作用于函数组件,因为函数组价没有实例,但是函数组件中可以传递ref
# 如何使用
- class 组件
React.createRef()声明
class MyComponent extends React.Component { constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
2
3
4
5
6
7
8
- 在函数组件使用 React.forwardRef() 来暴露函数组件的 DOM 元素
const Input = forwardRef((props, ref) => {
return (
<input {...props} ref={ref}>
)
})
class MyComponent extends React.Component {
constructor() {
this.ref = React.createRef(null);
}
clickHandle = () => {
// 操作Input组件聚焦
this.ref.current.focus();
}
render() {
return (
<div>
<button onClick={this.clickHandle}>
点击
</button>
<Input ref={this.ref}>
</div>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# render不能访问refs
因为 render 阶段 DOM还未生成,无法获取 DOM,DOM的获取需要在 pre-commit 和 commit 阶段
<>
<span id="name" ref={this.spanRef}>{this.state.title}</span>
<span>{
this.spanRef.current ? '有值' : '无值'
}</span>
</>
2
3
4
5
6
不可以!
因为render阶段DOM还没有生成
DOM的获取应该在 pre-commit 阶段和 commit 阶段
# 🍊 高阶组件(HOC)
是"纯组价",解决**代码复用!**
不是react API 的一部分,是基于React的组合特性而形成的设计模式,HOC 是参数为 组**件,返回值为 新组件 的函数!**
优点
- 逻辑复用、不影响被包裹组件的内部逻辑
- 缺点是HOC传递给被 包裹组件的props 容易和被包裹后的组件 重名,被覆盖
此处朱云博培训时讲过,BasicLayout中就存在这个情况,不知道谁传递的参数,而且参数名容易被覆盖
HOC就是装饰器模式的实现:通过给函数传入一个组件后在函数内部对该组件 进行功能的增强,最后返回这个组件,即允许向一个现有的组件添加新的功能,同时不修改该组件
# 函数式编程
# 声明式编程
# 惰性执行(Lazy Evaluation)
函数只在需要的时候执行,即不产生无意义的中间变量。
# 核心概念
# 数据不可变
# 无状态
强调对于一个函数,不管合适运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。

# 没有副作用
传递引用一时爽,代码重构火葬场
# 纯函数
# 💙 开发模式
# SPA
单页面应用,我们熟知的React就是典型的单页面应用
# MPA
# MVVM
Model-View-ViewModel,Model数据模型,View表示UI组件,ViewModel将Model和View关联起来
数据绑定到viewModel层并自动将数据渲染到页面,视图变化时通知viewModel层更新数据
将JS逻辑从繁琐的DOM操作中抽离出来
通信是双向的

应用场景
- 针对具有复杂交互逻辑的前端应用
- 提供复杂架构抽象
- Ajax数据持久化,保证用户体验
# MVC
Model View Controller

所有通信都是单向的
# MVVM、MVC
MVC是后端的思想
MVVM是前端的思想
# 单向数据流框架始祖Flux
一个Flux应用包含四个部分
- dispatcher,处理动作分发,维持store之间的依赖管理
- store 负责存储数据和处理数据逻辑
- action 驱动dispatcher的JS对象
- view 视图部分,负责用户界面的显示
访问view——view发送 action——dispatcher接收action——store更新——发送change事件——view更新页面

# import react 导致文件变大?
Gzip压缩
# React路由懒加载
# React.lazy
React16.6新增React.lazy方法,像渲染常规组件一样处理动态引入的组件,配合 webpack 的 Code Splitting ,只有当组件被加载,对应的资源才会导入 ,从而达到懒加载的效果。
# 使用 React.lazy
// 不使用 React.lazy
import OtherComponent from './OtherComponent';
// 使用 React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'))
2
3
4
React.lazy 接受一个函数作为参数,这个函数需要调用 import() 。它需要返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。
React.lazy 方法返回的是一个 lazy 组件的对象,类型是 react.lazy,并且 lazy 组件具有 _status 属性,与 Promise 类似它具有 Pending、Resolved、Rejected 三个状态,分别代表组件的加载中、已加载、和加载失败三中状态。
需要注意的一点是,React.lazy 需要配合 Suspense 组件一起使用,在 Suspense 组件中渲染 React.lazy 异步加载的组件。如果单独使用 React.lazy,React 会给出错误提示。
# Suspense 组件
Suspense 内部主要通过捕获组件的状态去判断如何加载,上面我们提到 React.lazy 创建的动态加载组件具有 Pending、Resolved、Rejected 三种状态,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件
# ⭐️ VDom
一个对象,最少包含tag、props和children三个属性,将真实的DOM树转换为JS对象树,VD和dom对象一一对应
<button class="myButton">
<span>this is button</span>
</button>
//转换
{
type: 'button',
props: {
className: 'myButton',
children: [{
type: 'span',
props: {
type: 'text'
children: 'this is button'
}
}]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
将多次DOM修改的结果一次性更新到页面上,有效减少页面渲染次数,减少修改DOM的重排重绘次数,提高渲染性能
优越之处在于,提供更爽的、更高效的研发模式(函数式的UI编程模式)的同时,仍然保持还不错的性能
- 保证性能下限,不手动优化时,提供过得去的性能
- 跨平台
原生DOM有很多属性和事件,创建空的div也要付出很多代价,可通过diff算法和数据改变前的DOM对比,计算出需要修改的DOM,只对变化的DOM操作,而不是更新整个视图
# render()
1.构建虚拟 DOM
2.通过虚拟 DOM 构建真正的 DOM
3.生成新的 VDOM
4.比较两棵 VDOM 异同
5.应用变更于 DOM
# diff
DOM是多叉树结构,若完整对比两棵树的差异,时间复杂度高,React团队优化实现了O(n)的复杂度
关键是只对比同层节点,而不是跨层对比
- JS建立节点描述对象
- 状态或属性改变后重新计算VD生成补丁对象,diff比较分析新旧VDOM差异
- 将差异patch到ADOM实现更新
JS层面的计算返回一个patch对象,即补丁,通过特定的操作解析patch,完成页面re-render

- ADOM映射为VDOM
- VDOM变化后,根据差距生成patch,这个patch是结构化数据,包括增加 更新 和移除
- 根据patch更新 ADOM

大部分diff算法思路是
- 只比较同一层级,不跨级比较
- tag不同,直接删除重建,不再深度比较
- tag和key都相同,则认为是相同节点,不再深度比较
diff 从 树 组件 及 元素 3个层面进行复杂度的优化
树diff
新旧两棵树逐层对比,找到需要更新的节点
若节点是组件就 component diff
1、忽略节点跨层级操作
若节点不存在了,则该节点及其子节点会被删除掉,不会进一步比较
组件diff
节点是组件,先看组件类型
类型不同直接替换(删除旧的)
类型相同则只更新属性,深入组件做树diff(递归)
若组件 是同一类型就进行树 对比,如果不是就直接放入 patch中,只要父组件类型不一致,就会re-render
元素diff
若节点是原生标签,则看标签名
标签名不同直接替换,相同则只更新属性,进入标签后代做树diff
同一层级子节点,通过key 进行列表对比
标记key,React 可以直接移动 DOM节点,降低消耗
patch做了啥
# re-render
- 对比新旧 VNode,使用 diff算法
- 对新旧两棵树进行深度优先遍历,标记每个节点,每遍历一个,就把该节点和新的节点进行对比,有差异就放在一个对象中
- 遍历差异对象,根据对应 规则 更新VNode
VDOM 优势之处在于,不管数据如何变化,它都会尽量以最小的代码更新 DOM
# VDOM比原生快?
网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么? (opens new window)
# React17新特性
一起来看 React 18 最新特性 (opens new window)
React 18 带来了什么 ——官网 (opens new window)
V15-V16,源码架构中的 Stack Reconciler-Fiber Reconciler
启发式更新算法——对开发者无感知
React纯JS写法太过灵活,编译时优化 方面先天不足,因此,React的优化主要在 运行时
React15的痛点
React15 实现了batchdUpdates(批量更新),即 同一事件回调函数上下文中的多次 setState 只会触发一次 更新
但是,如果单次更新就很耗时,造成页面卡顿
因为v15的更新流程是同步的,一旦开始直到页面渲染前都不能中断
为了解决 同步更新 长时间占用 线程 导致页面卡顿的问题,React开始重构,为了实现 Concurrent Mode(并发模式)
Concurrent Mode
为了 实现 一套 可中断/恢复 的更新机制
2个部分组成
- 一套 协程 架构
- 基于 协程架构的 启发式更新算法
协程架构就是V16实现的Fiber Reconciler,可将Fiber Reconciler理解为React自己实现的Generator
协程架构 使 更新 可在需要时被 中断,酱紫 浏览器就有时间 完成 样式布局和绘制,减少 卡顿
当进入下一次 Event Loop,协程架构 可 恢复中断 或 抛弃之前的 更新,重新开始新的更新流程
启发式更新算法 就是控制 协程架构 工作方式的算法
V16的启发式更新算法
启发式 ——不通过 显式指派,而是 通过 优先级 调度更新(优先级来自 人机交互的研究成果)
人机交互研究结果表明:
用户在输入框输入内容时,希望内容能实时显示
异步请求时,即使等待一会儿再显示内容,用户勉强也能接受
因此,V16中
输入框内容触发的 更新 优先级 >请求数据返回 后触发 更新 优先级
V16的 expirationTimes模型只区分是否>=expirationTimes 决定节点是否更新
V17启发式更新算法
最理想的模型——可指定任意几个 优先级,更新 以这些 优先级 对应的 update 生成页面快照
V17的lanes模型可选定一个 更新区间,动态向 区间 增减 优先级,可处理更细粒度的更新
# 🔥 Fiber
JS单线程运行,负责页面JS解析和执行、绘制、事件处理、资源加载和处理,只能一个一个执行,若一个任务耗时很长,后面的任务则不能执行,卡死。
JS引擎和页面渲染引擎两个线程互斥。
React 15 ,渲染时 递归对比 VDOM,找需要变动的节点,同步更新,一气呵成!!此过程React霸占浏览器资源,导致用户触发的事件得不到响应,掉帧 卡顿!!Stack Reconciler是同步过程,使用JS引擎自身的函数调用栈,执行到栈空为止,渲染组件时,开始到渲染完成整个过程一气呵成,无法中断
组件较大,js线程一直执行,等到整棵VDOM树计算完成 才会交给渲染的线程
所有任务没有优先级,按照顺序执行
期望找出有增删改的节点,同步更新这个过程分解成2个部分,或变成可中断/恢复的执行,类似 多任务OS的单处理调度
将浏览器 渲染 布局 绘制 资源加载 事件响应 脚本执行 看成OS的process,通过 调度策略合理分配 CPU 资源,提高浏览器 响应速率,兼顾 执行效率
React16起,引入 Fiber 架构
fiber是 链表,可以打断,可通过 requestIdleCallback 空闲调度 reconcile,不断循环,直到处理完所有的 VDOM 转fiber的reconcile,开始 commit,即 更新到ADOM
requestIdleCallback实现,React 团队 polyfill 了这个 API,使其兼容性更好且拓展了特性
# window.requestIdleCallback()
浏览器空闲时 被调用,使 开发者能在主事件循环上执行后台和低优先级工作,不影响延迟 关键事件,如 动画 和输入响应
函数一般按先进先调用顺序执行,然而,如果回调指定了执行超时时间 timeout,可能会为了在超时前执行函数 打乱执行顺序

- 分批延时 操作 DOM,避免一次性操作大量 DOM 节点,用户体验更好
- 给浏览器一点喘息的机会,对代码进行 编译 优化 及热代码优化
手写简易版 React 来彻底搞懂 fiber 架构 (opens new window)
实现元素的更新
# 双缓冲优化策略
连体婴结构
先绘制到一个缓冲区,再一次性传递给屏幕显示,防止抖动
好处
- 及时复用对象(fiber)
- 节省内存分配,GC时间开销
当前dom树对应的fiber树交currentFiber,正在构建的fiber树交workInProgress Fiber,两棵树 通过alternate相连

- 将每次渲染完后的fiber树赋值给
currentRoot - 第一次更新时将
rooterFiber的alternate指向上一次渲染好的currentRoot - 第二次之后的更新将
workInProgressRoot指向currentRoot.alternate,然后将当前的workInProgressRoot.alternate指向上一次渲染好的currentRoot - ...
- 进而达到复用fiber对象树
alternate对象会接收上方传递的新props,从getDerivedStateFromProps得到新的state,render不一样的组员,master和alternate差异越来越大,子组件出错 则回滚到组件的master分支
模拟git add、git commit和git revert
更新过程由current和workInProgress 两株树双缓冲 完成
workInProgress更新完成后 修改 current 相关指针 指向新节点
双缓冲:经过reconcile形成新的workInProgress Fiber,将workInProgress Fiber切换成current Fiber应用到ADOM,存在双fiber的好处是内存形成视图的描述,应用到dom,减少对dom的操作
# React渲染机制

# 获取dom
react-dom中的findDOMNode()
import ReactDOM from 'react-dom';
componentDidMount(){
const dom = ReactDOM.findDOMNode(this);
// this为当前组件的实例
}
render() {}
2
3
4
5
6
7
当组件被渲染 ,findDOMNode 返回该组件实例对应的DOM节点
refs
多用于React组件内子组件的引用
有两种情况:
(1)子组件为原生DOM:获取到的就是这个DOM节点。如下例,this.input就获取到了当前<input />节点。
<input ref={(ref)=>{this.myInput = ref}} />
通过this.myInput,我就可以对<input />进行一系列操作,比如让输入框聚焦:
this.myInput.focus();
注:refs也支持字符串格式:
<input ref='myInput' />
通过this.refs.myInput获取到节点。
(2)子组件为React组件,比如<MyInput/>:获得的就是<MyInput/>的实例,因此就可以调用<MyInput/>的实例方法。
示例:
componentDidMount(){
const myComp = this.refs.myComp; // 获取到的是<Comp />的实例myComp
const dom = ReactDOM.findDOMNode(myComp); // 获取到实例对应的DOM节点
}
render(){
return (
<div>
<Comp ref='myComp' />
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
注:调用<Comp />实例方法的方式:this.refs.myComp.method(),但并不建议这种调用方式。
# others
# dangerouslySetInnerHTML
是REact中innerHTML的替代品
网球系统中使用过,用来接收新闻正文,显示新闻数据HTML
<div dangerouslySetInnerHTML={{ __html: newsTexts }} />
React的HTML元素上的一个属性,它可能是危险的,因为我们容易收到XSS(跨站脚本攻击)——从第三方获取数据或用户提交内容时
React会识别HTML标签,然后渲染
HTML元素可能会执行脚本,当JS代码附加到HTML元素上
使用HTML净化工具DOMPurify检测HTML中潜在的恶意部分
# import React from 'react'
在使用JSX的JS文件中,必须显示声明,为啥?
JSX在编译时会被Babel编译为React.createElement()方法
否则在运行时该模块会报错——未定义变量React
React17中,不需要显式导入React