Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: provide/inject 完善父组件继承(小程序) #1781

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docs-vuepress/guide/advance/provide-inject.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,22 @@ const foo = inject(key, 1) // ❌ 默认值是非字符串则会 TS 类型报错
## 跨端差异

- Mpx 输出 Web 端后,使用规则与 Vue 一致,`provide/inject` 的生效范围严格遵行父子组件关系,只有父组件可以成功向子孙组件提供依赖。
- Mpx 输出小程序端会略有不同,由于小程序原生框架限制,暂时无法在子组件获取真实渲染时的父组件引用关系,所以不能像 Vue 那样基于父组件原型继承来实现 `provide`。在 Mpx 底层实现中,我们将组件层的 `provide` 挂载在所属页面实例上,相当于将组件 scope 提升到页面 scope,可以理解成一种“降级模拟”。当然,这并不影响父组件向子孙组件 `provide` 的能力,只是会额外存在“**副作用**”:同一页面中的组件可以向页面中其他所有在其之后渲染子组件提供依赖。比如同一页面下的组件 A 可以向后渲染的兄弟组件 B 的子孙组件提供数据,这在 Web 端是不允许的。因此,针对小程序端可能出现的“副作用”需要开发者自行保证,可以结合上述注入名的管理优化来规避。
- Mpx 输出小程序端后同样遵循类似 Web 端的父子组件关系,但是在 `slot` 场景下小程序表现会略有差异。举个例子,如下代码所示:在组件 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 模板中定义了此组件的组件)才是它的父组件。

```html
<!-- ComponentA -->
<template>
<view>组件 A</view>
<ComponentB>
<ComponentC></ComponentC>
</ComponentB>
</template>

<!-- ComponentB -->
<template>
<view>
<view>组件 B</view>
<slot></slot>
</view>
</template>
```
19 changes: 9 additions & 10 deletions packages/core/src/core/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export default class MpxProxy {
// web中BEFORECREATE钩子通过vue的beforeCreate钩子单独驱动
this.callHook(BEFORECREATE)
setCurrentInstance(this)
this.parent = this.resolveParent()
this.provides = this.parent ? this.parent.provides : Object.create(null)
// 在 props/data 初始化之前初始化 inject
this.initInject()
this.initProps()
Expand All @@ -193,6 +195,13 @@ export default class MpxProxy {
}
}

resolveParent () {
if (isFunction(this.target.selectOwnerComponent)) {
const parent = this.target.selectOwnerComponent()
return parent ? parent.__mpxProxy : null
}
}

createRenderTask (isEmptyRender) {
if ((!this.isMounted() && this.currentRenderTask) || (this.isMounted() && isEmptyRender)) {
return
Expand Down Expand Up @@ -232,16 +241,6 @@ export default class MpxProxy {
// 页面/组件销毁清除上下文的缓存
contextMap.remove(this.uid)
}
if (!isWeb && this.options.__type__ === 'page') {
// 小程序页面销毁时移除对应的 provide
if (isFunction(this.target.getPageId)) {
const pageId = this.target.getPageId()
const providesMap = global.__mpxProvidesMap
if (providesMap.__pages[pageId]) {
delete providesMap.__pages[pageId]
}
}
}
this.callHook(BEFOREUNMOUNT)
this.scope?.stop()
if (this.update) this.update.active = false
Expand Down
35 changes: 13 additions & 22 deletions packages/core/src/platform/export/inject.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { callWithErrorHandling, isFunction, isObject, warn } from '@mpxjs/utils'
import { currentInstance } from '../../core/proxy'

const providesMap = {
/** 全局 scope */
__app: Object.create(null),
/** 页面 scope */
__pages: Object.create(null)
}

global.__mpxProvidesMap = providesMap
/** 全局 scope */
let appProvides = Object.create(null)

/** @internal createApp() 初始化应用层 scope provide */
export function initAppProvides (appOptions) {
Expand All @@ -18,22 +12,20 @@ export function initAppProvides (appOptions) {
? callWithErrorHandling(provideOpt.bind(appOptions), appOptions, 'createApp provide function')
: provideOpt
if (isObject(provided)) {
providesMap.__app = provided
appProvides = provided
} else {
warn('App provides must be an object or a function that returns an object.')
}
}
}

function resolvePageId (context) {
if (context && isFunction(context.getPageId)) {
return context.getPageId()
function resolveProvides (vm) {
const provides = vm.provides
const parentProvides = vm.parent && vm.parent.provides
if (parentProvides === provides) {
return (vm.provides = Object.create(parentProvides))
}
}

function resolvePageProvides (context) {
const pageId = resolvePageId(context)
return providesMap.__pages[pageId] || (providesMap.__pages[pageId] = Object.create(null))
return provides
}

export function provide (key, value) {
Expand All @@ -42,8 +34,7 @@ export function provide (key, value) {
warn('provide() can only be used inside setup().')
return
}
// 小程序无法实现组件父级引用,所以 provide scope 设置为组件所在页面
const provides = resolvePageProvides(instance.target)
const provides = resolveProvides(instance)
provides[key] = value
}

Expand All @@ -53,11 +44,11 @@ export function inject (key, defaultValue, treatDefaultAsFactory = false) {
warn('inject() can only be used inside setup()')
return
}
const provides = resolvePageProvides(instance.target)
const provides = resolveProvides(instance)
if (key in provides) {
return provides[key]
} else if (key in providesMap.__app) {
return providesMap.__app[key]
} else if (key in appProvides) {
return appProvides[key]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.target)
Expand Down
Loading