虚拟DOM学习与总结
虚拟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)
我们在更新的时候,又分为两种情况:
prevVNode和nextVNode都有,执行比较操作
有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只分为了两类:
元素节点
文本节点
那么 prevVNode 和 nextVNode 可能出现的情况只会有以下三种:
二者类型不同
二者都是文本节点
二者都是元素节点,且标签相同
当二者类型不同时,只需删除原节点,挂载新节点即可:
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函数的功能主要有两个:
检查prevVNode和nextVNode对应的元素属性是否一致(style、class、event等),不一致更新
比较prevVNode和nextVNode对应的子节点(children)
关于元素属性的比较与挂载阶段的逻辑基本一致,就不在此继续展开,我们主要考虑如何对子节点进行比较
子节点可能出现的情况有三种:
没有子节点
一个子节点
多个子节点
所以关于prevVNode和nextVNode子节点的比较,共有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学习与总结的更多相关文章
- vue 源码学习三 vue中如何生成虚拟DOM
vm._render 生成虚拟dom 我们知道在挂载过程中, $mount 会调用 vm._update和vm._render 方法,vm._updata是负责把VNode渲染成真正的DOM,vm._ ...
- vue虚拟DOM源码学习-vnode的挂载和更新流程
代码如下: <div id="app"> {{someVar}} </div> <script type="text/javascript& ...
- React生命周期和虚拟DOM
一.虚拟DOM 1.React并不直接操作DOM,React中的render方法,返回一个DOM描述,React能够将这个DOM描述与内存中的表现进行比较,然后以最快的方式更新浏览器 2.React实 ...
- 虚拟DOM详解
虚拟DOM简介 Virtual Dom可以看做一棵模拟了DOM树的JavaScript对象树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发Virtual Dom数据的变 ...
- JavaScript是如何工作的:编写自己的Web开发框架 + React及其虚拟DOM原理
这是专门探索 JavaScript 及其所构建的组件的系列文章的第 19 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...
- Virtual DOM 虚拟DOM的理解(转)
作者:戴嘉华 转载请注明出处并保留原文链接( #13 )和作者信息. 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现 4.1 步骤一:用JS对象模拟DOM ...
- 解密虚拟 DOM——snabbdom 核心源码解读
本文源码地址:https://github.com/zhongdeming428/snabbdom 对很多人而言,虚拟 DOM 都是一个很高大上而且远不可及的专有名词,以前我也这么认为,后来在学习 V ...
- 如何编写自己的虚拟DOM
要构建自己的虚拟DOM,需要知道两件事.你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂--但实际上,虚拟DOM的主要部分只需不到50行代码. 有 ...
- 简谈react中的虚拟DOM
相信你在看到此篇前也翻阅大量的对DOM的文章讲解和介绍 react中的虚拟DOM 此篇我尽量说人话(大白话),不然想必你在看到别的大神的文章早就懂了. 不说废话了,上干货. 1.首先简单对Html中的 ...
随机推荐
- 一些关于中国剩余定理的数论题(POJ 2891/HDU 3579/HDU 1573/HDU 1930)
2891 -- Strange Way to Express Integers import java.math.BigInteger; import java.util.Scanner; publi ...
- 使用sqlyog链接多个主机的数据库
- LRJ 3-7
#define _CRT_SECURE_NO_WARNINGS #include <cstdio> int main() { int T; int m, n; ][]; // 4 < ...
- 高级教程: 作出动态决策和 Bi-LSTM CRF 重点
动态 VS 静态深度学习工具集 Pytorch 是一个 动态 神经网络工具包. 另一个动态工具包的例子是 Dynet (我之所以提这个是因为使用 Pytorch 和 Dynet 是十分类似的. 如果你 ...
- Javassist指引(二)--ClassPool
原文链接 上一章: Javassist指引(一) 2.ClassPool ClassPool是一个CtClass的容器.因为编译器随时可能访问一个CtClass类,所以一旦一个CtClass创建,它将 ...
- Navicat for MySQL 使用SSH方式链接远程数据库(二)
这里我们使用SSH连接远程mysql数据库 2 SSH这种方式,可以使我们连接到远程服务器,但是现在并不能访问数据库,因为我们还没有连接到数据库 3 既然已经连接到服务器了,我们就该连接服务器上的数据 ...
- Vue的事件修饰符
转载于:https://www.cnblogs.com/xuqp/p/9406971.html 事件处理 如果需要在内联语句处理器中访问原生DOM事件.可以使用特殊变量$event,把它传入到meth ...
- HDU 1828“Picture”(线段树+扫描线求矩形周长并)
传送门 •参考资料 [1]:算法总结:[线段树+扫描线]&矩形覆盖求面积/周长问题(HDU 1542/HDU 1828) •题意 给你 n 个矩形,求矩形并的周长: •题解1(两次扫描线) 周 ...
- 【js】vue 2.5.1 源码学习 (十一) 模板编译compileToFunctions渲染函数
大体思路(九) 本节内容: 1. compileToFunctions定位 1. compileToFunctions定位 ==> createCompiler = createCompiler ...
- java 一个类加载器的高级问题分析
编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassloader. 把MyServlet.class文件打jar包,放 ...