Vue2.x 源码 - 编译过程(compile)

上一篇:Vue2.x 源码 - 初始化:全局API

Vue 的渲染过程:
在这里插入图片描述

这一篇我们来看一下 Vue 是如何解析(compile)模板(template)为 render 函数。

1、编译入口

注意:这个源码引用关系有点绕,可以先找到 compileToFunctions 方法(最后面),然后往回看。

当我们使⽤ Runtime + Compiler 的 Vue.js,它的⼊⼝是 src/platforms/web/entry-runtime-with- compiler.js ,看⼀下它对 $mount 函数的定义:

//缓存原型上的 $mount 方法
const mount = Vue.prototype.$mount
//重新定义 $mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  //选择器没有则创建一个 div,有则返回
  el = el && query(el)
  // 不可把 Vue 挂载到 html body 上
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  //获取参数
  const options = this.$options
  //没有 render , 解析模板/el并转换为渲染函数
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          //获取 template 选择器的 innerHtml
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      //如果元素节点则直接返回 innerHtml
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    //获取元素的outerHTML,同时处理IE中的SVG元素
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      //日志 开始编译
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      //编译
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      //日志 编译结束
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  //调⽤原先原型上的 $mount ⽅法挂载
  return mount.call(this, el, hydrating)
}

这段函数逻辑之前分析过:参考(Vue2.x 源码 - 初始化:实例挂载($mount)的实现),关于编译的⼊⼝就是在这⾥:

const { render, staticRenderFns } = compileToFunctions(template, {
  outputSourceRange: process.env.NODE_ENV !== 'production', //记录模板解析
  shouldDecodeNewlines, //ie
  shouldDecodeNewlinesForHref,//chrome
  delimiters: options.delimiters, //Vue 提供的选项,改变纯文本插入分隔符,默认{{}}
  comments: options.comments  //Vue 提供的选项,当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

如果 shouldDecodeNewlines = true,意味着 Vue 在编译模板的时候,要对属性值中的换行符或制表符做兼容处理。而shouldDecodeNewlinesForHref = true 意味着 Vue 在编译模板的时候,要对 a 标签的 href 属性值中的换行符或制表符做兼容处理。

compileToFunctions ⽅法就是把模板 template 编译⽣成 render 以及 staticRenderFns ,它 的定义在 src/platforms/web/compiler/index.js 中:

import { baseOptions } from './options' 
import { createCompiler } from 'compiler/index' 
const { compile, compileToFunctions } = createCompiler(baseOptions) 
export { compile, compileToFunctions }

可以看到 compileToFunctions ⽅法实际上是 createCompiler ⽅法的返回值,该⽅法接收⼀个编 译配置参数,接下来我们来看⼀下 createCompiler ⽅法的定义,在 src/compiler/index.js 中:

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  //解析模板字符串生成AST
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    //优化语法树,对AST进行静态节点标记
    optimize(ast, options)
  }
  //抽象语法树(AST) 生成 render 函数代码字符串
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

createCompiler ⽅法实际上是通过调⽤ createCompilerCreator ⽅法返回的,该⽅法传⼊的参数 是⼀个函数,真正的编译过程都在这个 baseCompile 函数⾥执⾏,这个在后面细看;那么 createCompilerCreator ⼜是什么呢,它的定义在 src/compiler/create-compiler.js 中:

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      //缓存入参baseOptions,baseOptions是编译时默认参数
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []
      //warn方法,编译过程中对错误和提示收集
      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }
      //编译时传入的参数
      if (options) {
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // 定义warn方法
          const leadingSpaceLength = template.match(/^\s*/)[0].length
          warn = (msg, range, tip) => {
            const data: WarningMessage = { msg }
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            (tip ? tips : errors).push(data)
          }
        }
        // 合并modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        //  合并 directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        //  合并 options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }
      finalOptions.warn = warn
      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

可以看到该⽅法返回了⼀个 createCompiler 的函数,它接收⼀个 baseOptions 的参数,返回的 是⼀个对象,包括 compile ⽅法属性和 compileToFunctions 属性:
compile 方法是用于编译的,这个方法的核心代码是 const compiled = baseCompile(template.trim(), finalOptions) ,调用 baseCompile 方法,返回编译后的 compiled 对象;
compileToFunctions 对应的就是 $mount 函数调⽤的 compileToFunctions ⽅法,它是调⽤ createCompileToFunctionFn ⽅法的返回值,我们接下来看⼀下 createCompileToFunctionFn ⽅ 法,它的定义在 src/compiler/to-function/js 中:

export function createCompileToFunctionFn (compile: Function): Function {
  //缓存对象
  const cache = Object.create(null)
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    //对象上的属性扩展一份到新对象上
    options = extend({}, options)
    //提取出warn错误提示信息函数
    const warn = options.warn || baseWarn
    //删除options里面的warn方法
    delete options.warn
    if (process.env.NODE_ENV !== 'production') {
      // 检测可能的CSP限制(检测 new Function() 是否可用)
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }
    // 检查缓存里是否已经存在
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }
    // 调用compile来解析 template和options(主要操作)
    const compiled = compile(template, options)
    // 检查编译errors/tips,并打印
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        if (options.outputSourceRange) {
          compiled.errors.forEach(e => {
            warn(
              `Error compiling template:\n\n${e.msg}\n\n` +
              generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(
            `Error compiling template:\n\n${template}\n\n` +
            compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {
        if (options.outputSourceRange) {
          compiled.tips.forEach(e => tip(e.msg, vm))
        } else {
          compiled.tips.forEach(msg => tip(msg, vm))
        }
      }
    }
    // 将代码转换为函数
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })
    // 检查函数生成错误。只有在编译器本身存在错误时才会发生这种情况。主要用于代码生成开发
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }
    //缓存
    return (cache[key] = res)
  }
}

⾄此我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数、编译模板 template , 编译配置 options 和 Vue 实例 vm ;核⼼的编译过程就⼀⾏代码:const compiled = compile(template, options);然后对 error 和 tips 进行处理;

总结

1、在 $mount 挂载的时候执行 compileToFunctions 方法;
2、在 compileToFunctions 方法中调用作为参数传入的 compile 方法编译模板,处理编译后的 errors 和 tips,将编译后的 render 和 staticRenderFns 转化为函数然后返回,并缓存编译结果,防止重复编译;
3、 compile 方法中将默认参数 baseOptions 和编译传入的参数 options 进行合并,调用 baseCompile 方法返回编译后的 compiled 对象;
4、然后在 最最核心的方法 baseCompile 里面分别调用 parse 、optimize 、generate 这三个方法;

2、parse

parse 方法的作用是对 template 模板进行编译,编译的结果是一个 AST 抽象语法树,AST 是对源代码抽象语法结构的树状表现形式。

模板编译阶段,是使用createASTElement方法来创建 AST。

export function createASTElement (
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1, //元素类型
    tag, // 元素标签
    attrsList: attrs, // 元素属性数组
    attrsMap: makeAttrsMap(attrs), // 元素属性key-value
    rawAttrsMap: {}, //ASTAttr类型的元素属性
    parent, // 父元素
    children: [] // 子元素集合
  }
}

返回的是一个 ASTElement 类型的对象。

插入一下:AST是什么?
看看下面这段代码:

<div>
	<p>{{name}}</p>
</div>

转成 AST 就是:

{
	type: 1,
	tag:"div",
	staticRoot: false,
    static: false,
    plain: true,
    attrsList: [],
    attrsMap: {},
	children: [
		{
			type: 1,
			tag: "p",
			staticRoot: false,
      		static: false,
      		plain: true,
      		attrsList: [],
  			attrsMap: {},
			parent: {type: 1, tag: "div", ...},
			children: [{
		        type: 2,
		        text: "{{name}}",
		        static: false,
		        expression: "_s(name)"
		    }]
		}
	]
}

不难看出,AST 就是利用 javascript 中的对象来描述节点,一个对象对应一个节点,对象中的属性保存的都是对应节点相关的数据;

下面我们看看parse方法,在src/compiler/parser/index.js文件中:

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  ....
  let root
  parseHTML(template, {
    ....
    //开始标签钩子
    start (tag, attrs, unary, start, end) {
     ....
    },
    //结束标签钩子
    end (tag, start, end) {
      ....
    },
    //文本钩子
    chars (text: string, start: number, end: number) {
     ....
    },
    //注释钩子
    comment (text: string, start, end) {
      ....
    }
  })
  return root
}

parse 方法主要是通过调用parseHTML 函数对模板字符串进行解析,通过传入不同的钩子函数分别对 开始标签、结束标签
文本、注释 进行处理,实际上parseHTML 函数的作用就是用来做词法分析的,而parse函数的作用则是在词法分析的基础上做句法分析从而生成一棵 AST。接下来看一下整个解析的过程;

parseHTML 解析

src/compiler/parser/html-parser.js 文件中:

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) {
    //字符串进行缓存,用于后面的结束判断
    last = html
    // 确保我们不是在像script/style这样的纯文本内容元素中
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      //html 字符串的第一个字符就是左尖括号
      if (textEnd === 0) {
        // 注释节点,前进⾄末尾位置
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')
          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            advance(commentEnd + 3)
            continue
          }
        }
        // 条件注释节点,前进⾄末尾位置
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')
          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }
        // 文本类型节点,前进它⾃⾝⻓度的距离
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }
        // 结束标签:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }
        // 开始标签:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
      }
      let text, rest, next
      //再次循环,第一个不是<符号,
      if (textEnd >= 0) {
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf('<', 1)
          if (next < 0) break
          textEnd += next
          rest = html.slice(textEnd)
        }
        text = html.substring(0, textEnd)
      }
      if (textEnd < 0) {
        text = html
      }
      if (text) {
        advance(text.length)
      }

      if (options.chars && text) {
        options.chars(text, index - text.length, index)
      }
    } else {
      // parse 的内容是在纯文本标签里 (script,style,textarea)
      let endTagLength = 0
      const stackedTag = lastTag.toLowerCase()
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length
        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
          text = text
            .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
            .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
        }
        if (shouldIgnoreFirstNewline(stackedTag, text)) {
          text = text.slice(1)
        }
        if (options.chars) {
          options.chars(text)
        }
        return ''
      })
      index += html.length - rest.length
      html = rest
      parseEndTag(stackedTag, index - endTagLength, index)
    }
	//相等说明循环结束
    if (html === last) {
      options.chars && options.chars(html)
      if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
        options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
      }
      break
    }
  }
  // 清理任何残留的标签
  parseEndTag()
  //字符串前进方法,不断截取
  function advance (n) {
    index += n
    html = html.substring(n)
  }
 }

parseHTML 解析器是通过 while 循环解析模板字符串 template,用正则做各种匹配,对不同的情况分别进行不同的处理,直到整个 template 被解析完毕;整个匹配的过程中会用到 advance 方法来不断移动字符串,直到字符串的末尾;

注意:script,style,textarea 是纯文本标签解析的时候会用文本标签来解析;

HTML解析类型

1、开始标签(<>):textEnd ( < 位置)等于0,正则校验是开始标签,遍历开始标签里面的属性并解析保存,然后截取掉开始标签,将二元标签的开始标签放入 stack 数组中,调用 start 方法;
2、结束标签(</>):textEnd ( < 位置)等于0,正则校验是结束标签,截取掉结束标签,然后检查 stack 数组里面是否有缺少闭合标签的二元标签,会特殊处理:将 </br> 标签解析成<br>标签,将 </p> 标签正常解析成 <p></p> ;最后缺少闭合标签的回逐个发出警告,并调用 end 方法将其闭合;
3、文本:textEnd ( < 位置)小于0或者大于等于0,将其截取,调用 chars 方法;(其他类型都匹配不上则认为是文本,文本中可以有 < 符号)
4、注释:textEnd ( < 位置)等于0,正则校验是注释,前进⾄末尾位置,调用 comment 方法;
5、doctype:textEnd ( < 位置)等于0,正则校验是 doctype,只需要将其截取;
6、条件注释:textEnd ( < 位置)等于0,正则校验是 ‘]>’,只需要将其截取;

下面介绍一下解析过程中用到的其他解析器:

属性解析

makeAttrsMap 方法把 name/value 对象数组形式,转换成 key/value 对象 ,AST 如下:

attrsList: [
    { name: 'id', value: 'box' },
    { name: 'class', value: 'box-class' },
    { name: ':class', value: 'boxClass' }
  ],
attrsMap: {
  id: 'box',
  class: 'box-class',
  :class: 'boxClass'
}
指令解析

这里只说一下 v-if (其他的就不细说了,大致思路都差不多)

function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

if 指令会对 AST 上添加一个 if 属性和一个 ifConditions 属性,for 指令类似,添加之后的 AST:

attrsList: [
 { name: 'v-if', value: 'list.length' }
],
attrsMap: {
  v-if: 'list.length'
},
if: 'list.length',
ifConditions: [
  { exp: 'list.length', block: 'ast对象自身', }
],
文本解析

对 html 解析器解析出来的文本二次加个,区分纯文本、带变量的文本;
src/compiler/parser/text-parser.js 文件里:

export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // 先把 { { 前边的文本添加到 tokens 中
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // 把变量改成 `_s(x)` 这样的形式也添加到数组中
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  // 当所有变量都处理完毕后, 如果最后一个变量右边还有文本, 就将文本添加到数组中
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

如果是纯文本, 直接 return,在chars 方法中创建一个 type = 3 的对象。 如果是带变量的文本, 使用正则表达式匹配出文本中的变量, 先把变量左边的文本添加到数组中, 然后把变量改成 _s(x) 这样的形式也添加到数组中.;如果变量后面还有变量, 则重复以上动作, 直到所有变量都添加到数组中.,最后创建一个 type = 2 的对象;

处理后的 AST:

parseText('文本{{name}}');
->
'"文本" + _s(name)'
过滤器解析

vue 的 filter 允许用在两个地方,一个是双括号插值,一个是 v-bind 表达式后面,在 src/compiler/parser/filter-parser.js 文件里:

export function parseFilters (exp: string): string {
  let inSingle = false
  let inDouble = false
  let inTemplateString = false
  let inRegex = false
  let curly = 0
  let square = 0
  let paren = 0
  let lastFilterIndex = 0
  let c, prev, i, expression, filters
  //循环变量属性
  for (i = 0; i < exp.length; i++) {
    prev = c
    //exp第i个字符的ASCII码
    c = exp.charCodeAt(i)
    if (inSingle) {
      if (c === 0x27 && prev !== 0x5C) inSingle = false
    } else if (inDouble) {
      if (c === 0x22 && prev !== 0x5C) inDouble = false
    } else if (inTemplateString) {
      if (c === 0x60 && prev !== 0x5C) inTemplateString = false
    } else if (inRegex) {
      if (c === 0x2f && prev !== 0x5C) inRegex = false
    } else if (
      c === 0x7C && // pipe
      // 前后都不是| 确定不是或运算 ||
      exp.charCodeAt(i + 1) !== 0x7C &&
      exp.charCodeAt(i - 1) !== 0x7C &&
       不在各种括弧内
      !curly && !square && !paren
    ) {
      if (expression === undefined) {
        // first filter, end of expression
        lastFilterIndex = i + 1
        expression = exp.slice(0, i).trim()
      } else {
        pushFilter()
      }
    } else {
      switch (c) {
        case 0x22: inDouble = true; break         // "
        case 0x27: inSingle = true; break         // '
        case 0x60: inTemplateString = true; break // `
        case 0x28: paren++; break                 // (
        case 0x29: paren--; break                 // )
        case 0x5B: square++; break                // [
        case 0x5D: square--; break                // ]
        case 0x7B: curly++; break                 // {
        case 0x7D: curly--; break                 // }
      }
      if (c === 0x2f) { // /
        let j = i - 1
        let p
        // find first non-whitespace prev char
        for (; j >= 0; j--) {
          p = exp.charAt(j)
          if (p !== ' ') break
        }
        if (!p || !validDivisionCharRE.test(p)) {
          inRegex = true
        }
      }
    }
  }
  // 如果expression 为空,则说明没有没有filter函数,或者是写法出了问题。某些符号没闭合
  if (expression === undefined) {
    expression = exp.slice(0, i).trim()
  } else if (lastFilterIndex !== 0) {
    pushFilter()
  }
  // 初始化filters变量。将过滤器函数推入filters数组中
  function pushFilter () {
    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
    lastFilterIndex = i + 1
  }
  // filter函数结合expression生成最终的表达式
  if (filters) {
    for (i = 0; i < filters.length; i++) {
      expression = wrapFilter(expression, filters[i])
    }
  }
  return expression
}

parseFilters 方法是循环 exp,找出过滤器放到 expression 中,然后执行 pushFilter 将过滤器推到 filters 数组中,最后循环调用 wrapFilter 方法对每一个过滤器做处理;这里区分过滤器的写法:函数名、fn(sss)

生成的AST:

let html = '<div>{{ msg | reverse() | toUpperCase() }}</div>'
//filter解析之后
const expression = '_f("toUpperCase")(_f("reverse")(msg))'
//在文本中再次解析
const tokens = ['_s(_f("toUpperCase")(_f("reverse")(msg)))']

最后来看一下 parse 方法里面的四个钩子函数:

start

处理开始标签,这个钩子函数就是慢慢地给这个AST进行装饰,添加更多的属性和标志;

start (tag, attrs, unary, start, end) {
      // 检查命名规范,父元素有则继承
      const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
      // 兼容IE svg
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs)
      }
	  //通过createASTElement创建AST
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      //为AST添加相关属性
      if (ns) {
        element.ns = ns
      }
      if (process.env.NODE_ENV !== 'production') {
        if (options.outputSourceRange) {
          element.start = start
          element.end = end
          element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
            cumulated[attr.name] = attr
            return cumulated
          }, {})
        }
        attrs.forEach(attr => {
          if (invalidAttributeRE.test(attr.name)) {
            warn(
              `Invalid dynamic argument expression: attribute names cannot contain ` +
              `spaces, quotes, <, >, / or =.`,
              {
                start: attr.start + attr.name.indexOf(`[`),
                end: attr.start + attr.name.length
              }
            )
          }
        })
      }
	  // 服务端渲染的情况下是否存在被禁止标签
      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true
        process.env.NODE_ENV !== 'production' && warn(
          'Templates should only be responsible for mapping the state to the ' +
          'UI. Avoid placing tags with side-effects in your templates, such as ' +
          `<${tag}>` + ', as they will not be parsed.',
          { start: element.start }
        )
      }
      // 预处理一些动态类型:v-model
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }
      // 对vue的指令进行处理v-pre、v-if、v-for、v-once、slot、key、ref
      if (!inVPre) {
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        processFor(element)
        processIf(element)
        processOnce(element)
      }
      // 限制根节点不能是slot,template,v-for这类标签
      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(root)
        }
      }
	  // 不是单标签就入栈,是的话结束这个元素的
      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
    }
end

对缓存开始标签的 stack 数组操作,去除最后一个,记录结束位置,最后关闭元素;

end (tag, start, end) {
  //缓存栈顶元素
   const element = stack[stack.length - 1]
   // 栈顶元素推出
   stack.length -= 1
   currentParent = stack[stack.length - 1]
   if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
     element.end = end
   }
   //关闭元素
   closeElement(element)
 }
chars

文本标签,没有父元素将会报错,然后对空格做一些处理,后面就是根据文本类型生成对于的 AST 对象;

chars (text: string, start: number, end: number) {
	   // 判断有没有父元素,没有则抛出警告
      if (!currentParent) {
        if (process.env.NODE_ENV !== 'production') {
          if (text === template) {
            warnOnce(
              'Component template requires a root element, rather than just text.',
              { start }
            )
          } else if ((text = text.trim())) {
            warnOnce(
              `text "${text}" outside root element will be ignored.`,
              { start }
            )
          }
        }
        return
      }
      // IE textarea placeholder bug
      if (isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
      ) {
        return
      }
      // 储存下currentParent的子元素
      const children = currentParent.children
      if (inPre || text.trim()) {
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
      } else if (!children.length) {
        // 将连续的空格压缩为单个空格
        text = ''
      } else if (whitespaceOption) {
        if (whitespaceOption === 'condense') {
          // 在压缩模式下,如果包含换行符,则删除空白节点,否则压缩为单个空格
          text = lineBreakRE.test(text) ? '' : ' '
        } else {
          text = ' '
        }
      } else {
        text = preserveWhitespace ? ' ' : ''
      }
      if (text) {
        if (!inPre && whitespaceOption === 'condense') {
          // 将连续的空格压缩为单个空格
          text = text.replace(whitespaceRE, ' ')
        }
        let res
        let child: ?ASTNode
        // 解析文本,动态属性
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          child = {
            type: 3,
            text
          }
        }
        if (child) {
          if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
            child.start = start
            child.end = end
          }
          children.push(child)
        }
      }
    }
comment

直接生成 type = 3 的 AST 对象,放到 currentParent.children 里面;

comment (text: string, start, end) {
      // adding anything as a sibling to the root node is forbidden
      // comments should still be allowed, but ignored
      if (currentParent) {
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          child.start = start
          child.end = end
        }
        currentParent.children.push(child)
      }
    }
3、optimize

我们的模板 template 经过 parse 解析之后,会返回一个 AST 树,而 optimize 就是对这个树的优化;那么为什么要经过这个过程呢?

因为 Vue 是数据驱动,很多数据都是响应式的,但是我们的模板中并不是所有的数据都是响应式的,在 patch 的过程中需要跳过这些非响应式数据的对比来提升 patch 的性能,而 optimize 会将一些 AST 节点优化成静态节点;

src/compiler/optimizer.js 文件中:

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 第一次:标记所有非静态节点。
  markStatic(root)
  // 第二步:标记静态根。
  markStaticRoots(root, false)
}
静态节点标记
function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  //type = 2 、3 已经在isStatic里面处理过了
  if (node.type === 1) {
    // node.tag 不是 HTML 保留标签时、标签不是slot、node不是一个内联模板容器;直接返回false
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    //递归子节点,根据子节点的static设置父节点的static
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
    //如果当前节点有v-if/v-else-if/v-else等指令
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}
//当前节点是否是静态节点
function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

静态节点:有一个连续判断,当前节点的tag不是 HTML 保留标签也不是 slot ,同时当前节点不是一个内联模板容器,则直接返回当前节点的 static 为false,不在循环子节点;否则循环子节点,在对 children 子节点标记完毕后,会根据子节点的 static 属性来设置父节点的 static 属性,只要有一个子节点的 static 属性不为 true ,那么父节点也一定不为 true 。

下面这三种情况肯定是静态节点:

1、纯文本节点;
2、普通元素节点并且使用了v-pre指令都是静态节点;
3、在没有使用v-pre指令的情况下,还必须同时满足:没有动态绑定属性、没有使用v-if、没有使用v-for、不是内置组件slot/component、是平台保留标签、不是带有v-for的template标签的直接子节点、节点的所有属性的key都是静态key;

静态根节点标记
function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    // 当前节点是静态的,子节点不能只是一个且是纯文本节点
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

当前节点是静态的,那么它所有的子节点都应该是静态的,这是在标记静态节点的时候就处理好的;对于只有一个纯文本节点的根节点不做优化处理,每次都会重新渲染;

optimize 优化的过程中,它的处理方式是深度遍历 AST 树形结构,遇到静态节点的时候把它的 ast.static 属性设置为 true。同时对于一个父 AST 节点来说,当其 children 子节点全部为静态节点的时候,那么其本身也是一个静态节点,我们把它的 ast.staticRoot 设置为 true。

4、generate

compileToFunctions 中,会把这个 render 代码串转换成函数,它的定义在 src/compler/to-function.js

const compiled = compile(template, options)
res.render = createFunction(compiled.render, fnGenErrors) 
function createFunction (code, errors) { 
	try { 
		return new Function(code) 
	} catch (err) { 
		errors.push({ err, code }) 
		return noop } 
	}

把 render 代码串通过 new Function 的⽅式转换成可执⾏的函数,赋值给 vm.options.render ,这样当组件通过 vm._render 的时候,就会执⾏这个 render 函数,那么接下来就开始 generate 的部分看看 render 代码串的生成过程;

generate 这里是编译的最后一步,将 AST 树转换成可执行的代码(render),在src/compiler/codegen/index.js 里面:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

可以看出,关键部分在 code,ast 存在 则调用 genElement ,否则调用 _c_ccreateElement 方法用来创建 vnode;code 最后会用
with(this){return ${code}} 包裹起来,转换成方法:

const func = function () {
  with (this) {
    return ${code}
  }
}

接下来看看 genElement

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

这里没什么好说的,就是根据 AST 属性的不同来分别调用不同的代码生成函数,最后返回 code;

genStatic

在第一次执行genElement方法时,节点的 staticRoot 为 true,则会走这个方法;

function genStatic (el: ASTElement, state: CodegenState): string {
  //避免重复调用
  el.staticProcessed = true
  // 在v-pre节点中,有些元素(模板)的行为需要有所不同。所有的pre节点都是静态根节点
  // 所以我们可以使用它作为一个位置来包装状态更改,并在退出pre节点时重置它。
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

这里特殊处理了 v-pre 节点,默认state 的 pre 是 false,如果当前节点有 pre 属性,则赋值过来,push 完之后重置 pre 的值;然后递归调用 genElement 方法处理子节点,将所有是静态根节点都放到 staticRenderFns 数组中;最后返回一个用 _m 方法处理过的值;

_m 方法是一个辅助器,看看是走缓存还是重新渲染新的树;

这里可以看出:静态根节点代码生成的 render 存放在 staticRenderFns 中,而不是 render

genOnce

用来处理 v-once 指令的

function genOnce (el: ASTElement, state: CodegenState): string {
  el.onceProcessed = true
  if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.staticInFor) {
    let key = ''
    let parent = el.parent
    while (parent) {
      if (parent.for) {
        key = parent.key
        break
      }
      parent = parent.parent
    }
    if (!key) {
      process.env.NODE_ENV !== 'production' && state.warn(
        `v-once can only be used inside v-for that is keyed. `,
        el.rawAttrsMap['v-once']
      )
      return genElement(el, state)
    }
    return `_o(${genElement(el, state)},${state.onceId++},${key})`
  } else {
    return genStatic(el, state)
  }
}

如果与 if 并存,就先执行 genIf 再执行 genOnce;如果在 for 循环中且为静态节点,用 _o 方法进行标记;否则使用 genStatic 方法生成节点,genStatic具有缓存性;

genFor

用来处理 v-for 指令的

export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }
  el.forProcessed = true // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}

从 AST 节点获取和 for 相关属性,然后返回一个循环的 function 字符串;然后调用 genElement 方法处理子节点;

genIf

用来处理 v-if/v-else 等指令的;

export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }
  //获取数组的第一个元素
  const condition = conditions.shift()
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

主要是使用 genIfConditions方法;入参 el.ifConditions 调用了 slice() 返回的其实就是 el.ifConditions;每次使用 shift() 获取数组第一项,如果有 exp 属性则拼接并递归调用 genIfConditions 方法处理子节点;最后会调用 genTernaryExp 方法转换成 (a)?_m(0):_m(1) 这种模式;然后调用 genElement 方法处理子节点;

genChildren

处理子节点

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

如果是node.type为1,那么又会回到genElement方法继续生成 vnode ;否则生成文本节点或注释节点;

genSlot

处理插槽

function genSlot (el: ASTElement, state: CodegenState): string {
  const slotName = el.slotName || '"default"'
  const children = genChildren(el, state)
  let res = `_t(${slotName}${children ? `,${children}` : ''}`
  const attrs = el.attrs || el.dynamicAttrs
    ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
        // slot props are camelized
        name: camelize(attr.name),
        value: attr.value,
        dynamic: attr.dynamic
      })))
    : null
  const bind = el.attrsMap['v-bind']
  if ((attrs || bind) && !children) {
    res += `,null`
  }
  if (attrs) {
    res += `,${attrs}`
  }
  if (bind) {
    res += `${attrs ? '' : ',null'},${bind}`
  }
  return res + ')'
}
genComponent

处理组件

function genComponent (
  componentName: string,
  el: ASTElement,
  state: CodegenState
): string {
  const children = el.inlineTemplate ? null : genChildren(el, state, true)
  return `_c(${componentName},${genData(el, state)}${
    children ? `,${children}` : ''
  })`
}

和普通节点生成类似,不同点则是标签名称是组件名称;

genData

处理节点上各种属性、指令、事件、作用域插槽以及 ref 等等;

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind dynamic argument wrap
  // v-bind with dynamic arguments must be applied using the same v-bind object
  // merge helper so that class/style/mustUseProp attrs are handled correctly.
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

根据 AST 元素节点的属性构造出⼀个 data 对象字符串,这个在后⾯创建 vnode 的时候的时候会作为参数传⼊;

总结:
parse 方法将 tempalte 模板通过正则匹配进行词法语法分析,编译成 AST 树(抽象语法树); optimize 方法 将解析后的 AST 树通过静态根节点标记的方式进行优化; generate 方法将优化后的 AST 树转换成可执行的代码(字符串);

最后会通过 createFunction 方法将 compile 转化的结果通过 new Function(code) 转成函数,这样在 $mount 阶段的 renderstaticRenderFns 就得到了,后面就是 render 比对和真实 DOM 的渲染;

编译的部分就到这里了! 有不对的地方欢迎留言指正~~

下一篇:Vue2.x 源码 - render 函数生成 VNode

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值