Vue Source Code Interpretation (8) - Analysis of the Compiler (Part 2)

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

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

cover

Special Instructions

Due to the space limitation of the article, the Vue Source Code Interpretation (8) - Compiler Analysis is split into two articles. This article is a supplement to Vue Source Code Interpretation (8) - Compiler Analysis (Part 1) , so When reading, please open the Vue source code interpretation (8) - Compiler analysis (a) to read together.

processAttrs

/src/compiler/parser/index.js

/**
 * 处理元素上的所有属性:
 * v-bind 指令变成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...],
 *                或者是必须使用 props 的属性,变成了 el.props = [{ name, value, start, end, dynamic }, ...]
 * v-on 指令变成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] }
 * 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
 * 原生属性:el.attrs = [{ name, value, start, end }],或者一些必须使用 props 的属性,变成了:
 *         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++) {
    // 属性名
    name = rawName = list[i].name
    // 属性值
    value = list[i].value
    if (dirRE.test(name)) {
      // 说明该属性是一个指令

      // 元素上存在指令,将元素标记动态元素
      // mark element as dynamic
      el.hasBindings = true
      // modifiers,在属性名上解析修饰符,比如 xx.lazy
      modifiers = parseModifiers(name.replace(dirRE, ''))
      // support .foo shorthand syntax for the .prop modifier
      if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
        // 为 .props 修饰符支持 .foo 速记写法
        (modifiers || (modifiers = {})).prop = true
        name = `.` + name.slice(1).replace(modifierRE, '')
      } else if (modifiers) {
        // 属性中的修饰符去掉,得到一个干净的属性名
        name = name.replace(modifierRE, '')
      }
      if (bindRE.test(name)) { // v-bind, <div :id="test"></div>
        // 处理 v-bind 指令属性,最后得到 el.attrs 或者 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...]

        // 属性名,比如:id
        name = name.replace(bindRE, '')
        // 属性值,比如:test
        value = parseFilters(value)
        // 是否为动态属性 <div :[id]="test"></div>
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          // 如果是动态属性,则去掉属性两侧的方括号 []
          name = name.slice(1, -1)
        }
        // 提示,动态属性值不能为空字符串
        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}"`
          )
        }
        // 存在修饰符
        if (modifiers) {
          if (modifiers.prop && !isDynamic) {
            name = camelize(name)
            if (name === 'innerHtml') name = 'innerHTML'
          }
          if (modifiers.camel && !isDynamic) {
            name = camelize(name)
          }
          // 处理 sync 修饰符
          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)
        )) {
          // 将属性对象添加到 el.props 数组中,表示这些属性必须通过 props 设置
          // el.props = [{ name, value, start, end, dynamic }, ...]
          addProp(el, name, value, list[i], isDynamic)
        } else {
          // 将属性添加到 el.attrs 数组或者 el.dynamicAttrs 数组
          addAttr(el, name, value, list[i], isDynamic)
        }
      } else if (onRE.test(name)) { // v-on, 处理事件,<div @click="test"></div>
        // 属性名,即事件名
        name = name.replace(onRE, '')
        // 是否为动态属性
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          // 动态属性,则获取 [] 中的属性名
          name = name.slice(1, -1)
        }
        // 处理事件属性,将属性的信息添加到 el.events 或者 el.nativeEvents 对象上,格式:
        // el.events = [{ value, start, end, modifiers, dynamic }, ...]
        addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
      } else { // normal directives,其它的普通指令
        // 得到 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 {
      // 当前属性不是指令
      // 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]
          )
        }
      }
      // 将属性对象放到 el.attrs 数组中,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

/**
 * 处理事件属性,将事件属性添加到 el.events 对象或者 el.nativeEvents 对象中,格式:
 * el.events[name] = [{ value, start, end, modifiers, dynamic }, ...]
 * 其中用了大量的篇幅在处理 name 属性带修饰符 (modifier) 的情况
 * @param {*} el ast 对象
 * @param {*} name 属性名,即事件名
 * @param {*} value 属性值,即事件回调函数名
 * @param {*} modifiers 修饰符
 * @param {*} important 
 * @param {*} warn 日志
 * @param {*} range 
 * @param {*} dynamic 属性名是否为动态属性
 */
export function addHandler (
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: ?Function,
  range?: Range,
  dynamic?: boolean
) {
  // modifiers 是一个对象,如果传递的参数为空,则给一个冻结的空对象
  modifiers = modifiers || emptyObject
  // 提示:prevent 和 passive 修饰符不能一起使用
  // 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
    )
  }

  // 标准化 click.right 和 click.middle,它们实际上不会被真正的触发,从技术讲他们是它们
  // 是特定于浏览器的,但至少目前位置只有浏览器才具有右键和中间键的点击
  // 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) {
    // 右键
    if (dynamic) {
      // 动态属性
      name = `(${name})==='click'?'contextmenu':(${name})`
    } else if (name === 'click') {
      // 非动态属性,name = contextmenu
      name = 'contextmenu'
      // 删除修饰符中的 right 属性
      delete modifiers.right
    }
  } else if (modifiers.middle) {
    // 中间键
    if (dynamic) {
      // 动态属性,name => mouseup 或者 ${name}
      name = `(${name})==='click'?'mouseup':(${name})`
    } else if (name === 'click') {
      // 非动态属性,mouseup
      name = 'mouseup'
    }
  }

  /**
   * 处理 capture、once、passive 这三个修饰符,通过给 name 添加不同的标记来标记这些修饰符
   */
  // check capture modifier
  if (modifiers.capture) {
    delete modifiers.capture
    // 给带有 capture 修饰符的属性,加上 ! 标记
    name = prependModifierMarker('!', name, dynamic)
  }
  if (modifiers.once) {
    delete modifiers.once
    // once 修饰符加 ~ 标记
    name = prependModifierMarker('~', name, dynamic)
  }
  /* istanbul ignore if */
  if (modifiers.passive) {
    delete modifiers.passive
    // passive 修饰符加 & 标记
    name = prependModifierMarker('&', name, dynamic)
  }

  let events
  if (modifiers.native) {
    // native 修饰符, 监听组件根元素的原生事件,将事件信息存放到 el.nativeEvents 对象中
    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) {
    // 说明有修饰符,将修饰符对象放到 newHandler 对象上
    // { value, dynamic, start, end, modifiers }
    newHandler.modifiers = modifiers
  }

  // 将配置对象放到 events[name] = [newHander, 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

/**
 * 将传递进来的条件对象放进 el.ifConditions 数组中
 */
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

processPre

/src/compiler/parser/index.js

/**
 * 如果元素上存在 v-pre 指令,则设置 el.pre = true 
 */
function processPre(el) {
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    el.pre = true
  }
}

processRawAttrs

/src/compiler/parser/index.js

/**
 * 设置 el.attrs 数组对象,每个元素都是一个属性对象 { 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

/**
 * 处理 v-if、v-else-if、v-else
 * 得到 el.if = "exp",el.elseif = exp, el.else = true
 * v-if 属性会额外在 el.ifConditions 数组中添加 { exp, block } 对象
 */
function processIf(el) {
  // 获取 v-if 属性的值,比如 <div v-if="test"></div>
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    // el.if = "test"
    el.if = exp
    // 在 el.ifConditions 数组中添加 { exp, block }
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // 处理 v-else,得到 el.else = true
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    // 处理 v-else-if,得到 el.elseif = exp
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

processOnce

/src/compiler/parser/index.js

/**
 * 处理 v-once 指令,得到 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

/**
 * 检查根元素:
 *   不能使用 slot 和 template 标签作为组件的根元素
 *   不能在有状态组件的 根元素 上使用 v-for 指令,因为它会渲染出多个元素
 * @param {*} el 
 */
function checkRootConstraints(el) {
  // 不能使用 slot 和 template 标签作为组件的根元素
  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 }
    )
  }
  // 不能在有状态组件的 根元素 上使用 v-for,因为它会渲染出多个元素
  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

/**
 * 主要做了 3 件事:
 *   1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性
 *   2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent
 *   3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中
 */
function closeElement(element) {
  // 移除节点末尾的空格,当前 pre 标签内的元素除外
  trimEndingWhitespace(element)
  // 当前元素不再 pre 节点内,并且也没有被处理过
  if (!inVPre && !element.processed) {
    // 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性 
    element = processElement(element, options)
  }
  // 处理根节点上存在 v-if、v-else-if、v-else 指令的情况
  // 如果根节点存在 v-if 指令,则必须还提供一个具有 v-else-if 或者 v-else 的同级别节点,防止根元素不存在
  // 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') {
        // 检查根元素
        checkRootConstraints(element)
      }
      // 给根元素设置 ifConditions 属性,root.ifConditions = [{ exp: element.elseif, block: element }, ...]
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      // 提示,表示不应该在 根元素 上只使用 v-if,应该将 v-if、v-else-if 一起使用,保证组件只有一个根元素
      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 }
      )
    }
  }
  // 让自己和父元素产生关系
  // 将自己放到父元素的 children 数组中,然后设置自己的 parent 属性为 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
    }
  }

  // 设置自己的子元素
  // 将自己的所有非插槽的子元素设置到 element.children 数组中
  // 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
  }
  // 分别为 element 执行 model、class、style 三个模块的 postTransform 方法
  // 但是 web 平台没有提供该方法
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

trimEndingWhitespace

/src/compiler/parser/index.js

/**
 * 删除元素中空白的文本节点,比如:<div> </div>,删除 div 元素中的空白节点,将其从元素的 children 属性中移出去
 */
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) {
  // 找到 parent.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

/**
 * 找到 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

At this point, the parsing part of the compiler is over. I believe that many people have seen it in a fog, even if it is read several times, it may not be so clear.

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

You can watch the videos and articles several times, write some sample codes to assist debugging, and write detailed comments where you don't understand. It's the same sentence, read it a hundred times, and its meaning is self-evident.

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

So so much code is doing one thing, which is to parse the string template, and use the AST object to represent and record the entire template. Therefore, when you read, you can record the AST objects generated during the parsing process to help reading and understanding, so that you will not be so confused after reading, and it will also help everyone understand.

Here's a simple note while I'm 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: 别名,
  // :key
  key: xx,
  // ref
  ref: xx,
  refInFor: boolean,
  // 插槽
  slotTarget: slotName,
  slotTargetDynamic: boolean,
  slotScope: 作用域插槽的表达式,
  scopeSlot: {
    name: {
      slotTarget: slotName,
      slotTargetDynamic: boolean,
      children: {
        parent: container,
        otherProperty,
      }
    },
    slotScope: 作用域插槽的表达式,
  },
  slotName: xx,
  // 动态组件
  component: compName,
  inlineTemplate: boolean,
  // class
  staticClass: className,
  classBinding: xx,
  // style
  staticStyle: xx,
  styleBinding: xx,
  // attr
  hasBindings: boolean,
  nativeEvents: {同 evetns},
  events: {
    name: [{ value, dynamic, start, end, modifiers }]
  },
  props: [{ name, value, dynamic, start, end }],
  dynamicAttrs: [同 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,
}

Summarize

  • The interviewer asked : Briefly, what does the Vue compiler do?

    Answer :

    Vue's compiler does three things:

    • Parse the component's html template into an AST object

    • Optimize, traverse the AST, mark each node statically, 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 subsequent update process; marking the static root is used to generate In the rendering function stage, the rendering function of the static root node is generated

    • The runtime rendering function is generated from the AST, that is, the render function that everyone said. In fact, there is another one, the staticRenderFns array, which stores the rendering functions of all static nodes.

<hr />

  • The interviewer asked : Tell me in detail about the parsing process of the compiler, how does it turn the html string template into an AST object?

    Answer :

    • Traverse HTML template string, matching "<" by regular expression

    • Skip some tags that do not need to be processed, such as: comment tags, conditional comment tags, Doctype.

      Remark: The core of the whole parsing process is to handle the opening and closing tags

    • Parse start tag

      • Get an object, including the tag name (tagName), all attributes (attrs), and the index position of the tag in the html template string

      • Further process the attrs attribute obtained in the previous step and turn it into the form of [{ name: attrName, value: attrVal, start: xx, end: xx }, ...]

      • The AST object is generated by the tag name, attribute object and the parent element of the current element, which 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, v-once, and put the processing results on the AST object

      • After processing, store the ast object in the stack array

      • After the processing is completed, the html string will be truncated, and the processed string will be truncated.

    • Parse closing tags

      • If the end tag is matched, the last element is taken from the stack array, which is a pair with the currently matched end tag.

      • Process the attributes on the start tag again, these attributes are different from the previous ones, such as: key, ref, scopedSlot, style, etc., and put the processing result on the AST object of the element

        Remarks There is an error in the video, I looked back and found that there is no problem, no need to change, it is true

      • Then associate the current element with the parent element, set the parent property of the ast object of the current element, and then put itself in the children array of the ast object of the parent element

    • Finally, after traversing the entire html template string, return the ast object

Link

Thank you for your attention , likes , favorites and comments , see you in the next issue.


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

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324128023&siteId=291194637