环境搭建
- 创建一个文件夹,来到文件夹目录下,使用npm init -y 生成package.json文件。
下载webpack、webpack-cli、webpack-dev-server,这里需要特别注意的是webpack的各个版本。
下载对应的版本
cnpm i webpack@5 webpack-cli@3 webpack-dev-server@3 -S
- 创建webpack.config.js文件
module.exports = {
entry: {
index: './src/index.js'
},
output: {
path: __dirname + '/public',
filename: './js/[name].js'
},
devServer: {
contentBase: './public',
inline: true
}
}
- 根据webpack.config.js文件的内容创建相应的文件
创建完成之后的文件目录
- 在index.html文件中引入js文件
- 运行程序
需要在package.json文件中加入运行命令
{
"name": "diff",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^5.52.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2"
}
}
- 运行程序
npm run dev
创建h函数
- 在src下新建文件夹dom,新建文件h.js
import vnode from './vnode'
export default function(sel, data, params) {
// console.log(sel, data, params)
// 没有子元素的情况
if(typeof params === 'string') {
return vnode(sel, data, undefined, params, undefined)
} else if(Array.isArray(params)) { // 有子元素的情况
let children = []
for(let item of params) {
children.push(item)
}
return vnode(sel, data, children, undefined, undefined)
}
}
- 在dom文件夹下新建vnode.js,该函数的作用是将真实dom转化为虚拟dom
export default function(sel, data, children, text, elm) {
/**
* sel: 虚拟dom
* data: key
* children: 子元素
* text: 文本内容
* elm: 真实dom
*/
return {
sel,
data,
children,
text,
elm
}
}
- 在index.js向创建虚拟dom
import h from './dom/h'
let vnode1 = h("div", {}, "你好呀");
console.log(vnode1)
let vnode2 = h("ul", {}, [
h("li", {}, "a"),
h("li", {}, "b"),
h("li", {}, "c"),
h("li", {}, "d"),
h("li", {}, "e"),
]);
console.log(vnode2)
- 运行结果
将真实的dom转为虚拟dom,然后进行新旧虚拟节点比较
- 如果新老节点不是同一个节点名称,那么久暴力删除旧的节点,插入新的节点。
1、在index.html下新写一个div
<div id="container">这是container</div>
2、在index.js下获取获取到index.html下的div这个真实dom节点
// 获取真实的dom节点
const container = document.getElementById('container')
3、创建虚拟节点,将虚拟节点patch到真实dom上
// 虚拟节点
let vnode1 = h("h1", {}, "你好呀");
// console.log(vnode1)
let vnode2 = h("ul", {}, [
h("li", {}, "a"),
h("li", {}, "b"),
h("li", {}, "c"),
h("li", {}, "d"),
h("li", {}, "e"),
]);
patch(container, vnode1)
4、新建patch.js函数,作用是将真实的dom转化为虚拟dom,然后再进行新旧虚拟dom的比较
import vnode from './vnode'
import createElement from './createElement'
/**
* @param {*} oldVnode 旧虚拟节点
* @param {*} newVnode 新虚拟节点
*/
export default function (oldVnode, newVnode) {
// 如果oldVnode没有sel,则是非虚拟节点
if (oldVnode.sel === undefined) {
// console.log(oldVnode.tagName.toLowerCase())
oldVnode = vnode(
oldVnode.tagName.toLowerCase(), //虚拟节点名称
{},
[],
undefined,
oldVnode // 真实的dom元素
)
}
// console.log(oldVnode)
// 开始判断新旧虚拟节点
if (oldVnode.sel !== newVnode.sel) { // 如果新老节点不是同一个节点名称,那么久暴力删除旧的节点,插入新的节点。
// 把新的虚拟节点创建为真实dom节点
let newVnodeElm = createElement(newVnode) // 在这里最好将创建真实dom写成一个函数,方便递归调用
// console.log(newVnodeElm)
let oldVnodeElm = oldVnode.elm
if(newVnodeElm) {
oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
}
oldVnodeElm.parentNode.removeChild(oldVnodeElm)
} else { // 是同一个节点,逻辑变复杂
}
}
5、新建createElement.js文件,作用是将虚拟节点转化为真实节点,并挂载在真实dom上
export default function createElement (vnode) {
let domNode = document.createElement(vnode.sel)
// 判断有没有子节点
if(vnode.children === undefined) {
domNode.innerText = vnode.text
}else if(Array.isArray(vnode.children)) {
for(let child of vnode.children) {
let childDom = createElement(child)
domNode.appendChild(childDom)
}
}
vnode.elm = domNode
return domNode
}
- 如果是相同节点,分为以下几种情况
- (1)新节点没有children,直接替换
- (2)新节点有children,旧的节点也有children,diff算法核心
- (3)新节点有children,旧的没有,直接创建元素添加
新建patchVnode.js文件,判断新旧虚拟节点相同的情况
import createElement from './createElement'
export default function(oldVnode, newVnode) {
// 判断新节点有没有子元素
if(newVnode.children === undefined) { // 没有子元素
if(newVnode.text !== oldVnode.text) {
oldVnode.elm.innerText = newVnode.text
}
} else { // 有子元素
// 判断旧节点有没有子元素
if(oldVnode.children && oldVnode.children.length > 0) { // 旧节点有子元素,情况较为复杂
} else { // 旧节点没有子元素
oldVnode.elm.innerHTML = '' // 删掉旧节点的内容
for(let child of newVnode.children) {
let childDom = createElement(child)
oldVnode.elm.appendChild(childDom)
}
console.log(oldVnode.elm)
}
}
}
patch.js文件
import vnode from './vnode'
import createElement from './createElement'
import patchVnode from './patchVnode'
/**
* @param {*} oldVnode 旧虚拟节点
* @param {*} newVnode 新虚拟节点
*/
export default function (oldVnode, newVnode) {
// 如果oldVnode没有sel,则是非虚拟节点
if (oldVnode.sel === undefined) {
// console.log(oldVnode.tagName.toLowerCase())
oldVnode = vnode(
oldVnode.tagName.toLowerCase(), //虚拟节点名称
{},
[],
undefined,
oldVnode // 真实的dom元素
)
}
// console.log(oldVnode)
// 开始判断新旧虚拟节点
if (oldVnode.sel !== newVnode.sel) { // 如果新老节点不是同一个节点名称,那么久暴力删除旧的节点,插入新的节点。
// 把新的虚拟节点创建为真实dom节点
let newVnodeElm = createElement(newVnode) // 在这里最好将创建真实dom写成一个函数,方便递归调用
// console.log(newVnodeElm)
let oldVnodeElm = oldVnode.elm
if(newVnodeElm) {
oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
}
oldVnodeElm.parentNode.removeChild(oldVnodeElm)
} else { // 是同一个节点,逻辑变复杂
patchVnode(oldVnode, newVnode)
}
}
index.js文件
import h from './dom/h'
import patch from './dom/patch'
// 虚拟节点
// let vnode1 = h("div", {}, "你好呀");
let vnode1 = h("div", {}, [
h("span", {}, "a"),
h("span", {}, "b"),
h("span", {}, "c"),
h("span", {}, "d"),
h("span", {}, "e"),
]);
// 获取真实的dom节点
const container = document.getElementById('container')
patch(container, vnode1)
diff算法核心
- 1、旧前和新前
匹配:旧前指针++、新前指针++ - 2、旧后和新后
匹配:旧后指针–、新后指针– - 3、旧前和新后
匹配:旧前指针++、新后指针– - 4、旧后和新前
匹配:旧后指针–、新前指针++ - 5、以上都不满足条件 ===》查找
查找到:新的指针++,将新的指针指向的元素渲染到页面,并且将在旧的节点中找到相应的元素将其赋值为undefined - 6、创建或者删除
新建updateChildren.js,作用是处理diff算法中的5种情况
import patchVnode from './patchVnode'
import createElement from './createElement'
// 判断两个虚拟节点是否为同一个节点
function sameVnode(vnode1, vnode2) {
return vnode1.key === vnode2.key
}
/**
*
* @param {*} parentElm 旧的父节点
* @param {*} oldCh 旧的子节点
* @param {*} newCh 新的子节点
*/
export default function(parentElm, oldCh, newCh) {
// console.log(parentElm, oldCh, newCh)
let oldStartIdx = 0 // 旧前指针
let oldEndIdx = oldCh.length - 1 // 旧后指针
let newStartIdx = 0 // 新前指针
let newEndIdx = newCh.length - 1 // 新后指针
let oldStartVnode = oldCh[0] // 旧前虚拟节点
let oldEndVnode = oldCh[oldEndIdx] // 旧后虚拟节点
let newStartVnode = newCh[0] // 新前虚拟节点
let newEndVnode = newCh[newEndIdx] // 新后虚拟节点
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(oldStartVnode === undefined) {
oldStartVnode = oldCh[++oldStartIdx]
}
if(oldEndVnode === undefined) {
oldEndVnode = oldCh[--oldEndIdx]
}else if(sameVnode(oldStartVnode, newStartVnode)) {
// 旧前和新前
console.log(1)
patchVnode(oldStartVnode, newStartVnode)
if(newStartVnode) {
newStartVnode.elm = oldStartVnode?.elm
}
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if(sameVnode(oldEndVnode, newEndVnode)) {
// 旧后和新后
console.log(2)
patchVnode(oldEndVnode, newEndVnode)
if(oldEndVnode) {
newEndVnode.elm = oldEndVnode?.elm
}
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if(sameVnode(oldStartVnode, newEndVnode)) {
// 旧前和新后
console.log(3)
patchVnode(oldStartVnode, newEndVnode)
if(oldStartVnode) {
newEndVnode.elm = oldStartVnode?.elm
}
// 把旧前指定的节点移动到旧后指向的节点的后面
parentElm.insertBefore( oldStartVnode.elm, oldEndVnode.elm.nextSibling )
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if(sameVnode(oldEndVnode, newStartVnode)) {
// 旧后和新前
console.log(4)
patchVnode(oldEndVnode, newStartVnode)
if(oldEndVnode) {
newStartVnode.elm = oldEndVnode?.elm
}
// 把旧后指定的节点移动到旧前指向的节点的前面
parentElm.insertBefore( oldEndVnode.elm, oldStartVnode.elm )
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 以上都不满足条件 ===》查找
console.log(5)
// 创建一个对象存虚拟节点
let keyMap = {}
for(let i = oldStartIdx;i <= oldEndIdx;i++) {
const key = oldCh[i]?.key
if(key) {
keyMap[key] = i
}
}
// console.log(keyMap)
// 在旧节点中寻找前节点指向的节点
let indxInOld = keyMap[newStartVnode.key]
if(indxInOld) { // 能找到
const elmMove = oldCh[indxInOld]
patchVnode(elmMove, newStartVnode)
// 处理过的节点,在旧虚拟节点的数组中,设置为undefined
oldCh[indxInOld] = undefined
parentElm.insertBefore(elmMove.elm, oldStartVnode.elm)
}else { // 不能找到
// 说明是新节点,需要重新创建
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
}
// 新节点指针+1
newStartVnode = newCh[++newStartIdx]
}
}
}
在patchVnode.js中引用updateChildren方法
import createElement from './createElement'
import updateChildren from './updateChildren'
export default function patchVnode (oldVnode, newVnode) {
// console.log(oldVnode)
// 判断新节点有没有子元素
if (newVnode.children === undefined) { // 没有子元素
if (newVnode.text !== oldVnode.text) {
oldVnode.elm.innerText = newVnode.text
}
} else { // 有子元素
// 判断旧节点有没有子元素
if (oldVnode.children && oldVnode.children.length > 0) { // 旧节点有子元素,情况较为复杂
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
// console.log(oldVnode.elm, oldVnode.children, newVnode.children)
} else { // 旧节点没有子元素
oldVnode.elm.innerHTML = '' // 删掉旧节点的内容
for (let child of newVnode.children) {
let childDom = createElement(child)
oldVnode.elm.appendChild(childDom)
}
// console.log(oldVnode.elm)
}
}
}
- 创建或者删除节点
在updateChild.js中添加以下代码
// 结束while 只有两种情况 (新增和删除)
// 1、oldStartIdx > oldEndIdx
// 2、newStartIdx > newEndIdx
if(oldStartIdx > oldEndIdx) {
const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null
for(let i = newStartIdx;i <= newEndIdx;i++) {
parentElm.insertBefore((createElement(newCh[i])), before)
}
} else { // 进入删除操作
for(let i = oldStartIdx;i <= oldEndIdx;i++) {
// console.log(oldCh[i])
parentElm.removeChild(oldCh[i].elm)
}
}
参考文献
https://www.bilibili.com/video/BV1K64y1s7ot?p=9&spm_id_from=pageDriver
全部代码
gitee地址
https://gitee.com/jjm1/diff