为什么需要虚拟Dom
在不使用虚拟dom的情况下,修改一个节点会引起整个页面的重绘。比如又一个元素进行了修改(删除),剩余的9个元素都需要加载重绘。虚拟Dom就是有一个虚拟的节点树和原有节点数进行比对,做到了更小量更新元素更新。
main.js
import { createElement, render,renderDom } from './element'
import diff from './diff'
import patch from './patch';
// 旧节点
let myDom = createElement('div',{class:'container',style:'color:red'},
[
createElement('p',{class:'item'},'child1'),
createElement('p',{class:'item'},'child2'),
createElement('p',{class:'item'},'child3'),
createElement('input',{class:'item',value:'I am input'})
])
// 新节点
let myDom2 = createElement('div',{class:'container'},
[
createElement('p',{class:'item'},'child1'),
createElement('p',{class:'item'},'child2'),
createElement('p',{class:'item'},'child3'),
createElement('p',{class:'item'},'child4')
])
// 比较新旧节点状态,返回补丁
let patches = diff(myDom,myDom2)
// 渲染旧的节点
let el = render(myDom)
patch(el,patches)
renderDom(document.getElementById('app'),el)
diff.js
let _index = 0
function diff(oldNode,newNode){
// diff 算法的本质就是打补丁,如果有需要更改的元素就设置相应的补丁
// 同级、同位置进行比较,不会出现越级比较
// 存储当前修改的元素地址和修改类型
let patches ={}
// index 下标,用来记录修改元素的下标
let index = 0
setPatch(oldNode,newNode,index,patches)
return patches
}
// 设置类型,补丁类型
const ATTRS='ATTRS' // 修改或者删除该元素属性
const TEXT='TEXT' // 节点文本修改或者删除
const REMOV='REMOV' // 删除节点
const REPLACE='REPLACE' //替换节点
function setPatch(oldNode,newNode,index,patches){
// 存储需要打补丁的类型
let currentPatch = []
if(!newNode){
// 如果没有新节点,就说明此时的旧节点是删除状态
currentPatch.push({type:REMOV,_index})
if(currentPatch.length>0){
patches[_index]= currentPatch
}
}else if (isString(oldNode)&& isString(newNode)){
// 判断是否一致
if (oldNode !== newNode){
currentPatch.push({type:TEXT,text:newNode})
}
if(currentPatch.length>0){
patches[_index]= currentPatch
}
}
else if(newNode.type === oldNode.type){
// 如果节点类型都相同,那就比较节点属性是否相同
let attrs = diffAttr(oldNode.props,newNode.props)
// attrs 返回内容,说明需要补丁
if(Object.keys(attrs).length>0){
currentPatch.push({type:ATTRS,attrs,index})
}
if (currentPatch.length > 0) {
patches[_index] = currentPatch
}
// 递归子节点
diffChild(oldNode.children,newNode.children,index,patches)
}else{
// 节点替换
currentPatch.push({ type: REPLACE, newNode })
if (currentPatch.length > 0) {
patches[_index] = currentPatch
}
}
}
function isString(node){
return Object.prototype.toString.call(node) ==="[object String]"
}
function diffChild(oldChild,newChild,index,patches){
// 该函数的功能是递归每一个子节点,然后进行diff补丁
if(Array.isArray(oldChild)){
oldChild.forEach((child,idx) => {
setPatch(child,newChild[idx],++_index,patches)
});
}else{
setPatch(oldChild,newChild,++_index,patches)
}
}
function diffAttr(oldAttrs,newAttrs){
// 函数的功能是判断当前的新旧节点的属性是否相同,如果不同,新的节点属性覆盖旧的节点属性
// 维护的属性的补丁
let patch = {}
//判断旧的节点是否和新的相同,如果不同新的替换旧的
for(let key in oldAttrs){
if(oldAttrs[key] !== newAttrs[key]){
patch[key] = newAttrs[key]
}
}
for(let key in newAttrs){
// 新的节点属性如果在旧的节点属性中不存在就补丁更新
if(!oldAttrs.hasOwnProperty(key)){
patch[key] = newAttrs[key]
}
}
return patch
}
export default diff;
patch.js
import { render, Element } from './element'
let allPatches;
let index = 0;
function patch(node, patches) {
allPatches = patches
setDom(node)
}
function setDom(node) {
// 修改补丁及节点
let currentPatch = allPatches[index++]
let childNodes = node.childNodes
// 递归补丁
childNodes.forEach(item => setDom(item));
if (currentPatch) {
// 如果有补丁,打补丁
doPatch(node, currentPatch)
}
}
function doPatch(node, patches) {
// 打补丁的过程
patches.forEach(patch => {
console.log('~~~~~patch', patch)
switch (patch.type) {
case 'ATTRS':
for (const key in patch.attrs) {
let value = patch.attrs[key]
// 有值添加该属性
if (value) {
node.setAttribute(key, value)
} else {
node.removeAttribute(key)
}
}
break;
case 'TEXT':
node.textContent = patch.text
break;
case 'REMOV':
console.log('REMOV',node.parentNode,node)
node.parentNode.removeChild(node)
break;
case 'REPLACE':
// 替换节点,如果是文本节点,就追加文本节点,否则就替换元素节点
let newNode = patch.newNode instanceof Element ? render(patch.newNode):document.createTextNode(patch.newNode)
node.parentNode.replaceChild(newNode,node)
break;
}
})
}
export default patch
element.js
function Element(type, props, children) {
this.type = type
this.props = props
this.children = children
}
function createElement(type, props, children) {
return new Element(type, props, children)
}
function render(obj) {
// 创建节点
let el = document.createElement(obj.type)
for (let key in obj.props) {
el.setAttribute(key, obj.props[key])
}
if (Array.isArray(obj.children)) {
obj.children.forEach(element => {
// 递归操作,如果当前child不是文本,就继续进行操作,否则创建文本节点
element = element instanceof Element ? render(element) : document.createTextNode()
el.appendChild(element)
});
}
else if (typeof obj.children === 'string') { // 如果是字符串当成数组进行递归
el.appendChild(document.createTextNode(obj.children))
}
return el
}
function renderDom(node, target) {
node.appendChild(target)
}
export { createElement, render, renderDom, Element }
网友评论