Super large cup with rich American ☕️, chat about VNode generation details.
Before we begin, let’s review the execution diagram:
- Vue executes engineering simply to understand the entire process;
- The template compiler details the process of generating the render function.
- Reactive talked in detail about the intermediate process, the data dependent collection and distribution update process;
As the process of _render and _update is quite complicated, it is divided into two periods. Let’s start with the _render process in this installment.
VNode
If not for the virtual DOM, jump to knowing the virtual DOM. This section goes into more detail about render’s VNode generation process. They’re going to be reactive. They’re going to be reactive.
export function installRenderHelpers (target: any) { target._o = markOnce target._n = toNumber target._s = toString target._l = renderList target._t = renderSlot target._q = looseEqual target._i = looseIndexOf target._m = renderStatic target._f = resolveFilter target._k = checkKeyCodes target._b = bindObjectProps target._v = createTextVNode target._e = createEmptyVNode target._u = resolveScopedSlots target._g = bindObjectListeners }Copy the code
Check out a few of them at 🌰 :
<div id="app">
<Child a="hello vue" @click="handleClick"></Child>
<ul>
<li v-for="item of list" :key="item.id">
{{ item.name }}
</li>
</ul>
</div>
<script>
let Child = Vue.extend({
name: 'Child'.props: {
a: String
},
template: `
{{ a }}
`
})
new Vue({
el: '#app'.components: {
Child
},
data() {
return {
list: [{
name: 'A'.id: 'A'
}, {
name: 'B'.id: 'B'
}, {
name: 'C'.id: 'C'
}, {
name: 'D'.id: 'D'}}; },methods: {
handleClick () {
console.log('click event'); }}})</script>
Copy the code
🌰 generates the following render function:
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('child', {
attrs: {
"a": "hello vue"
},
on: {
"click": handleClick
}
}), _v(""), _c('ul', _l((list), function(item) {
return _c('li', {
key: item.id
}, [_v("\n " + _s(item.name) + "\n ")])}))],1)}Copy the code
RenderList is defined in SRC /core/instance/render-helpers/render-list.js:
export function renderList (val: any, render: ( val: any, keyOrIndex: string | number, index? : number ) => VNode): ?Array<VNode> {
let ret: ?Array<VNode>, i, l, keys, key
/ / array
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length)
// Iterate through the group to generate VNode
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i)
}
// A single number can be traversed
} else if (typeof val === 'number') {
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
// Iterate over the object
} else if (isObject(val)) {
keys = Object.keys(val)
ret = new Array(keys.length)
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i]
// The first argument is the value, the second argument is the object key, and the third argument is the array position
ret[i] = render(val[key], key, i)
}
}
if (isDef(ret)) {
(ret: any)._isVList = true
}
return ret
}
Copy the code
Render (renderList) {render (renderList);
function (item) {
return _c('li', { key: item.id }, [
_v("\n " + _s(item.name) + "\n ")])}Copy the code
If you look at it from the inside out, first of all, _s is simpler, which is toString function:
export function toString (val: any) :string {
return val == null
? ' '
: typeof val === 'object'
? JSON.stringify(val, null.2)
: String(val)
}
Copy the code
And then _v, which corresponds to createTextVNode, which is defined in core/vdom/vnode:
/ * * *@file Virtual node definition */
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodesfnOptions: ? ComponentOptions;// for SSR cachingfnScopeId: ? string;// functional scope id support
constructor (tag? : string,/ / tag namedata? : VNodeData,/ / data
children?: ?Array<VNode>, / / child nodestext? : string,/ / textelm? : Node, context? : Component, componentOptions? : VNodeComponentOptions, asyncFactory? :Function
) {
/ / tag name
this.tag = tag
// The corresponding object contains specific data information
this.data = data
/ / child nodes
this.children = children
this.text = text
// The actual DOM
this.elm = elm
// Namespace
this.ns = undefined
this.context = context
// Function component scope
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
// Whether the tag is native HTML or plain text
this.raw = false
// Mark the static node
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
/** * Get instance */
get child (): Component | void {
return this.componentInstance
}
}
/** * Create text VNode */
export function createTextVNode (val: string | number) {
return new VNode(undefined.undefined.undefined.String(val))
}
Copy the code
Then we can see more complex _c createElement method function, defined in SRC/core/instance/render. Js:
// Write the template call
vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false);
// Hand-write the render function call
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
Copy the code
CreateElement is located in SRC /core/vdom/create-element.js:
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
// If data is an array or a base type, the children parameter is passed, and it needs to be shifted back one bit
// The number of parameters is uniform processing
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// Render: true; // render: true
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
Copy the code
The function determines whether data is an array or a primitive type, and moves the argument back one bit if it is. When we write our own render function, we can write children as the second parameter if there is no propertyrelated configuration, for example 🌰 :
/ * * *@demo Write 🌰 */ for the render function
new Vue({
el: '#app',
render (h) {
return h('div', [
h('span'.'hello vue! ')]); }})Copy the code
H (‘span’, ‘hello vue! ‘) the second argument is neither an array nor a base type, and the argument is moved back one bit, so _createElement(context, ‘span’, undefined, ‘Hello vue! ‘, 2).
After the arguments are processed, _createElement is called:
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
/** * if the data parameter is passed and the __ob__ of the data has been defined (meaning that the data has been bound to the Oberver object), * https://cn.vuejs.org/v2/guide/render-function.html# constraint * that * / create an empty node
if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
`Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render! ',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
// Use
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
// If there is no tag name, create an empty node
if(! tag) {// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if(process.env.NODE_ENV ! = ='production'&& isDef(data) && isDef(data.key) && ! isPrimitive(data.key) ) {if(! __WEEX__ || ! ('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
// Default scope slot
if (Array.isArray(children) &&
typeof children[0= = ='function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// To normalize the subcomponent parameters, the hand-written render function will enter the first branch
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
// Get the namespace
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// Check whether the tag is reserved
if (config.isReservedTag(tag)) {
// platform built-in elements
// If yes, create the corresponding node
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// Look for the tag in the COMPONENTS of the VM instance's Option. If it exists, it is a component and the corresponding node is created. Ctor is the component's constructor class
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
/* Unknown elements, checked at run time, because the parent component may allocate a namespace */ when the child component is sequenced
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
} else {
// direct component options / constructor
// Create the component
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
// If there is a namespace, the namespace is recursively applied to all child nodes
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
// If the vnode is not created successfully, create an empty vnode
return createEmptyVNode()
}
}
Copy the code
Next look at the handwritten render 🌰, which will execute to normalizeChildren:
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
export function normalizeChildren (children: any): ?Array<VNode> {
// Create a text VNode if it is a basic child, otherwise call normalizeArrayChildren processing
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
function normalizeArrayChildren (children: any, nestedIndex? : string) :Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// Node c is an array, called recursively
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ' '}_${i}`)
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
// If node C is the base type, convert it to VNode using the createTextVNode method
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if(c ! = =' ') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__ `
}
res.push(c)
}
}
}
return res
}
Copy the code
The normalizeArrayChildren function handles three cases:
- The child
c
It’s an array. It’s a recursive callnormalizeArrayChildren
; - The child
c
Is a normal type and creates the textVNode
Processing; - The child
c
Is alreadyVNode
Type: Here are two more cases if the childchildren
If it’s a nested array, it will be defined automaticallykey
Otherwise, create the textVNode
.
In all three cases, the isTextNode is used to determine that if the two nodes are text vnodes, the two nodes will be merged. And it returns an array of VNodes.
Go back to the _c function in the first 🌰 to analyze the component and retain the label VNode generated. Look at the component first:
_c('child', {
attrs: {
"a": "hello vue"
},
on: {
"click": handleClick
}
})
Copy the code
IsDef (Ctor = resolveAsset(context.$options, ‘components’, tag)) The value of context.$options in 🌰 will print:
! [] (/ Users/apple/Documents/vue source series / / cafe chat patch with diff/context. The $options. JPG)
Is this the options in the app when performing a _init assignment, this part of logic is located in the SRC/core/instance/init. Js:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
Copy the code
Then look at the resolveAsset function, which is located in SRC /core/util/options.js:
export function resolveAsset (
options: Object, type: string, id: string, warnMissing? : boolean) :any {
/* istanbul ignore if */
if (typeofid ! = ='string') {
return
}
const assets = options[type]
// check local registration variations first
// Start with id
if (hasOwn(assets, id)) return assets[id]
// Change the id to a hump
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
// Change the first letter to a capital form on the basis of the hump
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if(process.env.NODE_ENV ! = ='production'&& warnMissing && ! res) { warn('Failed to resolve ' + type.slice(0, -1) + ':' + id,
options
)
}
return res
}
Copy the code
ResolveAsset goes through various transformations (hump, capital hump) and finds the definition of the component. Then create the component VNode through createComponent:
export function createComponent (
// The component constructor
Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// Construct the subcomponent constructor, defined at initGlobalAPI time
const baseCtor = context.$options._base // => Vue
// plain options object: turn it into a constructor
// Export default {... Export default vue.extend ({}) and export default {} are both valid
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
// If Ctor is not a constructor at this stage or an asynchronous component factory returns directly
if (typeofCtor ! = ='function') {
if(process.env.NODE_ENV ! = ='production') {
warn(`Invalid Component definition: The ${String(Ctor)}`, context)
}
return
}
/ /... Omit asynchronous component factories
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
// Extract the props attribute from VNodeData based on the options definition of the component
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
// Functional component, stateless, no instance
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
// Install the component hook function
// Add componentVNodeHooks to data.hook, and execute the hook functions when VNode executes patch
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
// Instantiate vNode
// The VNode structure of a component is not passed to children
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
data, undefined.undefined.undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Omit the logic related to WEEX
return vnode
}
Copy the code
For the Child component in 🌰, get and process the constructor, process the VNode properties, install the component hook, and finally generate the component VNode:
After looking at the component VNode, take a look at HTML and SVG (including SVG here at ⚠️!). .
In the case of retaining the tag, this article uses the Li tag as 🌰. Child-like procedures are not stated, but the following branch will be executed at _createElement:
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
);
}
Copy the code
VNode generated by li in 🌰 :
conclusion
When _render is executed, various dwarf functions are pulled up to generate vNodes. We focused on _l and _c. _c is divided into two cases: template and render. After processing the arguments, call _createElement. This function does two things: regulate the child elements and generate VNode. Vnodes can be generated in two different ways: built-in labels and components. The component VNode is generated by createComponent, which does three things:
Ctor
-> create constructor;installComponentHooks
-> install component hooks;new VNode
-> Instantiate the componentVNode
;
There are also many branch processes in the code, such as asynchronous factory functions, processing of dynamic components, and so on. These are not the main processes, you can run into problems and look back (lazy 😄). With VNode in hand, the next section details the update process — VNode generates the DOM and the interview required question Diff.