Handwritten Vue2 series compiler

Time:2022-9-23

When learning becomes a habit, knowledge becomes common sense.thank you allfocus onlikecollectandComment

New videos and articles will be sent on the WeChat public account as soon as possible, please pay attention:Li Yongning lyn

Articles have been included ingithub repository liyongning/blog, Welcome to Watch and Star.

foreword

Next, we will officially enter the handwritten Vue2 series. It will not start from scratch here, it will be based onlyn-vueGo straight to the upgrade, so if you haven’t readVue1.x of handwritten Vue series, please start with this article and study in order.

We all know that the problem with Vue1 is that there are too many Watchers in large applications. If you don’t know the principle, please checkVue1.x of handwritten Vue series

So in Vue2 by introducing VNode and diff algorithm to solve this problem. By reducing the granularity of Watcher, one component corresponds to one Watcher (rendering Watcher), so that there will be no problem of performance degradation caused by too many Watchers on large pages.

In Vue1, Watcher corresponds to the responsive data in the page one by one. When the responsive data changes, Dep notifies Watcher to complete the corresponding DOM update. But in Vue2, a component corresponds to a Watcher. When the responsive data changes, the Watcher does not know where the responsive data is in the component, so how to complete the update?

read the previousSource series, as everyone must know, Vue2 introduces VNode and diff algorithms to combine componentscompileEach time the responsive data changes, a new VNode will be generated, and the new and old VNodes will be compared through the diff algorithm to find out what has changed, and then perform the corresponding DOM operation to complete the update.

Therefore, everyone can understand here that Vue1 and Vue2 actually have no changes in the core data-responsive part, and the main changes are in the compiler part.

Target

Complete a simplified implementation of the Vue2 compiler, starting from string template parsing, and finally gettingrenderfunction.

translater

When writing Vue1, the compiler traverses the DOM structure of the template through the DOM API. In Vue2, this method is no longer used. Instead, just like the official one, the template string of the component is directly compiled, AST is generated, and then from AST generates render functions.

First back up the compiler directory of Vue1, and then create a new compiler directory as the compiler directory of Vue2

mv compiler compiler-vue1 && mkdir compiler

mount

/src/compiler/index.js

/**
 * translater
 */
export default function mount(vm) {
  if (!vm.$options.render) { // If no render option is provided, compile and generate a render function
    // get the template
    let template = ''

    if (vm.$options.template) {
      // template exists
      template = vm.$options.template
    } else if (vm.$options.el) {
      // there is a mount point
      template = document.querySelector(vm.$options.el).outerHTML
      // Record the mount point on the instance, which will be used in this._update
      vm.$el = document.querySelector(vm.$options.el)
    }

    // Generate render function
    const render = compileToFunction(template)
    // Mount the render function on $options
    vm.$options.render = render
  }
}

compileToFunction

/src/compiler/compileToFunction.js

/**
 * Parse the template string to get the AST syntax tree
 * Generate render function from AST syntax tree
 * @param { String } template template string
 * @returns render function
 */
export default function compileToFunction(template) {
  // Parse the template and generate ast
  const ast = parse(template)
  // Generate ast to render function
  const render = generate(ast)
  return render
}

parse

/src/compiler/parse.js

/**
 * Parse the template string and generate an AST syntax tree
 * @param {*} template template string
 * @returns {AST} root ast syntax tree
 */
export default function parse(template) {
  // AST object holding all unpaired start tags
  const stack = []
  // final AST syntax tree
  let root = null

  let html = template
  while (html.trim()) {
    // filter comment tags
    if (html.indexOf('<!--') === 0) {
      // Indicates that the starting position is a comment label, ignore it
      html = html.slice(html.indexOf('-->') + 3)
      continue
    }
    // match start tag
    const startIdx = html.indexOf('<')
    if (startIdx === 0) {
      if (html.indexOf('</') === 0) {
        // Description is a closing tag
        parseEnd()
      } else {
        // handle start tag
        parseStartTag()
      }
    } else if (startIdx > 0) {
      // Indicate that there is a piece of text between the start tags, find the start position of the next tag in html
      const nextStartIdx = html.indexOf('<')
      // If the stack is empty, it means that this text does not belong to any element, just throw it away without processing
      if (stack.length) {
        // Go here and say that the stack is not empty, then process this text and put it in the stomach of the top element of the stack
        processChars(html.slice(0, nextStartIdx))
      }
      html = html.slice(nextStartIdx)
    } else {
      // Indicates that the start tag is not matched, and the entire html is a piece of text
    }
  }
  return root
  
  // Declaration of the parseStartTag function
  // ...
  // declaration of the processElement function
}

// declaration of the processVModel function
// ...
// declaration of the processVOn function

parseStartTag

/src/compiler/parse.js

/**
 * Parse start tag
 * for example:<div id="app"> ...</div>
 */
function parseStartTag() {
  // First find the end position of the start tag >
  const end = html.indexOf('>')
  // Parse the content in the start tag <content>, tag name + attribute, for example: div id="app"
  const content = html.slice(1, end)
  // Truncate the html and remove the above parsed content from the html string
  html = html.slice(end + 1)
  // find the first space position
  const firstSpaceIdx = content.indexOf(' ')
  // tag name and attribute string
  let tagName = '', attrsStr = ''
  if (firstSpaceIdx === -1) {
    // If there is no space, content is considered to be the tag name, such as<h3></h3> In this case, content = h3
    tagName = content
    // no properties
    attrsStr = ''
  } else {
    tagName = content.slice(0, firstSpaceIdx)
    // The rest of the content are attributes, such as id="app" xx=xx
    attrsStr = content.slice(firstSpaceIdx + 1)
  }
  // Get the attribute array, [id="app", xx=xx]
  const attrs = attrsStr ? attrsStr.split(' ') : []
  // Further parse the attribute array to get a Map object
  const attrMap = parseAttrs(attrs)
  // Generate AST object
  const elementAst = generateAST(tagName, attrMap)
  // If the root node does not exist, the current node is the first node of the entire template
  if (!root) {
    root = elementAst
  }
  // Push the ast object to the stack. When the end tag is encountered, the ast object at the top of the stack is popped out. The two are a pair.
  stack.push(elementAst)

  // Self-closing label, call the end method directly, enter the processing truncation of the closing label, and do not push the stack
  if (isUnaryTag(tagName)) {
    processElement()
  }
}

parseEnd

/src/compiler/parse.js

/**
 * Handle closing tags, like:<div id="app"> ...</div>
 */
function parseEnd() {
  // Truncate the closing tag from the html string
  html = html.slice(html.indexOf('>') + 1)
  // process the top element of the stack
  processElement()
}

parseAttrs

/src/compiler/parse.js

/**
 * Parse the attribute array and get a Map object composed of attributes and values
 * @param {*} attrs attribute array, [id="app", xx="xx"]
 */
function parseAttrs(attrs) {
  const attrMap = {}
  for (let i = 0, len = attrs.length; i < len; i++) {
    const attr = attrs[i]
    const [attrName, attrValue] = attr.split('=')
    attrMap[attrName] = attrValue.replace(/"/g, '')
  }
  return attrMap
}

generateAST

/src/compiler/parse.js

/**
 * Generate AST object
 * @param {*} tagName tag name
 * @param {*} attribute map object composed of attrMap tags
 */
function generateAST(tagName, attrMap) {
  return {
    // element node
    type: 1,
    // Label
    tag: tagName,
    // The original attribute map object, which needs to be further processed later
    rawAttr: attrMap,
    // child node
    children: [],
  }
}

processChars

/src/compiler/parse.js

/**
 * process text
 * @param {string} text 
 */
function processChars(text) {
  // remove null characters or newlines
  if (!text.trim()) return

  // Construct the AST object of the text node
  const textAst = {
    type: 3,
    text,
  }
  if (text.match(/{{(.*)}}/)) {
    // Description is an expression
    textAst.expression = RegExp.$1.trim()
  }
  // put ast in the belly of the top element of the stack
  stack[stack.length - 1].children.push(textAst)
}

processElement

/src/compiler/parse.js

/**
 * This method is called when the closing tag of the element is processed
 * Further process each attribute on the element and put the processing result on the attr attribute
 */
function processElement() {
  // Pop the top element of the stack and process the element further
  const curEle = stack.pop()
  const stackLen = stack.length
  // Further process the rawAttr object in the AST object { attrName: attrValue, ... }
  const { tag, rawAttr } = curEle
  // The processing results are placed on the attr object, and the corresponding attributes in the rawAttr object are deleted
  curEle.attr = {}
  // Array of keys of property objects
  const propertyArr = Object.keys(rawAttr)

  if (propertyArr.includes('v-model')) {
    // handle v-model directive
    processVModel(curEle)
  } else if (propertyArr.find(item => item.match(/^v-bind:(.*)/))) {
    // Handle v-bind instructions, such as
    processVBind(curEle, RegExp.$1, rawAttr[`v-bind:${RegExp.$1}`])
  } else if (propertyArr.find(item => item.match(/^v-on:(.*)/))) {
    // Handle v-on instructions, such as <button v-on:click="add">add</button>
    processVOn(curEle, RegExp.$1, rawAttr[`v-on:${RegExp.$1}`])
  }

  // After the node is processed, let it have a relationship with the parent node
  if (stackLen) {
    stack[stackLen - 1].children.push(curEle)
    curEle.parent = stack[stackLen - 1]
  }
}

processVModel

/src/compiler/parse.js

/**
 * Process the v-model instruction and put the processing result directly on the curEle object
 * @param {*} curEle 
 */
function processVModel(curEle) {
  const { tag, rawAttr, attr } = curEle
  const { type, 'v-model': vModelVal } = rawAttr

  if (tag === 'input') {
    if (/text/.test(type)) {
      // <input type="text" v-model="inputVal" />
      attr.vModel = { tag, type: 'text', value: vModelVal }
    } else if (/checkbox/.test(type)) {
      // <input type="checkbox" v-model="isChecked" />
      attr.vModel = { tag, type: 'checkbox', value: vModelVal }
    }
  } else if (tag === 'textarea') {
    // <textarea v-model="test" />
    attr.vModel = { tag, value: vModelVal }
  } else if (tag === 'select') {
    // <select v-model="selectedValue">...</select>
    attr.vModel = { tag, value: vModelVal }
  }
}

processVBind

/src/compiler/parse.js

/**
 * Handle v-bind instructions
 * @param {*} the AST object currently being processed by curEle
 * @param {*} bindKey v-bind: the key in key
 * @param {*} bindVal v-bind:key = val in val
 */
function processVBind(curEle, bindKey, bindVal) {
  curEle.attr.vBind = { [bindKey]: bindVal }
}

processVOn

/src/compiler/parse.js

/**
 * Handle v-on instructions
 * @param {*} curEle the currently processed AST object
 * @param {*} vOnKey v-on: key in key
 * @param {*} vOnVal val in v-on:key="val"
 */
function processVOn(curEle, vOnKey, vOnVal) {
  curEle.attr.vOn = { [vOnKey]: vOnVal }
}

isUnaryTag

/src/utils.js

/**
 * Whether it is a self-closing tag, there are some built-in self-closing tags, for the sake of simplicity
 */
export function isUnaryTag(tagName) {
  const unaryTag = ['input']
  return unaryTag.includes(tagName)
}

generate

/src/compiler/generate.js

/**
 * Generate render function from ast
 * @param {*} ast ast syntax tree
 * @returns render function
 */
export default function generate(ast) {
  // render function string form
  const renderStr = genElement(ast)
  // Convert the function in the form of a string into an executable function through new Function, and use with to extend the scope chain for the rendering function
  return new Function(`with(this) { return ${renderStr} }`)
}

genElement

/src/compiler/generate.js

/**
 * Parse ast to generate rendering function
 * @param {*} ast syntax tree 
 * @returns {string} String form of render function
 */
function genElement(ast) {
  const { tag, rawAttr, attr } = ast

  // Generate property Map object, static property + dynamic property
  const attrs = { ...rawAttr, ...attr }

  // Process child nodes and get an array of all child node rendering functions
  const children = genChildren(ast)

  // Executable method to generate VNode
  return `_c('${tag}', ${JSON.stringify(attrs)}, [${children}])`
}

genChildren

/src/compiler/generate.js

/**
 * Process the child nodes of the ast node and turn the child nodes into a rendering function
 * @param {*} the ast object of the ast node 
 * @returns [childNodeRender1, ....]
 */
function genChildren(ast) {
  const ret = [], { children } = ast
  // loop through all child nodes
  for (let i = 0, len = children.length; i < len; i++) {
    const child = children[i]
    if (child.type === 3) {
      // text node
      ret.push(`_v(${JSON.stringify(child)})`)
    } else if (child.type === 1) {
      // element node
      ret.push(genElement(child))
    }
  }
  return ret
}

result

existmountadd a methodconsole.log(vm.$options.render), open the console, refresh the page, and see the following content, indicating that the compiler is completed

Handwritten Vue2 series compiler

Next, it will enter the formal mounting stage to complete the initial rendering of the page.

Link

Thanks to all of you:focus onlikecollectandComment, see you next time.


When learning becomes a habit, knowledge becomes common sense.thank you allfocus onlikecollectandComment

New videos and articles will be sent on the WeChat public account as soon as possible, please pay attention:Li Yongning lyn

Articles have been included ingithub repository liyongning/blog, Welcome to Watch and Star.

Recommended Today

CSci 4203 Introduction to Algorithms

CSci 4203/EE4367, Spring 2021Homework Assignment III (Issued March 30, 2021)Instructions:1.You can type in your solutions by downloading this MS Word file. Or, write your solutions, scan the file and upload your PDF file.2.Label your assignment with your name and UMN email address3.Submit assignments via Canvas course web page. Note: No late submissions accepted.4.Due Date: 11:59 […]