Vue is simple and easy to get started, requiring only simple documentation to get started. Although I have been using Vue 2.0 project development experience, before also only understand a little core code logic, did not read the source code of Vue 2.0. After the release of Vue 3.0, I also have some Vue 3.0 project experience, follow the guide to learn the Vue 3.0 source code, to learn from the master coding skills, in order to be more adept in the project.

Since Vue 3.0 is refactoring with TypeScript, you should be familiar with the basic TypeScript syntax before reading this series. You should also be familiar with recursion calls and function curlization.

Vue 3.0 series of articles is estimated to have about 30 articles, each article will only focus on the knowledge involved, so that the analysis will be clear, otherwise it will be very confused.

If you don’t want to see the complicated analysis, you can go straight to the final picture summary.

We often use code like this:

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");
Copy the code

App is a Vue component. The browser cannot recognize this component and does not know how to render it. How does Vue render an App component into a real DOM? In this article, we will learn how to render Vue components into the real DOM.

Application initialization

createAppThe entry function

export const createApp = ((... args) => { // 1. const app = ensureRenderer().createApp(... The args) / / 2. Const {mount} = app app. Mount = (containerOrSelector: Element | ShadowRoot | string) : any = > {/ / omit... } return app }) as CreateAppFunction<Element>Copy the code

The createApp entry function does two main things:

  • useensureRenderer().createApp()createappobject
  • rewriteappthemountMethods.
createappobject
Create the renderer object

A renderer is a JS object with the core logic of platform rendering. Vue can be used for cross-platform rendering, so it doesn’t have to be DOM rendering.

To create a renderer object with ensureRenderer() :

// Let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer function ensureRenderer() { return ( renderer || (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions)) ) }Copy the code

There is no renderer to create. It is a delayed creation method, and only when needed will the renderer be created.

The renderer is initialized with a renderer configuration parameter, rendererOptions, which defines attribute processing methods, DOM manipulation methods, and so on.

Export interface RendererOptions< HostNode = RendererNode, HostElement = RendererElement > {// Handle Prop, Attributes etc patchProp(EL: HostElement, key: string, prevValue: any, nextValue: any, isSVG? : boolean, prevChildren? : VNode<HostNode, HostElement>[], parentComponent? : ComponentInternalInstance | null, parentSuspense? : SuspenseBoundary | null, unmountChildren? Insert (el: HostNode, parent: HostElement, Anchor? : HostNode | null) : void / / omit... }Copy the code

While the developer does not need to manipulate the DOM directly, it can be guessed that all components will be converted to the DOM. This configuration parameter of the renderer contains methods that manipulate the DOM directly, and is therefore a critical configuration.

The createRenderer method internally calls the baseCreateRenderer method directly:

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}
Copy the code

The baseCreateRenderer method has the following code:

function baseCreateRenderer( options: RendererOptions, createHydrationFns? : typeof createHydrationFunctions ): any { // 1. const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options // 2. const patch: PatchFn = ( n1, n2, ... ) => {} const processComponent = ( n1: VNode | null, n2: VNode, ... ) => {} // Omit many methods... const render: RootRenderFunction = (vnode, container, isSVG) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) } } else { patch(container._vnode || null, vnode, container, null, null, null, isSVG) } flushPostFlushCbs() container._vnode = vnode } // 3. return { render, hydrate, createApp: createAppAPI(render, hydrate) } }Copy the code
  1. First deconstruct what’s passed inRendererOptionsobjectoptions, and then modified the operationDOMMethod parameter names;
  2. Defines a number of render related methods, the most important of which isrenderMethods.renderAn importantpatchMethod,patchMethod in turn calls other methods, such as component processing relatedprocessComponentMethods. If a method needs an operationDOMThat will callRendererOptionsobjectoptionsMethod in.
  3. Finally, return a containrenderandcreateAppMethod object.hydrateforundefined.

The value of createApp above is the return value of createAppAPI, so what does it do?

export function createAppAPI<HostElement>( render: RootRenderFunction, hydrate? : RootHydrateFunction ): CreateAppFunction<HostElement> { // 1. return function createApp(rootComponent, rootProps = null) { // 2. if (rootProps ! = null && ! isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.`) rootProps = null } // 2. const context = createAppContext() // 3. const installedPlugins = new Set() // 4. let isMounted = false // 5. 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) { }, use(plugin: Plugin, ... options: any[]) { if (installedPlugins.has(plugin)) { __DEV__ && warn(`Plugin has already been applied to target app.`) } else if  (plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) plugin.install(app, ... options) } else if (isFunction(plugin)) { installedPlugins.add(plugin) plugin(app, ... options) } return app }, mixin(mixin: ComponentOptions) { if (__FEATURE_OPTIONS_API__) { if (! context.mixins.includes(mixin)) { context.mixins.push(mixin) } return app }, component(name: string, component? : Component): any { if (! component) { return context.components[name] } context.components[name] = component return app }, directive(name: string, directive?: Directive) { if (! directive) { return context.directives[name] as any } context.directives[name] = directive return app }, mount( rootContainer: HostElement, isHydrate? : boolean, isSVG? : boolean ): any { if (! isMounted) { const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // store app context on the root VNode. // this will be set on the root instance on initial mount. vnode.appContext = context if (isHydrate && hydrate) { hydrate(vnode as VNode<Node, Element>, rootContainer as any) } else { render(vnode, rootContainer, isSVG) } isMounted = true app._container = rootContainer // for devtools and telemetry ; (rootContainer as any).__vue_app__ = app return vnode.component! .proxy } }, unmount() { if (isMounted) { render(null, app._container) delete app._container.__vue_app__ } }, provide(key, value) { context.provides[key as string] = value return app } }) if (__COMPAT__) { installAppCompatProperties(app, context, render) } return app } }Copy the code
  1. createAppAPIThe result of the execution iscreateAppMethod, the result of which is to return aAppObject;
  • Note: Don’t get confusedVueIn the frameworkAppAnd the developerApp, defined by the developerAppIt’s actually the argument passed to the functionrootComponentThis root component,rootPropsIs related to the root component passed in by the developerprops.
  1. checkprops– if not null, must be an object;
  2. To create aAppContextobjectcontext, it contains oneappAttribute points toAppObject,plugin.provide.directiveandcomponentAnd so on are mounted on this object;
  3. installedPluginsUsed to store the installationPlugin;
  4. isMountedSet tofalse, marked as not mounted;
  5. It generates aappObject that contains a number of properties:_componentDefined for developersAppThe root component,_propsFor the root component passed in by the developerprops, _context is defined aboveAppContextobjectcontext. It also contains some methods,useInstall plug-in method,mixinBlending method,componentGlobally define component methods,directiveInstruction method,mountMounting method,unmountUnloading method,provideShared data methods.
  • Here we can see thatVue 3.0One major change is that these methods have changed from beforeVue 2.0theThe global methodTurned out to beappObject methods.
  • One of the important ways to do this is tomountThe mounting method is described later. This method holdsrenderRender method, so callmountMethod without passing the renderer. This isThe function is curializedIs an important skill of.
rewritemountmethods

So let’s go back to the createApp entry function — const app = ensureRenderer().createApp(… Args), let’s analyze the following process:

export const createApp = ((... args) => { const app = ensureRenderer().createApp(... args) // 1. const { mount } = app app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { // 2. const container = normalizeContainer(containerOrSelector) if (! container) return // 3. const component = app._component // 4. container.innerHTML = '' // 5. const proxy = mount(container, false, container instanceof SVGElement) if (container instanceof Element) { // 6. container.removeAttribute('v-cloak') // 7. container.setAttribute('data-v-app', '') } return proxy } return app }) as CreateAppFunction<Element>Copy the code
  1. Deconstructs theappIn themountMethod, and then overrideappIn themountMethods;
  2. Standardized container, or if the container is a stringdocument.querySelector(container)Find the correspondingDOMNode, which is why we can pass “#app” as the container;
  3. willapp._componentAssigned tocomponentObject. This object is actually provided by the developerAppThe root component;
  4. Clear the contents of the container, that is, if the container has children, it will be cleared;
  5. Call frameworkAppOf themountMethods, i.e.,createAppAPIMethod of the app objectmount, this process is described in detail below;
  • Take a look atmountMethod calls:mount(container, true, container instanceof SVGElement)The first parameter is the container and the second parameter istrue, the third parameter isfalse.
  1. To clear the containerv-cloak Attribute, this can be the property can be the sum{display:none}Combined with solving the problem of page flash in the case of slow network;
  2. Add a data-v-app Attribute to the container. This Attribute has no real function, it is just a tag.

Let’s go to the App’s mount method:

mount( rootContainer: HostElement, isHydrate? : boolean, isSVG? : boolean ): any { if (! isMounted) { // 1. const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // 2. vnode.appContext = context // 3. render(vnode, rootContainer, isSVG) // 4. isMounted = true // 5. app._container = rootContainer return vnode.component! .proxy } }Copy the code
  1. First of all, according to therootComponentandrootPropsCreate the correspondingVNodeobjectvnode;
  2. tovnodetheappContextAssign to createappIs initialized whencontextthecontextAs described above, you can hang plug-ins and other content, in addition toappAttribute points toapp;
  3. Apply colours to a drawingvnode, this will be highlighted below, not in depth;
  4. Marked as mounted;
  5. toappthe_containerAssign to the parent container;

There are two important logic in the App’s mount process: creating a VNode’s createVNode and rendering a VNode’s Render (VNode, rootContainer, isSVG), which we will cover next.

createVNode

VNode is a JS object developed to describe the DOM at the front end. It can describe different types of nodes, such as component nodes, ordinary element nodes, and many other types of nodes. DOM is a tree structure, VNode is also a tree structure.

VNode is similar to the Widget in Flutter, except that it is a description tree of node information. The real rendering tree in Flutter is the RenderObject tree, while Vue’s rendering tree is the DOM tree in the front-end development.

The cross-platform logic of Flutter is different depending on the platform. Vue is also cross-platform based on VNode. Weex and UniApp are used for multi-platform development.

createVNode

CreateVNode internally points to the _createVNode function:

function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null, isBlockNode = false ): VNode { // 1. if (isVNode(type)) { const cloned = cloneVNode(type, props, true /* mergeRef: true */) if (children) { normalizeChildren(cloned, children) } return cloned } if (props) { // 2. props = guardReactiveProps(props)! // 3. let { class: klass, style } = props if (klass && ! isString(klass)) { props.class = normalizeClass(klass) } // 4. if (isObject(style)) { if (isProxy(style) && ! isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } } // 5. const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : __FEATURE_SUSPENSE__ && isSuspense(type) ? ShapeFlags.SUSPENSE : isTeleport(type) ? ShapeFlags.TELEPORT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 // 6. return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ) }Copy the code
  1. If the incomingtypeThe parameters are essentiallyVNode, then make a copy and normalize the child nodes and return;
  2. If you havepropsIf it is a reactive object, it will be copied, otherwise it will not be processed. Reactive object replication is to avoid other side effects of modifying reactive data;
  3. If it’s an array, it returns each element of the array. If it’s an object, it takes the property of the object and sets it to true and separates it with Spaces. Reference documentation
  4. If it is a string or an object, return the original value. If it is data, combine the key and value of each element in the array to form a style object. Reference documents;
  5. According to thetypeThe type is encoded asShapeFlags, if passed inObject, is encoded asShapeFlags.STATEFUL_COMPONENT;
  6. The last callcreateBaseVNodeDo the actual creationVNode;
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 ? Zero: ShapeFlags.ELEMENT, isBlockNode = false, needFullChildrenNormalization = false ) { // 1. 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 } as VNode if (needFullChildrenNormalization) { // 2. normalizeChildren(vnode, children) } else if (children) { vnode.shapeFlag |= isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN } return vnode }Copy the code
  1. generatevnodeObject that containstypeandpropsParameters;
  2. Normalize a child node – assigns a child node tovnodeThe object’schildrenProperty, according to the child nodeShapeFlagsModify the point beforeVNodetheShapeFlags;

Apply colours to a drawingVNode

Let’s look at the render(vnode, rootContainer, isSVG) logic in the mount method:

  const render: RootRenderFunction = (vnode, container, isSVG) => {
    // 1. 
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 2.
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    // 3. 
    container._vnode = vnode
  }
Copy the code
  1. ifvnodefornull, the parent container is uninstalled_vnodeObject;
  2. ifvnodeDon’t fornullThe callpatchMethod, first timecontainer._vnodefornull.vnodeFor developersAppThe generatedVNode.containerfor#appDOMElements;
  3. The parent container_vnodeSet tovnode;

Mount and updateVNode

// 1. const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = __DEV__ && isHmrUpdating ? false : !! n2.dynamicChildren ) => { // 2. if (n1 === n2) { return } // 3. 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 } // 4. const { type, ref, shapeFlag } = n2 switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.TELEPORT) { ; (type as typeof TeleportImpl).process( n1 as TeleportVNode, n2 as TeleportVNode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ; (type as typeof SuspenseImpl).process( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals ) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } } if (ref ! = null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, ! n2) } }Copy the code
  1. patchThe first argument to the method is oldVNodeThe second parameter is newVNodeThe third parameter is the parent nodeDOMElements;
  2. If the old and newVNodeIf it’s the same object, you don’t need to operate on it, you just return it;
  3. If the old and newVNodetheVNodeTypeIf not, uninstall the old one firstVNodeTo the oldVNodeEmpty and mount the new oneVNode;
  4. According to the newVNodethetypeandshapeFlagEnter if it is a componentprocessComponentMethod, if a normal node is enteredprocessElementMethods;

processComponentProcessing components

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) } }Copy the code

If the old VNode is NULL and is not a keep-alive component, then the mountComponent method is called to mount the component.

mountComponentMount components

const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { // 1. const compatMountInstance = __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component const instance: ComponentInternalInstance = compatMountInstance || (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )) // 2. if (! (__COMPAT__ && compatMountInstance)) { setupComponent(instance) } // 3. setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) }Copy the code
  1. Creating a component InstanceComponentInternalInstanceobjectcompatMountInstance, we see that the created instance receivedvnodeAnd the parent container nodeDOMElement, and all other attributes are initialized by default;
  2. Set component instances primarily from the ownedvnodeTo derivepropsandslotIn addition, if the developer has used in the componentsetupMethod, then thissetupMethods are also called, holding their properties and methods;
  3. By component instancecompatMountInstance.vnodeAnd the parent container nodeDOMElement to create a render function with side effects;

setupRenderEffect– Create a render function with side effects

const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { // 1. const componentUpdateFn = () => { if (! instance.isMounted) { const subTree = (instance.subTree = renderComponentRoot(instance)) patch( null, subTree, container, anchor, instance, parentSuspense, isSVG ) initialVNode.el = subTree.el } } // 2. const effect = new ReactiveEffect( componentUpdateFn, () => queueJob(instance.update), instance.scope // track it in component's effect scope ) // 3. const update = (instance.update = effect.run.bind(effect)  as SchedulerJob) update.id = instance.uid // allowRecurse // #1801, #2043 component render effects should allow recursive updates effect.allowRecurse = update.allowRecurse = true // 4. update() }Copy the code
  1. Create a first render or update rendercomponentUpdateFnMethod, actually they all get calledpatchMethod, the difference between them is that the first parameter of the first render isnull, while updating the render when the first parameter is oldVNode;
  • componentUpdateFnIn the callpatchOne of the characteristics of the method is that it is transmittedinstance, i.e., theComponentInternalInstanceObject is passed as a parameterpatchMethods.
  1. I created aReactiveEffectobjecteffect, the first argument to this objectfnIs a function wheneffectcallupdateMethod executesfnFunction call;
  2. willeffecttherunThe function is assigned toinstancetheupdateProperty and toupdateMarked with aid;
  3. performupdate()“, which is executioncomponentUpdateFnMethod,componentUpdateFnMethod will callpatchMethod, recurse.

Patch at the end of the day must be a VNode that handles normal elements, so let’s look at how the VNode handles normal elements.

processElementHandle common element nodes

  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      
    }
  }
Copy the code

When n1 is null, processElement is the method to mount elements into mountElement.

mountElementMount the element node

const mountElement = ( vnode: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { let el: RendererElement let vnodeHook: VNodeHook | undefined | null const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode // 1. el = vnode.el = hostCreateElement( vnode.type as string, isSVG, props && props.is, props ) // 2. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, vnode.children as string) } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren( vnode.children as VNodeArrayChildren, el, null, parentComponent, parentSuspense, isSVG && type ! == 'foreignObject', slotScopeIds, optimized ) } if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'created') } // props if (props) { for (const key in props) { if (key ! == 'value' && ! isReservedProp(key)) { hostPatchProp( el, key, null, props[key], isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } } // 4. hostInsert(el, container, anchor) }Copy the code
  1. throughhostCreateElementcreateDOMElement nodes, which we discussed earlierhostCreateElementIs a configuration parameter passed in when the renderer is createddoc.createElement(tag, is ? { is } : undefined);
  2. The child node is processed: if the child node is text, the actual call endsel.textContent = text; If the child node is an array, callmountChildrenMethod, called by each child node of the arraypatchMount child nodes;
  3. Judge if there ispropsThrough thehostPatchPropMethod to thisDOMNode Settings relatedclass.style.eventSuch attributes.
  4. Will create theDOMElement is mounted tocontainerOn.

We have seen that the DOM element created is mounted after the depth-first path, which means that the DOM element is mounted from the child nodes of the tree, then gradually mounts to the root node, and finally mounts to the whole element #app. At this point, the entire DOM rendering is complete.

conclusion

createApp

mount