什么是虚拟DOM?
一个表示UI的javascript对象。
如何创建虚拟DOM? 如何实现DOM Diff算法?
- 定义Element的基类,即用于创建一个虚拟DOM节点。
class Element {
constructor(type,props,children){
this.type = type
this.props = props
this.children = children
}
}
export default Element
- 定义创建虚拟DOM的方法。
export function createElement(type, props, children) {
return new Element(type, props, children)
}
- 创建虚拟DOM。
const vDom = createElement('ul', {
class: 'list',
style:'width:300px;height:300px'
}, [
createElement('li', {
class: 'item',
'data-id':'0'
}, [
createElement('p', {
class:'text'
}, [
'第1个列表项'
])
]),
createElement('li', {
class: 'item',
'data-id':'1'
}, [
createElement('p', {
class:'text'
}, [
createElement('span', {
class:'title'
}, [
'第2个列表项'
])
])
]),
createElement('li', {
class: 'item',
'data-id':'2'
}, [
'第3个列表项'
]),
])
- 控制台打印虚拟DOM。
- 有了虚拟DOM,如何将虚拟DOM转为真实DOM? A : 需要实现render方法。
// render 方法接受一个虚拟dom,并转化为真实dom
export function render(vDom) {
const { type, props , children} = vDom
const el = document.createElement(type)
for (let key in props){
setAttrs(el,key,props[key])
}
children.map((c) => {
// children中的元素可能是一个节点,也有可能是一串文本
if (c instanceof Element) {
c = render(c)
} else {
c = document.createTextNode(c)
}
el.appendChild(c)
})
return el
}
// setAttrs 方法负责辅助向dom节点中添加属性,原因在于有的dom节点添加属性的方式是不同的,可以去了解一下property和attribute有什么不同就能理解这一点。
// 给节点设置属性
export function setAttrs(node,prop,value) {
switch (prop) {
case 'value':
if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
node[prop] = value
} else {
node.setAttribute(prop, value)
}
break;
case 'style':
node.style.cssText = value
break
default:
node.setAttribute(prop,value)
}
}
- 将真实DOM应用于页面,使得在页面上进行显示。
// 负责将真实DOM挂载到页面当中
// 在页面当中渲染真实DOM
renderDOM(rDom, document.getElementById('app'))
export function renderDOM(rDom,rootEl) {
rootEl.appendChild(rDom)
}
- 实现DOM Diff算法,使得可以对比两个虚拟DOM的差异,生成补丁包patches。
// 全局的补丁包
const patches = {}
// 控制深度的index
let vnIndex = 0
export default function domDiff(oldVdom, newVdom) {
let index = 0
vNodeWalk(oldVdom,newVdom,index)
return patches
}
function vNodeWalk(oldNode,newNode,index) {
let vnPatch = []
if (!newNode) {
// 新节点不存在,表明这是移除节点
vnPatch.push({
type: REMOVE,
index
})
} else if (typeof oldNode === "string" && typeof newNode === "string") {
// 新旧节点都是文本节点
if (oldNode !== newNode) {
vnPatch.push({
type: TEXT,
text:newNode
})
}
} else if (oldNode.type === newNode.type) {
// 新旧节点都是普通的节点对象
// 获得属性补丁
const attrPatch = attrsWalk(oldNode.props, newNode.props)
if (Object.keys(attrPatch).length > 0) {
vnPatch.push({
type: ATTR,
attrs:attrPatch
})
}
childrenWalk(newNode.children,oldNode.children)
} else {
vnPatch.push({
type: REPLACE,
newNode
})
}
if (vnPatch.length > 0) {
patches[index] = vnPatch
}
}
function attrsWalk(oldAttrs,newAttrs) {
let attrsPatch = {}
for (let key in oldAttrs){
if (oldAttrs[key] !== newAttrs[key]) {
attrsPatch[key] = newAttrs[key]
}
}
for (let key in newAttrs){
if (!oldAttrs.hasOwnProperty(key)) {
attrsPatch[key] = newAttrs[key]
}
}
return attrsPatch
}
function childrenWalk(oldChildren,newChildren) {
oldChildren.map((c, idx) => {
vNodeWalk(c,newChildren[idx],++ vnIndex)
})
}
- 根据生成的补丁包,对真实DOM进行遍历并执行打补丁的动作,使得真实DOM的更新最小化。
// 计算出补丁包
const patches = domDiff(vDom1, vDom2)
let finalPatches = {}
let rnIndex = 0
function doPatch(rDom,patches) {
finalPatches = patches
rNodeWalk(rDom)
}
function rNodeWalk(rNode) {
// 获取到具体真实dom节点的 补丁包
const rnPatch = finalPatches[rnIndex++]
// 获取该真实dom节点的子节点 (类数组)
const childNodes = [...rNode.childNodes]
childNodes.map((c) => {
rNodeWalk(c)
})
// 当前节点的补丁包存在
if (rnPatch) {
patchAction(rNode,rnPatch)
}
}
function patchAction(rNode, rnPatch) {
rnPatch.map((p) => {
switch (p.type) {
case ATTR:
for (let key in p.attrs){
const value = p.attrs[key]
if (value) {
setAttrs(rNode,key,value)
} else {
rNode.removeAttribute(key)
}
}
break
case TEXT:
rNode.textContent = p.text
break
case REPLACE:
const newNode = p.newNode instanceof Element ?
render(p.newNode)
:
document.createTextNode(p.newNode)
rNode.parentNode.replaceChild(newNode,rNode)
break
case REMOVE:
rNode.parentNode.removeChild(rNode)
break
default:
break
}
})
}
// 应用于真实DOM上,从而最小化操作真实DOM
doPatch(rDom,patches)
- 辅助文件,actionType文件 (与Redux类似)
export const ATTR = 'ATTR'
export const TEXT = 'TEXT'
export const REPLACE = 'REPLACE'
export const REMOVE = 'REMOVE'