浏览器中的Dom更新

  在浏览器中渲染引擎将 node 节点添加到 另外节点中时会触发样式计算、布局、绘制、栅格化、合成等任务,这一过程称为重排。

  除了重排之外,还有可能引起重绘或者合成操作,也就是“牵一发而动全身”。

  另外,对于 DOM 的不当操作还有可能引发强制同步布局和布局抖动的问题,这些操作都会大大降低渲染效率。

  因此,对于 DOM 的操作时刻都需要非常小心谨慎。

  对于一些复杂的页面或者目前使用非常多的单页应用来说,其 DOM 结构是非常复杂的,而且还需要不断地去修改 DOM 树,每次操作 DOM 渲染引擎都需要进行重排、重绘或者合成等操作,因为 DOM 结构复杂,所生成的页面结构也会很复杂,对于这些复杂的页面,执行一次重排或者重绘操作都是非常耗时的,这就给浏览器带来了真正的性能问题。

  这解决办法就是虚拟Dom。

加入虚拟dom之后的浏览器更新

  1. 将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上;
  2. 变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了。
  3. 在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。

  如下图:

1、创建阶段

  首先依据 JSX 和基础数据创建出来虚拟 DOM(并缓存起来),它反映了初始的真实的 DOM 树的结构。

  然后由虚拟 DOM 树创建出真实 DOM 树,真实的 DOM 树生成完后,再触发渲染流水线往屏幕输出页面。

2、更新阶段

  如果数据发生了改变,那么就需要根据新的数据创建一个新的虚拟 DOM 树;

  然后比较(原虚拟Dom树和新虚拟Dom树,此时用到了Diff算法)两个树,找出变化的地方,并把变化的地方一次性更新到真实的 DOM 树上;

  最后渲染引擎更新渲染流水线,并生成新的页面。

框架中的Dom更新

如上图,可以把虚拟 DOM 看成是 MVC 的视图部分,其控制器和模型可以是redux也可以是Vuex。其具体实现过程如下:

  1. 图中的控制器是用来监控 DOM 的变化,一旦 DOM 发生变化,控制器便会通知模型,让其更新数据;
  2. 模型数据更新好之后,控制器会通知视图,通知其模型的数据发生了变化;
  3. 视图接收到更新消息之后,会根据模型所提供的数据来生成新的虚拟 DOM;
  4. 新的虚拟 DOM 生成好之后,就需要与之前的虚拟 DOM 进行比较,找出变化的节点(使用了Diff算法);
  5. 比较出变化的节点之后,React 将变化的虚拟节点应用到 DOM 上,这样就会触发 DOM 节点的更新;
  6. DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。

真实Dom转化为虚拟Dom

具体实现思路:

// 将真实DOM转化为虚拟DOM
// <div />  => {tag:'div'}   元素转化
// 文本节点 => {tag:undefined,value:'文本节点'}   文本节点转化
// <div title="1" class="c"  />  => { tag:'div',data:{ title:'1',class:"c" } }   多属性转化
// <div><div /></div> => {tag:'div',children:[{ tag:'div' }]}
// 用构造函数来 进行以上转换

其中进行元素节点和文本节点以及多属性节点的区分是利用了node节点的nodeType属性:

  • 如果节点是一个元素节点,nodeType 属性返回 1。
  • 如果节点是属性节点, nodeType 属性返回 2。
  • 如果节点是一个文本节点,nodeType 属性返回 3。
  • 如果节点是一个注释节点,nodeType 属性返回 8。

该属性是只读的。

        大概实现代码:

class VNode {// 构造函数constructor( tag,data,value,type ){// tag:用来表述 标签  // data:用来描述属性  // value:用来描述文本 // type:用来描述类型this.tag = tag && tag.toLowerCase();//文本节点时 tagName是undefinedthis.data = data;this.value = value;this.type = type;this.children = [];}appendChild(vnode){this.children.push(vnode);}
}
/**利用递归 来遍历DOM元素 生成虚拟DOMVue中的源码使用栈结构,使用栈存储父元素来实现递归生成
*/
function getVNode(node){let nodeType = node.nodeType;let _vnode = null;if(nodeType === 1){// 元素let nodeName = node.nodeName;//元素名,什么标签?let attrs = node.attributes;//属性伪数组元素上的属性let _attrObj = {};//attrs[i] 属性节点(nodeType == 2) 是对象                    for(let i=0;i<attrs.length;i++){//attrs[i].nodeName:属性名 attrs[i].nodeValue:属性值_attrObj[attrs[i].nodeName] = attrs[i].nodeValue;               }//标签名(DIV UI LI...)、所有属性对象、value值(只有文本标签有)、type类型(是元素还是文本)_vnode = new VNode( nodeName,_attrObj,undefined,nodeType);// 考虑node的子元素let childNodes = node.childNodes;for(let i = 0;i<childNodes.length;i++){_vnode.appendChild(getVNode(childNodes[i]));//递归}}else if(nodeType === 3){// 文本节点_vnode = new VNode(undefined,undefined,node.nodeValue,nodeType);//无标签名、无属性、有value、有type}return _vnode;
}// 读取根节点下dom数据,或者改成指定dom下数据
let root = document.querySelector("#root");let vroot = getVNode ( root );//虚拟DOM
console.log(vroot);

虚拟Dom转化为真实Dom

无论是什么类型的节点,只有三种类型的节点会被创建并插入到的Dom中:元素节点、注释节点、和文本节点:

// {tag:'div'} => <div />   元素转化
// {tag:undefined,value:'文本节点'} => 文本节点    文本节点转化
// { tag:'div',data:{ title:'1',class:"c" } } => <div title="1" class="c"  />     多属性转化
// {tag:'div',children:[{ tag:'div' }]} => <div><div /></div>

具体实现思路:

vnode数据类型:

vnode = {tag:'div', // 以div为例// attrs中包含插在节点上的属性,就比如类或者样式,内联样式等attrs:{class:'a'            }children:[], // children之中可能是另外的vnode
}
  • 将字符串转化为文本节点;
  • 将数字转化为字符串再转化为文本节点;
  • 将多属性节点转换为文本节点,子节点再延续上面的过程;

        大概实现代码如下:

//Virtual DOM => DOM
function render(vnode, container) {container.appendChild(_render(vnode));
}function _render(vnode) {// 如果是数字类型转化为字符串,然后转到生成文本节点if (typeof vnode === 'number') {vnode = String(vnode);}// 字符串类型直接就是文本节点if (typeof vnode === 'string') {return document.createTextNode(vnode);}// 普通DOMconst dom = document.createElement(vnode.tag);if (vnode.attrs) {// 遍历属性Object.keys(vnode.attrs).forEach(key => {const value = vnode.attrs[key];dom.setAttribute(key, value);})}// 子数组进行递归操作vnode.children.forEach(child => render(child, dom));return dom;
}

使用到的document方法解析:

1.DOM appendChild() :

appendChild() 方法向节点添加最后一个子节点。

也可以使用 appendChild() 方法从一个元素向另一个元素中移动元素。

2.DOM createElement():

createElement() 方法通过指定名称创建一个元素:

3.DOM setAttribute():

setAttribute() 方法添加指定的属性,并为其赋指定的值。

如果这个指定的属性已存在,则仅设置/更改值。

DOM-Diff算法

DOM-Diff就是一种比较算法,比较两个虚拟DOM的区别,也就是比较两个对象的区别。

也有真实DOM与虚拟DOM的比较,不过这里先只讨论虚拟DOM之间的比较。

DOM-Diff(传统)算法

处理方案: 循环递归每一个节点,如下图:

左侧树a节点依次进行如下对比:

a->e、a->d、a->b、a->c、a->a

之后左侧树其它节点b、c、d、e亦是与右侧树每个节点对比, 算法复杂度能达到O(n^2)

查找完差异后还需计算最小转换方式,最终达到的算法复杂度是O(n^3)。

将两颗树中所有的节点一一对比需要O(n²)的复杂度,在对比过程中发现旧节点在新的树中未找到,那么就需要把旧节点删除,删除一棵树的一个节点(找到一个合适的节点放到被删除的位置)的时间复杂度为O(n),同理添加新节点的复杂度也是O(n),合起来diff两个树的复杂度就是O(n³)

DOM-Diff(优化后)算法

vue和react的虚拟DOM的diff算法大致相同,其核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
  2. 同一层级的一组节点,他们可以通过唯一的id进行区分

考虑到很少进行跨层移动,所以用平层对比,时间复杂度从O(n^3)缩短为O(n),在对比过程中直接对真实dom更新。变更一般有三种: 文本 ,节点属性,节点变更,增删节点(绑定key值的作用)

DOM-Diff三种优化策略

  • web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
  • 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构。
  • 对于同一层级的一组自节点,他们可以通过唯一id进行区分。

也就是:

        比较只会在同层级进行, 不会跨层级比较。

具体表现如下:

  • 只比较平级

  • 不会跨级比较

  • 同一级的变化节点,如果节点相同只是位置交换,则会复用。(通过key来实现)

React优化Diff算法

基于以上优化的diff三点策略,react分别进行以下算法优化

  • tree diff
  • component diff
  • element diff

tree diff

react对树的算法进行了分层比较。react 通过 updateDepth对Virtual Dom树进行层级控制,只会对相同颜色框内的节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在,则该节点和其子节点都会被删除。这样是需要遍历一次dom树,就完成了整个dom树的对比

分层比较 :

如果是跨层级的移动操作,如图

当根结点发现A消失了,会删除掉A以及他的子节点。当发现D上多了一个A节点,会创建A(包括其子节点)节点作为子节点

所以:当进行跨层级的移动操作,react并不是简单的进行移动,而是进行了删除和创建的操作,这会影响到react性能。所以要尽量避免跨层级的操作。(例如:控制display来达到显示和隐藏,而不是真的添加和删除dom。

component diff

  • 如果是组件类相同(class)的组件,则直接对比virtual Dom tree
  • 如果组件类不同(结构相似)的组件,则判断为 dirty component(脏组件),整个替换掉组件以及组件下的所有子组件
  • 如果组件类相同,但是可能virtual DOM 没有变化,这种情况下我们可以使用shouldComponentUpdate() 来判断是否需要进行diff

如果组件D和组件G,如果组件类不同,但是结构类似。这种情况下,因为组件类不同,所以react会删除D,创建G。所以我们可以使用shouldComponentUpdate()返回false不进行diff。

        针对react15, 16出了新的生命周期

        所以:component diff 主要是使用shouldComponentUpdate() 来进行优化

element diff

element diff 涉及三种操作:

  • 插入
  • 移动
  • 删除

不使用key的情况:

不使用key的话,react对新老集合对比,发现新集合中B不等于老集合中的A,于是删除了A,创建了B,依此类推直到删除了老集合中的D,创建了C于新集合。

这样会产生渲染性能瓶颈,于是react允许添加key进行区分。

使用key的情况:

react首先对新集合进行遍历,通过唯一key来判断老集合中是否存在相同的节点,如果没有的话创建,如果有的话进行移动操作

移动优化

在移动前,会将节点在新集合中的位置(_mountIndex)和在老集合中位置(lastIndex)进行比较,如果if (child._mountIndex < lastIndex) 进行移动操作,否则不进行移动操作。这是一种顺序移动优化。只有在 新集合的位置 小于 在老集合中的位置 才进行移动。

        如果遍历的过程中,发现在新集合中没有,但是在老集合中的节点,会进行删除操作

        所以:element diff 通过唯一key 进行diff 优化。

总结:

  • react中尽量减少跨层级的操作。
  • 可以使用shouldComponentUpdate() 来避免react重复渲染。
  • 尽量添加唯一key,以减少不必要的重渲染

Vue2.x优化Diff

vue2.0加入了virtual dom,和react拥有相同的 diff 优化原则

差异就在于, diff的过程就是调用patch函数,就像打补丁一样修改真实dom。也就是通过js层面的计算,根据两个虚拟对象创建出差异的补丁对象patch,用来描述改变了哪些内容,然后用特定的操作解析patch对象,更新dom完成页面的重新渲染。

使用的主要方法:

  • patchVnode
  • updateChildren

updateChildren是vue diff的核心

过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较。

 Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue 3.x

Vue3.x借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。

 该算法中还运用了动态规划的思想求解最长递归子序列。

参考及复制文章链接

  • 彻底搞懂虚拟Dom到真实Dom的生成过程 - 浅笑· - 博客园
  • DIff算法优化策略_马超19991128的博客-CSDN博客
  • react学习笔记1(diff算法和Component相关)_weixin_41837346的博客-CSDN博客
  • DOM-DIFF原理及实现 - 简书
  • 浏览器工作原理:浅析浏览器中的页面 - 虚拟DOM与实际DOM有何不同 - 古兰精 - 博客园
  • 虚拟dom到真实dom - 简书
  • 虚拟dom转化到真实dom - 简书

        引用不分前后

从VirtualDom(虚拟Dom)到真实DOM相关推荐

  1. reactjs虚拟DOM与真实DOM

    关于虚拟DOM: 1.本质是Object类型的对象(一般对象) 2.虚拟DOM比较"轻",真实DOM比较"重",因为虚拟DOM是React内部在用,无需真实DO ...

  2. 虚拟DOM和真实DOM的区别和联系

    介绍一下虚拟dom和真实dom吧 一.DOM DOM是文档对象模型(Document Object Model),它是一个结构化文本的抽象. 二.虚拟DOM 虚拟DOM只是js模拟的DOM结构,是对真 ...

  3. 虚拟DOM和真实DOM的区别

    DOM DOM意思是文档对象模型(Dcoument Object Model),它是一个结构化文本的抽象 操作DOM 所以,只要我们想要动态修改网页的内容的时候,我们就修改DOM. var item ...

  4. virtual DOM和真实DOM的区别_让虚拟DOM和DOMdiff不再成为你的绊脚石

    来源 | https://juejin.im/post/5c8e5e4951882545c109ae9c Keep Moving 时至今日,前端对于知识的考量是越来越有水平了,逼格高大上了 各类框架大 ...

  5. 传递HTML字符串virtual,理解Virtual DOM(1) 真实DOM和虚拟DOM的映射

    什么是Virtual DOM? 所谓virtual,指的是对真实DOM的一种模拟.相对于直接操作真实的DOM结构,我们构建一棵虚拟的树,将各种数据和操作直接应用在这棵虚拟的树上,然后再将对虚拟的树的修 ...

  6. Vue 原理解析(五)之 虚拟Dom 到真实Dom的转换过程

    上一篇 vue 原理解析(四): 虚拟Dom 是怎么生成的 再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系.我们先回顾之前的mountComponn ...

  7. React--虚拟DOM和真实DOM

    学习资源推荐 真实dom <div className="foo"><h1>Hello React</h1> </div> 虚拟do ...

  8. 前端学习(3196):虚拟dom和真实dom

  9. 前端学习(3104):react-hello-虚拟dom和真实dom

最新文章

  1. 原生态的ajax 及json和gson学习资源
  2. Linux 重新挂载分区的方法
  3. Nvidia DX10 Lighting例子解析
  4. 函数递归以及尾递归调用
  5. php 分享微博,php微信分享到朋友圈、QQ、朋友、微博
  6. Mysql和Redis数据同步该怎么做
  7. pythonrange函数用法_python range()函数详细用法
  8. OpenCV中的reshape
  9. 二十一天学通JavaScript:cookie的安全性
  10. salesforce 零基础学习(六十八)http callout test class写法
  11. vue + element插件Popover弹出框
  12. 求解位置不可用无法访问介质受写入保护咋寻回??
  13. Git学习8 Git分支操作
  14. bundle adjustment 详解
  15. 8086CPU I/O系统组织 8253芯片 8255A芯片
  16. 计算机专业职业战队,「团长分享」计算机系Meiko?画家小姜?如果选手没打职业……...
  17. 青春无悔―追忆10年前的那场校园民谣
  18. 网站服务器 64位,如何将win7系统从32位升级到64位_网站服务器运行维护,win7,32位,64位...
  19. nowcoder 点击消除 (字符串 + 栈)
  20. 亚马逊营销和运营手法的运用知多少?

热门文章

  1. 入门JAVA第五天 方法与数组
  2. 风向风速图将Series中的风向风速数据data和xAxis中的时间data一一对应
  3. 【前端学习笔记】浮动属性
  4. Kubernetes_介绍
  5. vue 给iframe设置src_vue 中引入iframe,动态设置其src,遇到的一些小问题总结
  6. 正念的奇迹 - 喧嚣的世界中获取安宁
  7. Excel中使用F-检验
  8. 爸爸的信:学会鄙视自己,才不会妥协
  9. 制裁那么凶猛,无脑上会不会走到尽头,首先要知道单片机是什么
  10. 火山引擎云原生大数据在金融行业的实践