React.js

声明式 组件化 单向数据流 纯函数 不可变数据 函数式编程
React.js 官网

ahooks

React 需要使用 JSX,有一定的上手成本,并且需要一整套的工具链支持,但是完全可以通过 JS 来控制页面,更加的灵活。Vue 使用了模板语法,相比于 JSX 来说没有那么灵活,但是完全可以脱离工具链,通过直接编写 render 函数就能在浏览器中运行。

setState

  1. setState 触发时机受 React 控制, 异步更新
    • 生命周期内触发
    • 合成事件内触发
  2. 触发时机不在 React 所控制范围内, 同步更新
    • setTimeout setInterval
    • 自定义的 DOM 事件
    • promise.then
    • Ajax 回调
  3. setState 默认合并
    • 同步更新不会合并
    • 传入函数不会合并

React 18 更新,setState 都是异步 合并的了 Automatic Batching 自动批处理

1
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
29
30
31
32
33
34
35
36
class MyComponent extends React.Component {
constructor() {
super()
this.state = { count: 0 }
}
componentDidMount() {
console.log(this.state.count) // 0
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
/* Object.assign(
{},
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
) */
console.log(this.state.count) // 0

this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
console.log(this.state.count) // 4
})

setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 5
/* 同步更新,不会合并 */
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 6
}, 0)
}
render() {
return <div>{this.state.count}</div>
}
}

组件通信

  1. 父子组件通信
    • props 属性和方法
  2. 兄弟组件通信
    • 通过共同的父组件来管理数据状态和事件函数
  3. 跨层级组件通信
    • Context API
  4. 任意组件通信
    • Redux
    • EventBus

生命周期

mixin

  • 隐式依赖,mixin之间,mixin和组件之间
  • 命名冲突,会被重写
  • 维护成本高

HOC 高阶组件

  • 扩展性限制: 无法从外部访问子组件的 state, 因此无法通过 shouldComponentUpdate 滤掉不必要的更新, React.PureComponent 来解决
  • Ref 传递问题: Ref 被隔断, React.forwardRef 来解决
  • Wrapper Hell: 多层高阶组件包裹时, 多层抽象同样增加了复杂度和理解成本
  • 命名冲突: 如果高阶组件多次嵌套, 没有使用命名空间的话会产生冲突, 覆盖老属性
1
2
3
4
5
6
7
8
9
10
11
// 高阶函数
function withLog (fn) {
function wrapper(a, b) {
const result = fn(a, b)
console.log(result)
return result
}
return wrapper
}
const withLogAdd = withLog(add)
withLogAdd(1, 2)

首先实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码

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 架构

对于异步渲染,有两个阶段reconciliationcommit

  1. Reconciliation 阶段 可以被打断的,钩子函数会执行多次
    • componentWillMount
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
  2. Commit 阶段 不能暂停,一直更新界面直到完成
    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount
  • 除了shouldComponentUpdate之外,其他都应该避免去使用
  • getDerivedStateFromProps用于替换 componentWillReceiveProps, 该函数会在初始化和 update 时被调用
  • getSnapshotBeforeUpdate用于替换 componentWillUpdate, 该函数会在 render 后 componentDidUpdate 前调用,用于读取最新的 DOM 数据

requestAnimationFrame

  • 每次渲染都执行,高优
  • 宏任务
  • 可用于动画效果

requestIdleCallback

  • 渲染完成后,CPU 空闲时才执行,低优,不一定每一帧都执行
  • 宏任务
  • 用于低优先级的任务处理,Fiber 核心 API

性能优化

循环使用 key

修改 css 模拟 v-show

1
2
3
4
5
6
7
8
render() {
return <>
{!flag && <MyComponent style="display: none;" />}
{flag && <MyComponent />}

<MyComponent style={{display: flag ? 'block' : 'none'}} />
</>
}

使用 Fragment 减少层级

1
2
3
4
5
6
render() {
return <>
<p>hello</p>
<p>world</p>
</>
}

JSX 中不要定义函数

在 JSX 中定义函数,每次组件更新时都会初始化该函数,带来不必要的开销

1
2
3
4
5
6
7
8
9
10
11
class MyComponent extends React.Component {
clickHandler = () => { /* */ }
render() {
return <>
{/* Bad */}
<button onClick={() => { /* */ }}>点击</button>
{/* Good */}
<button onClick={this.clickHandler}>点击</button>
</>
}
}

函数组件 使用 useCallback 缓存函数

在构造函数 bind this

同理,如果在 JSX 中 bind this,那每次组件更新时都要 bind 一次
或者,直接使用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyComponent extends React.Component {
constructor() {
super()
this.clickHandler = this.clickHandler.bind(this)
}
clickHandler() { /* ... */ }
// clickHander = () => { }
render() {
return <>
<button onClick={this.clickHandler}>点击</button>
</>
}
}

如果是函数组件,则不用 bind this

shouldComponentUpdate 控制组件渲染

React 默认情况下,只要父组件更新,其下所有子组件都会“无脑”更新。如果想要手动控制子组件的更新逻辑

  • 可使用 shouldComponentUpdate 判断
  • 或者组件直接继承 React.PureComponent ,相当于在 shouldComponentUpdate 进行 props 的浅比较

但此时,必须使用不可变数据,例如不可用 arr.push 而要改用 arr.concat

不可变数据第三方库

  • 深拷贝
  • immutable 自成一体的一套数据结构
  • immer 👍 推荐 利用Proxy特性 学习成本低

React 默认情况(子组件“无脑”更新)这本身并不是问题,在大部分情况下并不会影响性能。因为组件更新不一定会触发 DOM 渲染,可能就是 JS 执行,而 JS 执行速度很快。所以,性能优化要考虑实际情况,不要为了优化而优化。

React.memo 缓存函数组件

如果是函数组件,没有 shouldComponentUpdateReact.PureComponent。React 提供了 React.memo 来缓存组件

1
2
3
4
5
6
7
function MyComponent(props) {

}
function areEqual(prevProps, nextProps) {
// 自定义比较函数 类似 shouldComponentUpdate
}
export default React.memo(MyComponent, areEqual)

useMemo 缓存数据、useCallback 缓存函数

1
2
3
4
5
6
7
8
9
10
11
function App(props) {
const [num1, setNum1] = useState(100)
const [num2, setNum2] = useState(200)

// 缓存数据 类似 Vue computed
const sum = useMemo(() => num1 + num2, [num1, num2])

// const fn = useCallback(() => { ... }, [...]) 缓存函数

return <p>hello world</p>
}

异步组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { lazy, Suspense } from 'react'

const AsyncComponent = lazy(
/* webpackChunkName: 'AsyncComponent' */
() => import('./AsyncComponent')
)

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</div>
)
}

路由懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { lazy, Suspense } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'

const Home = lazy(() => import('./Home'))
const List = lazy(() => import(
/* webpackChunkName: 'Home' */
'./List'
))

const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/list" component={List} />
</Switch>
</Suspense>
</Router>
)

SSR 服务端渲染

  • Next.js

避坑指南

  1. 在 JSX 中,自定义组件命名,开头字母要大写,html 标签开头字母小写

    1
    2
    3
    4
    5
    {/* 原生 html 组件 */}
    <input/>

    {/* 自定义组件 */}
    <Input/>
  2. JSX 中 for 写成 htmlForclass 写成 className

    1
    2
    3
    <label htmlFor="input-name" className="input-name">
    姓名 <input id="input-name" />
    <label>
  3. state 作为不可变数据,不可直接修改,使用纯函数

    1
    2
    3
    4
    // this.state.list.push() 👎
    this.setState({
    list: this.setState.concat()
    })
  4. 在 JSX 中,属性要区分 JS 表达式和字符串

    1
    2
    <Demo position={1} flag={true} />
    <Demo position="1" flag="true" />
  5. state 是异步更新的,要在 callback 中拿到最新的 state 值

    1
    2
    3
    this.setState({ count: this.state.count++ }, () => {
    console.log(this.state.count)
    })
  6. useEffect 内部不能修改 state

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function 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>
    }
  7. useEffect 依赖项里有对象、数组,会出现死循环

    1
    2
    3
    4
    useEffect(() => {
    /* React Hooks 是通过 Object.is 进行依赖项的前后比较
    如果是引用类型,前后的值是不一样的(纯函数)*/
    }, [obj, arr])

错误监控

ErrorBoundary 渲染错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
)

/* 函数组件 */
function App(props) {
return <ErrorBoundary>
{props.children}
</ErrorBoundary>
}
  • React 16+ 引入。可以监听所有下级组件报错,同时降级展示 UI
  • dev 环境下无法看到 ErrorBoundary 的报错 UI 效果,会显式的提示报错信息

componentDidCatch

window.onerror try…catch 其它错误、异步

1
2
3
4
window.onerror = function(msg, source, line, column, error) {
console.log(msg, source, line, column, error)
console.log(`https://stackoverflow.com/search?q=[js]+${msg}`)
}