preface
From the template string to the render function was done in the compiler for the handwritten Vue2 series in the previous article. Once we’ve got the render function, it’s time to start the actual mount phase:
Mount -> instantiate render Watcher -> execute updateComponent method -> execute render function to generate VNode -> execute patch for first render -> recursively traverse VNode Create each node and handle the general properties and instructions on the node -> create the component instance if the node is a custom component -> initialize and mount the component -> Finally all vNodes become real DOM nodes and replace the template content on the page -> complete the initial rendering
The target
Therefore, the goal of this article is to achieve the entire completion described above and complete the initial rendering. The whole process involves the following knowledge points:
-
render helper
-
VNode
-
Patch initial rendering
-
Instruction (V-model, V-bind, V-ON) processing
-
Instantiate child components
-
Processing of slots
implementation
Then we will formally enter the code implementation process, step by step to achieve all of the above content, complete the initial rendering of the page.
mount
/src/compiler/index.js
/** * compiler */
export default function mount(vm) {
if(! vm.$options.render) {// If no render option is provided, the render function is compiled
// ...
}
mountComponent(vm)
}
Copy the code
mountComponent
/src/compiler/mountComponent.js
/ * * *@param {*} Vm Vue instance */
export default function mountComponent(vm) {
// Update the component's function
const updateComponent = () = > {
vm._update(vm._render())
}
// Instantiate a render Watcher that will be executed when reactive data is updated
new Watcher(updateComponent)
}
Copy the code
vm._render
/src/compiler/mountComponent.js
$options. Render function */
Vue.prototype._render = function () {
// Bind this context to the render function as a Vue instance
return this.$options.render.apply(this)}Copy the code
render helper
/src/compiler/renderHelper.js
/** * Install the run-time render helper functions on the Vue instance, such as _c and _v, which generate Vnode *@param {VueContructor} Target Vue instance */
export default function renderHelper(target) {
target._c = createElement
target._v = createTextNode
}
Copy the code
createElement
/src/compiler/renderHelper.js
/** * Create Vnode * based on the label information@param {string} Tag Indicates the tag name *@param {Map} Attr tag attributes Map object *@param {Array<Render>} Render function */ for all children
function createElement(tag, attr, children) {
return VNode(tag, attr, children, this)}Copy the code
createTextNode
/src/compiler/renderHelper.js
/** * Generate VNode * for the text node@param {*} TextAst The AST object of the text node */
function createTextNode(textAst) {
return VNode(null.null.null.this, textAst)
}
Copy the code
VNode
/src/compiler/vnode.js
/**
* VNode
* @param {*} Tag Indicates the tag name *@param {*} Attr Attributes Map object *@param {*} Children VNode * composed of children@param {*} Text Ast object * of the text node@param {*} Context Vue instance *@returns VNode* /
export default function VNode(tag, attr, children, context, text = null) {
return {
/ / label
tag,
// Attribute Map object
attr,
/ / the parent node
parent: null.// An array of vNodes
children,
// Ast object of the text node
text,
// The real node of the Vnode
elm: null./ / the Vue instance
context
}
}
Copy the code
vm._update
/src/compiler/mountComponent.js
Vue.prototype._update = function (vnode) {
/ / old VNode
const prevVNode = this._vnode
/ / new VNode
this._vnode = vnode
if(! prevVNode) {// If the old VNode does not exist, the root component is rendered for the first time
this.$el = this.__patch__(this.$el, vnode)
} else {
// Subsequent updates to the component or the first rendering of the subcomponent go here
this.$el = this.__patch__(prevVNode, vnode)
}
}
Copy the code
Install __Patch__ and Render Helper
/src/index.js
/** * Initializes the configuration object *@param {*} options
*/
Vue.prototype._init = function (options) {
// ...
initData(this)
// Install the renderer function at runtime
renderHelper(this)
// Install patch on the instance
this.__patch__ = patch
// If an EL configuration item exists, the $mount method is called to compile the template
if (this.$options.el) {
this.$mount()
}
}
Copy the code
patch
/src/compiler/patch.js
/** * Entry for initial rendering and subsequent updates *@param {VNode} OldVnode oldVnode *@param {VNode} Vnode New vnode *@returns The actual DOM node of VNode */
export default function patch(oldVnode, vnode) {
if(oldVnode && ! vnode) {// If the old node exists and the new node does not exist, the component is destroyed
return
}
if(! oldVnode) {// oldVnode does not exist, indicating that the child is rendering for the first time
createElm(vnode)
} else {
if (oldVnode.nodeType) { // real node, which represents the first rendering of the root component
// The parent node, body
const parent = oldVnode.parentNode
// The reference node is the next node of the old vnode -- script. The new node is inserted before the script
const referNode = oldVnode.nextSibling
// Create the element
createElm(vnode, parent, referNode)
// Remove the old vNode
parent.removeChild(oldVnode)
} else {
console.log('update')}}return vnode.elm
}
Copy the code
createElm
/src/compiler/patch.js
/** * create element *@param {*} vnode VNode
* @param {*} Parent Parent node of a VNode, real node *@returns * /
function createElm(vnode, parent, referNode) {
// Record the parent node of the node
vnode.parent = parent
// Create a custom component. If it is not a component, the process continues
if (createComponent(vnode)) return
const { attr, children, text } = vnode
if (text) { // Text node
// Create a text node and insert it into the parent node
vnode.elm = createTextNode(vnode)
} else { // Element node
// Create the element and record the corresponding DOM node on the vNode
vnode.elm = document.createElement(vnode.tag)
// Set attributes for the element
setAttribute(attr, vnode)
// Create child nodes recursively
for (let i = 0, len = children.length; i < len; i++) {
createElm(children[i], vnode.elm)
}
}
// If parent exists, insert the created node into the parent node
if (parent) {
const elm = vnode.elm
if (referNode) {
parent.insertBefore(elm, referNode)
} else {
parent.appendChild(elm)
}
}
}
Copy the code
createTextNode
/src/compiler/patch.js
/** * Create text node *@param {*} TextVNode VNode */ of a text node
function createTextNode(textVNode) {
let { text } = textVNode, textNode = null
if (text.expression) {
// There is an expression whose value is a reactive data
const value = textVNode.context[text.expression]
textNode = document.createTextNode(typeof value === 'object' ? JSON.stringify(value) : String(value))
} else {
/ / plain text
textNode = document.createTextNode(text.text)
}
return textNode
}
Copy the code
setAttribute
/src/compiler/patch.js
/** * Sets the node attribute *@param {*} Attr Attributes Map object *@param {*} vnode* /
function setAttribute(attr, vnode) {
// If it is a common attribute, it is set directly, if it is a directive, it is special
for (let name in attr) {
if (name === 'vModel') {
/ / v - model instruction
const { tag, value } = attr.vModel
setVModel(tag, value, vnode)
} else if (name === 'vBind') {
/ / v - bind instruction
setVBind(vnode)
} else if (name === 'vOn') {
/ / v - on command
setVOn(vnode)
} else {
// Common attributes
vnode.elm.setAttribute(name, attr[name])
}
}
}
Copy the code
setVModel
/src/compiler/patch.js
/** * The principle of v-model *@param {*} Tag Specifies the tag name of the node@param {*} Value Indicates the attribute value *@param {*} * / node node
function setVModel(tag, value, vnode) {
const { context: vm, elm } = vnode
if (tag === 'select') {
Promise.resolve().then(() = > {
// Use promise to delay setting
// Because the option element has not been created yet
elm.value = vm[value]
})
elm.addEventListener('change'.function () {
vm[value] = elm.value
})
} else if (tag === 'input' && vnode.elm.type === 'text') {
elm.value = vm[value]
elm.addEventListener('input'.function () {
vm[value] = elm.value
})
} else if (tag === 'input' && vnode.elm.type === 'checkbox') {
elm.checked = vm[value]
elm.addEventListener('change'.function () {
vm[value] = elm.checked
})
}
}
Copy the code
setVBind
/src/compiler/patch.js
/** * the v-bind principle *@param {*} vnode* /
function setVBind(vnode) {
const { attr: { vBind }, elm, context: vm } = vnode
for (let attrName in vBind) {
elm.setAttribute(attrName, vm[vBind[attrName]])
elm.removeAttribute(`v-bind:${attrName}`)}}Copy the code
setVOn
/src/compiler/patch.js
/** * V-ON principle *@param {*} vnode
*/
function setVOn(vnode) {
const { attr: { vOn }, elm, context: vm } = vnode
for (let eventName in vOn) {
elm.addEventListener(eventName, function (. args) {
vm.$options.methods[vOn[eventName]].apply(vm, args)
})
}
}
Copy the code
createComponent
/src/compiler/patch.js
/** * Create a custom component@param {*} vnode* /
function createComponent(vnode) {
if(vnode.tag && ! isReserveTag(vnode.tag)) {// If the node is not reserved, it is a component
// Get component configuration information
const { tag, context: { $options: { components } } } = vnode
const compOptions = components[tag]
const compIns = new Vue(compOptions)
// Place the parent component's VNode on the child component's instance
compIns._parentVnode = vnode
// Mount child components
compIns.$mount()
// Records information about the parent node of the child vNode
compIns._vnode.parent = vnode.parent
// Add the child component to the parent node
vnode.parent.appendChild(compIns._vnode.elm)
return true}}Copy the code
isReserveTag
/src/utils.js
/** * Whether to reserve nodes for the platform */
export function isReserveTag(tagName) {
const reserveTag = ['div'.'h3'.'span'.'input'.'select'.'option'.'p'.'button'.'template']
return reserveTag.includes(tagName)
}
Copy the code
Principle of slot
The following example is a common way to slot. The principle of slot is actually very simple, just a little bit of trouble to implement.
-
parsing
If the component tag has child nodes, when you parse it, you parse those child nodes into a particular data structure that contains all the information about the slot, and then you put that data structure on the properties of the parent node, basically finding a place to put that information, and then pulling it out when you use it in the renderSlot. Of course this parsing takes place in the parent component’s parsing.
-
Generate render function
In the render function phase of generating a child component, if a slot tag is encountered, a _t render function is returned, which takes two arguments: a JSON string of attributes and a children array of render functions for all the children of the slot tag.
-
render helper
When executing the render function of the child component, if it reaches vm._t, the renderSlot method is called, which returns the vNodes of the slot and then enters the patch phase of the child component, turning these VNodes into real DOM and rendering them to the page.
So that’s how slots work, and then when it’s implemented, it’s a little bit convoluted in some places, more or less because there’s a problem with the overall architecture, so there’s some tinkering in there, which you can think of as a bit of business code to implement slots. You just have to hold the essence of the slot.
The sample
<! -- comp -->
<template>
<div>
<div>
<slot name="slot1">
<span>Slot Default content</span>
</slot>
</div>
<slot name="slot2" v-bind:test="xx">
<span>Slot Default content</span>
</slot>
<div>
</div>
</div>
</template>
Copy the code
<comp></comp>
Copy the code
<comp>
<template v-slot:slot2="xx">
<div>Scoped slots, through which content is passed from parent to child components</div>
</template>
<comp>
Copy the code
parse
/src/compiler/parse.js
function processElement() {
// ...
// Process the slot contents
processSlotContent(curEle)
// After the node is processed, make it relate to its parent node
if (stackLen) {
stack[stackLen - 1].children.push(curEle)
curEle.parent = stack[stackLen - 1]
// If the node has slotName, it is the content passed by the component to the slot
// Place the slot information on the rawattr. scopedSlots object of the component node
// This information is used to generate the VNode of the component slot (renderSlot)
if (curEle.slotName) {
const { parent, slotName, scopeSlot, children } = curEle
// The children operation here is simply to avoid the cyclic reference problem of json.stringify
// Because the render function needs to execute the json.stringify method on attr
const slotInfo = {
slotName, scopeSlot, children: children.map(item= > {
delete item.parent
return item
})
}
if (parent.rawAttr.scopedSlots) {
parent.rawAttr.scopedSlots[curEle.slotName] = slotInfo
} else {
parent.rawAttr.scopedSlots = { [curEle.slotName]: slotInfo }
}
}
}
}
Copy the code
processSlotContent
/src/compiler/parse.js
/** * processing slot * <scope-slot> * <template V-slot :default="scopeSlot"> * <div>{{scopeSlot}}</div> * </template> * </scope-slot> *@param { AST } AST object */ of the EL node
function processSlotContent(el) {
// Note that the template with the V-slot :xx attribute can only be the root element of the component
if (el.tag === 'template') { // Get slot information
// Attribute map object
const attrMap = el.rawAttr
// Traverse the attribute map to find the v-slot directive information in it
for (let key in attrMap) {
if (key.match(/v-slot:(.*)/)) { // Indicates the v-slot directive on the template tag
// Get the slot name and value after the instruction, for example: V-slot :default=xx
// default
const slotName = el.slotName = RegExp$1.// xx
el.scopeSlot = attrMap[`v-slot:${slotName}`]
// Return directly, since only one V-slot instruction is possible on this tag
return}}}}Copy the code
generate
/src/compiler/generate.js
/** * Parse ast to generate render function *@param {*} Ast Syntax tree *@returns {string} Render function in string form */
function genElement(ast) {
// ...
// Process the child nodes to get an array of all the child node rendering functions
const children = genChildren(ast)
if (tag === 'slot') {
// Generate a slot handler
return `_t(The ${JSON.stringify(attrs)}[${children}]) `
}
// Generate an executable method for VNode
return `_c('${tag}', The ${JSON.stringify(attrs)}[${children}]) `
}
Copy the code
renderHelper
/src/compiler/renderHelper.js
/** * Install the run-time render helper functions on the Vue instance, such as _c and _v, which generate Vnode *@param {VueContructor} Target Vue instance */
export default function renderHelper(target) {
// ...
target._t = renderSlot
}
Copy the code
renderSlot
/src/compiler/renderHelper.js
/** * The principle of slot is actually very simple, the difficulty is to implement * its principle is to generate VNode, the difficulty is to generate VNode before the various parsing, that is, data preparation stage * generate slot VNode *@param {*} Attrs Slot attribute *@param {*} An array of ast's for all the children of the children slot */
function renderSlot(attrs, children) {
// AttR information of the parent VNode
const parentAttr = this._parentVnode.attr
let vnode = null
if (parentAttr.scopedSlots) { // Indicates that content is passed to the slot of the current component
// Get slot information
const slotName = attrs.name
const slotInfo = parentAttr.scopedSlots[slotName]
// The logic here is a bit convoluted. It is recommended to open debugging and check the data structure to clarify the corresponding ideas
// The logic here is completely to implement the function of the slot, and the principle of the slot itself has nothing to do with
this[slotInfo.scopeSlot] = this[Object.keys(attrs.vBind)[0]]
vnode = genVNode(slotInfo.children, this)}else { // Slot default content
// Turn children into a vNode array
vnode = genVNode(children, this)}// If the children length is 1, the slot has only one child node
if (children.length === 1) return vnode[0]
return createElement.call(this.'div', {}, vnode)
}
Copy the code
genVNode
/src/compiler/renderHelper.js
/** * Convert a batch of AST nodes (arrays) to vNode arrays *@param {Array<Ast>} Childs node array *@param {*} Vm component instance *@returns * / vnode array
function genVNode(childs, vm) {
const vnode = []
for (let i = 0, len = childs.length; i < len; i++) {
const { tag, attr, children, text } = childs[i]
if (text) { // Text node
if (typeof text === 'string') { // Text is a string
// Construct the AST object of the text node
const textAst = {
type: 3,
text,
}
if (text.match(/ / {{(. *)}})) {
// The description is an expression
textAst.expression = RegExp.$1.trim()
}
vnode.push(createTextNode.call(vm, textAst))
} else { // text is the AST object of the text node
vnode.push(createTextNode.call(vm, text))
}
} else { // Element node
vnode.push(createElement.call(vm, tag, attr, genVNode(children, vm)))
}
}
return vnode
}
Copy the code
The results of
Ok, at this point, the initial rendering of the template is complete, and if you can see the image below, everything is fine. Because the whole process involves a lot of content, if you feel that some places are not clear, I suggest to look again and comb out the whole process carefully.
It can be seen that the original labels, custom components and slots have all been completely rendered on the page. After completing the initial rendering, it is time to implement the subsequent update process, that is, the patch of the next handwritten Vue2 series — DIff.
Focus on
Welcome everyone to pay attention to my nuggets account and B station, if the content to help you, welcome everyone to like, collect + attention
link
-
Proficient in Vue stack source code principles
-
Form a complete set of video
-
Learning exchange group