Skip to content

Latest commit

 

History

History
371 lines (343 loc) · 11.3 KB

7.md

File metadata and controls

371 lines (343 loc) · 11.3 KB

入口开始,解读Vue源码(六)—— $mount 内部实现 --- compile parse函数生成AST

上一小节中提到 compile 函数(src/compiler/index.js)就是将 template 编译成 render function 的字符串形式。接下来就详细讲解这个函数:

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) //1. parse
  optimize(ast, options) //2.optimize
  const code = generate(ast, options) //3.generate
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

createCompiler 函数主要通过3个步骤:parseoptimizegenerate来生成一个包含astrenderstaticRenderFns的对象。

parse函数

在说parse函数之前,我们先来了解一个概念:AST(Abstract Syntax Tree)抽象语法树:

AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。Vue 源码中借鉴 jQuery 作者 John Resig 的 HTML Parser 对模板进行解析,得到的就是 AST 代码。

接着我们看一下Vue中对AST数据的定义:

declare type ASTNode = ASTElement | ASTText | ASTExpression;

declare type ASTElement = {
  type: 1;
  tag: string;
  attrsList: Array<{ name: string; value: string }>;
  attrsMap: { [key: string]: string | null };
  parent: ASTElement | void;
  children: Array<ASTNode>;
  ...
}

declare type ASTExpression = {
  type: 2;
  expression: string;
  text: string;
  static?: boolean;
  // 2.4 ssr optimization
  // 2.4+ 增加了对ssr的标识
  ssrOptimizability?: number;
};

declare type ASTText = {
  type: 3;
  text: string;
  static?: boolean;
  isComment?: boolean;
  // 2.4 ssr optimization
  // 2.4+ 增加了对ssr的标识
  ssrOptimizability?: number;
};

可以看到 ASTNode 有三种形式:ASTElementASTExpressionASTText。通过属性type来进行标识。 下面我们正式进入parse函数功能:

function parse(template) {
    ...
    const stack = [];
    let currentParent;    //当前父节点
    let root;            //最终返回出去的AST树根节点
    ...
    parseHTML(template, {
        start: function start(tag, attrs, unary) {
           ......
        },
        end: function end() {
          ......
        },
        chars: function chars(text) {
           ......
        }
    })
    return root
}

我们省略了parse的相关内容,只看一下大体的功能,其主要的功能函数应该是parseHTML方法。接受了2个参数,一个使我们的模板template,另一个是包含startendchars的方法。 在看parseHTML之前,我们需要先了解一下下面这几个正则:

// 该正则式可匹配到 <div id="index"> 的 id="index" 属性部分
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配起始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
// 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 匹配DOCTYPE、注释等特殊标签
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/

Vue 通过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。有了上面这些基础,我们再来看看parseHtml的内部实行:

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  while (html) {
    // 保留 html 副本
    last = html
    // 如果没有lastTag,并确保我们不是在一个纯文本内容元素中:script、style、textarea
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        // Comment:
        if (comment.test(html)) {
          ...
        }
        if (conditionalComment.test(html)) {
          ...
        }
        // Doctype:
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          ...
        }
        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          ...
        }
        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          ...
        }
      }
      let text, rest, next
      if (textEnd >= 0) {
        ...
      }
      if (textEnd < 0) {
        text = html
        html = ''
      }
      // 绘制文本内容,使用 options.char 方法。
      if (options.chars && text) {
        options.chars(text)
      }
    } else {
      ...
    }
    ...
  }

上面只看一下代码的大概意思:

  1. 首先通过while (html)去循环判断html内容是否存在。
  2. 再判断文本内容是否在script/style标签中
  3. 上述条件都满足的话,开始解析html字符串

假设我们传递这样一个html字符串<div id="demo">{{msg}}</div>。我们来看其中一段关于Start tag解析的方法:

const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(lastTag, html)) {
    advance(1)
  }
  continue
}

这里面有parseStartTaghandleStartTag两个方法值得关注一下:

function parseStartTag () {
  //判断html中是否存在开始标签
  const start = html.match(startTagOpen)
  if (start) {
    // 定义 match 结构
    const match = {
      tagName: start[1], // 标签名
      attrs: [], // 属性名
      start: index // 起点位置
    }

    /**
     * 通过传入变量n来截取字符串,这也是Vue解析的重要方法,通过不断地蚕食掉html字符串,一步步完成对他的解析过程
     */
    advance(start[0].length)
    let end, attr

    // 如果还没有到结束标签的位置
    // 存入属性
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
      advance(attr[0].length)
      match.attrs.push(attr)
    }
    // 返回处理后的标签match结构
    if (end) {
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}

经过上面一步的解析,我们得到了一个起始标签match的数据结构:

match

function handleStartTag (match) {
  // match 是上面调用方法的时候传递过来的数据结构
  const tagName = match.tagName
  const unarySlash = match.unarySlash
  ...
  const unary = isUnaryTag(tagName) || !!unarySlash

  // 备份属性数组的长度
  const l = match.attrs.length
  // 构建长度为1的空数组
  const attrs = new Array(l)
  for (let i = 0; i < l; i++) {
    const args = match.attrs[i]
    ...
    // 取定义属性的值
    const value = args[3] || args[4] || args[5] || ''

    // 改变attr的格式为 [{name: 'id', value: 'demo'}]
    attrs[i] = {
      name: args[1],
      value: decodeAttr(
        value,
        options.shouldDecodeNewlines
      )
    }
  }

  // stack中记录当前解析的标签
  // 如果不是自闭和标签
  // 这里的stack这个变量在parseHTML中定义,作用是为了存放标签名 为了和结束标签进行匹配的作用。
  if (!unary) {
    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
    lastTag = tagName
  }
  // parse 函数传入的 start 方法
  options.start(tagName, attrs, unary, match.start, match.end)
}

到这里似乎一切明朗了许多,parseHTML主要用来蚕食html字符串,解析出字符串中的tagNameattrsmatch等元素,传入start方法:

start (tag, attrs, unary) {
  ...
  // 创建基础的 ASTElement
  let element: ASTElement = createASTElement(tag, attrs, currentParent)
  if (ns) {
    element.ns = ns
  }
  ...

  if (!inVPre) {
    // 判断有没有 v-pre 指令的元素。如果有的话 element.pre = true
    // 官网有介绍:<span v-pre>{{ this will not be compiled }}</span>
    // 跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
    processPre(element)
    if (element.pre) {
      inVPre = true
    }
  }
  if (platformIsPreTag(element.tag)) {
    inPre = true
  }
  if (inVPre) {
    // 处理原始属性
    processRawAttrs(element)
  } else if (!element.processed) {
    // structural directives
    // v-for v-if v-once
    processFor(element)
    processIf(element)
    processOnce(element)
    // element-scope stuff
    processElement(element, options)
  }

  // 检查根节点约束
  function checkRootConstraints (el) {
    if (process.env.NODE_ENV !== 'production') {
      if (el.tag === 'slot' || el.tag === 'template') {
        warnOnce(
          `Cannot use <${el.tag}> as component root element because it may ` +
          'contain multiple nodes.'
        )
      }
      if (el.attrsMap.hasOwnProperty('v-for')) {
        warnOnce(
          'Cannot use v-for on stateful component root element because ' +
          'it renders multiple elements.'
        )
      }
    }
  }

  // tree management
  if (!root) {
    // 如果不存在根节点
    root = element
    checkRootConstraints(root)
  } else if (!stack.length) {
    // 允许有 v-if, v-else-if 和 v-else 的根元素
    ...
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
      processIfConditions(element, currentParent)
    } else if (element.slotScope) { // scoped slot
      currentParent.plain = false
      const name = element.slotTarget || '"default"'
      ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
    } else {
      // 将元素插入 children 数组中
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }
  if (!unary) {
    currentParent = element
    stack.push(element)
  } else {
    endPre(element)
  }
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

其实start方法就是处理 element 元素的过程。确定命名空间;创建AST元素 element;执行预处理;定义root;处理各类 v- 标签的逻辑;最后更新 root、currentParent、stack 的结果。 最终通过 createASTElement 方法定义了一个新的 AST 对象:

export function createASTElement (
  tag: string,
  attrs: Array<Attr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent,
    children: []
  }
}

总结

下面我们来屡一下parse整体的过程:

  1. 通过parseHtml来一步步解析传入html字符串的标签、元素、文本、注释..
  2. parseHtml解析过程中,调用传入的startendchars方法来生成AST语法树

我们看一下最终生成的AST语法树对象: AST