虚拟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中的 ...
随机推荐
- POJ1664 放苹果
#include <iostream> #include <cstdio> #include <cstring> using namespace std; int ...
- 2019-8-31-dotnet-通过-WMI-获取设备厂商
title author date CreateTime categories dotnet 通过 WMI 获取设备厂商 lindexi 2019-08-31 16:55:59 +0800 2019- ...
- 06Redis入门指南笔记(安全、通信协议、管理工具)
一:安全 1:可信的环境 Redis以简洁为美.在安全层面Redis也没有做太多的工作.Redis的安全设计是在"Redis运行在可信环境"这个前提下做出的.在生产环境运行时不能允 ...
- mysql数据库之windows版本
安装 第一步:打开网址,http://www.mysql.com.点击downloads之后跳转到http://www.mysql.com/downloads/选择Community选项 第二步:按 ...
- STS Eclipse IDEA 指定启动JDK版本
使用场景: 开发人员在自己的机器上可能装了多个版本的JDK,但是在环境变量中只能配置一个 JAVA_HOME ,so你的IDEA Eclipse 可能因为你在 JAVA_HOME 配置JDK1.8 以 ...
- Java中Map/List/Set .
很实用,分享一下. 简单版本 复杂版本 参考: http://initbinder.com/articles/cheat-sheet-for-selecting-maplistset-in-java. ...
- Flex AIR使用ADT命令打包 ipa
1. 配置环境变量. 2. 测试adt命令 3. 将ShepherdPhone0815.mobileprovision和 my.p12文件都放入编译好的工程目录下,如下图: 4.切换到上述编译好的目录 ...
- Flex AIR应用的启动闪屏(必须)
说明: 一款移动应用,它必须具有启动屏幕,这点可以从我们常见的手机应用观察知道(如,你启动一个QQ,开始大约10秒钟会停留在一个界面上,之后才跳转到登陆或者是主界面). 在air移动应用中,如果不添加 ...
- P1100 三连击
题目描述 我们假设一个三位整数 \(N(100 \le N \le 999)\) ,它的百位上的数字是 \(A\) ,十位上的数字是 \(B\) ,个位上的数字是 \(C\) ,如果 \(A\) , ...
- 【js】 vue 2.5.1 源码学习(十二)模板编译
大体思路(十) 本节内容: 1. baseoptions 参数分析 2. options 参数分析 3. parse 编译器 4. parseHTNL 函数解析 // parse 解析 parser- ...