重学 React.js
React.js
声明式 组件化 单向数据流 纯函数 不可变数据 函数式编程
React.js 官网
React 需要使用 JSX,有一定的上手成本,并且需要一整套的工具链支持,但是完全可以通过 JS 来控制页面,更加的灵活。Vue 使用了模板语法,相比于 JSX 来说没有那么灵活,但是完全可以脱离工具链,通过直接编写 render 函数就能在浏览器中运行。
setState
- setState 触发时机受 React 控制,
异步更新
- 生命周期内触发
- 合成事件内触发
- 触发时机不在 React 所控制范围内,
同步更新
- setTimeout setInterval
- 自定义的 DOM 事件
- promise.then
- Ajax 回调
- setState 默认
合并
同步更新
不会合并传入函数
不会合并
React 18 更新,setState 都是
异步
合并
的了 Automatic Batching自动批处理
1 | class MyComponent extends React.Component { |
组件通信
- 父子组件通信
- props 属性和方法
- 兄弟组件通信
- 通过共同的父组件来管理数据状态和事件函数
- 跨层级组件通信
- Context API
- 任意组件通信
- Redux
- EventBus
生命周期
mixin
- 隐式依赖,mixin之间,mixin和组件之间
- 命名冲突,会被重写
- 维护成本高
HOC 高阶组件
- 扩展性限制: 无法从外部访问子组件的 state, 因此无法通过 shouldComponentUpdate 滤掉不必要的更新, React.PureComponent 来解决
- Ref 传递问题: Ref 被隔断, React.forwardRef 来解决
- Wrapper Hell: 多层高阶组件包裹时, 多层抽象同样增加了复杂度和理解成本
- 命名冲突: 如果高阶组件多次嵌套, 没有使用命名空间的话会产生冲突, 覆盖老属性
1 | // 高阶函数 |
首先实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码
Render Props
通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件
React Hooks
- 简洁: 解决了 HOC 和 Render Props 的嵌套问题
- 解耦: 更方便地把 UI 和状态分离
- 组合: Hooks 中可以引用另外的 Hooks 形成新的 Hooks, 组合变化万千
- 函数友好: React Hooks 为函数组件而生, 从而解决了类组件的几大问题
- this 指向容易错误
- 分割在不同声明周期中的逻辑使得代码难以理解和维护
- 代码复用成本高, 高阶组件容易使代码量剧增
事件机制
- 通过
事件代理
绑定在document
上 - 不是原生浏览器事件,而是 React 自己实现的
合成事件
SyntheticEvent - 阻止事件冒泡,调用 event.stopPropagation 是无效的,而应该调用
event.preventDefault
为什么要实现合成事件?
- 抹平了浏览器之间的兼容问题,同时赋予了跨浏览器开发的能力
- 事件池管理事件的创建和销毁,实现事件对象的复用,减少内存
Fiber
在 React 16 版本中引入了 Fiber 机制。Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染
,在不影响体验的情况下去分段计算更新组件渲染过程可以暂停
DOM Tree 转换为 链表
,循环才有可能随时中断、再继续。树结构递归到底,中间无法断开
时间分片 Time Slice 基于 Fiber 架构
对于异步渲染,有两个阶段reconciliation
和commit
- Reconciliation 阶段
可以被打断的,钩子函数会执行多次
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- Commit 阶段
不能暂停,一直更新界面直到完成
- componentDidMount
- componentDidUpdate
- componentWillUnmount
- 除了
shouldComponentUpdate
之外,其他都应该避免去使用 getDerivedStateFromProps
用于替换 componentWillReceiveProps, 该函数会在初始化和 update 时被调用getSnapshotBeforeUpdate
用于替换 componentWillUpdate, 该函数会在 render 后 componentDidUpdate 前调用,用于读取最新的 DOM 数据
requestAnimationFrame
- 每次渲染都执行,
高优
- 宏任务
- 可用于动画效果
requestIdleCallback
- 渲染完成后,CPU 空闲时才执行,
低优
,不一定每一帧都执行 - 宏任务
- 用于低优先级的任务处理,Fiber 核心 API
性能优化
循环使用 key
修改 css 模拟 v-show
1 | render() { |
使用 Fragment 减少层级
1 | render() { |
JSX 中不要定义函数
在 JSX 中定义函数,每次组件更新时都会初始化该函数,带来不必要的开销
1 | class MyComponent extends React.Component { |
函数组件 使用 useCallback 缓存函数
在构造函数 bind this
同理,如果在 JSX 中 bind this,那每次组件更新时都要 bind 一次
或者,直接使用箭头函数
1 | class MyComponent extends React.Component { |
如果是函数组件,则不用 bind this
shouldComponentUpdate 控制组件渲染
React 默认情况下,只要父组件更新,其下所有子组件都会“无脑”更新。如果想要手动控制子组件的更新逻辑
- 可使用
shouldComponentUpdate
判断 - 或者组件直接继承
React.PureComponent
,相当于在shouldComponentUpdate
进行 props 的浅比较
但此时,必须使用不可变数据,例如不可用 arr.push
而要改用 arr.concat
不可变数据第三方库
React 默认情况(子组件“无脑”更新)这本身并不是问题,在大部分情况下并不会影响性能。因为组件更新不一定会触发 DOM 渲染,可能就是 JS 执行,而 JS 执行速度很快。所以,性能优化要考虑实际情况,不要为了优化而优化。
React.memo 缓存函数组件
如果是函数组件,没有 shouldComponentUpdate
和 React.PureComponent
。React 提供了 React.memo
来缓存组件
1 | function MyComponent(props) { |
useMemo 缓存数据、useCallback 缓存函数
1 | function App(props) { |
异步组件
1 | import React, { lazy, Suspense } from 'react' |
路由懒加载
1 | import React, { lazy, Suspense } from 'react' |
SSR 服务端渲染
- Next.js
避坑指南
在 JSX 中,自定义组件命名,开头字母要大写,html 标签开头字母小写
1
2
3
4
5{/* 原生 html 组件 */}
<input/>
{/* 自定义组件 */}
<Input/>JSX 中
for
写成htmlFor
,class
写成className
1
2
3<label htmlFor="input-name" className="input-name">
姓名 <input id="input-name" />
<label>state 作为
不可变数据
,不可直接修改,使用纯函数
1
2
3
4// this.state.list.push() 👎
this.setState({
list: this.setState.concat()
})在 JSX 中,属性要区分 JS 表达式和字符串
1
2<Demo position={1} flag={true} />
<Demo position="1" flag="true" />state 是
异步更新
的,要在 callback 中拿到最新的 state 值1
2
3this.setState({ count: this.state.count++ }, () => {
console.log(this.state.count)
})useEffect
内部不能修改 state1
2
3
4
5
6
7
8
9
10
11
12
13
14function App() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // 如果依赖是 [] ,这里 setCount 不会成功
}, 1000)
return () => clearTimeout(timer)
/* Hooks 闭包陷阱 */
}, [count]) // 只有依赖是 [count] 才可以,这样才会触发组件 update
return <div>count: {count}</div>
}useEffect
依赖项里有对象、数组,会出现死循环1
2
3
4useEffect(() => {
/* React Hooks 是通过 Object.is 进行依赖项的前后比较
如果是引用类型,前后的值是不一样的(纯函数)*/
}, [obj, arr])
错误监控
ErrorBoundary 渲染错误
1 | ReactDOM.render( |
- React 16+ 引入。可以监听所有下级组件报错,同时降级展示 UI
- dev 环境下无法看到 ErrorBoundary 的报错 UI 效果,会显式的提示报错信息
componentDidCatch
window.onerror try…catch 其它错误、异步
1 | window.onerror = function(msg, source, line, column, error) { |