Initial rendering of handwritten Vue2 series

When learning becomes a habit, knowledge becomes common sense. Thank you for your attention , likes , favorites and comments .

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

cover

foreword

In the previous article Handwritten Vue2 Series Compiler Completed the work from template strings to render functions. When we get the render function, it's time to enter the real mount phase:

Mount -> instantiate the rendering Watcher -> execute the updateComponent method -> execute the render function to generate the VNode -> execute the patch for the first rendering -> recursively traverse the VNode to create each node and process the common attributes and instructions on the node -> if the node is self To define a component, create a component instance -> initialize and mount the component -> finally all VNodes become real DOM nodes and replace the template content on the page -> complete the initial rendering

Target

Therefore, the goal of this article is to implement the entire process described above and complete the initial rendering. The whole process involves the following knowledge points:

  • render helper

  • VNode

  • patch initial render

  • Processing of instructions (v-model, v-bind, v-on)

  • Instantiate child components

  • Handling of slots

accomplish

Next, we will officially enter the code implementation process, implement all the above content step by step, and complete the initial rendering of the page.

mount

/src/compiler/index.js

/**
 * 编译器
 */
export default function mount(vm) {
  if (!vm.$options.render) { // 没有提供 render 选项,则编译生成 render 函数
    // ...
  }
  mountComponent(vm)
}

mountComponent

/src/compiler/mountComponent.js

/**
 * @param {*} vm Vue 实例
 */
export default function mountComponent(vm) {
  // 更新组件的的函数
  const updateComponent = () => {
    vm._update(vm._render())
  }

  // 实例化一个渲染 Watcher,当响应式数据更新时,这个更新函数会被执行
  new Watcher(updateComponent)
}

vm._render

/src/compiler/mountComponent.js

/**
 * 负责执行 vm.$options.render 函数
 */
Vue.prototype._render = function () {
  // 给 render 函数绑定 this 上下文为 Vue 实例
  return this.$options.render.apply(this)
}

render helper

/src/compiler/renderHelper.js

/**
 * 在 Vue 实例上安装运行时的渲染帮助函数,比如 _c、_v,这些函数会生成 Vnode
 * @param {VueContructor} target Vue 实例
 */
export default function renderHelper(target) {
  target._c = createElement
  target._v = createTextNode
}

createElement

/src/compiler/renderHelper.js

/**
 * 根据标签信息创建 Vnode
 * @param {string} tag 标签名 
 * @param {Map} attr 标签的属性 Map 对象
 * @param {Array<Render>} children 所有的子节点的渲染函数
 */
function createElement(tag, attr, children) {
  return VNode(tag, attr, children, this)
}

createTextNode

/src/compiler/renderHelper.js

/**
 * 生成文本节点的 VNode
 * @param {*} textAst 文本节点的 AST 对象
 */
function createTextNode(textAst) {
  return VNode(null, null, null, this, textAst)
}

VNode

/src/compiler/vnode.js

/**
 * VNode
 * @param {*} tag 标签名
 * @param {*} attr 属性 Map 对象
 * @param {*} children 子节点组成的 VNode
 * @param {*} text 文本节点的 ast 对象
 * @param {*} context Vue 实例
 * @returns VNode
 */
export default function VNode(tag, attr, children, context, text = null) {
  return {
    // 标签
    tag,
    // 属性 Map 对象
    attr,
    // 父节点
    parent: null,
    // 子节点组成的 Vnode 数组
    children,
    // 文本节点的 Ast 对象
    text,
    // Vnode 的真实节点
    elm: null,
    // Vue 实例
    context
  }
}

vm._update

/src/compiler/mountComponent.js

Vue.prototype._update = function (vnode) {
  // 老的 VNode
  const prevVNode = this._vnode
  // 新的 VNode
  this._vnode = vnode
  if (!prevVNode) {
    // 老的 VNode 不存在,则说明时首次渲染根组件
    this.$el = this.__patch__(this.$el, vnode)
  } else {
    // 后续更新组件或者首次渲染子组件,都会走这里
    this.$el = this.__patch__(prevVNode, vnode)
  }
}

Install __patch__, render helper

/src/index.js

/**
 * 初始化配置对象
 * @param {*} options 
 */
Vue.prototype._init = function (options) {
  // ...
  initData(this)
  // 安装运行时的渲染工具函数
  renderHelper(this)
  // 在实例上安装 patch 函数
  this.__patch__ = patch
  // 如果存在 el 配置项,则调用 $mount 方法编译模版
  if (this.$options.el) {
    this.$mount()
  }
}

patch

/src/compiler/patch.js

/**
 * 初始渲染和后续更新的入口
 * @param {VNode} oldVnode 老的 VNode
 * @param {VNode} vnode 新的 VNode
 * @returns VNode 的真实 DOM 节点
 */
export default function patch(oldVnode, vnode) {
  if (oldVnode && !vnode) {
    // 老节点存在,新节点不存在,则销毁组件
    return
  }

  if (!oldVnode) { // oldVnode 不存在,说明是子组件首次渲染
    createElm(vnode)
  } else {
    if (oldVnode.nodeType) { // 真实节点,则表示首次渲染根组件
      // 父节点,即 body
      const parent = oldVnode.parentNode
      // 参考节点,即老的 vnode 的下一个节点 —— script,新节点要插在 script 的前面
      const referNode = oldVnode.nextSibling
      // 创建元素
      createElm(vnode, parent, referNode)
      // 移除老的 vnode
      parent.removeChild(oldVnode)
    } else {
      console.log('update')
    }
  }
  return vnode.elm
}

createElm

/src/compiler/patch.js

/**
 * 创建元素
 * @param {*} vnode VNode
 * @param {*} parent VNode 的父节点,真实节点
 * @returns 
 */
function createElm(vnode, parent, referNode) {
  // 记录节点的父节点
  vnode.parent = parent
  // 创建自定义组件,如果是非组件,则会继续后面的流程
  if (createComponent(vnode)) return

  const { attr, children, text } = vnode
  if (text) { // 文本节点
    // 创建文本节点,并插入到父节点内
    vnode.elm = createTextNode(vnode)
  } else { // 元素节点
    // 创建元素,在 vnode 上记录对应的 dom 节点
    vnode.elm = document.createElement(vnode.tag)
    // 给元素设置属性
    setAttribute(attr, vnode)
    // 递归创建子节点
    for (let i = 0, len = children.length; i < len; i++) {
      createElm(children[i], vnode.elm)
    }
  }
  // 如果存在 parent,则将创建的节点插入到父节点内
  if (parent) {
    const elm = vnode.elm
    if (referNode) {
      parent.insertBefore(elm, referNode)
    } else {
      parent.appendChild(elm)
    }
  }
}

createTextNode

/src/compiler/patch.js

/**
 * 创建文本节点
 * @param {*} textVNode 文本节点的 VNode
 */
function createTextNode(textVNode) {
  let { text } = textVNode, textNode = null
  if (text.expression) {
    // 存在表达式,这个表达式的值是一个响应式数据
    const value = textVNode.context[text.expression]
    textNode = document.createTextNode(typeof value === 'object' ? JSON.stringify(value) : String(value))
  } else {
    // 纯文本
    textNode = document.createTextNode(text.text)
  }
  return textNode
}

setAttribute

/src/compiler/patch.js

/**
 * 给节点设置属性
 * @param {*} attr 属性 Map 对象
 * @param {*} vnode
 */
function setAttribute(attr, vnode) {
  // 遍历属性,如果是普通属性,直接设置,如果是指令,则特殊处理
  for (let name in attr) {
    if (name === 'vModel') {
      // v-model 指令
      const { tag, value } = attr.vModel
      setVModel(tag, value, vnode)
    } else if (name === 'vBind') {
      // v-bind 指令
      setVBind(vnode)
    } else if (name === 'vOn') {
      // v-on 指令
      setVOn(vnode)
    } else {
      // 普通属性
      vnode.elm.setAttribute(name, attr[name])
    }
  }
}

setVModel

/src/compiler/patch.js

/**
 * v-model 的原理
 * @param {*} tag 节点的标签名
 * @param {*} value 属性值
 * @param {*} node 节点
 */
function setVModel(tag, value, vnode) {
  const { context: vm, elm } = vnode
  if (tag === 'select') {
    // 下拉框,<select></select>
    Promise.resolve().then(() => {
      // 利用 promise 延迟设置,直接设置不行,
      // 因为这会儿 option 元素还没创建
      elm.value = vm[value]
    })
    elm.addEventListener('change', function () {
      vm[value] = elm.value
    })
  } else if (tag === 'input' && vnode.elm.type === 'text') {
    // 文本框,<input type="text" />
    elm.value = vm[value]
    elm.addEventListener('input', function () {
      vm[value] = elm.value
    })
  } else if (tag === 'input' && vnode.elm.type === 'checkbox') {
    // 选择框,<input type="checkbox" />
    elm.checked = vm[value]
    elm.addEventListener('change', function () {
      vm[value] = elm.checked
    })
  }
}

setVBind

/src/compiler/patch.js

/**
 * v-bind 原理
 * @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}`)
  }
}

setVOn

/src/compiler/patch.js

/**
 * v-on 原理
 * @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)
    })
  }
}

createComponent

/src/compiler/patch.js

/**
 * 创建自定义组件
 * @param {*} vnode
 */
function createComponent(vnode) {
  if (vnode.tag && !isReserveTag(vnode.tag)) { // 非保留节点,则说明是组件
    // 获取组件配置信息
    const { tag, context: { $options: { components } } } = vnode
    const compOptions = components[tag]
    const compIns = new Vue(compOptions)
    // 将父组件的 VNode 放到子组件的实例上
    compIns._parentVnode = vnode
    // 挂载子组件
    compIns.$mount()
    // 记录子组件 vnode 的父节点信息
    compIns._vnode.parent = vnode.parent
    // 将子组件添加到父节点内
    vnode.parent.appendChild(compIns._vnode.elm)
    return true
  }
}

isReserveTag

/src/utils.js

/**
 * 是否为平台保留节点
 */
export function isReserveTag(tagName) {
  const reserveTag = ['div', 'h3', 'span', 'input', 'select', 'option', 'p', 'button', 'template']
  return reserveTag.includes(tagName)
}

Slot Principle

The following examples are common ways of slots. The principle of the slot is actually very simple, but it is a little troublesome to implement.

  • Parse

    If the component label has child nodes, these child nodes are parsed into a specific data structure during parsing, which contains all the information of the slot, and then the data structure is placed on the attribute of the parent node, which is actually Find a place to store this information and retrieve it when you use it in renderSlot. Of course, this parsing process happens in the parsing process of the parent component.

  • Generate render function

    In the rendering function stage of generating child components, if a slot tag is encountered, it returns _ta rendering function of . The function receives two parameters: the JSON string form of the attribute, and the children array composed of the rendering functions of all the child nodes of the slot tag.

  • render helper

    When the rendering function of the child component is executed, if it is executed , the method will bevm._t called , which will return the VNode of the slot, and then enter the patch phase of the child component to turn these VNodes into real DOM and render them on the page.renderSlot

The above is the principle of the slot, and then when it is implemented next, it may be a little bit rounded in some places, more or less because there are some problems with the overall architecture, so there will be some patching codes in it, you can understand these codes A little business code written to implement the slot function. You just need to live with the essence of the slot.

Example

<!-- comp -->
<template>
  <div>
    <div>
      <slot name="slot1">
        <span>插槽默认内容</span>
      </slot>
    </div>
      <slot name="slot2" v-bind:test="xx">
        <span>插槽默认内容</span>
      </slot>
    <div>
    </div>
  </div>
</template>

<comp></comp>
<comp>
  <template v-slot:slot2="xx">
    <div>作用域插槽,通过插槽从父组件给子组件传递内容</div>
  </template>
<comp>

parse

/src/compiler/parse.js

function processElement() {
    // ...

    // 处理插槽内容
    processSlotContent(curEle)

    // 节点处理完以后让其和父节点产生关系
    if (stackLen) {
      stack[stackLen - 1].children.push(curEle)
      curEle.parent = stack[stackLen - 1]
      // 如果节点存在 slotName,则说明该节点是组件传递给插槽的内容
      // 将插槽信息放到组件节点的 rawAttr.scopedSlots 对象上
      // 而这些信息在生成组件插槽的 VNode 时(renderSlot)会用到
      if (curEle.slotName) {
        const { parent, slotName, scopeSlot, children } = curEle
        // 这里关于 children 的操作,只是单纯为了避开 JSON.stringify 的循环引用问题
        // 因为生成渲染函数时需要对 attr 执行 JSON.stringify 方法
        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 }
        }
      }
    }
  }

processSlotContent

/src/compiler/parse.js

/**
 * 处理插槽
 * <scope-slot>
 *   <template v-slot:default="scopeSlot">
 *     <div>{{ scopeSlot }}</div>
 *   </template>
 * </scope-slot>
 * @param { AST } el 节点的 AST 对象
 */
function processSlotContent(el) {
  // 注意,具有 v-slot:xx 属性的 template 只能是组件的根元素,这里不做判断
  if (el.tag === 'template') { // 获取插槽信息
    // 属性 map 对象
    const attrMap = el.rawAttr
    // 遍历属性 map 对象,找出其中的 v-slot 指令信息
    for (let key in attrMap) {
      if (key.match(/v-slot:(.*)/)) { // 说明 template 标签上 v-slot 指令
        // 获取指令后的插槽名称和值,比如: v-slot:default=xx
        // default
        const slotName = el.slotName = RegExp.$1
        // xx
        el.scopeSlot = attrMap[`v-slot:${slotName}`]
        // 直接 return,因为该标签上只可能有一个 v-slot 指令
        return
      }
    }
  }
}

generate

/src/compiler/generate.js

/**
 * 解析 ast 生成 渲染函数
 * @param {*} ast 语法树 
 * @returns {string} 渲染函数的字符串形式
 */
function genElement(ast) {
  // ...

  // 处理子节点,得到一个所有子节点渲染函数组成的数组
  const children = genChildren(ast)

  if (tag === 'slot') {
    // 生成插槽的处理函数
    return `_t(${JSON.stringify(attrs)}, [${children}])`
  }

  // 生成 VNode 的可执行方法
  return `_c('${tag}', ${JSON.stringify(attrs)}, [${children}])`
}

renderHelper

/src/compiler/renderHelper.js

/**
 * 在 Vue 实例上安装运行时的渲染帮助函数,比如 _c、_v,这些函数会生成 Vnode
 * @param {VueContructor} target Vue 实例
 */
export default function renderHelper(target) {
  // ...
  target._t = renderSlot
}

renderSlot

/src/compiler/renderHelper.js

/**
 * 插槽的原理其实很简单,难点在于实现
 * 其原理就是生成 VNode,难点在于生成 VNode 之前的各种解析,也就是数据准备阶段
 * 生成插槽的的 VNode
 * @param {*} attrs 插槽的属性
 * @param {*} children 插槽所有子节点的 ast 组成的数组
 */
function renderSlot(attrs, children) {
  // 父组件 VNode 的 attr 信息
  const parentAttr = this._parentVnode.attr
  let vnode = null
  if (parentAttr.scopedSlots) { // 说明给当前组件的插槽传递了内容
    // 获取插槽信息
    const slotName = attrs.name
    const slotInfo = parentAttr.scopedSlots[slotName]
    // 这里的逻辑稍微有点绕,建议打开调试,查看一下数据结构,理清对应的思路
    // 这里比较绕的逻辑完全是为了实现插槽这个功能,和插槽本身的原理没关系
    this[slotInfo.scopeSlot] = this[Object.keys(attrs.vBind)[0]]
    vnode = genVNode(slotInfo.children, this)
  } else { // 插槽默认内容
    // 将 children 变成 vnode 数组
    vnode = genVNode(children, this)
  }

  // 如果 children 长度为 1,则说明插槽只有一个子节点
  if (children.length === 1) return vnode[0]
  return createElement.call(this, 'div', {}, vnode)
}

genVNode

/src/compiler/renderHelper.js

/**
 * 将一批 ast 节点(数组)转换成 vnode 数组
 * @param {Array<Ast>} childs 节点数组
 * @param {*} vm 组件实例
 * @returns vnode 数组
 */
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) { // 文本节点
      if (typeof text === 'string') { // text 为字符串
        // 构造文本节点的 AST 对象
        const textAst = {
          type: 3,
          text,
        }
        if (text.match(/{{(.*)}}/)) {
          // 说明是表达式
          textAst.expression = RegExp.$1.trim()
        }
        vnode.push(createTextNode.call(vm, textAst))
      } else { // text 为文本节点的 ast 对象
        vnode.push(createTextNode.call(vm, text))
      }
    } else { // 元素节点
      vnode.push(createElement.call(vm, tag, attr, genVNode(children, vm)))
    }
  }
  return vnode
}

result

Well, at this point, the initial rendering of the template has been completed. If you can see the following renderings, it means everything is normal. Because the whole process involves a lot of content, if you feel that some parts are not clear, it is recommended to look at it again and sort out the whole process carefully.

Animation link: https://gitee.com/liyongning/typora-image-bed/raw/master/202203141833484.image

Jun-18-2021 07-35-17.gif

It can be seen that the original tags, custom components, and slots have been completely rendered on the page. After the initial rendering is completed, it is time to implement the subsequent update process, which is the next patch of the handwritten Vue2 series. --diff .

Link

Thank you for your attention , likes , favorites and comments , see you in the next issue.


When learning becomes a habit, knowledge becomes common sense. Thank you for your attention , likes , favorites and comments .

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324073416&siteId=291194637