React | 青训营笔记

32 阅读10分钟

React

这是我参与第五届青训营伴学笔记创作活动的第2天,学习了React的一些思路。

一、React的历史与应用

应用场景

  • 前端应用开发

  • 移动原生应用开发

  • 结合Electron,进行桌面应用开发

  • React Three Fiber可以进行3D开发

  • WebGL

关键历史因素:组合式组件思想的产生

二、React的设计思路

1. 原生JS写UI编程痛点

  • 状态更新,UI不会自动更新,需要手动地调用DOM进行更新

  • 欠缺基本的代码层面的封装和隔离,代码层面没有组件化。

    • UI一般很明显分为几块,也就是应当分为几个组件
  • UI之间的数据依赖关系,需要手动维护,但一般这种数据依赖关系较多,因为UI编程的特点是每一步可能影响多个部分。如果依赖链路长,则会遇到“Callback Hell”(回调地狱)。

2. 响应式与转换式

  • 转换式系统:给定输入求解输出

    • 典型例子:编译器,数值计算
  • 响应式系统:具有及时响应性(Responsive)、恢复性(Resilient)、有弹性(Elastic)以及消息驱动(Message Driven)的系统称为响应式系统

    • 一般特征:监听事件、消息驱动、异步

    • 典型例子:监控系统、UI界面

响应式系统的一般过程:

1.png

具体到前端代码

2.png

我们希望的React

  • 状态更新,UI自动更新

  • 前端代码组件化,可复用,可封装

  • 状态之间的互相依赖关系,只需声明即可

3. 组件化及状态归属问题

例:我们可以把手机购物界面抽象为左边的树形结构

3.png

这个树形结构并不是DOM树,而且DOM并不是JS内部的对象,是浏览器内部的,我们只能通过调用JavaScript的API去修改DOM。

组件

  • 组件是组件的组合/原子组件

  • 组件内拥有状态,外部不可见,比如颜色内容

  • 父组件可将状态传入组件内部,组件间可以通信,增加复用性

    • 例如,上面的图片中,手机的当前价格会受到各种状态改变的影响,由于父组件可将状态传入子组件,因此,当前价格应当放在Root节点。

    • 状态归属于两个节点向上寻找到最近的祖宗节点

    • 但如果这样的数据很多,全都放在Root节点,好像组件化有没有那么的合理,我们会在后面解决这问题。

现在有了新的问题,当前价格在Root节点,可是改变当前价格的事件却不在Root节点,而是在型号选择和颜色选择中,那要怎么变呢?

4.png

  • 首先,JS中函数是一等公民,可以传来传去

  • 这样,先在Root节点中写一个改变当前价格的函数onChangeValue(),然后将这个函数向下传给能改变当前价格的节点。

  • 每当型号或颜色发生改变的时候,通过onclick调用这个Root传下来的改变当前价格的函数onChangeValue(),这样就可以改变Root节点的当前价格了

那么,这是一个双向传递的过程吗

并不是,onclick只是执行了onChangeValue()这个函数,这个函数只是改变了Root节点的当前价格,并没有反向传状态。而onChangeValue()才是真的将Root节点的状态传到了iPhone 13 pro

思考:
  1. React是单向数据流,还是双向数据流?

    React是单向数据流,永远是父组件给子组件传东西,子组件不能向父组件传东西。但是子组件可以通过执行父组件传来的函数来改变父组件的状态。

  2. 如何解决状态不合理上升的问题?

    使用React状态管理库(下面会讲)

  3. 组件的状态改变后,如何更新DOM?

通过更改前后虚拟DOM找出diff,然后进行重新渲染影响真实DOM(下面会讲)

组件设计

  1. 组件声明了状态和UI的映射

    • 输入几个状态,返回UI
  2. 组件有Props/State两种状态

    • 内部私有状态:State

    • 从外部传入的状态:Props

  3. “组件”可由其他组件拼装而成

那么组件代码会是什么样子呢?

  1. 组件内部拥有私有状态State

  2. 组件接受外部的Props状态提供复用性

  3. 根据当前的State/Props,返回一个UI

4. React生命周期

重点:挂载,重新渲染,结束挂载

5.png

三、React(hooks)的写法

  • useState()

    • 在React中,声明一个新状态需要调用useState函数,这个函数传入一个状态的初始值,返回一个数组,数组的第一个值是状态本身,第二个值是这个状态的setter

    • 虽然可能绕了一点,但是原理也很简单,React封装了这个函数,这样React本身就可以内置一些状态刷新的方法。

import React, { useState } from 'react';

function Example(){
    // 声明一个新状态count
    const [const, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onclick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}
  • 副作用:组件对外部内容产生影响,本质就是非纯函数。除了单纯的计算之外,还要做其他的一些事情。比如网络请求,更新DOM,localStorage存储数据等

  • useEffect():将副作用代码写在函数中,在组件mount的时候和依赖项被set的时候会执行。下面例子即为改变title

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

function Example(){
    // 声明一个新状态count
    const [const, setCount] = useState(0);

    // 更新title就是一种副作用,因为title并不是组件内部内容
    useEffect(() => {
        // 通过调用浏览器API更新title
        document.title = 'You clicked ${count} times';
    });

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onclick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

useEffect的第二个参数表示副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。

若没有第二个参数,useEffect只会在组件挂载的时候执行一次

function Title(argument) {
    useEffect(() => {
        document.title = ${argument.name};
    }, [argument.name])
    return <h1>{argument.name}</h1>
}

Hook

  • 可以挂载到React生命周期上执行的函数。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

  • 上面的useState()和useEffect()都是Hook

  • 其他的Hook大多是useState和useEffect的封装

不要在循环、条件或嵌套函数中调用Hook

四、React的实现

React代码有一些问题

  • JSX(React中的JS语法扩展)不符合JS标准语法

  • 返回的JSX发生改变时,如何更新DOM

  • State/Props更新时,要重新出发render函数(render函数就是组件本身)

我们来一个一个解决问题

  1. JSX(React中的JS语法扩展)不符合JS标准语法

由于HTML是一个树状结构,理所当然可以使用创建节点方式将React语法变为标准JS语法,底层就是这么转换的

6.png

  1. 返回的JSX发生改变时,如何更新DOM

想法:JSX更改后,内部算法找到更改前后的不同,并针对性更改,这样可以很快速

  • Virtual DOM(虚拟DOM)

    • Virtual DOM 是一种用于和真实DOM同步,而在JS内存中维护的一个对象,它具有和DOM类似的树状结构,并和DOM可以建立一一对应的关系。

    • 它赋予了React声明式的API:您告诉 React希望让UI是什么状态,React 就确保DOM匹配该状态。这使您可以从属性操作、事件处理和手动DOM更新这些在构建应用程序时必要的操作中解放出来。 真实dom不是js中的对象,是浏览器内部维护的状态,只能dom接口修改。js可以设置与dom对应的对象。

  • 指令式编程

    • 手动告诉程序一步一步怎么做,定义处理事情的逻辑和方法
  • 声明式编程(大部分前端框架)

    • 定义处理事情的逻辑框架,不必列出明确步骤,程序自行摸索
  • 响应式编程

    • 声明式编程的一类,通过异步和数据流来处理问题

那么既然声明式这么好,为什么声明式不直接移植进浏览器呢?

  • 浏览器只是应用平台,不能提供更高层的东西。如果声明式移植进浏览器,浏览器的自由度就降低了。它只能提供底层平台,不应限制开发的方式。

更新DOM的过程

  • 首先在虚拟DOM中,每次组件状态更新,开始计算Diff不同点

  • 计算Diff的过程实质上是递归的过程,因为父组件可以嵌套子组件,是一个树,树找不同就是要进行递归查询

    • 当父组件状态改变时,所有的子组件包括子组件的子组件会递归发生重新render,这也是影响React性能的大问题,因为位置很高的父组件一旦变化,重新render的组件太多,影响渲染
  • Diff求出来之后,进行re-render Virtual DOM,这样就真的改变了DOM树,完成了UI动态渲染

7.png

怎么高效计算Diff呢?

render函数本身执行速度太慢,所以我们要让Diff计算足够快;其次我们要让DOM更新足够少,Diff足够少;因此这两种需求变成了一种矛盾,出现了一种权衡。

8.png

客观事实:

  • 完美的最小Diff算法,需要O(n^3)的复杂度(太高了)

  • 牺牲理论最小Diff,换取时间,得到了O(n)复杂度的算法:

    • Heuristic(启发式) O(n) Algorithm

启发式算法:(百度百科)一个基于直观或经验构造的算法,在可接受的花费(指计算时间和空间)下给出待解决组合优化问题每一个实例的一个可行解,该可行解与最优解的偏离程度一般不能被预计。

那么这个O(n)的算法具体怎么工作呢?

  • 不同类型的元素 —— 替换

  • 同类型的DOM元素 —— 更新

  • 同类型的组件元素 —— 递归

仍存在的弊病:父组件变化,所有子组件及子组件的子组件都会重新渲染

五、React状态管理库

核心思想:将状态抽离到UI外部进行统一管理

既然放在UI组件内部容易影响性能,那我拿到外面去共享不就可以了

9.png

问题:降低了组件的复用性,因为这个组件会与外面的数据Store强耦合。如果开发组件库,不可能要依赖外部数据,一定是要封装好的。因此,一般用在业务代码如app,lib中少用

推荐的React状态管理库:

  • redux

  • xstate —— 基于状态机思想

  • mobx

  • recoil

状态机

  • 具有当前状态,在收到外部事件后,会迁移到下一个状态

  • 以下是知乎大佬的文章定义

  • 状态机的全称是有限状态自动机,自动两个字也是包含重要含义的。给定一个状态机,同时给定它的当前状态以及输入,那么输出状态时可以明确的运算出来的。

  • 四大概念:

    • 状态State:一个状态机至少要包含两个状态

    • 事件Event:事件就是执行某个操作的触发条件或者口令

    • 动作Action:事件发生以后要执行动作

    • 变化Transition:从一个状态变化为另一个状态

哪些状态应该放在状态管理库里面?

  • 这些状态被整个app拥有,例如用户的信息可能随处都会被用到,全局拥有的话只需要发起一次请求即可,省时省力

六、应用级框架科普

  • NEXT.js:硅谷明星创业公司Vercel(很厉害)的React开发框架,稳定,开发体验好,支持Unbundled Dev,SWC等,其同样有Serverless一键部署平台帮助开发者快速完成部署。口号是“Let's Make Web Faster”

  • MODERN.JS:字节跳动Web Infra团队研发的全栈开发框架,内置了很多开箱即用的能力与最佳实践,可以减少很多调研选择工具的时间。

  • Blitz:无API思想的全栈开发框架,开发过程中无需写API调用与CRUD逻辑,适合前后端紧密结合小团队项目。

七、引用

  • 字节录播课 - React 的历史与应用
  • 字节录播课 - React 的设计思路
  • 字节录播课 - React (hooks)的写法与 React 实现
  • 字节录播课 - React 状态管理库与应用级框架科普
  • 知乎 - 什么是状态机?