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