虚拟DOM

  虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,一般称之为虚拟节点(VNode)

优点:解决浏览器性能问题 ,真实DOM频繁排版与重绘的效率是相当低的,虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分(注意!),最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗。

例子1:

<div>我是文本</div>
let VNode = {
tag:'div',
children:'我是文本'
}

例子2:

<div class="container" style="color:yellow"></div>
let VNode = {
tag:'div',
data:{
class:'container',
style:{
color:'yellow'
}
},
children:''
}

例子3:

<div class="container">
<h1 style="color:red">标题</h1>
<span style="color:grey">内容</span>
<span></span>
<div>
let VNode = {
tag: 'div',
data:{
class:'container'
},
children:[
{
tag:'h1',
data:null,
children:{
data: {
style:{
color:'red'
}
},
children: '标题'
}
},
{
tag:'span',
data:null,
children:{
data: {
style:{
color:'grey'
}
},
children: '内容'
}
},
{
tag:'span',
data:null,
children:''
}
]
}

看完了例子,聪明的你一定知道了什么是虚拟dom。

 snabbdom

先看一眼github上的例子

snabbdom有几个核心函数,h函数,render函数和patch函数。

h函数

用于创建VNode(virtual node虚拟节点),追踪dom变化的。

React中通过babel将JSX转换为h函数的形式,Vue中通过vue-loader将模板转换为h函数。

假设在vue中我们有如下模板

<template>
<div>
<h1></h1>
</div>
</template>

用h函数来创建与之相符的VNode:

const VNode = h('div',null,h('h1'))

得到的VNode对象如下:

const VNode = {
tag: 'div',
data: null,
children: {
tag: 'span',
data: null,
children: null
}
}

什么是虚拟DOM的挂载

虚拟DOM挂载:将虚拟DOM转化为真实DOM的过程

主要用到如下原生属性或原生方法:

  • 创建标签:document.createElement(tag)

  • 创建文本:document.createTextNode(text);

  • 追加节点:parentElement.appendChild(element)

什么是虚拟DOM的更新

虚拟DOM更新:当节点对应得vnode发生改变时,比较新旧vnode的异同,从而更新真实的DOM节点。

let prevVNode = {
//...
}
let nextVNode = {
//...
} //挂载
render(prevVNode,container) //更新
setTimeout(function(){
render(nextVNode,container)
},2000)

我们在更新的时候,又分为两种情况:

  1. prevVNode和nextVNode都有,执行比较操作

  2. 有prevVNode没有nextVNode,删除prevVNode对应的DOM即可

function render(vNode,container){
const prevVNode = container.vNode;
//之前没有-挂载
if(prevVNode === null || prevVNode === undefined){
if(vNode){
mount(vNode,container);
container.vNode = vNode;
}
}
//之前有-更新
else{
//之前有,现在也有
if(vNode){
//比较
}
//以前有,现在没有,删除
else{
//删除原有节点
}
}
}

render函数

将VNode转化为真实DOM

接收两个参数:

  • 虚拟节点
  • 挂载的容器
function render(VNode,container){
//...
}

最终render代码

function render(vNode,container){
const prevVNode = container.vNode;
//之前没有-挂载
if(prevVNode === null || prevVNode === undefined){
if(vNode){
mount(vNode,container);
container.vNode = vNode;
}
}
//之前有-更新
else{
//之前有,现在也有
if(vNode){
patch(prevVNode,vNode,container);
container.vNode = vNode;
}
//以前有,现在没有,删除
else{
removeChild(container,prevVNode.el);
container.vNode = null;
}
}
}

patch函数

想了半天没想到怎么描述,我个人的理解就是,挂载更新,就是prevVNode 和 nextVNode 是如何进行对比的

我们现在将VNode只分为了两类:

  1. 元素节点

  2. 文本节点

那么 prevVNode 和 nextVNode 可能出现的情况只会有以下三种:

  1. 二者类型不同

  2. 二者都是文本节点

  3. 二者都是元素节点,且标签相同

当二者类型不同时,只需删除原节点,挂载新节点即可:

function patch (prevVNode, nextVNode, container) {
removeChild(container, prevVNode.el);
mount(nextVNode, container);
}

当二者都是文本节点时,只需修改文本即可

function patch (prevVNode, nextVNode, container) {
const el = (nextVNode.el = prevVNode.el)
if(nextVNode.children !== prevVNode.children){
el.nodeValue = nextVNode.children;
}
}

当二者都是元素节点且标签相同时,此时比较麻烦,考虑是一个patchElement函数用于处理此种情况

function patch (prevVNode, nextVNode, container) {
patchElement(prevVNode, nextVNode, container)
}

最终 patch 函数的代码如下:

function patch (prevVNode, nextVNode, container) {
// 类型不同,直接替换
if ((prevVNode.tag || nextVNode.tag) && prevVNode.tag !== nextVNode.tag) {
removeChild(container, prevVNode.el);
mount(nextVNode, container);
}
// 都是文本
else if(!prevVNode.tag && !nextVNode.tag){
const el = (nextVNode.el = prevVNode.el)
if(nextVNode.children !== prevVNode.children){
el.nodeValue = nextVNode.children;
}
}
// 都是相同类型的元素
else {
patchElement(prevVNode, nextVNode, container)
}
}

比较相同tag的VNode(patchElement)

因为tag相同,所以patchElement函数的功能主要有两个:

  1. 检查prevVNode和nextVNode对应的元素属性是否一致(style、class、event等),不一致更新

  2. 比较prevVNode和nextVNode对应的子节点(children)

关于元素属性的比较与挂载阶段的逻辑基本一致,就不在此继续展开,我们主要考虑如何对子节点进行比较

子节点可能出现的情况有三种:

  1. 没有子节点

  2. 一个子节点

  3. 多个子节点

所以关于prevVNode和nextVNode子节点的比较,共有9种情况:

  1. 旧:单个子节点 && 新:单个子节点

  2. 旧:单个子节点 && 新:没有子节点

  3. 旧:单个子节点 && 新:多个子节点

  4. 旧:没有子节点 && 新:单个子节点

  5. 旧:没有子节点 && 新:没有子节点

  6. 旧:没有子节点 && 新:多个子节点

  7. 旧:多个子节点 && 新:单个子节点

  8. 旧:多个子节点 && 新:没有子节点

  9. 旧:多个子节点 && 新:多个子节点

前8中情况都比较简单,这里简单概括一下:

1.旧:单个子节点 && 新:单个子节点

都为单个子节点,递归调用patch函数

2.旧:单个子节点 && 新:没有子节点

删除旧子节点对应的DOM

3.旧:单个子节点 && 新:多个子节点

删除旧子节点对应的DOM,并将多个新子节点依次递归调用mount函数进行挂载即可

4.旧:没有子节点 && 新:单个子节点

直接调用mount函数疆新单个子节点进行挂载即可

5.旧:没有子节点 && 新:没有子节点

什么也不做

6.旧:没有子节点 && 新:多个子节点

将多个新子节点依次递归调用mount函数进行挂载即可

7.旧:多个子节点 && 新:单个子节点

删除多个旧子节点对应的DOM,递归调用mount函数对单个新子节点进行挂载即可

8.旧:多个子节点 && 新:没有子节点

删除多个旧子节点对应的DOM即可

9.旧:多个子节点 && 新:多个子节点

对于新旧子节点均为多个子节点的情况,是VNode更新阶段最复杂的情况,无论是React还是Vue都有不同的实现方案,这些实现方案也就是我们常说的Diff算法。

今天先不涉及比较复杂的Diff算法,关于Diff算法的内容,留到日后进行讲解,我们先通过最简单的方式来实现多个新旧子节点的更新(性能最差的做法)。

遍历旧的子节点,将其全部移除:

for (let i = 0; i < prevChildren.length; i++) {
removeChild(container,prevChildren[i].el)
}

遍历新的子节点,将其全部挂载

for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container)
}

最终的代码如下:

export const patchElement = function (prevVNode, nextVNode, container) { 

    const el = (nextVNode.el = prevVNode.el); 

    const prevData = prevVNode.data;
const nextData = nextVNode.data; if (nextData) {
for (let key in nextData) {
let prevValue = prevData[key];
let nextValue = nextData[key];
patchData(el, key, prevValue, nextValue);
}
}
if (prevData) {
for (let key in prevData) {
let prevValue = prevData[key];
if (prevValue && !nextData.hasOwnProperty(key)) {
patchData(el, key, prevValue, null);
}
}
}
//比较子节点
patchChildren(
prevVNode.children,
nextVNode.children,
el
)
} function patchChildren(prevChildren, nextChildren, container) {
//旧:单个子节点
if(prevChildren && !Array.isArray(prevChildren)){
//新:单个子节点
if(nextChildren && !Array.isArray(nextChildren)){
patch(prevChildren,nextChildren,container)
}
//新:没有子节点
else if(!nextChildren){
removeChild(container,prevChildren.el)
}
//新:多个子节点
else{
removeChild(container,prevChildren.el)
for(let i = 0; i<nextChildren.length; i++){
mount(nextChildren[i], container)
}
}
}
//旧:没有子节点
else if(!prevChildren){
//新:单个子节点
if(nextChildren && !Array.isArray(nextChildren)){
mount(nextChildren, container)
}
//新:没有子节点
else if(!nextChildren){
//什么都不做
}
//新:多个子节点
else{
for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container)
}
}
}
//旧:多个子节点
else {
//新:单个子节点
if(nextChildren && !Array.isArray(nextChildren)){
for(let i = 0; i<prevChildren.length; i++){
removeChild(container,prevChildren[i].el)
}
mount(nextChildren,container)
}
//新:没有子节点
else if(!nextChildren){
for(let i = 0; i<prevChildren.length; i++){
removeChild(container,prevChildren[i].el)
}
}
//新:多个子节点
else{
// 遍历旧的子节点,将其全部移除
for (let i = 0; i < prevChildren.length; i++) {
removeChild(container,prevChildren[i].el)
}
// 遍历新的子节点,将其全部添加
for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container)
}
}
} }

此文参考:

冰山工作室 http://www.bingshangroup.com/blog2/action2/jspool%EF%BC%9A%E9%99%88%E5%85%B6%E4%B8%B0/VNode2.html

虚拟DOM学习与总结的更多相关文章

  1. vue 源码学习三 vue中如何生成虚拟DOM

    vm._render 生成虚拟dom 我们知道在挂载过程中, $mount 会调用 vm._update和vm._render 方法,vm._updata是负责把VNode渲染成真正的DOM,vm._ ...

  2. vue虚拟DOM源码学习-vnode的挂载和更新流程

    代码如下: <div id="app"> {{someVar}} </div> <script type="text/javascript& ...

  3. React生命周期和虚拟DOM

    一.虚拟DOM 1.React并不直接操作DOM,React中的render方法,返回一个DOM描述,React能够将这个DOM描述与内存中的表现进行比较,然后以最快的方式更新浏览器 2.React实 ...

  4. 虚拟DOM详解

    虚拟DOM简介 Virtual Dom可以看做一棵模拟了DOM树的JavaScript对象树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发Virtual Dom数据的变 ...

  5. JavaScript是如何工作的:编写自己的Web开发框架 + React及其虚拟DOM原理

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第 19 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...

  6. Virtual DOM 虚拟DOM的理解(转)

    作者:戴嘉华 转载请注明出处并保留原文链接( #13 )和作者信息. 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现 4.1 步骤一:用JS对象模拟DOM ...

  7. 解密虚拟 DOM——snabbdom 核心源码解读

    本文源码地址:https://github.com/zhongdeming428/snabbdom 对很多人而言,虚拟 DOM 都是一个很高大上而且远不可及的专有名词,以前我也这么认为,后来在学习 V ...

  8. 如何编写自己的虚拟DOM

    要构建自己的虚拟DOM,需要知道两件事.你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂--但实际上,虚拟DOM的主要部分只需不到50行代码. 有 ...

  9. 简谈react中的虚拟DOM

    相信你在看到此篇前也翻阅大量的对DOM的文章讲解和介绍 react中的虚拟DOM 此篇我尽量说人话(大白话),不然想必你在看到别的大神的文章早就懂了. 不说废话了,上干货. 1.首先简单对Html中的 ...

随机推荐

  1. 2018-2-13-WPF-获得触笔悬停元素上

    title author date CreateTime categories WPF 获得触笔悬停元素上 lindexi 2018-2-13 17:23:3 +0800 2018-2-13 17:2 ...

  2. html实体字符转换成字符串

    function EntityToString(value) { let tag = document.createElement("div"); tag.innerHTML = ...

  3. EC round 33 D. Credit Card 贪心

    因为到为0的点,充钱的范围都是不确定的,我们维护一个满足条件的最小值以及满足条件的最大值. 当min>d时,代表已经满足条件限制了 当a[ i ] = 0 并且 max<0,代表需要充钱, ...

  4. Win7如何显示文件后缀

    有些时候,我们需要修改文件的后缀名,但是Windows7系统默认不显示文件后缀.我们怎样显示和修改文件后缀呢?请接着往下看. 工具/原料   一个win7系统 方法/步骤   1 如图所示,此时是无法 ...

  5. phpstudy一直使用php5.6版本一直“”“报错应用程序无法正常启动0xc000007b”,亲测可行

    http://www.php.cn/xiazai/gongju/1351 vc9和vc11-vc14运行库 2018-01-26 来源/作者:php中文网 «» 下载次数:7808 工具简介: php ...

  6. 第一次作业:C++ 函数重载

    函数重载 函数重载是在C语言的学习中未涉及的新概念.我们在编程时经常会遇到这样一个问题:我们编写完一个函数准备准备调用时,一旦需要传入不同的数据类型的参数时,一个函数无法实现,我们又必须重写另一个或者 ...

  7. 很奇怪的问题(Chrome)

    <p>感觉这个问题跟Chrome浏览器本身的一些策略有关.</p> 在我写完登录页面后 准备美滋滋的登录验证下的时候,确一直卡在数据获取上了,查看NetWork,发现是Chro ...

  8. form组件类 钩子函数验证

    # 全局钩子 def clean(self): pwd = self.cleaned_data.get("password") re_pwd = self.cleaned_data ...

  9. Python--day19--os模块

    os模块 os模块是与操作系统交互的一个接口 os.makedirs('dirname1/dirname2') 可生成多层递归目录 os.removedirs('dirname1') 若目录为空,则删 ...

  10. python基础三之字符串

    Python的数据类型 数字(int),如1,2,3,用于计算. 字符串(str),如s = 'zxc',储存少量数据,进行操作. 布尔值(bool),True和False,用于进行判断. 列表(li ...