博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【大型干货】来看看vue对template做了什么(附部分源码及注释)
阅读量:7304 次
发布时间:2019-06-30

本文共 14120 字,大约阅读时间需要 47 分钟。

在整理这篇文章时,我感到了困惑,困惑的是不知道该怎么用更好的方式来将这部分繁琐的内容让你很容易的看的明白。我也希望这篇文章能作为你的在阅读时引导,你可以一起边看引导,边看源码。

如何找到我们需要关注的最终方法

还记得之前的《》吗?在那里,我们已经知道,在src/platform/web/entry-runtime-with-compiler.js重写原型的$mount方法时,已经将template转换为render函数了,接下来,我们从这个地方作为入口。

const { render, staticRenderFns } = compileToFunctions(template, {    shouldDecodeNewlines,    shouldDecodeNewlinesForHref,    delimiters: options.delimiters,    comments: options.comments  }, this)复制代码

compileToFunctions从哪来的?说真的 这个问题看源码的时候 还挺绕的,首先引自于platforms/web/compiler/index.js,然后发现是调用了src/compiler/index.jscreateCompiler方法,而createCompiler又是通过调用src/compiler/create-compiler.jscreateCompilerCreator方法,而我们使用的compileToFunctions方法呢,又是通过调用src/compiler/to-function.js中的createCompileToFunctionFn来创建的,所以,这里,我们为了好记,暂时忽略中间的所有步骤。 先从最后一步开始看吧。

createCompileToFunctionFn(src/compiler/to-function.js)

代码有点多久不在这里不全贴出来了,我说,你看着。

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.'	  )	}}复制代码

这段代码做了什么?在当前情景下,相当于eval('function fn() {return 1},检测当前网页的内容安全政策,具体CSP是什么,可以看一下。这里为什么要做检测?好问题,先记住,继续往下看,你会自己得到答案。

const key = options.delimiters  ? String(options.delimiters) + template  : templateif (cache[key]) {  return cache[key]}复制代码

又一段代码来为了提高效率,直接从缓存中查找是否有已经编译好的结果,有则直接返回。

const compiled = compile(template, options)复制代码

这里的compile方法就需要看src/compiler/create-compiler.js文件的createCompilerCreator中的function compile了。

compile(src/compiler/create-compiler.js)

在compile方法里,主要做了做了3件事

  1. 将传入的CompilerOptions经过处理后挂载至finalOptions

    这里finalOptions最终会变成:

  1. templatefinalOptions传入src/compiler/index.js文件的baseCompile中。
  2. 收集ast转换时的错误信息。

baseCompile(src/compiler/index.js)

这里我们进入到baseCompile中,看看baseCompile做了什么。

parse
// 将传入html 转换为ast语法树const ast = parse(template.trim(), options)复制代码

划重点啦,通过parse方法将我们传入的template中的内容,转换为AST语法树

一起来看下src/compiler/parser/index.js文件中的parse方法。

function parse (template, options){  warn = options.warn || baseWarn  platformIsPreTag = options.isPreTag || no  platformMustUseProp = options.mustUseProp || no  platformGetTagNamespace = options.getTagNamespace || no  // pluckModuleFunction:找出options.mudules中每一项中属性含有key方法  transforms = pluckModuleFunction(options.modules, 'transformNode')  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')  delimiters = options.delimiters  // 存放astNode  const stack = []  const preserveWhitespace = options.preserveWhitespace !== false  // 定义根节点  let root  // 当前处理节点的父节点  let currentParent  // 标识属性中是否有v-pre的  // 什么是v-pre,见 https://cn.vuejs.org/v2/api/#v-pre  let inVPre = false  // 标识是否为pre标签  let inPre = false  // 标识是否已经触发warn  let warned = false  function warnOnce (msg) {}  function closeElement (element) {}  // 通过循环的方式解析传入html  parseHTML(params)  /**	* 处理v-pre	* @param {*} el	*/  function processPre() {}    /**   * 处理html原生属性   * @param {*} el   */  function processRawAttrs (el) {}  return root}复制代码

从上面部分我们可以看出,实际做转换的是parseHTML方法,我们在上面省略了parseHTML的参数,因为在parseHTML方法内部,会用到参数中的startendcharscomment方法,所以为了防止大家迷惑,在这里我会在文章的最后,拆出来专门为每个方法提供注释,方便大家阅读。

这里先进入src/compiler/parser/html-parser.js中看看parseHTML方法吧。

function parseHTML (html, options) {  const stack = []  const expectHTML = options.expectHTML  const isUnaryTag = options.isUnaryTag || no  const canBeLeftOpenTag = options.canBeLeftOpenTag || no  // 声明index,标识当前处理传入html的索引  let index = 0  let last, lastTag  // 循环处理html  while (html) {...}  // Clean up any remaining tags  parseEndTag()  /**   * 修改当前处理标记索引,并且将html已处理部分截取掉   * @param {*} n 位数   */  function advance (n) {}  /**   * 处理开始标签,将属性放入attrs中   */  function parseStartTag () {}  /**   * 将parseStartTag处理的结果进行处理并且通过options.start生成ast node   * @param {*} match 通过parseStartTag处理的结果   */  function handleStartTag (match) {}  /**   * 处理结束标签   * @param {*} tagName 标签名   * @param {*} start 在html中起始位置   * @param {*} end 在html中结束位置   */  function parseEndTag (tagName, start, end) {}}复制代码

这里我们保留了部分片段,完整的注释部分,我会放在文章的最后。

通过parse方法,我们将整个抽象语法树拿到了。

optimize

对当前抽象语法树进行优化,标识出静态节点,这部分我们下一篇关于vNode的文章会再提到。

generate(scr/compiler/codegen/index.js)

这部分会将我们的抽象语法树,转换为对应的render方法的字符串。有兴趣的可以自行翻阅,看这部分时,你会更清晰一点 在vue instance时,为原型挂载了各种_字母的方法的用意

没错 你没看做,这里转换的是with(this){...}的字符串,所以上面为什么在编译template会检测是否允许使用eval是不是有眉目了?。

小结

通过compile中的parseoptimizegeneratetemplate转换为了render

最后

如果你喜欢,我会继续为你带来render时,虚拟DOM相关的文章。

附录—源码+注释

options.start

start (tag, attrs, unary) {  // check namespace.  // inherit parent ns if there is one  const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)  // handle IE svg bug  /* istanbul ignore if */  // 处理IE的svg的BUG  if (isIE && ns === 'svg') {    attrs = guardIESVGBug(attrs)  }  // 创建AST NODE  let element: ASTElement = createASTElement(tag, attrs, currentParent)  if (ns) {    element.ns = ns  }  // 判断当前节点如果是

options.end

end () {  // remove trailing whitespace  // 拿到stack中最后一个ast node  const element = stack[stack.length - 1]  // 找到最近处理的一个节点  const lastNode = element.children[element.children.length - 1]  if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {    element.children.pop()  }  // pop stack  // 将element从stack移除  stack.length -= 1  currentParent = stack[stack.length - 1]  closeElement(element)}复制代码

options.chars

chars (text: string) {  // 文本没有父节点处理  if (!currentParent) {    if (process.env.NODE_ENV !== 'production') {      if (text === template) {        warnOnce(          'Component template requires a root element, rather than just text.'        )      } else if ((text = text.trim())) {        warnOnce(          `text "${text}" outside root element will be ignored.`        )      }    }    return  }  // IE textarea placeholder bug  /* istanbul ignore if */  if (isIE &&    currentParent.tag === 'textarea' &&    currentParent.attrsMap.placeholder === text  ) {    return  }  const children = currentParent.children  // 格式化text  text = inPre || text.trim()    ? isTextTag(currentParent) ? text : decodeHTMLCached(text)    // only preserve whitespace if its not right after a starting tag    : preserveWhitespace && children.length ? ' ' : ''  if (text) {    // 处理{
{text}}部分,将{
{text}}转为 // {expression: '_s(text)', token: [{'@binding': 'text'}]} let res if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text }) } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text }) } }}复制代码

parseHTML

function parseHTML (html, options) {  const stack = []  const expectHTML = options.expectHTML  const isUnaryTag = options.isUnaryTag || no  const canBeLeftOpenTag = options.canBeLeftOpenTag || no  // 声明index,标识当前处理传入html的索引  let index = 0  let last, lastTag  while (html) {    last = html    // Make sure we're not in a plaintext content element like script/style    if (!lastTag || !isPlainTextElement(lastTag)) {      let textEnd = html.indexOf('<')      // 是否以
<开头 if (textend="==" 0) { comment: 判断是否是 // Special instructions for IE 6 here //
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment if (conditionalComment.test(html)) { const conditionalEnd = html.indexOf(']>') if (conditionalEnd >= 0) { advance(conditionalEnd + 2) continue } } // 判断是否以...' // 的标签处理为 html = '...
' // 返回的match = {
// start: 0, // 开始索引 // end: 15, // 结束索引 // tagName: 'div' // attrs: [] // 这里的数组为正则匹配出来标签的attributes // } // Start tag: const startTagMatch = parseStartTag() if (startTagMatch) { handleStartTag(startTagMatch) if (shouldIgnoreFirstNewline(lastTag, html)) { advance(1) } continue } } let text, rest, next // 这里由于我们的html代码可能会有制表位 换行等不需要解析的操作 // 这里将无用的东西移除,然后继续循环html 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) advance(textEnd) } if (textEnd < 0) { text = html html = '' } // 处理字符 if (options.chars && text) { options.chars(text) } } else { let endTagLength = 0 const stackedTag = lastTag.toLowerCase() const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(
]*>)', 'i')) const rest = html.replace(reStackedTag, function (all, text, endTag) { endTagLength = endTag.length if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { text = text .replace(/
/g, '$1') // #7298 .replace(/
/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}"`) } break } } // Clean up any remaining tags parseEndTag() /** * 修改当前处理标记索引,并且将html已处理部分截取掉 * @param {*} n 位数 */ function advance (n) { index += n html = html.substring(n) } /** * 处理开始标签,将属性放入attrs中 */ function parseStartTag () { const start = html.match(startTagOpen) if (start) { const match = { tagName: start[1], attrs: [], start: index } // 处理完头部信息,将头部移除掉 advance(start[0].length) let end, attr // 循环找尾部【>】,如果没有到尾部时,就向attrs中添加当前正则匹配出的属性。 while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { advance(attr[0].length) match.attrs.push(attr) } // 将尾部【>】从html中移除,记录当前处理完的索引 if (end) { match.unarySlash = end[1] advance(end[0].length) match.end = index return match } } } /** * 将parseStartTag处理的结果进行处理并且通过options.start生成ast node * @param {*} match 通过parseStartTag处理的结果 */ function handleStartTag (match) { const tagName = match.tagName const unarySlash = match.unarySlash if (expectHTML) { if (lastTag === 'p' && isNonPhrasingTag(tagName)) { parseEndTag(lastTag) } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { parseEndTag(tagName) } } const unary = isUnaryTag(tagName) || !!unarySlash const l = match.attrs.length const attrs = new Array(l) for (let i = 0; i < l; i++) { const args = match.attrs[i] // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778 if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) { if (args[3] === '') { delete args[3] } if (args[4] === '') { delete args[4] } if (args[5] === '') { delete args[5] } } const value = args[3] || args[4] || args[5] || '' const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines attrs[i] = { name: args[1], value: decodeAttr(value, shouldDecodeNewlines) } } if (!unary) { stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs }) lastTag = tagName } if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } } /** * 处理结束标签 * @param {*} tagName 标签名 * @param {*} start 在html中起始位置 * @param {*} end 在html中结束位置 */ function parseEndTag (tagName, start, end) { let pos, lowerCasedTagName if (start == null) start = index if (end == null) end = index if (tagName) { lowerCasedTagName = tagName.toLowerCase() } // Find the closest opened tag of the same type // 从stack中找到与当前tag匹配的节点,这里利用倒序,匹配最近的 if (tagName) { for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0 } if (pos >= 0) { // Close all the open elements, up the stack for (let i = stack.length - 1; i >= pos; i--) { if (process.env.NODE_ENV !== 'production' && (i > pos || !tagName) && options.warn ) { options.warn( `tag <${stack[i].tag}> has no matching end tag.` ) } if (options.end) { options.end(stack[i].tag, start, end) } } // Remove the open elements from the stack stack.length = pos lastTag = pos && stack[pos - 1].tag // 处理br } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end) } // 处理p标签 } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end) } if (options.end) { options.end(tagName, start, end) } } }}复制代码

转载地址:http://zqbnm.baihongyu.com/

你可能感兴趣的文章
Asp.net 程序连接orcle如果在安装 32 位 Oracle 客户端组件的情况下以 64 位模式运行,...
查看>>
自己写的模板引擎,模板生成静态页面
查看>>
Android 数据库管理— — —更新数据
查看>>
014_捆绑包与显示模式
查看>>
python : logging模块format类
查看>>
[LeetCode] Two Sum
查看>>
java类中的初始化顺序
查看>>
win10远程桌面连接
查看>>
[转]Web Service与WCF区别
查看>>
vs2010 .net4.0 错误 事件的显式接口实现必须使用事件访问器语法
查看>>
BZOJ1090:[SCOI2003]字符串折叠——题解
查看>>
Python网络爬虫-爬取微博热搜
查看>>
js 与或运算符 || && 妙用
查看>>
react-conponent-secondesElapsed
查看>>
DFS 10.1.5.253 1501
查看>>
vue 项目分享
查看>>
smb
查看>>
3.算法-二叉树遍历
查看>>
File类
查看>>
基于层次关联的鲁棒多目标跟踪
查看>>