Vue3 功能拆解⑪ expose options&api
文章目录
vue3 中 expose 的原理及使用。
本文涉及的源码包: runtime-core。
expose in options
组件中的所有选项处理(methods, data, …)都在这个函数中,其中就包括 expose:
componentOptions.ts:applyOptions(instance: ComponentInternalInstance)
options 初始化顺序:
props
inject
methods
data, 延迟处理,因为它依赖
this
computed
watch, 延迟处理,因为它依赖
this
|
|
expose 属性的访问路径:
expose[] -> instance.exposed -> publicThis -> instance.proxy
instance.proxy
对组件上下文对象的代理对象。
instance.proxy 创建自:
runtime-core/src/component.ts:setupStatefulComponent
|
|
是对 instance.ctx 的代理,当你在实例中通过 this.xxx
或 ctx.xxx
去访问属性的时候实际走的是下
面这个代理。
|
|
属性代理
取值操作可能经过的路径(依优先级从左到右):
非
$xxx
的属性:setup state
>data
>ctx
>props
$xxx
的属性:publicPropertiesMap
->cssModule
->ctx
->globalProperties
实际上
$xxx
的属性只是ctx.xxx
的别名。设值,只能设置
setup state
>options data
有一种情况比较特殊:当要设置的 key 在
setup state
,options data
,props
,$xxx
,globalProperties
上都没有的时候,最后会直接被添加的ctx
上去:ctx[key] = value
has 属性检查的顺序:
cache 中
>data
>setup state
>props
>ctx
>publicPropertiesMap
>globalProperties
publicPropertiesMap:
|
|
从上面的 代码流程 来看,好像和 props, data 没什么区别吧❓
最终不都是走了 proxy get 那一套❓
从这里好像看不出什么…
ref & setRef
来看下面官方(runtime-core/__tests__/apiExpose.spec.ts)的例子:
|
|
key = __v_raw key = __v_isReadonly key = __v_raw key = __v_skip childRef.value = { foo: [Getter/Setter], bar: RefImpl { _rawValue: 2, _shallow: false, __v_isRef: true, _value: 2 } } key = foo childRef.value.foo = undefined key = bar childRef.value.bar = 2 key = baz childRef.value.baz = undefined key = fox childRef.value.fox = undefined undefined
从测试用例来看,这个 expose 用途是将组件本身的属性暴露出去,可以在父组件中通过
ref 取到该组件元素的引用 childRef (严格来说当有 expose 时就不再是指向 vnode.el了
),
然后就可以通过这个引用来直接访问 expose 出来的属性。
那这个是怎么做到的?
既然和 ref 元素本身有关系,那就得从这个 ref 去下手看看了。
ref 值设置的地方(setRef(...)
):在组件渲染完成之后才会有实际的DOM元素,所以这个肯定是发生在
mounted 之后。
调用 setRef()
的地方有:
patch(n1, n2, …) 函数的最后
1 2 3 4
// set ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) }
unmount(vnode, …) 的时候取消引用,防止内存泄漏
1 2 3
if (ref != null) { setRef(ref, null, parentSuspense, vnode, true) }
TIP
另外,组件实际的 DOM 元素的引用是在 vnode.el 上,这个值是在 mountElement(vnode)
中创建真实 DOM 元素的时候被赋值的,而 setRef()
是在 patch() 最后执行,所以在这个
时候 vnode.el 上就已经有了该组件真实 DOM 元素的引用,因为所有的组件流程一开始都
是经过的 patch()
函数(组件渲染完整流程图)。
上面只是知道了设置,但实际这里是需要知道是对 childRef.value.foo 的取值会发生些什 么,流程是什么,最终又是怎么和 expose 发生关联的?
下面来仔细看下 setRef
里面又发生了什么,设想应该是对 ref 的引用是不是做了代理❓
runtime-core/src/renderer.ts:setRef
|
|
在 setRef 中检测到如果是有状态的组件,会先执行
getExposeProxy(instance)
去 instance.exposed
中取值,如果没有则再去组件实例的代
理(也就是最开始分析的instance.proxy)中去取 expose proxy:
|
|
上面代码加了打印可以从 🔗上面的例子 中看到 key 的值。
以上就是 expose + ref 的使用及原理,下面还会对一些其它细节进行回顾。
$root and $parent
在 Child 中可以通过 this.$root 和 this.$parent 去取到 parent 中 expose 出 来的属性,这个又是怎么实现的呢❓❓❓
先看下测试用例(runtime-core/__tests__/apiExpose.spec.ts):
|
|
> root = parent this.$parent.foo = 1 this.$parent.bar = undefined this.$root.foo = 1 this.$root.bar = undefined $root: { foo: 1 } Root: undefined > root = parent.$parent this.$parent.foo = 1 this.$parent.bar = undefined this.$root.foo = undefined this.$root.bar = undefined $root: {} Root: { render: [Function: render] } 0
结果显示, 在 Child 中都可以正确取到 foo ,而取不到 bar。
那么为什么呢❓
可以从 publicPropertiesMap 中找到 $parent 和 $root 的引用:
|
|
两者都是映射到了 getPublicInstance
那什么是 public instance❓
代码位于: runtime-core/src/componentPublicInstance.ts:getPublicInstance
|
|
this.$parent.foo
倒是好理解, $parent
被映射到 getPublicInstance(i.parent)
找自
己的父级组件,如果有 exposed 级返回这个对象,所以这里也就等于是 instance.exposed.foo
然而对于 this.$root.foo
为什么也能取到 expose
里面的 1
❓
每个组件都会持有一份对 root 组件的引用,instance.root,这个引用的设置发生在创建 组件实例的时候:
|
|
所以 #1 和 #2 的结果不一样(1
和 undefined
)。
expose()
除了能使用 options api expose 之外,还可以通过在 setup() 中调用 expose({...})
来
暴露部分属性。
|
|
key = __v_raw key = __v_isReadonly key = __v_raw key = __v_skip key = $el childRef.value.$el.tag = div 0
expose()
函数又做了什么呢?为什么这个不传参数呢?
WARNING
expose()
只能在 setup() 中执行,且只能执行一次(非强制,多次也无意义,会覆盖)。
有关 setup ctx 参数的说明见: setup(_, ctx) 的第二个参数?
expose 函数其实很简单,赋值及初始化,不能重复调用也只是给出了警告:
|
|
设置了 instance.exposed 的代理,并且如果要访问的属性这个对象本身没有的时候会去
publicPropertiesMap 中去找,所以这个去访问 childRef.value.$el
的时候在
instance.exposed
上面是找不到的,最后找到的是 $el: i => i.vnode.el
。
总结
支持 options api
expose: [...]
支持 setup context
expose({...})
同时支持 option api 和 setup context
empty expose 等于没有
this.$parent
在子组件中使用可以取到父组件 expose 的属性this.$root
取到根节点上 expose 的属性setup() 中调用
expose()
不传参,最后属性访问会被代理到publicPropertiesMap
上
因为在 root1 的时候 Parent 就是 root 组件,所以
instance.root == parent