1.DOM-DIFF原则 #

在 Vue 2 的 DOM-DIFF 算法中,主要采用以下几种原则来进行高效的比较:

  1. 节点类型相同: 只有当新旧 VNodes 的类型(即标签名)和键(key)都相同时,才认为它们是相同的节点。

  2. 同层比较: Vue 的 diff 算法只会在同一层级进行 DOM 元素的比较,不会跨层级去比较,这样大大降低了比较的复杂度。

  3. 双端比较: 在进行子节点比较时,Vue 2 使用双端比较的策略。即同时从头和尾开始比较,然后向中间靠拢,这样能更快地找出相同节点,减少不必要的 DOM 操作。

  4. 列表优化: 对于列表(通常是使用 v-for 渲染的列表),Vue 使用键(key)来标识每一个列表项,以优化重新排序的性能。

  5. 静态节点优化: 在编译阶段,Vue 会标记静态节点。因为静态节点不需要再次被比较或修改,所以这会跳过它们,从而提升性能。

  6. 批量更新: Vue 会将需要更新的操作存储起来,然后一次性更新 DOM,以减少重排和重绘。

  7. 懒更新和异步队列: Vue 使用异步队列来延迟 DOM 更新。只有在必要的时候才会去实际更新 DOM。

  8. 组件级别比较: 如果一个组件的数据没有发生变化,那么该组件的整个子树也将不会重新渲染,这也是由于 Vue 2 的组件级别缓存带来的优化。

  9. 函数式组件优化: 函数式组件没有状态和实例,所以渲染性能更高。

2.比较过程 #

2.1. patch 函数 #

2.2. updateChildren 函数 #

这个函数实现了高效的子节点更新算法。它使用了双端比较的策略,通过四个索引(新旧开始和结束索引)和四个变量(新旧开始和结束的 VNode)来进行比较。

  1. 同头部节点比较: 如果新旧开始节点相同,就直接 patch,然后移动新旧开始索引。

  2. 同尾部节点比较: 如果新旧结束节点相同,也直接 patch,然后移动新旧结束索引。

  3. 旧头与新尾比较: 如果旧开始节点与新结束节点相同,patch 然后将旧开始节点移动到旧结束节点之后。

  4. 新头与旧尾比较: 如果新开始节点与旧结束节点相同,patch 然后将旧结束节点移动到旧开始节点之前。

  5. 其他情况: 在旧子节点数组中查找与新开始节点相同的节点,如果找到就进行 patch,然后移动到旧开始节点之前。

  6. 添加新节点: 如果新子节点有剩余,则添加到 DOM 中。

  7. 删除旧节点: 如果旧子节点有剩余,则从 DOM 中删除。

通过这样的双端比较策略,updateChildren 可以减少不必要的节点创建和删除,从而提高性能。

2.3. 辅助函数 #

通过 patchupdateChildren 的递归调用,Vue 2 可以高效地找出新旧虚拟 DOM 树之间的差异,并最小化对实际 DOM 的更改。

3.新建项目 #

3.1 创建项目 #

mkdir vue2diff
cd vue2diff
npm init -y
npm install webpack webpack-cli webpack-dev-server --save

3.2 package.json #

{
  "scripts": {
    "build": "webpack --mode=development",
    "dev": "webpack serve --mode=development"
  }
}

3.3 src\index.js #

src\index.js

console.log('index');

3.4 public\index.html #

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>

4.虚拟DOM #

4.1 src\index.js #

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

4.2 src\vdom.js #

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

5.DOM挂载 #

5.1 src\index.js #

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

5.2 src\vdom.js #

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]);
+        }
+    }
+}

6.类型不同 #

6.1 src\index.js #

src\index.js

import './1.differentType';

6.2 1.differentType.js #

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

6.3 src\vdom.js #

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

7.文本不同 #

7.1 src\index.js #

src\index.js

//import './1.differentType';
+import './2.differentText';

7.2 2.differentText.js #

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

7.3 src\vdom.js #

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

8.属性更新 #

8.1 src\index.js #

src\index.js

//import './1.differentType';
//import './2.differentText';
+import './3.differentAttributes'

8.2 3.differentAttributes.js #

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

8.2 src\vdom.js #

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

9.有老儿子没有新儿子 #

9.1 src\index.js #

src\index.js

//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
+import './4.existOldChildren'

9.2 4.existOldChildren.js #

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

9.3 src\vdom.js #

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 = '';
+   }
}

10.没有老儿子有新儿子 #

10.1 src\index.js #

src\index.js

//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
+import './5.existNewChildren'

10.2 5.existNewChildren.js #

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

10.3 src\vdom.js #

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

11.直接对比 #

11.1 src\index.js #

src\index.js

//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
+import './6.direct'

11.2 src\direct.js #

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

11.3 vdom.js #

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

12.重用老DOM元素 #

12.1 src\index.js #

src\index.js

//import './1.differentType';
//import './2.differentText';
//import './3.differentAttributes'
//import './4.existOldChildren'
//import './5.existNewChildren'
//import './6.direct'
+import './7.reuse'

12.2 src\reuse.js #

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

12.3 vdom.js #

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

13.(push)结尾新增元素 #

13.1 src\index.js #

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'

13.2 8.push.js #

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

13.3 src\vdom.js #

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

14.(unshift)开头新增元素 #

14.1 src\index.js #

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'

14.2 9.unshift.js #

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

14.3 src\vdom.js #

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

15.(pop)尾部删除 #

15.1 src\index.js #

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'

15.2 10.pop.js #

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

15.3 src\vdom.js #

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

16.(shift)头部删除 #

16.1 src\index.js #

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'

16.2 src\9.shift.js #

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

17.置于底部 #

17.1 src\index.js #

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'

17.2 12.headToTail.js #

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

17.3 src\vdom.js #

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

18.置于项部 #

18.1 src\index.js #

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'

18.2 13.tailToHead.js #

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

18.3 src\vdom.js #

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

19.无规律 #

19.1 src\index.js #

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'

19.2 14.random.js #

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

19.3 src\vdom.js #

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