本文研究目标
虚拟DOM如何被渲染函数(h函数)产生,需要手写h函数diff算法原理并手写虚拟DOM如何通过diff变为真正的DOM,实际上虚拟DOM变回真正的DOM,是涵盖在diff算法里面的本文不研究如何把真实DOM变为虚拟DOM(模板编译范畴),只研究虚拟DOM如何变回真实DOM
snabbdom
snabbdom(瑞典语,“速度”)是著名的虚拟DOM库,是diff算法的鼻祖。
搭建初始环境
1.安装snabbdom
npm install -S snabbdom
2.安装webpack5并配置
cnpm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
3.跑通snabbdom官方demo
/src/index.js
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: function () { } } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: function () { } } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
www/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虚拟DOM与diff算法</title>
</head>
<body>
<div id="container"></div>
<script src="/xuni/bundle.js"></script>
</body>
</html>
package.json
{
"name": "vdom_diff_study",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server --open"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"snabbdom": "^3.6.2",
"webpack": "^5.92.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3"
}
}
webpack.config.js
const path = require('path')
module.exports = {
// webpack5 不用配置mode
// 入口
entry: "./src/index.js",
// 出口
output: {
// 虚拟打包路径,文件夹不会真正生成,而是在8080端口虚拟生成
publicPath: "xuni",
// 打包出来的文件名
filename: "bundle.js",
},
// 配置webpack-dev-server
devServer: {
// 静态根目录
contentBase: 'www',
// 端口号
port: 8080,
open: true, // 自动打开浏览器
hot: true, // 启用热模块替换
},
};
最后npm run dev启动项目
虚拟DOM和h函数
什么是虚拟DOM
虚拟DOM:用JS对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性
diff是发生在虚拟DOM上的
新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上
DOM如何变为虚拟DOM,本文章不研究
DOM如何变为虚拟DOM,属于模板编译原理范畴,本节不研究
h函数
h函数用来产生虚拟节点
虚拟节点有哪些属性
{
children:undefined,
data:{},
elm:undefined,//表示对应的真实DOM节点,如果为undefined表示还没有上树
key:undefined,//唯一标识
sel:'div'//选择器,
text:'文字'
}
创建第一个虚拟节点
index.js文件
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建虚拟节点
var myVnode = h('a', { props: { href: 'https://baidu.com' } }, '百度')
console.log('myVnode', myVnode);
打印结果:
但是发现虽然创建了虚拟节点,但是页面看不到这个虚拟节点,也就是并没有上树
需要使用patch方法让虚拟节点上树(patch函数后续再讲解,非常重要是diff算法的核心):
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建出patch函数
const patch = init([
// init 模块用来创建具有初始化功能的模块
classModule,
propsModule,
styleModule,
eventListenersModule
]);
// 创建虚拟节点
var myVnode = h('a', { props: { href: 'https://baidu.com' } }, '百度')
console.log('myVnode', myVnode);
// 让虚拟节点上树
const container = document.getElementById('container');
patch(container, myVnode);
h函数可以嵌套使用,从而得到虚拟DOM树(重要)
const myVnode3 = h('ul', [
h('li', '语文'),
h('li', '数学'),
h('li', [
h('div', [
h('span', '英语'),
h('span', '物理')
])
])
])
// 让虚拟节点上树
const container = document.getElementById('container');
patch(container, myVnode3);
手写h函数
只关注核心代码
vnode函数
vnode函数功能就是把传入的参数组成对象返回
vnode.js
// vnode函数功能就是把传入的参数组成对象返回
export default function (sel, data, children, text, elm) {
return { sel, data, children, text, elm }
}
h.js
import vnode from './vnode'
// 编写一个低配版本的h函数,这个函数必须接收3个参数,缺一不可
// 相当于它的重载能力较弱
// 也就是说,调用的时候形态必须是下面的三种之一
// 形态一:h('div', {}, '文字')
// 形态二:h('div', {}, [])
// 形态三:h('div', {}, h())
export default function h(sel, data, c) {
// 检查参数的个数
if (arguments.length !== 3) {
throw new Error('对不起,h函数必须传3个参数,我们这是低配版')
}
// 检查参数c的类型
if (typeof c === 'string' || typeof c === 'number') {
// 说明现在调用的h函数是形态一
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
//说明现在是形态二
let children = []
// 遍历c
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象
if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('形态二的c数组里面的元素必须是对象,并且有sel属性')
}
// 这里不用执行c[i],因为你的测试语句中已经有了执行
// 此时只需要收集好就行了
children.push(c[i])
}
// 循环结束,children收集完毕
// 此时可以返回children属性
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
// 说明现在是形态三
// 即传入的c是唯一的children
let children = [c]
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('对不起,h函数的第三个参数应该是字符串、数组、对象')
}
}
index.js使用自己写的h函数与snabbdom的patch函数结合使用
import h from './mysnabbdom/h'
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
} from "snabbdom";
// 创建出patch函数
const patch = init([
// init 模块用来创建具有初始化功能的模块
classModule,
propsModule,
styleModule,
eventListenersModule
]);
// 比较神奇,由h函数组成的参数已经形成树结构,所有h函数内部无需递归
var myVnode = h('ul', {}, [
h('li', {}, '语文'),
h('li', {}, '数学'),
h('li', {}, [
h('div', {}, [
h('span', {}, '英语'),
h('span', {}, '物理')
])
])
])
console.log('myVnode', myVnode);
// 让虚拟节点上树
const container = document.getElementById('container');
patch(container, myVnode);
感受diff算法
key的作用
key是节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点
代码说明:
index.js
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建出patch函数
const patch = init([
// init 模块用来创建具有初始化功能的模块
classModule,
propsModule,
styleModule,
eventListenersModule
]);
// 得到盒子和按钮
const btn = document.getElementById('btn')
const container = document.getElementById('container')
const vnode1 = h('ul', {}, [
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D'),
])
patch(container, vnode1)
const vnode2 = h('ul', {}, [
h('li', {}, 'E'),
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D'),
])
// 点击按钮时,将vnode1替换成vnode2
btn.onclick = function () {
patch(vnode1, vnode2)
}
点击按钮前:
点击按钮后:
为了证明实现最小量更新,利用浏览器直接修改li中的文字
但是有一些问题:
修改vnode2
const vnode2 = h('ul', {}, [
h('li', {}, 'E'),
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D'),
])
点击按钮前
点击按钮后,发现修改的结果被重置了
那是因为DOM的更新顺序是这样的,新的节点在尾部插入:
把虚拟DOM的key属性加上就可以解决这个问题,这样就会直接在头部创建一个DOM,key服务于最小量更新,可以极大提升DOM更新的效率:
const vnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
])
const vnode2 = h('ul', {}, [
h('li', { key: 'E' }, 'E'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
])
只是同一个虚拟节点,才进行精细化比较
只有同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。
如何定义同一个虚拟节点:选择器且key相同
案例演示:
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建出patch函数
const patch = init([
// init 模块用来创建具有初始化功能的模块
classModule,
propsModule,
styleModule,
eventListenersModule
]);
// 得到盒子和按钮
const btn = document.getElementById('btn')
const container = document.getElementById('container')
const vnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
])
patch(container, vnode1)
const vnode2 = h('ol', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
])
// 点击按钮时,将vnode1替换成vnode2
btn.onclick = function () {
patch(vnode1, vnode2)
}
点击前:
点击后,修改的内容重新更新了。说明是暴力重新创建
只进行同层比较不会进行跨层比较
diff函数只进行同层比较不会进行跨层比较,即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你,而是暴力删除旧的,然后插入新的
index.js
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建出patch函数
const patch = init([
// init 模块用来创建具有初始化功能的模块
classModule,
propsModule,
styleModule,
eventListenersModule
]);
// 得到盒子和按钮
const btn = document.getElementById('btn')
const container = document.getElementById('container')
const vnode1 = h('div', {}, [
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'C' }, 'C'),
h('p', { key: 'D' }, 'D'),
])
patch(container, vnode1)
const vnode2 = h('div', {}, h('section', {}, [
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'C' }, 'C'),
h('p', { key: 'D' }, 'D'),
]))
// 点击按钮时,将vnode1替换成vnode2
btn.onclick = function () {
patch(vnode1, vnode2)
}
模仿上述的调试过程,发现跨层变化也会直接暴力删除再暴力创建
diff处理新旧节点不是同一个节点时
- patch函数被调用时首先会判断oldVnode是虚拟节点还是真实DOM(真实DOM也有可能的,比如第一次上树1时
patch(container, vnode1),这个container盒子就是一个真实DOM) - 如果是真实节点就将oldVnode包装为虚拟节点
- 如果是虚拟节点执行下一步判断oldVnode和newVnode是不是同一个节点(key相同且sel相同)
- 如果是则精细化diff比较
- 不是则暴力删除旧的、插入新的
patch函数流程图:
如何定义是否同一个节点的大致代码:
创建节点时,所有子节点需要递归创建的大致代码:
手写第一次上树
什么是第一次上树,就是第一次调用patch方法,且patch方法的第一个参数一定是一个真实DOM
const myVnode1 = h('h1', {}, '你好')
const container = document.getElementById('container')
patch(container, myVnode1)
代码:
index.js
import h from './mysnabbdom/h'
import patch from './mysnabbdom/patch'
const myVnode1 = h('h1', {}, '你好')
const container = document.getElementById('container')
patch(container, myVnode1)
patch.js
import vnode from './vnode'
import createElement from './createElement';
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数是DOM节点还是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
console.log('oldVnode', oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
// 是同一个节点,精细化比较
} else {
// 不是同一个节点,暴力插入新的,删除旧的
createElement(newVnode, oldVnode.elm)
}
}
createElement.js
// 真正创建节点,将vnode创建为dom,插入到pivot这个元素之前
export default function (vnode, pivot) {
console.log('目的是把虚拟节点', vnode, '插入到标杆', pivot, '前');
// 以vnode的sel属性作为标签名创建一个dom节点,当前这个节点还是孤儿节点
let domNode = document.createElement(vnode.sel)
// 有子节点还是有文本(本课程为了方便教学只教文本与子元素互斥的情况,其实文本和子元素是可以共存的)
if (vnode.text !== '' && vnode.children === undefined || vnode.children.length === 0) {
// 它内部是文字
domNode.innerText = vnode.text
// 将孤儿节点上树,让标杆节点的父元素调用insertBefore方法,将新的孤儿节点插入到标杆节点之前
// 由于第一次上树时,pivot一定是一个真实DOM,所以pivot.parentNode一个是存在的
pivot.parentNode.insertBefore(domNode, pivot)
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 递归处理子节点
}
}
手写递归创建子节点
当patch函数的vnode参数有children属性时,代表其有子节点,这时需要递归创建。
什么时机停止递归:当h函数第三个参数是文本时
此时上一节的createElement函数无法实现递归创建子节点,因为缺乏标杆pivot参数
看这块h函数创建虚拟DOM的代码,发现创建出来的虚拟节点的children是一个数组,无法拿到标杆pivot
// 使用h函数创建虚拟DOM
const myVnode1 = h('h1', {}, [
h('h2', {}, 1111),
h('h2', {}, 2222),
h('h2', {}, 3333),
h('h2', {}, 4444),
])
综上所述,我们需要对createElement函数进行改造,去除createElement插入新的dom节点的操作,将这个插入新节点操作交给patch函数完成
调整的代码如下:
index.js
import h from './mysnabbdom/h'
import patch from './mysnabbdom/patch'
// 使用h函数创建虚拟DOM
const myVnode1 = h('h1', {}, [
h('h2', {}, 'A'),
h('h2', {}, 'B'),
h('h2', {}, 'C'),
h('div', {}, h('ol', {}, [
h('li', {}, '语文'),
h('li', {}, '数学'),
h('li', {}, '英语'),
])),
])
const myVnode2 = h('h2', {}, [
h('h3', {}, 'A'),
h('h3', {}, 'B'),
h('h3', {}, 'C'),
h('div', {}, h('ol', {}, [
h('li', {}, '苹果'),
h('li', {}, '葡萄'),
h('li', {}, '梨子'),
])),
])
const container = document.getElementById('container')
// 使用patch函数渲染虚拟DOM(上树)
patch(container, myVnode1)
const btn = document.getElementById('btn')
btn.onclick = function () {
console.log('点击按钮');
patch(myVnode1, myVnode2)
}
createElement.js
// 真正创建节点,将vnode创建为dom,是孤儿节点,不进行插入
export default function createElement(vnode) {
console.log('目的是把虚拟节点', vnode, '真正变为dom');
// 以vnode的sel属性作为标签名创建一个dom节点,当前这个节点还是孤儿节点
let domNode = document.createElement(vnode.sel)
// 有子节点还是有文本(本课程为了方便教学只教文本与子元素互斥的情况,其实文本和子元素是可以共存的)
if (vnode.text !== '' && vnode.children === undefined || vnode.children.length === 0) {
// 它内部是文字
domNode.innerText = vnode.text
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 它内部是子节点,需要递归创建节点
for (let i = 0; i < vnode.children.length; i++) {
// 得到当前children
let ch = vnode.children[i]
//创建出它的dom,一旦调用createElement意味着:创建出dom了,并且它的elm属性指向了创建出的dom,但是还没有上树,是一个孤儿节点
let chDom = createElement(ch)
// 上树
domNode.appendChild(chDom)
}
}
// 补充elm属性
vnode.elm = domNode
// 返回elm(真实DOM)
return domNode
}
patch.js
import vnode from './vnode'
import createElement from './createElement';
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数是DOM节点还是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
// 是同一个节点,精细化比较
console.log('是同一个节点,精细化比较');
} else {
// 不是同一个节点,暴力插入新的,删除旧的
let newVnodeElm = createElement(newVnode, oldVnode.elm)
// 插入到老节点之前
if (oldVnode.elm !== undefined && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
}
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
diff处理新旧节点是同一个节点时
diff处理新旧节点是同一个节点时的流程图:
手写新旧节点text的不同情况
patch.js
import vnode from './vnode'
import createElement from './createElement';
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数是DOM节点还是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
// 是同一个节点,精细化比较
console.log('是同一个节点,精细化比较');
// 判断新旧vnode是否是同一个对象
if (oldVnode === newVnode) return
// 判断newVnode有没有text属性
if (newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
// 新节点有text属性
console.log('newVnode有text属性');
if (oldVnode.text !== newVnode.text) {
// 新老节点的text不相等,直接替换
oldVnode.elm.innerText = newVnode.text
}
} else {
// 新节点没有text属性,意味着有children
console.log('newVnode没有text属性');
// 判断oldVnode有没有children
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// 此时就是最复杂的情况,新老就有children
} else {
console.log('老的没有,新的有children');
// 老的没有,新的有children
// 清空老的节点的内容
oldVnode.elm.innerText = ''
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
} else {
// 不是同一个节点,暴力插入新的,删除旧的
let newVnodeElm = createElement(newVnode)
// 插入到老节点之前
if (oldVnode.elm !== undefined && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
}
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
diff算法的子节点更新策略(重点)
经典的diff算法优化策略
四种命中查找(有顺序关系,都没有命中则循环,命中一种就不再进行命中判断了。所谓的命中就是判断两个指针指向的节点是否是同一个节点,如果未命中则执行下一个命中条件,如此循环往复。并且如果命中的化,前指针往后移动,后指针往前移动)
并且这个循环条件是while(新前<=新后&&旧前<=旧后)
- 新前与旧前
- 新后与旧后
- 新后于旧前
- 新前与旧后
什么意思呢?我们现在需要准备四个指针,分别指向新子节点的头部、新子节点的尾部、旧子节点的头部、旧子节点的尾部(面试常考一定要会)
这一小节课程讲得很烂,看这篇文章吧
手写子节点更新策略
第一部分
1.将patch函数中处理相同节点的部分抽离为一个函数patchVnode
patch.js
import vnode from './vnode'
import createElement from './createElement';
import patchVnode from './patchVNode.js'
export default function patch(oldVnode, newVnode) {
// 判断传入的第一个参数是DOM节点还是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
// 判断oldVnode和newVnode是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
// 是同一个节点,精细化比较
console.log('是同一个节点,精细化比较');
patchVnode(oldVnode, newVnode)
} else {
console.log('不同节点,暴力增删');
// 不是同一个节点,暴力插入新的,删除旧的
let newVnodeElm = createElement(newVnode)
// 插入到老节点之前
if (oldVnode.elm !== undefined && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
}
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
patchVNode.js 专门用来处理diff更新时新旧节点为相同节点时的情况
import createElement from './createElement'
import updateChildren from './updateChildren'
// patchVNode.js 专门用来处理diff更新时新旧节点为相同节点时的情况
export default function patchVNode(oldVnode, newVnode) {
// 判断新旧vnode是否是同一个对象
if (oldVnode === newVnode) return
// 判断newVnode有没有text属性
if (newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
// 新节点有text属性
console.log('newVnode有text属性');
if (oldVnode.text !== newVnode.text) {
// 新老节点的text不相等,直接替换
oldVnode.elm.innerText = newVnode.text
}
} else {
// 新节点没有text属性,意味着有children
console.log('newVnode没有text属性');
// 判断oldVnode有没有children
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// 此时就是最复杂的情况,新老都有children
console.log('最复杂的情况来了');
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
} else {
console.log('老的没有,新的有children');
// 老的没有,新的有children
// 清空老的节点的内容
oldVnode.elm.innerText = ''
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}
updateChildren.js
import patchVNode from "./patchVNode";
//updateChildren 实现子节点更新 parentElm 父节点 oldCh 老节点的子节点 newCh 新节点的子节点
export default function updateChildren(parentElm, oldCh, newCh) {
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
// 开始大while了
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log('循环中-------');
// 四种命中条件
if (checkSameVnode(oldStartVnode, newStartVnode)) {
console.log('命中1');
// 新前与旧前
// 递归 使用patchVNode
patchVNode(oldStartVnode, newStartVnode)
// 将旧前与新前后移
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
console.log('命中2');
// 新后与旧后
patchVNode(oldEndVnode, newEndVnode)
// 将旧后与新后前移
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
console.log('命中3');
// 新后与旧前
patchVNode(oldStartVnode, newEndVnode)
// 将旧前插入到旧后之后
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
console.log('命中4');
// 新前与旧后
patchVNode(oldEndVnode, newStartVnode)
// 将旧后插入到旧前之前
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[--newStartIdx];
}
}
}
// 判断是不是同一个虚拟节点
function checkSameVnode(oldVnode, newVnode) {
return oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel;
}
第二部分 循环结束后对剩余节点做新增删除处理
import patchVNode from "./patchVNode";
import createElement from "./createElement";
//updateChildren 实现子节点更新 parentElm 父节点 oldCh 老节点的子节点 newCh 新节点的子节点
export default function updateChildren(parentElm, oldCh, newCh) {
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
// 开始大while了
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log('循环中-------');
// 四种命中条件
if (checkSameVnode(oldStartVnode, newStartVnode)) {
console.log('命中1');
// 新前与旧前
// 递归 使用patchVNode
patchVNode(oldStartVnode, newStartVnode)
// 将旧前与新前后移
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
console.log('命中2');
// 新后与旧后
patchVNode(oldEndVnode, newEndVnode)
// 将旧后与新后前移
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
console.log('命中3');
// 新后与旧前
patchVNode(oldStartVnode, newEndVnode)
// 将旧前插入到旧后之后
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
console.log('命中4');
// 新前与旧后
patchVNode(oldEndVnode, newStartVnode)
// 将旧后插入到旧前之前
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[--newStartIdx];
} else {
console.log('没命中');
}
}
//处理while循环结束后的情况
if (newStartIdx <= newEndIdx) {
// 如果旧节点先循环完毕,新节点还有剩余,需要新增
console.log('新节点有剩余没有处理');
// 插入的标杆
const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
console.log(before);
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild方法一致
// newCh[i]现在还没有真正的DOM,所以要调用createElement() 函数变为DOM
parentElm.insertBefore(createElement(newCh[i]), before)
};
} else if (oldStartIdx <= oldEndIdx) {
// 如果新节点先循环完毕,老节点还有剩余,需要删除
console.log('old还有剩余节点没有处理')
// 删除多余的DOM
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldCh[i].elm)
}
}
}
// 判断是不是同一个虚拟节点
function checkSameVnode(oldVnode, newVnode) {
return oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel;
}
第三部分 四种命中条件都没有命中的处理
import patchVnode from './patchVnode.js'
import createElement from './createElement.js'
//判断是否同一个虚拟节点
function checkSameVnode(a, b) {
return a.sel == b.sel && a.key == b.key;
}
export default function updateChildren(parentElm, oldCh, newCh) {
console.log('updateCh')
console.log(oldCh, newCh);
//旧前
let oldStartIdx = 0;
//新前
let newStartIdx = 0;
//旧后
let oldEndIdx = oldCh.length - 1;
//新后
let newEndIdx = newCh.length - 1;
//旧前节点
let oldStartVnode = oldCh[oldStartIdx];
//旧后节点
let oldEndVnode = oldCh[oldEndIdx];
//新前节点
let newStartVnode = newCh[newStartIdx];
//新后节点
let newEndVnode = newCh[newEndIdx]
let keyMap = null;
//开始while
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log('⭐')
if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx]
} else if (checkSameVnode(oldStartVnode, newStartVnode)) {
console.log('新前旧前')//新前和旧前
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
console.log('新后旧后')//新后和旧后
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
console.log('新后旧前')//新后和旧前
patchVnode(oldStartVnode, newEndVnode);
//当新后旧前命中,移动节点,移动新前指向的这个节点到老节点的旧后的后面
//插入一个已经在DOM树上的节点,他就会移动
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
console.log('新前旧后')//新后和旧前
patchVnode(oldEndVnode, newStartVnode);
//当新后旧前命中,移动节点,移动新前指向的这个节点到老节点的旧前的前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
//四种都没有找到
//制作key的map一个映射对象,这样就不用每次都遍历老对象
if (!keyMap) {
keyMap = {};
// 从 oldStartIdx 开始,到oldEndIdx结束,创建keyMap映射对象
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key != undefined) {
keyMap[key] = i;
}
}
}
//寻找当前这项newStarIdx在keyMap中的映射位置序号
const idxInOld = keyMap[newStartVnode.key]
if (idxInOld == undefined) {
//判断,如果idxInOld是undefined表示他是全新的项
//被加入的项就是被newStartVnode这项,还不是真正的DOM节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
//如果不是undefined,不是全新的项,而是需要移动
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode)
//把这项设置为undefined,表示我已经处理完这项了
oldCh[idxInOld] = undefined;
//移动,调用insertBefore也可以实现移动
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
}
//指向下移,只移新的头
newStartVnode = newCh[++newStartIdx]
}
}
//继续看看有没有剩余的
if (newStartIdx <= newEndIdx) {
console.log('new还有剩余节点没处理,要加项,要把所有剩余的节点都插入到oldStartIdx之前');
//遍历新的newCh,添加到老的没有处理之前
for (let i = newStartIdx; i <= newEndIdx; i++) {
//insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了
//newCh[i]现在还没有成为真正的DOM,要调用createElment()函数变为DOM
parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
}
} else if (oldStartIdx <= oldEndIdx) {
console.log('old还有剩余节点没有处理')
//批量删除oldStart和oldEnd指针直接的项
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm)
}
}
}
}