compile

When using VUE, we often write some templates, for example:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Vue template compilation</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

</head>
<body>
 <div id="demo">
    <h1>Vue template compilation</h1>
    <p v-if="txt">xx{{name}}yy</p>
    <div :hhh="name" xxx="123"></div>
    <ul>
        <li v-for="item in names">{{item}}</li>
    </ul>
    <p v-text="txt" @click="addText"></p>
  </div>
  <script>
      // Create an instance
      const app = new Vue({
          el: '#demo'.data: {foo:'foo'.txt: "text"}})</script>
</body>
</html>
Copy the code

In the addvue.jsAfter we are in<div id="#demo"></div>The contents are different from the elements we see in the real Dom, so take a lookVueAfter processing,DomThe element becomes this:

In the previous Vue bidirectional data binding demo, Compile was used to complete the template conversion to the real Dom node. Using document. CreateDocumentFragment () to create a document fragments, is an in-memory Dom node. We just did a simple process at the time. But the actual compilation of Vue is much more complicated than this: we need to parse instructions like V-if and V-for, as well as v-bind, V-ON, v-text, and so on.

This article explores how Vue performs the

internal node conversion.

Vue.prototype.$mount

According to the source code analysis (1) in the previous section, we know that new Vue(options) will be initialized accordingly. The previous focus was on initialization of the data. In the process of data initialization, the data will be processed in a responsive manner. After the data processing is completed, we still cannot see the correct view on the browser. At this time, we need to call the $mount method on the Vue prototype to achieve the mount operation, so as to display the correct view on the browser.

To mount this operation, we need to parse the template string written in options.el or options.template. What is a template string? Simply put, this is HTML content containing Vue directives expressed as strings:

// In options.el, we specify option.el as the template
// for example option.el = 'demo'
<div id="demo">
    <h1>Initialization Process</h1>
    <p>{{foo}}</p>
</div>
// In vue, the object is converted to the following form: template is a template string
let template = '< div id = "demo" > < h1 > initialization process < / h1 > < p > {{foo}} < / p > < / div >'
Copy the code

As you will see in the development project, we actually sometimes in the new Vue(Options)

The $mount method is not called. As we learned in the previous article, the _init method is implemented in the constructor of the Vue, and the _init method is implemented in initMixin(), which implements Vue related initialization. At the end of the _init function, check if options has an EL attribute. If so, call the vm.$mount method. New Vue(options).$mount(‘#app’) is a new Vue(options).$mount(‘#app’) is a new Vue(options). So you can guess that $mount converts our template string into a real Dom node for display in the browser, and creates a watcher for responsive updates to the view during traversal.

In Vue, string template compilation is performed when the $mount method is executed. Let’s take a look at the implementation of this method on the Web platform:

import { compileToFunctions } from './compiler/index'

// Extension $mount for Web environment
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  // Get the DOM node
  el = el && query(el)

  const options = this.$options
  // resolve template/el and convert to render function
  if(! options.render) {// Find template without render, and render template as a template string
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) = = =The '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        return this}}else if (el) {
      // If there is no render and template, we will go to el and convert the content of el node to template string
      template = getOuterHTML(el)
    }

    // Compile the template string, returning the render function
    if (template) {
      / / access to render
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV ! = ='production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // Set render to options.render
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // Execute mount method on Vue prototype
  return mount.call(this, el, hydrating)
}
// Convert the el node to a template string, i.e., el => template
function getOuterHTML (el: Element) :string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
Copy the code

In the $mount method of the Web platform extension, do the following:

  1. To obtainrenderFunction and place it on the VM instance objectrenderOn.
    • Ignore options.render when presentoptions.templateandoptions.elSkip all subsequent content in this step
    • When there isoptions.templateWhen to ignoreoptions.el, get the template string for template, and callcompileToFunctionsMethod to get the render method corresponding to the template template string and place it in thevm.options.renderon
    • When options.el is present, we get the template string of the node corresponding to elcompileToFunctionsMethod, get the corresponding Render method, and place it in thevm.options.renderon
  2. On the Vue instance prototype$mountMethods.

Options el, template, and render are mutually exclusive. Priority is render>template>el. The render function takes precedence over the template when render is present. Consider using the EL parameter last.

Vue. Prototype.$mount implementation actually for different platforms to do the corresponding extension, the current source code weeX and Web two platforms, this article only about the specific implementation of the Web platform. Interested can go to vUE source code to view the specific implementation of WEEX.

After looking at the web platform extension to $mount, we’ll move on to the vue.prototype.$mount prototype method. Web platform source, the method of position: the SRC \ platforms \ web \ runtime \ index js:

import { mountComponent } from 'core/instance/lifecycle'
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  // Get the el element
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Copy the code

Core /instance/lifecycle mountComponent

export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  vm.$el = el
  // Render is a function that creates an empty node when no render exists.
  if(! vm.$options.render) { vm.$options.render = createEmptyVNode }// Call the beforeMount lifecycle hook function
  callHook(vm, 'beforeMount')

  // Pass in Watcher's update view function
  let updateComponent = () = > {
    vm._update(vm._render(), hydrating)
  }

  // Create Watcher for the component and pass updateComponent as the second parameter
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)

  // Execute the Mounted lifecycle hook function
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')}return vm
}
Copy the code

This function does the following:

  1. callbeforeMountLifecycle hook functions
  2. Define a function to update the DomupdateComponent
  3. createwatcherAnd will getupdateComponentThe incomingwatcherAs theexprOrFnThe parameters.
  4. createwatcherWhen will beDep.targetPoint to yourself and thenWathcerThe constructor executes a pass insideupdateComponentFunction. This function is used internallyoptions.dataData in theObject.Object.definePropertyIn thegetAnd collect them by dependency,updateComponentThe Dom view is also rendered.
  5. The last callmountedThis lifecycle hook function.

Every time we parse a Vue string template, that is, a Vue component, we create a Watcher. Pass in the updateComponet function. Without a doubt, this function is the way to update the Dom. Of these, the most important is undoubtedly getting the function that updates the template. In the source code above, we can see that this function does two things:

  1. callVue.prototype._render, returns the result asVue.prototype._updateThe parameters. This method returns a virtual Dom. A virtual Dom is a JS object that represents a real Dom node, more on that later.
  2. callVue.prototype._update, which will executepatchMethod to update the view.

See that Vue creates a watcher when it calls vm.$mount, recall the compiler implemented in Vue source code analysis (I) in the previous section. We iterate through all element nodes and text nodes directly through recursion. When we find Vue related instructions, we parse them and generate an instance of Watcher








Of these, the most important part of the method is undoubtedly the updateComponent function that defines the update template. In the source code above, we can see that this function does two things

  1. callVue.prototype._render, returns the result asVue.prototype._updateThe first argument to.
  2. callVue.prototype._update, which will executediffAlgorithm, complete the view update.

This is what vue.prototype.$mount looks like. To summarize what vue.prototype. $mount does on the Web platform:

  1. usingcompileToFunctionsMethod, according to the Vue constructoroptionsIn theelortemplateConvert to the correspondingrenderfunction
  2. performboforemountHook function
  3. To create aupdateComponetThe update component view function is used to update the component view
  4. To create awatcher, triggering the dependency collection of data, willupdateComponentIncoming ownexprOrFnParameter to update the view when the data changes.
  5. When creating awatcherThe constructor is called once during theupdateCompont, the function will changerenderThe corresponding virtual Dom is converted into a real Dom node and rendered on the browser
  6. performmountedHook function

CompileToFunctions are definitely the focus, and update views will definitely look at the _update and _render methods. The _update method is covered in the Vue source code analysis (3)—– update strategy. This article focuses on compiling part of the content.

Compile

In the above method, we know that compileToFunctions parse passed template strings into render functions. To do this, a separate folder Compile is created in Vue to do this. We can divide its parsing of string templates into three parts:

  1. Parse: Parses a string template to generate an AST.
  2. Optimize: Traversing AST markup static nodes.
  3. Generate code generation: Generate render functions based on the AST.

Here is a reference site where the Vue template you wrote on the left will be converted to the corresponding render function on the right. If you are interested, you can write your own small Demo: Vue template compilation.

1. parse

As we know above, parse primarily parses template strings into an AST(Abstract Syntax Tree). AST is an abstract syntax tree: a tree representation of the abstract syntax structure of source code, specifically the source code of a programming language. Sounds pretty impressive, but what does an AST look like in Vue?

For example, suppose we create a

element:

<div id="demo">
    <h1>Vue template compilation</h1>
    <p v-if="txt">xx{{name}}yy</p>
    <div :hhh="name" xxx="123"></div>
    <ul>
        <li v-for="item in names">{{item}}</li>
    </ul>
    <p v-text="txt" @click="addText"></p>
</div>
Copy the code

Some AST attributes generated in Vue are as follows:

{
	type: 1.tag: "div".parent: undefined;
    attrsMap: { id: "demo" },
    attrList: [{name: "id".value: "demo"}].children: [{type: 1.tag: "h1".parent: {type:1.tag: "div". },attrsMap: {},
            attrsList: []
            children: [{type: 3.text: "Vue template"}]
            
        },{
        	type: 3.text: ""}, {type: 1.tag: "p".attrsMap: {v-if: "txt"},
            children: [{type:2.expression: ""xx"+_s(name)+"yy"".text: "xx{{name}}yy".tokens: ["xx",{@binding:"name"}, "yy"]}],if: "txt"
            ifConditions: [{exp:"txt".block: {type: 1.tag:"p". }}].parent: {type:1.tag:"div". }}, {type:3.text: ""}, {type: 1.tag: "div".attrs: [{name: "hhh".value: "name"}, {name: "xxx".value: "123"}].attrsList: [{name: ":hhh".value: "name"}, {name: "xxx".value: "123"}].attrsMap:{:hhh: "name".xxx: "123"},
            hasBindings: true.parent: {type: 1.tag: "div".attrsList:....}
        },{
        	type:3.text: ""}, {type: 1.tag: "ul".parent: {type: 1.tag: div... },children: [{
            	type: 1.tag: "li".attrsMap: {v-for: "item in names"},
                for: "names".alias: "item".children: [{
                	type: 2.expression: "_s(item)".tokens: [{@binding: "item"}].text: "{{item}}",}],parent: {type: 1.tag: "ul". }}}, {type: 3.text: ""}, {type: 1.tag: "p".hasBingding: true.directives: {name: "text".rawName: "v-text".value: "txt"},
          events: {click: {value: "addText"}},
          parent: {type: 1.tag: "div". }}}]Copy the code

Finally, a tree structure is used to represent our root node, which can clearly describe the related attributes of tags and dependencies between tags. How do you parse a template string into an AST? Here’s a picture of the process:

When we match a string beginning with <, we have a start tag, and when we match a string beginning with
that we encountered all the strings in < to > are extracted from the entire HTML, where the start tag will have attributes that we parse.

When our string does not begin with <, find the subscript of the first < in the entire string, and cut off all characters from the beginning to the < subscript and treat them as text (regardless of the presence of < characters).

According to the flow of the diagram, we need to match the start tag, end tag and text content. Among them, when we match the start tag, we need to parse the attributes of the start tag, and when we parse the text content and the end tag, we need to make corresponding processing for them. We need to be able to figure out how to match a string with a re first, so we should need at least the following re

const ncname = '[a-zA-Z_][\\w\\-\\.]*' 
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `
const startTagOpen = new RegExp(` ^ <${qnameCapture}`)// Match the start tag with '
const startTagClose = /^\s*(\/?) >/   // Matches the end of the start tag with the > or /> end
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `) // Matches the end tag
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /   // Match attributes
const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g // Match text in the shape of {{XXX}}
const forAliasRE = / (. *?) \s+(? :in|of)\s+(.*)/  // Matches the contents of the V-for expression, such as' item in arr '
const dirRE = /^v-|^@|^:|^#/ // Matches attributes beginning with 'v-','@' or ':'
Copy the code

Here’s a site for understanding regular expressions: Regular parsing

We parse the template in a loop, so every time we match and parse a piece of content, we need to remove the matched content and then match the rest. So we need a function that intercepts a string template string:

function advance (n) {
  index += n
  html = html.substring(n)
}
Copy the code

Then try implementing a function that parses the template string:

export function parseHtml (html, options){
  let index = 0;
  while(html) {
    let textEnd = html.indexOf('<')
    if (textEnd === 0) {
      if (html.match(endTag)) {
      	advance(endTagMatch[0].length);
        // todo: process the start tag
        continue
      }
      if (html.match(startTagOpen)) {
        advance(endTagMatch[0].length);
        // todo: handles the closing tag
        continue}}else {
      if(textEnd > 0) {// Intercepts text
      	text = html.substring(0, textEnd);
      }else{
      	// The entire template is text nodes (no '<' matches)
      	text = html
      }
      if(text){
      	advance(text.length)
      }
      // todo: handle text nodes
      continue}}}Copy the code

In the example above, the compilation process of Vue is simplified. In this case, we only deal with tags and text. Where: In each while loop, the position at the beginning of the < character is first found. If the HTML begins with < (that is, textEnd is 0), we treat it as an element node, otherwise as a text node. The element node is processed by first matching the end tag, such as

, , and if not, trying to match the start tag, such as

,

. Each time a tag is matched, we need to process the matched content accordingly.

We know that the resulting AST is a JS object with a tree structure, so we need to add a stack (first in last out) to establish parent-child relationships. When we match startTagOpen, we place the parsed content on the stack as an object. When a match is made to startTagClose, the stack is unstacked and the parent-child relationship between labels is established. When the text node is parsed, the relationship between the text node and the current parent node is directly established.

So we define three functions start, end, and chars in the options passed to parseHtml to help us maintain the stack and generate the AST tree structure:

  • Start: This function is called when the start label is parsed, saves the label data, and pushes the node object onto the stack.
  • End: This function is called when the end tag is parsed, the unstack is performed, and the parent node is bound.
  • Chars: This function is called when the parsed text node is added to the parent node.

We also need to define three variables outside of parseHtml for stack maintenance

  • stack: is used to hold parsed tags (stored as JS objects), is a stack (advanced back out, in JS can be regarded as a only usepopandpushArray of methods)
  • currentParent: Stores the reference of the parent label node of the current label
  • root: Stores the root node label

From this, we can write the following code:

options: { start: (startTagMatch) =>{ // todo: // Update currentParent and root variables}, end () {// todo: }, chars (text) {// todo: string processing}}Copy the code

Tags such as and

that have no closing tag are self-closing tags;

,

are non-self-closing tags.

Start method: This method is called when the start tag is resolved. This method takes parsed attributes, tag names and other parameters. Create a JS object based on the content of the parameter to hold the label information. End method: This method is called when the end tag is parsed. This method unstacks the nodes and saves the parent-child relationship between nodes. Chars method: This method is called when a text node is parsed. The method first tries to see if the text matches defaultTagRE. If the match indicates that the text is not static, otherwise it is treated as static. Finally, a reference is established between the text node and the parent node.

Given the three methods in Options, you can extend parseHtml.

const stack = [];
const currentParent = root = null;
export function parseHtml (html, options){
  let index = 0;
  while(html) {
    let textEnd = html.indexOf('<');
    if (textEnd === 0) {
      // Parse the HTML node
      const endTagMatch = html.match(endTag);
      if (endTagMatch) {
      	advance(endTagMatch[0].length);
        parseEndTag(endTagMatch[1]);
        continue;
      }
      if (html.match(startTagOpen)) {
      	const start = html.match(startTagOpen)
      	advance(start[0].length);
        const startTagMatch = parseStartTag(start);
        options.start(startTagMatch);
        continue; }}else {
      // Parse text nodes
      let text;
      if (textEnd >= 0) {
        text = html.substring(0, textEnd);
      }

      if (textEnd < 0) {
        text = html;
      }

      if (text) {
        advance(text.length);
      }
      options.chars(text);
      continue; }}}Copy the code

Start by implementing a method that parseStartTag the start tag:

function parseStartTag (start) {
  const match = {
    tagName: start[1].attrs: [].start: index
  };
  let end, attr;
  // Loop through attributes in the start tag until '>' or '/>' is encountered
  while(! (end = html.match(startTagClose)) && html.match(attribute))) { attr.start = index advance(attr[0].length)
    attr.end = index
    match.attrs.push(attr)
  }
  // Handle the array of attributes
  match.attrs = match.attrs.map(args= >{
    const value = args[3] || args[4] || args[5] | |' '
    return {
      name: args[1],
      value
    }
  })
  if (end) {
    match.unarySlash = end[1]
    advance(end[0].length)
    match.end = index
    return match
  }
}
Copy the code

In this method, we define a match object to hold our parsed results, which include:

  • tagName: Indicates the label name
  • attrs: Holds a parsed list of properties in an array
  • start: The beginning subscript of the label in the template string
  • end: The subscript at the end of the tag in the template string
  • unarySlash: indicates whether the label is self-closing

Define a loop inside the method. When the end of the tag cannot be matched and the attributes of the tag can be matched, the loop body is entered. Inside the loop body, the parsed attributes are stored on mate.attrs.

When we finally break out of the loop, we’ve parsed all the tags of the element nodes, parsed them, and finally added the unarySlash, end attribute to match. It then parses to match and returns. Finally, the match is passed in by calling options.start on the outer parseHtml. Try writing the options.start method.

start(startTagMatch) {
  const element = {
    type: 1.tag: startTagMatch.tagName,
    lowerCasedTag: startTagMatch.tagName.toLowerCase(),
    attrsList: startTagMatch.attrs,
    attrsMap: makeAttrsMap(startTagMatch.attrs),
    parent: currentParent,
    children: []}if(! root) { root = element }if(! match.unarySlash){// The label is not self-closing
    currentParent = element
    stack.push(element)
  }else{
    // Self-closing label
    // Save the relationship between the label and the parent node directly
    currentParent.children.push(element)
    element.parent = currentParent
  }
}
// Convert attrs array to map (key-value pairs)
function makeAttrsMap (attrs) {
  const map = {}
  for (let i = 0, l = attrs.length; i < l; i++) {
      map[attrs[i].name] = attrs[i].value;
  }
  return map
}
Copy the code

We define a js object element in the start method to hold the tag information:

  • type: Node type
  • tag: Indicates the label name
  • lowerCasedTag: Lowercase of the label name
  • attrsList: Holds the property list in an array
  • attrsMap: Saves attributes (JS objects) with key-value
  • parent: The parent label node object reference of the current label
  • children: Saves the child node reference for the current label

The value of type of all nodes with labels is 1. In this function, check whether root exists first. If not, the root entry into the function is set to Element, and this judgment is used to set the root node when the function is first entered. If the tag is self-closing, then there is no child node. Add itself (element) to currentParent. Children. If not, add the current Element object to the stack and set currentParent to itself (Element).

Similarly, we continue to refine parseEndTag in parseHtml

function parseEndTag(tagName){
	let pos, lowerCasedTagName
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break}}}else {
      pos = 0
    }
    for (let i = stack.length - 1; i >= pos; i--) {
      options.end(stack[i].tag, start, end)
    }
}
Copy the code

In parseHtml, we pass the tag name of the closed tag as an argument to parseEndTag, and then use toLowerCase to get the lower case lowerCasedTagName of the tag name. Traverse the stack from the top of the stack (the last element of the array) to the bottom of the stack (the last element of the array). Find the subscript of the node element with the same name as the tag (if not, assign the subscript to 0). Then loop the stack up to the subscript of the node element, execute options.end to unstack and bind the parent node.

end () {
  // Establish parent-child relationship and maintain stack
  const element = stack[stack.length - 1]
  stack.length -= 1
  currentParent = stack[stack.length - 1]
  currentParent.children.push(element)
  element.parent = currentParent
}
Copy the code

End: Unstack the top element, assign the top element to Element, change the top direction (stack.length-=1), add Element to the children array of the top element, and finally point element’s parent property to JS objects of the top element.

One might ask: why write a loop when you can just unstack a parseEndTag? It is usually possible to unstack it, but it is possible to write and forget , so we need to find the same tag name closest to us on the stack. This way, the hierarchy can be established even if you forget to write the closing tag.

Finally, let’s implement parseText and options.chars

options.chars

chars(text){
  let element = {}
  if(defaultTagRE.test(text)){
    let res = parseText(text)
    element = {
      type: 2, 
      text, 
      parent: currentParent ... res } }else{
    element = { 
        type: 3,
        text,
        parent: currentParent
    }
  }
  currentParent.children.push(element)
}
Copy the code

Call option.charts in parseHtml and pass in the text string text. ParseText (text) parseText(text) parseText(text) parseText(text) parseText() parseText() parseText() parseText() parseText() parseText(); Save parsed content on element; If {{xx}} is not present, the content of the text node is static and will not change. Set element.type to 3 and place text on the element. Finally, add element to the currentParent.Children array.

ParseText parses dynamic text nodes:

function parseText(text){
  // If '{{}}' exists, the text content is split into multiple parts and stored in an array
  const tokens = []
  let lastIndex = defaultTagRE.lastIndex = 0
  let match, index, tokenValue
  // Process all content in the text that conforms to defaultTagRE, cutting the text into an array
  while ((match = defaultTagRE.exec(text))) {
    index = match.index
    XXX {{yyy}} ZZZ into [' XXX ','yyy',' ZZZ ']
    if (index > lastIndex) {
      tokenValue = text.slice(lastIndex, index)
      tokens.push(JSON.stringify(tokenValue))
    }
    // Get XXX in {{XXX}}
    const exp = match[1].trim()
    tokens.push(`_s(${exp}) `)
    lastIndex = index + match[0].length
  }
  // Process the last '}}' in the string to the content of the string (if present)
  if (lastIndex < text.length) {
    tokens.push(JSON.stringify(text.slice(lastIndex)))
  }
  return {
    expression: tokens.join('+'),
    tokens
  }
}
Copy the code

If we had a text like this:

// Assume name is 'Evan'hello {{name}}! welcome backCopy the code

The last thing we return is:

{
	expression: 'hello Evan! welcome back'.tokens: ['hello'.'_s(name)'.! "" welcome back"]}Copy the code

_s is short for toString(). $data. Name = vm.$data. Name = vm.$data.

Finally, how to implement v-text and V-if and v-for: We need to add the following to options.start

start(startTagMatch) {
  const element = {
    type: 1.tag: startTagMatch.tagName,
    lowerCasedTag: startTagMatch.tagName.toLowerCase(),
    attrsList: startTagMatch.attrs,
    attrsMap: makeAttrsMap(startTagMatch.attrs),
    parent: currentParent,
    children: []}// ---- add content
  processIf(element); // Try to parse v-if
  processFor(element); // Try to parse v-for
  processAttr(element); // Try to parse v-text,v-html...
  // ---- 
  / /...
}

Copy the code

If and ifConditions are added to the V-IF node in Vue to save the judgment condition and influence range, and the V-IF attribute is deleted from attrList and attrMap. So we need a delete function getAndRemoveAttr

function getAndRemoveAttr(element, name){
	var val;
    if((val = el.attrsMap[name]) ! =null) {
      var list = el.attrsList;
      for (var i = 0, l = list.length; i < l; i++) {
        if (list[i].name === name) {
          list.splice(i, 1);
          delete el.attrsMap[name];
          break}}}return val
}
Copy the code

We define a getAndRemoveAttr to try to find the name attribute from the node’s property array, remove it from the array, and return the value of the name attribute or undefined

With this function we try to implement the processIf function:

function processIf (el) {
    const exp = getAndRemoveAttr(el, 'v-if'); 
    if (exp) {
        el.if = exp;
        if (!el.ifConditions) {
            el.ifConditions = [];
        }
        el.ifConditions.push({
            exp: exp,
            block: el
        });
    }
}
Copy the code

In the same way, the V-for directive ends up on the object’s for and alias properties, which are the names of the elements in the loop array. Following the logic of v-F, we implement the following V-for:

function processFor (el) {
    let exp = getAndRemoveAttr(el, 'v-for');
    if (exp) {
        const inMatch = exp.match(forAliasRE);
        el.for = inMatch[2].trim();
        el.alias = inMatch[1].trim(); }}Copy the code

After processIf and processFor are implemented, there may be v-text, V-html content in the property array, and we’ll use processAttrs to parse the other instructions. Let’s just implement v-text, V-ON, v-bind for simplicity. Other implementation readers have the interest and energy to read the source code. To implement V-text, we need a function called addDirective, which saves the content of the directive used by the node to the directive property of the AST node

export function addDirective (el, name, rawName, value) {
  (el.directives || (el.directives = [])).push({
    name,
    rawName,
    value,
  })
}
Copy the code

Then we try to write processAttrs

function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
    	el.hasBindings = true
      	name = name.replace(dirRE, ' ')
      	const argMatch = name.match(argRE)
      	addDirective(el, name, rawName, value)
      }
     }
   }
}
Copy the code

We need to do something special for v-bind and V-ON

  • v-onCreate one in the AST nodeeventProperty holds relevant content. writeaddHandlerAssist to complete
  • v-bindCreate one in the AST nodeattrProperty holds the contents of the binding. writeaddAttrAssist to complete

Implement addHandler and addAttr functions:

addAttr(el, name, value){
	const attrs = el.attrs || (el.attrs = [])
	attrs.push({name, value})
}
addHandler(el, name, value){
	const event = el.event || (el.event = {})
    event[name] = {value}
}
Copy the code

So extend processAttrs:

function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      el.hasBindings = true
      if (/^:|^v-bind:/.test(name)) { // v-bind
        name = name.replace(/^:|^v-bind:/.' ')
        addAttr(el, name, value, list[i], isDynamic)
      }else if(/^@|^v-on:/.test(name)){ // v-on
      	name = name.replace(/^@|^v-on:/.' ')
      	addHandler(el, name, value)
      }else{ // v-text, v-html
      	name = name.replace(dirRE, ' ')
      	const argMatch = name.match(argRE)
      	addDirective(el, name, rawName, value)
      }
     }
   }
}
Copy the code

A simple parsing process was implemented.

2. optimize

The optimizer’s job is to find and mark static subtrees in the AST tree generated after parse completes. This step involves the patch function later. When the view is updated through virtual Dom comparison, we can avoid comparison on some static nodes (i.e. nodes whose content does not change), so that we can save some performance consumption. So the benefits of doing this:

  • No new nodes need to be created for static subtrees each time the Dom is rendered.
  • In the virtual DomDiffWe don’t have to compare static nodes.

With optimize we add static to each node of the AST tree resulting from the parse process to indicate whether the node is static.

During the parse process above, different nodes generate type values based on different values.

type instructions
1 Element nodes
2 Dynamic text node with variable
3 A plain text node with no variables

Obviously when type is 2, we can determine that it cannot be a dynamic node, and when type is 3, it must be a static node. When type is 1, all the following conditions must be met to determine that it is a static node.

  • There can bev-.@.:Leading properties
  • You can’t usev-if.v-fororv-elseThe instruction of
  • The label name cannot beslotorcomponentSuch as Vue built-in labels
  • Tag name must be Vue reserved tag (divided into HTML reserved tag and SVG reserved tag)
  • The parent of the current node cannot bev-forInstruction label
  • There are no properties on nodes that only dynamic nodes have, as seen in the code discussed.

Here we simplify the judgment by determining only whether the element has an attribute starting with a V -. So we can write a function, isStatic, to determine whether a node isStatic.

function isStatic (astNode) {
  if (astNode.type === 2) { 
    return false
  }
  if (astNode.type === 3) { 
    return true
  }
  return(! astNode.if && ! astNode.for && ! astNode.hasBindings); }Copy the code

The Optimize core implementation in Vue has only two functions: markStatic and markStaticRoots

function optimize (root) {
  if(! root)return
  markStatic(root)
  markStaticRoots(root, false)}Copy the code

Since we know that the AST is a tree structure, we mark static nodes in two steps

  1. Mark all static nodes and label them, i.emarkStatic(root)
  2. Identify and label all static root nodes by marking all static nodes, i.e(root, false)

Mark all static nodes, then we need to traverse the AST tree structure. Traversing a tree structure is usually done recursively.

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if(! child.static) { node.static =false}}}}Copy the code

And then the markStaticRoots

function markStaticRoots (node) {
    if (node.type === 1) {
        if(node.static && node.children.length && ! ( node.children.length ===1 &&
        node.children[0].type === 3
        )) {
            node.staticRoot = true;
            return;
        } else {
            node.staticRoot = false; }}}Copy the code

When an element node is a static node with child nodes, and the node is not a static node with only one text type (in this case, it may be that the cost of optimization is greater than the cost of subsequent diff), the staticRoot of the node is set to true. Otherwise set staticRoot to false

3. generate

generateConvert the AST we obtained earlier torenderFunction to obtain the vDom(virtual Dom node) described earlierVue Template ExplorerWe can see the render function generated after editingAmong them_c._vEtc is a short form of other method names:

type Create method The alias parameter
Element nodes createElement _c Tag, data, children, etc.
Text node createTextVNode _v Val (text content)
Create a list of renderList _l Val (iterated content), render(function to render the list)
Creating an empty node createEmptyVNode _e There is no parameter
We’re not going to go into all the details of these functions hereVue source code analysis (three)“Will be introduced in detail.

What we need to do now is to take the string, concatenate it into an executable internal code for the Function from the table, and then use new Function(code) to generate the Render Function.

Now that we know what the AST looks like, how do we turn the AST into the render function again? After one or two steps, we have an AST tree marked with static nodes. The last step is to convert the AST into the render function, and when the render function is executed, we get the virtual Dom. So how do you do that?

First there must be a function genElement for element nodes and genText for text nodes. In the function that handles element nodes, we need to do v-if, v-for genFor, genIf.

And then we need to ask you to do things like V-ON and V-bind. GenData does the processing and finally we need to process the child node, so we also need genChildren

Then we tried to write genElement:

function genElement (el) {
  if(el.for && ! el.forProcessed) {return genFor(el)
  } else if(el.if && ! el.ifProcessed) {return genIf(el)
  } else {
      data = genDate(el)
      const children = genChildren(el)
      let code
      code = `_c('${el.tag}'${
        data ? `,${data}` : ' ' // data
      }${
        children ? `,${children}` : ' ' // children
      }) `
      return code
  }
 }
Copy the code

And then I’m going to refine the functions, genFor and genIf first

function genFor (el) {
  el.forProcessed = true
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ' '
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ' '

  return `_l((${exp}), ` +
    `function(${alias}${iterator1}${iterator2}) {` +
      `return ${genElement(el)}` +
    '}) '
}

function genIf (el) {
    el.ifProcessed = true
    if(! el.ifConditions.length) {return '_e()'
    }
    return ` (${el.ifConditions[0].exp})?${genElement(el.ifConditions[0].block)}: _e()`
}
Copy the code

Perfect genData:

// Process instructions
function genDirectives(el){
  const dirs = el.directives
  if(! dirs)return
  let res = 'directives:['
  for (i = 0, l = dirs.length; i < l; i++) {
 	res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:The ${JSON.stringify(dir.value)}` : ' '
      }}, `
  }
  return res.slice(0, -1) + '] '
}

function genData (el) {
  let data = '{'
  // Add directives
  const dirs = genDirectives(el)
  if (dirs) data += dirs + ', '

  // Add attributes
  if (el.attrsMap) {
    data += `attrs:The ${JSON.stringify(el.attrsMap)}, `
  }
  // Add events
  if (el.events) {
    data += `on:The ${JSON.stringify(el.events)}, `
  }

  data = data.replace($/ /,.' ') + '} ' // If there is a ',' end remove the comma and add '}'
  return data
}
Copy the code

And finally genChildren:

// This method calls genNode
function genChildren (el) {
  const children = el.children;

  if (children && children.length > 0) {
      return `${children.map(genNode).join(', ')}`; }}function genNode(el){
	if (el.type === 1) {
        return genElement(el);
    } else {
        returngenText(el); }}function genText(el){
	if(el.text){ Static text node
    	reutrn `_v(${el.text}) `
    }
    if(el.expression){ // Dynamic text node
    	return `_v(${el.expression}) `}}Copy the code

At this point, we’ll write compileToFunctions to convert the Vue template string to the Render function.

function compile(){
  const ast = parse(template.trim(), options)
  optimize(ast, options)

  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
  }
}

Copy the code

conclusion

In this article, we will focus on how to convert the string we wrote into render and execute the render function to get the virtual Dom. With the virtual Dom, you can write the patch function in view update to compare the old and new virtual Dom and update the Dom view with the smallest operation steps.

Later, I will integrate the content of this article into a small Demo and put it on my Github. If you are interested, you can follow me

In the next article, we will talk about virtual Dom, patch comparison algorithm and Vue’s asynchronous batch update strategy. Stay tuned for Vue source code analysis (iii)—–Vue update strategy

Reference Documents:

Vue. Github.com/vuejs/vue js source code





If you think it’s good, please give it a thumbs up. The author writes a technical blog at least once a month

Faith is the bird that feels the light and sings when the dawn is still dark.


— tagore