前言 Vue3 官网中有下面这样一张图,基本展现出了 Vue3 的渲染原理:
本文会从源码角度来简单的看一下 Vue3 的运行全流程,旨在加深对上图的理解。
初始化渲染 从下面这个很简单的使用示例开始:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { createApp, ref } from 'vue' createApp({ template: ` <div class="card"> <button type="button" @click="count++">count is {{ count }}</button> </div> ` , setup() { return { count: ref(0 ), } }, }).mount('#app' )
首先调用了 createApp
方法:
1 2 3 4 5 export const createApp = ((...args ) => { const app = ensureRenderer().createApp(...args) return app })
我们可以看到重点在于 ensureRenderer
1 2 3 4 5 6 7 8 9 const rendererOptions = extend({ patchProp }, nodeOps)function ensureRenderer ( ) { return ( renderer || (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions)) ) }
rendererOptions
是一个对象,渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法。
这么做主要是方便跨平台,比如在其他非浏览器环境,可以替换成对应的节点操作方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { insert: (child, parent, anchor ) => { parent.insertBefore(child, anchor || null ) }, remove: child => { const parent = child.parentNode if (parent) { parent.removeChild(child) } }, }
1 2 3 function createRenderer (options: RendererOptions ) { return baseCreateRenderer(options) }
调用 baseCreateRenderer
, 这个函数简直可以用庞大来形容,patch、mount、diff均在这个方法中实现,回头我们再来细看实现,现在我们只需要关心他最后返回的什么
1 2 3 4 5 6 7 8 9 10 11 function baseCreateRenderer ( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions, ) { return { render, hydrate, createApp: createAppAPI(render, hydrate), } }
我们发现createApp是通过createAppAPI方法调用返回的,下面我们看createAppAPI。
createAppAPI 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 71 72 73 74 75 76 77 78 79 export function createAppAPI <HostElement >( render: RootRenderFunction<HostElement>, hydrate?: RootHydrateFunction, ): CreateAppFunction <HostElement > { return function createApp (rootComponent, rootProps = null ) { if (!isFunction(rootComponent)) { rootComponent = extend({}, rootComponent) } if (rootProps != null && !isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.` ) rootProps = null } const context = createAppContext() const installedPlugins = new WeakSet () let isMounted = false const app: App = (context.app = { _uid: uid++, _component: rootComponent as ConcreteComponent, _props: rootProps, _container: null , _context: context, _instance: null , version, get config() { return context.config }, set config(v) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` , ) } }, use(plugin: Plugin, ...options: any[]) { }, mixin(mixin: ComponentOptions) { }, component(name: string, component?: Component): any { }, directive(name: string, directive?: Directive) { }, mount( rootContainer: HostElement, isHydrate?: boolean, namespace?: boolean | ElementNamespace, ): any { }, unmount() { }, provide(key, value) { } }) return app } }
这个就是最终的 createApp
方法,接受了 rootComponent 和 rootProps 两个参数,我们在应用层面执行 createApp(App) 方法时,会把 App 组件对象作为根组件传递给 rootComponent。这样,createApp 内部就创建了一个 app 对象。
所谓的应用实例app其实就是一个对象,我们传进去的组件选项作为根组件存储在_component属性上,另外还可以看到应用实例提供的一些方法,比如注册插件的use方法,挂载实例的mount方法等。
createAppContext 实现 context其实也是一个普通对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export function createAppContext ( ): AppContext { return { app: null as any, config: { isNativeTag: NO, performance: false , globalProperties: {}, optionMergeStrategies: {}, errorHandler: undefined , warnHandler: undefined , compilerOptions: {}, }, mixins: [], components: {}, directives: {}, provides: Object .create(null ), optionsCache: new WeakMap (), propsCache: new WeakMap (), emitsCache: new WeakMap (), } }
这部分是vue的初次渲染逻辑,首先官方解构了mount方法, 然后又重写了app.mount,并调用normalizeContainer校验挂载元素,临时保存了需要渲染的内容。并对vue2的写法做了兼容处理,挂载元素进行渲染。
在以上整个 app 对象创建过程中,Vue.js 利用闭包和函数柯里化的技巧,很好地实现了参数保留。比如,在执行 app.mount 的时候,并不需要传入渲染器 render,这是因为在执行 createAppAPI 的时候渲染器 render 参数已经被保留下来了。
至此,createApp的流程大概到此结束,下一部分来分析mount挂载部分。
mounted挂载 重写app.mount方法 createApp 函数中,首先取出 app 对象中的 mount 函数,然后通过 app.mount = () => {} 对 mount 函数进行重写:
首先调用 normalizeContainer 函数来获取container节点;
判断该节点是否存在,若不存在,则直接返回;
清空container的innerHTML;
调用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 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 export const createApp = ((...args ) => { const app = ensureRenderer().createApp(...args) const { mount } = app app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { const container = normalizeContainer(containerOrSelector) if (!container) return const component = app._component if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML if (__COMPAT__ && __DEV__) { for (let i = 0 ; i < container.attributes.length; i++) { const attr = container.attributes[i] if (attr.name !== 'v-cloak' && /^(v-|:|@)/ .test(attr.name)) { compatUtils.warnDeprecation( DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null , ) break } } } } container.innerHTML = '' const proxy = mount(container, false , resolveRootNamespace(container)) if (container instanceof Element) { container.removeAttribute('v-cloak' ) container.setAttribute('data-v-app' , '' ) } return proxy } return app })
首先是通过 normalizeContainer 标准化容器(这里可以传字符串选择器或者 DOM 对象,但如果是字符串选择器,就需要把它转成 DOM 对象,作为最终挂载的容器),然后做一个 if 判断,如果组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容;接着在挂载前清空容器内容,最终再调用 app.mount 的方法走标准的组件渲染流程。
在这里,重写的逻辑都是和 Web 平台相关的,所以要放在外部实现。此外,这么做的目的是既能让用户在使用 API 时可以更加灵活,也兼容了 Vue.js 2.x 的写法,比如 app.mount 的第一个参数就同时支持选择器字符串和 DOM 对象两种类型。
从 app.mount 开始,才算真正进入组件渲染流程。
真正的挂载mounted 核心流程:
根据传入的根组件App创建vnode
渲染vnode。
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 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 mount( rootContainer: HostElement, isHydrate?: boolean, namespace?: boolean | ElementNamespace, ) { if (!isMounted) { if (__DEV__ && (rootContainer as any).__vue_app__) { warn( `There is already an app instance mounted on the host container.\n` + ` If you want to mount another app on the same host container,` + ` you need to unmount the previous app by calling \`app.unmount()\` first.` , ) } const vnode = createVNode(rootComponent, rootProps) vnode.appContext = context if (namespace === true ) { namespace = 'svg' } else if (namespace === false ) { namespace = undefined } if (__DEV__) { context.reload = () => { render( cloneVNode(vnode), rootContainer, namespace as ElementNamespace, ) } } if (isHydrate && hydrate) { hydrate(vnode as VNode<Node, Element>, rootContainer as any) } else { render(vnode, rootContainer, namespace) } isMounted = true app._container = rootContainer ;(rootContainer as any).__vue_app__ = app if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { app._instance = vnode.component devtoolsInitApp(app, version) } return getExposeProxy(vnode.component!) || vnode.component!.proxy } else if (__DEV__) { warn( `App has already been mounted.\n` + `If you want to remount the same app, move your app creation logic ` + `into a factory function and create fresh app instances for each ` + `mount - e.g. \`const createMyApp = () => createApp(App)\`` , ) } }
createVNode createVNode方法会根据组件的类型生成一个标志,后续会通过这个标志做一些优化处理。我们传的是一个组件选项,也就是一个普通对象,shapeFlag的值为4。后续我们会重点关注我们的主线组件 vnode 和普通元素 vnode。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const createVNode = _createVNode;function _createVNode (type, props = null, children = null, patchFlag = 0 , dynamicProps = null, isBlockNode = false ) { const shapeFlag = isString(type) ? 1 : isSuspense(type) ? 128 : isTeleport(type) ? 64 : isObject(type) ? 4 : isFunction(type) ? 2 : 0 ; return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ); }
然后调用了createBaseVNode方法:
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 function createBaseVNode ( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps ) | null = null , children : unknown = null , patchFlag = 0, dynamicProps: string[] | null = null, shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT, isBlockNode = false , needFullChildrenNormalization = false , ) { const vnode = { __v_isVNode: true , __v_skip: true , type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null , children, component: null , suspense: null , ssContent: null , ssFallback: null , dirs: null , transition: null , el: null , anchor: null , target: null , targetAnchor: null , staticCount: 0 , shapeFlag, patchFlag, dynamicProps, dynamicChildren: null , appContext: null , ctx: currentRenderingInstance, } as VNode return vnode }
可以看到返回的虚拟DOM也是一个普通对象,我们传进去的组件选项会存储在type属性上。
render 虚拟DOM创建完后就会调用render方法将虚拟DOM渲染为实际的DOM节点,render方法就是在baseCreateRenderer中创建的,通过参数传给createAppAPI的:
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 const render: RootRenderFunction = (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 }
如果要渲染的新VNode不存在,那么从容器元素上获取之前VNode进行卸载,否则调用patch方法进行打补丁,如果是首次渲染,container._vnode不存在,那么直接将新VNode渲染为DOM元素即可,否则会对比新旧VNode,使用diff算法进行打补丁,Vue2中使用的是双端diff算法,Vue3中使用的是快速diff算法。
patch 看看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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 const patch: PatchFn = ( n1, n2, container, anchor = null , parentComponent = null , parentSuspense = null , namespace = undefined , slotScopeIds = null , optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren, ) => { if (n1 === n2) { return } if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true ) n1 = null } if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } const { type, ref, shapeFlag } = n2 switch (type) { case Text: case Comment: case Static: case Fragment: default : if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, ) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, ) } else if (shapeFlag & ShapeFlags.TELEPORT) { } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { } else if (__DEV__) { warn('Invalid VNode type:' , type, `(${typeof type} )` ) } } if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } }
patch这个函数有两个功能,一个是根据 vnode 挂载 DOM,一个是根据新旧 vnode 更新 DOM。
patch的多个参数中,重点关注前三个:
第一个参数 n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次挂载的过程;
第二个参数 n2 表示新的 vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑;
第三个参数 container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面。
switch里面根据VNode的类型不同做的处理也不同,因为我们的例子传的是一个组件选项对象,所以会走processComponent处理分支:
processComponent 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 const processComponent = ( n1: VNode | null , n2: VNode, container: RendererElement, anchor: RendererNode | null , parentComponent: ComponentInternalInstance | null , parentSuspense: SuspenseBoundary | null , namespace: ElementNamespace, 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, namespace, optimized, ) } else { mountComponent( n2, container, anchor, parentComponent, parentSuspense, namespace, optimized, ) } } else { updateComponent(n1, n2, optimized) } }
根据是否存在旧的VNode判断是调用挂载方法还是更新方法,先看mountComponent方法:
mountComponent 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 const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, namespace: ElementNamespace, optimized, ) => { const compatMountInstance = __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component const instance: ComponentInternalInstance = compatMountInstance || (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense, )) if (!(__COMPAT__ && compatMountInstance)) { if (__DEV__) { startMeasure(instance, `init` ) } setupComponent(instance) if (__DEV__) { endMeasure(instance, `init` ) } } if (__FEATURE_SUSPENSE__ && instance.asyncDep) { } else { setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, namespace, optimized, ) } if (__DEV__) { popWarningContext() endMeasure(instance, `mount` ) } }
调用ComponentInternalInstance创建组件的实例
设置组件实例,初始化组件 处理setup的两个参数, 执行setup 生成render函数(所以setup是在所有选项式API钩子之前调用 包括beforeCreate)
调用设置和运行有副作用的渲染函数
下面我们依次来看这三个方法:
1. createComponentInstance 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export function createComponentInstance ( vnode: VNode, parent: ComponentInternalInstance | null, suspense: SuspenseBoundary | null, ) { const type = vnode.type as ConcreteComponent const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext const instance: ComponentInternalInstance = { uid: uid++, vnode, type, parent, appContext, } return instance }
2. setupComponent 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export function setupComponent ( instance: ComponentInternalInstance, 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) : undefined isSSR && setInSSRSetupState(false ) return setupResult }
可以看到setupComponent会初始化props和slots,然后执行setupStatefulComponent,这里主要是执行setup函数,并返回结果
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 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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 function setupStatefulComponent ( instance: ComponentInternalInstance, isSSR: boolean, ) { const Component = instance.type as ComponentOptions instance.accessCache = Object .create(null ) instance.proxy = markRaw(new Proxy (instance.ctx, PublicInstanceProxyHandlers)) if (__DEV__) { exposePropsOnRenderContext(instance) } 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, ErrorCodes.SETUP_FUNCTION, [ __DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext, ], ) resetTracking() reset() if (isPromise(setupResult)) { setupResult.then(unsetCurrentInstance, unsetCurrentInstance) if (isSSR) { return setupResult .then((resolvedResult: unknown ) => { handleSetupResult(instance, resolvedResult, isSSR) }) .catch(e => { handleError(e, instance, ErrorCodes.SETUP_FUNCTION) }) } else if (__FEATURE_SUSPENSE__) { 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) } }
在这个方法里会调用组件选项的setup方法,这个函数中返回的对象会暴露给模板和组件实例,看一下handleSetupResult方法:
handleSetupResult 1 2 3 4 5 6 7 8 function handleSetupResult (instance, setupResult, isSSR ) { if (isFunction(setupResult)) { instance.render = setupResult; } else if (isObject(setupResult)) { instance.setupState = proxyRefs(setupResult); } finishComponentSetup(instance, isSSR); }
如果setup返回的是一个函数,那么这个函数会直接被作为渲染函数。否则如果返回的是一个对象,会使用proxyRefs将这个对象转为Proxy代理的响应式对象。
finishComponentSetup 最后又调用了finishComponentSetup方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function finishComponentSetup (instance, isSSR ) { 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); } }
这个函数主要是判断组件是否存在渲染函数render,如果不存在则判断是否存在template选项,我们传的组件选项显然是没有render属性,而是传的模板template,所以会使用compile方法来将模板编译成渲染函数。
3. setupRenderEffect 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 const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, namespace: ElementNamespace, optimized, ) => { const componentUpdateFn = () => {} const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, NOOP, () => queueJob(update), instance.scope, )) const update: SchedulerJob = (instance.update = ( ) => { if (effect.dirty) { effect.run() } }) update.id = instance.uid 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() }
这一步就涉及到Vue3的响应式原理了,核心就是使用Proxy拦截数据,然后在属性读取时将属性和读取该属性的函数(称为副作用函数)关联起来,然后在更新该属性时取出该属性关联的副作用函数出来执行。
简化后的ReactiveEffect类就是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let activeEffect;class ReactiveEffect { constructor (fn, scheduler = null, scope) { this .fn = fn; } run() { activeEffect = this ; try { return this .fn(); } finally { activeEffect = null } } }
执行它的run方法时会把自身赋值给全局的activeEffect变量,然后执行副作用函数时如果读取了Proxy代理后的对象的某个属性时就会将对象、属性和这个ReactiveEffect示例关联存储起来,如果属性发生改变,会取出关联的ReactiveEffect实例,执行它的run方法,达到自动更新的目的。
componentUpdateFn 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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 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 ) if (bm) { invokeArrayFns(bm) } if (el && hydrateNode) { } else { if (__DEV__) { startMeasure(instance, `render` ) } const subTree = (instance.subTree = renderComponentRoot(instance)) patch( null , subTree, container, anchor, instance, parentSuspense, isSVG ) if (__DEV__) { endMeasure(instance, `patch` ) } initialVNode.el = subTree.el } if (m) { queuePostRenderEffect(m, parentSuspense) } instance.isMounted = true if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsComponentAdded(instance) } initialVNode = container = anchor = null as any } else { let { next, bu, u, parent, vnode } = instance let originNext = next let vnodeHook: VNodeHook | null | undefined if (next) { next.el = vnode.el updateComponentPreRender(instance, next, optimized) } else { next = vnode } const nextTree = renderComponentRoot(instance) const prevTree = instance.subTree instance.subTree = nextTree patch( prevTree, nextTree, hostParentNode(prevTree.el!)!, getNextHostNode(prevTree), instance, parentSuspense, isSVG ) } }
这里的patch操作其实就是调用本章开头的那个patch,可以看到patch其实是一个递归操作,这里patch subtree如果根组件的根元素是组件则会继续执行processComponent,如果是一个element元素则会执行processElement,processElement中会处理children,又会调用patch,如此递归直到整个组件挂载完成。
组件无论是首次挂载,还是更新,做的事情核心是一样的,先调用renderComponentRoot方法生成组件模板的虚拟DOM,然后调用patch方法打补丁。
renderComponentRoot 1 2 3 4 5 6 7 function renderComponentRoot (instance ) { const { type : Component, vnode, proxy, withProxy, props, propsOptions : [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx, inheritAttrs } = instance; let result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx)) return result }
renderComponentRoot核心就是调用组件的渲染函数render方法生成组件模板的虚拟DOM,然后扔给patch方法更新就好了。
updateComponent 看完了mountComponent方法,再来看看updateComponent方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const updateComponent = (n1, n2, optimized ) => { const instance = (n2.component = n1.component); if (shouldUpdateComponent(n1, n2, optimized)) { instance.next = n2; invalidateJob(instance.update) instance.update(); }else { n2.el = n1.el; instance.vnode = n2; } }
先调用shouldUpdateComponent方法判断组件是否需要更新,主要是通过检测和对比组件 vnode 中的 props、chidren、dirs、transiton 等属性,来决定子组件是否需要更新。
如果需要更新,那么会执行instance.update方法,这个方法就是前面setupRenderEffect方法里保存的effect.run方法,所以最终执行的也是componentUpdateFn方法。
updateComponentPreRender 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 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() flushPreFlushCbs(instance) resetTracking() }
processElement 1 2 3 4 5 6 7 8 9 10 11 const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { isSVG = isSVG || n2.type === 'svg' if (n1 == null ) { mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) } else { patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } }
该函数的逻辑很简单,如果 n1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。
我们接着来看挂载元素的 mountElement 函数的实现:
mountElement 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 mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { let el const { type, props, shapeFlag } = vnode el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is) if (props) { for (const key in props) { if (!isReservedProp(key)) { hostPatchProp(el, key, null , props[key], isSVG) } } } if (shapeFlag & 8 ) { hostSetElementText(el, vnode.children) } else if (shapeFlag & 16 ) { mountChildren(vnode.children, el, null , parentComponent, parentSuspense, isSVG && type !== 'foreignObject' , optimized || !!vnode.dynamicChildren) } hostInsert(el, container, anchor) }
可以看到,挂载元素函数主要做四件事:创建 DOM 元素节点、处理 props、处理 children、挂载 DOM 元素到 container 上。
首先是创建 DOM 元素节点,通过 hostCreateElement 方法创建,这是一个平台相关的方法,我们来看一下它在 Web 环境下的定义:
1 2 3 4 function createElement (tag, isSVG, is ) { isSVG ? document .createElementNS(svgNS, tag) : document .createElement(tag, is ? { is } : undefined ) }
它调用了底层的 DOM API document.createElement 创建元素,如果是其他平台比如 Weex,hostCreateElement 方法就不再是操作 DOM ,而是平台相关的 API 了,这些平台相关的方法是在创建渲染器阶段作为参数传入的。
创建完 DOM 节点后,接下来要做的是判断如果有 props 的话,给这个 DOM 节点添加相关的 class、style、event 等属性,并做相关的处理,这些逻辑都是在 hostPatchProp 函数内部做的,这里就不展开讲了。
接下来是对子节点的处理,我们知道 DOM 是一棵树,vnode 同样也是一棵树,并且它和 DOM 结构是一一映射的。
如果子节点是纯文本,则执行 hostSetElementText 方法,它在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本:
1 2 3 function setElementText (el, text ) { el.textContent = text }
如果子节点是数组,则执行 mountChildren 方法:
mountChildren 1 2 3 4 5 6 7 8 9 10 const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0 ) => { for (let i = start; i < children.length; i++) { const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i])) patch(null , child, container, anchor, parentComponent, parentSuspense, isSVG, optimized) } }
子节点的挂载逻辑同样很简单,遍历 children 获取到每一个 child,然后递归执行 patch 方法挂载每一个 child 。
可以看到,mountChildren 函数的第二个参数是 container,而我们调用 mountChildren 方法传入的第二个参数是在 mountElement 时创建的 DOM 节点,这就很好地建立了父子关系。
另外,通过递归 patch 这种深度优先遍历树的方式,我们就可以构造完整的 DOM 树,完成组件的渲染。
处理完所有子节点后,最后通过 hostInsert 方法把创建的 DOM 元素节点挂载到 container 上,它在 Web 环境下这样定义:
1 2 3 4 5 6 7 8 function insert (child, parent, anchor ) { if (anchor) { parent.insertBefore(child, anchor) } else { parent.appendChild(child) } }
这里会做一个 if 判断,如果有参考元素 anchor,就执行 parent.insertBefore ,否则执行 parent.appendChild 来把 child 添加到 parent 下,完成节点的挂载。
因为 insert 的执行是在处理子节点后,所以挂载的顺序是先子节点,后父节点,最终挂载到最外层的容器上。
总结
到这里,从我们创建实例到页面渲染,再到更新的全流程就讲完了,总结一下,大致就是:
每个Vue组件都需要产出一份虚拟DOM,也就是组件的render函数的返回值,render函数你可以直接手写,也可以通过template传递模板字符串,由Vue内部来编译成渲染函数,平常我们开发时写的Vue单文件,最终也会编译成普通的Vue组件选项对象;
render函数会作为副作用函数执行,也就是如果在模板中使用到了响应式数据(所谓响应式数据就是能拦截到它的各种读取、修改操作),那么响应式数据和属性会与render函数关联起来,那么当响应式数据被修改以后,就能找到依赖它的render函数,那么就可以通知依赖的组件进行更新;