Vue源码:虚拟DOM和diff算法(1)

import vnode from “./vnode”;

import createElement from “./createElement”;

export default function (oldVnode, newVnode) {

// 判断传入的第一个参数,是DOM节点还是虚拟节点

if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {

// 传入的第一个参数是DOM节点,此时要包装为虚拟节点

oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);

}

// 判断oldVnode和newVnode是不是同一个节点

if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {

// 是同一个节点

// TODO:精细化比较

} else {

// 不是同一个节点

createElement(newVnode, oldVnode.elm);

}

};

createElement.js

// 真正创建节点。将vnode创建为DOM,插入到pivot元素之前

export default function (vnode, pivot) {

// 目的是把虚拟节点vnode插入到标杆pivot之前

// 创建一个DOM节点,这个节点目前还是孤儿节点

let domNode = document.createElement(vnode.sel);

// 有子节点还是文本

if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {

// 内部是文字

domNode.innerText = vnode.text;

// 将孤儿节点上树。让标杆节点的父元素调用insertBefore方法,将新的孤儿节点插入到标签节点之前

pivot.parentNode.insertBefore(domNode, pivot);

} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {

}

};

手写递归创建子节点


为了适应递归操作,将插入操作放入到patch.js中,而不是在createElement中进行

这里处理的是diff处理新旧节点不是同一个节点的情况,创建新的插入并暴力删除

index.js

import h from ‘./mySnabbdom/h’;

import patch from ‘./mySnabbdom/patch’

const container = document.getElementById(‘container’);

const btn = document.getElementById(‘btn’);

const myVnode1 = h(‘h1’, {}, ‘你好’);

const myVnode2 = h(‘ul’, {}, [

h(‘li’, {}, ‘A’),

h(‘li’, {}, ‘B’),

h(‘li’, {}, [

h(‘div’, {}, [

h(‘ol’, {}, [

h(‘li’, {}, ‘哈哈哈’),

h(‘li’, {}, ‘嘿嘿嘿’),

h(‘li’, {}, ‘呵呵呵’),

])

])

]),

h(‘li’, {}, ‘D’),

])

const myVnode3 = h(‘section’, {}, [

h(‘h1’, {}, ‘我是新的h1’),

h(‘h2’, {}, ‘我是新的h2’),

h(‘h3’, {}, ‘我是新的h3’),

])

patch(container, myVnode2);

btn.onclick = function () {

patch(myVnode2, myVnode3);

}

createElement.js

// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入

export default function createElement(vnode) {

// console.log(目的是把虚拟节点${vnode}变成真正的DOM)

// 创建一个DOM节点,这个节点目前还是孤儿节点

let domNode = document.createElement(vnode.sel);

// 有子节点还是文本

if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {

// 内部是文字

domNode.innerText = vnode.text;

// 补充elm属性

vnode.elm = domNode;

} 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,elm是一个纯DOM对象

return vnode.elm;

};

patch.js

import vnode from “./vnode”;

import createElement from “./createElement”;

export default function (oldVnode, newVnode) {

// 判断传入的第一个参数,是DOM节点还是虚拟节点

if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {

// 传入的第一个参数是DOM节点,此时要包装为虚拟节点

oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);

}

// 判断oldVnode和newVnode是不是同一个节点

if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {

// 是同一个节点

// TODO:精细化比较

} else {

// 不是同一个节点

let newVnodeElm = createElement(newVnode);

// 插入到老节点之前

if (oldVnode.elm.parentNode && newVnodeElm)

oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);

// 删除老节点

oldVnode.elm.parentNode.removeChild(oldVnode.elm);

}

};

diff处理新旧节点是同一个节点时候


在这里插入图片描述

手写新旧节点text的不同情况


patch.js

import vnode from “./vnode”;

import createElement from “./createElement”;

export default function (oldVnode, newVnode) {

// 判断传入的第一个参数,是DOM节点还是虚拟节点

if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {

// 传入的第一个参数是DOM节点,此时要包装为虚拟节点

oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);

}

// 判断oldVnode和newVnode是不是同一个节点

if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {

console.log(‘是同一个节点’)

// 判断新旧node是否是同一个对象

if (oldVnode === newVnode) return;

// 判断newVnode有没有text属性

if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {

// 新vnode有text属性

// console.log(‘新vnode有text属性’)

if (newVnode.text !== oldVnode.text)

// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉

oldVnode.elm.innerText = newVnode.text;

} else {

// 新vnode没有text属性,有children

// console.log(‘新vnode没有text属性’)

// 判断老的有没有children

if (oldVnode.children !== undefined && oldVnode.children.length > 0) {

// 老的有children,此时就是最复杂的情况。就是新老都有children

} else {

// 老的没有children,新的有children

// 清空老的节点的内容

oldVnode.elm.innerHTML = ‘’;

// 遍历newVnode的子节点,创建dom,循环上树

newVnode.children.forEach(node => {

let dom = createElement(node);

oldVnode.elm.appendChild(dom);

})

}

}

} else {

// 不是同一个节点

let newVnodeElm = createElement(newVnode);

// 插入到老节点之前

if (oldVnode.elm.parentNode && newVnodeElm)

oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);

// 删除老节点

oldVnode.elm.parentNode.removeChild(oldVnode.elm);

}

};

vnode中添加key


如果data中存在key,将key也绑定在vnode上

vnode.js

/**

  • vnode函数的功能非常简单,就是把传入的5个参数组合对象返回

*/

export default function (sel, data, children, text, elm) {

const key = data.key;

return {

sel, data, children, text, elm, key

};

}

尝试书写diff更新子节点


patchVnode.js

import createElement from “./createElement”;

export default function patchVnode(oldVnode, newVnode) {

// 判断新旧node是否是同一个对象

if (oldVnode === newVnode) return;

// 判断newVnode有没有text属性

if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {

// 新vnode有text属性

// console.log(‘新vnode有text属性’)

if (newVnode.text !== oldVnode.text)

// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉

oldVnode.elm.innerText = newVnode.text;

} else {

// 新vnode没有text属性,有children

// console.log(‘新vnode没有text属性’)

// 判断老的有没有children

if (oldVnode.children !== undefined && oldVnode.children.length > 0) {

// 老的有children,此时就是最复杂的情况。就是新老都有children

// 所有未处理的节点的开头

let un = 0;

for (let i = 0; i < newVnode.children.length; i++) {

let ch = newVnode.children[i];

// 再次遍历,看看oldVnode中有没有节点和它是same的

let isExist = false;

for (let j = 0; j < oldVnode.children.length; j++) {

if (oldVnode.children[j].sel === ch.sel && oldVnode.children[j].key === ch.key) {

isExist = true;

}

}

if (!isExist) {

let dom = createElement(ch);

ch.elm = dom;

if (un < oldVnode.children.length)

oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm);

else

oldVnode.elm.appendChild(dom);

} else {

// 让处理的节点下移一位

un++;

// 判断前后节点位置是否一致

}

}

} else {

// 老的没有children,新的有children

// 清空老的节点的内容

oldVnode.elm.innerHTML = ‘’;

// 遍历newVnode的子节点,创建dom,循环上树

newVnode.children.forEach(node => {

let dom = createElement(node);

oldVnode.elm.appendChild(dom);

})

}

}

}

如下面三种情况,所有情况非常复杂,杂糅在一起,因此需要一个优秀的更新算法

新增的情况

新创建的节点要插入到所有未处理节点之前,而不是所有已处理节点之后。

例如途中,如果插入MN,所有已处理节点为AB,则MN会依次插入到B之后,与目的不一致。

在这里插入图片描述

删除的情况

在这里插入图片描述

更新的情况

在这里插入图片描述

diff算法的子节点更新策略


四种命中查找:

新前:新的虚拟节点当中的所有没有处理的开头的节点

新后:新的虚拟节点当中的所有没有处理的最后的节点

旧前:旧的虚拟节点当中的所有没有处理的开头的节点

旧后:旧的虚拟节点当中的所有没有处理的最后的节点

经典的diff算法优化策略

  • 新前与旧前:如果是旧节点先循环完毕,说明新节点中有要插入的节点。

  • 新后与旧后:如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和新后指针中间的节点),说明他们是要被删除的节点。

  • 新后与旧前:(此种发生了,涉及移动节点,那么新前指向的节点,移动的旧后之后

  • 新前与旧后:(此种发生了,涉及移动节点,那么新前指向的节点,移动的旧前之前

命中一种就不再进行命中判断了

如果都没有命中,就需要用循环来寻找了。移动到oldStartIdx之前。

新增的情况


在这里插入图片描述

在这里插入图片描述

删除的情况


在这里插入图片描述

多删除的情况


在这里插入图片描述

复杂的情况


在这里插入图片描述

在这里插入图片描述

手写子节点更新策略


updateChildren.js

import patchVnode from “./patchVnode”;

import createElement from “./createElement”;

// 判断是否是同一个虚拟节点

function checkSameVnode(vnodeA, vnodeB) {

return vnodeA.sel === vnodeB.sel && vnodeA.key === vnodeB.key;

}

export default function updateChildren(parentElm, oldCh, newCh) {

// console.log(‘我是updateChildren’)

// console.log(oldCh, newCh)

// 定义旧前、新前、旧后、新后 编号

let oldStartIdx = 0, newStartIdx = 0, oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1;

// 定义旧前、旧后、新前、新后 节点

let oldStartVnode = oldCh[oldStartIdx], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[newStartIdx],

newEndVnode = newCh[newEndIdx];

let keyMap = null;

// 开始大while

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

// 首先不是判断①②③④命中,而是要略过已经加undefined标记的东西

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(newStartVnode, oldStartVnode)) {

// 新前与旧前

console.log(“①新前和旧前命中”)

patchVnode(oldStartVnode, newStartVnode);

newStartVnode = newCh[++newStartIdx];

oldStartVnode = oldCh[++oldStartIdx];

} else if (checkSameVnode(newEndVnode, oldEndVnode)) {

// 新后与旧后

console.log(“②新后与旧后命中”)

patchVnode(oldEndVnode, newEndVnode);

oldEndVnode = oldCh[–oldEndIdx];

newEndVnode = newCh[–newEndIdx];

} else if (checkSameVnode(newEndVnode, oldStartVnode)) {

// 新后与旧前

console.log(“③新后与旧前命中”)

patchVnode(oldStartVnode, newEndVnode);

// 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面

// 如何移动节点??只要插入一个已经在DOm树上的节点,它就会被移动

parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);

newEndVnode = newCh[–newEndIdx];

oldStartVnode = oldCh[++oldStartIdx];

} else if (checkSameVnode(newStartVnode, oldEndVnode)) {

// 新前与旧后

console.log(“④新前与旧后命中”)

patchVnode(oldEndVnode, newStartVnode);

// 当④新前与旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面

// 如何移动节点??只要插入一个已经在DOm树上的节点,它就会被移动

parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);

newStartVnode = newCh[++newStartIdx];

oldEndVnode = oldCh[–oldEndIdx];

} 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;

}

}

//console.log(keyMap);

// 寻找当前这项(nextStartIdx)这项在keyMap中的映射的位置序号

const idxInOld = keyMap[newStartVnode.key];

// console.log(idxInOld);

if (idxInOld === undefined) {

// 判断,如果idxInOlx是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];

}

}

// while结束后,需要继续看看有没有剩余,判断是否需要删除或新增节点

if (newStartIdx <= newEndIdx) {

// console.log(‘new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStart之前’);

// 循环结束后,start还是比old小

// before是插入的标杆

// const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;

// 遍历新的newCh,添加到老的没有处理的之前

for (let i = newStartIdx; i <= newEndIdx; i++) {

// insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。

// newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM

// parentElm.insertBefore(createElement(newCh[i]), before);

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);

}

};

完整版手写

================================================================

在这里插入图片描述

patch.js


import vnode from “./vnode”;

import createElement from “./createElement”;

import patchVnode from “./patchVnode”;

export default function patch(oldVnode, newVnode) {

// 判断传入的第一个参数,是DOM节点还是虚拟节点

if (oldVnode.sel === ‘’ || oldVnode.sel === undefined) {

// 传入的第一个参数是DOM节点,此时要包装为虚拟节点

oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);

}

// 判断oldVnode和newVnode是不是同一个节点

if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {

// console.log(‘是同一个节点’)

patchVnode(oldVnode, newVnode);

} else {

// 不是同一个节点

let newVnodeElm = createElement(newVnode);

// 插入到老节点之前

if (oldVnode.elm.parentNode && newVnodeElm)

oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);

// 删除老节点

oldVnode.elm.parentNode.removeChild(oldVnode.elm);

}

};

vnode.js


/**

  • vnode函数的功能非常简单,就是把传入的5个参数组合对象返回

*/

export default function (sel, data, children, text, elm) {

const key = data.key;

return {

sel, data, children, text, elm, key

};

}

createElement.js


// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入

export default function createElement(vnode) {

// console.log(目的是把虚拟节点${vnode}变成真正的DOM)

// 创建一个DOM节点,这个节点目前还是孤儿节点

let domNode = document.createElement(vnode.sel);

// 有子节点还是文本

if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {

// 内部是文字

domNode.innerText = vnode.text;

// 补充elm属性

vnode.elm = domNode;

} 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,elm是一个纯DOM对象

return vnode.elm;

};

patchVnode.js


import createElement from “./createElement”;

import updateChildren from “./updateChildren”;

// 对比同一个虚拟节点

export default function patchVnode(oldVnode, newVnode) {

// 判断新旧node是否是同一个对象

if (oldVnode === newVnode) return;

// 判断newVnode有没有text属性

if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {

// 新vnode有text属性

// console.log(‘新vnode有text属性’)

if (newVnode.text !== oldVnode.text)

// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉

oldVnode.elm.innerText = newVnode.text;

} else {

// 新vnode没有text属性,有children

// console.log(‘新vnode没有text属性’)

// 判断老的有没有children

if (oldVnode.children !== undefined && oldVnode.children.length > 0) {

// 老的有children,新的也有children,此时就是最复杂的情况。就是新老都有children

updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);

} else {

// 老的没有children,新的有children

// 清空老的节点的内容

oldVnode.elm.innerHTML = ‘’;

// 遍历newVnode的子节点,创建dom,循环上树

newVnode.children.forEach(node => {

let dom = createElement(node);

oldVnode.elm.appendChild(dom);

})

}

}

}

h.js


import vnode from “./vnode”;

/*

  • 编写一个低配版本的h函数,这个函数必须要接收3个参数,缺一不可 —— 重载功能较弱

  • 也就是说,调用的时候形态必须是下面三种之一:

  • 形态① h(‘div’, {}, ‘文字’)

  • 形态② h(‘div’, {}, [])

  • 形态③ h(‘div’, {}, h())

  • */

export default function (sel, data, c) {

// 检查参数的个数

if (arguments.length !== 3)

throw new Error(‘对不起,h函数必须传入3个参数,我们是低配版h函数’);

// 检查参数c的类型

if (typeof c === ‘string’ || typeof c === ‘number’) {

return vnode(sel, data, undefined, c, undefined);

} else if (Array.isArray©) {

// 说明现在调用h函数是形态②

let children = [];

// 遍历c,手机children

for (let i = 0; i < c.length; i++) {

// 检查c[i]必须是一个对象

if (!(typeof c[i] === ‘object’ && c[i].hasOwnProperty(‘sel’)))

throw new Error(‘传入的数组参数中有项不是h函数’);

// 这里不用执行c[i],因为测试语句中已经执行了

// 只需要收集好children

children.push(c[i]);

}

// 循环结束了,说明children收集完毕了,此时可以返回虚拟节点,有children节点

return vnode(sel, data, children, undefined, undefined);

} else if (typeof c === ‘object’ && c.hasOwnProperty(‘sel’)) {

// 说明现在调用h函数是形态③

// 即传入的c是唯一的children. 不用执行c,因为测试语句中已经执行了c

return vnode(sel, data, [c], undefined, undefined);

} else {

throw new Error(‘传入的参数类型有误’);

}

};

updateChildren.js


import patchVnode from “./patchVnode”;

import createElement from “./createElement”;

// 判断是否是同一个虚拟节点

function checkSameVnode(vnodeA, vnodeB) {

return vnodeA.sel === vnodeB.sel && vnodeA.key === vnodeB.key;

}

export default function updateChildren(parentElm, oldCh, newCh) {

// console.log(‘我是updateChildren’)

// console.log(oldCh, newCh)

// 定义旧前、新前、旧后、新后 编号

let oldStartIdx = 0, newStartIdx = 0, oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1;

// 定义旧前、旧后、新前、新后 节点

let oldStartVnode = oldCh[oldStartIdx], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[newStartIdx],

newEndVnode = newCh[newEndIdx];

let keyMap = null;

// 开始大while

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

// 首先不是判断①②③④命中,而是要略过已经加undefined标记的东西

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(newStartVnode, oldStartVnode)) {

// 新前与旧前

console.log(“①新前和旧前命中”)

patchVnode(oldStartVnode, newStartVnode);

newStartVnode = newCh[++newStartIdx];

oldStartVnode = oldCh[++oldStartIdx];

} else if (checkSameVnode(newEndVnode, oldEndVnode)) {

// 新后与旧后

console.log(“②新后与旧后命中”)

patchVnode(oldEndVnode, newEndVnode);

oldEndVnode = oldCh[–oldEndIdx];

newEndVnode = newCh[–newEndIdx];

} else if (checkSameVnode(newEndVnode, oldStartVnode)) {

// 新后与旧前

console.log(“③新后与旧前命中”)

patchVnode(oldStartVnode, newEndVnode);

// 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面

// 如何移动节点??只要插入一个已经在DOm树上的节点,它就会被移动

parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);

newEndVnode = newCh[–newEndIdx];

oldStartVnode = oldCh[++oldStartIdx];

} else if (checkSameVnode(newStartVnode, oldEndVnode)) {

// 新前与旧后

console.log(“④新前与旧后命中”)

patchVnode(oldEndVnode, newStartVnode);

// 当④新前与旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面

// 如何移动节点??只要插入一个已经在DOm树上的节点,它就会被移动

parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);

newStartVnode = newCh[++newStartIdx];

oldEndVnode = oldCh[–oldEndIdx];

} 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;

}

}

//console.log(keyMap);

// 寻找当前这项(nextStartIdx)这项在keyMap中的映射的位置序号

const idxInOld = keyMap[newStartVnode.key];

// console.log(idxInOld);

if (idxInOld === undefined) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后的最后

面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
需要完整面试题的朋友可以点击蓝色字体免费获取

大厂面试题

面试题目录

ore(oldEndVnode.elm, oldStartVnode.elm);

newStartVnode = newCh[++newStartIdx];

oldEndVnode = oldCh[–oldEndIdx];

} 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;

}

}

//console.log(keyMap);

// 寻找当前这项(nextStartIdx)这项在keyMap中的映射的位置序号

const idxInOld = keyMap[newStartVnode.key];

// console.log(idxInOld);

if (idxInOld === undefined) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-d3ydWDts-1713576142009)]

[外链图片转存中…(img-fuNa7JlP-1713576142010)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-qBhZuy0d-1713576142010)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

[外链图片转存中…(img-UPolir6c-1713576142010)]

最后的最后

面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
需要完整面试题的朋友可以点击蓝色字体免费获取

[外链图片转存中…(img-yyZApK5r-1713576142011)]

[外链图片转存中…(img-bzrQ9VXB-1713576142011)]

[外链图片转存中…(img-PFvxNpQP-1713576142011)]

[外链图片转存中…(img-iE2celWE-1713576142011)]

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值