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
createApp
The 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:
- use
ensureRenderer().createApp()
createapp
object - rewrite
app
themount
Methods.
createapp
object
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
- First deconstruct what’s passed inRendererOptionsobject
options
, and then modified the operationDOMMethod parameter names;- Defines a number of render related methods, the most important of which is
render
Methods.render
An importantpatch
Method,patch
Method in turn calls other methods, such as component processing relatedprocessComponent
Methods. If a method needs an operationDOMThat will callRendererOptionsobjectoptions
Method in.- Finally, return a contain
render
andcreateApp
Method object.hydrate
forundefined
.
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
createAppAPI
The result of the execution iscreateApp
Method, the result of which is to return aApp
Object;
- Note: Don’t get confused
Vue
In the frameworkApp
And the developerApp
, defined by the developerApp
It’s actually the argument passed to the functionrootComponent
This root component,rootProps
Is related to the root component passed in by the developerprops
.
- check
props
– if not null, must be an object;- To create aAppContextobject
context
, it contains oneapp
Attribute points toApp
Object,plugin.provide.directiveandcomponentAnd so on are mounted on this object;installedPlugins
Used to store the installationPlugin;isMounted
Set tofalse, marked as not mounted;- It generates a
app
Object that contains a number of properties:_component
Defined for developersApp
The root component,_props
For the root component passed in by the developerprops
, _context is defined aboveAppContextobjectcontext
. It also contains some methods,use
Install plug-in method,mixin
Blending method,component
Globally define component methods,directive
Instruction method,mount
Mounting method,unmount
Unloading method,provide
Shared data methods.
- Here we can see that
Vue 3.0
One major change is that these methods have changed from beforeVue 2.0
theThe global methodTurned out to beappObject methods.- One of the important ways to do this is to
mount
The mounting method is described later. This method holdsrender
Render method, so callmount
Method without passing the renderer. This isThe function is curializedIs an important skill of.
rewritemount
methods
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
- Deconstructs the
app
In themount
Method, and then overrideapp
In themount
Methods;- Standardized container, or if the container is a string
document.querySelector(container)
Find the correspondingDOMNode, which is why we can pass “#app” as the container;- will
app._component
Assigned tocomponent
Object. This object is actually provided by the developerAppThe root component;- Clear the contents of the container, that is, if the container has children, it will be cleared;
- Call frameworkAppOf the
mount
Methods, i.e.,createAppAPI
Method of the app objectmount
, this process is described in detail below;
- Take a look at
mount
Method calls:mount(container, true, container instanceof SVGElement)
The first parameter is the container and the second parameter istrue, the third parameter isfalse.
- 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;- 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
- First of all, according to the
rootComponent
androotProps
Create the correspondingVNodeobjectvnode
;- to
vnode
theappContext
Assign to createappIs initialized whencontext
thecontext
As described above, you can hang plug-ins and other content, in addition toappAttribute points toapp;- Apply colours to a drawing
vnode
, this will be highlighted below, not in depth;- Marked as mounted;
- to
app
the_container
Assign 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
- If the incoming
type
The parameters are essentiallyVNode
, then make a copy and normalize the child nodes and return;- If you have
props
If 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;- 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
- 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;
- According to the
type
The type is encoded asShapeFlags, if passed inObject
, is encoded asShapeFlags.STATEFUL_COMPONENT;- The last call
createBaseVNode
Do 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
- generate
vnode
Object that containstype
andprops
Parameters;- Normalize a child node – assigns a child node to
vnode
The object’schildren
Property, according to the child nodeShapeFlags
Modify the point beforeVNode
theShapeFlags
;
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
- if
vnode
fornull
, the parent container is uninstalled_vnode
Object;- if
vnode
Don’t fornull
The callpatch
Method, first timecontainer._vnode
fornull
.vnode
For developersAppThe generatedVNode
.container
for#app
DOMElements;- The parent container
_vnode
Set 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
patch
The first argument to the method is oldVNode
The second parameter is newVNode
The third parameter is the parent nodeDOMElements;- If the old and new
VNode
If it’s the same object, you don’t need to operate on it, you just return it;- If the old and new
VNode
theVNodeType
If not, uninstall the old one firstVNode
To the oldVNode
Empty and mount the new oneVNode
;- According to the new
VNode
thetype
andshapeFlag
Enter if it is a componentprocessComponent
Method, if a normal node is enteredprocessElement
Methods;
processComponent
Processing 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.
mountComponent
Mount 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
- Creating a component InstanceComponentInternalInstanceobject
compatMountInstance
, we see that the created instance receivedvnode
And the parent container nodeDOM
Element, and all other attributes are initialized by default;- Set component instances primarily from the owned
vnode
To deriveprops
andslot
In addition, if the developer has used in the componentsetup
Method, then thissetup
Methods are also called, holding their properties and methods;- By component instance
compatMountInstance
.vnode
And the parent container nodeDOM
Element 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
- Create a first render or update render
componentUpdateFn
Method, actually they all get calledpatch
Method, the difference between them is that the first parameter of the first render isnull
, while updating the render when the first parameter is oldVNode
;
componentUpdateFn
In the callpatch
One of the characteristics of the method is that it is transmittedinstance
, i.e., theComponentInternalInstanceObject is passed as a parameterpatch
Methods.
- I created aReactiveEffectobject
effect
, the first argument to this objectfn
Is a function wheneffect
callupdate
Method executesfn
Function call;- will
effect
therun
The function is assigned toinstance
theupdate
Property and toupdate
Marked with aid
;- perform
update()
“, which is executioncomponentUpdateFn
Method,componentUpdateFn
Method will callpatch
Method, 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.
processElement
Handle 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.
mountElement
Mount 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
- through
hostCreateElement
createDOMElement nodes, which we discussed earlierhostCreateElement
Is a configuration parameter passed in when the renderer is createddoc.createElement(tag, is ? { is } : undefined)
;- The child node is processed: if the child node is text, the actual call ends
el.textContent = text
; If the child node is an array, callmountChildren
Method, called by each child node of the arraypatch
Mount child nodes;- Judge if there is
props
Through thehostPatchProp
Method to thisDOMNode Settings relatedclass
.style
.event
Such attributes.- Will create theDOMElement is mounted to
container
On.
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.