Render to VNode: Render to VNode: render to VNode As we know, Vue has the ability to cross multiple terminals, provided that VNode (JavaScript objects) is used. If you have “compile 🔨”, you can do whatever you want. In this article, I’ll look at VNode’s DOM generation process for the Web platform. By the end of this article, you will have learned:
- Ordinary nodal
patch
The process of rendering; - Component node
patch
The process of rendering; - A little trick (hidden in the article 🤭🤭🤭);
Patch of a common node
Normal nodes analyze the rendering process using a div 🌰 :
<div id="app">
</div>
<script>
new Vue({
el: '#app'.name: 'App',
render (h) {
return h('div', {
id: 'foo'
}, 'Hello, patch')}})</script>
Copy the code
🌰 after _render, get VNode as shown in the following figure:
Then we call _update to generate the DOM:
updateComponent = function () {
// vm._render() generates the virtual node
// Call vm._update to update the DOM
vm._update(vm._render(), hydrating);
};
Copy the code
Vm._update is defined in init lifecycleMixin (this part of the code is in SRC \core\instance\lifecycle. Js) :
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
const vm: Component = this
// Update parameters
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
// Cache virtual nodes in _vnode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// The first render did not compare VNode, null here
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
Copy the code
The main logic is vm.__patch__, which is the core of being able to accommodate multiple applications! A global search for vue.prototype. __patch__ shows several definitions:
This article only looks at the logic on the Web side:
// src\platforms\web\runtime\index.js
// This function is null to determine whether it is in the browser environment. Server rendering does not need to render to the DOM, so it is null
Vue.prototype.__patch__ = inBrowser ? patch : noop
// src\platforms\web\runtime\patch.js
/ * * *@param {Object} -nodeOps encapsulates a set of DOM manipulation methods *@param {Object} -modules defines the implementation of some module hook functions */
export const patch: Function = createPatchFunction({ nodeOps, modules })
// src\core\vdom\patch.js
export function createPatchFunction (backend) {
/ /... 😎 Wear sunglasses and don't look at these hundreds of lines of code
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
return vnode.elm
}
}
Copy the code
With hundreds of lines of code removed, we can clearly understand the patch acquisition process:
- Clients are distinguished by directories.
web
的DOM
Operations and hooks are all locatedsrc\platforms\web
.weex
The render functions and hooks are all located insrc\platforms\weex
. - By calling at
src\core\vdom\patch.js
Under thecreatePatchFunction
generatepatch
Function. Our application will definitely call the render function repeatedly, throughThe technique of curryingThe difference of the platform is smoothed once, and then called each timepatch
There is no need to iterate over and over to get manipulation functions for the platform (❗❗❗ tip).
After retrieving the patch function, let’s look at the rendering process:
// This is null for the first rendering, and is not null when the data changes and is rerendered
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
Copy the code
Vm. __Patch__ is the return value of createPatchFunction:
/** * render function *@param {VNode} OldVnode oldVnode *@param {VNode} Vnode Indicates the current Vnode node@param {Boolean} Hydrating Specifies whether to render on the server@param {Boolean} RemoveOnly this is used by transition-group components. */ is not involved
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// No vnode is generated after _render, and the old node, if any, is destroyed
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// There is no oldNode for the first render
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// The component was updated with oldNode, so perform sameVnode for comparison
if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
/ /... Eliminating server-side rendering logic
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// Convert the real DOM to VNode
// 🌰 is the DOM id = app
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
// Mount the node, 🌰 is the id = app DOM
const oldElm = oldVnode.elm
// Mount the parent node of the node, with body in 🌰
const parentElm = nodeOps.parentNode(oldElm)
// Create a new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
// 🌰 returns a newline
nodeOps.nextSibling(oldElm)
)
// ...
// Destroy the old node. In 🌰 parentElm is the body element
if (isDef(parentElm)) {
// oldVnode is a div with id = app
removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// Perform the insert hook
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
Copy the code
CreateElement creates a new DOM via VNode and inserts it into its parent node. Is the main process of rendering:
// Create a real DOM from the virtual node and insert it into its parent node
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
// ...
// 🌰 {id: 'foo'}
const data = vnode.data
// 🌰 is a text node
const children = vnode.children
// in 🌰 is div
const tag = vnode.tag
/ /... Omit to determine whether the label is valid
// Call a platform DOM operation to create a placeholder element
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// Scoped style processing, not involved here
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
/ /... Omit the WEEX code
} else {
// Create a child node
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// Execute all create hooks and push vnode into insertedVnodeQueue
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// Insert the DOM into the parent node. Since this is a deep recursive call, insert the DOM first after the child
insert(parentElm, vnode.elm, refElm)
}
if(process.env.NODE_ENV ! = ='production' && data && data.pre) {
creatingElmInVPre--
}
// Comment node
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// Text node
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
Copy the code
Create a placeholder element with nodeops.createElement:
export function createElement (tagName: string, vnode: VNode) :Element {
// create a div element in 🌰
const elm = document.createElement(tagName)
// return div instead of select
if(tagName ! = ='select') {
return elm
}
// false or null will remove the attribute but undefined will not
if(vnode.data && vnode.data.attrs && vnode.data.attrs.multiple ! = =undefined) {
elm.setAttribute('multiple'.'multiple')}return elm
}
Copy the code
The createChildren depth iterates recursively to create child nodes:
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
// Check for duplicate keys, if any
if(process.env.NODE_ENV ! = ='production') {
checkDuplicateKeys(children)
}
// Iterate over the child virtual nodes, recursively calling createElm
for (let i = 0; i < children.length; ++i) {
// Vnode. elm will be used as a DOM node placeholder for the parent container during traversal
createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
Copy the code
InvokeCreateHooks execute all createXX hooks:
// Execute all create hooks and push vnode into insertedVnodeQueue
invokeCreateHooks(vnode, insertedVnodeQueue)
function invokeCreateHooks (vnode, insertedVnodeQueue) {
// This function is defined under SRC \platforms\web\runtime\modules
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode "i")
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
// Create and insert hooks
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
Copy the code
Finally, insert into the DOM by calling insert:
// Insert the DOM into the parent node. Since this is a deep recursive call, insert the DOM first after the child
insert(parentElm, vnode.elm, refElm)
/** * dom insert function *@param {*} parent- Parent node *@param {*} elm- Child node *@param {*} ref* /
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (ref.parentNode === parent) {
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/insertBefore
nodeOps.insertBefore(parent, elm, ref)
}
} else {
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
nodeOps.appendChild(parent, elm)
}
}
}
Copy the code
After performing the insert, the DOM of the page changes once, as shown in the figure below in 🌰 :
Patch removes the placeholder node and performs the destroy and insert hook functions:
// Destroy the old node. In 🌰 parentElm is the body element
if (isDef(parentElm)) {
// oldVnode is a div with id = app
removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
Copy the code
summary
At this point, the initial rendering process of ordinary nodes is analyzed. The patch process completes rendering by creating nodes, inserting nodes recursively, and finally destroying placeholder nodes. The actions in the procedure are all calls to the DOM native API. Component nodes are also placeholder nodes. Let’s analyze the rendering process of components.
Components of the patch
🌰 in this section transfers the above logic to the Child component:
<div id="app">
</div>
<script>
const Child = {
render(h) {
return h('div', {
id: 'foo'.staticStyle: {
color: 'red',},style: [{
fontWeight: 600}},'component patch')}}new Vue({
el: '#app'.components: {
Child
},
name: 'App'.render(h) {
return h(Child)
}
})
</script>
Copy the code
Component rendering returns true when executing the following logic on createElm:
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
/ / keep alive - components
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// There is an installComponentHooks logic in the generate VNode section that generates components
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)}// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true}}}Copy the code
I (vnode, false /* hydrating */)
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ? boolean {// The keepalive logic is not concerned in this article
if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// Create a Vue instance
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// Call the $mount method to mount the child component
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// ...
}
/** * Create virtual node component instances *@param {*} vnode
* @param {*} parent
*/
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
) :Component {
// Construct the component parameters
const options: InternalComponentOptions = {
_isComponent: true._parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// Subcomponent constructor
return new vnode.componentOptions.Ctor(options)
}
Copy the code
New to vnode.com ponentOptions. Ctor (options) will be performed to _init logic, but the different is the following logic will be performed:
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
}
// src\core\instance\init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
$options = object.create (sub.options) $options = object.create (sub.options)
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
Copy the code
The initInternalComponent is also simple, with parameters merged into $options. $mount(hydrating? Vnode.elm: undefined, hydrating) mount logic, where “hydrating” is false because it is not rendered on the server, equivalent to child. mount(undefined, false) Run mount -> mountComponent -> updateComponent -> vm._render-> vm._update. When you go to vm._update, the process is the same as the normal node process.
Another difference in this section is the addition of staticStyle and style to the Child component. We know that these styles will eventually render as strings on the DOM. The render results for 🌰 are as follows:
<div style="color: red; font-weight: 600;">component patch</div>
Copy the code
When you execute the invokeCreateHooks, the updateStyle hook is executed:
function updateStyle (oldVnode: VNodeWithData, vnode: VNodeWithData) {
const data = vnode.data
const oldData = oldVnode.data
if (isUndef(data.staticStyle) && isUndef(data.style) &&
isUndef(oldData.staticStyle) && isUndef(oldData.style)
) {
return
}
let cur, name
const el: any = vnode.elm
When first rendered in 🌰, these oldXX are empty objects
const oldStaticStyle: any = oldData.staticStyle
const oldStyleBinding: any = oldData.normalizedStyle || oldData.style || {}
// If the static style exists, then the style binding has been merged into it when normalizeStyleData is executed
const oldStyle = oldStaticStyle || oldStyleBinding
// Formalize the dynamic style and convert it to {key: value}
const style = normalizeStyleBinding(vnode.data.style) || {}
// store normalized style under a different key for next diff
// make sure to clone it if it's reactive, since the user likely wants
// to mutate it.
// Cache the style result to data.normalizedStyle
vnode.data.normalizedStyle = isDef(style.__ob__)
? extend({}, style)
: style
// Concatenate all style properties, including staticStyle and style
const newStyle = getStyle(vnode, true)
for (name in oldStyle) {
if (isUndef(newStyle[name])) {
setProp(el, name, ' ')}}// After smoothing out the data structure differences, iterate over the set to the DOM
for (name in newStyle) {
cur = newStyle[name]
if(cur ! == oldStyle[name]) {// ie9 setting to null has no effect, must use empty string
setProp(el, name, cur == null ? ' ' : cur)
}
}
}
Copy the code
To set a STYLE for a DOM, there are setProperty and style[property] methods. When we write components, there are many ways to write them. Array objects, strings, objects are supported! There is a framework design concept involved here: the design of the application layer. When designing a tool or component, design it from the top down — think about how to use it first, that is, how to design the application layer first, and then how to connect it to the underlying layer (style.setProperty and style.csspropertyName in 🌰). Starting from the bottom (fixed, limited features) tends to limit the imagination and thus reduce the ease of use of the framework. (❗❗❗ Tips)
summary
This section analyzes the patch process of the component: Unlike normal nodes, the component returns true when it executes createComponent on createElm. In createComponent, _init is re-executed, and then the component’s mount logic is executed (mount -> mountComponent -> updateComponent -> vm._render-> vm._update).
conclusion
Finally, summarize this article with a picture:
After VNode is obtained through vm._render, vm._update is executed to start rendering. Different clients have different patches. The difference is smoothed at one time by using the Curlization technique in the createPatchFunction. For normal elements, an element is created, and then invokeCreateHooks are executed to handle attributes like style, class, attrs, and so on. This part involves the framework design idea – layering, from the application layer, to make the framework easier to use. Finally, remove the placeholder node and insert the hook. For component nodes, createComponent will return true when executed to createElm, and will execute init hooks that were installed when the component VNode was created, thus executing the component’s constructor. Init -> $mount -> mountComponent -> updateComponent -> vm._render -> vm._update stream
When the data changes, a VNode update is triggered, and the VNode diff is executed and re-rendered, which will be analyzed in the next article. Pay attention to the code farmer xiaoyu, work together, harvest together!