Interpretation of Vue source code -- Analysis of compiler

When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

Special instructions

Due to the limited length of the article, the interpretation of Vue source code (8) - the analysis of compiler is divided into two articles Interpretation of Vue source code (8) -- Analysis of compiler (I) A supplement to, so please open it at the same time when reading Interpretation of Vue source code (8) -- Analysis of compiler (I) Read together.

processAttrs

/src/compiler/parser/index.js

/**
 * Process all attributes on the element:
 * v-bind The command becomes: El Attrs or EL dynamicAttrs = [{ name, value, start, end, dynamic }, ...],
 *                Or the attribute of props must be used, which becomes el props = [{ name, value, start, end, dynamic }, ...]
 * v-on The command becomes: El Events or EL nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] }
 * Other instructions: El directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
 * Native attribute: El Attrs = [{name, value, start, end}], or some attributes that must use props, become:
 *         el.props = [{ name, value: true, start, end, dynamic }]
 */
function processAttrs(el) {
  // list = [{ name, value, start, end }, ...]
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  for (i = 0, l = list.length; i < l; i++) {
    // Attribute name
    name = rawName = list[i].name
    // Attribute value
    value = list[i].value
    if (dirRE.test(name)) {
      // Indicates that the attribute is an instruction

      // There are instructions on the element, marking the element as a dynamic element
      // mark element as dynamic
      el.hasBindings = true
      // Modifiers: resolve modifiers on attribute names, such as XX lazy
      modifiers = parseModifiers(name.replace(dirRE, ''))
      // support .foo shorthand syntax for the .prop modifier
      if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
        // For props modifier support foo shorthand
        (modifiers || (modifiers = {})).prop = true
        name = `.` + name.slice(1).replace(modifierRE, '')
      } else if (modifiers) {
        // Remove the modifier in the attribute to get a clean attribute name
        name = name.replace(modifierRE, '')
      }
      if (bindRE.test(name)) { // v-bind, <div :id="test"></div>
        // Handle the attributes of the v-bind instruction, and finally get el Attrs or EL dynamicAttrs = [{ name, value, start, end, dynamic }, ...]

        // Attribute name, such as id
        name = name.replace(bindRE, '')
        // Attribute value, such as: test
        value = parseFilters(value)
        // Whether it is a dynamic attribute < div: [ID] = "test" > < / div >
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          // If it is a dynamic attribute, remove the square brackets [] on both sides of the attribute
          name = name.slice(1, -1)
        }
        // Tip: the dynamic attribute value cannot be an empty string
        if (
          process.env.NODE_ENV !== 'production' &&
          value.trim().length === 0
        ) {
          warn(
            `The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
          )
        }
        // Modifier present
        if (modifiers) {
          if (modifiers.prop && !isDynamic) {
            name = camelize(name)
            if (name === 'innerHtml') name = 'innerHTML'
          }
          if (modifiers.camel && !isDynamic) {
            name = camelize(name)
          }
          // Handle sync modifier
          if (modifiers.sync) {
            syncGen = genAssignmentCode(value, `$event`)
            if (!isDynamic) {
              addHandler(
                el,
                `update:${camelize(name)}`,
                syncGen,
                null,
                false,
                warn,
                list[i]
              )
              if (hyphenate(name) !== camelize(name)) {
                addHandler(
                  el,
                  `update:${hyphenate(name)}`,
                  syncGen,
                  null,
                  false,
                  warn,
                  list[i]
                )
              }
            } else {
              // handler w/ dynamic event name
              addHandler(
                el,
                `"update:"+(${name})`,
                syncGen,
                null,
                false,
                warn,
                list[i],
                true // dynamic
              )
            }
          }
        }
        if ((modifiers && modifiers.prop) || (
          !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
        )) {
          // Add attribute object to El Props array indicates that these properties must be set through props
          // el.props = [{ name, value, start, end, dynamic }, ...]
          addProp(el, name, value, list[i], isDynamic)
        } else {
          // Add attribute to El Attrs array or EL Dynamicattrs array
          addAttr(el, name, value, list[i], isDynamic)
        }
      } else if (onRE.test(name)) { // Event handling, < - div >, < div > div
        // Property name, i.e. event name
        name = name.replace(onRE, '')
        // Is it a dynamic attribute
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          // Dynamic attribute, get the attribute name in []
          name = name.slice(1, -1)
        }
        // Handle the event attribute and add the information of the attribute to El Events or EL On the nativeevents object, format:
        // el.events = [{ value, start, end, modifiers, dynamic }, ...]
        addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
      } else { // normal directives
        // Get el directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
        name = name.replace(dirRE, '')
        // parse arg
        const argMatch = name.match(argRE)
        let arg = argMatch && argMatch[1]
        isDynamic = false
        if (arg) {
          name = name.slice(0, -(arg.length + 1))
          if (dynamicArgRE.test(arg)) {
            arg = arg.slice(1, -1)
            isDynamic = true
          }
        }
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
        if (process.env.NODE_ENV !== 'production' && name === 'model') {
          checkForAliasModel(el, value)
        }
      }
    } else {
      // The current property is not an instruction
      // literal attribute
      if (process.env.NODE_ENV !== 'production') {
        const res = parseText(value, delimiters)
        if (res) {
          warn(
            `${name}="${value}": ` +
            'Interpolation inside attributes has been removed. ' +
            'Use v-bind or the colon shorthand instead. For example, ' +
            'instead of <div id="{{ val }}">, use <div :id="val">.',
            list[i]
          )
        }
      }
      // Put the attribute object in El In attrs array, El attrs = [{ name, value, start, end }]
      addAttr(el, name, JSON.stringify(value), list[i])
      // #6887 firefox doesn't update muted state if set via attribute
      // even immediately after element creation
      if (!el.component &&
        name === 'muted' &&
        platformMustUseProp(el.tag, el.attrsMap.type, name)) {
        addProp(el, name, 'true', list[i])
      }
    }
  }
}

addHandler

/src/compiler/helpers.js

/**
 * Process event attributes and add event attributes to El Events object or EL In the nativeevents object, format:
 * el.events[name] = [{ value, start, end, modifiers, dynamic }, ...]
 * A lot of space is spent dealing with the case where the name attribute has a modifier
 * @param {*} el ast object
 * @param {*} name Property name, i.e. event name
 * @param {*} value Property value, that is, the event callback function name
 * @param {*} modifiers Modifier 
 * @param {*} important 
 * @param {*} warn journal
 * @param {*} range 
 * @param {*} dynamic Is the property name a dynamic property
 */
export function addHandler (
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: ?Function,
  range?: Range,
  dynamic?: boolean
) {
  // modifiers is an object. If the passed parameter is empty, it will be given to a frozen empty object
  modifiers = modifiers || emptyObject
  // Tip: the prevent and passive modifiers cannot be used together
  // warn prevent and passive modifier
  /* istanbul ignore if */
  if (
    process.env.NODE_ENV !== 'production' && warn &&
    modifiers.prevent && modifiers.passive
  ) {
    warn(
      'passive and prevent can\'t be used together. ' +
      'Passive handler can\'t prevent default event.',
      range
    )
  }

  // Standardized click Right and click Middle, they are not actually triggered. Technically, they are them
  // It is browser specific, but at least at the current location, only the browser has right-click and middle click
  // normalize click.right and click.middle since they don't actually fire
  // this is technically browser-specific, but at least for now browsers are
  // the only target envs that have right/middle clicks.
  if (modifiers.right) {
    // Right click
    if (dynamic) {
      // Dynamic properties
      name = `(${name})==='click'?'contextmenu':(${name})`
    } else if (name === 'click') {
      // Non dynamic attribute, name = contextmenu
      name = 'contextmenu'
      // Delete the right attribute in the modifier
      delete modifiers.right
    }
  } else if (modifiers.middle) {
    // Intermediate key
    if (dynamic) {
      // Dynamic attribute, name = > mouseup or ${name}
      name = `(${name})==='click'?'mouseup':(${name})`
    } else if (name === 'click') {
      // Non dynamic attribute, mouseup
      name = 'mouseup'
    }
  }

  /**
   * Handle the three modifiers capture, once and passive, and mark these modifiers by adding different tags to name
   */
  // check capture modifier
  if (modifiers.capture) {
    delete modifiers.capture
    // Add to the attribute with the capture modifier! sign
    name = prependModifierMarker('!', name, dynamic)
  }
  if (modifiers.once) {
    delete modifiers.once
    // The once modifier is marked with ~
    name = prependModifierMarker('~', name, dynamic)
  }
  /* istanbul ignore if */
  if (modifiers.passive) {
    delete modifiers.passive
    // passive modifier with & Mark
    name = prependModifierMarker('&', name, dynamic)
  }

  let events
  if (modifiers.native) {
    // The native modifier listens to the native event of the component root element and stores the event information in El In the nativeevents object
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else {
    events = el.events || (el.events = {})
  }

  const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
  if (modifiers !== emptyObject) {
    // It indicates that there is a modifier. Put the modifier object on the newHandler object
    // { value, dynamic, start, end, modifiers }
    newHandler.modifiers = modifiers
  }

  // Put the configuration object into events [name] = [newhandler, handler,...]
  const handlers = events[name]
  /* istanbul ignore if */
  if (Array.isArray(handlers)) {
    important ? handlers.unshift(newHandler) : handlers.push(newHandler)
  } else if (handlers) {
    events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
  } else {
    events[name] = newHandler
  }

  el.plain = false
}

addIfCondition

/src/compiler/parser/index.js

/**
 * Put the passed in condition object into el In ifconditions array
 */
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

processPre

/src/compiler/parser/index.js

/**
 * If there is a v-pre instruction on the element, set el pre = true 
 */
function processPre(el) {
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    el.pre = true
  }
}

processRawAttrs

/src/compiler/parser/index.js

/**
 * Set el Attrs array object. Each element is an attribute object {name: attrName, value: attrVal, start, end}
 */
function processRawAttrs(el) {
  const list = el.attrsList
  const len = list.length
  if (len) {
    const attrs: Array<ASTAttr> = el.attrs = new Array(len)
    for (let i = 0; i < len; i++) {
      attrs[i] = {
        name: list[i].name,
        value: JSON.stringify(list[i].value)
      }
      if (list[i].start != null) {
        attrs[i].start = list[i].start
        attrs[i].end = list[i].end
      }
    }
  } else if (!el.pre) {
    // non root node in pre blocks with no attributes
    el.plain = true
  }
}

processIf

/src/compiler/parser/index.js

/**
 * Handle v-if, v-else-if, v-else
 * Get el if = "exp",el.elseif = exp, el.else = true
 * v-if Attribute will be added in El Add {exp, block} object to ifconditions array
 */
function processIf(el) {
  // Get the value of the v-if attribute, such as < div v-if = "test" > < / div >
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    // el.if = "test"
    el.if = exp
    // In El Add {exp, block} to ifconditions array
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // Deal with v-else and get el else = true
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    // If-el-se, get elseif = exp
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

processOnce

/src/compiler/parser/index.js

/**
 * Process the v-once instruction to get el once = true
 * @param {*} el 
 */
function processOnce(el) {
  const once = getAndRemoveAttr(el, 'v-once')
  if (once != null) {
    el.once = true
  }
}

checkRootConstraints

/src/compiler/parser/index.js

/**
 * Check the root element:
 *   You cannot use slot and template tags as the root element of a component
 *   You cannot use the v-for directive on the root element of a stateful component because it renders multiple elements
 * @param {*} el 
 */
function checkRootConstraints(el) {
  // You cannot use slot and template tags as the root element of a component
  if (el.tag === 'slot' || el.tag === 'template') {
    warnOnce(
      `Cannot use <${el.tag}> as component root element because it may ` +
      'contain multiple nodes.',
      { start: el.start }
    )
  }
  // You cannot use v-for on the root element of a stateful component because it renders multiple elements
  if (el.attrsMap.hasOwnProperty('v-for')) {
    warnOnce(
      'Cannot use v-for on stateful component root element because ' +
      'it renders multiple elements.',
      el.rawAttrsMap['v-for']
    )
  }
}

closeElement

/src/compiler/parser/index.js

/**
 * Three main things have been done:
 *   1,If the element has not been processed, that is, El If processed is false, the processElement method is called to process many attributes on the node
 *   2,Let yourself have a relationship with the parent element, put yourself in the children array of the parent element, and set your parent attribute to currentParent
 *   3,Set your own child elements and put all your non slot child elements into your own children array
 */
function closeElement(element) {
  // Remove the space at the end of the node, except for the elements in the current pre tag
  trimEndingWhitespace(element)
  // The current element is no longer in the pre node and has not been processed
  if (!inVPre && !element.processed) {
    // Handle the key, ref, slot, closed slot tag, dynamic component, class, style, v-bind, v-on, other instructions and some native attributes of the element node respectively 
    element = processElement(element, options)
  }
  // Handle the case where there are v-if, v-else-if and v-else instructions on the root node
  // If the root node has a v-if instruction, you must also provide a node of the same level with v-else-if or v-else to prevent the root element from not existing
  // tree management
  if (!stack.length && element !== root) {
    // allow root elements with v-if, v-else-if and v-else
    if (root.if && (element.elseif || element.else)) {
      if (process.env.NODE_ENV !== 'production') {
        // Check root element
        checkRootConstraints(element)
      }
      // Set ifconditions attribute on root element, root ifConditions = [{ exp: element.elseif, block: element }, ...]
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      // The prompt indicates that you should not only use v-if on the root element, but use v-if and v-else-if together to ensure that the component has only one root element
      warnOnce(
        `Component template should contain exactly one root element. ` +
        `If you are using v-if on multiple elements, ` +
        `use v-else-if to chain them instead.`,
        { start: element.start }
      )
    }
  }
  // Make yourself related to the parent element
  // Put yourself in the children array of the parent element, and then set your parent attribute to currentParent
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
      processIfConditions(element, currentParent)
    } else {
      if (element.slotScope) {
        // scoped slot
        // keep it in the children list so that v-else(-if) conditions can
        // find it as the prev node.
        const name = element.slotTarget || '"default"'
          ; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
      }
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }

  // Set your own child elements
  // Set all of your non slot child elements to element In the children array
  // final children cleanup
  // filter out scoped slots
  element.children = element.children.filter(c => !(c: any).slotScope)
  // remove trailing whitespace node again
  trimEndingWhitespace(element)

  // check pre state
  if (element.pre) {
    inVPre = false
  }
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
  // Execute the postTransform methods of model, class and style for element
  // However, the web platform does not provide this method
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

trimEndingWhitespace

/src/compiler/parser/index.js

/**
 * Delete the blank text node in the element, for example: < div > < / div >, delete the blank node in the div element and move it out of the children attribute of the element
 */
function trimEndingWhitespace(el) {
  if (!inPre) {
    let lastNode
    while (
      (lastNode = el.children[el.children.length - 1]) &&
      lastNode.type === 3 &&
      lastNode.text === ' '
    ) {
      el.children.pop()
    }
  }
}

processIfConditions

/src/compiler/parser/index.js

function processIfConditions(el, parent) {
  // Find parent The last element node in children
  const prev = findPrevElement(parent.children)
  if (prev && prev.if) {
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    })
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
      `used on element <${el.tag}> without corresponding v-if.`,
      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
    )
  }
}

findPrevElement

/src/compiler/parser/index.js

/**
 * Find the last element node in children 
 */
function findPrevElement(children: Array<any>): ASTElement | void {
  let i = children.length
  while (i--) {
    if (children[i].type === 1) {
      return children[i]
    } else {
      if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
        warn(
          `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
          `will be ignored.`,
          children[i]
        )
      }
      children.pop()
    }
  }
}

help

Here, the parsing part of the compiler is over. I believe many people see it in the clouds. Even if they see it several times, it may not be so clear.

Don't worry. This is normal. The amount of code in the compiler is really large. However, the content itself is not complex. The complex thing is that it has to deal with too many things, which leads to a huge amount of code in this part. Correspondingly, it will feel more difficult. It's really not simple. At least I think it's the most complex and difficult part of the whole framework.

You can read the videos and articles several times. If you don't understand, write some sample code to assist debugging and write detailed comments. Still that sentence, the meaning of reading a book a hundred times is self-evident.

In the process of reading, you need to grasp the essence of the compiler parsing part: parsing the HTML like string template into AST objects.

So one thing that so many codes are doing is parsing the string template and representing and recording the whole template with AST objects. Therefore, when you read, you can record the AST objects generated in the parsing process to help you read and understand, so that you will not be so confused after reading and help you understand.

This is a simple record of my reading:

const element = {
  type: 1,
  tag,
  attrsList: [{ name: attrName, value: attrVal, start, end }],
  attrsMap: { attrName: attrVal, },
  rawAttrsMap: { attrName: attrVal, type: checkbox },
  // v-if
  ifConditions: [{ exp, block }],
  // v-for
  for: iterator,
  alias: alias,
  // :key
  key: xx,
  // ref
  ref: xx,
  refInFor: boolean,
  // slot 
  slotTarget: slotName,
  slotTargetDynamic: boolean,
  slotScope: Expression for the scope slot,
  scopeSlot: {
    name: {
      slotTarget: slotName,
      slotTargetDynamic: boolean,
      children: {
        parent: container,
        otherProperty,
      }
    },
    slotScope: Expression for the scope slot,
  },
  slotName: xx,
  // Dynamic component
  component: compName,
  inlineTemplate: boolean,
  // class
  staticClass: className,
  classBinding: xx,
  // style
  staticStyle: xx,
  styleBinding: xx,
  // attr
  hasBindings: boolean,
  nativeEvents: {with evetns},
  events: {
    name: [{ value, dynamic, start, end, modifiers }]
  },
  props: [{ name, value, dynamic, start, end }],
  dynamicAttrs: [with attrs],
  attrs: [{ name, value, dynamic, start, end }],
  directives: [{ name, rawName, value, arg, isDynamicArg, modifiers, start, end }],
  // v-pre
  pre: true,
  // v-once
  once: true,
  parent,
  children: [],
  plain: boolean,
}

summary

  • The interviewer asked: briefly, what did Vue's compiler do?

    Answer:

    Vue's compiler does three things:

    • Parse the html template of the component into an AST object
    • Optimize, traverse the AST, make static marks for each node, mark whether it is a static node, and then further mark the static root node, so that these static nodes can be skipped in the process of subsequent updates; The marked static root is used to generate the render function stage and generate the render function of the static root node
    • Generate runtime rendering functions from AST, that is, what we call render. In fact, there is another one, the staticRenderFns array, which stores the rendering functions of all static nodes

  • The interviewer asked: let's talk about the parsing process of the compiler in detail. How does it turn the html string template into an AST object?

    Answer:

    • Traverse the HTML template string and match "<" through regular expression
    • Skip some tags that do not need to be processed, such as annotation tag, conditional annotation tag and Doctype.

      Note: the core of the whole parsing process is to deal with the start tag and end tag

    • Parse start tag

      • Get an object, including tag name, all attributes and the index position of the tag in the html template string
      • Further process the attrs attribute obtained in the previous step and change it into [{name: attrName, value: attrVal, start: xx, end: xx},...] Form of
      • The AST object generated from the tag name, attribute object and parent element of the current element is actually an ordinary JS object, which records some information of the element in the form of key and value
      • Next, further process some instructions on the start tag, such as v-pre, v-for, v-if and v-once, and put the processing results on the AST object
      • After processing, store the ast object into the stack array
      • After processing, the html string will be truncated and the processed string will be truncated
    • Resolve closed label

      • If the end tag is matched, the last element is taken from the stack array, which is a pair of the currently matched end tag.
      • Process the attributes on the start tag again, which are different from the previous processing, such as key, ref, scopedSlot, style, etc., and put the processing results on the AST object of the element

        Note: the video said there was an error in this area. After looking back, there was no problem and there was no need to change it. That's true

      • Then connect the current element with the parent element, set the parent attribute to the ast object of the current element, and then put yourself in the children array of the ast object of the parent element
    • Finally, after traversing the entire html template string, the ast object is returned

link

Thank you for your attention, likes, collections and comments. See you next time.

When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

Keywords: Javascript Front-end ECMAScript TypeScript Vue.js

Added by dajawu on Thu, 03 Mar 2022 02:44:17 +0200