接上次看vue-router看到了provideinject, 觉得应该比较简单, 打算看一下实现.

过程中发现provide, inject与其他一些 api 比如onMounted, onUnmounted是不能在异步结果中调用的. (更不能在setup外调用)

另外provide的时候可不可以inject到自己也不太确定. (虽然文档里都说得很明确)

provide/inject用法

普通的用法就是provide(key, value), 这样是子孙组件中就可以通过inject(key)获取到value了.

需要注意的是这需要在setup()中同步调用. 其实inject()也是一样的, 只是取值不会有异步场景所以文档中没提示.

setup()是组件里的, 而在插件中不是一定加载组件的, 所以还有所谓”app level provide”, 方式就是app.provide(key, value).

先从比较简单的app-level provide开始.

app-level provide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function createApp(rootComponent, rootProps = null) {
// ...option api兼容和其他app方法用到的变量声明
const context = {
// ...
app: null as any,
provides: Object.create(null),
}
// ...
const app: App = (context.app = {
_context: context,
// ...use, directive, component等方法
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
if (!isMounted) {
const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// ...mount流程
},
provide(key, value) {
context.provides[key as string | symbol] = value
return app
},
runWithContext(fn) {
const lastApp = currentApp
currentApp = app
try {
return fn()
} finally {
currentApp = lastApp
}
},
})
return app
}

这个是我们启动vue流程要调用的createApp(), 他返回的app对象中的provide()方法非常简单, 就是把键值写到context.provides里.

而这个context可以从app._context, 或者是根节点的vnodeappContext里获取到.

这个vnode又会被mount()后续的动作传到后面的子组件实例里.

inject

provide设置的内容放在了app._context中, 我们看一下inject是如何取到的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

// also support looking up from app-level provides w/ `app.runWithContext()`
if (instance || currentApp) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
// #11488, in a nested createApp, prioritize using the provides from currentApp
const provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined

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) {
// ...默认值相关
}
}
}

可以看到关键点就在于const provides = ...的取值, 之后就是把provides对应的key返回就行.

我们仔细来看provides的取值优先级:

  1. 如果有currentApp就取currentApp._context.provides. 而currentApp这个变量非常明确, 只有runWithContext()可以调用他.

    所以这第一个情况是runWithContext()+inject()专属情况.

    接下来的情况是有currentInstance的, 也就是在setup()里调用的. (currentRenderingInstance是执行render()函数的时候设置的, 其实是同一个实例.)

  2. 如果是根节点, 就取instance.vnode.appContext.provides. 也就是app-level provide设置的值.

  3. 如果不是根节点, 就取instance.parent.provides.

看到这里就需要去了解currentInstance了, 因为:

  • 上面提到的(2)中, 其实我们只知道vnode.appContext是上面app-level provide的值, 但并不知道instancevnode是如何挂上的, 挂的是不是期望的vnode, 没有连起来.
  • 上面提到的(3)中, instanceparent是如何挂上的, parentprovides又是什么. (不能因为provides名字而和上文提到的context.provides搞混, 名字类似并不表示他们是指到一个地址的)
  • 最重要的是currentInstance是什么时候被创建的, 对应的是什么实例, parent之间的数据结构又是什么.

currentInstance的来源和相关的vue的启动流程

总结下我们现在的信息: app.provide是把信息存在了一个变量context里, inject取变量的时候分为三个情况, 是通过三个不同的路径取到context的.

其中通过runWithContext()currentApp_context, 比较明确, 而后面2个涉及到currentInstance这个变量, 就需要简单理一下从项目入口到调用inject()的过程了.

从入口到patch()

一个普通vue项目的入口大概是这样的: createApp(App).mount('#app').

其中App从sfc编译过来是个组件声明的js对象, 有setuprender属性, 其实就对应了sfc的scripttemplate.

.mount()方法是createApp()返回的, 本文开头有, 现在补充.mount()的详细内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
if (!isMounted) {
const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// ...hmr相关
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, namespace)
}
isMounted = true
app._container = rootContainer

return getComponentPublicInstance(vnode.component!)
}
}
}

我们是客户端的情况, 所以会走到render(), 创建vnode的参数rootComponent就是createApp(App)App.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const render = (vnode, container, namespace) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
} else {
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace
);
}
if (!isFlushing) {
isFlushing = true;
flushPreFlushCbs();
flushPostFlushCbs();
isFlushing = false;
}
container._vnode = vnode;
};

挂载和卸载都是调用render(), 如果是挂载的情况, 就会调用patch().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const patch = (...) => {
if (n1 === n2) {
return;
}
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1);
unmount(n1, parentComponent, parentSuspense, true);
n1 = null;
}
if (n2.patchFlag === -2) {
optimized = false;
n2.dynamicChildren = null;
}
const { type, ref, shapeFlag } = n2;
switch (type) {
case Text:
processText(...);
break;
case Comment:
processCommentNode(...);
break;
case Static:
if (n1 == null) {
mountStaticNode(...);
} else if (!!(process.env.NODE_ENV !== "production")) {
patchStaticNode(...);
}
break;
case Fragment:
processFragment(...);
break;
default:
if (shapeFlag & 1) {
processElement(...);
} else if (shapeFlag & 6) {
processComponent(...);
} else if (shapeFlag & 64) {
type.process(...);
} else if (shapeFlag & 128) {
type.process(...);
}
}
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
}
};

patch()的任务是对比”上个状态”和”目标状态”的vnode(n1n2)调用dom操作.

叶子节点vnodepatch()分2步:

  1. 根据vnode的类型, 分配给不同函数处理.
  2. 根据n1是否存在, 来判断是挂载还是diff. 最后进行dom操作.

而我们关心的组件, 不是叶子节点, 会交给processComponent()处理.

组件里最终还是会包含叶子节点的, 在经过一些处理后, 会递归调用patch(), 直到叶子节点, 以dom操作退出递归.

挂载组件

n1为空的情况下, processComponent()会把挂载流程交给mountComponent().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, namespace, optimized) => {
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
));
// ...keep-alive组件处理
setupComponent(instance);
if (instance.asyncDep) {
// ...异步组件处理
} else {
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized
);
}
};

可以看到关键的代码分为这三步:

  1. 创建组件实例, 并挂到vnodecomponent属性上.

    inject()就是从这个组件实例中获取provides.

    在一些流程中, 组件实例会被设置为currentInstance.

    而要获取实例, 以及相关的变量关系是:

    dom => dom._vnode (vnode) => vnode.component (component实例), vnode.type (component定义)

  2. 进行组件的setup.

    setup()作用是为render()做准备的.

    render()的作用是每次运行会返回最新的vnode, 把最新的vnode与老的一起给patch(), 就可以进行diff最后操作dom.

    setup()只会执行一次, 而render()在每次更新组件都会执行.

    setup()的形式有很多种, 最常见的sfc是返回template的执行环境, 在一些组件里会直接返回render()函数, 或者是异步组件会返回promise. (但作用都是为render()的执行做准备)

  3. 创建组件的render-effect.

    effect的内容是执行render()函数, 获取到vnode, 并且patch(). (这个流程本文前面已经提到几次了, 以前的文章里有详细说)

    然后把effect挂到组件实例上, 再给组件实例挂个update()方法, 就是执行一下effect.run().

    顺带一提, patch组件如果有老vnode, 就会走到updateComponent, 而不是现在的mountComponent, 这时候就会直接执行组件实例的update()方法, 并且把老vnode上的组件实例赋值给新vnode, 而这个vnode会在effect执行的时候被挂到组件实例的subTree上.

其实讲到这里已经理清了. 如果想更清晰, 下面会贴一些这三个步骤的具体代码.

createComponentInstance细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
// ...省略了一些属性, 保留了一些本文中提及的属性.
type,
parent,
appContext,
root: null,
next: null,
subTree: null,
effect: null,
update: null,
render: null,
proxy: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
};
// ...一些属性的初始化设置
return instance;
}

在创建组件实例的时候我们细看2个点.

  • appContext的取值: 根节点会取createApp()时创建的context, 其余组件实例都会指向父实例.

    也就是所有组件实例挂上的是同一个context.

    (可以通过在任何组件的setup()中打印getCurrentInstance()appContext都是可以三等的)

  • provides的取值: 与context类似, 但根组件用Object.create()来创建了新对象.

    利用js原型链来使修改组件实例的provides不影响context中的, 却能取到context中的值.

    如果不是根节点, 就指向父节点. (但在调用provide()的时候会修改, 后面展开)

创建完的组件实例会被频繁的使用, 获取这个实例的方式请看上文的总结.

setup细节

1
2
3
4
5
6
7
8
9
10
function setupComponent(instance, isSSR = false) {
isSSR && setInSSRSetupState(isSSR);
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance);
initProps(instance, props, isStateful, isSSR);
initSlots(instance, children);
const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : void 0;
isSSR && setInSSRSetupState(false);
return setupResult;
}

vnode上的属性同步到组件实例上, 并调用setupStatefulComponent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function setupStatefulComponent(instance, isSSR) {
var _a;
const Component = instance.type;
instance.accessCache = /* @__PURE__ */ Object.create(null);
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
const { setup } = Component;
if (setup) {
const setupContext = instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null;
const reset = setCurrentInstance(instance);
pauseTracking();
const setupResult = callWithErrorHandling(
setup,
instance,
0,
[
!!(process.env.NODE_ENV !== "production") ? shallowReadonly(instance.props) : instance.props,
setupContext
]
);
resetTracking();
reset();
if (isPromise(setupResult)) {
// ...异步组件的处理
} else {
handleSetupResult(instance, setupResult, isSSR);
}
} else {
finishComponentSetup(instance, isSSR);
}
}

可以看到在执行setup()前后分别调用了setCurrentInstance(instance)reset().

然后获得setup()的执行结果, 会在接下来的handleSetupResult()里来处理不同类型的结果.

1
2
3
4
5
6
7
8
9
10
11
12
function handleSetupResult(instance, setupResult, isSSR) {
if (isFunction(setupResult)) {
if (instance.type.__ssrInlineRender) {
instance.ssrRender = setupResult;
} else {
instance.render = setupResult;
}
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance, isSSR);
}

常用的sfc, setup()返回的是对象, 这个对象会作为template的执行环境, 会走到instance.setupState = proxyRefs(setupResult)

组件因为比较灵活, setup()可能返回render()函数, 会走到instance.render = setupResult.

(到这里已经可以猜到, 在创建render-effect的时候, 就会带着”setupState“来跑render()函数来获取最新的vnode)

到现在, sfc情况的组件实例还没有render()函数, 所以在finishComponentSetup()里处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function finishComponentSetup(instance, isSSR, skipOptions) {
const Component = instance.type;
if (!instance.render) {
if (!isSSR && compile && !Component.render) {
const template = Component.template || resolveMergedOptions(instance).template;
if (template) {
const { isCustomElement, compilerOptions } = instance.appContext.config;
const { delimiters, compilerOptions: componentCompilerOptions } = Component;
const finalCompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
);
Component.render = compile(template, finalCompilerOptions);
}
}
instance.render = Component.render || NOOP;
if (installWithProxy) {
installWithProxy(instance);
}
}
}

可以看到给组件实例的render()函数赋值为instance.render = Component.render.

这个Component是组件定义对象. 按我理解, 正常的sfc走到这里, template已经在编译时被编译成render()函数了.

如果在组件定义时使用了js对象, 又手动写了template属性, 在这里会进行一次运行时编译. (如果引入的vue没有运行时编译, 会进行提示, 我这里没有贴这段代码)

到这里, setup()的任务已经做完了, 组件实例有了render()方法和执行环境, (执行render()方法后就能获得最新vnode), 就可以下一步建立组件的render-effect了.

render-effect细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, namespace, optimized) => {
const componentUpdateFn = () => {
// ..
};
const effect = instance.effect = new ReactiveEffect(
componentUpdateFn,
NOOP,
() => queueJob(update),
instance.scope
// track it in component's effect scope
);
const update = instance.update = () => {
if (effect.dirty) {
effect.run();
}
};
update.id = instance.uid;
toggleRecurse(instance, true);
update();
};

上文的总结已经提到, 建立render-effect的时候做了这几件事:

  • 创建一个effect, 内容是执行render()函数, 获取最新vnode, 再把新老vnode进行patch().

    (effect属于响应式知识, 以前的文章有写过, patch()的作用前文也提到几次了)

  • 组件实例挂上这个effect.

  • 组件实例挂上update()方法, 内容是执行effect(). 这是便于updateComponent调用.

  • 立马执行这个effect(). (最后一行update())

现在来看一下执行render()函数, 获取vnode进行patch()的细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook;
const { el, props } = initialVNode;
const { bm, m, parent } = instance;
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode);
toggleRecurse(instance, false);
if (bm) {
invokeArrayFns(bm);
}
if (!isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode);
}
toggleRecurse(instance, true);
if (el && hydrateNode) {
// ...同构渲染的情况
} else {
const subTree = instance.subTree = renderComponentRoot(instance);
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
namespace
);
initialVNode.el = subTree.el;
}
// ...后续流程, 以后再讨论
instance.isMounted = true;
initialVNode = container = anchor = null;
} else {
let { next, bu, u, parent, vnode } = instance;
let originNext = next;
let vnodeHook;
toggleRecurse(instance, false);
if (next) {
next.el = vnode.el;
updateComponentPreRender(instance, next, optimized);
} else {
next = vnode;
}
if (bu) {
invokeArrayFns(bu);
}
if (vnodeHook = next.props && next.props.onVnodeBeforeUpdate) {
invokeVNodeHook(vnodeHook, parent, next, vnode);
}
toggleRecurse(instance, true);
const nextTree = renderComponentRoot(instance);
const prevTree = instance.subTree;
instance.subTree = nextTree;
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el),
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
namespace
);
next.el = nextTree.el;
if (originNext === null) {
updateHOCHostEl(instance, nextTree.el);
}
}
};

根据instance.isMounted判断是首次挂载还是更新, 其实都调用了同一个函数renderComponentRoot()来根据组件实例获得vnode.

也调用了同样的patch()函数, 新建的时候”老vnode”参数为空而已.

那么更新时是怎么获取老vnode呢, 把vnode挂在instancesubTree属性下.

(组件实例还有另外个属性vnode是创建时就有的, 他表示组件本身, 而组件本身其实是空, 所有组件的vnode属性的el都是空文本, 真正内容在他subTree的children里.)

(所以推论vnode下的component(组件实例)只要不为null, 这个componenttype一定是组件定义, 并且这个vnodeel一定是空文本.)

然后更新组件的时候设个临时变量简简单单操作下就好了.

最后深入看一下renderComponentRoot是如何执行render()函数获得vnode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function renderComponentRoot(instance) {
const {
// ...取了很多组件实例的属性
} = instance;
const prev = setCurrentRenderingInstance(instance);
let result;
let fallthroughAttrs;
try {
if (vnode.shapeFlag & 4) {
const proxyToUse = withProxy || proxy;
const thisProxy = proxyToUse;
result = normalizeVNode(
render.call(
thisProxy,
proxyToUse,
renderCache,
!!(process.env.NODE_ENV !== "production") ? shallowReadonly(props) : props,
setupState,
data,
ctx
)
);
fallthroughAttrs = attrs;
} else {
const render2 = Component;
// ...这里让Component作为render函数, 我猜测是函数组件的case, 不是本文探究范围
}
} catch (err) {
blockStack.length = 0;
handleError(err, instance, 1);
result = createVNode(Comment);
}
// ...有一段比较长的逻辑看起来不是主线, 没有贴上来
setCurrentRenderingInstance(prev);
return result;
}

可以看到在执行render()函数前后也设置了CurrentRenderingInstance, 来使上面提到的provide/inject系列的api生效. 但render()函数里调用这些api, 应该是setup()返回的函数, sfc不会出现这个情况.

然后用一些参数执行了render()函数. proxy是用来提示错误设置的, setupState就是在setup阶段准备好的render执行环境.

最后用normalizeVNode()包了一下, 交给调用方去patch()了.

provide

从上文inject()的分析知道了三种取值, 在了解了组件实例后, 再配合组件setup中的provide看就能得出最后结论了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function provide(key, value) {
if (!currentInstance) {
if (!!(process.env.NODE_ENV !== "production")) {
warn$1(`provide() can only be used inside setup().`);
}
} else {
let provides = currentInstance.provides;
const parentProvides = currentInstance.parent && currentInstance.parent.provides;
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
provides[key] = value;
}
}

从刚才createComponentInstance()我们可以知道, 创建组件实例的时候, provides就是取parent.provides地址的.

所以在一个setup里调用的第一次provide(), 会走到if里, 把当前组件实例的provides修改成继承parent.provides的对象.

这个操作在createComponentInstance()provides属性里是有过的, 目的就是”改变自己不影响父级, 但能取到父级的值”.

这样保证了inject()只能获取到自己祖先级的provides.

我们分析下根节点, 其实也是如此的: inject()取值是instance.vnode.appContext.provides, 而自己组件实例上的provides值是Object.create(vnode.appContext.provides), 此时provide()取到的provides不是当前环境inject()的取值, 所以同一个setup()中是取不到当前环境provide()的值的.

总结

2个方面的总结.

第一是provide/inject的.

  • provide/inject是有传递方向的. 由app.provide, 根组件实例, 向更深的组件实例传递.
  • 同级组件实例的setup()中, 自己取不到自己provide()的值.
  • 如果位于不同2个大分支的组件实例, 是可以provide同一个key不同值的. (key是同一个Symbol也如此)

另外个总结是为了看组件实例, 对 vue3 有了一些深一些的认识.

vue3 的composition api为了代码的复用, 使用了这个useXXX的形式, 其实就是让代码在不同地方都可以取到变量, 而不需要在组件内部.

组件实例还是在的, 为了useXXX能准确的指到期望的实例, 就有了currentXXX的概念.

而设置currentXXX是 vue 内部流程进行的, 异步操作要注意回调执行的时候是不是已经脱离环境, 即使看起来代码是写在组件的setup()中的. 具体解决方案主要靠避免, 或者是调用一些 api 里预留的参数, 用getCurrentInstance()把实例传进去.