资讯专栏INFORMATION COLUMN

vue parseHTML函数源码解析start钩子函数

3403771864 / 223人阅读

  关于parseHTML 函数源码解析 AST 相关知识已做过介绍,下面可以看看Vue start钩子函数源码。

  start: function start(tag, attrs, unary) {
  // check namespace.
  // inherit parent ns if there is one
  var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);
  // handle IE svg bug
  /* istanbul ignore if */
  if (isIE && ns === 'svg') {
  attrs = guardIESVGBug(attrs);
  }
  var element = createASTElement(tag, attrs, currentParent);
  if (ns) {
  element.ns = ns;
  }
  if (isForbiddenTag(element) && !isServerRendering()) {
  element.forbidden = true;
  warn$2(
  '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.'
  );
  }
  // apply pre-transforms
  for (var i = 0; i < preTransforms.length; i++) {
  element = preTransforms[i](element, options) || element;
  }
  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);
  // element-scope stuff
  processElement(element, options);
  }
  function checkRootConstraints(el) {
  {
  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) {
  // allow root elements with v-if, v-else-if and v-else
  if (root.if && (element.elseif || element.else)) {
  checkRootConstraints(element);
  addIfCondition(root, {
  exp: element.elseif,
  block: element
  });
  } else {
  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."
  );
  }
  }
  if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
  processIfConditions(element, currentParent);
  } else if (element.slotScope) { // scoped slot
  currentParent.plain = false;
  var name = element.slotTarget || '"default"';
  (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
  } else {
  currentParent.children.push(element);
  element.parent = currentParent;
  }
  }
  if (!unary) {
  currentParent = element;
  stack.push(element);
  } else {
  closeElement(element);
  }
  }

    在上面的代码中start 钩子函数能接受的参数有三个,分别是 tag 标签名称,attrs表示该标签的属性数组,和unary是代表着是否是一元标签的标识 。

  不懂的不要担心,现在我们一起来解析函数体中的代码。

  var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

  上面的开头定义了标签的命名空间的 ns 变量,主要是为获取当前元素的命名空间,那如何实现?我们要首先检测currentParent 变量是否存在,众所周知当前元素的父级元素描述对象就是 currentParent 变量,比如,当前父级元素存在命名空间,那当前元素的命名空间就是父级的命名空间名称。

  假如父级元素不存在或父级元素没有命名空间,调用platformGetTagNamespace函数,platformGetTagNamespace 函数显示获取svg 和 math 这两个标签的命名空间,它们两个的命名空间是继承所有子标签。

  platformGetTagNamespace 源码

  function getTagNamespace(tag) {
  if (isSVG(tag)) {
  return "svg"
  }
  if (tag === "math") {
  return "math"
  }
  }
  接下来源码:
  1
  2
  3
  if (isIE && ns === "svg") {
  attrs = guardIESVGBug(attrs);
  }

  这里通过isIE来判断宿主环境是不是IE浏览器,并且前元素的命名空间为svg, 如果是通过guardIESVGBug处理当前元素的属性数组attrs,并使用处理后的结果重新赋值给attrs变量,该问题是svg标签中渲染多余的属性,如下svg标签:

  <svg xmlns:feature="http://www.openplans.org/topp"></svg>

  被渲染为:

  <svg xmlns:NS1="" NS1:xmlns:feature="http://www.openplans.org/topp"></svg>

  标签中多了 'xmlns:NS1="" NS1:' 这段字符串,解决办法也很简单,将整个多余的字符串去掉即可。而 guardIESVGBug 函数就是用来修改NS1:xmlns:feature属性并移除xmlns:NS1="" 属性的。

  接下来源码:

  var element = createASTElement(tag, attrs, currentParent);
  if (ns) {
  element.ns = ns;
  }

  在上章节聊过,createASTElement 它将生成当前标签的元素描述对象并且赋值给 element 变量。紧接着检查当前元素是否存在命名空间 ns ,如果存在则在元素对象上添加 ns 属性,其值为命名空间的值。

  接下来源码:

  if (isForbiddenTag(element) && !isServerRendering()) {
  element.forbidden = true;
  warn$2(
  '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.'
  );
  }

  这里的作用就是判断在非服务端渲染情况下,当前解析的开始标签是否是禁止在模板中使用的标签。哪些是禁止的呢?

  isForbiddenTag 函数

  function isForbiddenTag(el) {
  return (
  el.tag === 'style' ||
  (el.tag === 'script' &amp;&amp; (
  !el.attrsMap.type ||
  el.attrsMap.type === 'text/javascript'
  ))
  )
  }

  可以看到,style,script 都是在禁止名单中,但通过isForbiddenTag 也发现一个彩蛋。

  <script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
  </script>

  当定义模板的方式如上,在 <script> 元素上添加 type="text/x-template" 属性。 此时的script不会被禁止。

  最后还会在当前元素的描述对象上添加 element.forbidden 属性,并将其值设置为true。

  接下来源码:

  for (var i = 0; i < preTransforms.length; i++) {
  element = preTransforms[i](element, options) || element;
  }

  如上代码中使用 for 循环遍历了preTransforms 数组,preTransforms 是通过pluckModuleFunction 函数从options.modules 选项中筛选出名字为preTransformNode 函数所组成的数组。实际上 preTransforms 数组中只有一个 preTransformNode 函数该函数只用来处理 input 标签我们在后面章节会来讲它。

  接下来源码:

  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);
  // element-scope stuff
  processElement(element, options);
  }

  可以看到这里会有大量的process*的函数,这些函数是做什么用的呢?实际上process* 系列函数的作用就是对元素描述对象做进一步处理,比如其中一个函数叫做 processPre,这个函数的作用就是用来检测元素是否拥有v-pre 属性,如果有v-pre 属性则会在 element 描述对象上添加一个 pre 属性,如下:

  {
  type: 1,
  tag,
  attrsList: attrs,
  attrsMap: makeAttrsMap(attrs),
  parent,
  children: [],
  pre: true
  }

  总结:所有process* 系列函数的作用都是为了让一个元素的描述对象更加充实,使这个对象能更加详细地描述一个元素, 不过我们本节主要总结解析一个开始标签需要做的事情,所以稍后去看这些代码的实现。

  接下来源码:

  function checkRootConstraints(el) {
  {
  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.'
  );
  }
  }
  }

  我们知道在编写 Vue 模板的时候会受到两种约束,首先模板必须有且仅有一个被渲染的根元素,第二不能使用 slot 标签和 template 标签作为模板的根元素。

  checkRootConstraints 函数内部首先通过判断 el.tag === 'slot' || el.tag === 'template' 来判断根元素是否是slot 标签或 template 标签,如果是则打印警告信息。接

  着又判断当前元素是否使用了 v-for 指令,因为v-for 指令会渲染多个节点所以根元素是不允许使用 v-for 指令的。

  接下来源码:

  if (!root) {
  root = element;
  checkRootConstraints(root);
  } else if (!stack.length) {
  // allow root elements with v-if, v-else-if and v-else
  if (root.if &amp;&amp; (element.elseif || element.else)) {
  checkRootConstraints(element);
  addIfCondition(root, {
  exp: element.elseif,
  block: element
  });
  } else {
  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."
  );
  }
  }

  这个 if 语句先检测 root 是否存在!我们知道 root 变量在一开始是不存在的,如果 root 不存在那说明当前元素应该就是根元素,所以在 if 语句块内直接把当前元素的描述对象 element 赋值给 root 变量,同时会调用 checkRootConstraints函数检查根元素是否符合要求。

  再来看 else if 语句的条件,当 stack 为空的情况下会执行 else if 语句块内的代码, 那stack 什么情况下才为空呢?前面已经多次提到每当遇到一个非一元标签时就会将该标签的描述对象放进数组,并且每当遇到一个结束标签时都会将该标签的描述对象从 stack 数组中拿掉,那也就是说在只有一个根元素的情况下,正常解析完成一段 html 代码后 stack 数组应该为空,或者换个说法,即当 stack 数组被清空后则说明整个模板字符串已经解析完毕了,但此时 start 钩子函数仍然被调用了,这说明模板中存在多个根元素,这时 else if 语句块内的代码将被执行:

  接下来源码:

  if (root.if &amp;&amp; (element.elseif || element.else)) {
  checkRootConstraints(element);
  addIfCondition(root, {
  exp: element.elseif,
  block: element
  });
  } else {
  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."
  );
  }

  想要能看懂这个代码,你需要懂一些前置知识。

  [ Vue条件渲染 ] (https://cn.vuejs.org/v2/guide/conditional.html)

  我们知道在编写 Vue 模板时的约束是必须有且仅有一个被渲染的根元素,但你可以定义多个根元素,只要能够保证最终只渲染其中一个元素即可,能够达到这个目的的方式只有一种,那就是在多个根元素之间使用 v-if 或 v-else-if 或 v-else 。

  示例代码:

  <div v-if="type === 'A'">
  A
  </div>
  <div v-else-if="type === 'B'">
  B
  </div>
  <div v-else-if="type === 'C'">
  C
  </div>
  <div v-else>
  Not A/B/C
  </div>

  在回归到代码部分。

  if (root.if && (element.elseif || element.else))

  root 对象中的 .if 属性、.elseif 属性以及 .else 属性都是哪里来的,它们是在通过 processIf 函数处理元素描述对象时,如果发现元素的属性中有 v-if 或 v-else-if 或 v-else ,则会在元素描述对象上添加相应的属性作为标识。

  上面代码如果第一个根元素上有 .if 的属性,而非第一个根元素 element 有 .elseif 属性或者 .else 属性,这说明根元素都是由 v-if、v-else-if、v-else 指令控制的,同时也保证了被渲染的根元素只有一个。

  接下来继续看:

  if (root.if && (element.elseif || element.else)) {
  checkRootConstraints(element);
  addIfCondition(root, {
  exp: element.elseif,
  block: element
  });
  } else {
  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."
  );
  }

  checkRootConstraints 函数检查当前元素是否符合作为根元素的要求,这都能理解。

  addIfCondition是什么

  看下它的源代码。

  function addIfCondition(el, condition) {
  if (!el.ifConditions) {
  el.ifConditions = [];
  }
  el.ifConditions.push(condition);
  }

  代码很简单,调用addIfCondition 传递的参数 root 对象,在函数体中扩展一个属性addIfCondition, root.addIfCondition 属性值是一个对象。 此对象中有两个属性exp、block。实际上该函数是一个通用的函数,不仅仅用在根元素中,它用在任何由 v-if、v-else-if 以及 v-else 组成的条件渲染的模板中。

  通过如上分析我们可以发现,具有 v-else-if 或 v-else 属性的元素的描述对象会被添加到具有 v-if 属性的元素描述对象的 .ifConnditions 数组中。

  举个例子,如下模板:

  <div v-if="A"></div>
  <div v-else-if="B"></div>
  <div v-else-if="C"></div>
  <div v-else></div>

  解析后生成的 AST 如下(简化版):

  {
  type: 1,
  tag: 'div',
  ifConditions: [
  {
  exp: 'A',
  block: { type: 1, tag: 'div' /* 省略其他属性 */ }
  },
  {
  exp: 'B',
  block: { type: 1, tag: 'div' /* 省略其他属性 */ }
  },
  {
  exp: 'C',
  block: { type: 1, tag: 'div' /* 省略其他属性 */ }
  },
  {
  exp: 'undefined',
  block: { type: 1, tag: 'div' /* 省略其他属性 */ }
  }
  ]
  // 省略其他属性...
  }

  假如当前元素不满足条件:root.if && (element.elseif || element.else) ,那么在非生产环境下会打印了警告信息。

  接下来源码:

  if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
  processIfConditions(element, currentParent);
  } else if (element.slotScope) { // scoped slot
  currentParent.plain = false;
  var name = element.slotTarget || '"default"';
  (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
  } else {
  currentParent.children.push(element);
  element.parent = currentParent;
  }
  }
  if (!unary) {
  currentParent = element;
  stack.push(element);
  } else {
  closeElement(element);
  }

  我们先从下往上讲, 为什么呢?原因是在解析根元素的时候currentParent并没有赋值。

  !unary 表示解析的是非一元标签,此时把该元素的描述对象添加到stack 栈中,并且将 currentParent 变量的值更新为当前元素的描述对象。如果一个元素是一元标签,那么应该调用 closeElement 函数闭合该元素。

  老生常谈的总结:每当遇到一个非一元标签都会将该元素的描述对象添加到stack数组,并且currentParent 始终存储的是 stack 栈顶的元素,即当前解析元素的父级。

  if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
  processIfConditions(element, currentParent);
  } else if (element.slotScope) { // scoped slot
  currentParent.plain = false;
  var name = element.slotTarget || '"default"';
  (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
  } else {
  currentParent.children.push(element);
  element.parent = currentParent;
  }
  }

  这里的条件要成立,则说明当前元素存在父级( currentParent ),并且当前元素不是被禁止的元素。

  常见的情况如下:

  if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
  //...
  } else if (element.slotScope) { // scoped slot
  //...
  } else {
  currentParent.children.push(element);
  element.parent = currentParent;
  }
  }

  在 else 语句块内,会把当前元素描述对象添加到父级元素描述对象 ( currentParent ) 的children 数组中,同时将当前元素对象的 parent 属性指向父级元素对象,这样就建立了元素描述对象间的父子级关系。

  如果一个标签使用 v-else-if 或 v-else 指令,那么该元素的描述对象实际上会被添加到对应的v-if 元素描述对象的 ifConditions 数组中,而非作为一个独立的子节点,这个工作就是由如下代码完成:

  if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
  processIfConditions(element, currentParent);
  } else if (element.slotScope) { // scoped slot
  currentParent.plain = false;
  var name = element.slotTarget || '"default"';
  (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
  } else {
  //...
  }
  }

  如当前解析的元素使用了 v-else-if 或 v-else 指令,则会调用 processIfConditions 函数,同时将当前元素描述对象 element 和父级元素的描述对象 currentParent 作为参数传递:

  processIfConditions 源码

  function processIfConditions(el, parent) {
  var prev = findPrevElement(parent.children);
  if (prev && prev.if) {
  addIfCondition(prev, {
  exp: el.elseif,
  block: el
  });
  } else {
  warn$2(
  "v-" + (el.elseif ? ('else-if="' + el.elseif + '"') : 'else') + " " +
  "used on element <" + (el.tag) + "> without corresponding v-if."
  );
  }
  }

  findPrevElement 函数是去查找到当前元素的前一个元素描述对象,并将其赋值给 prev 常量,addIfCondition 不用多说如果prev 、prev.if 存在,调用 addIfCondition 函数在当前元素描述对象添加 ifConditions 属性,传入的对象存储相关信息。

  如果当前元素没有使用 v-else-if 或 v-else 指令,下面就是判断 slot-scope 特性,如下:

  if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
  //...
  } else if (element.slotScope) { // scoped slot
  currentParent.plain = false;
  var name = element.slotTarget || '"default"';
  (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
  } else {
  //...
  }
  }

   slot-scope 特性就是该元素的描述对象会被添加到父级元素的scopedSlots 对象下,这也说明使用 slot-scope 特性的元素与使用了v-else-if 或 v-else 指令的元素一样,无法作为父级元素的子节点, slot-scope 的特性就是父级元素描述对象的 scopedSlots 对象下。

      后续更多相关精彩内容,请大家多多关注。

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

转载请注明本文地址:https://www.ucloud.cn/yun/127761.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源码解析harsendcomment钩子函数

      我们现在要讲述的是当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,那又该作何处理。  parseHTML(template,{   chars:function(){   //...   },   //...   })  chars源码:   chars:functionchars(text){   if(!currentParent){   {   if(text===templ...

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

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

    Forest10 评论0 收藏0
  • Vue编程三部曲之将template编译成AST示例详解

      知道吗?Vue.js 有 2 个版本,一个是Runtime + Compiler版本,另一个是Runtime only版本。Runtime + Compiler版本是包含编译代码的,简单来说就是Runtime only版本不包含编译代码的,在运行时候,需要借助 webpack 的 vue-loader 事先把模板编译成 render 函数。  假如在你需要在客户端编译模板 (比如传入一个字符串...

    3403771864 评论0 收藏0

发表评论

0条评论

3403771864

|高级讲师

TA的文章

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