vue源码手写实现(三):生成抽象语法树和render函数

  1. 1. 获取模板
  2. 2. 解析模板
  3. 3. 生成render函数
  4. 4. 执行render函数,生成真实DOM

获取模板

目前已经能将data中的数据处理为响应式数据,接下来应该解析html代码的内容。

vue2生命周期

在vue的生命周期图示中,可以看到在created之后会判断是否有el选项,判断之后再判断是否有template选项。所以我们应当在init处理完data数据之后,再判断el和template获取应当渲染的html代码。

在init.js下边为在vue原型上增加$mount函数,在init中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// init.js
Vue.prototype.$mount = function(el){
const vm = this;
el = document.querySelector(el);
let ops = vm.$options;

if(!ops.render){// 先查看有没有render函数
let template;// 没有render看下是否写了template,没写template采用外部的模板
if(!ops.template&&el){// 没有写模板,但是写了el
template = el.outerHTML;
}else{
if(el){
template = ops.template; // 如果有el,则采用模板的内容
}
}

// 写了template,就用写了的template
if(template){
// 对模版编译
const render = compileToFunction(template);// 将模板编译成render函数
ops.render = render; // jsx最终会被编译成 h('xxx')
}
}
}

解析模板

可以看到上边代码最终编译部分是调用compileToFunction(),这个方法即是编译html模板的核心方法。这里我们抽出作为单独逻辑,新建文件夹compiler,在其中新建index.js。

index.js首先导出核心方法compileToFunction,这个方法我们要实现两个内容。

  1. 将html模板生成为抽象语法树(AST:Abstract Syntax Tree);
  2. 根据抽象语法树生成render函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// compiler/index.js compileToFunction
export function compileToFunction(template){
//1.就是将template 转化成ast语法树

let ast = parseHTML(template); // 解析html模板


//2.生成render方法(render方法返回的结果就是 虚拟D0M)
// codegen(ast); // 生成render方法
let code = codegen(ast);


// 模板引擎的实现原理就是with + new Function
let render = new Function(`with(this){return ${code}}`);

return render;
}

parseHTML即是编译AST的核心方法。

这里我们编译的逻辑主要是正则匹配。html的形式基本是<tagName attrs=””>textContent</tagName>。所以我们可以先用正则去匹配尖括号,代表标签的开始,通过字符串处理截取到标签名;后续是标签的属性attrs,直到出现反向的尖括号标签的头部结束。

头部解析完成后,接着的内容可能会是标签的textContent,也可能是一个新标签,所以解析的操作需要递归处理,并且需要一个栈用于判断标签是否配对(即括号匹配算法)。

那么将html的嵌套结构递归编译完成后,接着处理textContent,正则会匹配到尖括号开始,到尖括号代表着标签的结束,中间的内容就是textContent。而后匹配到</xxx>,也表示标签结束,应当与栈顶元素判断,匹配则出栈。

AST总共有五个属性,children可以放子元素,以此递归。

1
2
3
4
5
6
7
8
// AST
{
attrs:[],
children:[],
parent:null,
tag:'tagName',
type:1
}

大体逻辑如此,这一段逻辑被抽离到parse.js,具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// compiler/parse.js

const ncname =`[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnamecapture =`((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnamecapture}`); // 标签开始
const endTag = new RegExp(`^<\\/${qnamecapture}[^>]*>`);// 标签结束
const attribute = /^\s*([^\s<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>]+)))?/;
const startTagClose=/^s*(\/?)>/; // <div><br/>
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

// vue3不是采用正则,而是每个字符进行匹配

/**
* 解析html
* 思路:解析一段就删除一段,直至没有能解析的数据
*/


export function parseHTML(html){
/**
* 解析到标签、文本、结尾时的处理函数
* 最终要生成一颗抽象语法树
* 利用栈来实现,类似于括号匹配
*/
const ELMENTTYPE = 1;
const TEXTTYPE = 3;
const stack = [];// 用于存放元素
let currentParent;// 栈顶指针
let root;

function createASTElement(tag, attrs){
return {
tag,
type: ELMENTTYPE,
children: [],
attrs,
parent: null
}
}

function start(tag,attrs){
// console.log('开始',tag,attrs);
let node = createASTElement(tag, attrs);
if(!root){
root = node;
}
if(currentParent){
node.parent = currentParent;
currentParent.children.push(node);
}
stack.push(node);
currentParent = node;
}
function chars(text){
// console.log('文本',text);
text = text.replace(/\s/g,'');
text && currentParent.children.push({
type: TEXTTYPE,
text,
parent: currentParent
});
}
function end(tag){
// console.log('结束',tag);
stack.pop();
currentParent = stack[stack.length-1];
}



/**
* 解析HTML主体代码
*/
function advance(n){// 向前移动n个字符(刪除)
html = html.substring(n);
}

function parseStartTag(){
const start = html.match(startTagOpen); // 匹配开始标签
if(start){
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length);


let end, attr;
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))){
advance(attr[0].length);
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5] || true
})
}

if(end){
advance(end[0].length);
}
return match;
}

return false;// 不是开始标签
}

while(html){// 一定是<开始
// 如果textEnd 为0 说明是一个开始标签或者结束标签
// 如果textEnd >@说明就是文本的结束位置
let textEnd = html.indexOf('<');
if(textEnd == 0){
const startTagMatch = parseStartTag();

if(startTagMatch){// 解析到的开始标签
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}

let endTagMatch = html.match(endTag);
if(endTagMatch){// 解析到的结束标签
end(endTagMatch[1]);
advance(endTagMatch[0].length);
continue;
}
}

if(textEnd > 0){// 解析文本内容
let text = html.substring(0, textEnd);
if(text){
chars(text);
advance(textEnd);
}
}
}

return root;
}

根据下边html结构生成的AST:

1
2
3
4
5
6
<div id="app" class="app" style="background-color: yellow;font-size: 28px;">
<div style="color: red;">
<p style="font-size: 26px;">hello:{{name}},你今年{{age}}岁,</p>
</div>
<span>{{age}}</span>
</div>

AST:由于有原型,只能截图展示

生成AST

生成render函数

parseHTML函数能够返回完整的AST,我们拿到AST后再以此生成render函数。这段逻辑通过codeGen函数实现。

render函数本质上仍旧是字符串,最终会作为一个函数运行,生成真实的dom。vue中的data数据能够在花括号中直接使用,所以当解析到花括号时需要去data中变量取值。而render函数就是内置三个编译函数用来处理。

_c(tagName,{attrs},children):创建节点函数,根据ast递归创建出真实的dom。

_v(str): 创建文本节点内容,即标签内部的内容。

_s(variable): 传入一个data中的变量,用于解析花括号表达式的值。

coedGen代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// compiler/indexjs
function genProps(attrs){
let str = '';
for(let i = 0; i < attrs.length; i++){
let attr = attrs[i];

if(attr.name === 'style'){
let obj = {};
attr.value.split(';').forEach(item => {
let [key, value] = item.split(':');
if(typeof value == 'string'){
value = value.trim()
}
obj[key] = value;
});
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0,-1)}}`
}

function genChildren(children){
return children.map(child => gen(child)).join(',');
}

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;// 用于匹配花括号

function gen(node){
if(node.type === 1){
return codegen(node);
}else{
let text = node.text;
if(!defaultTagRE.test(text)){
return `_v('${text}')`
}else{
let tokens = [];
let match
defaultTagRE.lastIndex = 0;
let lastIndex = 0;
while(match = defaultTagRE.exec(text)){
let index = match.index;
if(index > lastIndex){
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
tokens.push(`_s(${match[1].trim()})`);
lastIndex = index + match[0].length;
}
if(lastIndex < text.length){
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
return `_v(${tokens.join('+')})`
}
}
}

function codegen(ast){
let children = ast.children;
let code = (`_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : null},${children.length > 0 ? genChildren(children) : ''})`)

return code;
}

上边html生成的render函数如下:

1
_c('div',{id:"app",class:"app",style:{"background-color":"yellow","font-size":"28px"}},_c('div',{style:{"color":"red"}},_c('p',{style:{"font-size":"26px"}},_v("hello:"+_s(name)+",你今年"+_s(age)+"岁,"))),_c('span',null,_v(_s(age))))

执行render函数,生成真实DOM

在根目录下新建lifcycle.js,编译模板已经到生命周期created之后,相关初始化操作应当放在这里。

这里需要实现初始化之后调用一次渲染,并且要实现_c,_v,_s函数。

注意initLifecycle时,会在vue原型挂上_update和_render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// lifecycle.js
import { createElement, createTextNode } from "./vdom/index";

/**
* 创建真实节点
*/
function createElm(vnode){
let {tag,props,children,text} = vnode;
if(typeof tag === 'string'){
vnode.el = document.createElement(tag);
patchProps(vnode.el,props)
children.forEach(child=>{
vnode.el.appendChild(createElm(child));
})
}else{// 文本节点
vnode.el = document.createTextNode(text);
}
return vnode.el
}

function patchProps(el,props){
for(let key in props){
if(key === 'style'){
for(let styleName in props.style){
el.style[styleName] = props.style[styleName];
}
}else{
el.setAttribute(key,props[key]);
}
}
}

function patch(oldVNode,vnode){
const isRealNode = oldVNode.nodeType;
if(isRealNode){
const elm = oldVNode;// 获取到真实元素
const parentElm = elm.parentNode;// 获取父元素

let newElm = createElm(vnode);
parentElm.insertBefore(newElm,elm.nextSibling);
parentElm.removeChild(elm);

return newElm;
}else{
// diff算法
}
}

export function initLifecycle(Vue) {
Vue.prototype._update = function(vnode) {// 将虚拟dom转化成真实dom
const vm = this;
const el = vm.$el;
//既有初始化功能,又有更新功能
vm.$el = patch(el,vnode);
}

Vue.prototype._render = function() {
const vm = this;
return vm.$options.render.call(vm);// render内部的this设置为vm
}
// render函数内部的函数
Vue.prototype._c = function(){
return createElement(this,...arguments)
}
Vue.prototype._v = function(){
return createTextNode(this,...arguments)
}
Vue.prototype._s = function(value){
if(typeof value == 'object') return value
return JSON.stringify(value)
}

}

export function mountComponent(vm,el) {
vm.$el = el
vm._update(vm._render());// 执行即可重新渲染
}