编程知识 cdmana.com

Principle of Vue template compilation

The official account is from WeChat : Frontend time and space ;
It comes from the official account of WeChat : The more amazing front end ;
author : shenfq

Written in the beginning

Yes Vue Of my classmates must have experienced , .vue How convenient this single file component is . But we also know ,Vue The bottom is through virtual DOM To render , that .vue How the template of the file is converted to virtual DOM What about ? This piece has always been a black box for me , I haven't studied it before , I'm going to find out today .

<center>Virtual Dom</center>

Vue 3 After the release , I wanted to look directly at Vue 3 Template compilation of , But I turn on Vue 3 Source code time , It seems that I am even Vue 2 I don't know how to compile the template . Lu Xun told us from childhood that , You can't be a fat man at one bite , I can only look back Vue 2 Template compiler source code , as for Vue 3 Let's wait until the official release .

Vue Version of

Many people use Vue When , It's all directly through vue-cli Generated template code , Don't know Vue There are actually two build versions available .

  • vue.js: Full version , Includes the ability to compile templates ;
  • vue.runtime.js: Runtime version , No template compilation capability , Need to pass through vue-loader Compile ahead of time .

<center>Vue Different builds </center>

<center> The difference between the full version and the runtime version </center>

Simply speaking , If you use vue-loader , You can use vue.runtime.min.js, Pass the template compilation process to vue-loader, If you are in the browser directly through script Tags introduced Vue, Need to use vue.min.js, Compile templates at run time .

Compile entry

I understand Vue Version of , Let's see. Vue Full version of the entry file (src/platforms/web/entry-runtime-with-compiler.js).

//  Some code is omitted , Only the key parts 
import { compileToFunctions } from './compiler/index'

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el) {
  const options = this.$options

  //  without  render  Method , Is to  template  compile 
  if (!options.render) {
    let template = options.template
    if (template) {
      //  call  compileToFunctions, compile  template, obtain  render  Method 
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments,
        },
        this
      )
      //  there  render  The way is to create virtual  DOM  Methods 
      options.render = render
    }
  }
  return mount.call(this, el, hydrating)
}

I want to see others ./compiler/index Of documents compileToFunctions Where does the method come from .

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

//  adopt  createCompiler  Method to generate compiler functions 
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }

The main logic that follows is compiler Module , This one is a little twisted , Because this article is not about source code analysis , Don't post the whole source code . Just look at the logic of this paragraph .

export function createCompiler(baseOptions) {
  const baseCompile = (template, options) => {
    //  analysis  html, Turn into  ast
    const ast = parse(template.trim(), options)
    //  Optimize  ast, Tag static nodes 
    optimize(ast, options)
    //  take  ast  Convert to executable code 
    const code = generate(ast, options)
    return {
      ast,
      render: code.render,
      staticRenderFns: code.staticRenderFns,
    }
  }
  const compile = (template, options) => {
    const tips = []
    const errors = []
    //  Collect error information during compilation 
    options.warn = (msg, tip) => {
      ;(tip ? tips : errors).push(msg)
    }
    //  compile 
    const compiled = baseCompile(template, options)
    compiled.errors = errors
    compiled.tips = tips

    return compiled
  }
  const createCompileToFunctionFn = () => {
    //  Compile cache 
    const cache = Object.create(null)
    return (template, options, vm) => {
      //  Compiled templates go directly to cache 
      if (cache[template]) {
        return cache[template]
      }
      const compiled = compile(template, options)
      return (cache[key] = compiled)
    }
  }
  return {
    compile,
    compileToFunctions: createCompileToFunctionFn(compile),
  }
}

Main process

You can see that the main compilation logic is basically baseCompile In the way , There are three main steps :

  1. Template compilation , Convert template code to AST;
  2. Optimize AST, Convenient follow-up virtual DOM to update ;
  3. The generated code , take AST Into executable code ;
const baseCompile = (template, options) => {
  //  analysis  html, Turn into  ast
  const ast = parse(template.trim(), options)
  //  Optimize  ast, Tag static nodes 
  optimize(ast, options)
  //  take  ast  Convert to executable code 
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  }
}

parse

AST

First of all to see parse Method , The main function of this method is to analyze HTML, And into AST( Abstract syntax tree ), Come into contact with ESLint、Babel My classmates must be right AST No stranger , We can look at the process first parse After that AST What does it look like .

Here's an ordinary piece of Vue Templates :

new Vue({
  el: '#app',
  template: `<div> <h2 v-if="message">{{message}}</h2> <button @click="showName">showName</button> </div>`,
  data: {
    name: 'shenfq',
    message: 'Hello Vue!',
  },
  methods: {
    showName() {
      alert(this.name)
    },
  },
})

after parse After that AST:

<center>Template AST</center>

AST It's a tree structure object , Each layer represents a node , The first floor is divtag: "div").div The child nodes of are all in children Properties of the , Namely h2 label 、 Blank line 、button label . We can also notice that there is an attribute to mark the node type :type, here div Of type by 1, Representation is an element node ,type There are three types :

  1. Element nodes ;
  2. expression ;
  3. Text ;
    stay h2 and button The empty line between the tags is type by 3 Text node of , and h2 Under the tag is an expression node .

<center> Node type </center>

analysis HTML

parse The overall logic of is more complex , We can simplify the code first , have a look parse The process of .

import { parseHTML } from './html-parser'

export function parse(template, options) {
  let root
  parseHTML(template, {
    // some options...
    start() {}, //  The callback that resolves to the beginning of the tag position 
    end() {}, //  The callback that resolves to the end of the tag position 
    chars() {}, //  Callback when parsing to text 
    comment() {}, //  Callback when parsing to comments 
  })
  return root
}

You can see parse Mainly through parseHTML Work on , This parseHTML It comes from an open source library :simple html parser, It's just passed by Vue Some modifications of the team , Fixed related issue.

 Node type
HTML parser

Let's take a look at it parseHTML The logic of .

export function parseHTML(html, options) {
  let index = 0
  let last, lastTag
  const stack = []
  while (html) {
    last = html
    let textEnd = html.indexOf('<')

    // "<"  The character is currently  html  String start position 
    if (textEnd === 0) {
      // 1、 Match to comment : <!-- -->
      if (/^<!\--/.test(html)) {
        const commentEnd = html.indexOf('-->')
        if (commentEnd >= 0) {
          //  call  options.comment  Callback , Pass in comments 
          options.comment(html.substring(4, commentEnd))
          //  Cut out the notes 
          advance(commentEnd + 3)
          continue
        }
      }

      // 2、 Match to conditional comment : <![if !IE]>  <![endif]>
      if (/^<!\[/.test(html)) {
        // ...  Logic is similar to matching to annotations 
      }

      // 3、 Match to  Doctype: <!DOCTYPE html>
      const doctypeMatch = html.match(/^<!DOCTYPE [^>]+>/i)
      if (doctypeMatch) {
        // ...  Logic is similar to matching to annotations 
      }

      // 4、 Match to end tag : </div>
      const endTagMatch = html.match(endTag)
      if (endTagMatch) {
      }

      // 5、 Match to start tag : <div>
      const startTagMatch = parseStartTag()
      if (startTagMatch) {
      }
    }
    // "<"  The character is currently  html  In the middle of the string 
    let text, rest, next
    if (textEnd > 0) {
      //  Extracting intermediate characters 
      rest = html.slice(textEnd)
      //  This part is treated as text 
      text = html.substring(0, textEnd)
      advance(textEnd)
    }
    // "<"  The character is currently  html  There is no... In the string 
    if (textEnd < 0) {
      text = html
      html = ''
    }

    //  If there is  text  Text 
    //  call  options.chars  Callback , Pass in  text  Text 
    if (options.chars && text) {
      //  Character related callbacks 
      options.chars(text)
    }
  }
  //  Push forward , Cutting  html
  function advance(n) {
    index += n
    html = html.substring(n)
  }
}

The above code is simplified parseHTML,while One segment at a time in the loop html Text , Then through the regular judgment text type processing , This is similar to the finite state machine commonly used in compilation principles . Every time I get it "<" Text before and after characters ,"<" The word before the character is treated as text ,"<" After the character through the regular judgment , A limited number of states can be calculated .

<center>html Several states of </center>

Other logical processing is not complicated , It's mainly the start tag and the end tag , Let's first look at the regularities related to the start tag and the end tag .

const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)

It looks very long , But it's not hard to sort it out . Here is a regular visualization tool . Let's take a look at the tools startTagOpen:

<center>startTagOpen</center>

The more puzzling point here is why tagName Will exist :, This is XML Of Namespace , Now it's rarely used , We can ignore , So let's simplify this regularization :

const ncname = '[a-zA-Z_][\\w\\-\\.]_'
const startTagOpen = new RegExp(`^<${ncname}`)
const startTagClose = /^\s_(\/?)>/
const endTag = new RegExp(`^<\\/${ncname}[^>]*>`)

<center>startTagOpen</center>

<center>endTag</center>
In addition to the above rules about the beginning and end of tags , There is also a regular section to extract tag attributes , It's really smelly and long .

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']\*)'+|([^\s"'=<>`]+)))?/

Put the regular on the tool and you can see it at a glance , With = Demarcation , In front of it is the name of the attribute , Followed by the value of the attribute .

<center>attribute</center>
After straightening out the regularization, it will be more convenient for us to look at the following code .

while (html) {
  last = html
  let textEnd = html.indexOf('<')

  // "<"  The character is currently  html  String start position 
  if (textEnd === 0) {
    // some code ...

    // 4、 Match to the end of the tag : </div>
    const endTagMatch = html.match(endTag)
    if (endTagMatch) {
      const curIndex = index
      advance(endTagMatch[0].length)
      parseEndTag(endTagMatch[1], curIndex, index)
      continue
    }

    // 5、 Match to label start position : <div>
    const startTagMatch = parseStartTag()
    if (startTagMatch) {
      handleStartTag(startTagMatch)
      continue
    }
  }
}
//  Push forward , Cutting  html
function advance(n) {
  index += n
  html = html.substring(n)
}

//  Determine whether the label start position , If it is , The tag name and related attributes are extracted 
function parseStartTag() {
  //  extract  <xxx
  const start = html.match(startTagOpen)
  if (start) {
    const [fullStr, tag] = start
    const match = {
      attrs: [],
      start: index,
      tagName: tag,
    }
    advance(fullStr.length)
    let end, attr
    //  Recursively extract properties , Until it appears  ">"  or  "/>"  character 
    while (
      !(end = html.match(startTagClose)) &&
      (attr = html.match(attribute))
    ) {
      advance(attr[0].length)
      match.attrs.push(attr)
    }
    if (end) {
      //  If it is  "/>"  Represents a single label 
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}

//  Process start tag 
function handleStartTag(match) {
  const tagName = match.tagName
  const unary = match.unarySlash
  const len = match.attrs.length
  const attrs = new Array(len)
  for (let i = 0; i < l; i++) {
    const args = match.attrs[i]
    //  there  3、4、5  There are three different ways to copy attributes 
    // 3: attr="xxx"  Double quotes 
    // 4: attr='xxx'  Single quotation marks 
    // 5: attr=xxx  Omit quotation marks 
    const value = args[3] || args[4] || args[5] || ''
    attrs[i] = {
      name: args[1],
      value,
    }
  }

  if (!unary) {
    //  Not a single label , Push 
    stack.push({
      tag: tagName,
      lowerCasedTag: tagName.toLowerCase(),
      attrs: attrs,
    })
    lastTag = tagName
  }

  if (options.start) {
    //  Start tag callback 
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}

//  Processing closed labels 
function parseEndTag(tagName, start, end) {
  let pos, lowerCasedTagName
  if (start == null) start = index
  if (end == null) end = index

  if (tagName) {
    lowerCasedTagName = tagName.toLowerCase()
  }

  //  Look up unclosed tags of the same type in the stack 
  if (tagName) {
    for (pos = stack.length - 1; pos >= 0; pos--) {
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break
      }
    }
  } else {
    pos = 0
  }

  if (pos >= 0) {
    //  Close the unclosed tags in the tag , Update stack 
    for (let i = stack.length - 1; i >= pos; i--) {
      if (options.end) {
        // end  Callback 
        options.end(stack[i].tag, start, end)
      }
    }

    //  Delete closed tags from the stack 
    stack.length = pos
    lastTag = pos && stack[pos - 1].tag
  }
}

When parsing the start tag , If the label is not a single label , The tag will be put into a stack , Every time you close the label , It looks down the top of the stack for the tag with the same name , Until you find the tag with the same name , This action will close all tags above the same name tag . Here's an example :

<div>
  <h2>test</h2>
  <p>
  <p>
</div>

In the analysis of div and h2 After the start tag of , There are two elements in the stack .h2 After closing , Will be h2 Out of the stack . And then we'll parse the two unclosed p label , here , There are three elements in the stack (div、p、p). If this time , Parsed div Closed label for , In addition to div Closed outside ,div Inside two unclosed p The tag will also follow the closure , At this point, the stack is cleared .

For the sake of understanding , Specially recorded a moving picture , as follows :

<center> In and out of the stack </center>
Sort out the parseHTML After the logic of , Let's go back to calling parseHTML The location of , When calling this method , Four callbacks are passed in , Corresponding to the beginning and end of the label respectively 、 Text 、 notes .

parseHTML(template, {
  // some options...

  //  The callback that resolves to the beginning of the tag position 
  start(tag, attrs, unary) {},
  //  The callback that resolves to the end of the tag position 
  end(tag) {},
  //  Callback when parsing to text 
  chars(text: string) {},
  //  Callback when parsing to comments 
  comment(text: string) {},
})

Process start tag

First of all, when you parse to the start tag , Will generate a AST node , Then deal with the attributes on the tag , The final will be AST Nodes are put into a tree structure .

function makeAttrsMap(attrs) {
  const map = {}
  for (let i = 0, l = attrs.length; i < l; i++) {
    const { name, value } = attrs[i]
    map[name] = value
  }
  return map
}
function createASTElement(tag, attrs, parent) {
  const attrsList = attrs
  const attrsMap = makeAttrsMap(attrsList)
  return {
    type: 1, //  Node type 
    tag, //  The name of the node 
    attrsMap, //  Node attribute mapping 
    attrsList, //  Array of node attributes 
    parent, //  Parent node 
    children: [], //  Child node 
  }
}

const stack = []
let root //  The root node 
let currentParent //  Staging the current parent node 
parseHTML(template, {
  // some options...

  //  The callback that resolves to the beginning of the tag position 
  start(tag, attrs, unary) {
    //  establish  AST  node 
    let element = createASTElement(tag, attrs, currentParent)

    //  A processing instruction : v-for v-if v-once
    processFor(element)
    processIf(element)
    processOnce(element)
    processElement(element, options)

    //  Handle  AST  Trees 
    //  The root node does not exist , Then set the element as the root node 
    if (!root) {
      root = element
      checkRootConstraints(root)
    }
    //  There is a parent node 
    if (currentParent) {
      //  Push the element into the child node of the parent node 
      currentParent.children.push(element)
      element.parent = currentParent
    }
    if (!unary) {
      //  Non single tags need to be put on the stack , And switch the position of the current parent element 
      currentParent = element
      stack.push(element)
    }
  },
})

Process end tag

The logic of tag ending is simpler , Just remove the last unclosed tag in the stack , Just close it .

parseHTML(template, {
  // some options...

  //  The callback that resolves to the end of the tag position 
  end() {
    const element = stack[stack.length - 1]
    const lastNode = element.children[element.children.length - 1]
    //  Dealing with trailing spaces 
    if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
      element.children.pop()
    }
    //  Out of the stack , Reset the current parent node 
    stack.length -= 1
    currentParent = stack[stack.length - 1]
  },
})

Processing text

After processing the label , You also need to process the text inside the tag . There are two ways to deal with text , One is text with expressions , Another is pure static text .

parseHTML(template, {
// some options...

//  Callback when parsing to text 
chars(text) {
if (!currentParent) {
//  If there is no parent node outside the text node, it will not be processed 
return
}

    const children = currentParent.children
    text = text.trim()
    if (text) {
      // parseText  Used to parse expressions 
      // delimiters  Represents an expression identifier , The default is  ['{{', '}}']
      const res = parseText(text, delimiters))
      if (res) {
        //  expression 
        children.push({
          type: 2,
          expression: res.expression,
          tokens: res.tokens,
          text
        })
      } else {
        //  static text 
        children.push({
          type: 3,
          text
        })
      }
    }

}
})

So let's see parseText How to parse expressions .

//  Construct regular expressions that match expressions 
const buildRegex = (delimiters) => {
  const open = delimiters[0]
  const close = delimiters[1]
  return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
}

function parseText(text, delimiters) {
  // delimiters  The default is  {{ }}
  const tagRE = buildRegex(delimiters || ['{{', '}}'])
  //  No match to expression , Go straight back to 
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = (tagRE.lastIndex = 0)
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    //  Where the expression begins 
    index = match.index
    //  Extract the static character in front of the start position of the expression , Put in  token  in 
    if (index > lastIndex) {
      rawTokens.push((tokenValue = text.slice(lastIndex, index)))
      tokens.push(JSON.stringify(tokenValue))
    }
    //  Extract the content inside the expression , Use  \_s()  Method package 
    const exp = match[1].trim()
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  //  There are other static characters after the expression , Put in  token  in 
  if (lastIndex < text.length) {
    rawTokens.push((tokenValue = text.slice(lastIndex)))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens,
  }
}

First, we extract the expression through a regular section :

<center> Extract expression </center>
It can be a bit difficult to look at the code , Let's go straight to the example , Here's a text that contains the expression .

<div> Log in :{{isLogin ? ' yes ' : ' no '}}</div>

<center> Running results </center>

<center> Parse text </center>

optimize

Some of the above are handled by , We got it Vue Template AST. because Vue Design is responsive , So I got it AST After that, a series of optimizations are needed , Make sure that static data doesn't get into the virtual DOM The update phase of , To optimize performance .

export function optimize(root, options) {
  if (!root) return
  //  Tag static nodes 
  markStatic(root)
}

Simply speaking , That is to put all static nodes static Property is set to true.

function isStatic(node) {
  if (node.type === 2) {
    //  expression , return  false
    return false
  }
  if (node.type === 3) {
    //  static text , return  true
    return true
  }
  //  Some conditions are omitted here 
  return !!(
    (
      !node.hasBindings && //  No dynamic binding 
      !node.if &&
      !node.for && //  No,  v-if/v-for
      !isBuiltInTag(node.tag) && //  It's not a built-in component  slot/component
      !isDirectChildOfTemplateFor(node) && //  be not in  template for  Within the loop 
      Object.keys(node).every(isStaticKey)
    ) //  Non static nodes 
  )
}

function markStatic(node) {
  node.static = isStatic(node)
  if (node.type === 1) {
    //  If it's an element node , You need to traverse all the child nodes 
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        //  If there is a child node that is not static , Then the node must also be dynamic 
        node.static = false
      }
    }
  }
}

generate

Get optimized AST after , You need to AST Turn into render Method . Or use the previous template , Let's see what the generated code looks like :

<div>
  <h2 v-if="message">{{message}}</h2>
  <button @click="showName">showName</button>
</div>
{
render: "with(this){return \_c('div',[(message)?\_c('h2',[_v(_s(message))]):\_e(),\_v(" "),\_c('button',{on:{"click":showName}},[_v("showName")])])}"
}

Expand the generated code :

with (this) {
  return \_c('div', [
    message ? \_c('h2', [_v(_s(message))]) : \_e(),
    \_v(' '),
    \_c('button', { on: { click: showName } }, [_v('showName')]),
  ])
}

It must be silly to see a bunch of underscores here , there _c The corresponding is virtual DOM Medium createElement Method . Other underline methods are in core/instance/render-helpers All of them are defined , What does each method do not expand .

<center>render-helpers</center>

The specific transformation method is some simple character splicing , Here's the part that simplifies the logic , Don't tell too much .

export function generate(ast, options) {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns,
  }
}

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

summary

Sort out the Vue The whole process of template compilation , It's all about analysis HTML Generate AST Part of . This article only describes the main process , A lot of details are omitted , such as : Yes template/slot To deal with 、 Instruction processing and so on , If you want to know the details, you can read the source code directly . I hope you can gain something after reading this article .

版权声明
本文为[Li Congfan]所创,转载请带上原文链接,感谢
https://cdmana.com/2020/12/20201224084735393P.html

Scroll to Top