Virtual DOM

2018-04-24  本文已影响65人  IsaacHHH

前端发展至今,从直接的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

[图片上传中...(2.png-17cc3b-1524548971902-0)]
2.png

通过对比,很容易发现我们需要在AB之间插入KLM节点,然后在通过KLM的关系得知,我们只需要插入K节点即可。

在对比过程中,我们要进行一系列的记录,比如发生的类型、位置,进行相应的增删改查操作等。

渲染差异化Virtual DOM

前面通过对比获取到了差异化Virtual DOM,拿到了类型和位置,我们直接通过DOM操作将内容渲染到对应位置上即可。

总结

Virtual DOM 是JS对象,对DOM树结构的一种抽象表现。

Virtual DOM最大的优势就是通过减少对DOM的操作次数,来提高交互性能和效率。但是我们也要清楚,其本质还是操作DOM,只不过仅仅是操作那些发生变化的DOM,减少了多余的操作。

基本的实现步骤为以下3点:


谈谈自己的目标

自己毕业也将近一年了,回顾自己刚开始接触自己前端的时候,发现确实成长了不少,特别是在毕业之后到现在,这段时间相较之前的成长是最快的,自己的知识体系丰富了不少。但是,当知识体系丰富了之后,发现自己需要掌握的还有很多。

总的目标方向不能变,就是提升自己的技术水平,当技术水平达到baseline后,开始寻求某一方面的突破,并提升自己的领导和业务能力

当前目标

自己在毕业的时候和同学注册了公司,但是一年下来,感觉对自己的技术成长有局限性。所以准备辞去当前公司工作,出去历练一下。所以当前目标,就是为了近期面试做准备。

短期目标

在2-3年内,全面掌握这次课程大纲中涉及的内容,进入一线互联网公司,并慢慢成长为一个可以领导小型项目leader,或者具备一个leader的能力,对前端知识体系有一个全面掌握,并在个别方便有突破。

所以,成为一个leader,除了技术要达标外,还要有业务和领导能力。

总的来说,短期目标就是将自己前端技术水平提升到一个高度,具体来说:除了基本的前端语言外,需要掌握HTTP、WEB安全、算法、部分计算机原理等。另外,还要提升自己的业务能力和领导能力。

长期目标

长期目标,对于我来说,在技术水平达到一个高度后,选择重新创业的可能性会更大。但是在自己成长过程中,自己最基本的技术仍然要时刻学习,技术没有尽头的,况且前端行业发展迅速,知识更新快。

另外,继续坚持每周写周报的习惯。

文坚老师给出的这段话我觉得很好,这也是成长过程中需要的:

  • 知道敌人在哪。也就是业务问题在哪里?
  • 知道敌人从哪里来。也就是这个问题可以用什么来解决?是用技术来解决?还是用流程解决?还是要靠协同合作解决?
  • 知道怎么去伏击敌人,自己的优势在哪里。也就是根据自己的情况(资源、协同能力、影响力等)去制定最有效的方案(可能是技术,也可能是流程,亦可能是协同)
  • 知道在哪里避战,哪里主动出击,避其锋芒。知道哪些东西可以放弃,哪些东西必须解决,有重点有主线。
  • 知道怎么提升小队的战斗力。也就是,怎么培养人,怎么用事情来锻炼人
  • 知道怎么去请战功。也就是打了小规模胜战要让别人知道,例如让主管知道,让合作伙伴知道,让竞争对手知道,来建立跟你打战有肉吃的感受,提升小队凝聚力
  • 如有可能,甚至知道怎么联动友军去夹击敌人。也就是怎么跨团队合作,怎么通过合作来达成局部战略目的。
上一篇下一篇

猜你喜欢

热点阅读