Skip to content

Commit 42f7c32

Browse files
committedMar 7, 2020
feat: 模板编译
1 parent f4006a1 commit 42f7c32

File tree

6 files changed

+291
-1
lines changed

6 files changed

+291
-1
lines changed
 

‎img/3.png

8.64 KB
Loading

‎note/模板编译.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# 模板编译
2+
3+
## 1. 如果有el,去调用$mount方法
4+
5+
在前面对数据进行`inintState`之后,如果用户配置了`el`属性,会通过调用`$mount`方法,将数据渲染到页面上,此时:
6+
7+
```javascript
8+
Vue.prototype._init = function(options) {
9+
// vue 中的初始化 this.$options 表示 Vue 中的参数
10+
let vm = this;
11+
vm.$options = options;
12+
13+
// MVVM 原理, 需要数据重新初始化
14+
initState(vm);
15+
16+
+ if (vm.$options.el) {
17+
+ vm.$mount();
18+
+ }
19+
}
20+
```
21+
22+
23+
24+
### $mount
25+
26+
此时的`$mount`需要做两件事:
27+
28+
1. 通过用户配置的`el`字段,获取DOM元素,并将该元素挂载到`vm.$el`字段上;
29+
2. 通过实例化一个渲染 `Watcher`,去进行页面渲染;
30+
31+
```javascript
32+
function query(el) {
33+
if (typeof el === 'string') {
34+
return document.querySelector(el);
35+
};
36+
return el;
37+
}
38+
39+
// 渲染页面 将组件进行挂载
40+
Vue.prototype.$mount = function () {
41+
let vm = this;
42+
let el = vm.$options.el; // 获取元素
43+
el = vm.$el = query(el); // 获取当前挂载的节点 vm.$el 就是我要挂在的一个元素
44+
45+
// 渲染通过 watcher来渲染
46+
let updateComponent = () => { // 更新、渲染的逻辑
47+
vm._update(); // 更新组件
48+
}
49+
new Watcher(vm, updateComponent); // 渲染Watcher, 默认第一次会调用updateComponent
50+
}
51+
```
52+
53+
这里会生成一个渲染`Watcher`的实例。下面先简单实现一下这个`Watcher`类,在`observe`目录下新建`watcher.js`
54+
55+
```javascript
56+
let id = 0; // Watcher 唯一标识
57+
58+
class Watcher { // 每次产生一个watch 都会有一个唯一的标识
59+
/**
60+
*
61+
* @param {*} vm 当前逐渐的实例 new Vue
62+
* @param {*} exprOrFn 用户可能传入的一个表达式 也可能传入一个函数
63+
* @param {*} cb 用户传入的回调函数 vm.$watch('msg', cb)
64+
* @param {*} opts 一些其他参数
65+
*/
66+
constructor(vm, exprOrFn, cb = () => {}, opts = {}) {
67+
this.vm = vm;
68+
this.exprOrFn = exprOrFn;
69+
if (typeof exprOrFn === 'function') {
70+
this.getter = exprOrFn;
71+
}
72+
this.cb = cb;
73+
this.opts = opts;
74+
this.id = id++;
75+
this.get();
76+
}
77+
get() {
78+
this.getter(); // 让传入的函数执行
79+
}
80+
}
81+
export default Watcher;
82+
83+
```
84+
85+
根据现在的`Watcher`实现,新生成这个渲染`Watcher`的实例,会默认去执行`UpdateComponent`方法,也就是去执行`vm._update`方法,下面我们去看一下`_update`方法的实现。
86+
87+
88+
89+
## 2. _update
90+
91+
`_update`方法主要实现页面更新,将编译后的DOM插入到对应节点中,这里我们暂时先不引入虚拟DOM的方式,我们首先用一种较简单的方式去实现文本渲染。
92+
93+
首先使用`createDocumentFragment`把所有节点都剪贴到内存中,然后编译内存中的文档碎片。
94+
95+
```javascript
96+
Vue.prototype._update = function() {
97+
let vm = this;
98+
let el = vm.$el;
99+
100+
/** TODO 虚拟DOM重写 */
101+
// 匹配 {{}} 替换
102+
let node = document.createDocumentFragment();
103+
let firstChild;
104+
while(firstChild = el.firstChild) {
105+
node.appendChild(firstChild);
106+
}
107+
108+
compiler(node, vm); // 编译节点内容 匹配 {{}} 文本,替换为变量的值
109+
110+
el.appendChild(node);
111+
}
112+
```
113+
114+
下面我们去实现`compiler`方法:
115+
116+
## 3. Compiler 方法实现
117+
118+
```javascript
119+
const defaultRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
120+
export const util = {
121+
getValue(vm, expr) { // school.name
122+
let keys = expr.split('.');
123+
return keys.reduce((memo, current) => {
124+
memo = memo[current]; // 相当于 memo = vm.school.name
125+
return memo;
126+
}, vm);
127+
},
128+
/**
129+
* 编译文本 替换{{}}
130+
*/
131+
compilerText(node, vm) {
132+
node.textContent = node.textContent.replace(defaultRE, function(...args) {
133+
return util.getValue(vm, args[1]);
134+
});
135+
}
136+
}
137+
138+
/**
139+
* 文本编译
140+
*/
141+
export function compiler(node, vm) {
142+
let childNodes = node.childNodes;
143+
[...childNodes].forEach(child => { // 一种是元素一种是文本
144+
if (child.nodeType == 1) { // 1表示元素
145+
compiler(child, vm); // 如果子元素还是非文本, 递归编译当前元素的孩子节点
146+
} else if (child.nodeType == 3) { // 3表示文本
147+
util.compilerText(child, vm);
148+
}
149+
})
150+
}
151+
```
152+
153+
好了到现在我们的节点编译方法也实现了,我们去看下页面效果,将 `index.html`修改为`Vue`模板的形式:
154+
155+
```html
156+
<div id="app">
157+
{{msg}}
158+
<div>
159+
<p>学校名字 {{school.name}}</p>
160+
<p>学校年龄 {{school.age}}</p>
161+
</div>
162+
<div>{{arr}}</div>
163+
</div>
164+
```
165+
166+
可以看到页面展示:
167+
168+
![image-20200307144823192](../img/3.png)
169+
170+
说明我们的变量被正常渲染到页面上了,但是我们去修改变量的值,发现页面不能正常更新,别急,下一部分我们去搞定依赖收集去更新视图。

‎public/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
<title>Simple-Vue</title>
77
</head>
88
<body>
9-
<div id="app"></div>
9+
<div id="app">
10+
{{msg}}
11+
<div>
12+
<p>学校名字 {{school.name}}</p>
13+
<p>学校年龄 {{school.age}}</p>
14+
</div>
15+
<div>{{arr}}</div>
16+
</div>
1017
</body>
1118
</html>

‎source/vue/index.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {initState} from './observe';
2+
import Watcher from './observe/watcher';
3+
import {compiler} from './util';
24

35
function Vue(options) { // Vue 中原始用户传入的数据
46
this._init(options); // 初始化 Vue, 并且将用户选项传入
@@ -14,6 +16,54 @@ Vue.prototype._init = function(options) {
1416

1517
// MVVM 原理, 需要数据重新初始化
1618
initState(vm);
19+
20+
if (vm.$options.el) {
21+
vm.$mount();
22+
}
23+
}
24+
25+
/**
26+
* 获取DOM节点
27+
* @param {*} el
28+
*/
29+
function query(el) {
30+
if (typeof el === 'string') {
31+
return document.querySelector(el);
32+
};
33+
return el;
34+
}
35+
36+
/**
37+
* 用用户传入的数据,更新视图
38+
*/
39+
Vue.prototype._update = function() {
40+
let vm = this;
41+
let el = vm.$el;
42+
43+
/** TODO 虚拟DOM重写 */
44+
// 匹配 {{}} 替换
45+
let node = document.createDocumentFragment();
46+
let firstChild;
47+
while(firstChild = el.firstChild) {
48+
node.appendChild(firstChild);
49+
}
50+
51+
compiler(node, vm);
52+
53+
el.appendChild(node);
54+
}
55+
56+
// 渲染页面 将组件进行挂载
57+
Vue.prototype.$mount = function () {
58+
let vm = this;
59+
let el = vm.$options.el; // 获取元素
60+
el = vm.$el = query(el); // 获取当前挂载的节点 vm.$el 就是我要挂在的一个元素
61+
62+
// 渲染通过 watcher来渲染
63+
let updateComponent = () => { // 更新、渲染的逻辑
64+
vm._update(); // 更新组件
65+
}
66+
new Watcher(vm, updateComponent); // 渲染Watcher, 默认调用updateComponent
1767
}
1868

1969
export default Vue; // 首先默认导出一个Vue

‎source/vue/observe/watcher.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
let id = 0;
2+
3+
class Watcher { // 每次产生一个watch 都会有一个唯一的标识
4+
/**
5+
*
6+
* @param {*} vm 当前逐渐的实例 new Vue
7+
* @param {*} exprOrFn 用户可能传入的一个表达式 也可能传入一个函数
8+
* @param {*} cb 用户传入的回调函数 vm.$watch('msg', cb)
9+
* @param {*} opts 一些其他参数
10+
*/
11+
constructor(vm, exprOrFn, cb = () => {}, opts = {}) {
12+
this.vm = vm;
13+
this.exprOrFn = exprOrFn;
14+
if (typeof exprOrFn === 'function') {
15+
this.getter = exprOrFn;
16+
}
17+
this.cb = cb;
18+
this.opts = opts;
19+
this.id = id++;
20+
21+
this.get();
22+
}
23+
24+
get() {
25+
this.getter(); // 让传入的函数执行
26+
}
27+
}
28+
29+
export default Watcher;

‎source/vue/util.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// (?:.|\r?\n) 任意字符或者是回车
2+
// 非贪婪模式 `{{a}} {{b}}` 保证识别到是两组而不是一组
3+
const defaultRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
4+
export const util = {
5+
getValue(vm, expr) { // school.name
6+
let keys = expr.split('.');
7+
return keys.reduce((memo, current) => {
8+
memo = memo[current]; // 相当于 memo = vm.school.name
9+
return memo;
10+
}, vm);
11+
},
12+
/**
13+
* 编译文本 替换{{}}
14+
*/
15+
compilerText(node, vm) {
16+
node.textContent = node.textContent.replace(defaultRE, function(...args) {
17+
return util.getValue(vm, args[1]);
18+
});
19+
}
20+
}
21+
22+
/**
23+
* 文本编译
24+
*/
25+
export function compiler(node, vm) {
26+
let childNodes = node.childNodes;
27+
[...childNodes].forEach(child => { // 一种是元素一种是文本
28+
if (child.nodeType == 1) { // 1表示元素
29+
compiler(child, vm); // 如果子元素还是非文本, 递归编译当前元素的孩子节点
30+
} else if (child.nodeType == 3) { // 3表示文本
31+
util.compilerText(child, vm);
32+
}
33+
})
34+
}

0 commit comments

Comments
 (0)
Please sign in to comment.