Web前端页面的开发避免不了与DOM的交互操作。前端框架的一次次变化,从提升效率的阶段,慢慢走向改善性能的阶段。
对于开发者来说,所有数据内容都可以通过DOM结构来组织和展示的。数据的处理和操作的核心其实就是DOM的处理和操作。DOM API可以分为:节点查询型、节点创建型、节点修改型、节点关系型、节点属性型和内容加载型。
类型 | 方法 | jQuery方法 |
---|---|---|
节点查询型 | getElementById、getElementByName、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll | $(selector)、find()等 |
节点创建型 | createElement、createDocumentFragment、createTextNode、cloneNode | $(selector)、clone()等 |
节点修改型 | appendChild、replaceChild、removeChild、insertBefore、innerHTML | html()、replace()、remove()、append()、before()、after()等 |
节点关系型 | parentNode、previousSibling、childNodes | parent()、siblings()、closest()、next()、children()等 |
节点属性型 | innerHTML、attributes、getAttribute、setAttribute、getComputedStyle | attr()、data()、css()、hide()、show()、slideDown()、slideUp()、animate()等 |
内容加载型 | XMLHttpRequest、ActiveX | ajax()、get()、post()等 |
因为createDocumentFragment可以将多个文档内容片段进行缓存,最后一次性插入DOM中,减少DOM操作次数,提高效率。
需要在请求头部加上xhrFields:{withCredentials: true}
才能将Cookie信息正常带到请求中发送给服务器。
随着AJAX技术盛行,SPA(Single Page Application,单页面应用)开始广泛被认可。其基本思路:将整个应用内容都在一个页面中实现并完全通过异步交互来根据用户操作加载不同的内容。在这期间,DOM操作和事件绑定将变得十分混乱,不便于管理,于是MV*框架运应而生。
将页面DOM相关的内容抽象成数据模型、视图、事件控制函数(Model-View-Controller)三部分。
const Router = {
states: {},
state(hash, conf) {
this.states[hash] = conf;
},
fire(match) {
let routerReg = new RegExp(match, 'g');
for(let state in this.states) {
if(routerReg.test(state)) {
this.states[state].controller();
break;
}
}
return this;
}
};
Router.state('#index', {
controller: () => {
console.log('_loadIndex()');
}
});
Router.state('#detail', {
controller: () => {
console.log('_loadDetail()');
}
});
// 监听路由变化
window.addEventListener("hashchange", (event) => {
// console.log(event.newURL, event.oldURL);
Router.fire(location.hash);
}, false);
也可以使用HTML5的pushState来实现路由history.pushState(state, title, url)
在MVC里,组件将自己的控制权给统一的控制对象来调用,大部分MVC框架通过事件监听或者观察者模式来实现的!
<div id="A" onclick="A.event.change">div>
<script>
// 公共的Component基类
let component = new Component();
let A = component.extend({
$el: document.getElementById('A'),
model: {
text: 'ViewA渲染完成'
},
view(data) {
let tpl = '{{text}}';
// 渲染模板
let html = render(tpl, data);
this.$el.innerHTML = html;
},
controller() {
let self = this;
// 将Model数据传入View中渲染
self.view(self.model);
// 监听hash变化
window.addEventListener('hashChange', () => {/*...*/});
// 监听事件
self.event['change'] = function() {
self.model.text = '...';
self.view(self.model);
}
}
})
script>
需要注意:用户操作引起的DOM修改操作主要通过Controller来直接控制的,但是Controller只进行修改操作指令的分发,数据的渲染一般是在View层来完成!
MVP(Model-View-Presenter)和MVC区别在于:用户在进行DOM修改操作时将通过View上的行为触发,然后将修改通知给Presenter来完成后面的Model修改和其他View的更新(View和Presenter是双向的);而MVC模式下,用户的操作时直接通过Controller来控制的。
<div id="A" onclick="A.event.change">div>
<script>
// 公共的Component基类
let component = new Component();
let A = component.extend({
$el: document.getElementById('A'),
model: {
text: 'ViewA渲染完成'
},
view: '{{text}}',
presenter() {
let self = this;
// 渲染模板
let html = render(tpl, data);
this.$el.innerHTML = html;
// View上的改变通知Presenter改变Model和其他View
$('#input').on('change', () => {
self.model.text = this.value;
html = render('{{text}}', self.model);
$('#showText').html(html);
});
// 监听事件
self.event['change'] = function() {
self.model.text = '...';
html = render('{{text}}', self.model);
$('#showText').html(html);
}
}
})
script>
View和Model主要用于提供视图模板和数据而不做任何逻辑处理
MVVM可认为是一个自动化的MVP,使用ViewModel代替了Presenter。数据Model的调用和模板内容的渲染不需要我们主动操作,而是ViewModel自动来触发完成,任何用户的操作也是通过ViewModel的改变驱动的。
数据变更检测:
方式 | 原理 | 说明 |
---|---|---|
手动触发绑定 | 通过在数据对象上定义get()、set()方法(函数中包含View层的渲染),手动触发 | 需要主动调用重新扫描HTML页面上的所有节点的方法 |
脏检测机制 | ViewModel对象的某个属性值发生变化时找到与这个属性值相关的所有元素,然后再比较数据变化,如果发生变化则进行Directive指令调用,对这个元素进行重新扫描渲染 | 只针对可能修改的元素进行扫描 |
前端数据对象劫持 | 使用Object.defineProperty和Object.defineProperties对ViewModel数据对象进行属性get()和set()的监听,当有数据读取和赋值等操作则扫描元素节点,运行指定节点的Directive指令。 | 对象和数组新增成员需要手动调用 |
ES6 Proxy | 在现有对象基础上重新定义一个对象,并重新定义对象原型上的方法,包括get()和set()。 | ES6方式,存在兼容性 |
MVVM的前端交互模式大大提高了编程效率,自动双向数据绑定让我们可以将页面逻辑实现的核心转移到数据层的修改操作上,而不再是在页面中直接操作DOM。MVVM最终数据层反应到页面上View层的渲染和改变仍是通过对应的指令进行DOM操作来完成的,而且通过一次ViewModel的变化可能会触发页面上多个指令操作DOM的变化,带来大量的页面结构层DOM操作或渲染。
<ul id="root">
<li v-for="item in list">
<a href="item.href">item.valuea>
li>
ul>
<script>
var root = new Vue({
el: '#root',
data() {
return {
list: [{href: '', value: ''},{href: '', value: ''}]
}
}
})
script>
在起初的MVVM框架中一般会重新渲染整个列表,包括列表中无需改变的部分也会重新渲染一次。但实际上如果直接操作改变DOM的话,只需变更或新的
Virtual DOM是一个能够直接描述一段HTML DOM结构的JavaScript对象,浏览器可以根据它的结构按照一定规则创建出确定唯一的HTML DOM结构。 Virtual DOM的核心实现:创建原始页面或组件的Virtual DOM结构,用户操作后需要进行DOM更新时,生成用户操作后页面或组件的Virtual DOM结构并与之前的结构进行对比,找到最小变换Virtual DOM的差异化描述对象,然后把差异化的Virtual DOM根据特定的规则渲染到页面上。
创建Virtual DOM:把一段HTML字符串文本解析成一个能够描述它的JavaScript对象。
<ul id="root"> <li> <a href="链接1">文本1a> li> <li> <a href="链接2">文本2a> li> ul> <script> let ulElement = { tagName: 'ul', attribute: [{ id: 'root' }], children: [{ tagName: 'li', children: [{ tagName: 'a', attribute: [{ href: '链接1' }], nodeText: '文本1' }] },{ tagName: 'li', children: [{ tagName: 'a', attribute: [{ href: '链接2' }], nodeText: '文本2' }] }] } script>
对比Virtual DOM:对比前后变化的两个Virtual DOM差异性,得到一个差异树对象。可使用广度优先算法和深度优先算法。这里需要记录节点改变的内容,还要记录发生差异化改变的类型和位置。
渲染Virtual DOM:根据差异化内容将其渲染到页面上,减少了对DOM对象的操作次数。
前端MNV*时代
使用JavaScript调用原生控件或事件绑定来生成应用程序的交互模式称为前端MNV*开发模式。如果说Virtual DOM减少了DOM的交互次数,那么MNV*
想要做的就是完全抛弃使用DOM。这种模式仅适用于移动端Hybrid应用,因为需要依赖原生应用控件的调用支持。将JSBridge和DOM编程的方式进行结合,让前端能够快速构建开发原生页面的引用,从而脱离DOM的交互模式。