接上次看vue-router
看到了provide
和inject
, 觉得应该比较简单, 打算看一下实现.
过程中发现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 ) { const context = { app : null as any , provides : Object .create (null ), } const app : App = (context.app = { _context : context, mount ( rootContainer : HostElement , isHydrate ?: boolean , namespace ?: boolean | ElementNamespace , ): any { if (!isMounted) { const vnode = app._ceVNode || createVNode (rootComponent, rootProps) vnode.appContext = context }, 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
, 或者是根节点的vnode
的appContext
里获取到.
这个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 , ) { const instance = currentInstance || currentRenderingInstance if (instance || 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) { return provides[key as string ] } else if (arguments .length > 1 ) { } } }
可以看到关键点就在于const provides = ...
的取值, 之后就是把provides
对应的key返回就行.
我们仔细来看provides
的取值优先级:
如果有currentApp
就取currentApp._context.provides
. 而currentApp
这个变量非常明确, 只有runWithContext()
可以调用他.
所以这第一个情况是runWithContext()
+inject()
专属情况.
接下来的情况是有currentInstance
的, 也就是在setup()
里调用的. (currentRenderingInstance
是执行render()
函数的时候设置的, 其实是同一个实例.)
如果是根节点, 就取instance.vnode.appContext.provides
. 也就是app-level provide
设置的值.
如果不是根节点, 就取instance.parent.provides
.
看到这里就需要去了解currentInstance
了, 因为:
上面提到的(2)中, 其实我们只知道vnode.appContext
是上面app-level provide
的值, 但并不知道instance
的vnode
是如何挂上的, 挂的是不是期望的vnode
, 没有连起来.
上面提到的(3)中, instance
的parent
是如何挂上的, parent
的provides
又是什么. (不能因为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对象, 有setup
和render
属性, 其实就对应了sfc的script
和template
.
.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) vnode.appContext = context 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
(n1
和n2
)调用dom操作.
叶子节点vnode
的patch()
分2步:
根据vnode
的类型, 分配给不同函数处理.
根据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 )); setupComponent (instance); if (instance.asyncDep ) { } else { setupRenderEffect ( instance, initialVNode, container, anchor, parentSuspense, namespace, optimized ); } };
可以看到关键的代码分为这三步:
创建组件实例, 并挂到vnode
的component
属性上.
inject()
就是从这个组件实例中获取provides
.
在一些流程中, 组件实例会被设置为currentInstance
.
而要获取实例, 以及相关的变量关系是:
dom => dom._vnode (vnode) => vnode.component (component实例), vnode.type (component定义)
进行组件的setup
.
setup()
作用是为render()
做准备的.
render()
的作用是每次运行会返回最新的vnode
, 把最新的vnode
与老的一起给patch()
, 就可以进行diff
最后操作dom.
setup()
只会执行一次, 而render()
在每次更新组件都会执行.
setup()
的形式有很多种, 最常见的sfc是返回template
的执行环境, 在一些组件里会直接返回render()
函数, 或者是异步组件会返回promise
. (但作用都是为render()
的执行做准备)
创建组件的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 = 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 ); 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, hostParentNode (prevTree.el ), getNextHostNode (prevTree), instance, parentSuspense, namespace ); next.el = nextTree.el ; if (originNext === null ) { updateHOCHostEl (instance, nextTree.el ); } } };
根据instance.isMounted
判断是首次挂载还是更新, 其实都调用了同一个函数renderComponentRoot()
来根据组件实例获得vnode
.
也调用了同样的patch()
函数, 新建的时候”老vnode”参数为空而已.
那么更新时是怎么获取老vnode
呢, 把vnode
挂在instance
的subTree
属性下.
(组件实例还有另外个属性vnode
是创建时就有的, 他表示组件本身, 而组件本身其实是空, 所有组件的vnode属性的el都是空文本, 真正内容在他subTree的children里. )
(所以推论vnode
下的component
(组件实例)只要不为null, 这个component
的type
一定是组件定义, 并且这个vnode
的el
一定是空文本.)
然后更新组件的时候设个临时变量简简单单操作下就好了.
最后深入看一下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 ; } } 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()
把实例传进去.