项目结构
public/index.html
Title
src/index.js
import {createElement, render, renderDom} from './virtualDom'
import domDiff from './domDiff'
import doPatch from './doPatch'
const vDomOld = createElement('ul', {
class: 'list1',
style: 'width: 300px;height: 300px; background-color: orange',
}, [
createElement('li', {
class: 'item',
'data-index': 0
}, [
createElement('p', {class: 'text'}, ['第1个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 1
}, [
createElement('p', {class: 'text'}, ['第2个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 2
}, [
createElement('p', {class: 'text'}, ['第3个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 3
}, [
createElement('p', {class: 'text'}, ['第4个列表项'])
])
]
)
const rDom = render(vDomOld);
// 渲染dom
renderDom(rDom,document.getElementById('app'));
console.log(vDomOld)
console.log(rDom)
const vDomNew = createElement('ul', {
class: 'list2',
style: 'width: 300px;height: 300px; background-color: orange',
}, [
createElement('li', {
class: 'item',
'data-index': 0
}, [
createElement('p', {class: 'text'}, ['第1个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 1
}, [
createElement('p', {class: 'text'}, ['第2个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 2
}, [
createElement('p', {class: 'text'}, ['第3个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 3
}, [
createElement('h1', {class: 'text'}, ['零三的笔记','https://web03.cn'])
])
]
)
// diff计算差异
const patches = domDiff(vDomOld,vDomNew)
console.log(patches)
// 将计算出来的差异更新到dom中
doPatch(rDom, patches)
vDomOld是原有dom,vDomNew是新的虚拟dom
src/virtualDom.js
import Element from "./Element";
function createElement(type, props, children) {
return new Element(type,props,children)
}
/**
* 考虑input以及其他组件
*/
function setAttrs(node,prop,value) {
switch (prop) {
case 'value':
if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA'){
node.value = value
} else {
node.setAttribute(prop, value)
}
break
case 'style':
node.style.cssText = value
break
default:
node.setAttribute(prop,value)
}
}
//将虚拟dom转换成真实dom
function render(vDom) {
const {type, props, children} = vDom,
el = document.createElement(type);
// 处理父一级
for (let key in props) {
setAttrs(el,key, props[key])
}
// 处理子节点
children.map(cEl => {
if (cEl instanceof Element){
cEl = render(cEl)
} else {
cEl = document.createTextNode(cEl)
}
el.appendChild(cEl)
})
return el;
}
// 挂载dom
function renderDom(rDom, rooTel) {
rooTel.appendChild(rDom)
}
export {
createElement,
render,
renderDom,
setAttrs
}
src/pathType.js
const ATTR = 'ATTR',//更改attr 自定义属性
TEXT = 'TEXT',//更改文本
REPLACE = 'REPLACE',//替换节点
REMOVE = 'REMOVE';//删除节点
export {
ATTR,
TEXT,
REPLACE,
REMOVE
}
src/domDiff.js
import {
ATTR,
TEXT,
REPLACE,
REMOVE
} from './patchTypes'
// 存储dom差异
let patches = {},
vnIndex = 0;
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){
// 组件名一样
// 对比props
const attrPath = attrsWalk(oldNode.props, newNode.props);
// 有差异
if (Object.keys(attrPath).length > 0){
vnPatch.push({
type: ATTR,
attrs: attrPath
})
}
childrenWalk(oldNode.children, newNode.children)
} else {
// 替换
vnPatch.push({
type: REPLACE,
newNode
})
}
if (vnPatch.length > 0){
patches[index] = vnPatch;
}
}
// 对比属性异同
function attrsWalk(oldAttrs, newAttrs) {
let attrPath = {};
for (let key in oldAttrs) {
// 修改
if (oldAttrs[key] !== newAttrs[key]){
attrPath[key] = newAttrs[key]
}
}
for (let key in newAttrs) {
// 添加
if (!oldAttrs.hasOwnProperty(key)){
attrPath[key] = newAttrs[key]
}
}
return attrPath;
}
// 对比儿子异同
function childrenWalk(oldChildren, newChildren) {
oldChildren.map((children, index) => {
vNodeWalk(children,newChildren[index], ++vnIndex)
})
}
export default domDiff;
src/Element.js
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
export default Element;
src/doPath.js
import {
ATTR,
TEXT,
REPLACE,
REMOVE
} from './patchTypes';
import {setAttrs, render} from './virtualDom';
import Element from "./Element";
let finalPatches = {},//储存patches
rnIndex = 0;//真实节点index
function doPatch(rDom, patches) {
finalPatches = patches;
rNodeWalk(rDom)
}
// 处理真实节点
function rNodeWalk(rNode) {
const rnPath = finalPatches[rnIndex++],
childNodes = rNode.childNodes;
//子节点为类数组
[...childNodes].map((childrenNode)=>{
rNodeWalk(childrenNode)
})
// 判断是否需要更新节点
if (rnPath){
patchAction(rNode, rnPath);//当前节点所对应的需要更新的内容
}
}
function patchAction(rNode, rnPath) {
rnPath.map(path => {
switch (path.type) {
case ATTR:
for(let key in path.attrs){
const value = path.attrs[key];
// 之前有的,现在有的->添加更新
if (value){
setAttrs(rNode, key, value)
}else {
// 之前有的,现在没了 ->删除
rNode.removeAttribute(key)
}
}
break
case TEXT:
// 设置节点文本内容
rNode.textContent = path.text
break
case REPLACE:
// 判断是否被Element构造出来的,不是那会是文本节点,是的话直接给render转换成真实节点
const newNode = (path.newNode instanceof Element)
? render(path.newNode)
: document.createTextNode(path.newNode);
// 替换真实节点
rNode.parentNode.replaceChild(newNode, rNode);
break
case REMOVE:
// 找到父亲杀儿子
rNode.parentNode.removeChild(rNode);
break
default:
break
}
})
}
export default doPatch;
计算结果
以上代码思想已经实现了最最基本的dom更新,仅供参考学习
流程图