vue parseHTML源码解析hars end comment钩子函数

目录
  • 引言
  • chars源码:
    • parseText
  • end 源码:

引言

接上文  parseHTML 函数源码解析(六) start钩子函数

接下来我们主要讲解当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,又会如何对文本节点做哪些特殊的处理。

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 变量指向的是当前节点的父节点:。

如果 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 是一个布尔值代表着是否保留空格,只有它为真的情况下才会保留空格。但即使 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 &gt; 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 &lt; 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 &amp;&amp; lastNode.type === 3 &amp;&amp; lastNode.text === ' ' &amp;&amp; !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;
&gt;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 &lt; 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 &amp;&amp; lastNode.type === 3 &amp;&amp; lastNode.text === ' ' &amp;&amp; !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,目的就是用来与普通文本节点作区分的。

以上就是vue parseHTML源码解析hars end comment钩子函数的详细内容,更多关于vue parseHTML钩子函数的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue3生命周期钩子函数详解

    本文实例为大家分享了Vue3生命周期钩子函数的具体代码,供大家参考,具体内容如下 一.Vue3生命周期钩子 setup() : 开始创建组件之前,在 beforeCreate 和 created 之前执行,创建的是 data 和 methodonBeforeMount() : 组件挂载到节点上之前执行的函数:onMounted() : 组件挂载完成后执行的函数:onBeforeUpdate(): 组件更新之前执行的函数:onUpdated(): 组件更新完成之后执行的函数:onBeforeUnm

  • vue 自定义指令directives及其常用钩子函数说明

    目录 自定义指令directives及常用钩子函数 说明 钩子函数 vue 全局定义 局部定义(vue-cli) 钩子函数里面的参数 vue 自定义指令 directives选项 directives选项中定义 指令 自定义指令directives及常用钩子函数 说明 除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令 使用的地方:有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令 钩子函数 inserted:被绑定元素插

  • vue-router钩子函数实现路由守卫

    概述 何为路由守卫?路由守卫有点类似于ajax的请求拦截器,就是请求发送之前先给你拦截住做一些事情之后再去发送请求,同样这里的路由守卫意思差不多:简单理解为就是你在进路由之前,首先把你拦住,对你进行检查:这是不是有点中学门口的保安?进来之前拦住,有学生证就进,没有学生证就不让进:当然,路由守卫不仅仅只是在你进入之前拦住你,还有其他的钩子函数进行其他操作: vue-router一共给我们提供了三大类钩子函数来实现路由守卫: 1.全局钩子函数(beforeEach.afterEach) 2.路由独享

  • vue button的@click方法无效钩子函数没有执行问题

    目录 Vue项目中使用button绑定click事件 事件无法触发methods中的方法解决办法 跨域问题 userData is not defined Vue的第四个bug 钩子函数(mounted/created) Vue的第五个bug vue @click失效问题 贴代码(直接截图) 解决方法 Vue项目中使用button绑定click事件 事件无法触发methods中的方法解决办法 事故还原 小胖做完公司的项目,老大看着小胖疲惫的脸庞,有点心疼小胖,就给小胖放了三天假,没有给小胖新的需

  • Vue生命周期中的八个钩子函数相机

    目录 1.beforeCreate和created函数 2.beforeMount和mounted函数 3.beforeUpdate和updated函数 4.beforeDestroy和destroyed函数 总结 1.beforeCreate和created函数 beforeCreate和created以初始化:数据监测.数据代理为分界线. 在执行beforeCreate()之前,将初始化生命周期.时间,但数据代理还没有开始. (1)beforeCreate():在初始化数据监测.数据代理之前

  • vue created钩子函数与mounted钩子函数的用法区别

    1:在使用vue框架的过程中,我们经常需要给一些数据做一些初始化处理,这时候我们常用的就是在created与mounted选项中作出处理. 首先来看下官方解释,官方解释说created是在实例创建完成后呗立即调用. 在这一步,实例已完成以下配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调.然而,挂载阶段还没开始,$el 属性目前不可见. 这话的意思我觉得重点在于说挂架阶段还没开始,什么叫还没开始挂载,也就是说,模板还没有被渲染成html,也就是这

  • vue parseHTML源码解析hars end comment钩子函数

    目录 引言 chars源码: parseText end 源码: 引言 接上文  parseHTML 函数源码解析(六) start钩子函数 接下来我们主要讲解当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,又会如何对文本节点做哪些特殊的处理. parseHTML(template, { chars: function(){ //... }, //... }) chars源码: chars: function chars(text) { if (!currentParent) { {

  • Vue 2源码解析ParseHTML函数HTML模板

    ParseHTML函数 - HTML 模板解析 之前在解析 parse 函数时,我们知道整个 解析 template 模板并生成 ast 对象 的过程都发生在这个函数的执行过程中. 但是 parse 函数内部本身只定义了一些标签.指令的处理方法和警告函数,并且在传递给 parseHTML 函数的参数中定义了四个处理方法. 最终是通过调用 parseHTML 来解析 template 模板 整个解析过程,其实就是 通过一系列正则表达式来匹配 template 模板字符串,并截取该部分匹配内容并重新

  • Vue 2源码解析Parse函数定义

    目录 Parse 函数 parseHTML Parse 函数 在 baseCompile() 执行过程中,首先就是通过 parse方法 解析 template模板字符串,生成对应的 AST 抽象语法树. 整个 parse函数 定义太长,这里省略几个内部方法 /** * Convert HTML string to AST. */ export function parse(template: string, options: CompilerOptions): ASTElement { warn

  • 详解vue mint-ui源码解析之loadmore组件

    本文介绍了vue mint-ui源码解析之loadmore组件,分享给大家,具体如下: 接入 官方接入文档mint-ui loadmore文档 接入使用Example html <div id="app"> <mt-loadmore :top-method="loadTop" :bottom-method="loadBottom" :bottom-all-loaded="allLoaded" :max-dis

  • Vue 2源码解析HTMLParserOptions.start函数方法

    目录 HTMLParserOptions.start() 处理后的 input ast element HTMLParserOptions.start() 用来解析标签的开始部分(匹配到标签开始部分时调用),主要区分标签类型.解析标签指令配置与动态绑定参数等等. let root let currentParent function start(tag, attrs, unary, start, end) { const ns = (currentParent && currentPare

  • Vue AST源码解析第一篇

    讲完了数据劫持原理和一堆初始化,现在是DOM相关的代码了. 上一节是从这个函数开始的: // Line-3924 Vue.prototype._init = function(options) { // 大量初始化 // ... // Go! if (vm.$options.el) { vm.$mount(vm.$options.el); } }; 弄完data属性的数据绑定后,开始处理el属性,也就是挂载的DOM节点,这里的vm.$options.el也就是传进去的'#app'字符串. 有一个

  • Vue编译器源码分析compileToFunctions作用详解

    目录 引言 Vue.prototype.$mount函数体 源码出处 options.delimiters & options.comments compileToFunctions函数逐行分析 createFunction 函数源码 引言 Vue编译器源码分析 接上篇文章我们来分析:compileToFunctions的作用. 经过前面的讲解,我们已经知道了 compileToFunctions 的真正来源你可能会问为什么要弄的这么复杂?为了搞清楚这个问题,我们还需要继续接触完整的代码. 下面

  • Vue源码解析之Template转化为AST的实现方法

    什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式. Virtual Dom Vue的一个厉害之处就是利用Virtual DOM模拟DOM对象树来优化DOM操作的一种技术或思路. Vue源码中虚拟DOM构建经历 template编译成AST语法树 -> 再转换为render函数 最终返回一个VNode(VNod

  • vue loadmore 组件滑动加载更多源码解析

    上一篇讲到在项目中使用上拉加载更多组件,但是由于实际项目开发中由于需求变更或者说在webview中上拉加载有些机型在上拉时候会把webview也一起上拉导致上拉加载不灵敏等问题,我们有时候也会换成滑动到底部自动加载的功能. 既然都是加载更多,很多代码思想势必相似,主要区别在于上拉和滑动到底部这个操作上,所以,我们需要注意: 上拉加载是point指针touch触摸事件,现在因为是滑动加载,需要添加scroll事件去监听然后执行相应回调 上拉加载主要计算触摸滚动距离,滑动加载主要计算containe

  • Vue如何实现组件的源码解析

    官网上关于组件继承分为两大类,全局组件和局部组件.无论哪种方式,最核心的是创建组件,然后根据场景不同注册组件. 有一点要牢记,"Vue.js 组件其实都是被扩展的 Vue 实例"! 1. 全局组件 // 方式一 var MyComponent = Vue.extend({ name: 'my-component', template: '<div>A custom component!</div>' }); Vue.component('my-component

随机推荐