广播模式
Provide design暂时想不到更好的命名方法, 就先使用广播模式吧[捂脸]
这个模式在常见的框架React, Vue 中经常起很大的作用, 这一节也主要是基于这两个框架的实现来展开.
由于我自己目前深度不够, 还没琢磨透这两个框架, 所以这里也是浅浅记录下.
1. 应用场景
在我们日常的开发中, 经常出现父子组件传值的问题, 这时候我们可以使用props 来进行信息传递, 但是如果子组件的子组件, 也就是孙组件也想要使用父组件的值怎么办? 如果只有三个层级, 可能使用props 也是可以接受的, 但是如果层级很深, 那无疑就不适用了. 一层一层的props 传递会是一个很耗费精力, 同时在父组件有修改时, 很容易出现修改不全出现bug的事情. 所以provide 模式应运而生, 其在很多不同层级中的组件可以轻松访问同样的一些数据, 比较突出的为: 孙子组件获取父组件中相应的数据.
在vue 中, 表现形式是provide 和inject API, 前者负责提供数据的定义, 后者负责在具体的深层级组件中进行注入使用; 在react 中的表现形式则是createContext 和 useContext API, 同样是前者定义数据, 后者获取数据.
这种模式虽然很好用, 但是也不能滥用, 否则的话会导致组件挂在数据过多, 如果有重名的provide 还可能会出现一些未知的bug(react 的文档中说其会使得组件的复用性变差). 我个人理解这种模式更适合用于大型业务组件中, 比如我现在开发的项目有工作项详情页面, 一些逻辑在下面引用中大致说下.
工作项详情页面为C1组件; 然后详情页面分为标题, 描述, 文件上传区, 评论日志区, 侧边属性区, 这些可以称为C组件; 然后中评论日志区又有具体的评论组件C3, 日志组件C3; 评论组件有新增和回复组件C4.
即组件结构为C1 -> C2 -> C3 -> C4
数据的获取是在C1组件中, 如工作项id等, 同时C4组件会用到工作项id, 所以使用provide进行参数传递无疑是更好的选择.(全局状态管理有些太大了, 不适用)

2. Vue中的实现
vue中的实现比较容易看懂, 写起来也比较简单.
2.1 demo
// vue2
export default {
// ...
// use function syntax so that we can access `this`
provide() {
return {
staticId: this.staticId, // 静态传递
dynamatic: () => this.dynamaticId, // 动态传递(这里vue2不用包裹computed就可实现, 官网上说vue3.3以后也默认不需要)
}
},
}
export default {
// ...
inject: ['staticId', 'dynamatic'],
mounted() {
console.log(this.staticId) // 静态数据
console.log(this.dynamatic()) // 动态数据
},
}// vue3
<script setup>
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
</script>
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>// vue3响应式
<!-- inside provider component -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
// define update function
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- in injector component -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
// vue3 只能在provide组件中修改
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>2.2 实现原理
下面主要是根据上面掘金文章进行的一个总结.
vue3 中的主要实现是基于原型链实现的, 可以理解为当父组件提供了provide 数据, 那么该对象会在实例上的provide 属性上进行赋值. 如果该元素有子组件, 那么在子组件里面会调用相应的方法去继承父组件上的provide 数据, 同时挂载其本身的provide 数据(如果有), 同理后续组件依次进行继承和挂载provide数据.
通过这一系列操作后, 后续需要使用该数据的组件就可以基于原型链依次向上查找,通过inject API获取对应数据.
两者的源码地址都在packages/runtime-core/src/apiInject.ts文件中.
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides
// by default an instance inherits its parent's provides object
// but when it needs to provide values of its own, it creates its
// own provides object using parent provides object as prototype.
// this way in `inject` we can simply look up injections from direct
// parent and let the prototype chain do the work.
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
// TS doesn't allow symbol as index type
provides[key as string] = value
}
}
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T,
treatDefaultAsFactory?: false
): T
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T | (() => T),
treatDefaultAsFactory: true
): T
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance
if (instance) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides =
instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}2.3 两个API的渲染时机
vue2中注入inject是执行data 前, 这是为了可以在data 里可以访问; 提供数据provide 是在data, computed, watch 之后, 这是为了可以使用其中的一些数据.

在vue3这里的处理时机/顺序我找了好久, 包括搜索和询问组内大佬, 发现vue3应该是不存在这个渲染时机/变量覆盖问题的, 因为setup 中变量的名字都是唯一的, 所以不存在优先级问题.
下面是初始化时setup的调用地址, 以及合并options的地方.
setup的调用

合并options

3. React中的实现
在react的官方文档Context 章节中, 不是完全推荐使用这种模式进行开发的. 其中提到了组件组合 的解决方案, 这种解决方案相当于把组件当做props 进行传递, 使用到了React 中的props.children, 这和vue的slot 概念很像, 但是这种解决方案也是需要依次传递的.
所以综上所述, 在具体的开发中是使用provide 还是 props.children, 亦或是Redux 这种全局管理插件, 需要按需分析.
3.1 demo
- 标准写法
父组件中提供数据
// parent.tsx
const MyContext = React.createContext(defaultValue);
// 一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
<MyContext.Provider value={/* 某个值(如MyContext) */}>class类型的子组件获取数据
// children.tsx
// Class.contextType
// 可以使用 this.context 来获取最近 Context 上的值
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;
// 实验性的 public class fields 语法
class MyClass extends React.Component {
static contextType = MyContext;
render() {
let value = this.context;
/* 基于这个值进行渲染工作 */
}
}function类型的子组件获取数据
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>- 使用
Hook钩子的写法
// parent.tsx
export const ThemeContext = React.createContext();
// children.tsx
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export function children() {
const theme = useContext(ThemeContext);
}这种写法省去了标签, 但是每次到了新的页面都需要导入ThemeContext , 所以这里可以再提取一个自定义的Hook 出来, 具体如下
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}在这个自定义的Hook 里面导入ThemeContext, 然后其他页面统一导入该Hook 进行数据读取, 少了import { ThemeContext } from "./App" 的步骤, 也将业务和数据获取做了拆分.
在组件中更新Context
theme-context.js
// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const ThemeContext = React.createContext({
theme: themes.dark, toggleTheme: () => {},}
);theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
// Theme Toggler 按钮不仅仅只获取 theme 值,
// 它也从 context 中获取到一个 toggleTheme 函数
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;app.js
// 定义初始状态和更新函数
import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
// State 也包含了更新函数,因此它会被传递进 context provider。
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}
render() {
// 整个 state 都被传递进 provider
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<App />);3.2 实现原理
createContext` 的内部实现如下(主要是依据上面的链接总结下)
// createContext 可以让我们实现状态管理
// 还能够解决传递 Props drilling 的问题
// 假如一个子组件需要父组件的一个属性,但是中间间隔了好几层,这就会出现开发和维护的一个成本。这时候就可以通过这个 API 来解决
function createContext(defaultValue, calculateChangedBits) {
var context = {
?typeof: REACT_CONTEXT_TYPE,
_calculateChangedBits: calculateChangedBits,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
// 以下两个属性是为了适配多平台
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0,
// These are circular
Provider: null,
Consumer: null
};
// 以下的代码很简单,就是在 context 上挂载 Provider 和 Consumer,让外部去使用
context.Provider = {
?typeof: REACT_PROVIDER_TYPE,
_context: context
};
var Consumer = {
?typeof: REACT_CONTEXT_TYPE,
_context: context,
_calculateChangedBits: context._calculateChangedBits
};
context.Consumer = Consumer;
context._currentRenderer = null;
context._currentRenderer2 = null;
return context;
}其他更多的后面看完再更新, 它的源码我还没开始看过.
4. 其他
最近越来越有一种感觉, vue和react就像是自动挡的车和手动挡的车.
vue是开箱即用, 什么都给你配置优化好, 不用管什么几档几档, react就是需要自己去挂离合换挡, 可操纵性高. 虽然手动档的react省油(性能高), 但是自动挡的vue省心啊(优化负担小, 比较现在生活水平上来了). vue写起来, 包括看文档还是比较轻松一些的.
