项目结构
public/index.html
<html lang="en"> <head> <meta charset="UTF-8"> <title>TitlecreateElement, 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更新,仅供参考学习
流程图