1. 前言
嘿,还记得上次我们聊的常用 React 设计模式详解上吗?那次我们主要聊了创建型模式和结构性模式在 React 里的用法。这次咱们继续聊聊行为型设计模式,主要讨论提供者模式和组件的受控与非受控模式。
2. 提供者模式
2.1 提供者模式定义
为啥要有这种模式呀?想象一下,在React里有个theme主题对象,UI组件得根据这个theme来换皮肤。那怎么做呢?如果一层层用props传的话,好麻烦啊,还得改好多组件,风险也不小呢。
提供者模式是一种高效的处理策略,它通过集中化的对象或组件来供给数据或服务,以供其他依赖于此的组件使用,从而实现了依赖注入的机制。
- 定义: 通过一个中心化的对象或组件来提供数据或服务给其他依赖它的组件,实现依赖注入。
- 使用: 在React组件树中,父节点或根节点(Root Node)通过Provider提供者注入共享上下文。随后,在需要访问该上下文的组件中,利用Consumer消费者来提取并渲染所需的数据。
- 提供者:需要是消费者的某一层父级
咱们来看看下面的流程哈:先是传入上下文数据,然后是消费者订阅数据。
来,咱们用Context来举个主题切换的例子吧!
2.2 原理
其实提供者实现原理挺简单的,createContext 会帮你生成一个包含 currentValue、Provider 和 Consumer 的对象,你看看下面的代码图就明白了。
_currentValue 就是用来存值的地方,Provider 就是用来改这个 _currentValue 的,也就是改 context 的值。Consumer 和 useContext 就是来读这个 _currentValue 的。
另外,为了实现 Context 嵌套的机制,React 利用栈的特性(后入先出),通过 pushProvider 入栈和 popProvider 栈,实现最外层先入栈,最内层的先出栈。可以看看下面的嵌套 Provider 例子。
注入数据时,最外层 Provider 将 value 值 10 压入栈,此时栈顶是 10。遍历里层 Provider 时将 value 值 100 压入栈,此时栈顶是 100,即context._currentValue 的值为 100。
消费数据时,组件 <Test2 />
开始读取,在其所在 Provider 范围内先读取栈顶的值,所以读取的是 100;里层的 Provider 完成遍历工作离开时,弹出栈顶的值 100,此时栈顶的值是 10, 即 context._currentValue 的值为 10,<Test1 />
里面读到的值也就为 10 了。
2.3 小结
提供者模式跟传统的依赖注入方式是类似的,只是提供者模式在 React 中通过 Provider 控制,用于全局状态和跨组件通信。
- 缺点:
- 可能导致组件耦合:过度使用 Context 会导致组件耦合度增加,影响可维护性。
- 性能问题:当 Provider 的 value 发生变化时,所有消费该 Context 的组件都会重新渲染,这可能引发性能问题
- 优点:
- 减少 props drilling:避免了必须将 props 逐层传递给深层组件的繁琐过程。
- 共享状态:适合需要在多个组件之间共享状态的情况,如用户信息、主题、语言等
3. 组件的受控与非受控模式
3.1 定义
什么是组件的受控与非受控?
定义:受控模式和非受控模式的概念来自用户输入场景,通过代码改变表单 value 是受控模式,通过用户控制表单 value 是非受控模式。
看这里,我给你举两个例子。
第一个是非受控组件,你在input框里输入东西,它就会直接打印出value。
第二个是受控组件,我们通过useState来给它传value,然后用setValue来回填信息。
你可以看看下面总结的流程图。
3.2 受控组件是单向数据流的一种实现方式
单向数据流:指数据自上而下(从父组件到子组件)流动, 子组件通过 props 接收数据并渲染视图。当需要修改数据时,通常通过回调函数将修改请求传递回给父组件,由父组件管理状态和数据更新。
单向数据流的特点:
- 数据单向流动:状态保存在上层组件中,并通过 props 传递给子组件。
- 可预测性:每次数据流动的路径和影响都是可预测的,便于调试和测试。
- 易于调试:状态变化清晰明确,错误易于定位
咱们来看看一个计数器的例子
先左边的例子,父组件会声明一个叫做 count
的状态和一个 increment
的方法,然后就把它们传给子组件,让子组件来展示和回调执行。
再看看右边的表单输入的例子,数据是从父组件的状态流向受控组件的 value
属性的,而用户输入的数据呢,就会通过事件处理程序再流回到父组件的状态中,是不是有异曲同工之秒呀?
3.3 进一步抽象:组件状态控制管理
现在,我们可以进一步抽象用户表单输入场景,怎么进行组件状态管理。
- 非受控组件:
- 如果 props 中没有 value,则组件内部自己管理 state
- 受控组件:
- 如果 props 有 value 字段、onChange 字段,则由父级接管控制 state
- 组件的值由 React 状态控制,组件的每一次用户输入都会触发事件处理程序(通常是 onChange),该处理程序更新 React 状态,从而引发组件的重新渲染。
举个日历组件的例子,通过点击日期切换,展示不同的日期字符串。
第一个例子是非受控的,它有defaultValue和onChange,而第二个例子则有value和onChange,它的状态完全由父组件掌控。
3.4 如何实现组件既受控也非受控
原理:props 同时支持 value 和 defaultValue,通过判断 value 是不是 undefined 来区分受控模式和非受控模式。
初始化的时候,如果咱们是受控模式的话,useState 的初始值就直接用 props.value,然后渲染的时候也是用 props.value。
那如果是非受控模式呢,渲染就用内部 state 的 value,然后 changeValue 的时候调用 setValue 更新它。
另外,咱们在 useEffect 里监听一下 propsValue,如果它不是第一次渲染,但 value 突然变成了 undefined,那就说明咱们从受控模式切换到非受控模式了,这时候要赶紧把 state 同步成 propsValue,保持数据一致性。
3.5 小结
受控组件与观察者模式类似,React 监听并更新组件状态,类似观察者的主题与观察者之间的关系,非受控组件与命令模式类似,通过 ref 发送命令获取组件的值,而不直接管理状态。
- 什么情况使用受控模式
- 当你需要对输入的值做处理之后设置到表单的时候,或者你想实时同步状态值到父组件。
- 例子,比如用户下单页面,再来一单、持久化等。
- 什么情况使用非受控模式
- 调用方只需要拿到用户的输入就行。
- 例子,比如一些业务弹框组件。
总结
本文讲解了 React 中的行为型设计模式,包括提供者模式和组件受控与非受控模式。
- 提供者模式:通过中心化 Context API 实现跨组件状态共享
- 组件的受控与非受控模式:通过 value 和 onChange 、defaultValue 确保不同的用户场景下,React 状态管理方便和数据流动清晰。