Writing in the front

This is the fourth vuE2 series implementation from scratch, adding the virtual DOM to YourVue.

When implementing the VUE process in the first article, I parsed the template into an AST, directly generating the real DOM. This is not how vUE is implemented. The actual implementation is to use gencode to generate the render function from the AST generated by Parse (Template), and then execute the render function to generate the VNode and build the virtual DOM tree. The real DOM is then created from the virtual DOM tree.

This article will be first updated on our official account: BUPPT. Code repository: github.com/buppt/YourV…

The body of the

VNode

First of all, we define the node of the virtual DOM, VNode, which stores nothing more than tags, attributes, child nodes, text content, etc.

export class VNode{
  constructor(tag, data={}, children=[], text=' ', elm, context){
    this.tag=tag;
    this.props=data ;
    this.children=children;
    this.text=text
    this.key = data && data.key
    var count = 0;
    children.forEach(child= > {
      if(child instanceof VNode){
        count += child.count;
      }
      count++;
    });
    this.count = count; }}Copy the code

render

Define the four basic render functions using the same _c and _s names as vue, each simply creating a different VNode.

  • _c Creates a normal VNode with a tag
  • _v Creates a VNode for the text node
  • _s is the toString function that converts a variable to a string
  • _e creates an empty VNode
export default class YourVue{
  _init(options){
      initRender(this)}}export function initRender(vm){
  vm._c = createElement
  vm._v = createTextVNode
  vm._s = toString
  vm._e = createEmptyVNode
}
function createElement (tag, data={}, children=[]){
  children = simpleNormalizeChildren(children)
  return new VNode(tag, data, children, undefined.undefined)}export function createTextVNode (val) {
  return new VNode(undefined.undefined.undefined.String(val))
}

export function toString (val) {
  return val == null
    ? ' '
    : Array.isArray(val)
      ? JSON.stringify(val, null.2)
      : String(val)
}
export function createEmptyVNode (text) {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

export function simpleNormalizeChildren (children) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
Copy the code

If a child element is an array, concat the element in the array onto children.

Because children should be vNodes, v-for and slot add an array element to children, and the vnodes in these array elements are parallel to the vnodes in children. So we expand the child element only one level.

gencode

What Gencode does is generate the AST that will be parsed, using the render function above to generate the VNode string code.

The code generated by Gencode needs to be read in conjunction with the arguments to the previous render function, such as _c with tag as the first argument, element attributes as the second argument, and child nodes as the third.

_c('h4', {attrs: {"style":"color: red"}},[
  _v(_s(message))]),
  _v(""),
  _c('button', {on: {click:decCount}},[_v("decCount")])Copy the code

The generated code is also nested like a DOM tree, with the outermost layer of a Node. type === 1 element node. Using the _c function, the element attributes are passed to VNode as the second argument, and the remaining elements are structurally stored in the third children argument.

If node.type === 3 indicates a plain text node, use json.stringify (Node.text) directly.

If Node. type === 2 indicates a text node with variables, use Node. expression generated by parse.

export function templateToCode(template){
  const ast = parse(template, {})
  return generate(ast)
}
export function generate(ast){
  const code = ast ? genElement(ast) : '_c("div")'
  return `with(this){return ${code}} `
}

function genElement(el){
  let code
  let data = genData(el)
  const children = el.inlineTemplate ? null : genChildren(el, true)
  code = `_c('${el.tag}'${
    data ? `,${data}` : ' ' // data
  }${
    children ? `,${children}` : ' ' // children
  }) `
  return code
}

export function genChildren (el){
  const children = el.children
  if (children.length) {
    const el = children[0]
    return ` [${children.map(c => genNode(c)).join(', ')}] `}}function genNode (node) {
  if (node.type === 1) {
    return genElement(node)
  } else if (node.type === 3 && node.isComment) {
    return `_e(The ${JSON.stringify(node.text)}) `
  } else {
    return `_v(${node.type === 2
      ? node.expression
      :JSON.stringify(node.text)
    }) `}}function genData(el){
  let data = '{'
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)}, `
  }
  if (el.props) {
    data += `domProps:${genProps(el.props)}, `
  }
  if (el.events) {
    data += `on:${genHandlers(el.events)}, `
  }
  data = data.replace($/ /,.' ') + '} '
  return data
}

function genHandlers(events){
  let res = '{'
  for(let key in events){
    res += key + ':' + events[key].value
  }
  res += '} '
  return res
}
Copy the code

To an executable function

With (this){return ${code}}, how do you turn it into an executable function? New Function.

export default class YourVue{
    $mount(){
        const options = this.$options
        if(! options.render) {let template = options.template
            if (template) {
                const code = templateToCode(template)
                const render = new Function(code).bind(this)
                options.render = render
            }
        }
        const vm = this
        new Watcher(vm, vm.update.bind(vm), noop)
    }
}
Copy the code

We have converted the render function template into a VNode and attached it to the options property of YourVue instance. How do we convert the render function into a real dom? See the next article.