Vue源码解析系列——模板编译篇:parseHTML的各种hook

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

parseHTML hooks

上一篇我们分析了parse方法中的parseHTML,在parseHTML中会调用很多的hooks,这些hooks定义在:

parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    start(tag, attrs, unary, start, end) { ... },
    end(tag, start, end) { ... },
    chars(text: string, start: number, end: number) { ... },
    comment(text: string, start, end) { ... },
    }
)
return root;

总共有四个hook:startendcharscomment。从字面意思上可以看出分别会在标签开始、标签结束、文本节点、注释节点中调用。
我们一个一个来分析,先看start

start

    start(tag, attrs, unary, start, end) {
      // check namespace.
      // inherit parent ns if there is one
      const ns =
        (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

      // handle IE svg bug
      /* istanbul ignore if */
      if (isIE && ns === "svg") {
        attrs = guardIESVGBug(attrs);
      }

      let element: ASTElement = createASTElement(tag, attrs, currentParent);
      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 }
          );
      }

      // apply pre-transforms
      for (let 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);
      }

      if (!root) {
        root = element;
        if (process.env.NODE_ENV !== "production") {
          checkRootConstraints(root);
        }
      }

      if (!unary) {
        currentParent = element;
        stack.push(element);
      } else {
        closeElement(element);
      }
    },

代码非常的多,我们一点一点来看,同时也会跳过一些分主线的逻辑。

 let element: ASTElement = createASTElement(tag, attrs, currentParent);
 if (ns) {
   element.ns = ns;
 }

创建了一个AST元素,同时也有命名空间的判断,继续。

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,
         }
       );
     }
   });
 }

从报错可以看出这里的逻辑是对HTML属性的规范判断,继续。

 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 }
     );
 }

这里是如果你是客户端渲染,防止你在模板里写<style>和<script>,继续。


if (!inVPre) {
	 processPre(element);
	 if (element.pre) {
	   inVPre = true;
	 }
}
if (platformIsPreTag(element.tag)) {
  inPre = true;
}

v-pre环境的判断,继续。

 if (inVPre) {
   processRawAttrs(element);
 } else if (!element.processed) {
   // structural directives
   processFor(element);
   processIf(element);
   processOnce(element);
 }

这边是有两个分支,一个是v-pre环境下的处理,v-pre环境下只处理html属性。其他环境会处理v-forv-ifv-once,这边有兴趣的童鞋可以进去看看具体是怎么处理的。

if (!root) {
  root = element;
  if (process.env.NODE_ENV !== "production") {
    checkRootConstraints(root);
  }
}

这里是一个check,判断你在书写template的时候是不是只有一个根元素,继续。

if (!unary) {
  currentParent = element;
  stack.push(element);
} else {
  closeElement(element);
}

一个判断,如果不是一元标签(自闭合标签),就将当前的元素赋值给currentParent,并将当前的元素入栈stack,这边的stack的用途和patseHTML中的stack用途差不多。

接下来我们来看endhook。

end hook

 end(tag, start, end) {
   const element = stack[stack.length - 1];
   // pop stack
   stack.length -= 1;
   currentParent = stack[stack.length - 1];
   if (process.env.NODE_ENV !== "production" && options.outputSourceRange) {
     element.end = end;
   }
   closeElement(element);
 },

先是将stack出栈,然后还原currentParent,之后调用closeELement,这个方法在刚刚starthook最后一步如果是一元标签时,也会调用。这个方法其实我也理解的不是很透彻,貌似是用来捋清父子关系的,同时还会解析refslotcomponent、事件等。

chars hook

    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
      /* istanbul ignore if */
      if (
        isIE &&
        currentParent.tag === "textarea" &&
        currentParent.attrsMap.placeholder === text
      ) {
        return;
      }
      const children = currentParent.children;
      if (inPre || text.trim()) {
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text);
      } else if (!children.length) {
        // remove the whitespace-only node right after an opening tag
        text = "";
      } else if (whitespaceOption) {
        if (whitespaceOption === "condense") {
          // in condense mode, remove the whitespace node if it contains
          // line break, otherwise condense to a single space
          text = lineBreakRE.test(text) ? "" : " ";
        } else {
          text = " ";
        }
      } else {
        text = preserveWhitespace ? " " : "";
      }
      if (text) {
        if (!inPre && whitespaceOption === "condense") {
          // condense consecutive whitespaces into single space
          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);
        }
      }
    },

这个hook是用来解析文本节点的。
根节点的判断。
IE的兼容。
使用parseText方法解析插值语法:{{xx}},还顺带解析了filter

comment hook

 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);
   }
 },

为AST创建注释节点,还有一些父子关系,感觉没啥好说的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
好的,让我来回答你的问题。 Vue 组件 object_vue 源码系列一是关于 Object.defineProperty 的。Object.defineProperty 是 JavaScript 中的一个方法,可以用来定义对象的属性。这个方法可以让我们定义一个新的属性或者修改一个已经存在的属性。这个方法的语法如下: ```javascript Object.defineProperty(obj, prop, descriptor) ``` 其中,obj 是要定义属性的对象,prop 是要定义或修改的属性名,descriptor 是属性的描述符,它是一个对象,可以包含以下属性: - value:属性的值,默认为 undefined。 - writable:属性是否可写,默认为 false。 - enumerable:属性是否可枚举,默认为 false。 - configurable:属性是否可配置,默认为 false。 使用 Object.defineProperty 方法,可以实现一些高级的对象操作,例如: 1. 将一个属性设置为只读,即无法修改。 2. 将一个属性设置为不可枚举,即无法通过 for...in 循环遍历到该属性。 3. 将一个属性设置为不可配置,即无法删除该属性或者修改该属性的描述符。 在 Vue 中,Object.defineProperty 方法被广泛地应用于组件的实现中,例如: 1. 监听数据变化,通过设置 getter 和 setter 方法,实现数据的响应式更新。 2. 实现 computed 计算属性,通过设置 getter 方法,实现计算属性的缓存和响应式更新。 3. 实现 watch 监听器,通过设置 getter 方法,监听数据的变化并触发回调函数。 以上就是我对你提出的问题的回答。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值