①:创建虚拟DOM流程
定义vnode函数:
//函数的功能非常简单,就是把传入的字段拼成一个对象返回出去
export default function(sel,data,children,text,elm) {
return {sel,data,children,text,elm}
}
定义h函数:
import vnode from "./vnode";
//调用的时候一定是下面三种形态之一:
// h('div',{},'文字') 1
// h('div',{},[]) 2
// h('div',{},h()) 3
export default function (sel, data, c) {
//检查参数的个数
if (arguments.length !== 3) throw new Error("对不起,h函数必须传入三个参数,我们是低配版的h函数");
//检查C的类型
if (typeof c == "string" || typeof c == "number") {
//说明传入的是第一种形态的参数
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
//说明传入的是第二种形态的参数
const children = [];
//遍历C
for (let i = 0; i < c.length; i++) {
const element = c[i];
if (!(typeof element === "object" && "sel" in element))
throw new Error("传入的第三个参数,数组中有项不是h函数");
children.push(element)
}
//循环结束说明children收集完毕了,此时就可以返回虚拟节点了,它有children属性的
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c === "object" && "sel" in c) {
//hasOwnproperty方法或者"sel" in c 方式用来判断对象上是否有输入的属性,返回值是布尔值
//说明现在传入的是第三种形态参数
//所以传入的C是唯一的children
const children = [c];
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error("传入的第三个参数类型不正确");
}
}
创建虚拟DOM:
import h from "./mySnabbdom/h"
const mySnabbdom = h('div', {}, [
h('b', {}, '1'),
h('h4', {}, '2'),
h('h1', {}, '3'),
h('br', {}, ''),
h('p', {}, h('i',{},"qweqwe")),
])
console.log(mySnabbdom);
②:diff算法流程
diff算法的初始流程:
如何判断旧节点新节点是同一个?
//查看新旧节点,key唯一值相同并且是同一个选择器
function sameVnode (vnode1, vnode2){
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
创建patch.js文件(方法)
import vnode from "./vnode";
import createElement from "./createElement"
import patchVnode from "./patchVnode"
/**
*
*
* @export
* @param {老DOM} oldVnode
* @param {新DOM} newVnode
*/
export default function (oldVnode, newVnode) {
//判断传入的第一个参数是DOM节点还是虚拟节点
if (oldVnode.sel == "" || oldVnode.sel == undefined) {
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 && newVnodeElm) {
//insertBefore() 方法可在已有的子节点前插入一个新的子节点
//第一个参数:新节点 第二个参数:已有的节点
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
}
//删除老的节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
创建createElement.js(方法)
//真正创建节点,将vnode创建为DOM,是孤儿节点不进行插入
/**
*
*
* @export
* @param {新DOM} vnode
* @returns
*/
export default function createElement(vnode) {
//创建一个DOM节点,这个节点现在还是孤儿节点
const 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
const ch = vnode.children[i];
//console.log(ch);
//创建出它的DOM,一旦调用createrElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点
let chDom = createElement(ch);
//上树
domNode.appendChild(chDom)
}
}
//补充domNode
vnode.elm = domNode;
//返回elm,elm是一个纯DOM属性
return vnode.elm
}
精细化比较:
经典的diff算法优化策略:
什么是新前和旧前:
创建patchVnode.js(方法)
import createElement from "./createElement"
import updataChildren from "./updataChildren"
/**
*
*
* @export
* @param {真实DOM} oldVnode
* @param {虚拟DOM} newVnode
*/
export default function patchVnode(oldVnode, newVnode) {
//判断老节点和新节点是否是同一片内存空间的
if (oldVnode !== newVnode) {
//判断新节点是否有文本
if (newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
//console.log("新节点有text属性");
//判断新节点和老节点的文本是否相同,如果不相同那么直接替换(老节点的内容无论是什么,使用innerText方法都会全部替换掉)
if (newVnode.text !== oldVnode.text) {
oldVnode.elm.innerText = newVnode.text;
}
} else {
//console.log("新节点没有text属性");
//如果新节点没有文本那么肯定有子节点(children),那么判断老节点是否有子节点(children)这部分是最复杂的
if (oldVnode.children && oldVnode.children.length) {
updataChildren(oldVnode.elm,oldVnode.children,newVnode.children)
} else {
//老节点没有子节点(children),那么清空老节点中的文本,并把新节点中的子节点(children)插入到DOM中
//为什么要先清空在插入,是因为appendChild不会替换之前已经存在的文本,而是追加元素上树
oldVnode.elm.innerHTML = "";
for (let i = 0; i < newVnode.children.length; i++) {
const ch = newVnode.children[i];
let dom = createElement(ch)
oldVnode.elm.appendChild(dom)
}
}
}
}
}
创建updataChildren.js(方法)
import patchVnode from "./patchVnode"
import createElement from "./createElement"
/**
*
*
* @export
* @param {父节点} parentElm
* @param {老的子元素} oldCh
* @param {新的子元素} newCh
*/
export default function updataChildren(parentElm, oldCh, newCh) {
//四种命中指针
let newStarIdx = 0; //新前
let newEndIdx = newCh.length - 1; //新后
let oldStarIdx = 0; //旧前
let oldEndIdx = oldCh.length - 1; //旧后
let newStarVnode = newCh[0]; //新前节点
let newEndVnode = newCh[newEndIdx]; //新后节点
let oldStarVnode = oldCh[0]; //旧前节点
let oldEndVnode = oldCh[oldEndIdx]; //旧后节点
let keyMap = null;
//开始大while了
while (newStarIdx <= newEndIdx && oldStarIdx <= oldEndIdx) {
console.log('★');
//首先不是判断四种指针是否命中,而是要略过已经家undefined标记的东西
if (oldStarVnode == null || oldCh[oldStarIdx] == undefined) {
oldStarVnode = oldCh[++oldStarIdx];
} else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStarVnode == null || newCh[newStarIdx] == undefined) {
newStarVnode = newCh[++newStarIdx];
} else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[++newEndIdx];
} else if (checkSameVnode(newStarVnode, oldStarVnode)) {
//新前和旧前
console.log('新前和旧前');
patchVnode(oldStarVnode, newStarVnode)
//前指针向后走
newStarVnode = newCh[++newStarIdx]
oldStarVnode = oldCh[++oldStarIdx]
} else if (checkSameVnode(newEndVnode, oldEndVnode)) {
//新后和旧后
console.log('新后和旧后');
patchVnode(oldEndVnode, newEndVnode)
//后指针向前走
newEndVnode = newCh[--newEndIdx]
oldEndVnode = oldCh[--oldEndIdx]
} else if (checkSameVnode(newEndVnode, oldStarVnode)) {
//新后和旧前
console.log('新后和旧前');
patchVnode(oldStarVnode, newEndVnode)
//当新后和旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
//如何移动节点? 只要你插入一个已经在DOM树上的节点,它就会被移动
parentElm.insertBefore(oldStarVnode.elm, oldEndVnode.elm.nextSibling) //insertBefore方法参数是把第一个参数DOM插入到第二个参数DOM的前面
newEndVnode = newCh[--newEndIdx] //新的后指针向前
oldStarVnode = oldCh[++oldStarIdx] //旧的前指针向后
} else if (checkSameVnode(newStarVnode, oldEndVnode)) {
//新前和旧后
console.log('新前和旧后');
patchVnode(oldEndVnode, newStarVnode)
//当新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
//如何移动节点? 只要你插入一个已经在DOM树上的节点,它就会被移动
parentElm.insertBefore(oldEndVnode.elm, oldStarVnode.elm) //insertBefore方法参数是把第一个参数DOM插入到第二个参数DOM的前面
newStarVnode = newCh[++newStarIdx] //新的前指针向后
oldEndVnode = oldCh[--oldEndIdx] //旧的后指针向前
} else {
//四种命中都没有命中,都没有找到的情况
//寻找key的map
if (!keyMap) {
keyMap = {}
//创建keyMap映射对象,这样就不用每次都遍历老对象了
for (let i = oldStarIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key !== undefined) {
keyMap[key] = i;
}
}
}
//寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
const idxInOld = keyMap[newStarVnode.key];
if (idxInOld == undefined) {
//判断,如果idxInOld是undefined表示他是全新的项
//现在被加入的项(就是被newStartVnode这项)现在还不是真正的DOM节点
parentElm.insertBefore(createElement(newStarVnode), oldStarVnode.elm)
} else {
//如果不是undefined,就表示不是全新的项,而是需要移动
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStarVnode);
//把这项设置为undefined,表示我已经处理完了
oldCh[idxInOld] = undefined;
//移动,调用insertBefore也可以实现移动
parentElm.insertBefore(elmToMove.elm, oldStarVnode.elm)
}
//指针下移,之移动新的头
newStarVnode = newCh[++newStarIdx]
}
}
//继续看看有没有剩余的,循环结束了start还是比End小,老节点走完了新节点还有没走完的,那么剩下start和end中间的节点就是需要增加的节点
if (newStarIdx <= newEndIdx) {
console.log(newCh, newStarIdx, newEndIdx, oldCh[oldEndIdx + 1], parentElm);
const before = oldCh[oldEndIdx + 1] == null ? null : oldCh[oldEndIdx + 1].elm;
console.log(before);
for (let i = newStarIdx; i <= newEndIdx; i++) {
const element = newCh[i];
parentElm.insertBefore(createElement(element), before)
}
} else if (oldStarIdx <= oldEndIdx) {
//批量删除oldStart和oldEnd指针之前的项,新节点走完了老节点还有剩余,start和end之间是老节点需要删除的
for (let i = oldStarIdx; i <= oldEndIdx; i++) {
const element = oldCh[i];
if (element) {
console.log(element);
parentElm.removeChild(element.elm)
}
}
}
}
//判断是否是同一个虚拟节点
function checkSameVnode(a, b) {
return a.key === b.key && a.sel === b.sel
}