现代前端技术解析读后感
# 简介
这是一本以现代前端技术思想与理论为主要内容的书。前端技术发展迅速,涉及的技术点很多,我们往往需要阅读很多书籍才能理解前端技术的知识体系。《现代前端技术解析》
在前端知识体系上做了很好的总结和梳理,涵盖了现代前端技术绝大部分的知识内容,起到一个启蒙作用,能帮助读者快速把握前端技术的整个脉络,培养更完善的体系化思维,掌握更多灵活的前端代码架构方法,使读者获得成为高级前端工程师或架构师所必须具备的思维和能力。
# 第一章
前后端开发模式的演变
静态黄页 ==》
服务器组装动态网页数据 ==》
后端为主的MVC ==》
前后端分离 ==》
纯前端MV*
为主、中间层直出 ==》
前端Virtual DOM、MNV*
、前后端同构
// 解释
// MV* 分为:MVC、MVP、MVVM
// 1. MVC(Model-View-Controller)
// 2. MVP (Model-View-Presenter)
// 3. MVVM
2
3
4
5
# 1.1、MVC
总结:
- 通过controller 根据前端条件调用不同的Model给View渲染不同的数据内容
- Controller 只进行修改操作指令的分发,数据 的渲染一般是在View层来完成
MVC 可以认为是一种开发设计模式,其基本思路是将DOM交互的内容分为数据模型、视图和事件控制函数三个部分,并对它们进行统一管理。Model用来存放请求的数据结果和数据对象,View用于页面DOM的更新与修改,Controller则用于根据前端路由条件(例如不同的HASH路由)来调用不同的Model给View渲染不同的数据内容。常用页面路由的实现也很简单,代码如下,其主要思咱是让URL地址内容匹配对应的字符串然后进行相应的操作。
const router = {
get (match, fn) {
let url = location.href,
routeReg = new RegExp(match, 'g');
if (routeReg.test(rul)) {
fn();
}
return this;
}
}
router.get('#index', function(){
_loadIndex(); // 注册hash含有#index 的路由执行对应的操作
}).get('#detail', function(){
_loadDetail(); // 注册hash含有#detail的路由执行对应的操作
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
另外我们也可以使用HTML5的pushState来实现路由。history.pushState(state,title,url)
方法可以改变当前页面的url而不发生跳转,并将不同的state数据和对应的url对应起来。如果页面显示的内容是根据不同的数据状态来自动完成的,这样根据state的内容来加载不同的组件就很有用了。
history.pushState({page:'A'}, 'page A', 'a.html');
console.log(history.state); // {page: 'A'}对象
history.pushState({page: 'B'}, 'page B', 'b.html');
console.log(history.state); // {page: 'B'}
history.pushState({page: 'C'}, 'page C', 'c.html')
console.log(history.state); // {page: 'C'}
2
3
4
5
6
7
8
这里访问不同URL地址a.html、b.html、c.html
,页面不会发生跳转刷新,而是改变了当前的history.state
内容,我们使用hisotry.state
数据的改变来动态改变页面DOM的内容这种方式来实现SPA就很方便了。
使用路由后,如果将SPA中的每个路由或用户操作加载的页面内容都看成是一个组件,那么之前的做法是每个组件独立完成各自的数据请求操作、渲染和数据绑定,一旦组件多了,每个组件自行处理就会造成逻辑混乱。到了MVC里面,所有的组件数据请求、渲染、页面逻辑操作都分别使用Model、View、Controller
来注册调用。通俗地讲,就像是组件交出了自己的控制权给统一的控制对象来调用一样。
// 放图
如上图所示,前端应用页面加载完成后,根据不同的用户操作,页面会执行不同的响应。例如用户操作或URL哈希改变时会去调用与之对应的ControllerA方法,ControllerA方法会请求获取对应的数据ModelA,ModelA很可能是前端AJAX请求返回的一个接口数据,然后将ModelA传递给视图模板ViewA并将最终内容渲染到浏览器中,这样就完成了用户的一次交互操作,用户下一次操作的过程也是一样的。同时如果用户的操作需要进行DOM结构修改,那么会传到到Controller中,通过Controller获取数据并控制View的更新。对其中的一个组件A的实现操作代码如下.
<div id ="A" onclick="Controller.A.event.change"></div>
let Controller = {}, Model = {}, View = {};
View['A'] = function(data){
let tpl = '<input id="input" type="text" value="{{text}}"><span id="showText">{{text}}</span>';
//调用模板渲染数据获取HTML片段
let html = render(tpl, data);
document.getElementById('A').innerHTML = html;
};
Model['A'] = {
text: 'ViewA 渲染完成'
}
Controller['A'] = function(){
View['A'](model['A']);
// 用户操作一般通过改变Hash完成,并触发Controller来改变Model和View
$('window').on('hashChange', function(){
model['A'].text = location.hash;
View['A'](model['A']);
})
// 点击事件可以直接触发Controller改变Model并重新渲染view
self.event['change'] = function(){
model['A'].text = '新的ViewA渲染完成';
View['A'](model['A']);
}
}
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
为了方便大家理解,这里采用了一个最直接的实现方式,而且没有对A、B、C
组件进行单独管理,而是将它的调用内容分为Controller、Model、View
并放在一起统一管理,相互之间的调用是直接进行的。成熟的MVC框架一般是通过事件监听或者观察者模式来实现的,这里只是为了让读者更好的理解MVC的原理。当然这样的组件如果多了也比较混乱。所以通过改进宋史以将页面分成不同的小模块来处理,同时考虑到代码复用性,因此可以使用继承的方式来定义这三个组件,继续以A组件为例,我们一般看到的主流MVC框架的组件定义代码如下。
<div id="A" onclick="A.event.change"></div>
//可能有一个公用的Component基类
let component = new Component();
let A = component.extend({
$el: document.getElementById('A'),
model: {
text: 'viewA 渲染完成'
},
View (data) {
let tpl = '<input id="input" type="text" value="{{text}}"><span id="showText">{{text}}</span>';
// 调用模板渲染数据获取HTML片段
let html = render(tpl, data);
this.$el.innerHTML = html;
},
controller () {
let self = this;
// 调用model数据传入view中渲染内容
self.view(self.model);
// 用户操作一般通过Hash来出发Controller改变Model和View
$('window').on('hashchange', function() {
self.model.text = localtion.hash;
self.view(self.model);
})
// 点击事件可以直接触发Model改变并重新渲染View
self.event['change'] = function() {
self.model.text = '新的ViewA渲染完成';
self.view(self.model);
}
}
})
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
尽管这里写法不太一样,但实现的功能和上面一段代码是相同的。当Model或View复杂后,我们可以考虑将Model、View、Controller
拆分成不同文件导入引用。这段代码就是MVC基础原型的设计和实现,需要注意的是,用户操作引起的DOM修改操作主要是通过Controller来直接控制的,但是Controller只进行修改操作指令的分发,数据的渲染一般是在View层来完成。
# 1.2 MVP
总结:
- MVP 中的Presenter与controller有点相似,但不同的是,用户在进行DOM修改操作时通过View上的行为触发修改通知给Presenter完后Model修改和其它View的更新,Presenter和View的操作绑定通常是双向的,View的改变一般会触发Presenter的动作,Presenter的动作也会改变View
- View 与 Model 只用于提供视图模板和数据而不做任何逻辑的处理,Presenter负责逻辑操作,职责清晰。但这样Presenter层的内容就可能变得很重了,另外用户在View上操作会反馈到Presenter中进行Model修改,并更新其它对应部分的View内容
MVP和MVC一样,M就是Model,V就是View,而p代表Presenter,它与Controller有点相似,但不同的是,用户在进行DOM修改操作时将通过View上的行为出发,然后将修改通知给Presenter来完成后面的Model修改和其它的View的更新,而MVC模式下,用户的操作是直接通过Controller来控制的,Presenter和View的操作绑定通常是双向的,View的改变一般会触发Presenter的动作,Presenter的动作也会改变View。
<div id="A" onclick="A.event.change"></div>
// 可能有一个公用的Component基类
let component = new Component();
let A = component.extend({
$el:document.getElementById('A'),
model:{
text: 'ViewA 渲染完成'
},
view: '<input id="input" type="text" value="{{text}}"><span id="showText">{{text}}<span>',
presenter () {
let self = this;
// 调用模板渲染数据获取HTML片段
let html = render(self.view,self.model);
// View上的改变将通知Presenter改变Model和其它的View
$('#input').on('change', function() {
self.model.text = this.value;
html = render('{{text}}',self.model);
$('#showText').html(html);
});
// Controller上的操作处理和MVC的方式类似
self.event['change'] = function(){
self.model.text = '新的ViewA渲染完成';
html = render('{{text}}', self.model);
$('#showText').html(html);
}
}
});
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
可以看出,这时View和Model主要用于提供视图模板和数据而不做任何逻辑处理,这是有好处的,因为我们现在只要关注Presenter上面的逻辑操作就可以了,它的职责很清晰,Presenter作为为间部分连接Model和View的通信交互完成所有的逻辑操作,但这样Presenter层的仙鹤就可能变得很重了。另外用户在View上的操作会反馈到Presenter中进行Model修改,并更新其它对应部分的View内容。
# 1.3 MVVM 模式
MVVM则可以认为是一个自动化的MVP框架,并且使用ViewModel代替了Presenter,即数据Model的调用和模板内容的渲染不需要我们主动操作,而是ViewModel自动来触发完成,任何用户的操作也都是通过ViewModel的改变来驱动的。
MVVM设计的一个很大的好处是将MVP中的Presenter的工作拆分成多个小的指令步骤,然后绑定到相对应的元素中,根据相对应的数据变化来驱动出发,自动管理交互操作,同时也免去了查看Presenter中事件列表的工作,而且一般ViewModel初始化时会自动进行数据绑定,并将页面中所有的同类操作复用,大大节省了我们自己进行内容渲染和事件绑定的代码。用MVVM模式实现的代码如下:
<div id="A" q-on="click: change">
<input type="text" q-value="text"><span q-html="text"></span>
</div>
2
3
let viewModel = new VM({
$el: document.getElementById('A'),
data: {
text: 'View 渲染完成'
},
method: {
change(){
this.text = '新的ViewA渲染完成';
}
}
})
2
3
4
5
6
7
8
9
10
11
整体上简洁了很多,模板数据的渲染和数据绑定可以通过q-html或q-click等特殊的属性来控制完成,这些特殊的元素标签属性就是我们所说的Directive,当然不同的MVVM框架作用的Directive前缀不一样,但作用是类似的。
<form action="#" id="form">
<label for="text" q-html="label"></label>
<input type="text" q-value="value" q-model="value" q-mydo="number | getValue">
<button q-on="click: submit"></button>
</form>
2
3
4
5
let viewModel = new VM({
$el: document.getElementById('form'),
data: {
label: '用户名',
value: '输入初始值',
number: 0
},
method: {
submit () {
// doSubmit
}
},
directive: {
mydo (value) {
console.log(value);
}
},
filter: {
getValue () {
return ++ value;
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在这段代码中,ViewModel初始化时自动进行了数据填充、数据双向绑定和事件绑定。我们再来看一下执行new VM()时进行ViewModel初始化所完成的事情。
首先javaScript会找到document.getElementById('form')
这个元素并开始扫描元素节点,对这个元素的属性节点attributes和所有子节点中的attributes进行便利,一旦便利到名称中含有q-开头的属性时,就认为是MVVM框架定义的Directive,此时会执行相对应的操作。例如便利到q-html="label"时,就将ViewModel初始化时默认数据对象data中的label值赋给这个元素的innerHTML;遍历到q-on="click:submit"
时,就在这个元素上绑定click事件,事件函数名为submit;也可以自定义q-mydo
的指令,便利到该节点属性时,调用Directive中的mydo方法,输入参数为data中getValue方法的返回值,这里getValue()将输入number值自动加1并返回,getValue函数则一般被称为过滤器。用户在View层操作时会自动改变改变ViewModel的数据,然后ViewModel会检测数据变化,重新遍历扫描节点属性,执行对应的Directive,渲染HTML视图或做事件绑定。
这里还要知道的是,q-开头的标签属性被称为指令,这是框架约定的,不同的框架约定的通常不一样,例如ng-、v- 、ms-
,相信大家也见过甚至用过。这里ViewModel创建并进行视图渲染和事件绑定的过程非常简单,按照这个思路去扩充,我们就可以自己实现一个简单的MVVM框架了。当然完整的框架东西远比这要多,如含有丰富的directive、filter、表达式、ViewModel
中完善的API,甚至包含一些浏览器兼容性处理等。
directive、filter
具体是什么呢?我们结合MVVM框架设计的相关内容来具体了解一下。
Directive。 翻译为指令,简单地说就是自定义的执行函数,例如:
q-html、q-calss、q-on、q-show、q-attr
等封装了DOM的一些基本可复用性的操作函数API。
Filter。 也叫过滤器,如
bool、upperCase、lowerCase
等,指用户希望对传入的初始数据进行处理,然后再将这个处理的结果交给Directive或下一个Filter.例如,ViewModel初始化时传入的是一个时间戳time转化为时间格式的Filter函数
表达式设计。如if...else等,类似前端普通的页面模板表达式,其作用也是控制页面内容按照具体条件来显示
ViewModel设计。是实现传入的Model数据在内在中存放的环节,通常ViewModel也会提供一些基本的操作API,方便开发者对数据进行读取或修改
数据变更检测,我们上面讲到了MVVM通常是通过数据改变来驱动的,这样就需要进行数据的双向绑定。一般若要根据View层的变化来改变Model,是通过一些特殊元素(例如
<input>、<select>、<textarea>
等元素)的onchange事件来触发修改JavaScript中的ViewModel对象数据的内容来实现的,这点比较容易理解。另一方面是ViewModel修改,如何触发View的自动更新或得新渲染呢。这种根据数据的变化来自动触发其它操作的机制就是我们说的数据变更检测,实现数据变更检测的方法主要有手动触发绑定、脏数据检测、对象劫持、proxy等。下面来具体分析
# 1.3.1 数据变更检测示例
# 手动触发绑定
手动触发指令绑定是比较直接的实现方式,主要思路是通过在数据对象上定义get()方法和set()方法(当然也可以使用其它命名方法),调用时手动出发get()或set()函数来获取、修改数据,改变数据后会主动触发get()和get()函数中View层的重新渲染功能。前面提到了,根据视图View来驱动ViewModel变化的常见主要应用于<input>、<select>、<textarea>
等元素中,当入内容变化时,通过监听DOM的change、select、keyup等事件来触发操作改变ViewModel的数据。我们来看一个简单的数据双向绑定的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>data-binding-method-set</title>
</head>
<body>
<input q-value="value" type="text" id="input">
<span q-text="value" id="el"></span>
<script >
let elems = [document.getElementById('el'), document.getElementById('input')];
let data = {
value: 'hello'
};
// 定义Directive
let directive = {
text: function(text) {
this.innerHTML = text;
},
value: function(value) {
this.setAttribute('value', value);
}
};
// 数据绑定监听
if(document.addEventListener){
elems[1].addEventListener('keyup',function(e) {
ViewModelSet('value', e.target.value);
}, false);
} else {
elems[1].attachEvent('onkeyup', function(e) {
ViewModelSet('value', e.target.value);
}, false);
}
// 事件监听的第三个值,是可选的,默认为false,且只有在部分浏览器中支持,true表明该事件监听器绑定在捕获阶段(和目标阶段),false则表明绑定在冒泡阶段(和目标阶段)。
// 开始扫描节点
scan();
//设置页面2秒后自动改变数据更新视图
setTimeout(function() {
ViewModelSet('value', 'hello world');
}, 1000)
function scan() {
// 扫描带指令的节点属性
for(let elem of elems){
elem.directive = [];
for (let attr of elem.attributes) {
if (attr.nodeName.indexOf('q-') >= 0){
// 调用属性指令
directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
elem.directive.push(attr.nodeName.slice(2));
}
}
}
}
// 设置数据改变后扫描节点
function ViewModelSet(key, value) {
data[key] = value;
scan();
}
</script>
</body>
</html>
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
通过浏览器加载执行这个页面结构,ViewModel的变化会自动改变输入框的内容,同样,输入内容的变化也会驱动ViewModel数据的变化。我们通过ViewModelSet()方法改变iewModel的数据后,需要主动调用scan()方法重新扫描HTML页面上的节点,并在需要的地方重新渲染HTML结构。
# 1.3.2 脏检测机制
总结: 与手动触发绑定不同的是,脏检测只针对可能修改的元素进行扫描,这样提高了ViewModel内容变化后扫描视图渲染的效率
以典型的MVVM框架Angularjs为例,Angularjs是通过检查脏数据来进行View层操作更新的,但我们并不针对某个框架来分析脏数据检测的机制,因为成熟框架的实现考虑了很多全面的东西,比较复杂,我们需要的是通过简洁明了的代码演示脏数据检测的实现原理。脏检测的实现原理是在ViewModel对象的某个属性值发生变化时代到这个属性值相关的所有元素,然后再比较数据变化,如果变化则进行Directive指令调用,对这个元素进行重新扫描渲染。用这种方法实现上面的例,代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>data-binding-dirty-check</title>
</head>
<body>
<input qg-event="value" qg-bind="value" type="text" id="input">
<span qg-event="text" qg-bind="value" id="el"></span>
<script >
let elems = [document.getElementById('el'), document.getElementById('input')];
let data = {
value: 'hello'
};
// 定义Directive
let directive = {
text: function(str) {
this.innerHTML = str;
},
value: function(str) {
this.setAttribute('value', str);
}
};
// 初始化扫描节点
scan(elems);
$digest('value');
/**
* 输入框数据绑定监听
*/
if (document.addEventListener) {
elems[1].addEventListener('keyup', function(e) {
data.value = e.target.value;
$digest(e.target.getAttribute('qg-bind'));
}, false);
} else {
elems[1].attachEvent('onkeyup', function(e) {
data.value = e.target.value;
$digest(e.target.getAttribute('qg-bind'));
}, false);
}
setTimeout(function() {
data.value = 'hello world';
$digest('value');
}, 2000);
function scan(elems) {
// 扫描带指令的节点属性
for ( let elem of elems){
elem.directive = [];
}
}
// 可以理解为数据劫持监听
function $digest(value) {
let list = document.querySelectorAll('[qg-bind='+ value + ']');
digest(list);
}
// 脏数据循环检测
function digest(elems) {
for (let i = 0, len = elems.length; i < len; i ++){
let elem = elems[i];
for (let j = 0 ,len1= elem.attributes.length; j < len1; j++){
let attr = elem.attributes[j];
if (attr.nodeName.indexOf('qg-event') >= 0){
// 调用属性指令
let dataKey = elem.getAttribute('qg-bind') || undefined;
// 进行脏数据检测,如果数据改变,则重新执行指令,否则跳过
if ( elem.directive[attr.nodeValue] !== data[dataKey]){
directive[attr.nodeValue].call(elem,data[dataKey]); // 给DOM赋值
elem.directive[attr.nodeValue] == data[dataKey];
}
}
}
}
}
</script>
</body>
</html>
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
其实这里的和手动绑定扫描节点的方式类似,不同的是,脏检测只针对可能修改的元素进行扫描,这样就提高了ViewModel内容变化后扫描视图渲染的效率
# 1.3.3 前端数据对象劫持(Hijacking)
数据劫持是目前使用比较广泛的方式。其基本思路是使用 Object.defineProperty
和Object.defineProperies
对ViewModel数据对象进行属性get()和set()的监听,当有数据读取和赋值操作时则扫描元素节点,运行指定对应节点的Directive指令,这样ViewModel使用通用的等号赋值就可以了。具体例子如下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>data-binding-hijacking</title>
</head>
<body>
<input q-value="value" type="text" id="input">
<span q-text="value" id="el"></span>
<script >
let elems = [document.getElementById('el'), document.getElementById('input')];
let data = {
value: 'hello'
};
// 定义Directive
let directive = {
text: function(text) {
this.innerHTML = text;
},
value: function(text) {
this.setAttribute('value', text);
}
};
// 初始化扫描节点
let bValue;
scan(elems);
// 可以理解为数据劫持监听
defineGetAndSet(data, 'value');
/**
* 输入框数据绑定监听
*/
if (document.addEventListener) {
elems[1].addEventListener('keyup', function(e) {
data.value = e.target.value;
}, false);
} else {
elems[1].attachEvent('onkeyup', function(e) {
data.value = e.target.value;
}, false);
}
setTimeout(function() {
data.value = 'hello world';
}, 2000);
function scan() {
// 扫描带指令的节点属性
for ( let elem of elems) {
elem.directive = [];
for ( let attr of elem.attributes) {
if (attr.nodeName.indexOf('q-') >=0 ){
// 调用属性指令
directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
elem.directive.push(attr.nodeName.slice(2));
}
}
}
}
// 定义对象属性调协劫持
function defineGetAndSet(obj,propName){
Object.defineProperty(obj,propName, {
get: function () {
return bValue;
},
set: function (newValue) {
bValue = newValue;
scan();
},
enumerable: true,
configurable: true
});
}
</script>
</body>
</html>
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
需要注意的是,defineProperty只支持Internet Explorer 8 以上和Chrome等标准的浏览器,且Internet Explorer 8 浏览器中需要使用es5-shim 来提供支持。Firefox浏览器不支持该方法,需要使用__defineGetter__
和__defineSetter__
来代替,这里为了让大家理解其中的原理,所有直接使用了defineProperty来实现
# 1.3.4 ECMAScript 6 Proxy
Proxy可以用于在已有对象基础上定义一个对象,并重新定义对象原型上的方法,包括get()方法和set()方法,同时我们也可以将它和defineProperty进行了比较自己学习一下,下面来看一下使用Proxy如何进行数据的变更检测。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>data-binding-proxy</title>
</head>
<body>
<input q-value="value" type="text" id="input">
<span q-text="value" id="el"></span>
<script >
'use strict';
let elems = [document.getElementById('el'), document.getElementById('input')];
// 定义Directive
let directive = {
text: function(text) {
this.innerHTML = text;
},
value: function(text) {
this.setAttribute('value', text);
}
};
// 设置data的访问Proxy
let data = new Proxy({}, {
get: function(target, key, receiver) {
return target.value;
},
set: function(target, key, value, receiver) {
target.value= value;
scan();
return target.value;
}
});
data['value'] = 'hello';
scan();
/**
* 输入框数据绑定监听
*/
if (document.addEventListener) {
elems[1].addEventListener('keyup', function(e) {
data.value = e.target.value;
}, false);
} else {
elems[1].attachEvent('onkeyup', function(e) {
data.value = e.target.value;
}, false);
}
setTimeout(function() {
data.value = 'hello world';
}, 2000);
function scan() {
// 扫描带指令的节点属性
for ( let elem of elems){
elem.directive = [];
for ( let attr of elem.attributes) {
if (attr.nodeName.indexOf('q-') >= 0 ){
// 调用属性指令
directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
elem.directive.push(attr.nodeName.slice(2));
}
}
}
}
</script>
</body>
</html>
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
这里通过简单的代码体现了MVVM双向数据绑定的基本原理,关于MVVM的数据变更检测介绍的比较多,因为可以认为这是MVVM设计实现的最关键的部分,如果处理得好会大大提升MVVM框架的效率,否则会有较多重复的元素扫描过程而拖累代码执行速度。到守里相信大家也对MVVM的设计原理有了较深的了解,自己可以实现一个类似的框架。
总结来看,前端框架从直接DOM操作到MVC设计模式,然后到MVP,再到MVVM框架,前端设计模式的改进原则一直向着高效、易实现、易维护、易扩展的方向发展,虽然目前前端各类框架也已经成熟并开始向高版本迭代,但是还没有结束,我们现在的变成对象依然没有脱离DOM编程的基本套路,一次次框架的改进大大提高了开发效率,但是DOM元素运行的效率仍然没有变。要解决这个问题,就必须了解下一切中介绍的前端Virtual DOM.
# 1.4 Virtual DOM 交互模式
# 1.4.1 Virtual DOM设计理念
MVVM的前端交互模式大大提高了变成效率,自动双向数据绑定让我们可以将页面逻辑实现的核心转移到数据层的修改操作上,而不再是在页面中直接操作DOM。但实际上,通过上一切的内容可以看出,尽管MVVM改变了前端开发的逻辑方式,但是最终数据层反应到页面上View层的渲染和改变仍是通过对应的指令进行DOM操作来完成的,而且通常一次ViewModel的变化可能会触发页面上多个指令操作DOM的变化,带来大量页面结构层DOM操作或渲染。先来看下面这个应用场景
<ul id="root">
<li q-repeat="list">
<span q-text="value"></span>
<span>固定文本</span>
</li>
</ul>
2
3
4
5
6
let viewModel = new VM({
$el: document.getElementById('root'),
data: {
list: [{value: 1}, {value: 2}, {value: 3}]
}
})
2
3
4
5
6
使用MVVM框架时就生成了一个数字列表,此时如果需要显示的内容变成了[{value:0},{value:1},{value:2},{value:3}]
,在MVVM框架中一般会重新渲染整个列表,包括列表中无须改变的部分也会重新渲染一次。但实际上如果直接操作改变DOM的话,只需要在<ul>
子元素前插入一个新的<li>
元素就可以了。但在一般的MVVM框架中,我们通常不会这样做。毫无疑问,这种情况下MVVM的View层更新模式就消耗了更多没必要的性能
那么该如何对ViewModel进行改进,让浏览器知道实际上只是增加了一个元素呢?通过对比两个数组,我们发现只了一个{value:0}
,那么该怎样将这个的数据反映到View层上呢?我们可以这样想,将新的Model data和旧的Model data进行对比,然后记录ViewModel的方式和位置,就知道了这次View层应该怎样去更新,这样比直接重新渲染整个列表高效得多。
这里其实可以理解为,ViewModel里的数据就是描述页面View内容的另一种数据结构标识,不过需要结合特定的MVVM描述语法编译来生成完整的DOM结构。根据差异性描述的对象,可以很执迷不悟就地创建出变更的DOM结构,完成这次HTML DOM结构的修改,而不是直接对整个更表重新渲染。
Virtual DOM是一个能够直接描述一段HTML DOM的javascript对象,浏览器可以根据它的结构按照 一定规则创建出确定唯 一的HTML DOM。整体来看Virtual DOM 的交互模式减少了MVVM或其它框架中对DOM的扫描或操作次数,并且在数据发生改变后只在合适的地方根据JavaScript对象来进行最小化的页面DOM操作,避免大量重新渲染
# 1.4.2 Virtual DOM的核心实现
核心操作可以抽象成三个步骤:创建Virtual DOM;对比两个Virtual DOM成生差异化 Virtual DOM ; 将差异化Virtual DOM 到页面上。
1.创建Virtual DOM
把一段HTML字符串文本解析成一个能够描述它的javascript 对象
<ul id="root">
<li>
<span>1</span>
<span>固定文本</span>
</li>
</ul>
let ulElement = {
tagName: 'ul',
attribute:[{
id: 'root'
}],
children:[{
tagName:'li',children:[{
tagName: 'span',
nodeText: 1
},{
tagName: 'span',
nodeText: '固定文本'
}]
}]
}
2.对比Virtual DOM
3.渲染差异化Virtual DOM
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
# 1.5 MNV*模式
如果说Virtul DOM 减少了DOM的交互次数,那么MNV*
想要做的一件事情就是完全抛弃使用DOM,而交互数据的操作依然可以使用ViewModel、Virtual DOM
或者直接的Model来实现。这种模式目前只适用于移动端Hybrid应用,因为需要以来原生应用控件调用支持,而只有这种特殊的应用场景才满足条件。
我们知道javaScript 可以借助通用协议来调用原生应用的方法,然后按照标准的JSBridge协议将Model和Directive封装成协议串jsbridge://DOMRender:success/createView?{"text":"hello","element":"TextView"}
的形式,通过Prompt(android),IOS上使用iframe发送到客户端,这里调用的是解析DOMRender的creatView方法创建一个TextView,TextView是android原生系统上的一个内置文本控件的基类,和HTML的
标签类似,可以用来创建移动端上的一个文本域展示一段文字。此时客户端会解析到这段协议串,调用原生应用动态创建文本空间TextView的界面内容碎片(类似于HTML的文档碎片documentFragment或一段XML片段),这里Android中TextView的控件内容描述通过地XML规范。
整体上设计实现一个Native渲染机制思路并不难,但是要实现好JavaScript端和Native端的封装,难度就比较大了。MNV*框架端的主要任务是解析Model、ViewModel或Virtual DOM 组成JSBridge协议串并发关,而Native端的实现将会比较复杂,而要处理不同的标签元素解析,例 如遇到<TextView>
标签则创建TextView空间,遇到<Layout>
标签创建Layout空间,还可能需要处理事件的绑定等,即将JavaScript事件通过Native事件来实现。整体上像是使用移动端原生的方式来解析HTML上需 要实现的就用功能。
MNV*
的基本原理主要是将JSBridge和DOM变成的方式进行结合,让前端能够快速构建开发原生界面的应用,从而脱离DOM的交互模式。目前MNV*开发模式也正处一直一个推广应用极端,相信未来会更加普及。
# 到这里第一个话题终于说完了
# 下面言归正传
# 2 浏览器基础应用
在介绍浏览器组成结构之前我们先来看一个经常被问到的问题:从我们打开浏览器输入一个网址到页面展示网面内容的这段时间内,浏览器和服务端发生了什么事情?我们直接来看一个相对简介但比较清晰的过程描述
- 在接收到用户输入的网址后,浏览器会开启一个线程来处理这个请求,对用户输入的URL地址进行分析判断,如果是HTTP协议就按照HTTP方式来处理。
- 调用浏览器引擎中对应方法,比如WebView中的loadUrl方法,分木质并加载这个URL地址
- 通过DNS解析获取该网站地址对应的IP地址,查询完成后边同浏览器的Cookie、userAgent等信息向网站目的IP发出GET请求。
- 进行HTTP协议会话,浏览器客户端向Web服务器发送报文
- 进入网站后台上的Web服务器处理请求,如Apache、Tomcat、Node.js等服务器
- 进入部署好的后端应用,如PHP、java、JavaScript、Python等后端程序,找到对的请求处理逻辑,这期间可能会读取服务器缓存或查询数据库等。
- 服务器处理请求并回响应报文,此时如果浏览器访问过该页面,缓存上有对应资源,会与服务器最后修改记录对比,一致则返回304,否则返回200和对应的内容
- 浏览器开始下载HTML文档(响应报头状态码为200时)或者从本地缓存读取文件内容(浏览器缓存有效或相应报头状态码为304时)
- 浏览器根据下载接收到的HTML文件解析结构建立DOM(Document Object Model, 文档对象模型)文档树,并根据HTML中的标记请求下载指定的MIME类型文件(如CSS、JavaScript脚本等),同时设置缓存等内容
- 页面开始解析渲染DOM,CSS根据规则解析并结合DOM文档树进行网页内容布局和绘制渲染,JavaScript执行引擎、客户端存储等。下面,我们具体来看一个浏览器的主要结构
2
3
4
5
6
7
8
9
10