Here is a Hello World code from the Vue3 documentation:
<div id="app">
<p>{{ message }}</p>
</div>
Copy the code
const HelloVueApp = {
data() {
return {
message: 'Hello Vue!! '
}
}
}
Vue.createApp(HelloVueApp).mount('#app')
Copy the code
It can render the string “Hello Vue” in the browser, which is not too easy for developers to do – add a text node to the HTML. But what steps does Vue as a framework go through to achieve this simple function?
As you can see from the above code, we need to call two vUe-related functions createApp and mount. These are the two main processes:
- Create an
createApp
- Mount the application
mount
Create an
const createApp = ((. args) = > {
constapp = ensureRenderer().createApp(... args)if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | string): any= > {
const container = normalizeContainer(containerOrSelector)
if(! container)return
const component = app._component
if(! isFunction(component) && ! component.render && ! component.template) { component.template = container.innerHTML }// clear content before mounting
container.innerHTML = ' '
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app'.' ')
return proxy
}
return app
})
Copy the code
The createApp function does two things:
- Creating an App Instance
- call
ensureRenderer
Create the Renderer singleton - Call the Renderer
createApp
Method to create an App instance
- call
- The agent of App
mount
Method, which handles the App container before calling the original method (i.emount
The input parameter to the function#app
)
Create renderer
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
Copy the code
One can see that the ensureRenderer is a simple singleton implementation that guarantees that the Renderer will be created only once. RendererOptions, the parameter passed into createRenderer, is a set of instructions that consists of DOM manipulation and prop Patch. For example, the createText method provided by renderOptions calls the DOM’s document.createTextNode method to create the actual DOM node (we’ll use that later).
createRenderer
function createRenderer<
HostNode = RendererNode.HostElement = RendererElement> (options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
Copy the code
Is createRenderer a redundant function? No, there is a sibling function called createHydrationRenderer that creates the SSR Renderer. They both call the same baseCreateRenderer, but pass different parameters. The createHydrationRenderer also relies on the logic associated with hydration.
So using two functions here to divide and conquer two kinds of logic has two advantages:
- Tree-shaking, WEB environments are not packaged when packaged
hydration
Relevant code. - The code is easy to read and no arguments or comments are required to distinguish between creating two renderers.
baseCreateRenderer
function baseCreateRenderer(options: RendererOptions, createHydrationFns? :typeof createHydrationFunctions
) :any {
// Alias directives in renderOptions with host prefix
const {
insert: hostInsert,
...
createText: hostCreateText,
...
} = options
// Very important method, the VNode -> DOM
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) = > {
// patching & not same type, unmount old tree
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:
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,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if(shapeFlag & ShapeFlags.TELEPORT) { ; (typeas typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
internals
)
} else if(__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ; (typeas typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, ` (The ${typeof type}) `)}}// set ref
if(ref ! =null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
// Related to this article, the method that handles text nodes (called in the patch method)
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) = > {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if(n2.children ! == n1.children) { hostSetText(el, n2.childrenas string)
}
}
}
...
// Very important method, one of only three methods exposed by Renderer (one of which is SSR), calls patch and unmount internally
const render: RootRenderFunction = (vnode, container) = > {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null.null.true)}}else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
Copy the code
This is the core function for creating the Renderer, and I can only show part of the code here. The main function of baseCreateRenderer is to create VNode handler methods that call the DOM API wrapped in renderOptions and implement processText in the code above. Two important entry methods are also derived (ignoring SSR) :
render
Is responsible for “rendering” a VNode into a real DOM, or for removing the DOM associated with a VNode, which is always called after a VNode has changed.createApp
Method to create an App instance.
CreateApp, which we focused on, is a method returned by createAppAPI(Render, Hydrate).
createAppAPI
function createAppAPI<HostElement> (render: RootRenderFunction, hydrate? : RootHydrateFunction) :CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {... }}Copy the code
The createAppAPI function forms a closure that allows the internal createApp method to call the render method of the renderer.
Create an
function createApp(rootComponent, rootProps = null) {...const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null._context: context, ... mount(rootContainer: HostElement, isHydrate? : boolean): any { ... }})...return app
}
Copy the code
EnsureRenderer ().createApp(… Args) is the createApp method returned by createAppAPI, so the HelloVueApp object passed in to the Demo code is assigned to the Component property of the App.
App is an oft-mentioned instance of Vue with the following properties and methods (implemented in the createApp method of Runtime-core/SRC/apicreateapp.ts) :
export interface App<HostElement = any> {
version: string
config: AppConfig use(plugin: Plugin, ... options:any[]) :this
mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined
component(name: string.component: Component): this
directive(name: string): Directive | undefined
directive(name: string.directive: Directive): this
mount(
rootContainer: HostElement | string, isHydrate? :boolean
): ComponentPublicInstance
unmount(rootContainer: HostElement | string) :void
provide<T>(key: InjectionKey<T> | string.value: T): this
// internal, but we need to expose these for the server-renderer and devtools
_uid: number
_component: ConcreteComponent
_props: Data | null
_container: HostElement | null
_context: AppContext
}
Copy the code
Notice that App has a mount method, which is very important and will be covered later.
Mount the application
const { mount } = app
app.mount = (containerOrSelector: Element | string) :any= > {
const container = normalizeContainer(containerOrSelector)
if(! container)return
const component = app._component
if(! isFunction(component) && ! component.render && ! component.template) { component.template = container.innerHTML }// clear content before mounting
container.innerHTML = ' '
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app'.' ')
return proxy
}
Copy the code
So if I go back to my original createApp method, it creates the App and then it’s going to proxy mount, and it’s going to take precedence in proxy mount and it’s going to call normalizeContainer and it’s going to handle containerOrSelector, #app in the Demo code, and then execute the original mount method.
Standardized application container
function normalizeContainer(container: Element | string): Element | null { if (isString(container)) { const res = document.querySelector(container) if (__DEV__ && ! res) { warn(`Failed to mount app: mount target selector returned null.`) } return res } return container }Copy the code
The normalizeContainer function simply replaces the string entry with the corresponding DOM object. The proxy mount method supports two parameter types:
- String (in Demo
#app
The string is used to findid
为app
The DOM) - DOM object
Let’s move on to the actual implementation of mount, as mentioned above, in the createApp method of the run-time core/ SRC/apicreateapp. ts file.
Mount the application
mount(rootContainer: HostElement, isHydrate? :boolean) :any {
if(! isMounted) {const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
...
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)}else {
render(vnode, rootContainer)
}
isMounted = true. }},Copy the code
As you can see, the mount method has only two responsibilities:
- Create a VNode
createVNode
- Calls the renderer’s render function
render
Let’s look at creating a VNode…
createVNode
_createVNode
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 {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: The ${type}. `)}type = Comment
}
if (isVNode(type)) {
// createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// class component normalization.
if (isClassComponent(type)) {
type = type.__vccOpts
}
// class & style normalization.
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, props)
}
let { class: klass, style } = props
if(klass && ! isString(klass)) { props.class = normalizeClass(klass) }if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if(isProxy(style) && ! isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } }// encode the vnode type information into a bitmap
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
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`.`\nComponent that was made reactive: `.type)}const vnode: VNode = {
__v_isVNode: true,
[ReactiveFlags.SKIP]: true.type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null.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
}
// validate key
if(__DEV__ && vnode.key ! == vnode.key) { warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
const { content, fallback } = normalizeSuspenseChildren(vnode)
vnode.ssContent = content
vnode.ssFallback = fallback
}
if (
shouldTrack > 0 &&
// avoid a block node from tracking itself! isBlockNode &&// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.patchFlag ! == PatchFlags.HYDRATE_EVENTS ) { currentBlock.push(vnode) }return vnode
}
Copy the code
It should be clear from the diagram and code that createVNode creates a specific object to describe the node. It is worth noting that the shapeFlag attribute in VNode is related to the type of the child node, which usually indicates that the attribute will play an important role in the recursive process. In this case, the createApp argument passed in is an object with no child nodes, so the value of shapeFlag is shapeFlags.stateful_component, which represents the stateful component.
If you’re not familiar with VNode, check out these two articles:
- Design VNode
- Cognitive misunderstandings of Virtual DOM
normalizeChildren
function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
if (children == null) {
children = null
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') {
if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
// Normalize slot to plain children for plain element and Teleport
const slot = (children as any).default
if (slot) {
// _c marker is added by withCtx() indicating this is a compiled slot
slot._c && setCompiledSlotRendering(1)
normalizeChildren(vnode, slot())
slot._c && setCompiledSlotRendering(-1)}return
} else {
type = ShapeFlags.SLOTS_CHILDREN
const slotFlag = (children as RawSlots)._
if(! slotFlag && ! (InternalObjectKeyinchildren!) ) {// if slots are not normalized, attach context instance
// (compiled / normalized slots already have context); (childrenas RawSlots)._ctx = currentRenderingInstance
} else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
// a child component receives forwarded slots from the parent.
// its slot type is determined by its parent's slot type.
if( currentRenderingInstance.vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS ) { ; (childrenas RawSlots)._ = SlotFlags.DYNAMIC
vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
} else {
;(children as RawSlots)._ = SlotFlags.STABLE
}
}
}
} else if (isFunction(children)) {
children = { default: children, _ctx: currentRenderingInstance }
type = ShapeFlags.SLOTS_CHILDREN
} else {
children = String(children)
// force teleport children to array so it can be moved around
if (shapeFlag & ShapeFlags.TELEPORT) {
type = ShapeFlags.ARRAY_CHILDREN
children = [createTextVNode(children as string)]}else {
type = ShapeFlags.TEXT_CHILDREN
}
}
vnode.children = children as VNodeNormalizedChildren
vnode.shapeFlag |= type
}
Copy the code
This method determines the type of the child node and changes the shapeFlag value of the VNode. In this way, the type of the child node can be known according to the shapeFlag value during the operation of the VNode, facilitating the next step. In addition, in the function diagram of createVNode above, THE reason why THERE is a problem with Suspense component judgment is that it does not judge until shapeFlag is changed. I will confirm this later and add the rest information.
render
const render: RootRenderFunction = (vnode, container) = > {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null.null.true)}}else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
Copy the code
As mentioned above, the function of rendering function is to associate VNode with DOM. It translates VNode into DOM through patch method, whose parameters are:
- The container’s VNode
- The VNode of the current node to render (
HelloVueApp
Object generated VNode) - The container itself (id is
app
The DOM)
patch
const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) => { // patching & not same type, unmount old tree 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: 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, optimized ) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else ... }Copy the code
We already know that n2 in the patch parameter is the VNode generated by the HelloVueApp object, and its shapeFlags value is 4 (ststate component). ShapeFlags.COMPONENT has a value of 6 and contains stateful component 4 and function component 2, so it goes into the processComponent method.
Note: 4&6 === true
processComponent
const processComponent = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { 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
The processComponent function has only one distribution responsibility, which is to determine if the parent VNode of the current node exists, update updateComponent if it exists, and mount mountComponent if it does not.
mountComponent
const mountComponent: MountComponentFn = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
...
setupComponent(instance)
...
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
...
}
Copy the code
The mountComponent function does three things:
- Creating a component instance
createComponentInstance
- Preparing component Content
setupComponent
- Rendering component
setupRenderEffect
createComponentInstance
function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const type = vnode.type as ConcreteComponent
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
...
}
return instance
Copy the code
The createComponentInstance function creates and returns an object that is the component instance we accessed using this in the Options API.
setupComponent
function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
Copy the code
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {...const { setup } = Component
if (setup) {
...
} else {
finishComponentSetup(instance, isSSR)
}
}
Copy the code
The setupComponent function handles component configuration information in two steps:
- Initialize the
props
和slots
- call
setupStatefulComponent
Handles other configuration information for components
The setupStatefulComponent function handles the rest of the component’s configuration information (methods/data/Watch, etc.) by calling the finishComponentSetup method. Prior to this, the Composition API is also distinguished from the Options API by the presence or absence of setup, which requires the setup function to be executed first.
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {...if(compile && Component.template && ! Component.render) {if (__DEV__) {
startMeasure(instance, `compile`)
}
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
})
if (__DEV__) {
endMeasure(instance, `compile`)}}...// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
currentInstance = instance
applyOptions(instance, Component)
currentInstance = null}... }Copy the code
The responsibilities of the finishComponentSetup function are also clear, two things:
- If there is
compile
Function and the template is not compiled - call
applyOptions
Process component configuration information
The Vue3compile
Although the process of Vue2 is clearer than that of Vue2, but it also needs a longer length to explain, so I leave it to other articles. Here is only a compiled process diagram for easy understandingcompile
Input and output of functions and core flow:
The applyOptions function handles a number of options, each of which involves a bit of a waste of time. Since we have only configured the data option, we will only analyze the flow of data processing.
export function applyOptions(
instance: ComponentInternalInstance,
options: ComponentOptions,
deferredData: DataFn[] = [],
deferredWatch: ComponentWatchOptions[] = [],
deferredProvide: (Data | Function)[] = [],
asMixin: boolean = false
) {
const { data: dataOptions, ... } = options
...
isInBeforeCreate = true
callSyncHook(
'beforeCreate',
LifecycleHooks.BEFORE_CREATE,
options,
instance,
globalMixins
)
isInBeforeCreate = false.if (dataOptions) {
resolveData(instance, dataOptions, publicThis)
}
...
callSyncHook(
'created',
LifecycleHooks.CREATED,
options,
instance,
globalMixins
)
...
}
Copy the code
You can see that applyOptions does the following:
- call
beforeCreate
The props and slots that were handled earlier can only be fetched by instance because the configuration item has not yet been processed - Distribute handlers, like the one here
resolveData
- call
created
Hook to retrieve all configuration items - Inject other lifecycle hooks into the corresponding properties of the instance, for example
instance.bm.push(beforeMount)
(No code shown)
I’ll store it in an arraycreated
Later lifecycle hooks, because mixin and extends’s parent classes may also have these hook functions configured
function resolveData(instance: ComponentInternalInstance, dataFn: DataFn, publicThis: ComponentPublicInstance) {
const data = dataFn.call(publicThis, publicThis)
if(! isObject(data)) { __DEV__ && warn(`data() should return an object.`)}else if (instance.data === EMPTY_OBJ) {
instance.data = reactive(data)
} else {
// existing data: this is a mixin or extends.
extend(instance.data, data)
}
}
Copy the code
The dataFn is the data option configured by the user. ResolveData will call the dataFn function to get the returned object (if the dataFn is not a function, it will send an alarm and no code is attached), and then determine whether the data source is mixin/extends. If yes, reactive objects are merged directly into the data property of the component instance. If not, reactive objects are called to become reactive objects and then merged.
Componentoptions.ts = componentOptions.ts = componentOptions.ts = componentOptions.ts
setupRenderEffect
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { // create reactive effect for rendering instance.update = effect(function componentEffect() {... }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }Copy the code
The setupRenderEffect function internally provides an update method to the component instance, which is returned by the effect function.
function isEffect(fn: any) :fn is ReactiveEffect {
return fn && fn._isEffect === true
}
export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if(! options.lazy) { effect() }return effect
}
Copy the code
Function effect
- Determine the reference
fn
(that is, passed in when calledcomponentEffect
) whether has beeneffect
If so, return the function before processingfn.raw
。 - call
createReactiveEffect
Function gets the proxy function, which is called internallycomponentEffect
Take some notes beforetrackStack
Record a Boolean value. What is iteffectStack
Record in executioneffect
Function stack, render complete then out of the stackactiveEffect
Record the latest executioneffect
function
- Executing proxy functions
effect
Therefore, the update function of the component instance is actually the proxy function effect returned by createReactiveEffect.
function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
const effect = function reactiveEffect() :unknown {
if(! effect.active) {return options.scheduler ? undefined : fn()
}
if(! effectStack.includes(effect)) { cleanup(effect)try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]}}}asReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
Copy the code
As you can see, the proxy function returned by createReactiveEffect records some information and adds some attributes to it:
id
On the logoallowRecurse
Whether or not recursion is allowed_isEffect
Is it handled by the side effect function (proxy)active
Whether to stop the side effect of the functionraw
The original functiondeps
Collect additional side effects functions tracked by the current componentoptions
Side effect option
Next we look at the original function componentEffect executed by fn(), which is also the actual mount and update function in effect.
function componentEffect() {
if(! instance.isMounted) { ...const { bm, m, parent } = instance
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
...
// Get the component VNode
const subTree = (instance.subTree = renderComponentRoot(instance))
...
// patch
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
...
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
...
instance.isMounted = true
} else {
// Update logic. }Copy the code
The function componentEffect mounts the following logic:
- call
beforeMount
The hook - call
renderComponentRoot
Get component VNode (where component will be calledrender
Function) - call
patch
function - call
mount
hook
RenderComponentRoot provides the following vNodes for the current component:
{
children: "Hello Vue!!".patchFlag: 0.shapeFlag: 8.type: Symbol(Text),
__v_isVNode: true. }Copy the code
Then we continue to look at the patch function:
.switch (type) {
case Text:
processText(n1, n2, container, anchor)
...
Copy the code
Since VNode’s type attribute is Symbol(Text), the Text node is processed by calling the original processText function.
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) = > {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if(n2.children ! == n1.children) { hostSetText(el, n2.childrenas string)}}}Copy the code
Document. insertBefore (processText); hostCreateText (processText);
const doc = (typeof document! = ='undefined' ? document : null) as Document
...
export {
insert: (child, parent, anchor) = > {
parent.insertBefore(child, anchor || null)},...createText: text= > doc.createTextNode(text),
}
Copy the code
At this point the Hello Vue!!!!! It is rendered to HTML, displayed on the page, and then calls the Mounted lifecycle hook (Suspense component, of course).
conclusion
For Vue3, rendering the Hello World component goes through several main stages:
- Create renderer
createRenderer
- Create an
createApp
- Creating a Virtual Node
_createVNode
- Perform rendering
render
The patch process we often refer to in Vue2 is in the rendering stage of Vue3.
Render distribution functionpatch
It does not deal with the actual logic, but just allocates the corresponding handler function for each node type, which is internally recursive because of the node tree.