preface

Next will formally enter the handwriting Vue2 series. Instead of starting from scratch, the upgrade will be directly based on lyn-Vue, so if you haven’t read the written Vue series vu1.x, please start with this article and work through it in sequence.

The problem with Vue1 is that there are too many Watcher’s in large applications. If you don’t know how this works, check out the Handwritten Vue series vu1.x.

Therefore, VNode and DIff algorithms are introduced in Vue2 to solve this problem. By reducing the granularity of Watcher to one component for each Watcher (rendering Watcher), you don’t have the performance degradation problem of too many large page Watcher.

In Vue1, Watcher corresponds to responsive data in the page one by one. When responsive data changes, Dep informs Watcher to complete the corresponding DOM update. But in Vue2, each component corresponds to a Watcher. When the responsive data changes, Watcher does not know where the responsive data is in the component. How can the update be done?

As you probably know from reading the previous source code series, Vue2 introduces the VNode and DIff algorithms to compile components into VNodes. Each time the responsive data changes, a new VNode is generated, and the diff algorithm is used to compare the old and new VNodes to find the changes. Then perform the corresponding DOM operations to complete the update.

So, as you can see, Vue1 and Vue2 don’t really have any changes in the core data responsiveness. The main changes are in the compiler.

The target

Complete a simplified implementation of the Vue2 compiler, starting with string template parsing and ending with the render function.

The compiler

When writing Vue1, the compiler uses the DOM API to iterate over the TEMPLATE’s DOM structure. In Vue2, this method is no longer used. Instead, it directly compiles the template string of the component, generates the AST, and then generates the rendering function from the AST.

First, back up the COMPILER directory for Vue1, and then create a compiler directory as the compiler directory for Vue2

mv compiler compiler-vue1 && mkdir compiler
Copy the code

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
    // Get the template
    let template = ' '

    if (vm.$options.template) {
      // The template exists
      template = vm.$options.template
    } else if (vm.$options.el) {
      // Mount point exists
      template = document.querySelector(vm.$options.el).outerHTML
      // Record mount points on the instance, as used in this._update
      vm.$el = document.querySelector(vm.$options.el)
    }

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

Copy the code

compileToFunction

/src/compiler/compileToFunction.js

/** * parse the template string to get the AST syntax tree@param { String } Template template string *@returns Render function */
export default function compileToFunction(template) {
  // Parse the template to generate an AST
  const ast = parse(template)
  // The ast generates the rendering function
  const render = generate(ast)
  return render
}

Copy the code

parse

/src/compiler/parse.js

/** * parse the template string to generate the AST syntax tree *@param {*} Template template string *@returns {AST} Root ast Syntax tree */
export default function parse(template) {
  // Store all unpaired start tag AST objects
  const stack = []
  // Final AST syntax tree
  let root = null

  let html = template
  while (html.trim()) {
    // Filter comment tags
    if (html.indexOf('<! -- ') = = =0) {
      // The start position is a comment tag, which is ignored
      html = html.slice(html.indexOf('-->') + 3)
      continue
    }
    // Match the start tag
    const startIdx = html.indexOf('<')
    if (startIdx === 0) {
      if (html.indexOf('< /') = = =0) {
        // Indicates a closed label
        parseEnd()
      } else {
        // Process the start tag
        parseStartTag()
      }
    } else if (startIdx > 0) {
      Find the start position of the next tag in the HTML
      const nextStartIdx = html.indexOf('<')
      // If the stack is empty, the text does not belong to any element and is discarded
      if (stack.length) {
        // Process the text and place it in the belly of the top element of the stack
        processChars(html.slice(0, nextStartIdx))
      }
      html = html.slice(nextStartIdx)
    } else {
      // The start tag is not matched, and the entire HTML is just a text}}return root
  
  // Declaration of the parseStartTag function
  // ...
  // processElement declaration
}

// processVModel function declaration
// ...
// processVOn function declaration

Copy the code

parseStartTag

/src/compiler/parse.js


      
...
*/
function parseStartTag() { // Find the end of the start tag > const end = html.indexOf('>') Div id="app"; div id="app"; const content = html.slice(1, end) // Truncate the HTML to remove the content parsed above 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) {

content = h3
tagName = content // No attributes attrsStr = ' ' } else { tagName = content.slice(0, firstSpaceIdx) // The rest of the content will be attributes, such as id="app" xx=xx attrsStr = content.slice(firstSpaceIdx + 1)}[id="app", xx=xx] const attrs = attrsStr ? attrsStr.split(' ') : [] // Further parse the property array to get a Map object const attrMap = parseAttrs(attrs) // Generate an AST object const elementAst = generateAST(tagName, attrMap) // If the root node does not exist, the current node is the first node in the template if(! root) { root = elementAst }// Push the AST object onto the stack and pop the ast object at the top of the stack when the end tag is encountered stack.push(elementAst) // Call the end method to truncate the closed tag if (isUnaryTag(tagName)) { processElement() } } Copy the code

parseEnd

/src/compiler/parse.js

/** * handle closing tags, such as 
      
...
*/
function parseEnd() { // Truncate the closing tag from the HTML string html = html.slice(html.indexOf('>') + 1) // Handle the top of the stack element processElement() } Copy the code

parseAttrs

/src/compiler/parse.js

/** * parses the attributes array to get a Map of attributes and values *@param {*} Attrs 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
}

Copy the code

generateAST

/src/compiler/parse.js

/** * Generates the AST object *@param {*} TagName Label name *@param {*} The attrMap tag consists of the attribute map object */
function generateAST(tagName, attrMap) {
  return {
    // Element node
    type: 1./ / label
    tag: tagName,
    // The original attribute map object, which needs further processing later
    rawAttr: attrMap,
    / / child nodes
    children: [].}}Copy the code

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(/ / {{(. *)}})) {
    // The description is an expression
    textAst.expression = RegExp.$1.trim()
  }
  // Put the AST in the stomach of the top element
  stack[stack.length - 1].children.push(textAst)
}

Copy the code

processElement

/src/compiler/parse.js

/** * This method is called when the element's closing tag is processed * to further process the individual attributes on the element, placing the result on the attr attribute */
function processElement() {
  // Pops the top element of the stack to further process that element
  const curEle = stack.pop()
  const stackLen = stack.length
  // Further process the rawAttr in the AST object {attrName: attrValue,... }
  const { tag, rawAttr } = curEle
  // Put the results on the attr object and delete the corresponding attributes in the rawAttr object
  curEle.attr = {}
  // An array of keys for the property object
  const propertyArr = Object.keys(rawAttr)

  if (propertyArr.includes('v-model')) {
    // Process the V-model directive
    processVModel(curEle)
  } else if (propertyArr.find(item= > item.match(/^v-bind:(.*)/))) {
    
    processVBind(curEle, RegExp.$1, rawAttr[`v-bind:The ${RegExp. $1}`])}else if (propertyArr.find(item= > item.match(/^v-on:(.*)/))) {
    
    processVOn(curEle, RegExp.$1, rawAttr[`v-on:The ${RegExp. $1}`])}// 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]}}Copy the code

processVModel

/src/compiler/parse.js

/** * Process the V-model instruction and place the 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') {
    // 
    attr.vModel = { tag, value: vModelVal }
  }
}

Copy the code

processVBind

/src/compiler/parse.js

/** * Handle the V-bind directive *@param {*} The AST object * that curEle is currently processing@param {*} BindKey v-bind:key * in key@param {*} BindVal v-bind:key = val */ in val
function processVBind(curEle, bindKey, bindVal) {
  curEle.attr.vBind = { [bindKey]: bindVal }
}

Copy the code

processVOn

/src/compiler/parse.js

/** * Handle the V-ON instruction *@param {*} CurEle The AST object currently being processed *@param {*} VOnKey V-ON :key * in key@param {*} VOnVal v-on:key= val */ in "val"
function processVOn(curEle, vOnKey, vOnVal) {
  curEle.attr.vOn = { [vOnKey]: vOnVal }
}

Copy the code

isUnaryTag

/src/utils.js

/** * Indicates whether it is a self-closing label. Some self-closing labels are built in for simple processing
export function isUnaryTag(tagName) {
  const unaryTag = ['input']
  return unaryTag.includes(tagName)
}

Copy the code

generate

/src/compiler/generate.js

/** * Generate render function * from ast@param {*} Ast Ast Syntax tree *@returns Render function */
export default function generate(ast) {
  // Render the function as a string
  const renderStr = genElement(ast)
  // Make a string Function executable with new Function and extend the scope chain for rendering functions with with
  return new Function(`with(this) { return ${renderStr}} `)}Copy the code

genElement

/src/compiler/generate.js

/** * Parse ast to generate render function *@param {*} Ast Syntax tree *@returns {string} Render function in string form */
function genElement(ast) {
  const { tag, rawAttr, attr } = ast

  // Generate property Map object, static property + dynamic property
  constattrs = { ... rawAttr, ... attr }// Process the child nodes to get an array of all the child node rendering functions
  const children = genChildren(ast)

  // Generate an executable method for VNode
  return `_c('${tag}', The ${JSON.stringify(attrs)}[${children}]) `
}

Copy the code

genChildren

/src/compiler/generate.js

/** * Processes the children of the AST node, turning the children into rendering functions *@param {*} Ast object * of the AST node@returns [childNodeRender1, ....] * /
function genChildren(ast) {
  const ret = [], { children } = ast
  // Traverses 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(The ${JSON.stringify(child)}) `)}else if (child.type === 1) {
      // Element node
      ret.push(genElement(child))
    }
  }
  return ret
}

Copy the code

The results of

Add console.log(vm.$options.render) to the mount method, open the console, refresh the page, and see the following

The formal mount phase will then proceed to complete the initial rendering of the page.

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