虚拟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. 远程监控JVM

    设置tomcat中catalina.sh设置JAVA_OPTS= JAVA_OPTS="-server -Xms595M -Xmx595M -Xmn223M -XX:SurvivorRati ...

  2. iptables 详细使用

    检查状态 先检查是否安装了iptables $ service iptables status 安装iptables $ yum install iptables 升级iptables $ yum u ...

  3. Helm V3 新版本发布

    Helm v3.0.0 Alpha 1 is coming! Helm 作为 Kubernetes 体系的包管理工具,已经逐渐成为了事实上的应用分发标准.根据 2018 年 CNCF 的一项云原生用户 ...

  4. 2016国产开源软件Top100(Q1)

    2016国产开源软件Top100(Q1) 随着互联网的发展.开放标准的普及和虚拟化技术的应用等诸多IT新领域的创新及拓展,开源技术凭借其开放性.低成本.稳定性.灵活性.安全性和技术创新性等特点迅速走向 ...

  5. Git的提交与查看差异

    本文转载于:http://blog.csdn.net/crylearner/article/details/7685158 代码提交 代码提交一般有五个步骤: 1.查看目前代码的修改状态 2.查看代码 ...

  6. svcs (service status) 和 svcadm (service administration) 使用

    1. svcs  显示服务实例的状态信息 svcs - report service status  显示服务状态命令 DESCRIPTION The svcs command displays in ...

  7. Springboot 自定义多个404页面

    在Springboot中,可以通过修改配置.或者在static文件夹下添加error文件夹引入个性化的404模版.但是如果需要针对不同url地址规则,返回不同样式的404页面,则难以实现了.针对这个问 ...

  8. webpack学习(三)配置loader

    首先搞清楚两个问题: 1 什么是loader? 2 为啥要用各种loader 答: loader 就是各种打包规则,为什么要用是显而易见的,因为webpack还没智能到给它什么文件都能打包,对于js文 ...

  9. H3C 调试OSPF

  10. css 百分比继承关系的探讨

    引入:近日在回顾css基础的时候发现了一个有趣的问题,就是css在继承百分比类属性的时候是继承百分比后再根据父级宽高计算,还是继承父级根据百分比计算过后的绝对值.下面举一个简单的例子来描述这一个问题, ...