在 Vue 2 的 DOM-DIFF 算法中,主要采用以下几种原则来进行高效的比较:
节点类型相同: 只有当新旧 VNodes 的类型(即标签名)和键(key)都相同时,才认为它们是相同的节点。
同层比较: Vue 的 diff 算法只会在同一层级进行 DOM 元素的比较,不会跨层级去比较,这样大大降低了比较的复杂度。
双端比较: 在进行子节点比较时,Vue 2 使用双端比较的策略。即同时从头和尾开始比较,然后向中间靠拢,这样能更快地找出相同节点,减少不必要的 DOM 操作。
列表优化: 对于列表(通常是使用 v-for
渲染的列表),Vue 使用键(key)来标识每一个列表项,以优化重新排序的性能。
静态节点优化: 在编译阶段,Vue 会标记静态节点。因为静态节点不需要再次被比较或修改,所以这会跳过它们,从而提升性能。
批量更新: Vue 会将需要更新的操作存储起来,然后一次性更新 DOM,以减少重排和重绘。
懒更新和异步队列: Vue 使用异步队列来延迟 DOM 更新。只有在必要的时候才会去实际更新 DOM。
组件级别比较: 如果一个组件的数据没有发生变化,那么该组件的整个子树也将不会重新渲染,这也是由于 Vue 2 的组件级别缓存带来的优化。
函数式组件优化: 函数式组件没有状态和实例,所以渲染性能更高。
textContent
。updateProperties
函数来更新元素的属性。updateChildren
函数。这个函数实现了高效的子节点更新算法。它使用了双端比较的策略,通过四个索引(新旧开始和结束索引)和四个变量(新旧开始和结束的 VNode)来进行比较。
同头部节点比较: 如果新旧开始节点相同,就直接 patch
,然后移动新旧开始索引。
同尾部节点比较: 如果新旧结束节点相同,也直接 patch
,然后移动新旧结束索引。
旧头与新尾比较: 如果旧开始节点与新结束节点相同,patch
然后将旧开始节点移动到旧结束节点之后。
新头与旧尾比较: 如果新开始节点与旧结束节点相同,patch
然后将旧结束节点移动到旧开始节点之前。
其他情况: 在旧子节点数组中查找与新开始节点相同的节点,如果找到就进行 patch
,然后移动到旧开始节点之前。
添加新节点: 如果新子节点有剩余,则添加到 DOM 中。
删除旧节点: 如果旧子节点有剩余,则从 DOM 中删除。
通过这样的双端比较策略,updateChildren
可以减少不必要的节点创建和删除,从而提高性能。
createElm
: 根据 VNode 创建真实的 DOM 元素。updateProperties
: 用于更新 DOM 元素的属性。isSameVnode
: 用于判断两个 VNode 是否是相同类型的。通过 patch
和 updateChildren
的递归调用,Vue 2 可以高效地找出新旧虚拟 DOM 树之间的差异,并最小化对实际 DOM 的更改。
mkdir vue2diff
cd vue2diff
npm init -y
npm install webpack webpack-cli webpack-dev-server --save
{
"scripts": {
"build": "webpack --mode=development",
"dev": "webpack serve --mode=development"
}
}
src\index.js
console.log('index');
public\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<br/><button id="differentType">1.标签类型不同</button>
<br/><button id="differentText">2.文本内容不同</button>
<br/><button id="differentAttributes">3.属性不同</button>
<br/><button id="existOldChildren">4.有老儿子没有新儿子</button>
<br/><button id="existNewChildren">5.没有老儿子有新儿子</button>
<br/><button id="push">6.结尾新增元素</button>
<br/><button id="unshift">7.开头新增元素</button>
<br/><button id="pop">8.尾部删除</button>
<br/><button id="shift">9.头部删除</button>
<br/><button id="headToTail">10.头移尾</button>
<br/><button id="tailToHead">11.尾移头</button>
<br/><button id="random">12.随机顺序</button>
<br/><button id="simple">13.简单实现</button>
<script src="main.js"></script>
</body>
</html>
src\index.js
import { createElement,createTextNode } from './vdom';
const vnode = createElement('div', { id: 'container' },
createElement('span', { style: { color: 'red' } }, createTextNode('hello')),
createTextNode('world')
);
console.log(vnode);
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
src\index.js
+import { createElement,createTextNode,mount } from './vdom';
const vnode = createElement('div', { id: 'container' },
createElement('span', { style: { color: 'red' } }, createTextNode('hello')),
createTextNode('world')
);
+let app = document.querySelector('#app');
+mount(app,vnode);
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
+export function mount(container, vnode) {
+ let el = createElm(vnode);
+ container.replaceWith(el);
+}
+function createElm(vnode) {
+ let { tag, children, text } = vnode;
+ if (typeof tag === 'string') {
+ vnode.el = document.createElement(tag);
+ updateProperties(vnode);
+ children.forEach(child => {
+ return vnode.el.appendChild(createElm(child));
+ });
+ } else {
+ vnode.el = document.createTextNode(text);
+ }
+ return vnode.el
+}
+function updateProperties(vnode) {
+ let newProps = vnode.data || {};
+ let el = vnode.el;
+ for (let key in newProps) {
+ if (key === 'style') {
+ for (let styleName in newProps.style) {
+ el.style[styleName] = newProps.style[styleName]
+ }
+ } else if (key === 'class') {
+ el.className = newProps.class
+ } else {
+ el.setAttribute(key, newProps[key]);
+ }
+ }
+}
src\index.js
import './1.differentType';
src\1.differentType.js
import { createElement, createTextNode, mount ,patch} from './vdom';
const oldVnode = createElement('div', { id: 'container' },
createElement('span', { style: { color: 'red' } }, createTextNode('hello')),
createTextNode('world')
);
let app = document.querySelector('#app');
mount(app, oldVnode);
let differentType = document.querySelector('#differentType');
differentType.addEventListener('click', () => {
const newVnode = createElement('p', { id: 'container' },
createElement('span', { style: { color: 'red' } }, createTextNode('hello')),
createTextNode('world')
);
patch(oldVnode, newVnode);
});
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
export function mount(container, vnode) {
let el = createElm(vnode);
container.replaceWith(el);
}
function createElm(vnode) {
let { tag, children, text } = vnode;
if (typeof tag === 'string') {
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
function updateProperties(vnode) {
let newProps = vnode.data || {};
let el = vnode.el;
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else {
el.setAttribute(key, newProps[key]);
}
}
}
+export function patch(oldVnode, newVnode) {
+ if(oldVnode.tag !== newVnode.tag){
+ return oldVnode.el.replaceWith(createElm(newVnode));
+ }
+}
src\index.js
//import './1.differentType';
+import './2.differentText';
src\2.differentText.js
import { createElement, createTextNode, mount ,patch} from './vdom';
let app = document.querySelector('#app');
const oldVnode = createTextNode('hello');
mount(app, oldVnode);
let differentText = document.querySelector('#differentText');
differentText.addEventListener('click', () => {
const newVnode = createTextNode('world');;
patch(oldVnode, newVnode);
});
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
export function mount(container, vnode) {
let el = createElm(vnode);
container.replaceWith(el);
}
function createElm(vnode) {
let { tag, children, text } = vnode;
if (typeof tag === 'string') {
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
function updateProperties(vnode) {
let newProps = vnode.data || {};
let el = vnode.el;
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else {
el.setAttribute(key, newProps[key]);
}
}
}
export function patch(oldVnode, newVnode) {
if(oldVnode.tag !== newVnode.tag){
return oldVnode.el.replaceWith(createElm(newVnode));
}
+ if(!oldVnode.tag){
+ if(oldVnode.text !== newVnode.text){
+ oldVnode.el.textContent = newVnode.text;
+ }
+ return;
+ }
}
src\index.js
//import './1.differentType';
//import './2.differentText';
+import './3.differentAttributes'
src\3.differentAttributes.js
import { createElement,createTextNode, mount ,patch} from './vdom';
let app = document.querySelector('#app');
const oldVnode = createElement('div',{id:'container',style:{color:'red'},class:'oldClass'},createTextNode('hello'));
mount(app, oldVnode);
let differentAttributes = document.querySelector('#differentAttributes');
differentAttributes.addEventListener('click', () => {
const newVnode = createElement('div',{style:{backgroundColor:'green'},class:'newClass'},createTextNode('hello'));
patch(oldVnode, newVnode);
});
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
export function mount(container, vnode) {
let el = createElm(vnode);
+ updateProperties(el,{},vnode.data);
container.replaceWith(el);
}
function createElm(vnode) {
let { tag, children, text } = vnode;
if (typeof tag === 'string') {
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
+function updateProperties(el,oldProps={},newProps={}) {
+ let newStyle = newProps.style || {};
+ let oldStyle = oldProps.style || {};
+ for(let key in oldStyle){
+ if(!newStyle[key]){
+ el.style[key] = ''
+ }
+ }
+ for(let key in oldProps){
+ if(!newProps[key]){
+ el.removeAttribute(key);
+ }
+ }
+ for (let key in newProps) {
+ if (key === 'style') {
+ for (let styleName in newProps.style) {
+ el.style[styleName] = newProps.style[styleName];
+ }
+ } else if (key === 'class') {
+ el.className = newProps.class;
+ } else {
+ el.setAttribute(key, newProps[key]);
+ }
+ }
+}
export function patch(oldVnode, newVnode) {
if(oldVnode.tag !== newVnode.tag){
return oldVnode.el.replaceWith(createElm(newVnode));
}
if(!oldVnode.tag){
if(oldVnode.text !== newVnode.text){
oldVnode.el.textContent = newVnode.text;
}
return;
}
+ let el = newVnode.el = oldVnode.el;
+ updateProperties(el,oldVnode.data,newVnode.data);
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
+import './4.existOldChildren'
src\4.existOldChildren.js
import { createElement, createTextNode, mount ,patch} from './vdom';
const oldVnode = createElement('div', { id: 'container' },
createElement('span', { style: { color: 'red' } }, createTextNode('hello')),
createTextNode('world')
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#existOldChildren').addEventListener('click', () => {
const newVnode = createElement('div', { id: 'container2' } );
patch(oldVnode, newVnode);
});
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
export function mount(container, vnode) {
let el = createElm(vnode);
updateProperties(el,{},vnode.data);
container.replaceWith(el);
}
function createElm(vnode) {
let { tag, children, text } = vnode;
if (typeof tag === 'string') {
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
function updateProperties(el,oldProps={},newProps={}) {
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
for(let key in oldStyle){
if(!newStyle[key]){
el.style[key] = ''
}
}
for(let key in oldProps){
if(!newProps[key]){
el.removeAttribute(key);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
el.className = newProps.class;
} else {
el.setAttribute(key, newProps[key]);
}
}
}
export function patch(oldVnode, newVnode) {
if(oldVnode.tag !== newVnode.tag){
return oldVnode.el.replaceWith(createElm(newVnode));
}
if(!oldVnode.tag){
if(oldVnode.text !== newVnode.text){
oldVnode.el.textContent = newVnode.text;
}
return;
}
let el = newVnode.el = oldVnode.el;
updateProperties(el,oldVnode.data,newVnode.data);
+ let oldChildren = oldVnode.children || [];
+ let newChildren = newVnode.children || [];
+ if(oldChildren.length > 0 && newChildren.length > 0){
//TODO
+ }else if(oldChildren.length > 0 ){
+ el.innerHTML = '';
+ }
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
+import './5.existNewChildren'
src\5.existNewChildren.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('div', { id: 'container2' });
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#existNewChildren').addEventListener('click', () => {
const newVnode = createElement('div', { id: 'container' },
createElement('span', { style: { color: 'red' } }, createTextNode('hello')),
createTextNode('world')
);
patch(oldVnode, newVnode);
});
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
export function mount(container, vnode) {
let el = createElm(vnode);
updateProperties(el,{},vnode.data);
container.replaceWith(el);
}
function createElm(vnode) {
let { tag, children, text } = vnode;
if (typeof tag === 'string') {
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
function updateProperties(el,oldProps={},newProps={}) {
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
for(let key in oldStyle){
if(!newStyle[key]){
el.style[key] = ''
}
}
for(let key in oldProps){
if(!newProps[key]){
el.removeAttribute(key);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
el.className = newProps.class;
} else {
el.setAttribute(key, newProps[key]);
}
}
}
export function patch(oldVnode, newVnode) {
if(oldVnode.tag !== newVnode.tag){
return oldVnode.el.replaceWith(createElm(newVnode));
}
if(!oldVnode.tag){
if(oldVnode.text !== newVnode.text){
oldVnode.el.textContent = newVnode.text;
}
return;
}
let el = newVnode.el = oldVnode.el;
updateProperties(el,oldVnode.data,newVnode.data);
let oldChildren = oldVnode.children || [];
let newChildren = newVnode.children || [];
if(oldChildren.length > 0 && newChildren.length > 0){
}else if(oldChildren.length > 0 ){
el.innerHTML = '';
+ }else if(newChildren.length > 0){
+ for(let i = 0 ; i < newChildren.length ;i++){
+ let child = newChildren[i];
+ el.appendChild(createElm(child));
+ }
+ }
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
+import './6.direct'
src\direct.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#direct').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'A' }, createTextNode('A'))
);
patch(oldVnode, newVnode);
});
src\vdom.js
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text)
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
export function mount(container, vnode) {
let el = createElm(vnode);
updateProperties(el, {}, vnode.data);
container.replaceWith(el);
}
function createElm(vnode) {
let { tag, children, text } = vnode;
if (typeof tag === 'string') {
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
function updateProperties(el, oldProps = {}, newProps = {}) {
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = ''
}
}
for (let key in oldProps) {
if (!newProps[key]) {
el.removeAttribute(key);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
el.className = newProps.class;
} else {
el.setAttribute(key, newProps[key]);
}
}
}
function isSameVnode(oldVnode, newVnode) {
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
export function patch(oldVnode, newVnode) {
if (oldVnode.tag !== newVnode.tag) {
return oldVnode.el.replaceWith(createElm(newVnode));
}
if (!oldVnode.tag) {
if (oldVnode.text !== newVnode.text) {
oldVnode.el.textContent = newVnode.text;
}
return;
}
let el = newVnode.el = oldVnode.el;
updateProperties(el, oldVnode.data, newVnode.data);
let oldChildren = oldVnode.children || [];
let newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
+ updateChildren(el, oldChildren, newChildren)
} else if (oldChildren.length > 0) {
el.innerHTML = '';
} else if (newChildren.length > 0) {
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
}
+function updateChildren(parent, oldChildren, newChildren) {
+ for (let i = 0; i < Math.min(oldChildren.length, newChildren.length); i++) {
+ const oldChild = oldChildren[i];
+ const newChild = newChildren[i];
+ if (isSameVnode(oldChild, newChild)) {
+ patch(oldChild, newChild);
+ } else {
+ oldChild.el.replaceWith(createElm(newChild));
+ }
+ }
+ if (oldChildren.length > newChildren.length) {
+ for (let i = newChildren.length; i < oldChildren.length; i++) {
+ oldChildren[i].el.remove();
+ }
+ }
+ if (newChildren.length > oldChildren.length) {
+ for (let i = oldChildren.length; i < newChildren.length; i++) {
+ let newEl = createElm(newChildren[i]);
+ oldVnode.el.appendChild(newEl);
+ }
+ }
+}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
+import './7.reuse'
src\reuse.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#reuse').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'E' }, createTextNode('E')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'F' }, createTextNode('F')),
createElement('li', { key: 'G' }, createTextNode('G'))
);
patch(oldVnode, newVnode);
});
src\vdom.js
const isString = val => typeof val === 'string';
function isUndef(v) {
return v === undefined || v === null;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
};
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
for (let key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
elm.setAttribute(key, null);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
elm.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
elm.className = newProps.class;
} else {
elm.setAttribute(key, newProps[key]);
}
}
}
function createElm(vnode) {
const {
tag,
data,
children,
text
} = vnode;
if (isString(tag)) {
vnode.elm = document.createElement(tag);
updateDOMProperties(vnode.elm, {}, data);
children.forEach(child => {
return vnode.elm.appendChild(createElm(child));
});
} else {
vnode.elm = document.createTextNode(text);
}
return vnode.elm;
}
export function mount(app, vnode) {
let elm = createElm(vnode);
app.replaceWith(elm);
}
function updateChildren(parentElm, oldChildren, newChildren) {
+ const oldChildrenMap = new Map(oldChildren.map(child => [child.key, child]));
+ let lastPlaceNode = null;
+ for (const newChild of newChildren) {
+ const newKey = newChild.key;
+ const oldChild = oldChildrenMap.get(newKey);
+ if (oldChild) {
+ patch(oldChild, newChild);
+ if (isDef(lastPlaceNode)) {
+ if (lastPlaceNode.nextSibling !== oldChild.elm) {
+ parentElm.insertBefore(oldChild.elm, lastPlaceNode.nextSibling);
+ }
+ } else {
+ parentElm.insertBefore(oldChild.elm, parentElm.firstChild);
+ }
+ lastPlaceNode = oldChild.elm;
+ oldChildrenMap.delete(newKey);
+ } else {
+ const newElm = createElm(newChild);
+ if (isDef(lastPlaceNode)) {
+ parentElm.insertBefore(newElm, lastPlaceNode.nextSibling);
+ } else {
+ parentElm.insertBefore(newElm, parentElm.firstChild);
+ }
+ lastPlaceNode = newElm;
+ }
+ }
+ oldChildrenMap.forEach((oldChild) => oldChild.elm.remove());
}
export function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) {
return;
}
const elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateDOMProperties(elm, oldVnode.data, newVnode.data);
const oldChildren = oldVnode.children || [];
const newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(elm, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
elm.innerHTML = '';
} else if (newChildren.length > 0) {
newChildren.forEach(newChild => {
elm.appendChild(createElm(newChild));
});
}
} else {
if (oldVnode.text !== newVnode.text) {
oldVnode.elm.textContent = newVnode.text;
}
return;
}
}
export function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
return patchVnode(oldVnode, newVnode);
} else {
return oldVnode.elm.replaceWith(createElm(newVnode));
}
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
//import './7.reuse'
+import './8.push'
src\8.push.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C'))
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#push').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'E' }, createTextNode('E'))
);
patch(oldVnode, newVnode);
});
src\vdom.js
const isString = val => typeof val === 'string';
function isUndef(v) {
return v === undefined || v === null;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
};
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
for (let key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
elm.setAttribute(key, null);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
elm.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
elm.className = newProps.class;
} else {
elm.setAttribute(key, newProps[key]);
}
}
}
function createElm(vnode) {
const {
tag,
data,
children,
text
} = vnode;
if (isString(tag)) {
vnode.elm = document.createElement(tag);
updateDOMProperties(vnode.elm, {}, data);
children.forEach(child => {
return vnode.elm.appendChild(createElm(child));
});
} else {
vnode.elm = document.createTextNode(text);
}
return vnode.elm;
}
export function mount(app, vnode) {
let elm = createElm(vnode);
app.replaceWith(elm);
}
function updateChildren(parentElm, oldChildren, newChildren) {
+ let oldStartIdx = 0;
+ let oldStartVnode = oldChildren[0];
+ let oldEndIdx = oldChildren.length - 1;
+ let oldEndVnode = oldChildren[oldEndIdx];
+ let newStartIdx = 0;
+ let newStartVnode = newChildren[0];
+ let newEndIdx = newChildren.length - 1;
+ let newEndVnode = newChildren[newEndIdx];
+ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
+ if (sameVnode(oldStartVnode, newStartVnode)) {
+ patch(oldStartVnode, newStartVnode);
+ oldStartVnode = oldChildren[++oldStartIdx];
+ newStartVnode = newChildren[++newStartIdx];
+ }
+ }
+ if (oldStartIdx > oldEndIdx) {
+ for (let i = newStartIdx; i <= newEndIdx; i++) {
+ parentElm.appendChild(createElm(newChildren[i]));
+ }
+ }
}
export function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) {
return;
}
const elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateDOMProperties(elm, oldVnode.data, newVnode.data);
const oldChildren = oldVnode.children || [];
const newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(elm, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
elm.innerHTML = '';
} else if (newChildren.length > 0) {
newChildren.forEach(newChild => {
elm.appendChild(createElm(newChild));
});
}
} else {
if (oldVnode.text !== newVnode.text) {
oldVnode.elm.textContent = newVnode.text;
}
return;
}
}
export function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
return patchVnode(oldVnode, newVnode);
} else {
return oldVnode.elm.replaceWith(createElm(newVnode));
}
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
//import './7.reuse'
//import './8.push'
+import './9.unshift'
src\9.unshift.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'E' }, createTextNode('E')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#unshift').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'E' }, createTextNode('E'))
);
patch(oldVnode, newVnode);
});
src\vdom.js
const isString = val => typeof val === 'string';
function isUndef(v) {
return v === undefined || v === null;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
};
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
for (let key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
elm.setAttribute(key, null);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
elm.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
elm.className = newProps.class;
} else {
elm.setAttribute(key, newProps[key]);
}
}
}
function createElm(vnode) {
const {
tag,
data,
children,
text
} = vnode;
if (isString(tag)) {
vnode.elm = document.createElement(tag);
updateDOMProperties(vnode.elm, {}, data);
children.forEach(child => {
return vnode.elm.appendChild(createElm(child));
});
} else {
vnode.elm = document.createTextNode(text);
}
return vnode.elm;
}
export function mount(app, vnode) {
let elm = createElm(vnode);
app.replaceWith(elm);
}
function updateChildren(parentElm, oldChildren, newChildren) {
let oldStartIdx = 0;
let oldStartVnode = oldChildren[0];
let oldEndIdx = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIdx];
let newStartIdx = 0;
let newStartVnode = newChildren[0];
let newEndIdx = newChildren.length - 1;
let newEndVnode = newChildren[newEndIdx];
let refElm;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIdx];
newStartVnode = newChildren[++newStartIdx];
+ } else if (sameVnode(oldEndVnode, newEndVnode)) {
+ patch(oldEndVnode, newEndVnode);
+ oldEndVnode = oldChildren[--oldEndIdx];
+ newEndVnode = newChildren[--newEndIdx];
+ }
}
if (oldStartIdx > oldEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
+ refElm = isUndef(newChildren[newEndIdx + 1]) ? null : newChildren[newEndIdx + 1].elm;
+ if(isDef(refElm)){
+ parentElm.insertBefore(createElm(newChildren[i]),refElm);
+ }else{
+ parentElm.appendChild(createElm(newChildren[i]));
+ }
}
}
}
export function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) {
return;
}
const elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateDOMProperties(elm, oldVnode.data, newVnode.data);
const oldChildren = oldVnode.children || [];
const newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(elm, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
elm.innerHTML = '';
} else if (newChildren.length > 0) {
newChildren.forEach(newChild => {
elm.appendChild(createElm(newChild));
});
}
} else {
if (oldVnode.text !== newVnode.text) {
oldVnode.elm.textContent = newVnode.text;
}
return;
}
}
export function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
return patchVnode(oldVnode, newVnode);
} else {
return oldVnode.elm.replaceWith(createElm(newVnode));
}
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
//import './7.reuse'
//import './8.push'
//import './9.unshift'
+import './10.pop'
src\10.pop.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#pop').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B'))
);
patch(oldVnode, newVnode);
});
src\vdom.js
const isString = val => typeof val === 'string';
function isUndef(v) {
return v === undefined || v === null;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
};
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
for (let key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
elm.setAttribute(key, null);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
elm.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
elm.className = newProps.class;
} else {
elm.setAttribute(key, newProps[key]);
}
}
}
function createElm(vnode) {
const {
tag,
data,
children,
text
} = vnode;
if (isString(tag)) {
vnode.elm = document.createElement(tag);
updateDOMProperties(vnode.elm, {}, data);
children.forEach(child => {
return vnode.elm.appendChild(createElm(child));
});
} else {
vnode.elm = document.createTextNode(text);
}
return vnode.elm;
}
export function mount(app, vnode) {
let elm = createElm(vnode);
app.replaceWith(elm);
}
function updateChildren(parentElm, oldChildren, newChildren) {
let oldStartIdx = 0;
let oldStartVnode = oldChildren[0];
let oldEndIdx = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIdx];
let newStartIdx = 0;
let newStartVnode = newChildren[0];
let newEndIdx = newChildren.length - 1;
let newEndVnode = newChildren[newEndIdx];
let refElm;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIdx];
newStartVnode = newChildren[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIdx];
newEndVnode = newChildren[--newEndIdx];
}
}
if (oldStartIdx > oldEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
refElm = isUndef(newChildren[newEndIdx + 1]) ? null : newChildren[newEndIdx + 1].elm;
if (isDef(refElm)) {
parentElm.insertBefore(createElm(newChildren[i]), refElm);
} else {
parentElm.appendChild(createElm(newChildren[i]));
}
}
}else if (newStartIdx > newEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldChildren[i].elm)
}
}
}
export function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) {
return;
}
const elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateDOMProperties(elm, oldVnode.data, newVnode.data);
const oldChildren = oldVnode.children || [];
const newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(elm, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
elm.innerHTML = '';
} else if (newChildren.length > 0) {
newChildren.forEach(newChild => {
elm.appendChild(createElm(newChild));
});
}
+ } else {
+ if (oldVnode.text !== newVnode.text) {
+ oldVnode.elm.textContent = newVnode.text;
+ }
+ return;
+ }
}
export function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
return patchVnode(oldVnode, newVnode);
} else {
return oldVnode.elm.replaceWith(createElm(newVnode));
}
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
//import './7.reuse'
//import './8.push'
//import './9.unshift'
//import './10.pop'
+import './11.shift'
src\9.shift.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#shift').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
);
patch(oldVnode, newVnode);
});
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
//import './7.reuse'
//import './8.push'
//import './9.unshift'
//import './10.pop'
//import './11.shift'
+import './12.headToTail'
src\12.headToTail.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#headToTail').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'A' }, createTextNode('A')),
);
patch(oldVnode, newVnode);
});
src\vdom.js
const isString = val => typeof val === 'string';
function isUndef(v) {
return v === undefined || v === null;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
};
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
for (let key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
elm.setAttribute(key, null);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
elm.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
elm.className = newProps.class;
} else {
elm.setAttribute(key, newProps[key]);
}
}
}
function createElm(vnode) {
const {
tag,
data,
children,
text
} = vnode;
if (isString(tag)) {
vnode.elm = document.createElement(tag);
updateDOMProperties(vnode.elm, {}, data);
children.forEach(child => {
return vnode.elm.appendChild(createElm(child));
});
} else {
vnode.elm = document.createTextNode(text);
}
return vnode.elm;
}
export function mount(app, vnode) {
let elm = createElm(vnode);
app.replaceWith(elm);
}
function updateChildren(parentElm, oldChildren, newChildren) {
let oldStartIdx = 0;
let oldStartVnode = oldChildren[0];
let oldEndIdx = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIdx];
let newStartIdx = 0;
let newStartVnode = newChildren[0];
let newEndIdx = newChildren.length - 1;
let newEndVnode = newChildren[newEndIdx];
let refElm;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIdx];
newStartVnode = newChildren[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIdx];
newEndVnode = newChildren[--newEndIdx];
+ } else if (sameVnode(oldStartVnode, newEndVnode)) {
+ patch(oldStartVnode, newEndVnode);
+ parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
+ oldStartVnode = oldChildren[++oldStartIdx];
+ newEndVnode = newChildren[--newEndIdx]
+ }
}
if (oldStartIdx > oldEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
refElm = isUndef(newChildren[newEndIdx + 1]) ? null : newChildren[newEndIdx + 1].elm;
if (isDef(refElm)) {
parentElm.insertBefore(createElm(newChildren[i]), refElm);
} else {
parentElm.appendChild(createElm(newChildren[i]));
}
}
} else if (newStartIdx > newEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldChildren[i].elm)
}
}
}
export function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) {
return;
}
const elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateDOMProperties(elm, oldVnode.data, newVnode.data);
const oldChildren = oldVnode.children || [];
const newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(elm, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
elm.innerHTML = '';
} else if (newChildren.length > 0) {
newChildren.forEach(newChild => {
elm.appendChild(createElm(newChild));
});
}
} else {
if (oldVnode.text !== newVnode.text) {
oldVnode.elm.textContent = newVnode.text;
}
return;
}
}
export function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
return patchVnode(oldVnode, newVnode);
} else {
return oldVnode.elm.replaceWith(createElm(newVnode));
}
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
//import './7.reuse'
//import './8.push'
//import './9.unshift'
//import './10.pop'
//import './11.shift'
//import './12.headToTail'
+import './13.tailToHead'
src\13.tailToHead.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#tailToHead').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
);
patch(oldVnode, newVnode);
});
src\vdom.js
const isString = val => typeof val === 'string';
function isUndef(v) {
return v === undefined || v === null;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
};
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
for (let key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
elm.setAttribute(key, null);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
elm.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
elm.className = newProps.class;
} else {
elm.setAttribute(key, newProps[key]);
}
}
}
function createElm(vnode) {
const {
tag,
data,
children,
text
} = vnode;
if (isString(tag)) {
vnode.elm = document.createElement(tag);
updateDOMProperties(vnode.elm, {}, data);
children.forEach(child => {
return vnode.elm.appendChild(createElm(child));
});
} else {
vnode.elm = document.createTextNode(text);
}
return vnode.elm;
}
export function mount(app, vnode) {
let elm = createElm(vnode);
app.replaceWith(elm);
}
function updateChildren(parentElm, oldChildren, newChildren) {
let oldStartIdx = 0;
let oldStartVnode = oldChildren[0];
let oldEndIdx = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIdx];
let newStartIdx = 0;
let newStartVnode = newChildren[0];
let newEndIdx = newChildren.length - 1;
let newEndVnode = newChildren[newEndIdx];
let refElm;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIdx];
newStartVnode = newChildren[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIdx];
newEndVnode = newChildren[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patch(oldStartVnode, newEndVnode);
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldChildren[++oldStartIdx];
newEndVnode = newChildren[--newEndIdx]
+ } else if (sameVnode(oldEndVnode, newStartVnode)) {
+ patch(oldEndVnode, newStartVnode);
+ parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
+ oldEndVnode = oldChildren[--oldEndIdx];
+ newStartVnode = newChildren[++newStartIdx]
+ }
}
if (oldStartIdx > oldEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
refElm = isUndef(newChildren[newEndIdx + 1]) ? null : newChildren[newEndIdx + 1].elm;
if (isDef(refElm)) {
parentElm.insertBefore(createElm(newChildren[i]), refElm);
} else {
parentElm.appendChild(createElm(newChildren[i]));
}
}
} else if (newStartIdx > newEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldChildren[i].elm)
}
}
}
export function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) {
return;
}
const elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateDOMProperties(elm, oldVnode.data, newVnode.data);
const oldChildren = oldVnode.children || [];
const newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(elm, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
elm.innerHTML = '';
} else if (newChildren.length > 0) {
newChildren.forEach(newChild => {
elm.appendChild(createElm(newChild));
});
}
} else {
if (oldVnode.text !== newVnode.text) {
oldVnode.elm.textContent = newVnode.text;
}
return;
}
}
export function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
return patchVnode(oldVnode, newVnode);
} else {
return oldVnode.elm.replaceWith(createElm(newVnode));
}
}
src\index.js
//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
//import './7.reuse'
//import './8.push'
//import './9.unshift'
//import './10.pop'
//import './11.shift'
//import './12.headToTail'
//import './13.tailToHead'
+import './14.random'
src\14.random.js
import { createElement, createTextNode, mount, patch } from './vdom';
const oldVnode = createElement('ul', {},
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'C' }, createTextNode('C')),
createElement('li', { key: 'D' }, createTextNode('D')),
);
let app = document.querySelector('#app');
mount(app, oldVnode);
document.querySelector('#random').addEventListener('click', () => {
const newVnode = createElement('ul', {},
createElement('li', { key: 'E' }, createTextNode('E')),
createElement('li', { key: 'B' }, createTextNode('B')),
createElement('li', { key: 'A' }, createTextNode('A')),
createElement('li', { key: 'D' }, createTextNode('D')),
createElement('li', { key: 'F' }, createTextNode('F')),
createElement('li', { key: 'G' }, createTextNode('G'))
);
patch(oldVnode, newVnode);
});
src\vdom.js
const isString = val => typeof val === 'string';
function isUndef(v) {
return v === undefined || v === null;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag;
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
};
}
export function createElement(tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
return vnode(tag, data, key, children);
}
export function createTextNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
for (let key in oldProps) {
if (!newProps.hasOwnProperty(key)) {
elm.setAttribute(key, null);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
elm.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
elm.className = newProps.class;
} else {
elm.setAttribute(key, newProps[key]);
}
}
}
function createElm(vnode) {
const {
tag,
data,
children,
text
} = vnode;
if (isString(tag)) {
vnode.elm = document.createElement(tag);
updateDOMProperties(vnode.elm, {}, data);
children.forEach(child => {
return vnode.elm.appendChild(createElm(child));
});
} else {
vnode.elm = document.createTextNode(text);
}
return vnode.elm;
}
export function mount(app, vnode) {
let elm = createElm(vnode);
app.replaceWith(elm);
}
+function createKeyToOldIdx(children, beginIdx, endIdx) {
+ var i, key;
+ var map = {};
+ for (i = beginIdx; i <= endIdx; ++i) {
+ key = children[i].key;
+ if (isDef(key))
+ map[key] = i;
+ }
+ return map;
+}
+function findIdxInOld(node, oldChildren, start, end) {
+ for (var i = start; i < end; i++) {
+ var c = oldChildren[i];
+ if (isDef(c) && sameVnode(node, c))
+ return i;
+ }
+}
function updateChildren(parentElm, oldChildren, newChildren) {
let oldStartIdx = 0;
let oldStartVnode = oldChildren[0];
let oldEndIdx = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIdx];
let newStartIdx = 0;
let newStartVnode = newChildren[0];
let newEndIdx = newChildren.length - 1;
let newEndVnode = newChildren[newEndIdx];
+ let refElm, oldKeyToIdx, idxInOld, vnodeToMove;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
+ if (isUndef(oldStartVnode)) {
+ oldStartVnode = oldChildren[++oldStartIdx];
+ }else if (isUndef(oldEndVnode)) {
+ oldEndVnode = oldChildren[--oldEndIdx];
+ } else if (sameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIdx];
newStartVnode = newChildren[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIdx];
newEndVnode = newChildren[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patch(oldStartVnode, newEndVnode);
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldChildren[++oldStartIdx];
newEndVnode = newChildren[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patch(oldEndVnode, newStartVnode);
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldChildren[--oldEndIdx];
newStartVnode = newChildren[++newStartIdx]
+ } else {
+ if (isUndef(oldKeyToIdx))
+ oldKeyToIdx = createKeyToOldIdx(oldChildren, oldStartIdx, oldEndIdx);
+ idxInOld = isDef(newStartVnode.key)
+ ? oldKeyToIdx[newStartVnode.key]
+ : findIdxInOld(newStartVnode, oldChildren, oldStartIdx, oldEndIdx);
+ if (isUndef(idxInOld)) {
+ parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm);
+ } else {
+ vnodeToMove = oldChildren[idxInOld];
+ patch(vnodeToMove, newStartVnode);
+ oldChildren[idxInOld] = undefined;
+ parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm);
+ }
+ newStartVnode = newChildren[++newStartIdx];
+ }
}
if (oldStartIdx > oldEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
refElm = isUndef(newChildren[newEndIdx + 1]) ? null : newChildren[newEndIdx + 1].elm;
if (isDef(refElm)) {
parentElm.insertBefore(createElm(newChildren[i]), refElm);
} else {
parentElm.appendChild(createElm(newChildren[i]));
}
}
} else if (newStartIdx > newEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldChildren[i].elm)
}
}
}
export function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) {
return;
}
const elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateDOMProperties(elm, oldVnode.data, newVnode.data);
const oldChildren = oldVnode.children || [];
const newChildren = newVnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(elm, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
elm.innerHTML = '';
} else if (newChildren.length > 0) {
newChildren.forEach(newChild => {
elm.appendChild(createElm(newChild));
});
}
} else {
if (oldVnode.text !== newVnode.text) {
oldVnode.elm.textContent = newVnode.text;
}
return;
}
}
export function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
return patchVnode(oldVnode, newVnode);
} else {
return oldVnode.elm.replaceWith(createElm(newVnode));
}
}