前端发展至今,从直接的DOM操作到MVC设计,然后到MVP,再到如今的MVVM设计模式,其方向都是朝着更高效、更方便、易维护的角度去发展,极大的方便了我们开发过程。
但是,我们需要知道的,目前所有的MV*的设计模式,总的来说仅仅改变了一件事情:提高开发效率。其根本仍旧是DOM操作。对于DOM操作所产生的开销,大家可以看这篇文章。简单来说,操作DOM会产生页面重绘和回流,开销就花在这个上面。
当然,我们要清楚一点是,这里我们优化DOM开销的任何手段都不及浏览器的一次优化升级所产生的效益大,其他优化也是一样。
为了优化DOM元素运行的效率,前端出现了Virtual DOM(虚拟DOM)。当然,Virtual DOM 也仅仅是减少DOM操作,不可能完全做到不操作DOM。
为什么要有Virtual DOM
前面我们简单的说了,为了解决DOM操作所带来的效率问题而设计了Virtual DOM。这里我们通过一段代码来直观的感受下解决了什么问题。
<ul id='root'>
<!-- 遍历list -->
<li h-for='value in list'>{{value}}</li>
</ul>
// 伪代码
let viewModel = new VM({
$el: document.getElementById('root'),
data: {
list: [
{value: 1},
{value: 2},
{value: 3}
]
}
})
这里我模仿Vue的语法来写了一段伪代码,约定了一个以h
开头的Directive(指令),虽然实际不可能会运行。
通过遍历list
数组,来显示3个li
元素。这个很容易理解。但是当我此时list
增加一个{value: 4}
的元素:
list: [
{value: 1},
{value: 2},
{value: 3},
{value: 4}
]
此时,在一般MVVM框架中,页面中会重新渲染DOM,更新View,显示4个li
元素。
但是,在这个过程中, 我们只是希望向ul
元素中的尾部添加一个li
元素就可以了,没必要再去重新渲染list数据,因为这样会导致前面3个没有改变的数组元素也会重新渲染,这样无疑产生了一次性能浪费。
所以,此时Virtual DOM就是为了解决这一个问题,不去渲染未改变的数据,而是仅仅通过某种算法,去检查哪个数据需要添加或者删除,这样就减少了DOM操作,也就提升了性能。
获取差异化DOM的思路
前面我们提到了,我们只需要通过对比变化前的数据和变化后的数据来获得差异化数据,然后将该数据重新渲染即可。
那么,具体的思路是如何的?
我们都知道,ViewModel的变化会反映到View层上,我们可以通过对比新ViewModel和旧的ViewMode 来获得发生改变的位置,从而获取需要View需要改变的位置。
ViewModel从某个角度来说,就是描述View层的一种数据结构。了解AST的同学会清楚,可以通过数据结构来生成一种DOM对象树结构。
我们还是以前面的代码为例,模拟一下DOM树结构:
//旧的dom对象
let ulTree = {
tagName: 'ul',
attributes: [
{id: 'root'}
],
children: [
{
tagName: 'li',
nodeText: '1',
children: []
}
]
}
这里我只写了一个li,但是通过这段代码,大家应该可以清楚所谓的DOM树结构是什么。
当list
新增了一个元素,对应的会生成如下一种JS对象的DOM树结构:
// 新的dom对象
let ulTree = {
tagName: 'ul',
attributes: [
{id: 'root'}
],
children: [
{
tagName: 'li',
nodeText: '1',
children: []
},
{
// 新增的
tagName: 'li',
nodeText: '2',
children: []
}
]
}
有了新旧两段JS对象来描述的DOM树结构,我们就可以通过对比这两个JS对象,获得一个差异化的数据,并且拿到该发生变化数据的位置,在该位置进行相应的DOM操作。避免产生其他没必要的操作。
在这里,我个人对Virtual DOM有一个比较俗的理解:使用Virtual DOM的过程就好比我们生病了,去医院检查出什么病,直接对症下药,直接命中🎯。而不使用Virtual DOM的过程就好比我们不管什么病,直接吃一种包治百病的药💊,反正也能治好。
核心实现
前面我们用一些伪代码来介绍了VIrtual DOM的实现基本原理和概念。总的可以概括为三个步骤:1. 创建原始Virtual DOM;2.对比原始(旧)Virtual DOM 和用户操作所生成的新的Virtual DOM,生成差异化Virtual DOM;3.将差异化Virtual DOM渲染到页面上。
我们分别来看这3个步骤如何去实现。
创建Virtual DOM
创建Virtual DOM即把一段HTML字符串解析成一个可以描述它的JS对象,也就是类似一个DOM树结构的对象,前面我们也已经介绍了。但是,如何去创建Virtual DOM是一件很重的事情。一般的,我们可以能想到直接去遍历HTML去实现一个Virtual DOM结构。但是这样是错误的。
我们要清楚的是,Virtual DOM的优点就是减少DOM的操作,而我们如果直接去通过DOM API去扫描HTML代码,这本身就会使用的DOM的读取操作,很显然违背了我们的原则。
所以,我们选择另外一种方式,将HTML代码写成JS中的一个字符串,通过某种解析规则去解析这段HTML的字符串,在解析的过程中,我们就可以生成Virtual DOM。
🌰🌰🌰
let htmlString = `
<ul id="root">
<li>1</li>
</ul>
`;
// 调用parse方法来解析这段字符串,比如通过正则。
// parse方法逐个分析字符,
// 将标签存入tagName,
// 将属性存入attributes,
// 将子标签存入children,
// 这样就会产生前面我们所描述的DOM树结构对象。
// 整个过程中仅仅是对js字符串的操作
let vdom = parse(htmlString);
简单来说,创建Virtual DOM往往就是将一段DOM描述字符串解析成VIrtual DOM对象的过程。
接着,我们生成了Virtual DOM后,还没有交给浏览器去解析,目前仅仅是有一个JS对象。所以我们还要去渲染DOM,但是,我们不会直接丢给浏览器去解析,而是通过自己去解析Virtual DOM 去生成HTML元素。
🌰🌰🌰
// 接收一个Virtual DOM对象。
function render(vdom) {
let element = document.createElement(vdom.tagName); // 这也是React 外面只能有一个根节点的原因。
let attribuest = vdom.attributes;
// 遍历,设置根节点的dom属性
for (let key in attribuets) {
element.setAttribuets(key, attributes[key]);
}
// 处理子元素
let children = vdom.children || [];
for (let child of children) {
// 判断是文本节点还是元素节点
let childNode = (typeof child === 'string') ? document.createTextNode(child) : render(child);
element.appendChild(childNode);
}
return element;
}
上面是一个简单的DOM渲染的逻辑处理,此时我们就获取了Virtual DOM 并且重新在页面上渲染出了对应的元素。
获取差异化Virtual DOM
前面我们也提到了通过对比来获取差异化Virtual DOM。这里涉及到一个算法,即多叉树算法。Virtual DOM的对比实际上就是对于多叉树结构的遍历算法。其中,又有广度优先算法和深度优先算法。我对这个算法不熟悉,大家可以自行去搜索学习。
但是,基本的概念还是要理解的。
假设我们两个Virtual DOM每个节点对应一个唯一的字母,节点顺序分别使用深度优先遍历算法表示为:
1.ABCEFGHDIJ
2.AKLMBEFCGHDIJ
通过对比,很容易发现我们需要在A和B之间插入KLM节点,然后在通过KLM的关系得知,我们只需要插入K节点即可。
在对比过程中,我们要进行一系列的记录,比如发生的类型、位置,进行相应的增删改查操作等。
渲染差异化Virtual DOM
前面通过对比获取到了差异化Virtual DOM,拿到了类型和位置,我们直接通过DOM操作将内容渲染到对应位置上即可。
总结
Virtual DOM 是JS对象,对DOM树结构的一种抽象表现。
Virtual DOM最大的优势就是通过减少对DOM的操作次数,来提高交互性能和效率。但是我们也要清楚,其本质还是操作DOM,只不过仅仅是操作那些发生变化的DOM,减少了多余的操作。
基本的实现步骤为以下3点:
- 初始化Virtual DOM;
- 对比初始化(旧)的Virtual DOM和新的Virtual DOM 来生成差异化 Virtual DOM;
- 渲染差异化Virtual DOM。
谈谈自己的目标
自己毕业也将近一年了,回顾自己刚开始接触自己前端的时候,发现确实成长了不少,特别是在毕业之后到现在,这段时间相较之前的成长是最快的,自己的知识体系丰富了不少。但是,当知识体系丰富了之后,发现自己需要掌握的还有很多。
总的目标方向不能变,就是提升自己的技术水平,当技术水平达到baseline后,开始寻求某一方面的突破,并提升自己的领导和业务能力。
当前目标
自己在毕业的时候和同学注册了公司,但是一年下来,感觉对自己的技术成长有局限性。所以准备辞去当前公司工作,出去历练一下。所以当前目标,就是为了近期面试做准备。
- 掌握Vue原理
- 夯实JS基础知识
- 复习一下基本的WEB安全和HTTP协议
- 刷一些面试题
短期目标
在2-3年内,全面掌握这次课程大纲中涉及的内容,进入一线互联网公司,并慢慢成长为一个可以领导小型项目leader,或者具备一个leader的能力,对前端知识体系有一个全面掌握,并在个别方便有突破。
所以,成为一个leader,除了技术要达标外,还要有业务和领导能力。
总的来说,短期目标就是将自己前端技术水平提升到一个高度,具体来说:除了基本的前端语言外,需要掌握HTTP、WEB安全、算法、部分计算机原理等。另外,还要提升自己的业务能力和领导能力。
长期目标
长期目标,对于我来说,在技术水平达到一个高度后,选择重新创业的可能性会更大。但是在自己成长过程中,自己最基本的技术仍然要时刻学习,技术没有尽头的,况且前端行业发展迅速,知识更新快。
另外,继续坚持每周写周报的习惯。
文坚老师给出的这段话我觉得很好,这也是成长过程中需要的:
- 知道敌人在哪。也就是业务问题在哪里?
- 知道敌人从哪里来。也就是这个问题可以用什么来解决?是用技术来解决?还是用流程解决?还是要靠协同合作解决?
- 知道怎么去伏击敌人,自己的优势在哪里。也就是根据自己的情况(资源、协同能力、影响力等)去制定最有效的方案(可能是技术,也可能是流程,亦可能是协同)
- 知道在哪里避战,哪里主动出击,避其锋芒。知道哪些东西可以放弃,哪些东西必须解决,有重点有主线。
- 知道怎么提升小队的战斗力。也就是,怎么培养人,怎么用事情来锻炼人
- 知道怎么去请战功。也就是打了小规模胜战要让别人知道,例如让主管知道,让合作伙伴知道,让竞争对手知道,来建立跟你打战有肉吃的感受,提升小队凝聚力
- 如有可能,甚至知道怎么联动友军去夹击敌人。也就是怎么跨团队合作,怎么通过合作来达成局部战略目的。