react系列-react入门

React官网:https://reactjs.org/docs/getting-started.html

Creat-react-app官网:https://create-react-app.dev/

教程:

https://github.com/the-road-to-learn-react

构建

脚手架工具搭建项目的优势:

  • 帮助我们搭建项目的通用性目录结构,进行通用的组织划分
  • 管理文件之间的项目依赖
  • 项目打包和压缩等通用性配置

添加 ts 支持

https://create-react-app.dev/docs/adding-typescript/

创建一个新的 app :

npx create-react-app my-app --template typescript

# or

yarn create react-app my-app --template typescript

在已存在的项目中添加 typescript 支持:

npm install --save typescript @types/node @types/react @types/react-dom @types/jest

# or

yarn add typescript @types/node @types/react @types/react-dom @types/jest

基础

常见概念

声明式编程

将传统的命令式编程,如jQuery,转变为声明式编程

声明式写法强调结果的,命令式写法是强调过程的。

React 元素是不可变对象。一旦被创建,就无法更改它的子元素或者属性。

React组件一般不提供方法,而是某种状态机。

状态提升:通常情况下,同一个数据的变化需要几个不同的组件来反映。建议提升共享的状态到它们最近的祖先组件中。

在一个 React 应用中,对于任何可变的数据都应该循序“单一数据源”原则。

JSX

在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。

React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

无效值不会展示在前端界面,如undefined、null、NaN等数据,如果需要展示则要转换成 string 的数据格式。

语法

严格区分大小写。默认组件首字母大写。首字母小写则会被判断为普通标签元素

html 中是对大小写不敏感

注释写法
{/* 注释 */}

js中的关键字不能和html标签中的属性混合,如 html标签中的class属性 需要使用 className 替代,与js中的class 关键字区分开。label标签的for属性需要使用htmlFor进行替代。

本质

可以使用 babeljs 在线转换工具 进行体验。

jsx写法(script 标签需要写type=”text/babel”,进行转义):

const msg = <h2>hello react</h2>
ReactDOM.reader(msg, decument.getElementById('app'))

javascript写法(script 标签上可以不用写type):

const msg2 = React.createElement('h2', null, 'hello react')
ReactDOM.reader(msg2, decument.getElementById('app'))

jsx -> babel ->React.createElement()

React.createElement 纯函数最终创建出来一个 ReactElement 对象。

React 利用 ReactElement 对象组成了一个 JavaScript 的对象树(AST树,即虚拟DOM树)

ReactDOM.render 将对象树渲染到目标节点中。

state

不能直接修改state。出于性能考虑,使用setState()去修改 state 。

因为 this.propsthis.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

setState() 之后会重新进行 render(重新渲染)。

setState

react 中没有实现数据的响应式,所以数据更新需要进行手动调用 setState 方法来告知 react 数据发生了变化。

异步更新数据(但是具体也要分情况,有存在同步的情况)。

在组件生命周期或 React 合成事件中,setState 是异步的;

在 setTimeout 或 原生 DOM 事件中,setState 是同步的;

为什么设计为异步更新?

参考链接:

https://zh-hans.reactjs.org/docs/faq-state.html#when-is-setstate-asynchronous

https://github.com/facebook/react/issues/11527#issuecomment-360199710

  • setState 设计为异步,可以显著的提升性能;
    • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步;
    • state 和 props 不能保持一致性,会在开发中产生很多的问题;
如何获取异步更新后的数据

方法一:使用 setState 方法的回调函数,回调函数会等待 setState 数据更新后立即执行。

类似 vue 中的 nextTick

方法二:生命周期函数 componentDidUpdate ,会在 render 函数执行后执行。此时可以拿到最新的 state 数据。

会先执行 componentDidUpdate 之后再执行 setState 的回调函数

setState 同步的情况
// 情况一:将 setState 放入到定时器中
changeText() {
    setTimeout(() => {
        this.setState({
            message: 'xxxx'
        })
        console.log(this.state.message) // xxxx
    }, 0)
}
// 情况二:原生DOM事件
componentDidMount() {
    const btnEl = document.getElementById('btn')
    btnEl.addEventListener('click', () => {
        this.setState({
            message: 'xxxx'
        })
        console.log(this.state.message) // xxxx
    })
}
合并
  • 本身调用合并

    同时多次调用 setState 会合并成一个操作, react 内部会维护一个数据更新队列,将队列中的结果进行合并,执行最终的数据更新结果。

    increment() {
        console.log(this.state.counter) // 0
        this.setState({
            counter: this.state.counter + 1
        })
        this.setState({
            counter: this.state.counter + 1
        })
        this.setState({
            counter: this.state.counter + 1
        })
    }
    // 最终的结果是 1
  • 合并时进行累加

    将同时多次调用 setState 传入函数的方式,可以将上一次更新的数据用在下一次更新数据中。

    increment() {
        console.log(this.state.counter) // 0
        this.setState((prevState, props) => {
            return {
                counter: prevState.counter + 1
            }
        })
        this.setState((prevState, props) => {
            return {
                counter: prevState.counter + 1
            }
        })
        this.setState((prevState, props) => {
            return {
                counter: prevState.counter + 1
            }
        })
        // 一次性就会加 3次
    }
    // 最终的结果是 3
setState 传递的数据需要是不可变的数据

参考链接:https://zh-hans.reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data

如直接修改引用类型的数据,shouldComponentUpdate 函数中进行数据是否变更对比时,因为引用类型地址未变,所以拿到的新旧数据都是一致的(已更新过的),这是就会容易出现无法监控到数据变更问题 。

深层拷贝容易消耗性能,不能滥用。

更新机制
渲染流程

JSX ——> 虚拟 DOM ——> 真实 DOM

更新流程
  1. props / state 改变
  2. render 函数重新执行
  3. 产生新的 DOM 树
  4. 新旧 DOM 树进行 diff 比较
  5. 计算出差异进行更新
  6. 更新到真实的 DOM
diff 算法

当前最优的diff算法进行了优化(简化),时间复杂度从O(n^3)变为O(n).

  • 同层节点之间相互比较,不会跨节点比较;

  • 不同类型的节点,产生不同的树结构;

    • 当节点为不同的元素,React会拆卸原有的树,并且建立起新的树;这个流程会触发组件的生命周期。

      即如果新旧DOM树中父节点和不同就不会再进行比较子节点了,即使子节点相同,会更新以父节点为根的整棵DOM子树

    • 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅更新有改变的属性。会继续向下比对子节点。

      变更的属性细粒程度,如 style 中的 color 元素改变,其他元素不变时,仅更新 color 元素。

    • 如果是同类型的组件元素,组件会保持不变,React 会更新该组件的 props,并且调用相关更新的生命周期函数后,调用 render 方法,diff 算法会在 render 函数后再进行递归比较。

    • 同时遍历新旧DOM树节点,如第一个节点和第一个节点进行比较。例如,list 向前插入数据,就会发现第一个子节点不一样,react 就会直接更新节点第一个节点,而旧树的第二个和新树的第三个节点相同也不会进行复用。

      这里和 vue 2.x中的diff算法不同,vue 中的diff算法即使没有key值也会比对普通节点的元素是否完全一致。

  • 开发中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定;

    当子元素有 key 属性时,React 使用 key 来匹配旧DOM树上的子元素以及最新树上的子元素。可以提高树的转换效率和复用程度。

    使用数组下标作为key值,如果数组元素不进行重新排序时比较合适,如果有顺序修改则 diff 就会变慢。

    所以避免使用数组下标或随机数作为key值,这样和不使用key值可能效率一样。

react 中的 state 如何管理

管理方式:

  • 组件中自己的 state 管理
  • Context 数据的共享状态
  • Redux 管理应用状态

建议的 state 管理方案:

  • UI 相关的组件内部可以维护的状态,在组件内部自己来维护;
  • 大部分需要共享的状态,都交给 redux 来管理和维护;
  • 从服务器请求的数据(包括请求的操作),交给 redux 来维护;

虚拟DOM

原因:

  • 很难跟踪状态的发生;

  • 频繁操作真实DOM性能较低

    比如容易频繁引起浏览器的回流和重绘,虚拟DOM比较容易批量进行DOM操作

优势:

  • 从命令式编程(传统jQuery方式)转到了声明式编程模式
  • UI以一种虚拟化的方式保存在内存中,并且是一个相对简单的 JavaScript 对象
  • 协调:通过ReactDOM.render 让虚拟DOM 和 真实DOM 同步起来

事件

React 元素的事件处理:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

对于绑定原生 DOM 上的事件返回的都是合成事件。

const el = document.getElementById('app')
class App extends React.Component {
    constructor() {
        super()
        this.state = {
            msg: 'hello world',
            list: [1, 2, 3, 4]
        }
    }

    // 写法一
    render() {
        const { state, change } = this
        // 列表写法一
        const listArr = []
        for (let item of state.list) {
            listArr.push(<li>{ item }</li>)
        }

        return (
            <div>
                <h2>{ state.msg }</h2>
                <button onClick={change.bind(this)}>change</button>
                <ul>
                    { /* 列表写法二 */}
                    {
                        state.list.map(item => {
                            return (<li>{ item }</li>)
                        })
                    }
                    { listArr }
                </ul>  
            </div>
        )
    }

    change() {
        this.setState({
            msg: 'hello react'
        })
    }

    // 写法二
    // render() {
    //     const { state } = this
    //     return (
    //         <div>
    //             <h2>{ state.msg }</h2>
    //             <button onClick={() => this.setState({ msg: 'hello react' })}>change</button>    
    //         </div>
    //     )
    // }
}

ReactDOM.render(<App />, el)
合成事件SyntheticEvent

SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()

如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可合成事件与浏览器的原生事件不同,也不会直接映射到原生事件。例如,在 onMouseLeave 事件中 event.nativeEvent 将指向 mouseout 事件。

React 通过将事件 normalize 以让他们在不同浏览器中拥有一致的属性。

大部分事件处理函数在冒泡阶段被触发。如需注册捕获阶段的事件处理函数,则应为事件名添加 Capture例如,处理捕获阶段的点击事件请使用 onClickCapture,而不是 onClick

当使用事件时,需要查看是否有合成事件,如果没有则需要使用原生事件。

参考链接:

React合成事件和DOM原生事件混用须知

React 合成事件和 DOM 原生事件混用

组件化

分而治之思想的应用。

尽可能的将页面拆分成一个个小的、可复用的组件,降低代码耦合性,更加方便组织和管理,并且扩展性也更强。

React 的规则:所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

单向数据流,数据从父组件通过 props 流向子组件,而子组件不能修改父组件的内容。

组件分类

相对于Vue而言,可以按照不同的方式分成很多类组件:

  • 根据组件的定义方式,分为:函数式组件(Functional Component)和类组件(Class Component)
  • 根据组件内部是否有状态需要维护,分为:无状态组件(Stateless Component)和有状态组件(Stateful Component)
  • 根据组件的不同职责,分为:展示型组件(Presentational Component)和容器型组件(Container Component)

组件分类具有概念重叠部分,还有其他组件概念如异步组件、高阶组件等

总结:

最主要是关注数据逻辑和UI展示的分离,

  • 函数组件、无状态组件、展示型组件主要关注UI的展示
  • 类组件、有状态组件、容器型组件主要关注数据逻辑
类组件

定义:

  • 组件的名称是首字母大写(所有的组件名称约定为大写驼峰
  • 类组件需要继承自 React.Component
  • 类组件必须实现 render 函数

使用 class 定义一个组件:

  • constructor 是可选的,通常在constructor 中初始化一些数据
  • this.state 中维护的就是组件内部的数据
  • render() 方法是 class 组件中唯一必须实现的方法

缺点:

  • 复杂组件变得难以理解,难以拆分。强行拆分反而会造成过度设计,增加代码的复杂度。
  • 组件复用状态很难。类似于 Provider、Consumer类共享一些状态,多次使用Consumer时,代码会出现很多层嵌套。
函数式组件

定义:函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。

特点:

  • 没有this对象
  • 没有内部状态,不需要涉及到状态更新和数据交互(可以使用hooks)
  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数

生命周期

从创建到销毁的过程

生命周期和生命周期函数的关系:

React 内部为了告诉我们当前处于那些阶段,会对组件内部实现的某些函数进行回调,这些就是生命周期函数。

常用的生命周期函数

react生命周期函数part

constructor

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

constructor中通常只做两件事情:

  • 通过给 this.state 赋值对象来初始化内部的state
  • 为事件绑定实例(this)

避免把props赋值给state

render()

是 class 组件中唯一必须实现的方法。

render函数应该为纯函数,并且不会直接与浏览器交互。

render函数的返回值

当 render 被调用时,会检查 this.props 和 this.state 的变化并返回以下类型之一:

  • React元素

    • 通常通过 JSX 创建
  • 数组或 fragments(片段):使得 render 方法可以返回多个元素

  • Portals:可以渲染子节点到不同的 DOM 子树中

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

  • 字符串或数值类型:在DOM 中会被渲染为文本节点

  • 布尔类型或 null:什么都不渲染

componentDidMount()

componentDidMount() 会在组件挂载后(插入DOM树中)立即调用

componentDidMount 中通常进行:

  • 依赖于DOM操作可以在这个方法中执行
  • 在此处发送网络请求(官方建议)
  • 可以在此处添加一些订阅和监听(会在componentWillUnmount取消订阅和监听)
componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法

  • 当组件更新后,可以在此处对 DOM 进行操作
  • 如果对更新前后的props进行了比较,可以选择在此处进行网络请求(例如,当 props 未变化时,则不会执行网络请求)

setState 时注意避免导致死循环

如果组件实现了 getSnapshotBeforeUpdate() 方法,则它的返回值作为 componentDidUpdate() 的第三个参数”snapshot”参数传递,否则此参数将为 undefined 。

componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前直接调用

  • 在此方法中执行必要的清理操作,例如,清理 timer,取消网络请求或清楚在 componentDidMount() 中创建的订阅等

不要调用 setState ,因为不会重新渲染执行

不常用的生命周期

完整的生命周期函数

react生命周期函数all

static getDerivedStateFromProps(props, state)

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

此方法适用于 state 的值在任何时候都取决于 props。例如,实现 <Transition> 组件可能很方便,该组件会比较当前组件与下一组件,以决定针对哪些组件进行转场动画。

此方法不能访问组件实例

注意:派生状态会导致代码冗余,并使组件难以维护。

可以使用替代方案进行处理时尽量使用可替代方案。详细查看:你可能不需要使用派生state

shouldComponentUpdate(nextProps, nextState)

shouldComponentUpdate() 会在当 props 和 state 发生变化渲染执行前被调用。返回值默认为 true 。首次渲染或使用 forceUpdate() 时不会调用此方法。

返回 false 可以跳过更新,但是并不会阻止子组件在 state 更改时重新渲染。

此方法进作为性能优化的方式而存在。

可以使用 PureComponent 组件进行替代,PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

**避免在此方法中进行深层次比较或使用 JSON.stringify()**,这样非常影响效率,且会损害性能。

getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如:滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate() 。

render 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdatecomponentDidUpdate)之间可能存在延迟。

总结
  • 减少不常用的生命周期方法的使用,如果有可替代方案则使用可替代方案
  • 不要使用过期的生命周期方法

组件之间的通信

参数验证 propTypes
父子组件间的通信
父组件传递给子组件

父组件通过 属性=值 的形式来传递数据给子组件

子组件通过 props 参数获取父组件传递过来的数据

类组件中不进行显式存储props 或 将props传递给父类存储,依旧可以在生命周期函数和 render 函数中获取到 this.props 的值。

原因:https://zh-hans.reactjs.org/docs/test-renderer.html

react-test-renderer 库中进行了手动赋值 props,使得进行显式存储props依旧可以获取到 this.props 的值。

import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Parent extends Component {
    render() {
        return (
            <div>
                <SonClass name="xxx" age="18"></SonClass>
                <SonClassNoCon name="xxx" age={18} ></SonClassNoCon>
                <SonFun name="yyy" age="18"></SonFun>
            </div>
        )
    }
}

class SonClass extends Component {
    constructor(props) {
        super()
        this.props = props
    }

    render() {
        const { name, age } = this.props
        return (
            <div>
                类子组件:{ name + ' ' + age }
            </div>
        )
    }
}

// 属性类型验证
SonClass.propTypes = {
    name: PropTypes.string,
    age: PropTypes.number.isRequired, // 必传参数
}

// 属性默认值
SonClass.defaultProps = {
    name: 'defaultName',
    age: 25,
    height: 180
}

class SonClassNoCon extends Component {
    // constructor(props) {  // 非必须可以省略 1. Component父类源码中传入了 props 2. 派生类默认写法将传入的参数通过super的方式传入父类
    //     super(props) // 传给父类的属性最终是通过call 的形式挂载到当前子类this上 具体可以进行babel转换成es5后查看
    // }

    // es6 中的 class field 属性
    static propTypes = { // 类属性
        name: PropTypes.string,
        age: PropTypes.number.isRequired, // 必传参数
    }

    static defaultProps = {
        name: 'defaultName',
        age: 25,
        height: 180
    }

    render() {
        const { name, age, height } = this.props
        return (
            <div>
                类子组件:{ name + ' ' + age + ' ' + height }
            </div>
        )
    }
}

function SonFun(props) {
    const { name, age } = props
    return (
        <div>
            函数子组件:{ name + ' ' + age }
        </div>
    )
}

export default Parent
子组件给父组件传值

可以通过自定义事件调用时向父组件传值

事件:父组件给子组件传递一个回调函数,在子组件中调用这个函数即可

// 事件通信
import React, { Component } from 'react'

class EventCommunication extends Component {
    constructor(props) {
        super(props)
        this.state = {
            counter: 0
        }
    }
    
    render() {
        return (
            <div>
                <h3>{ this.state.counter }</h3>
                <Button increment={() => this.increment()} />
            </div>
        )
    }

    increment() {
        this.setState({
            counter: this.state.counter + 1
        })
    }
}

class Button extends Component {
    render() {
        const { increment } = this.props
        return (
            <button onClick={increment}>+1</button>
        )
    }
}

export default EventCommunication
跨组件通讯
基础 props 层层传递

祖孙组件之间通过 props 自上而下层层传递。(可以使用属性展开

容易造成数据冗余

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

应用场景:非父子组件数据的共享。

设计目的:为了共享那些对于一个组件树而言是“全局”的数据。

多个 Context 可以进行嵌套使用, 替代方案: Redux 或 Hooks。

函数式组件无法使用 this.context 方法,但是可以使用 Context.Consumer 的方式进行操作

Event

跨组件事件传递,可以使用 EventBus(事件总线) 的方式进行处理。

vue 中内置了eventBus 的 api,如果 react 需要使用可以安装第三方库(event)进行。

Redux 或 Hooks

应用于更加复杂的场景,简化 Context 操作。

关于 slot

react 中没有插槽的概念,但是可以使用 props.children 实现,或者使用 JSX 将 html 元素当作 props 传到子组件中。

import React, { Component } from 'react'

class SlotComponent extends Component {
    render() {
        return (
            <div>
                <Slot>
                    <div>aaaa</div>
                    <div>bbbb</div>
                </Slot>
                <div>================</div>
                <Slot2 leftSlot={<div>xxxx</div>} rightSlot={<div>yyyy</div>}></Slot2>
            </div>
        )
    }
}

class Slot extends Component {
    render() {
        const { children } = this.props
        return (
            <div>
                { children[0] }
                <div>-------------------</div>
                { children[1] }
            </div>
        )
    }
}

class Slot2 extends Component {
    render() {
        const { leftSlot, rightSlot } = this.props
        return (
            <div>
                { leftSlot }
                <div>-------------------</div>
                { rightSlot }
            </div>
        )
    }
}


export default SlotComponent

Ref

访问DOM元素,进行DOM操作,可以避免直接操作原生DOM。

应用场景如:管理焦点,文本选择或媒体播放;触发强制动画;集成第三方 DOM 库。

获取 refs 的方式:
方式一:传入一个对象(推荐)

对象是通过 React.createRef() 方式创建出来的;

使用时获取到创建的对象其中一个 current 属性就是对应的元素;

方式二:传入一个函数

该函数会在 DOM 被挂载时进行回调,这个函数会传入一个元素对象,我们可以自己保存,使用时,直接拿到之前保存的元素对象即可。

refs 的类型

ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;
  • 不能在函数组件上直接使用 ref 属性,因为函数组件没有实例。如果想要获取函数式组件中的某个DOM 元素,可以通过 React.forwardRef 进行 ref 转发操作。
refs 转发

Ref 转发是将 ref 自动地通过组件传递到其一子组件的一种技巧。对于可重用的组件库很有用。

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递给子组件。

受控和非受控组件

受控组件

在 React 中,可变元素通常保存在组件的 state 属性中,并且只能通过使用 setState() 来更新。

将两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。

被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

import React, { PureComponent } from 'react'

export default class Form extends PureComponent {

    constructor(props) {
        super(props)

        this.state = {
            username: ''
        }
    }

    render() {
        return (
            <div>
                <form onSubmit={e => this.handleSubmit(e)}>
                    <label htmlFor="username">
                        用户名: 
                      	{/* 受控组件 */}
                        <input id="username" type="text" value={ this.state.username } onChange={e => this.handleChange(e, 'username')} />
                    </label>
                    <input type="submit" value="提交" />
                </form>
            </div>
        )
    }
    handleSubmit(e) {
        console.log(e)
        e.preventDefault()
    }
    handleChange(e, type) {
        console.log(e, type)
        this.setState({
            [type]: e.target.value
        })
    }
}

React 推荐大多数情况下使用 受控组件 来处理表单数据:一个受控组件中,表单数据是由 React 组件来管理的;另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。

非受控组件

要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,可以使用 ref 来从 DOM 节点中获取表单数据。

高阶组件HOC

高阶函数:接受一个或多个函数作为输入,输出一个函数。

常见的高阶函数:filter、map、reduce等。

高阶组件:参数为组件,返回值为新组件的函数。对组件实现了一个拦截。

高阶组件是一种基于 React 的组合特性而形成的设计模式。

高阶组件将组件转换成另一个新的组件。

高阶组件在一些 React 第三方库中非常常见:

  • 如 redux 中的 connect
  • 如 react-router 的 withRouter
  • 如 React.forwardRef()
class Main extends PureComponent {
    render() {
        console.log('main:', this.props)
        return (
            <div>
                高阶组件
            </div>
        )
    }
}

/**
 * 高阶组件 返回类组件
 * @param {Component} WrappedComponent 
 * @returns 
 */
function enhanceComponent(WrappedComponent) {
    return class NewComponent extends PureComponent {
        render() {
            console.log('enhanceComponent')
            return <WrappedComponent {...this.props} />
        }
    }
}

/**
 * 高阶组件 返回函数式组件
 * @param {Component} WrappedComponent 
 * @returns 
 */
function enhanceComponent2(WrappedComponent) {
    return function NewComponent(props) {
        return (
            <WrappedComponent {...props} />
        )
    }
}

const EnhanceComponent = enhanceComponent(Main)

export default EnhanceComponent
使用场景
  • 对原有的组件 props 再多加属性,使用高阶函数对原本导出的组件进行处理,避免修改多个文件的组件使用,和避免修改旧组件使用。

  • 多个组件使用 context 共享数据,相同代码可以使用高阶组件进行抽取。

  • 渲染判断鉴权,根据 props 显示不同的组件。

  • 生命周期劫持。

  • 注意使用高阶组件透传 ref。

    function logProps(Component) {
      class LogProps extends React.Component {
        componentDidUpdate(prevProps) {
          console.log('old props:', prevProps);
          console.log('new props:', this.props);
        }
    
        render() {
          const {forwardedRef, ...rest} = this.props;
    
          // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
          return <Component ref={forwardedRef} {...rest} />;
        }
      }
    
      // 注意 React.forwardRef 回调的第二个参数 “ref”。
      // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
      // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
      return React.forwardRef((props, ref) => {
        return <LogProps {...props} forwardedRef={ref} />;
      });
    }
HOC的意义
  • Mixin的替代方案
    • Mixin 可能会项目依赖,相互耦合,不利于代码维护
    • 不同的 Mixin 中的方法可能会相互冲突
    • Mixin 非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
HOC的缺陷
  • HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;
  • HOC 可以劫持 props,在不遵守约定的情况下可能造成冲突;

Portals

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

某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的DOM元素上的)。

用法
import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'

const el = document.createElement('div')
document.body.append(el)

class Home extends PureComponent {
    render() {
        return ReactDOM.createPortal(
            this.props.children,
            el
        )
    }
}

class UsePortals extends PureComponent {
    render() {
        return (
            <div>
                <Home>portals' content</Home>
            </div>
        )
    }
}

export default UsePortals
应用场景

在组件中创建全屏的弹窗。

性能优化

组件的 key 值
组件嵌套的render调用

如果父组件中修改了 state 仅修改父组件中的数据,与子组件无关,但是会重复调用子组件的render 重新渲染。

  • 使用 shouldComponentUpdate 生命周期函数来控制类组件的 render 函数调用。

  • 继承 PureComponent 类创建类组件,避免类组件过多重新渲染,提升页面性能。

    PureComponent类 比 Component类中多实现了一个 shouldComponentUpdate 生命周期函数,以浅层对比 prop 和 state 的方式进行比较。

  • 继承 memo 类创建函数式组件,可以避免函数式组件过多重新渲染问题。

    如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

避免直接修改原始数据

HOOK

可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性(比如生命周期)。

Hook 就是 JavaScript 函数,这个函数可以钩入(hook into)React State以及生命周期等特性。

Hook 指的类似于 userState、useEffect这类的函数

Hooks 是对这类函数的统称

使用场景:

  • Hook 的出现基本可以代替我们之前所有使用 class 组件的地方(除了一些非常不常用的场景);
  • Hook 完全向下兼容,可以渐进式进行使用;
  • Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;

使用规则

只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

可以在自定义 Hook 中调用其他 Hook

有相关 linter 插件进行约束

底层原理

Hooks 内部是以 链表的形式顺序存储的,如果使用循环或条件判断会打乱链表顺序。

useState

是一个 Hook 需要从 react 中导入。

参数:初始化值,如果不设置则为 undefined。可以传入值或函数返回的值。

返回值: 数组。包含两个元素:元素一,当前状态的值(第一调用为初始化值);元素二,设置状态值的函数。

更新值之后,组件会重新渲染,并且更具新的值返回 DOM 结构。

import React, { useState } from 'react'

export default function CounterHook() {
    // Hook: useState 本身是一个函数,来自 react 包,参数为声明变量的初始值,返回值为数组 [当前的状态, 设置状态的函数]
    // 声明 counter 变量 初始值为0
    // const arr = userState(0) 
    // const counter = arr[0]
    // const setState = arr[1]
    const [ counter, setCounter ] = useState(0)

    return (
        <div>
            <hr />
            <h4>Hook 当前计数:{ counter }</h4>
            <button onClick={() => setCounter( counter + 1 )} > +1 </button>
            <button onClick={() => setCounter( counter - 1 )} > -1 </button>
        </div>
    )
}

set 函数类似于 setState 方法的使用,进行的是异步操作,同时多个操作会进行合并,可以接收函数的形式如setCounter(prevCounter => prevCounter + 1) 获取到原来的值进行处理。

源码中,useState 本质上是使用的 useReducer

useEffect

Effect Hook 可以在函数组件中执行副作用操作。

副作用操作,类似于网络请求、手动更新DOM、事件监听等。

Effect Hook 可以完成一些类似于 Class组件中生命周期的功能。

如果不传入依赖变量或者 [] 则会在每次渲染之后执行。

使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这会让应用看起来响应更快。

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

function TestShow() {
    useEffect(() => {
        console.log('组件挂载')
        console.log('订阅一些事件')
        // 可选的清除机制  React 会在组件卸载和更新的时候执行清除操作 会在执行当前 effect 之前对上一个 effect 进行清除 
        // 如果 effect 不需要清除则不需要设置返回函数
        return () => {
            console.log('组件卸载')
            console.log('取消订阅事件')
        }
    }, []) // 如果传入 [] 则表示只会在组件挂载和卸载时执行

    return (
        <div>test show</div>
    )
}

export default function CounterHook() {
    const [ counter, setCounter ] = useState(0)
    const [ show, setShow ] = useState(true)

    // useEffect 会在每次渲染后都执行
    useEffect(() => {
        document.title = counter
    }, [counter]) // 仅在 counter 更改时更新 

    return (
        <div>
            <hr />
            <h4>Hook 当前计数:{ counter }</h4>
            <button onClick={() => setCounter( counter + 1 )} > +1 </button>
            <button onClick={() => setCounter( counter - 1 )} > -1 </button>
            <button onClick={() => setShow( !show )} > switch show </button>
            { show && <TestShow /> }
        </div>
    )
}
数据频繁变化
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
    }, 1000);
    return () => clearInterval(id);
  }, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量

  return <h1>{count}</h1>;
}

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但是容易出现 bug,所以需要Hook 在组件的最顶层调用,将条件判断放在 effect 中。

useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

useContext 的参数必须是 context 对象本身

import React, { createContext, useContext } from 'react'

const UserContext = createContext()
const ThemeContext = createContext()

function Son() {
    const sonUserInfo = useContext(UserContext)
    const sonTheme = useContext(ThemeContext)

    console.log(sonUserInfo, sonTheme)

    return (
        <div style={sonTheme}>
            { `name: ${sonUserInfo.name}, age: ${sonUserInfo.age}` }
        </div>
    )
}

export default function UseContext() {
    const userInfo = {
        name: 'xxx',
        age: 18
    }

    const themeData = {
        color: 'blue'
    }

    return (
        <UserContext.Provider value={userInfo}>
            <ThemeContext.Provider value={themeData}>
                <Son></Son>
            </ThemeContext.Provider>
        </UserContext.Provider>
    )
}
useReducer

useState 的替代方案,搭配 reducer 使用。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

import React, { useReducer } from 'react'

const actionTypes = {
    INCREMENT: 'increment',
    DECREMENT: 'decrement'
}

function reducer(state, action) {
    switch (action.type) {
        case actionTypes.INCREMENT:
            return {...state, counter: state.counter + 1}
        case actionTypes.DECREMENT:
            return {...state, counter: state.counter - 1}
        default:
            return state
    }
}

export default function UseReducer() {
    const [state, dispatch] = useReducer(reducer, { counter: 0 })

    return (
        <div>
            <hr />
            <h4>UseReducer 当前计数:{ state.counter }</h4>
            <button onClick={() => dispatch({ type: actionTypes.INCREMENT })} > +1 </button>
            <button onClick={() => dispatch({ type: actionTypes.DECREMENT })} > -1 </button>
        </div>
    )
}
useCallback

useCallback 实际的目的是为了进行性能的优化

useCallback 会返回一个函数的 memoized(记忆的)值。

在依赖不变的情况下,多次定义的时候,返回的值是相同的。

当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

使用场景:在将一个组件中的函数,传递给子组件进行回调使用时,使用 useCallback 对函数进行处理。

useCallback 搭配高阶组件 memo 使用,处理函数组件,避免组件非必要渲染。

针对函数进行优化。

import React, { useCallback, useState, memo } from 'react'

const Button = memo(props => {
    console.log('Button 重新渲染 ', props.title)
    return (
        <button onClick={props.callback}>Button { props.title } counter</button>
    )
})

export default function UseCallback() {
    const [counter, setCounter] = useState(0)
    const [isShow, setIsShow] = useState(true)

    // 每次重新渲染时都会重新创建并执行
    const callback = () => {
        console.log('callback')
        setCounter(counter + 1)
    }

    const callback2 = useCallback(() => {
        console.log('callback2')
        setCounter(counter + 1)
    }, [counter]) // 如果没有任何依赖 则设置空数组 表示只会在挂载组件时 定义一次 闭包环境保留定义时的数值

    return (
        <div>
            <hr />
            <h4>useCallback counter: { counter }</h4>
            <Button title='callback1' callback={callback}></Button>
            <Button title='callback2' callback={callback2}></Button>
            <button onClick={() => { setIsShow(!isShow) }}>switch isShow</button>
        </div>
    )
}
使用 useMemo 转换 useCallback

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useMemo

useMemo 实际目的是为了进行性能的优化。

useMemo 返回的是一个 memoized(记忆的)值。

在依赖不变的情况下,多次定义的时候,返回的值是相同的。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

传入 useMemo 的函数会在渲染期间执行。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useMemo 搭配高阶组件 memo 使用,处理函数组件,避免组件非必要渲染。

针对返回值进行优化。

import React, { useState, useMemo, memo } from 'react'

const calcSum = counter => {
    console.log('calcSum 重新计算')
    let total = 0
    for (let i = 1; i <= counter; i++) {
        total += i
    }
    return total
}

const Info = memo(props => {
    console.log('Info 组件重新渲染')
    return (
        <div>name: {props.info.name}, age: {props.info.age}</div>
    )
})

export default function UseMemo() {
    const [counter, setCounter] = useState(10)
    const [show, setShow] = useState(true)

    // const total = calcSum(counter) // 这种方式 组件的值改变重新渲染时会重新进行计算
    const total = useMemo(() => calcSum(counter), [counter]) // 使用 useMemo 依赖值不改变 则不会进行重新计算

    // 这种方式 组件重新渲染时会重新定义 info 导致组件重新渲染
    // const info = {
    //     name: 'xxx',
    //     age: 18
    // }
    // 使用 useMemo 依赖值不改变 则不会重新定义 搭配高阶组件 memo 进行使用
    const info = useMemo(() => ({name: 'xxx', age: 18}), [])

    return (
        <div>
            <hr />
            <h4>useMemo counter sum: { total }, show: { String(show) }</h4>
            <button onClick={() => setCounter(counter + 1)}> +1 </button>
            <button onClick={() => setShow(!show)}>switch show</button>
            <Info info={info}></Info>
        </div>
    )
}
useRef

useRef 返回一个 ref 对象, 其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期保持不变。

用法:

  • 引入 DOM (或者组件,但是需要是 class 组件)元素;
  • 保存一个数据,这个对象在整个生命周期中可以保存不变;
import React, { PureComponent, useRef, useState, useEffect } from 'react'

class Text extends PureComponent{
    render() {
        return (
            <div>class component text</div>
        )
    }
}

export default function UseRef() {
    const [counter, setCounter] = useState(0)

    const titleRef = useRef()
    const textRef = useRef(null)
    const oldCountRef = useRef(counter)

    useEffect(() => {
        oldCountRef.current = counter
    }, [counter])

    const changeText = () => {
        console.log(titleRef.current)
        console.log(textRef.current)
        titleRef.current.innerHTML = 'Hello world'
    }

    return (
        <div>
            <hr />
            <h4 ref={titleRef}>useRef hook</h4>
            <Text ref={textRef}></Text>
            <button onClick={() => changeText()}>change text</button>
            <div>上一次的数据:{ oldCountRef.current }</div>
            <div>当前的数据:{ counter }</div>
            <button onClick={() => setCounter(counter + 1)}> +1 </button>
        </div>
    )
}
useImperativeHandle

useImpertiveHandle 可以在使用 ref 时自定义暴露给父组件的实例值。限制父组件使用子组件实例的自由度。useImperativeHandle 应当与 forwardRef 一起使用。

import React, { useRef, useImperativeHandle, forwardRef } from 'react'

const Input = forwardRef((props, ref) => {
    const sonInputRef = useRef()

    useImperativeHandle(ref, () => ({
        onFocus() {
            sonInputRef.current.focus()
        }
    }))

    return (
        <input ref={sonInputRef} type="text" />
    )
})

export default function UseImperativeHandle() {
    const inputRef = useRef()

    return (
        <div>
            <hr />
            <h4>usesImperativeHandle test</h4>
            <Input ref={inputRef} />
            <button onClick={() => inputRef.current.onFocus()}>onfocus</button>
        </div>
    )
}
useLayoutEffect

useLayoutEffect 会在所有的 DOM 变更之后同步调用 effect。

可以使用它来读取 DOM 布局并同步触发重渲染。

在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

与 useEffect 的区别:

  • useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新;
  • userLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新;

如果希望在某些操作发生之后再更新 DOM ,那么应该将这个操作放到 useLayoutEffect。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useLayoutEffect 的调用阶段与 componentDidMount、componentDidUpdate 的调用阶段时一样的。

自定义HOOK

自定义 Hook 本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算 React 的特性。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。

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

/**
 * 需求: 所有组件在创建喝销毁时都进行打印
 * 组件被创建:打印 组件被创建了
 * 组件被销毁:打印 组件被销毁了
 * @returns 
 */
function useLoggerLife(name) {
    useEffect(() => {
        console.log(`${name} 被创建`)
        return () => {
            console.log(`${name} 被销毁`)
        }
    }, [])
}

const Test = () => {
    useLoggerLife('Test')
    return (
        <div>test component</div>
    )
}

export default function CustomLifeHook() {
    const [show, setShow] = useState(true)
    useLoggerLife('CustomLifeHook')
    return (
        <div>
            <hr />
            <h4>custom hook</h4>
            <button onClick={() => setShow(!show)}>switch show</button>
            { show && <Test></Test> }
        </div>
    )
}

查找其他人写的hooks资源:https://usehooks.com/

关于优化
禁止不必要的useCallback和useMemo

有些初学hooks的同学喜欢见到callback就useCallback,见到calculate就useMemo,这些都属于“过早优化”的典型表现。你以为这样可以提升性能,实际上这一点不一定,但有一点可以肯定的是代码更容易出bug。

具体可以看这篇文章

只有在必要的时候使用useCallback和useMemo,什么叫必要的时候呢?就是不使用useCallback和useMemo就会导致性能瓶颈的时候。

配置

vue-cli 使用了 webpack-chain进行 webpack配置扩展和自定义

react 项目进行扩展配置:

方法一:使用 react-scripts eject将配置文件暴露出来,然后进行修改。

方法二:不进行暴露项目初始配置,在初始配置上进行扩展修改,参考链接:https://github.com/timarney/react-app-rewired/blob/master/README_zh.md

方法三:使用 craco 库进行配置扩展(推荐)

样式

使用 className 定义组件或标签的样式类名。

不使用 class 为了和 js 中的 class 类关键字区分开

import React, { PureComponent } from 'react'
import classNames from 'classnames'

export default class UseClassname extends PureComponent {
    render() {
        const isActive = true
        const isFoo = true
        const isBar = false
        const errClass = 'error'
        const warnClass = null // undefined 0
        return (
            <div>
                {/* 原生中添加 className 的方法 */}
                <h5 className={'foo bar baz'} >classnames title 1</h5>
                <h5 className={'title' + (isActive ? ' active' : '') } >classnames title 2</h5>
                <h5 className={['title', (isActive ? ' active' : '')].join(' ')} >classnames title 3</h5>

                {/* classnames库添加class */}
                <h5 className='foo bar baz' >classnames title 4</h5>
                <h5 className={classNames('foo bar baz')} >classnames title 5</h5>
                <h5 className={classNames({'active': isActive, 'bar': isBar, 'foo': isFoo}, 'title')} >classnames title 6</h5>
                <h5 className={classNames('foo', errClass, warnClass, { "active": isActive })} >classnames title 7</h5>
                <h5 className={classNames(['active', 'title'])} >classnames title 8</h5>
                <h5 className={classNames(['active', 'title', {'foo': isFoo}])} >classnames title 9</h5>
            </div>
        )
    }
}

常用的方式:

  • 原生的CSS

  • 内联样式

  • CSS Modules

    React 脚手架中已经内置了关于 CSS Modules 的相关配置,可以直接使用,将相关的 css/less/scss 文件后缀改为 .module.[css/less/scss] 。

    import React, { PureComponent } from 'react'
    import Style from './style.module.css'
    
    
    export default class UseStyle extends PureComponent {
        render() {
            return (
                <div>
                    <div className={ Style.title }>use style component</div>
                </div>
            )
        }
    }
  • CSS in JS

    指一种模式,其中 CSS 由 JS 生成而不是在外部文件中定义。注意此功能并不是 React 的一部分,而是由第三方库提供。

    目前比较流行的 CSS-in-JS 的库有:

其他常用的第三方库

classnames:帮助动态设置 classname,类似 vue 中提供的写法。

react-transition-group:组件 transition 过渡动画。

小技巧

组件名重定义(chorme 插件上显示)

class App extends PureComponent {
    render() {
        return (
        	<div>app</div>
        )
    }
}
App.displayName = 'DisplayApp'
// 在chorme插件上展示的就是DisplayApp组件名

扩展属性(属性全部传递给子组件)

props 穿透

class NewComponent extends PureComponent {
    render() {
        return <WrappedComponent {...this.props} />
    }
}

严格模式

严格模式会刻意地调用多次组件。

StrictMode 有助于:

  • 识别不安全的生命周期

  • 使用果实字符串ref API 的警告

  • 使用废弃的 findDOMNode 方法的警告

  • 检测意外的副作用

    在开发环境下,故意重复调用以下函数来检测发现是否存在副作用:

    • class 组件的 constructorrender 以及 shouldComponentUpdate 方法
    • class 组件的生命周期方法 getDerivedStateFromProps
    • 函数组件体
    • 状态更新函数 (即 setState 的第一个参数)
    • 函数组件通过使用 useStateuseMemo 或者 useReducer
  • 检测过时的 context API

数据的不可变性

immutable.js

数据的不可变性,主要解决了两个问题:

  1. 以防数据发生不可预知的改变;
  2. 当数据量大的时浅拷贝或深拷贝赋值变量再修改数据,带来的性能问题和内存浪费。

Immutable 对象的特点时只要修改了对象,就会返回一个新的对象,旧的对象不会发生改变。

为了节约内存,出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性数据结构)。

持久化数据结构:使用了结构,当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费。结构共享。

更多阅读:

Immutable.js, persistent data structures and structural sharing