Skip to content

Commit db66545

Browse files
authored
Merge pull request #1781 from didi/feat-mp-parent-reference
Feat: provide/inject 完善小程序父组件继承 & 支持 RN
2 parents 3156cfd + 0873896 commit db66545

File tree

5 files changed

+68
-37
lines changed

5 files changed

+68
-37
lines changed

docs-vitepress/guide/advance/provide-inject.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,5 +244,24 @@ const foo = inject(key, 1) // ❌ 默认值是非字符串则会 TS 类型报错
244244

245245
## 跨端差异
246246

247-
- Mpx 输出 Web 端后,使用规则与 Vue 一致,`provide/inject` 的生效范围严格遵行父子组件关系,只有父组件可以成功向子孙组件提供依赖。
248-
- Mpx 输出小程序端会略有不同,由于小程序原生框架限制,暂时无法在子组件获取真实渲染时的父组件引用关系,所以不能像 Vue 那样基于父组件原型继承来实现 `provide`。在 Mpx 底层实现中,我们将组件层的 `provide` 挂载在所属页面实例上,相当于将组件 scope 提升到页面 scope,可以理解成一种“降级模拟”。当然,这并不影响父组件向子孙组件 `provide` 的能力,只是会额外存在“**副作用**”:同一页面中的组件可以向页面中其他所有在其之后渲染子组件提供依赖。比如同一页面下的组件 A 可以向后渲染的兄弟组件 B 的子孙组件提供数据,这在 Web 端是不允许的。因此,针对小程序端可能出现的“副作用”需要开发者自行保证,可以结合上述注入名的管理优化来规避。
247+
- Mpx 输出 **Web** 端,使用规则与 Vue 一致,`provide/inject` 的生效范围严格遵行父子组件关系,只有父组件可以成功向子孙组件提供依赖。
248+
- Mpx 输出 **RN** 端,框架内部基于 React 的 `useContext` 钩子来转换实现,表现和 Web 端一致。
249+
- Mpx 输出 **小程序** 端同样遵循类似 Vue 的父子组件关系,但是在 `slot` 场景下小程序表现会和 Web、RN 端略有差异。举个例子,如下代码所示:在组件 A 的 `template` 模板中包含子组件 B,然后组件 B 通过 `slot` 包含子组件 C,组件 B 和 C 都定义在组件 A 的模板中。该场景下:在 Web 端,组件 A 是 B 的父组件,B 是 C 的父组件,所以 A 和 B 都可以向 C 提供依赖。但在小程序端,B 和 C 的父组件都是 A,组件 B 不能再向 C 提供依赖。这个跨端差异或者说“副作用”可以理解成:Web 端是基于“节点树”,即虚拟组件节点纬度,而小程序的父子组件关系是基于“组件树”,即组件模板维度,也就是说当前组件的创建者(在 WXML/AXML 模板中定义了此组件的组件)才是它的父组件。
250+
251+
```html
252+
<!-- ComponentA -->
253+
<template>
254+
<view>组件 A</view>
255+
<ComponentB>
256+
<ComponentC></ComponentC>
257+
</ComponentB>
258+
</template>
259+
260+
<!-- ComponentB -->
261+
<template>
262+
<view>
263+
<view>组件 B</view>
264+
<slot></slot>
265+
</view>
266+
</template>
267+
```

packages/core/src/core/proxy.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export default class MpxProxy {
171171
// web中BEFORECREATE钩子通过vue的beforeCreate钩子单独驱动
172172
this.callHook(BEFORECREATE)
173173
setCurrentInstance(this)
174+
this.parent = this.resolveParent()
175+
this.provides = this.parent ? this.parent.provides : Object.create(null)
174176
// 在 props/data 初始化之前初始化 inject
175177
this.initInject()
176178
this.initProps()
@@ -195,6 +197,18 @@ export default class MpxProxy {
195197
}
196198
}
197199

200+
resolveParent () {
201+
if (isReact) {
202+
return {
203+
provides: this.target.__getParentProvides
204+
}
205+
}
206+
if (isFunction(this.target.selectOwnerComponent)) {
207+
const parent = this.target.selectOwnerComponent()
208+
return parent ? parent.__mpxProxy : null
209+
}
210+
}
211+
198212
createRenderTask (isEmptyRender) {
199213
if ((!this.isMounted() && this.currentRenderTask) || (this.isMounted() && isEmptyRender)) {
200214
return
@@ -234,16 +248,6 @@ export default class MpxProxy {
234248
// 页面/组件销毁清除上下文的缓存
235249
contextMap.remove(this.uid)
236250
}
237-
if (!isWeb && this.options.__type__ === 'page') {
238-
// 小程序页面销毁时移除对应的 provide
239-
if (isFunction(this.target.getPageId)) {
240-
const pageId = this.target.getPageId()
241-
const providesMap = global.__mpxProvidesMap
242-
if (providesMap.__pages[pageId]) {
243-
delete providesMap.__pages[pageId]
244-
}
245-
}
246-
}
247251
this.callHook(BEFOREUNMOUNT)
248252
this.scope?.stop()
249253
if (this.update) this.update.active = false

packages/core/src/platform/createApp.ios.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Mpx from '../index'
77
import { createElement, memo, useRef, useEffect } from 'react'
88
import * as ReactNative from 'react-native'
99
import { Image } from 'react-native'
10+
import { initAppProvides } from './export/inject'
1011

1112
const appHooksMap = makeMap(mergeLifecycle(LIFECYCLE).app)
1213

@@ -35,6 +36,7 @@ export default function createApp (options) {
3536
const { NavigationContainer, createStackNavigator, SafeAreaProvider } = global.__navigationHelper
3637
// app选项目前不需要进行转换
3738
const { rawOptions, currentInject } = transferOptions(options, 'app', false)
39+
initAppProvides(rawOptions.provide, rawOptions)
3840
const defaultOptions = filterOptions(spreadProp(rawOptions, 'methods'), appData)
3941
// 在页面script执行前填充getApp()
4042
global.getApp = function () {

packages/core/src/platform/export/inject.js

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
import { callWithErrorHandling, isFunction, isObject, warn } from '@mpxjs/utils'
22
import { currentInstance } from '../../core/proxy'
33

4-
const providesMap = {
5-
/** 全局 scope */
6-
__app: Object.create(null),
7-
/** 页面 scope */
8-
__pages: Object.create(null)
9-
}
10-
11-
global.__mpxProvidesMap = providesMap
4+
/** 全局 scope */
5+
let appProvides = Object.create(null)
126

137
/** @internal createApp() 初始化应用层 scope provide */
148
export function initAppProvides (provideOpt, instance) {
@@ -17,22 +11,20 @@ export function initAppProvides (provideOpt, instance) {
1711
? callWithErrorHandling(provideOpt.bind(instance), instance, 'createApp provide function')
1812
: provideOpt
1913
if (isObject(provided)) {
20-
providesMap.__app = provided
14+
appProvides = provided
2115
} else {
2216
warn('App provides must be an object or a function that returns an object.')
2317
}
2418
}
2519
}
2620

27-
function resolvePageId (context) {
28-
if (context && isFunction(context.getPageId)) {
29-
return context.getPageId()
21+
function resolveProvides (vm) {
22+
const provides = vm.provides
23+
const parentProvides = vm.parent && vm.parent.provides
24+
if (parentProvides === provides) {
25+
return (vm.provides = Object.create(parentProvides))
3026
}
31-
}
32-
33-
function resolvePageProvides (context) {
34-
const pageId = resolvePageId(context)
35-
return providesMap.__pages[pageId] || (providesMap.__pages[pageId] = Object.create(null))
27+
return provides
3628
}
3729

3830
export function provide (key, value) {
@@ -41,8 +33,7 @@ export function provide (key, value) {
4133
warn('provide() can only be used inside setup().')
4234
return
4335
}
44-
// 小程序无法实现组件父级引用,所以 provide scope 设置为组件所在页面
45-
const provides = resolvePageProvides(instance.target)
36+
const provides = resolveProvides(instance)
4637
provides[key] = value
4738
}
4839

@@ -52,11 +43,11 @@ export function inject (key, defaultValue, treatDefaultAsFactory = false) {
5243
warn('inject() can only be used inside setup()')
5344
return
5445
}
55-
const provides = resolvePageProvides(instance.target)
56-
if (key in provides) {
46+
const provides = instance.parent && instance.parent.provides
47+
if (provides && key in provides) {
5748
return provides[key]
58-
} else if (key in providesMap.__app) {
59-
return providesMap.__app[key]
49+
} else if (key in appProvides) {
50+
return appProvides[key]
6051
} else if (arguments.length > 1) {
6152
return treatDefaultAsFactory && isFunction(defaultValue)
6253
? defaultValue.call(instance && instance.target)

packages/core/src/platform/patch/getDefaultOptions.ios.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { queueJob, hasPendingJob } from '../../observer/scheduler'
1111
import { createSelectorQuery, createIntersectionObserver } from '@mpxjs/api-proxy'
1212
import { IntersectionObserverContext, RouteContext, KeyboardAvoidContext } from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/context'
1313

14+
const ProviderContext = createContext(null)
15+
1416
function getSystemInfo () {
1517
const window = ReactNative.Dimensions.get('window')
1618
const screen = ReactNative.Dimensions.get('screen')
@@ -193,7 +195,7 @@ const instanceProto = {
193195
}
194196
}
195197

196-
function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx, relation }) {
198+
function createInstance ({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx, relation, parentProvides }) {
197199
const instance = Object.create(instanceProto, {
198200
dataset: {
199201
get () {
@@ -244,6 +246,12 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps
244246
return currentInject.getRefsData || noop
245247
},
246248
enumerable: false
249+
},
250+
__getParentProvides: {
251+
get () {
252+
return parentProvides || null
253+
},
254+
enumerable: false
247255
}
248256
})
249257

@@ -435,6 +443,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
435443
const propsRef = useRef(null)
436444
const intersectionCtx = useContext(IntersectionObserverContext)
437445
const pageId = useContext(RouteContext)
446+
const parentProvides = useContext(ProviderContext)
438447
let relation = null
439448
if (hasDescendantRelation || hasAncestorRelation) {
440449
relation = useContext(RelationsContext)
@@ -443,7 +452,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
443452
let isFirst = false
444453
if (!instanceRef.current) {
445454
isFirst = true
446-
instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx, relation })
455+
instanceRef.current = createInstance({ propsRef, type, rawOptions, currentInject, validProps, components, pageId, intersectionCtx, relation, parentProvides })
447456
}
448457
const instance = instanceRef.current
449458
useImperativeHandle(ref, () => {
@@ -536,6 +545,12 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
536545
// update root props
537546
root = cloneElement(root, rootProps)
538547
}
548+
549+
const provides = proxy.provides
550+
if (provides) {
551+
root = createElement(ProviderContext.Provider, { value: provides }, root)
552+
}
553+
539554
return hasDescendantRelation
540555
? createElement(RelationsContext.Provider,
541556
{

0 commit comments

Comments
 (0)