虚拟DOM
1、新建react项目
//全局安装脚手架工具
$ npm install create-react-app -g
//创建项目
$ create-react-app dom-diff
// 进入项目目录
$ cd dom-diff
// 编译
$ npm run start
2、定义虚拟DOM类
什么是虚拟DOM? 虚拟DOM就是使用javascript对象来表示真实DOM,是一个树形结构。
这个对象结构如下:
const virtualDom = {
type: 'ul',
props: {
class: 'lists'
},
children: [{
type: 'li',
props: {},
children: [1]
},
{
type: 'li',
props: {},
children: [2]
},
{
type: 'li',
props: {},
children: [3]
}
]
}
为了构建这样的对象,我们需要新建一个 element.js 文件。
- 需要定义对象,用来描述虚拟dom
class Element{
constructor(type,props,childrens){
this.type= type
this.props = props
this.childrens= childrens
}
- }
提供个方法,生成element对象
function createElement(type,props,children){
return new Element(type,props,children)
}
element.js
全部代码
/**
* 定义虚拟dom对象
*/
class Element{
constructor(type,props,childrens){
this.type= type
this.props = props
this.childrens= childrens
}
}
/**
* 生成虚拟dom
* @param {*} type 元素类型
* @param {*} props 元素属性
* @param {*} children 子元素
*/
function createElement(type,props,children){
return new Element(type,props,children)
}
3、构建虚拟DOM
在 index.js 文件中
import {createElement} from './element'
let virtualDom = createElement('ul',{className:'lists'},[
createElement('li',{},[1]),
createElement('li',{},[2]),
createElement('li',{},[3])
])
console.log(virtualDom)
变量 virtualDom 在控制台的打印结果:
4、虚拟DOM生成真实DOM
我们已经生成了构建出了虚拟dom对象,下一步就是将这个对象渲染成真实的dom对象。
我们在 element.js 文件中新增一个render方法。
/**
* 将虚拟dom转化成真实dom
* @param {Element} virtualDom 虚拟dom
*/
function render(virtualDom){
if(!virtualDom) throw new Error('传入的虚拟dom为空')
// 根据type类型来创建对应的元素
let el = document.createElement(virtualDom.type)
//遍历虚拟dom的属性
for(let attrKey in virtualDom.props){
const attrValue = virtualDom.props[attrKey]
el.setAttribute(attrKey,attrValue)
}
//处理子元素
if(virtualDom.childrens){
for(let child of virtualDom.childrens){
//判断是不是element类型。
if(child instanceof Element){
//递归
el.appendChild(render(child))
}else{
//如果不是element,则证明是文本节点
el.appendChild(document.createTextNode(child))
}
}
}
return el
}
5、将DOM元素渲染到页面上
在element.js 中新增一个方法:renderElement(node,element)
//...省略其他代码
/**
* 将构建好的真实dom插入到目标元素里
* @param {*} el 目标元素
* @param {*} dom 真实dom
*/
function renderElement(el,dom){
el.appendChild(dom)
}
export {Element,createElement,render,renderElement}
在 index.js 页面操作,将虚拟dom转成真实dom,再添加到id为root的节点下面
import {createElement,render,renderElement} from './element'
let virtualDom = createElement('ul',{class:'lists'},[
createElement('li',{},[1]),
createElement('li',{},[2]),
createElement('li',{},[3])
])
//将虚拟dom转成真实dom
let actualDom = render(virtualDom)
//将真实dom渲染到页面上
renderElement(document.getElementById(‘root’),actualDom)
界面效果
DOM-DIFF
- dom-diff 三种优化策略
- 更新的时候只比较平级虚拟节点,依次进行比较并不会跨级比较,比较差异部分,生成对应补丁包,根据补丁包改变的内容更新差异的dom。
- 更新的时候平级比较两个虚拟DOM,当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。如果该删除的节点之下有子节点,那么这些子节点也会被完全删除,它们也不会用于后面的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。新增的节点,会对应的创建一个新的节点。
- 第三种更新的时候平级比较两个虚拟DOM,如果只是一层变了,互换了位置,那么它会复用此虚拟节点,把对应的位置互换一下即可,这个是通过给对应元素添加的key不同来实现的。
-
前置知识
树的先序深度遍历
按照 “根-左-右” 的顺序进行遍历 -
代码实现
3.1 根据虚拟dom差异得到补丁对象
import {
isString
}
from './utils/types.js'
const ATTRS = 'ATTRS'const REPLACE = 'REPLACE'const REMOVE = 'REMOVE'const TEXT = 'TEXT'
//todo
//const ADD ='ADD'
/**
*
* @param {*} newDom 新节点
* @param {*} oldDom 老节点
*/
// 所有都基于一个序号来实现
let num = 0
function diff(oldDom, newDom) {
const patches = {}
num = 0 walk(oldDom, newDom, num, patches) return patches
}
/**
* 比较子节点
* @param {*} oldChilds
* @param {*} newChilds
*/
function diffChildren(oldChilds, newChilds, patches) {
// 比较老的第一个和新的第一个
oldChilds.forEach((old, idx) = >{
walk(old, newChilds[idx], ++num, patches)
});
}
function walk(oldNode, newNode, index, patches) {
//每个元素都有一个补丁
let currentPatch = []
//是否是文本节点
if (isString(oldNode) && isString(newNode)) {
if (oldNode !== newNode) {
currentPatch.push({
type: TEXT,
text: newNode
})
}
}
/**是否新节点为空:被删除 */
else if (!newNode) {
currentPatch.push({
type: REMOVE
})
} //类型是否一致
else if (oldNode.type === newNode.type) {
//比较属性
let attrPatch = diffAttrs(oldNode.props, newNode.props)
//存在属性差异
if (Object.keys(attrPatch).length > 0) {
currentPatch.push({
type: ATTRS,
attr: attrPatch
})
}
//比较子节点
diffChildren(oldNode.childrens, newNode.childrens, patches)
} else {
//节点被替换
currentPatch.push({
type: REPLACE,
node: newNode
})
}
if (currentPatch.length > 0) {
patches[index] = currentPatch
}
}
/**
* 比较属性
*/
function diffAttrs(oldProps, newProps) {
const patch = {}
//先遍历老属性,比较与新属性的差异
for (let key in oldProps) {
if (oldProps[key] !== newProps[key]) {
patch[key] = newProps[key]
}
}
//再遍历新属性,看是否有新增加的属性
for (let key in newProps) {
//如果旧属性里没有该值
if (!oldProps.hasOwnProperty(key)) {
patch[key] = newProps[key]
}
}
return patch
}
export
3.2 根据补丁对象,更新dom
import {
render
} from './element'
let allPatches;
let index = 0
function patch(node, patches) {
allPatches = patches
index = 0
walk(node)
}
const ATTRS = 'ATTRS'
const REPLACE = 'REPLACE'
const REMOVE = 'REMOVE'
const TEXT = 'TEXT'
/**
* 开始循环打补丁
*/
function walk(node) {
let current = allPatches[index++]
let childNodes = node.childNodes;
// 先序深度,继续遍历递归子节点
childNodes.forEach(child => walk(child));
if (current) {
doPatch(node, current);
}
}
function doPatch(node, patches) {
//同一个元素上可能打了多个补丁
patches.forEach(current => {
switch (current.type) {
case ATTRS:
const attrs = current.attr
for (let key in attrs) {
node.setAttribute(key, attrs[key])
}
break
case TEXT:
node.textContent = current.text
break;
case REMOVE:
node.parentNode.removeChild(node)
break;
case REPLACE:
let newNode = current.node
if (newNode instanceof Element) {
node.parentNode.replaceChild(render(newNode), node)
} else {
node.parentNode.replaceChild(document.createTextNode(newNode), node)
}
break;
default:
break;
}
})
}
export default patch
3.3. 模拟数据变化,触发重新渲染
import {
createElement,
render,
renderElement
} from './element'
import './index.css'
import diff from './diff'
import patch from './patch'
let virtualDom1 = createElement('ul', {
style: 'background:red;color:#fff;'
}, [
createElement('li', {}, ["2"]),
createElement('li', {}, ["2"]),
createElement('li', {}, ["3"])
])
//将虚拟dom转成真实dom
let actualDom = render(virtualDom1)
//将真实dom渲染到页面上
renderElement(document.getElementById('root'), actualDom)
//模拟数据变化
let virtualDom2 = createElement('ul', {
style: 'color:red;'
}, [
createElement('li', {}, ["2"]),
createElement('li', {}, ["2"]),
createElement('li', {}, ["4"]),
])
const patchs = diff(virtualDom1, virtualDom2)
patch(actualDom, patchs)