React 基础

341 阅读14分钟

一、React 的理解

React 是一个用于构建用户界面的JS库。在代码开发过程中,我们将 UI 拆分成独立的小块,每块就是一个组件,通过组件之间的组合嵌套来构建页面。

React不是MVVM,最多算是Model -> View。官方给的公式就是UI = render(data),即数据驱动视图。

1. 设计思想: From 核心开发者

  1. 组件化开发:将UI拆分成组件,通过组件之间的组合、复用来构建复杂UI
  2. 单向数据流:组件间数据流动方向单一,所有状态改变都是可追溯的。
  3. 推崇函数式编程:React 设计时认为 UI 只是把数据通过映射关系变换成另一种形式的数据,相同的输入应该得到相同的输出 UI = f(data)
  4. 数据的不可变性:数据一旦被创建就不能再修改了。props不能修改,state要通过this.setState更新。
  5. 虚拟dom和diff算法:将真实 DOM 抽象为 js 对象,然后结合diff算法来最小化 DOM 的更新。

2. react 工作原理?

  • 虚拟dom
  • 更新渲染 / 数据驱动视图。

3. react 解决了什么问题?相比原生JS的优势

  • 性能提升:引入虚拟 DOM 的概念,结合diff算法来最小化 DOM 的更新,批量更新机制减少页面重排重绘的次数,有利于提升性能。
  • 数据驱动视图的设计让我们不用再直接写一大堆DOM操作了,只需要关注数据,这提高了工作效率。
  • 可维护性:单向数据流使得所有状态的改变都是可追溯的,排查问题比较容易。
  • 组件化开发有利于代码复用、提升开发效率

React 有哪些问题吗?

  • JSX 语法将 js、html 混搭,很难看;
  • Diff 算法的改进点。

二、函数式编程

React 推崇的是函数式编程。函数式编程是一种以使用函数为主的开发风格,最重要的概念是纯函数,纯函数需要满足三个特征:

  • 无副作用:副作用指的是函数执行时产生了外部可观察的变化。例如发送网络请求、操作DOM、打印数据等,副作用会影响代码可读性、可能导致一些难以排查的错误。

  • 引用透明:指的是一个函数对于相同输入始终会得到相同的输出,不依赖外部环境。

  • 数据不可变immutable:数据一旦被创建就不能再更改。如果想要修改只能创建一个新对象,在新对象上修改。

1. react 为什么推崇函数式编程/纯函数的好处

  • 更容易进行测试,因为结果只依赖输入。
  • 结果可以缓存,因为相同输入会得到相同输出。
  • 更容易调用,因为不用担心副作用的问题。
  • 更容易维护和重构。

2. 函数式编程/纯函数的应用

  • 函数式组件和 hooks
  • 高阶函数:函数做参数或返回值是函数,像数组的 map、reduce、filter 等方法。
  • redux 中的 reducer(纯函数)
  • 柯里化:将一个多元函数转换成一个依次调用的单元函数。

三、生命周期(16.3)

每个组件都包含 “生命周期方法”,组件在进入和离开DOM时要经历一系列的生命周期方法。

image.png

1. 新旧版本对比

  • 删除了三个生命周期componentWillMountcomponentWillUpdatecomponentWillReceiveProps

    • v16 引入 Fiber 后协调过程会多次执行willxxx周期,失去了原有的意义;
    • 这些生命周期也经常被误用,在里面使用setState会触发多次重绘,影响性能。
  • 新增两个生命周期getDerivedStateFromPropsgetSnapshotBeforeUpdate

2. 生命周期执行顺序

  1. 父子组件初始化:父子组件第一次渲染加载时:
    • parentconstructor => ... => render
    • Childconstructor => ... => componentDidMount。相比上面多了一个生命周期。
    • ParentcomponentDidMount
  2. 修改父组件传入子组件的props
    • ParentgetDerivedStateFromProps => ... => render
    • ChildgetDerivedStateFromProps => ... => getSnapshotBeforeUpdate。相比上面多了一个生命周期。
    • ParentgetSnapshotBeforeUpdate
    • ChildcomponentDidUpdate
    • ParentcomponentDidUpdate
  3. 子组件修改自身的state,此时只有子组件执行更新阶段的5个生命周期。
  4. 卸载子组件:点击父组件的卸载子组件按钮,
    • ParentgetDerivedStateFromProps => ... => getSnapshotBeforeUpdate
    • ChildcomponentWillUnmount(如果是子组件自己卸载时就只执行这一个)。
    • ParentcomponentDidUpdate

3. 触发render的时机

  • setState
  • useState
  • 父组件渲染

四、其他特性

  1. Fragment 特性允许组件返回多个根元素,并且不用额外添加 DOM 节点。
<>
    <Child1 />
    <Child2 />
</>
  1. lazy 用于组件的懒加载,并且要在 Suspense 组件中渲染该组件。
const Avatar = lazy(() => import('./Avatar.js'));
  1. memo 是高阶组件,它接收一个组件作为参数并返回一个组件。如果组件在相同的 props 情况下会渲染相同的结果就可以用 memo 进行包装,当 props 相同时会直接复用上次渲染的结果。
const Greet = memo(function Greet({ name }) {
  return <h1>Hello, {name}</h1>;
});

默认只会做浅比较,可用作性能优化。如果想深层比较,可以传入第二个参数。

和 useMemo 的区别:

  • memo 是一个高阶组件,useMemo 是一个 hook。
  • memo 用来缓存组件的渲染,useMemo 用来缓存值。
  1. Context:当很多不同层级的组件都需要访问同样的数据时,可以借助Context来共享这个数据,而不需要给每个组件添加 props,例如 UI 主题。
import { createContext } from 'react';
// 1. 使用 createContext 创建一个上下文
const ThemeContext = createContext('dark'); // 参数是低优先级的默认值
// 2. 用 Provider 包裹组件,为里面的组件指定context值
function App() {
    const [theme, setTheme] = useState('dark')
    //...
    return (
        <ThemeContext.Provider value={theme}>
            <Toolbar />
        </ThemeContext.Provider>
    );
}
// 3. 在包裹的组件内部可以调用 useContext 读取context值
function Toolbar() {
    const theme = useContext(ThemeContext)
}

useContext 会获取距离它最近的上层组件的 Provider 提供的 value。

五、单向数据流、数据绑定

  • 数据流指的是组件之间的数据流动,和数据绑定没有关系
  • 数据绑定指的是View层和Model层的映射关系。单双向数据绑定的区别在于 View 改变会不会触发 Model 更新

React(Vue)是单向数据流,只能由父组件向子组件传递props,并且子组件不能修改父组件传递来的props,只能通过事件通知父组件进行数据修改。单向数据流使得所有的状态改变是可追溯的,排查问题更方便。

Vue 是双向数据绑定,React 不支持双向数据绑定、是单向数据流。

六、React 性能优化

1. 虚拟化长列表(不能分页)

当要渲染成千上万个列表项时,因为浏览器需要处理大量的DOM节点,所以速度会很慢。

列表虚拟化是一种按需加载的优化技术,它只会渲染可见区域中的数据,对不可见区域不渲染或部分渲染。react、vue都有一些虚拟列表库。它的原理是:

  • 根据滚动距离和列表项高度,计算出当前需要展示列表的起始index可视区列表的偏移距离
  • 根据起始index和可视区高度,计算出...的末尾index
  • 根据起始index末尾index截取相应的列表数据,然后赋值给可视区列表、进行渲染;
  • 将之前计算的偏移距离设置在列表上。(可视区是动态变化的)

滚动距离?scrollTop属性的值是元素内容的顶部到视窗可见内容的顶部之间的距离。

如果列表项不固定呢?以预估高度先渲染,然后获取真实高度缓存。

2. 避免不必要的 re-render

通常当父组件的state或props更新时,子组件的数据不论是否发生变化都会触发重新渲染。为了避免无效 render,可以采取:

(1) memo、useCallback

  • memo 可以缓存子组件,如果 props 没有改变就不需要重新渲染。
  • useCallback 可以缓存子组件的参数函数。

另外 useMemo 一般用于密集型、计算量大的缓存。

(2) shouldComponentUpdate通过比较新旧 props 和新旧 state 来控制组件的渲染行为。设置成“只有组件的 state 或 props 改变时才会 return true”、组件才会渲染。

shouldComponentUpdate (nextProps, nextState) {}

(了解忽略)引用类型需要深比较才能判断是否修改,非常影响效率,推荐结合 immutable 数据进行浅比较,只要被修改就会返回一个全新的对象

(3) PureComponent(过时) 通过浅比较的方式,只有 props、state改变时才会重新渲染。

3. 其他优化

  • diff算法能够最小化DOM的更新,批量更新机制可以减少重排重绘次数,提升渲染速度。
  • 引入 key 来识别列表元素,以尽量复用 DOM 节点、减少开销。
  • lazy 和 Suspense 组件搭配实现懒加载组件。

七、React 与 Vue

1. 相同点

  • 组件化开发。
  • 都有虚拟DOM,通过 diff 算法最大化地复用 DOM。
  • 单向数据流,数据只能由父组件流向子组件。
  • 都是数据驱动视图,不直接操作DOM。但Vue是响应式的、会自动改变,而React不是响应式的,需要手动setState。

2. 不同点

  • 语法:vue 是模板语法,react 是 jsx 语法。都是 render 的一种形式。
  • 双向数据绑定:vue 是双向数据绑定,react 不支持。
  • 监听数据变化的原理不同:react 强调数据的不可变,它是通过比较对象引用的方式。vue 中的数据是可变的,它的响应式原理是基于追踪对象属性的读写来实现的。
  • vue 内置多个指令,封装程度较高;react更灵活、扩展性高。

3. react 相比 vue 的优势

  • 生态圈更大,有更多的扩展工具和相关技术支持。
  • 函数式编程会有更好的测试性
  • 版本过渡比较平滑,项目更新升级时成本较低。

4. vue 相比 react 的优势

  • 封装程度高、中文文档丰富,初学者容易上手。
  • 模板语法会更容易阅读和维护。

八、新特性

  1. React16 新特性
  • Fiber 架构、Hooks、Context
  • 生命周期更新
  • Fragment组件、memo缓存组件、lazy懒加载
  1. React17 新特性
  • v17 最大的特点就是无新特性,旨在方便后续版本的升级。
  • 去除事件池。事件池指的是合成事件对象放入池中统一管理。
  • 事件委托到根节点。事件不再委托到 document 上,而是委托到根节点。

Untitled (5).png

React18

v18 不再支持 IE 浏览器,如需兼容应回退到 v17。

1. Concurrent Mode(并发模式)

Fiber 为状态更新提供了可中断的能力。虽然 v16、v17 已经应用了 Fiber 新架构,但并没有开启并发更新,所以实际表现和老架构一致。而 v17 和 v18 的区别在于从同步不可中断更新变成了异步可中断更新

并发更新参考为什么要设计 Fiber

(1)并发模式和并发更新:

  • 使用createRoot会开启并发模式,但开启了并发模式并不一定会开启并发更新,只是基本前提。
  • v18 中以是否使用并发特性作为是否开启并发更新的依据。

(2)并发特性:useDeferredValue、useTransition,开启并发模式后才能使用。

两者都是用于管理并发渲染的过渡效果,旨在让应用更加平滑地响应用户操作,提高用户体验。

  • useTransition 生成的startTransitionAPI 会将内部回调函数所触发的渲染标记为非紧急渲染,可以被其他紧急渲染抢占。
// isPending 指处于过渡状态,正在加载中
// startTransition 通过回调将状态更新包装为一个过渡任务,是一个低优先级的更新
const [isPending, startTransition] = useTransition(); // 返回数组
startTransition(() => {
    setList(new Array(10000).fill(null));
})
  • useDeferredValue:返回一个延迟更新的值。只有当前没有紧急更新时,该值才会变为最新值。
const deferredList = useDeferredValue(value);

两者异同点

  • 两者都是将任务标记成了非紧急更新
  • useTransition 是将任务包装成了延迟更新任务,useDeferredValue是生成一个表示延时状态的值。(一个用来包装方法,一个用来包装值)

Untitled (3).png

2. 引入新的 root API

调用createRoot可以开启并发模式。

// React 17
import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.render(<App />, root)

// React 18
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root'); 
ReactDOM.createRoot(root).render(<App />); // 区别

3. 自动批处理(包括 setState)

v18 默认将多个状态更新合并成一次更新,以提高应用的性能。退出批量更新可以使用flushSync

4. 新增 API

  • useId:支持同一个组件在客户端和服务端生成相同的 ID,避免 hydration 的不兼容。
const id = useId();
  • useSyncExternalStore(忽略):一般是三方状态管理库使用,日常业务中不需要关注。
  • useInsertionEffect(忽略):只建议 css-in-js 库使用。

九、设计模式和架构模式

1. 前端工程化

能够提高软件质量、开发效率和方便协同开发的事情都属于工程化,比如提高应用的性能、稳定性、可维护性等。前端工程化可以分为以下四个方面:

  • 模块化是从业务功能层面对代码和资源进行拆分,一个模块就是一个实现特定功能的文件,比如支付功能。JS 模块化方案有AMD/CMD/module等,CSS 模块化方案有less/sass等。

  • 组件化是从UI层面进行拆分,UI是由多个组件进行嵌套、组合构成的,比如输入框、提交按钮。

  • 规范化包括编码规范(eslint、文件命名规范)、开发流程规范(codeReview)等。

  • 自动化包括自动化测试、构建、部署等,将这些简单重复的工作交给机器来做。

    • 开发者提交一个代码合入 Master 的请求后就会去跑自动化测试(单元测试、集成测试、e2e测试)。
    • 测试通过后代码合入Master,然后开始 Build,将源码转换为可运行的实际代码,需要安装依赖、配置各种资源等。
    • 然后当前代码就是一个可以直接部署的release版本。

2. 如何封装一个组件

组件封装是不会直接暴露内部结构,而是提供 props 去控制组件。

  1. 为什么要封装组件
    • 避免重复的工作量,能够提高开发效率
    • 使代码逻辑更加清晰,方便项目的后期维护
    • 将业务功能收敛到组件里开发,不会影响其他人的业务,有利于协同开发
  2. 如何封装一个组件
    • 单一职责原则:一个组件应该只负责一件事情,不要耦合一些不相关逻辑。
    • 复用性:Button等基础组件需要考虑通用性,业务组件需要考虑当前业务的一些特殊场景。
    • 扩展性:有新的业务需要时是否容易扩展。
    • 可维护性:如果组件过度追求复用性会导致内部的代码臃肿,维护成本较高。
    • 组件的粒度:拆分并不是越细越好,遵循高内聚、低耦合的理念。

3. 架构模式:MVC/MVP/MVVM

MVX架构是为了分离 View 和 Model,让应用有清晰合理的架构,方便模块化开发和后期维护。三者的区别在于解耦的方案不同。

三个架构的公共部分有:Model 模型层负责存取、管理数据,View 视图层就是用户界面

MVC:Model-View-Controller

image.png Controller控制器连接 View 和 Model,它负责处理用户的输入请求,然后调用 Model 处理数据并将结果传给 View。

所有通信都是单向的。

  • view 传送指令到 controller
  • controller 完成业务逻辑后,要求 model 改变状态
  • model 将新的数据发送给 view,用户得到反馈。

MVVM(Vue实践):Model-View-ViewModel

image.png

ViewModel视图模型层负责把 Model 的数据同步到 View 显示出来,把 View 的修改同步回 Model。

ViewModel 和 View 层是双向数据绑定,和 Model 层通过接口进行数据交互。

MVVM 和 MVC 区别

mvvm 通过双向数据绑定实现了 View和Model的自动同步,view 变动会自动改变 model。

  • MVC 用于服务端,像Spring MVC。
  • MVP(Model-View-Presenter)用于安卓开发。
  • MVVM 用于前端。

参考

React18 新特性解读

一文解读 React17 和 React18 的更新变化