前言

上篇文章分析了Vue中render函数的构建以及调用过程,在整体上梳理了构建出render以及调用的逻辑。知其然也知其所以然,本文就主要分析html -> vnode的过程。

具体分析

首先确定简单实例:

<div id="app">
	{{ text }}
</div>

承上启下,上一篇文章中知道构建出render函数的逻辑如下:
在这里插入图片描述

其中主要点就是compile和createFunction的处理了:

compile是将template转换为指定结构的对象
createFunction是根据compile创建的render函数主体结构生成函数,即可执行的render函数

compile具体处理

阅读源码逻辑可得,实际上compile的主要处理点是:

var compiled = baseCompile(template, finalOptions);

即调用baseCompile函数,而baseCompile则是核心,这边主要代码如下:

var ast = parse(template.trim(), options);
var code = generate(ast, options);
return {
	ast: ast,
	render: code.render
};

从上面代码可知baseCompile主要处理是:

  • 将template装换为ast树结构对象
  • 根据ast树结构构建render函数主体部分
parse解析template

parse部分非常复杂,这里也只会分析主要脉络,例如解析html、解析属性、解析文本主要点,例如指令、注释等暂时不具体分析。

现在具体看看parse主要的处理逻辑,如下图:
在这里插入图片描述

实际上parse中会调用parseHTML也具体解析,其中parseHTML在new Vue实例化时的主要处理点如下:

function parseHTML(html, options) {
	// 用于存储标签对象
	var stack = [];
	// tokens解析的位置,即
	var index = 0;
	while(html) {
		// 其他处理
		// 判断是否以<开头,即标签开始
		var textEnd = html.indexOf('<');
		if   (textEnd === 0) {
			// parseStartTag解析出开始标签
			var startTagMatch = parseStartTag();
			 if (startTagMatch) {
			 	// 处理开始标签
			     handleStartTag(startTagMatch);
			      if (shouldIgnoreFirstNewline(lastTag, html)) {
			          advance(1);
			       }
			      continue
			  }
		}
		if(textEnd >= 0{
			// 处理文本
			// 主要处理
			options.chars(text)
         }
	}
}

从parseHTML涉及处理实际上会调用parseStartTag和handleStartTag来解析出开始标签,具体的处理如下:

// 正则匹配开始标签
var start = html.match(startTagOpen);
if (start) {
	var match = {
		tagName: start[1],
		attrs: [],
		start: index
	};
	advance(start[0].length);
	// 收集该标签的属性
}

通过parseStartTag函数会得到:

  • 标签名,即tagName
  • 该标签的属性集合,即attrs

上面中还有一个重要的函数,即advance,该函数是处理index和html值,这两个值分别表示:

  • index:当前解析开始位置
  • html:解析源

实际上parseStartTag会将html中开始标签和属性字符串截取掉,index的位置也会改变,例如:

“<div id=“app”> {{ text }} </div>” 经过parseStartTag会将其截取为 {{ text }} </div>
即截取了开始标签部分

handleStartTag处理开始标签和属性

通过parseStartTag获取了开始标签和其所有属性之后,接下来就是handleStartTag函数的调用,通过其源码分析,主要的处理点如下:

attrs[i] = {
  name: args[1],
  value: decodeAttr(value, shouldDecodeNewlines)
};
stack.push({
 tag: tagName, 
 lowerCasedTag: tagName.toLowerCase(), 
 attrs: attrs
});
options.start(tagName, ...);

从上面的代码中可以看出,实际上是主要处理属性,并构建结构对象存入stack中。
构建了标签和属性结构对象,之后调用options中的start,实际上start主要的处理点可看成两点(这里只关心这两点):

createASTElement函数调用
将创建的astElement存入stack中

// createASTElement函数主要处理
 return {
    type: 1,
    tag: tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent: parent,
    children: []
  }

当createASTElement创建结果之后存入stack中,一次循环就结束了。

此时会进行下一次循环,而之前advance将html中存储的字符串截取了。
实例:

<div id=“app”> {{text}} </div> 则会变成{{text}} </div>,即此时html的值

那下一轮循环就是处理{{text}}文本相关的,实际上会调用options的chars函数,该函数的主要处理逻辑如下:

var children = currentParent.children;
var res = parseText(text);
children.push({
	type: 2,
	expression: res.expression,
	tokens: res.tokens,
	text: text
})
parseText处理文本

处理类似{{text}}文本节点,实际上就是同parseText来实现的,而该函数实际上处理文本主要如下:

var tokens = [];
var rawTokens = [];
tokens.push(("_s(" + exp + ")"));
rawTokens.push({ '@binding': exp });
return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }

实际上会将使用到响应式数据的文本变成:_s(text),并且设置了@binding属性表示引用了哪个响应式变量。
还有一点就是对于换行的处理也会保留,即文本区域原样,该有换行就有换行只是响应式变量的特殊处理而已。

例如:
<div>
{{text}}
</div>
实际上text前后都有换行,此时expression就是 “\n + _s(text) + \n”,tokens中也是

至于结束标签的处理,其中没有本次需要关注点,就略过。

parse总结

template完整值是:

<div id=“app”> {{ text }} </div>

经过parseHTML + parseText的解析,得到如下的主要ast结构:

type: 1,
tag: 'div',
attrs: [{ name: 'id', value: 'app'}],
attrList: [{name: 'id', value: 'app'}],
attrMap: {id: 'app'}
children: [
	expression: ""\n    "+_s(text)+"\n  ""
	text: "↵    {{ text }}↵  "
	tokens: (3) ["↵    ", {}, "↵  "]
	type: 2
]
generate是如何构建render的

通过parse获得了指定ast结构对象,接下来就通过generate来构建render主体部分,这部分也比较复杂,这里就概括性的梳理下:

实际上generate是构建成类似Vue官方提供的jsx渲染的结构

实例的ast结构实际上构建成了:

with(this) {
	return _c('div',
		{
			attrs:{"id":"app"}
		},
		[
			_v("\n    "+_s(text)+"\n  ")
		]
	)
}

上面我是使用函数形式表示的,实际上Vue源码此处是字符串,即:

“with(this){return _c(‘div’,{attrs:{“id”:“app”}},[_v(”\n “+_s(text)+”\n “)])}”

解释下_c和_v是什么,在上篇文章中,我提及到了_c:

_c就是$createElement,即h函数

-v、_s是指什么呢?

-v实际上是createTextVNode,即创建虚拟文本节点
-s实际上是toString,处理对象和其他形式文本输出

通过parse和generate得到了上面的函数体,那么接下来就调用createFunction函数了。

createFunction函数

该函数实际上就是创建函数,如何创建?调用Function构造函数:

new Function(code);

至此,我们得到了render函数:

var render = new Function("with(this) { return _c('div', attrs: {'id': 'app'}, [_v('\n  ' + _s(text) + '   \n')])}");

总结

通过上面主要分析了处理html、处理文本情况,对于指令相关等部分直接略过,满足了分析这边的目的:探寻下Vue是如何解析template的。

整个处理流程如下:
构建code:

$mount -> compileToFunctions -> compile -> baseCompile -> parse -> generate
其中parse中主要的处理:
parse -> parseHTML (循环处理startTag、text等)

构建render函数:

parse得到ast结构 -> generate -> render

而vnode的创建实际上是在调用阶段发生的,即执行render函数,函数体重_c、_v等函数触发的VNode创建。

这部分会在下篇文章中重点分析。

Logo

前往低代码交流专区

更多推荐