关于vue.js:Vue-源码解读8-编译器-之-解析下

38次阅读

共计 14509 个字符,预计需要花费 37 分钟才能阅读完成。

当学习成为了习惯,常识也就变成了常识。 感激各位的 关注 点赞 珍藏 评论

新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn

文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。

非凡阐明

因为文章篇幅限度,所以将 Vue 源码解读(8)—— 编译器 之 解析 拆成了两篇文章,本篇是对 Vue 源码解读(8)—— 编译器 之 解析(上)的一个补充,所以在浏览时请同时关上 Vue 源码解读(8)—— 编译器 之 解析(上)一起浏览。

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()}
  }
}

帮忙

到这里编译器的解析局部就完结了,置信很多人看的是云里雾里的,即便多看几遍可能也没有那么清晰。

不要焦急,这个很失常,编译器这块儿的代码量的确是比拟大。然而内容自身其实不简单,简单的是它要解决货色切实是太多了,这才导致这部分的代码量微小,绝对应的,就会产生比拟难的感觉。的确不简略,至多我感觉它是整个框架最简单最难的中央了。

对照着视频和文章大家能够多看几遍,不明确的中央写一些示例代码辅助调试,编写具体的正文。还是那句话,书读百遍,其义自现。

浏览的过程中,大家须要抓住编译器解析局部的实质:将类 HTML 字符串模版解析成 AST 对象。

所以这么多代码都在做一件事件,就是解析字符串模版,将整个模版用 AST 对象来示意和记录。所以,大家浏览的时候,能够将解析过程中生成的 AST 对象记录下来,帮忙浏览和了解,这样在读完当前不至于那么迷茫,也有助于大家了解。

这是我在浏览的时候的一个简略记录:

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,
}

总结

  • 面试官 问:简略说一下 Vue 的编译器都做了什么?

    Vue 的编译器做了三件事件:

    • 将组件的 html 模版解析成 AST 对象
    • 优化,遍历 AST,为每个节点做动态标记,标记其是否为动态节点,而后进一步标记出动态根节点,这样在后续更新的过程中就能够跳过这些动态节点了;标记动态根用于生成渲染函数阶段,生成动态根节点的渲染函数
    • 从 AST 生成运行时的渲染函数,即大家说的 render,其实还有一个,就是 staticRenderFns 数组,外面寄存了所有的动态节点的渲染函数

  • 面试官 问:具体说一说编译器的解析过程,它是怎么将 html 字符串模版变成 AST 对象的?

    • 遍历 HTML 模版字符串,通过正则表达式匹配 “<“
    • 跳过某些不须要解决的标签,比方:正文标签、条件正文标签、Doctype。

      备注:整个解析过程的外围是解决开始标签和完结标签

    • 解析开始标签

      • 失去一个对象,包含 标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引地位
      • 进一步解决上一步失去的 attrs 属性,将其变成 [{name: attrName, value: attrVal, start: xx, end: xx}, …] 的模式
      • 通过标签名、属性对象和以后元素的父元素生成 AST 对象,其实就是一个 一般的 JS 对象,通过 key、value 的模式记录了该元素的一些信息
      • 接下来进一步解决开始标签上的一些指令,比方 v-pre、v-for、v-if、v-once,并将处理结果放到 AST 对象上
      • 解决完结将 ast 对象寄存到 stack 数组
      • 解决实现后会截断 html 字符串,将曾经解决掉的字符串截掉
    • 解析闭合标签

      • 如果匹配到完结标签,就从 stack 数组中拿出最初一个元素,它和以后匹配到的完结标签是一对。
      • 再次解决开始标签上的属性,这些属性和后面解决的不一样,比方:key、ref、scopedSlot、款式等,并将处理结果放到元素的 AST 对象上

        备注 视频中说这块儿有误,回头看了下,没有问题,不须要改,的确是这样

      • 而后将以后元素和父元素产生分割,给以后元素的 ast 对象设置 parent 属性,而后将本人放到父元素的 ast 对象的 children 数组中
    • 最初遍历残缺个 html 模版字符串当前,返回 ast 对象

链接

  • 配套视频,微信公众号回复:” 精通 Vue 技术栈源码原理视频版 ” 获取
  • 精通 Vue 技术栈源码原理 专栏
  • github 仓库 liyongning/Vue 欢送 Star

感激各位的:关注 点赞 珍藏 评论,咱们下期见。


当学习成为了习惯,常识也就变成了常识。 感激各位的 关注 点赞 珍藏 评论

新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn

文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。

正文完
 0