前言#
前面由于篇幅的问题,processComponent这块的代码没有分析
坏蛋Dan:vue runtime源码分析学习——day7:patch打补丁part2:根据不同类型进行patch处理
今天我们来分析下
processComponent#
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}代码很好理解。如果是keep-alive包裹下的组件,交给keep-alive处理。
如果不是,判断此时新的节点是否为空,为空执行mountComponent,不为空则调用updateComponent。
- mountComponent:放到下面去分析
- updateComponent:同上
mountComponent#
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
if (__DEV__ && instance.type.__hmrId) {
registerHMR(instance)
}
if (__DEV__) {
pushWarningContext(initialVNode)
startMeasure(instance, `mount`)
}
// inject renderer internals for keepAlive
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
// setup() is async. This component relies on async logic to be resolved
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
return
}
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}老规矩,兼容性的就不分析了。
- createComponentInstance:就是创建一个组件实例,由于篇幅的问题,这里就不贴出来代码了。
__hmrId来自于vue-loader,之前说过了就不多说了。
如果是热更新阶段,就先注册组件实例到热更新的集合中。
export function registerHMR(instance: ComponentInternalInstance) {
const id = instance.type.__hmrId!
let record = map.get(id)
if (!record) {
createRecord(id, instance.type as HMRComponent)
record = map.get(id)!
}
record.instances.add(instance)
}
function createRecord(id: string, initialDef: HMRComponent): boolean {
if (map.has(id)) {
return false
}
map.set(id, {
initialDef: normalizeClassComponent(initialDef),
instances: new Set()
})
return true
}
function normalizeClassComponent(component: HMRComponent): ComponentOptions {
return isClassComponent(component) ? component.__vccOpts : component
}- map:它就是热更新组件存放的集合, 它的类型是<__hmrId, { componentOptions , instances }>。
- createRecord:创建一个map里的项。
就是判断你这个组件是否已经注册过了,没注册过的先注册,然后把最新的组件加到instances里。这里为啥是instances呢?因为每个引用了这个组件的父组件都会创建一个这个组件对应的实例节点。
回到我们的mountComponent里
- pushWarningContext:这个方法我们就不看代码了,简单地说就是组件需要放到堆栈上下文中,这样到时候吐出warn的时候才会准确。
- startMeasure:这个就不说了,也是和我们分析的无关的。
- isKeepAlive:用于判断是否是keep-alive包裹下的组件,如果是,这个组件会带有__isKeepAlive标志位。这里就不分析了,到时候单独分析。
- setupComponent(instance) 这个放到下面分析。
- asyncDep:用于表示这个组件是否是[async component](https://zhuanlan.zhihu.com/p/610958287/[Async Components | Vue.js (vuejs.org)](https://vuejs.org/guide/components/async.html#async-components)),一般和suspense搭配。同样,suspense相关的这里也不分析。
- processCommentNode:创建一个注释节点占位。
- setupRenderEffect:我们来看下代码
setupRenderEffect#
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeBeforeMount)
) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeMount')
}
toggleRecurse(instance, true)
if (el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount.
const hydrateSubTree = () => {
if (__DEV__) {
startMeasure(instance, `render`)
}
instance.subTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
if (__DEV__) {
startMeasure(instance, `hydrate`)
}
hydrateNode!(
el as Node,
instance.subTree,
instance,
parentSuspense,
null
)
if (__DEV__) {
endMeasure(instance, `hydrate`)
}
}
if (isAsyncWrapperVNode) {
;(initialVNode.type as ComponentOptions).__asyncLoader!().then(
// note: we are moving the render call into an async callback,
// which means it won't track dependencies - but it's ok because
// a server-rendered async wrapper is already in resolved state
// and it will never need to change.
() => !instance.isUnmounted && hydrateSubTree()
)
} else {
hydrateSubTree()
}
} else {
if (__DEV__) {
startMeasure(instance, `render`)
}
const subTree = (instance.subTree = renderComponentRoot(instance))
if (__DEV__) {
endMeasure(instance, `render`)
}
if (__DEV__) {
startMeasure(instance, `patch`)
}
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if (
!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeMounted)
) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
parentSuspense
)
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
if (
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
(parent &&
isAsyncWrapper(parent.vnode) &&
parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
) {
instance.a && queuePostRenderEffect(instance.a, parentSuspense)
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:activated'),
parentSuspense
)
}
}
instance.isMounted = true
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
if (__DEV__) {
pushWarningContext(next || instance.vnode)
}
// Disallow component effect recursion during pre-lifecycle hooks.
toggleRecurse(instance, false)
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
// onVnodeBeforeUpdate
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeUpdate')
}
toggleRecurse(instance, true)
// render
if (__DEV__) {
startMeasure(instance, `render`)
}
const nextTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
const prevTree = instance.subTree
instance.subTree = nextTree
if (__DEV__) {
startMeasure(instance, `patch`)
}
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,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
next.el = nextTree.el
if (originNext === null) {
// self-triggered update. In case of HOC, update parent component
// vnode el. HOC is indicated by parent instance's subTree pointing
// to child component's vnode
updateHOCHostEl(instance, nextTree.el)
}
// updated hook
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
// onVnodeUpdated
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:updated'),
parentSuspense
)
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentUpdated(instance)
}
if (__DEV__) {
popWarningContext()
}
}
}
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)
if (__DEV__) {
effect.onTrack = instance.rtc
? e => invokeArrayFns(instance.rtc!, e)
: void 0
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
update.ownerInstance = instance
}
update()
}queueJob:这个方法我们放到下面去分析,其实我们之前的文章中unmount的方法里就有说到过这一点,不过我们说的是生命周期的cbs,那些的执行时间是在我们的update的任务之后的。- 然后我们再来看下这个
new ReactiveEffect做了什么。这个也是放到下面去分析。
然后我们来看下这个componentUpdateFn方法做了什么。
我们分析ReactiveEffect的run的时候,执行的fn()实际上就是这个componentUpdateFn方法。
上来先是判断是否mount了。
如果还没有mount,那么表示这个组件处于初始化状态。
initialVNode是我们的n2。isAsyncWrapper方法我们就不看代码了,之前都看过了,也没啥好说的,就是判断这个组件是否是一个异步组件。toggleRecurse:用于控制是否支持递归执行effect
function toggleRecurse(
{ effect, update }: ComponentInternalInstance,
allowed: boolean
) {
effect.allowRecurse = update.allowRecurse = allowed
}bm是beforeMount生命周期的缩写invokeArrayFns:这个方法我们就不看代码了,因为它只是在执行我们注册到beforeMount里的回调函数,注意这里是array,因为composition API允许我们传入多个回调。props.onVnodeBeforeMount:这个就不都说了,我们前面说过很多次的@vue:xxx,不过需要注意,组件初始化的时候是先触发子组件的生命周期,之后才是触发@vue:xxx监听的生命周期回调,而注销的时候则反过来。__COMPAT__兼容性代码老规矩跳过。startMeasure等用于跟踪代码的方法我们就不多说了hydrateNode这个应该是和ssr相关的,和ssr相关的我们都跳过。renderComponentRoot:这个方法我们放到下面去分析,看名字可能知道是用来渲染组件自身节点的。m:自然就是onMounted生命周期的简写。queuePostRenderEffect:我们在分析queueJob以及之前unmount的时候都有说到,就是用来存放生命周期等方法的任务调度cbs。它们会在effect调度完之后才会执行。这里将onMounted里注册的回调都放到cbs里是因为这个时候刚渲染完毕,为了确保此时拿到的dom都是对的,需要放到调度里,另外也是为了兼容suspense的场景。props.onVnodeMounted:不多说。keep-alive相关的我们先跳过,老规矩到时候单独分析。devtoolsComponentAdded:这个是vue-devtools相关的,简单地说就是将当前的组件注册到devtools里,这样我们的面板中才能跟踪到它。pushWarningContext:这个方法就不看代码了,就是和warn相关的updateComponentPreRender:这个方法我们简单的看下代码
const updateComponentPreRender = (
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean
) => {
nextVNode.component = instance
const prevProps = instance.vnode.props
instance.vnode = nextVNode
instance.next = null
updateProps(instance, nextVNode.props, prevProps, optimized)
updateSlots(instance, nextVNode.children, optimized)
pauseTracking()
// props update may have triggered pre-flush watchers.
// flush them before the render update.
flushPreFlushCbs()
resetTracking()
}
export function flushPreFlushCbs(
seen?: CountMap,
// if currently flushing, skip the current job itself
i = isFlushing ? flushIndex + 1 : 0
) {
if (__DEV__) {
seen = seen || new Map()
}
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.pre) {
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
queue.splice(i, 1)
i--
cb()
}
}
}这个方法我们简单的看下代码
其它没啥好说的,我们来看下flushPreFlushCbs。这个方法做的事情也很简单,就是清洗pre字段是true的任务,因为props的更新可能导致有新的任务进入到队列里面。
注意这里是props先更新,我们之前initProps也是比renderComponentRoot和patch的步骤快的,因为props来自父组件,所以更新的时候为了确保顺序的正常,父组件的props一定得在子组件的patch之前更新。
所以更新的时候自然需要比组件的patch快。
bu:beforeUpdateprops.onVnodeBeforeUpdate:不多说。updateHOCHostEl:HOC(Hight order Component)也就是高阶组件,这玩意儿相信大家学react的时候应该都接触过。简单地说就和高阶函数一样,一个函数返回一个函数就叫高阶函数,比如我们最常用的debounce等。那么高阶组件同理,但是这个时候container定位就不对了,不应该指向包裹的那个组件。所以这里特殊处理下。
export function updateHOCHostEl(
{ vnode, parent }: ComponentInternalInstance,
el: typeof vnode.el // HostNode
) {
while (parent && parent.subTree === vnode) {
;(vnode = parent.vnode).el = el
parent = parent.parent
}
}devtoolsComponentUpdated:这个方法就不多说了,自然就是更新对应devtools里的数据。
那么简单总结下这个componentUpdateFn方法做了什么。
这里分为更新和初始化两种场景
我们先说下初始化也就是还没有mount的场景
- 禁止递归任务
- 触发
beforeMount里的回调 - 触发来自父组件通过
@vue:onBeforeMount监听的回调 - 放开递归任务限制
- 调用
renderComponentRoot方法渲染把我们的render function转换为vnode。赋值给subTree - 调用
patch处理这个subTree也就是vnode。 - 将组件自身的
vnode的el指向subTree的el - 触发
onMounted里的回调,注意这里和下面这两个都是通过调度任务的方式来执行,因为需要确保此时dom是真实存在的。 - 触发来自父组件通过
@vue:onMounted监听的回调。 - 将组件注册到
devtools里。 - 将
n2等赋值为null解引用,为了防止存在引用导致无法被垃圾回收致使内存泄漏。
然后我们再来说下updae的场景
- 一样是限制递归任务
- 然后执行
updateComponentPreRender - 触发
beforeUpdate里的回调 - 触发通过
@vue:onBeforeUpdate监听生命周期的回调 - 取消递归任务的限制
- 调用
renderComponentRoot方法重新获取renderVnodeTree - 然后拿到组件之前的
subTree - 调用
patch比对这两个vnodeTree,然后渲染 - 将
next的el指向最新的tree的el - 如果有高阶组件,特殊处理
host node,也就是挂载的节点。 - 触发
onUpdated里的回调 - 触发通过
@vue:onUpdated监听的生命周期的回调。注意它俩也都是放到任务调度里的。 - 调用
devtoolsComponentsUpdated更新devtools里对应组件的数据。
那么这个setupRenderEffect方法我们就分析完了,我们来简单的总结下这个方法做了什么。
- 创建一个
ReactiveEffect实例,这个实例是组件的依赖收集器和更新管理器,具体分析可以看下面。 - 将组件的
update对象执行() => effect.run() - 触发
update方法。
而触发了update方法之后,就会去执行effect的run方法,这个run方法中会更新依赖deps的状态,然后触发componentUpdateFn方法,这个方法将组件的render function转换成vnode,也就是subTree,然后调用patch方法处理这个subTree,如果是update阶段,那么还有一个newTree来diff。
这里有一点需要注意,组件的初始化就是在这个方法里最后的这个update()触发的。
那么这个mountComponent放法我们就分析完了。
这个方法就如它的名字描述的一样,创建组件,然后渲染。
而我们编译的产物就是在这个阶段中转换成我们runtime熟知的东西,比如vnode等。
先是调用createComponentInstance方法创建组件实例instance,注意这个instance现在只是一个空壳,后面的一系列操作就是为了把这个instance填充满,变成一个鲜活的组件。
然后调用setupComponent将我们编译阶段的setup函数执行了,创建render context和setup context。这个阶段中,如果遇到没有编译的比如inline-template,那么就会调用编译器compiler-dom将template转换成render function。
另外这里面还做了将vnode.props接到instance.attrs里面,到时候通过render function创建的vnode接入这个instance.attrs作为props。
然后调用setupRenderEffect渲染我们的组件。
这个setupRenderEffect上面分析过了,这里就不多说了。
updteComponent#
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// async & still pending - just update props and slots
// since the component's reactive effect for render isn't set-up yet
if (__DEV__) {
pushWarningContext(n2)
}
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect.
instance.update()
}
} else {
// no update needed. just copy over properties
n2.el = n1.el
instance.vnode = n2
}
}在开始分析这个方法之前,我们有一个疑惑需要解答:前面分析setupRenderEffect方法的updateComponentFn方法的时候,它里面不仅有mount的逻辑,还有update的逻辑。但是这里又有updateComponent方法,这两者的作用分别是什么呢?
实际上updateComponent最终执行的逻辑就是触发instace.update方法,而这个方法我们前面分析过了,是() => effect.run(),而run触发updateComponentFn引起更新。
然后我们来看下这个shouldUpdateComponent方法做了什么
export function shouldUpdateComponent(
prevVNode: VNode,
nextVNode: VNode,
optimized?: boolean
): boolean {
const { props: prevProps, children: prevChildren, component } = prevVNode
const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
const emits = component!.emitsOptions
// Parent component's render function was hot-updated. Since this may have
// caused the child component's slots content to have changed, we need to
// force the child to update as well.
if (__DEV__ && (prevChildren || nextChildren) && isHmrUpdating) {
return true
}
// force child update for runtime directive or transition on component vnode.
if (nextVNode.dirs || nextVNode.transition) {
return true
}
if (optimized && patchFlag >= 0) {
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
// slot content that references values that might have changed,
// e.g. in a v-for
return true
}
if (patchFlag & PatchFlags.FULL_PROPS) {
if (!prevProps) {
return !!nextProps
}
// presence of this flag indicates props are always non-null
return hasPropsChanged(prevProps, nextProps!, emits)
} else if (patchFlag & PatchFlags.PROPS) {
const dynamicProps = nextVNode.dynamicProps!
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
if (
nextProps![key] !== prevProps![key] &&
!isEmitListener(emits, key)
) {
return true
}
}
}
} else {
// this path is only taken by manually written render functions
// so presence of any children leads to a forced update
if (prevChildren || nextChildren) {
if (!nextChildren || !(nextChildren as any).$stable) {
return true
}
}
if (prevProps === nextProps) {
return false
}
if (!prevProps) {
return !!nextProps
}
if (!nextProps) {
return true
}
return hasPropsChanged(prevProps, nextProps, emits)
}
return false
}这个方法看名字就知道是在比对两个组件之间的不同。
- 如果是热更新阶段,直接`return true。
- 如果有指令或者
transition发生了变化,直接return true。 - 如果涉及到动态插槽,比如
v-for里面的item引用了局部变量或者插槽名字是动态的,也return true。 - 如果
vnode的patchFlasg是FULL_PROPS,那么就给所有props做前后对比.hasPropsChanged方法我们就不看了。 - 而如果只是普通的
PORPS的patch类型,那么就判断它里面是否有动态指令发生变化,这个动态指令和动态阶段一个意思,都是一个block里遇到了就被track了的节点。 - 如果不能优化或者没有被
track的,那么需要full diff,也就是全部都diff,如果有任何的不同就return true。
老规矩suspense相关的部分先不分析。
然后也没啥好说的了。。。就是将component的instance的next指向新的vnode,然后触发instance.update去更新component自身。
这里还有个invalidateJob方法,这个方法看名字就知道是在校验这个job是否是正常的,我们来看下里面做了什么。
export function invalidateJob(job: SchedulerJob) {
const i = queue.indexOf(job)
if (i > flushIndex) {
queue.splice(i, 1)
}
}如果组件自身的job已经存在与调度队列里了,这个时候把它移除,避免重复触发更新。
setupComponent#
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}这个方法的存在主要是用来处理vue3中setup写法的,因为我们的组件在编译阶段的最终产物是一个对象,这个对象有一个setup方法和render方法,还有一些属性,比如props、expose等。
那么就缺少一个将这个对象转换成正常格式的方法,这个方法的功能主要就是处理这个问题。
isStatefulComponent:这个方法我们就不看代码了,就是判断这个shapeFlag是否是STATEFUL_COMPONENT类型。initProps:因为篇幅问题,这里就不分析了,实际上就是在把来自setup的props搭载到组件自身上initSlots:同上。setupStatefulComponent:我们来看下这个方法做了什么。
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
if (__DEV__) {
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config)
}
if (Component.components) {
const names = Object.keys(Component.components)
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config)
}
}
if (Component.directives) {
const names = Object.keys(Component.directives)
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i])
}
}
if (Component.compilerOptions && isRuntimeOnly()) {
warn(
`"compilerOptions" is only supported when using a build of Vue that ` +
`includes the runtime compiler. Since you are using a runtime-only ` +
`build, the options should be passed via your build tool config instead.`
)
}
}
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup()
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
unsetCurrentInstance()
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
if (__DEV__ && !instance.suspense) {
const name = Component.name ?? 'Anonymous'
warn(
`Component <${name}>: setup function returned a promise, but no ` +
`<Suspense> boundary was found in the parent component tree. ` +
`A component with async setup() must be nested in a <Suspense> ` +
`in order to be rendered.`
)
}
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}这里的__DEV__里的我们就不分析了,都是在校验组件的规范,比如命名是否有冲突等。
instance.proxy:这个是我们响应式的核心部分,这里不准备讲。markRaw:这个是用来标记某个属性可以不被observe也就是观察,方式是用Object.defineProperty标记这个属性为ReactiveFlags.SKIP,也就是__v_skip。PublicInstanceProxyHandlers:响应式核心部分,这里不准备讲,里面放的是getter/setter等,访问组件的数据会被这个proxy给劫持,然后触发发布,通知相关数据的订阅者去更新。exposePropsOnRenderContext:将通过defineExpose暴露出来数据给放到context上,但是这个仅在开发模式下存在。setup:就是我们编译阶段最后的那个setup,它还原封不动的放在instance.type里。createSetupContext:这个方法就不看代码了,我们这篇文章的目的是patch中的组件,后面分析组件的时候再单独分析。这里知道会创建一个setup上下文即可。简单的看下代码
export function createSetupContext(
instance: ComponentInternalInstance
): SetupContext {
const expose: SetupContext['expose'] = exposed => {
if (__DEV__ && instance.exposed) {
warn(`expose() should be called only once per setup().`)
}
instance.exposed = exposed || {}
}
let attrs: Data
if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
return Object.freeze({
get attrs() {
return attrs || (attrs = createAttrsProxy(instance))
},
get slots() {
return shallowReadonly(instance.slots)
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)
},
expose
})
} else {
return {
get attrs() {
return attrs || (attrs = createAttrsProxy(instance))
},
slots: instance.slots,
emit: instance.emit,
expose
}
}
}setCurrentInstance:将currentInstance指向当前的组件实例parseTracking和resetTracking这两个方法也不看代码了,之前说过了,就是基于栈存储/获取当前是否track的状态。callWithErrorHandling:这个也是,老熟人了。这里就是直接看作是执行了setup函数即可。unsetCurrentInstance:将currentInstance指向null。handleSetupResult:这个方法我们来看下代码
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup returned an inline render function
if (__SSR__ && (instance.type as ComponentOptions).__ssrInlineRender) {
// when the function's name is `ssrRender` (compiled by SFC inline mode),
// set it as ssrRender instead.
instance.ssrRender = setupResult
} else {
instance.render = setupResult as InternalRenderFunction
}
} else if (isObject(setupResult)) {
if (__DEV__ && isVNode(setupResult)) {
warn(
`setup() should not return VNodes directly - ` +
`return a render function instead.`
)
}
// setup returned bindings.
// assuming a render function compiled from template is present.
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
instance.devtoolsRawSetupState = setupResult
}
instance.setupState = proxyRefs(setupResult)
if (__DEV__) {
exposeSetupStateOnRenderContext(instance)
}
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${
setupResult === null ? 'null' : typeof setupResult
}`
)
}
finishComponentSetup(instance, isSSR)
}isFunction(setupResult):这个要提一嘴,在ssr或者直接return一个render function的场景,实际上就是inline mode,编译阶段render function就会被整合到setup函数里面。sfc的生产模式就是这样,我之前在编译阶段汇总的那篇文章中总结了,感兴趣的可以去看下。长得是一大堆h函数嵌套的样子。proxyRefs:这个方法简单的看下代码exposeSetupStateOnRenderContext:这个也不看代码了,就是将setup里的数据绑定到ctx上,这个ctx前面expose的时候也有遇到,实际上就是render function接受的那个ctx。所以这个方法就和它的名字一样,将数据绑定到render function的上下文中。这里加了个__DEV__的控制,因为只有dev阶段需要,prod是直接inline mode的,都在setup里了,自然不需要ctx。finishComponentSetup:我们来看下代码
// dev only
export const isRuntimeOnly = () => !compile
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
if (__COMPAT__) {
convertLegacyRenderFn(instance)
if (__DEV__ && Component.compatConfig) {
validateCompatConfig(Component.compatConfig)
}
}
// template / render function normalization
// could be already set when returned from setup()
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
// is done by server-renderer
if (!isSSR && compile && !Component.render) {
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template ||
resolveMergedOptions(instance).template
if (template) {
if (__DEV__) {
startMeasure(instance, `compile`)
}
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } =
Component
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)
if (__COMPAT__) {
// pass runtime compat config into the compiler
finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
if (Component.compatConfig) {
// @ts-expect-error types are not compatible
extend(finalCompilerOptions.compatConfig, Component.compatConfig)
}
}
Component.render = compile(template, finalCompilerOptions)
if (__DEV__) {
endMeasure(instance, `compile`)
}
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (installWithProxy) {
installWithProxy(instance)
}
}
// support for 2.x options
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
setCurrentInstance(instance)
pauseTracking()
applyOptions(instance)
resetTracking()
unsetCurrentInstance()
}
// warn missing template/render
// the runtime compilation of template in SSR is done by server-render
if (__DEV__ && !Component.render && instance.render === NOOP && !isSSR) {
/* istanbul ignore if */
if (!compile && Component.template) {
warn(
`Component provided template option but ` +
`runtime compilation is not supported in this build of Vue.` +
(__ESM_BUNDLER__
? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
: __ESM_BROWSER__
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */
)
} else {
warn(`Component is missing template or render function.`)
}
}
}如果没有render function,那么说明编译阶段并没有生成render function,注意,这里prod也是会有的,刚我们在前面的代码中有这么一行instance.render = setupResult as InternalRenderFunction。那么这里没有render function,只能说明是runtime-compile。虽然我们不分析SSR,但是这里有一点需要了解,SSR是一定有render function的,也不会是on-the-fly,因为它一定会在服务端处理完毕然后渲染。
这种runtime-compile一般叫做on-the-fly。
compile自然就是我们的compiler-dom,我们在createApp也就是入口那篇文章里有说到过compiler-dom注册的逻辑,这里就不多说了。
let compile: CompileFunction | undefined
let installWithProxy: (i: ComponentInternalInstance) => void
/**
* For runtime-dom to register the compiler.
* Note the exported method uses any to avoid d.ts relying on the compiler types.
*/
export function registerRuntimeCompiler(_compile: any) {
compile = _compile
installWithProxy = i => {
if (i.render!._rc) {
i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
}
}
}既然知道这里是在编译,那我们简单的了解下即可。
resolveMergedOptions:代码就不看了,就是在合并一些属性。
这里最重要的一段就是Component.render = compile(template, finalCompilerOptions)。将runtime-compile得到的render function赋值给Component.render以及instance.render。这个Component是instance.type。
最后的applyOptions方法就是一些warn和兼容vue2.x的东西,比如执行beforeCreate和created两个生命周期hook以及一些属性的初始化比如createWatcher等。总之vue2.x里的初始化就是在这个方法里执行的,以后有机会我们再回来分析,或者感兴趣的大佬可自行了解。
ok这一大块就说完了,简单的说就是在将我们编译的产物同步到instance里,毕竟之前我们并不能直接使用到这里面的东西。然后如果是需要runtime-compile处理的inline-template,那么调用compiler-dom处理拿到render function之后在赋值给instance。
里面还有vue2.x初始化的逻辑,但是我们这里就不分析了。
queueJob#
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}如果当前任务队列中没有这个任务,那么就将这个任务放到这个任务队列中。
然后调用queueFlush方法通知清空任务队列。
这里有几个点需要注意:
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex:这里往Array,includes传了第二个参数,表示从哪里开始找。而任务是allowRecurse也就是允许递归的,并且此时是正在“清空”任务队列,那么这个index就应该是清空队列中最后一个flushIndex + 1开始,否则就从flushIndex开始找。我们暂时还不清楚这个flushIndex的具体逻辑,后面会接触到。queue.splice(findInsertionIndex(job.id), 0, job):如果job自身有id,那么找到最接近这个id的job的index然后返回,这个findInsertionIndex代码我们就不看了,就是一个二分查询。拿到这个index之后把我们的job插入到里面。这样就能保证顺序了。
然后我们来看下queueFlush方法
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}如果此时isFlushing的标志位和isFlushPending的标志位都为false,那么表明现在并没有在清空任务队列。
那么从这个任务开始,开始准备清空任务队列,将isFlushPending置为true。
isFlushing:表示当前正在清除队列中。isFlushPending:表示当前正在准备清除队列中。
resolvePromise就是Promise.resolve(),这么做是为了异步效果,确保执行的顺序正确。
我们来看下flushJobs方法
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
queue.sort(comparator)
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}异步结束,执行flushJobs方法,此时将isFlushPending标志位置为false,将isFlushing置为true,表示已经准备好,可以开始清除任务了。
不过在开始清除之前,还需要对整个队列进行排序。
为了确保以下两点:
- 组件的更新得从父组件到子组件,因为父组件的创建是早于子组件的,所以它的优先级理应在子组件之前。
- 如果组件正在更新,但是它的子组件却在注销中,这个时候子组件的更新可以被跳过。
我们来看下comparator方法
const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
const diff = getId(a) - getId(b)
if (diff === 0) {
if (a.pre && !b.pre) return -1
if (b.pre && !a.pre) return 1
}
return diff
}这里就是在给任务队列进行排序处理,根据任务的id来排序。
如果id为null,那么直接放到队列的最后面。
如果由id,那么就根据id从小到大来排序。
这里面还有涉及到pre需要优先处理的任务。如果两边都有pre,那么按id来排序,而如果只有一边有pre,那么优先级以pre的先。
然后我们回到flushJobs方法里面。
check:开发模式下会确认是否存在递归的任务,确认的方法是通过一个seen的map,它的key是任务自身,而key表示这个任务在这次调度中存在的次数。它会被传入到下一次任务调度中,如果此时的seen里还是能找到相同的任务并且记录的次数达到了上限100个,那么就说明这个任务是一个递归任务。
这种递归自己的任务大多发生在watch或者computed里面触发的更新同时又修改自身依赖的数据导致重复更新的问题。
接着开始清理任务,如果任务的active还是true,表示任务自身还不能被清理,那么就需要跳过,不过一般不会有这个问题,除非是递归中。
循环结束后清空队列,将标志位都初始化处理。
然后调用flushPostFlushCbs,这个我们之前的文章中有说到过,这个主要是vue内部的任务以及一些生命周期任务的jobs比如beforeUnmount里注册的回调事件(它里面会对重复的任务进行过滤,所以一直保留着最新的任务),他们是在我们的watch/computed等任务执行之后才会执行。当然,这个过程可能会导致有新的job被放入到任务调度队列中,所以需要结束前再check一次是否清空了,如果没有清空,那么就重新触发flushJobs,此时传入的seen是用于确认是否存在递归任务的。
ReactiveEffect#
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}它new的时候接收了三个参数: 第一个是componentUpdateFn,第二个则是我们的调度任务注册和触发,第三个则是instance.scope,这第三个参数是用来跟踪组件的作用域的。
对于第二个参数,这里之后都把它叫做调度器了(scheduler)。
而这个ReactiveEffect的类在初始化的过程中执行了recordEffectScope的方法,我们来看下这个方法中做了什么。
export function recordEffectScope(
effect: ReactiveEffect,
scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
scope.effects.push(effect)
}
}这个方法很简单,实际上就是在将这个刚new的ReactiveEffect实例给存储到组件自身的scope.effects里。这样这个effect就和组件实例关联起来了。我们的组件就可以跟踪这个任务了。
注意这里的scope.active字段,它应该是用来控制组件里面effects是否需要触发的字段。
如果为false,有可能是这个任务处于keep-alive的deactivated状态,不需要触发更新。任务也自然不能被放到effects里跟踪。
它还有两个方法
run
如果当前effect处于active为false时,直接执行``fn也就是我们传入的componentUpdateFn`。
activeEffect表示当前正在更新的effect,然后找这个effect的parent,因为这个effect可能不是当前组件触发的,它可能来自于父组件,也可能来自爷组件,还有可能是这个组件自己递归调用自己。如果是是自己调用自己触发的更新会直接return。如果不是则继续往下,将parent标记为activeEffect。
然后将自己提供给activeEffect,这样下一个effect就能知道parent了。接着就是一个位运算,trackOpBit根据左移effectTrackDepth位,trackOpBit表示当前跟踪的effect目前有几位,最多有30位用于位标记递归深度。
effectTrackDepth则表示当前的递归调用深度。如果是小于等于最大深度30位,那么调用initDepMarkers方法,否则调用cleanupEffect方法。最后调用fn并返回它的返回值。
接着执行finally的逻辑,如果深度没有达到最大值,那么这个时候调用finalizeDepMarkers。执行完之后深度减一,将当前的activeEffect赋值回parent,将自己的parent赋值为null,即当前effect已经更新完毕,状态初始化处理。
最后如果deferStop字段是true,则调用stop方法。
initDepMarkers:
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // set was tracked
}
}
}这个方法看名字就知道是在初始化位标记,按位或等(|=表示按位或等运算)更新dep的位标记,这样保证了dep的深度正常。
cleanupEffect:
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}看名字就知道是用来清空effect的。也没啥好说的,不过需要记住这个delete方法,它是Map的delete方法,我们传入的key是effect自身,所以这里应该是在解除依赖和当前effect的关联。先mark下,我们还没接触到dep那一块的内容。
finalizeDepMarkers:
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
deps[ptr++] = dep
}
// clear bits
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}这个方法就是在去掉一些已经失去跟踪的dep。同时清空了保留的dep的位标记, &=是位运算的按位与等,只有前后两个数的对应位的值都为1才是1,否则都为0。~是按位取反的意思,也就是1变0,0变1。
执行完这个方法之后,当前effect就是最新的状态了。
然后我们再来看下stop方法。
**stop**如果当前activeEffect指向自己,那么表示自己正在被执行中,此时不应该发生状态的变化,需要推迟,推迟到run完之后再stop。
或者当前active为true,那么将自己的effect和dep解绑。
这里我突然想起之前分析unmount的时候遇到的unmountComponent方法,这个方法中有调用过stop这个方法和将active置为false的时候。
// stop effects in component scope
scope.stop()
// update may be null if a component is unmounted before its async
// setup has resolved.
if (update) {
// so that scheduler will no longer invoke it
update.active = false
unmount(subTree, instance, parentSuspense, doRemove)
}这里就是在清空自己的effect,然后将active置为false之后effect自己就不会再被scheduler触发了。
那么这个ReactiveEffect就分析完了,虽然有些点我们有些疑惑,比如位标记跟踪和位标记深度等,和递归相关的。我们先mark下。
另外,这里多次出现了dep这个字眼,它非常重要,他就是我们所说的依赖,我们后面分析响应式的时候会说到它。
那么简单的总结下这个ReactiveEffect的类做了什么。
这个类实际上就是组件的依赖收集器和管理器,虽然我们还不知道它什么时候会被放到scheduler里面,但是我们能确定是调度器将它触发,而它被触发之后它会去更新它收集到的依赖来确保依赖都是最新的,并执行更新方法,也就是前面我们说到的updateComponentFn
依赖都被收集到dep字段里。
它通过active字段控制是否执行更新逻辑。
renderComponentRoot#
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx,
inheritAttrs
} = instance
let result
let fallthroughAttrs
const prev = setCurrentRenderingInstance(instance)
if (__DEV__) {
accessedAttrs = false
}
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
} else {
// functional
const render = Component as FunctionalComponent
// in dev, mark attrs accessed if optional props (attrs === props)
if (__DEV__ && attrs === props) {
markAttrsAccessed()
}
result = normalizeVNode(
render.length > 1
? render(
props,
__DEV__
? {
get attrs() {
markAttrsAccessed()
return attrs
},
slots,
emit
}
: { attrs, slots, emit }
)
: render(props, null as any /* we know it doesn't need it */)
)
fallthroughAttrs = Component.props
? attrs
: getFunctionalFallthrough(attrs)
}
} catch (err) {
blockStack.length = 0
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
result = createVNode(Comment)
}
// attr merging
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result
let setRoot: SetRootFn = undefined
if (
__DEV__ &&
result.patchFlag > 0 &&
result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
) {
;[root, setRoot] = getChildRoot(result)
}
if (fallthroughAttrs && inheritAttrs !== false) {
const keys = Object.keys(fallthroughAttrs)
const { shapeFlag } = root
if (keys.length) {
if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) {
if (propsOptions && keys.some(isModelListener)) {
// If a v-model listener (onUpdate:xxx) has a corresponding declared
// prop, it indicates this component expects to handle v-model and
// it should not fallthrough.
// related: #1543, #1643, #1989
fallthroughAttrs = filterModelListeners(
fallthroughAttrs,
propsOptions
)
}
root = cloneVNode(root, fallthroughAttrs)
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
const allAttrs = Object.keys(attrs)
const eventAttrs: string[] = []
const extraAttrs: string[] = []
for (let i = 0, l = allAttrs.length; i < l; i++) {
const key = allAttrs[i]
if (isOn(key)) {
// ignore v-model handlers when they fail to fallthrough
if (!isModelListener(key)) {
// remove `on`, lowercase first letter to reflect event casing
// accurately
eventAttrs.push(key[2].toLowerCase() + key.slice(3))
}
} else {
extraAttrs.push(key)
}
}
if (extraAttrs.length) {
warn(
`Extraneous non-props attributes (` +
`${extraAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes.`
)
}
if (eventAttrs.length) {
warn(
`Extraneous non-emits event listeners (` +
`${eventAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes. ` +
`If the listener is intended to be a component custom event listener only, ` +
`declare it using the "emits" option.`
)
}
}
}
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance) &&
vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&
root.shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)
) {
const { class: cls, style } = vnode.props || {}
if (cls || style) {
if (__DEV__ && inheritAttrs === false) {
warnDeprecation(
DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE,
instance,
getComponentName(instance.type)
)
}
root = cloneVNode(root, {
class: cls,
style: style
})
}
}
// inherit directives
if (vnode.dirs) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Runtime directive used on component with non-element root node. ` +
`The directives will not function as intended.`
)
}
// clone before mutating since the root may be a hoisted vnode
root = cloneVNode(root)
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
}
// inherit transition data
if (vnode.transition) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Component inside <Transition> renders non-element root node ` +
`that cannot be animated.`
)
}
root.transition = vnode.transition
}
if (__DEV__ && setRoot) {
setRoot(root)
} else {
result = root
}
setCurrentRenderingInstance(prev)
return result
}我们来看下这个setCurrentRenderingInstance
export function setCurrentRenderingInstance(
instance: ComponentInternalInstance | null
): ComponentInternalInstance | null {
const prev = currentRenderingInstance
currentRenderingInstance = instance
currentScopeId = (instance && instance.type.__scopeId) || null
// v2 pre-compiled components uses _scopeId instead of __scopeId
if (__COMPAT__ && !currentScopeId) {
currentScopeId = (instance && (instance.type as any)._scopeId) || null
}
return prev
}这个方法做的事情很简单,
先是将当前currentRenderingInstance替换成自己,然后返回之前的currentRenderingInstance指向的组件。
这个currentRenderingInstance字段看名字就知道是用来存储当前正在渲染中的实例对象,全局上下文环境中唯一的。
normalizeVNode:这个方法我们分析vnode那一章的时候说过了,这里就不多说了。render!.call:这个render就是我们组件的render function,在这里被转换成vnode。前面分析过这里render的来源,这里就不多说了。functionnal template的我这里就跳过了
如果在转换的过程中出了错则使用一个Comment节点来代替。
生成vnode之后开始处理节点,开发阶段是不会过滤注释节点的,所以可能存在组件的根下面紧跟一个注释节点这种,这个时候会创建一个fragment包裹。
所以getChildRoot方法就是用来处理这个问题的。
fallthroughAttrs:是instance上的attr


这个attrs来自我们没有分析的initProps这个方法,实际上就是来自父组件的props 。
然后过滤掉v-model指令的数据之后将其余的props传入给root也就是前面的vnode。
这里有一点需要注意,最开始这个attrs是来自instance.vnode.props,然后在setupComponent阶段给放到instance.attrs,然后来到这个renderComponentRoot的时候再将它传递给root,这个root是通过render function生成的。绕着一圈就是为了将来自父组件的props传到我们的组件中。
instance.vnode并不是组件本身,后者说不完全是组件本身,instance.type才是我们编译结束后的组件内容,render function在它里面。
当然,v-model的要单独处理。
如果是开发者阶段,还需要去除事件的on,编译阶段事件会变成onXxx,这里自然就需要调整回去,全部小写,然后去掉开头的on。
接着就是开始继承父爱,开始继承来自父组件的style / class / dir。
最后setCurrentRenderingInstance方法将currentRenderingInstance重新指回父组件。
然后返回vnode。
简单的总结下renderComponentRoot这个方法,就是基于render function创建vnode,然后让它继承父组件传入的比如style/class/dir以及最重要的props。
总结#
通过这一章,我们了解到了组件从创建到渲染的过程。
还了解到了它的依赖管理器以及更新逻辑,还有组件的props来源和如何传递的。
另外我们这里还将我们编译阶段的产物关联起来以及知道了runtime-compile处理inline-template的位置等。
编辑于 2023-03-16 12:34・IP 属地广东
