资讯专栏INFORMATION COLUMN

讲解vue parseHTML源码解析harsendcomment钩子函数

3403771864 / 290人阅读

  我们现在要讲述的是当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,那又该作何“处理”。

  parseHTML(template, {
  chars: function(){
  //...
  },
  //...
  })

  chars源码:

   chars: function chars(text) {
  if (!currentParent) {
  {
  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
  }
  var children = currentParent.children;
  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) {
  var res;
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  children.push({
  type: 2,
  expression: res.expression,
  tokens: res.tokens,
  text: text
  });
  } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  children.push({
  type: 3,
  text: text
  });
  }
  }
  }

  当解析器遇到文本节点时,如上代码中的 chars 钩子函数就会被调用,并且接收该文本节点的文本内容作为参数。

  我们来看chars钩子函数最开始的这段代码:

  if (!currentParent) {
  {
  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
  }

  先确定 currentParent 变量存在与否,指向的是当前节点的父节点:。

  如果 currentParent 变量不存在说明什么问题?

  1:没有根元素,只有文本。

  2: 文本在根元素之外。

  当遇到第一种情况打印警告信息:"模板必须要有根元素",第二种情况打印警告信息:" 根元素外的文本将会被忽略"。

  接下来:

  if (isIE &&
  currentParent.tag === 'textarea' &&
  currentParent.attrsMap.placeholder === text
  ) {
  return
  }

  其实上面的代码就是解决 IE 浏览器中渲染 <textarea> 标签的 placeholder 属性时存在的 bug 的。现在我们看看issue。

  接下来是个嵌套三元表达式:

  var children = currentParent.children;
  text = inPre || text.trim() ?
  isTextTag(currentParent) ? text : decodeHTMLCached(text)
  // only preserve whitespace if its not right after a starting tag
  :
  preserveWhitespace && children.length ? ' ' : '';

  上面的嵌套三元表达式判断就是为了明确条件 inPre || text.trim() 的真假,当结果为 true,检测了当前文本节点的父节点是否是文本标签,当文本标签则直接使用原始文本,否则使用decodeHTMLCached 函数对文本进行解码。

  假如反馈inPre || text.trim() 如果为 false,检测 preserveWhitespace 是否为 true 。preserveWhitespace 简单来说就是一个布尔值代表着是否保留空格,仅在返回为它为 false的情况下才会保留空格。但即使 preserveWhitespace 常量的值为真,如果当前节点的父节点没有子元素则也不会保留空格,换句话说,编译器只会保留那些不存在于开始标签之后的空格。

  接下来:

  if (text) {
  var res;
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  children.push({
  type: 2,
  expression: res.expression,
  tokens: res.tokens,
  text: text
  });
  } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  children.push({
  type: 3,
  text: text
  });
  }
  }

  这里相当就比较简单了一个 if else if 操作,第一个 if 判断当前元素未使用v-pre 指令,text不为空,使用 parseText 函数成功解析当前文本节点的内容。

  对于前两个条件很好理解,关键在于 parseText 函数能够成功解析文本节点的内容说明了什么,如下示例代码:

  <div> hello: {{ message }} </div>

  如上模板中存在的文本节点包含了 Vue 语法中的字面量表达式,而 parseText 函数的作用就是用来解析这段包含了字面量表达式的文本的。此时会执行以下代码创建一个类型为2(type = 2) 的元素描述对象:

  children.push({
  type: 2,
  expression: res.expression,
  tokens: res.tokens,
  text: text
  });

  注意:类型为 2 的元素描述对象拥有三个特殊的属性,分别是 expression 、tokens 以及text ,其中 text 就是原始的文本内容,而 expression 和 tokens 的值是通过 parseText 函数解析的结果中读取的。

  现在讲讲parseText函数,当出现 if 判断失败出现的三种可能性。

  当前解析的元素使用v-pre 指令

  text 为空

  parseText 解析失败

  只要以上三种情况中,有一种情况出现则代码会来到else...if 分支的判断,如下:

  else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  children.push({
  type: 3,
  text: text
  });
  }

  如果满足 else if 中的条件直接,创建一个类型为3(type = 3) 的元素描述对象:类型为3 的元素描述对象只拥有一个的属性text存储原始的文本内容。

  在看下要满足 else if 中的这些条件吧!

  文本内容不是空格

  文本内容是空格,但是该文本节点的父节点还没有子节点(即 !children.length )

  文本内容是空格,并且该文本节点的父节点有子节点,但最后一个子节点不是空格

  接下来我们来聊聊之前讲到的parseText 函数。

  parseText

  function parseText(
  text,
  delimiters
  ) {
  var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
  if (!tagRE.test(text)) {
  return
  }
  var tokens = [];
  var rawTokens = [];
  var lastIndex = tagRE.lastIndex = 0;
  var match, index, tokenValue;
  while ((match = tagRE.exec(text))) {
  index = match.index;
  // push text token
  if (index > lastIndex) {
  rawTokens.push(tokenValue = text.slice(lastIndex, index));
  tokens.push(JSON.stringify(tokenValue));
  }
  // tag token
  var 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
  }
  }

  parseText 接收两个参数 text 要解析的文本,delimiters 是编译器的一个用户自定义选项delimiters,通过它可以改变文本插入分隔符。所以才有了如下代码。

  var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;

  这里是解析文本所用正则之间的一个较量,delimiters 有值就调用buildRegex函数,我们默认是没有值,使用 defaultTagRE 来解析文本。

  var defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

  这个正则还是非常简单,接下来会判断,如果文本中没有与正则相匹配的文本直接直接终止函数的执行。

  if (!tagRE.test(text)) {
  return
  }

  接下来代码就有意思了一起看下。

  var tokens = [];
  var rawTokens = [];
  var lastIndex = tagRE.lastIndex = 0;
  var match, index, tokenValue;
  while ((match = tagRE.exec(text))) {
  index = match.index;
  // push text token
  if (index > lastIndex) {
  rawTokens.push(tokenValue = text.slice(lastIndex, index));
  tokens.push(JSON.stringify(tokenValue));
  }
  // tag token
  var 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
  }

  这段代码不难,初始定义了一系列变量。 接着开启一个while循环,使用 tagRE 正则匹配文本内容,并将匹配结果保存在 match 变量中,直到匹配失败循环才会终止,这时意味着所有的字面量表达式都已经处理完毕了。

  在这个while循环结束返回一个对象,expression、tokens分别存储解析过程中的信息。

  假设文本如下:

 <div id="app">hello {{ message }}</div>

  parseText 解析文本后返回的对象。

  {
  expression: "'hello'+_s(message)",
  tokens: [
  'hello',
  {
  '@binding': 'message'
  }
  ]
  }

  接下来我们聊聊对结束标签的处理。

  end 源码

  end: function end() {
  // remove trailing whitespace
  var element = stack[stack.length - 1];
  var lastNode = element.children[element.children.length - 1];
  if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  element.children.pop();
  }
  // pop stack
  stack.length -= 1;
  currentParent = stack[stack.length - 1];
  closeElement(element);
  }

  end 钩子函数,当解析 html 字符串遇到结束标签的时候,。那么在 end 钩子函数中都需要做哪些事情呢?

  在之前的文章中我们讲过解析器遇到非一元标签的开始标签时,会将该标签的元素描述对象设置给 currentParent 变量,代表后续解析过程中遇到的所有标签都应该是 currentParent 变量所代表的标签的子节点,同时还会将该标签的元素描述对象添加到 stack 栈中。

  而当遇到结束标签的时候则意味着 currentParent 变量所代表的标签以及其子节点全部解析完毕了,此时我们应该把 currentParent 变量的引用修改为当前标签的父标签,这样我们就将作用域还原给了上层节点,以保证解析过程中正确的父子关系。

  下面代码就是来完成这个工作:

  stack.length -= 1;

  currentParent = stack[stack.length - 1];

  closeElement(element);

  首先将当前节点出栈:stack.length -= 1 什么意思呢?

  看一个代码就懂了。

  var arr = [1,2,3,4];
  arr.length-=1;
  >arr [1,2,3]

  接着读取出栈后 stack 栈中的最后一个元素作为 currentParent 变量的值。 那closeElement 函数是做什么用的呢?

  closeElement 源码


  function closeElement(element) {
  // check pre state
  if (element.pre) {
  inVPre = false;
  }
  if (platformIsPreTag(element.tag)) {
  inPre = false;
  }
  // apply post-transforms
  for (var i = 0; i < postTransforms.length; i++) {
  postTransforms[i](element, options);
  }
  }

  closeElement 的作用有两个:第一个是对数据状态的还原,第二个是调用后置处理转换钩子函数。

  接下来看下end函数中剩余代码:

  // remove trailing whitespace
  var element = stack[stack.length - 1];
  var lastNode = element.children[element.children.length - 1];
  if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  element.children.pop();
  }

  这个代码的作用是去除当前元素最后一个空白子节点,我们在讲解 chars 钩子函数时了解到:preserveWhitespace 只会保留那些不在开始标签之后的空格,所以当空白作为标签的最后一个子节点存在时,也会被保留,如下代码所示:

  <div><span>test</span> <!-- 空白占位 --> </div>

  如上代码中 <span> 标签的结束标签与 <div> 标签的结束标签之间存在一段空白,这段空白将会被保留。要知道这段空白在被保留下来后,就会对于布局产生影响,尤其是对行内元素的影响。

  为了消除这些影响带来的问题,好的做法是将它们去掉,而如代码就是用来完成这个工作的。

  comment 注释节点描述对象

  解析器是否会解析并保留注释节点,是由 shouldKeepComment 编译器选项决定的,开发者可以在创建Vue 实例的时候通过设置 comments 选项的值来控制编译器的shouldKeepComment 选项。默认情况下 comments 选项的值为 false ,即不保留注释,假如将其设置为 true ,则当解析器遇到注释节点时会保留该注释节点,此时 parseHTML 函数的 comment 钩子函数会被调用,如下:

  comment: function comment(text) {
  currentParent.children.push({
  type: 3,
  text: text,
  isComment: true
  });
  }

  注意事项:要对普通文本节点作区分的话,当普通文本节点与注释节点的元素描述对象的类型是一样的都是 3 ,且在不同的是注释节点的元素描述对象拥有 isComment 属性,并且该属性的值为 true。


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/127972.html

相关文章

  • vue parseHTML函数源码解析AST基本形成

      vue parseHTML函数解析器遇到结束标签,在之前文章中已讲述完毕。  例如有html(template)字符串:  <divid="app">   <p>{{message}}</p>   </div>  产出如下:  {   attrs:["id="app"","id...

    3403771864 评论0 收藏0
  • Vue.js 模板解析器原理 - 来自《深入浅出Vue.js》第九章

    摘要:模板解析器原理本文来自深入浅出模板编译原理篇的第九章,主要讲述了如何将模板解析成,这一章的内容是全书最复杂且烧脑的章节。循环模板的伪代码如下截取模板字符串并触发钩子函数为了方便理解,我们手动模拟解析器的解析过程。 Vue.js 模板解析器原理 本文来自《深入浅出Vue.js》模板编译原理篇的第九章,主要讲述了如何将模板解析成AST,这一章的内容是全书最复杂且烧脑的章节。本文未经排版,真...

    pinecone 评论0 收藏0
  • vue-loader 源码解析系列之 selector

    摘要:当前正在处理的节点,以及该节点的和等信息。源码解析之一整体分析源码解析之三写作中源码解析之四写作中作者博客作者作者微博 笔者系 vue-loader 贡献者之一(#16) 前言 vue-loader 源码解析系列之一,阅读该文章之前,请大家首先参考大纲 vue-loader 源码解析系列之 整体分析 selector 做了什么 const path = require(path) co...

    miqt 评论0 收藏0
  • vue parseHTML函数解析器遇到结束标签

      在之前文章中我们讲述了parseHTML 函数源码解析拿到返回值后的处理,这篇文章就为我们讲述了当 textEnd === 0 解析器遇到结束标签,parse 结束标签的代码如下:  //Endtag:   varendTagMatch=html.match(endTag);   if(endTagMatch){   varcurIndex=index;   advance(endTagMat...

    3403771864 评论0 收藏0
  • Vue原理】Compile - 源码版 之 Parse 主要流程

    写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】Compile - 源码版 之 Parse 主要流程 本文难度较繁琐,需要耐心观看,如果你对 compile 源码暂时...

    Forest10 评论0 收藏0

发表评论

0条评论

3403771864

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<