Vue原理解析之diff算法

Vue原理解析之diff算法

一、简述

​ 以装修房子为例,如果我们仅需要在客厅新添一座沙发或者将卧室的床换个位置。那么将整个房子重新翻修显然是不切实际的,我们通常的做法是在原先装修的基础上做微小的改动即可。

​ 对于 DOM 树来讲,也是同样的道理,如果仅仅是新增了一个标签或者修改了某一个标签的属性或内容。那么引起整个 DOM 树的重新渲染显然是对性能和资源的极大浪费,虽然我们的计算机每秒能进行上亿次的计算。实际上,我们只需要找出新旧 DOM 树存在差异的地方,只针对这一块区域进行重新渲染就可以了。

​ 所以 Diff 算法应运而生,diff 取自 different (不同的),Diff算法的作用,总结来说,就是:精细化对比,最小量更新。

二、虚拟DOM

  • 用 JavaScript 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性。

  • diff 是发生在虚拟 DOM 上的:新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真实的 DOM 上。

真实DOM:

<div class="box">
  <h3>我是一个标题h3>
  <ul>
    <li>HTMLli>
    <li>CSSli>
    <li>JavaScriptli>
  ul>
div>

虚拟DOM:

{
  sel: div,
  data: {
    class: box
  },
  children: [
    { sel: h3, data: {}, text: 我是一个标题 },
    { sel: ul, data: {}, children: [
      { sel: li, data: {}, text: HTML }
      { sel: li, data: {}, text: CSS }
      { sel: li, data: {}, text: JavaScript }
    ]
  ]
}

三、h函数

作用:生成虚拟节点(Vnode)

示例:

h('ul', {}, [
  h('li', {}, 'HTML'),
  h('li', {}, 'CSS'),
  h('li', {}, 'JavaScript')
])
// 会得到
{
  "sel": "ul",
  "data": {},
  "children": [
    { "sel": "li", "data": {}, "text": "HTML" },
    { "sel": "li", "data": {}, "text": "CSS" },
    { "sel": "li", "data": {}, "text": "JavaScript" }
  ]
}

手写简单版h函数

/**
 * @name: 简单版h函数
 * @param {*} sel 选择器
 * @param {*} data 属性
 * @param {*} c 子节点或文字
 * @return {*} vnode虚拟节点
 */
export default function(sel, data, c) {
    if (arguments.length !== 3) throw new Error('h函数要求3个参数')
    if (typeof c === 'string' || typeof c === 'number') {
        // 判断参数c是否为文字
        return vnode(sel, data, undefined, c, undefined)
    } else if (typeof c === 'object' && c.sel) {
        // 判断参数c是否为单个h函数
        return vnode(sel, data, c, undefined, undefined)
    } else if (Array.isArray(c)) {
        // 判断参数c是否为数组
        let children = []
        // 循环遍历参数c收集children
        for (let i = 0; i < c.length; i++) {
            if (typeof c[i] === 'object' && c[i].sel) {
                children.push(c[i])
            } else if (typeof c[i] === 'string' || typeof c[i] === 'number') {
                children.push(
                    vnode(undefined, undefined, undefined, c[i], undefined)
                )
            } else {
                console.log(c[i]);
                throw new Error('children中有子项类型错误')
            }
        }
        return vnode(sel, data, children, undefined, undefined)
    } else {
        throw new Error('第三个参数类型错误')
    }
}

/**
 * @param {*} sel 选择器
 * @param {*} data 属性
 * @param {*} children 子节点
 * @param {*} text 文字节点
 * @param {*} el 真实dom
 * @return {*}
 */
function vnode(sel, data, children, text, el) {
    const key = data.key
    return {
        sel, data, children, text, el, key
    }
}

四、diff算法的鼻祖——snabbdom

  • 官方 Git:https://github.com/snabbdom/snabbdom
import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
  } from "snabbdom";
  
  const patch = init([
    // Init patch function with chosen modules
    classModule, // makes it easy to toggle classes
    propsModule, // for setting properties on DOM elements
    styleModule, // handles styling on elements with support for animations
    eventListenersModule, // attaches event listeners
  ]);
  
  const container = document.getElementById("container");
  
  const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
    h("span", { style: { fontWeight: "bold" } }, "This is bold"),
    " and this is just normal text",
    h("a", { props: { href: "/foo" } }, "I'll take you places!"),
  ]);
  console.log(vnode);
  // Patch into empty DOM element – this modifies the DOM as a side effect
  patch(container, vnode);
  
  const newVnode = h(
    "div#container.two.classes",
    { on: { click: anotherEventHandler } },
    [
      h(
        "span",
        { style: { fontWeight: "normal", fontStyle: "italic" } },
        "This is now italic type"
      ),
      " and this is still just normal text",
      h("a", { props: { href: "/bar" } }, "I'll take you places!"),
    ]
  );
  // Second `patch` invocation
  patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
  

  function someFn() {
      console.log(111);
  }

  function anotherEventHandler() {
    console.log(222);
  }

五、diff算法原理

  • diff 算法确实是最小量更新,key 很重要,key 是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点
  • 只有是同一个虚拟节点,才进行虚拟化比较,否则就是暴力删除旧的、插入新的。延伸问题:如何定义同一个虚拟节点?答:选择器相同且 key 相同
  • 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是如果跨层了,那么 diff 算法也不会进行精细化比较。而是暴力删除旧的、然后插入新的。

diff算法比较流程图:

Vue原理解析之diff算法_第1张图片

patch函数手写代码:

import vnode from './vnode'
import createElement from './createElement'
import patchVnode from './patchVnode'

export default function patch(oldVnode, newVnode) {
    if (!oldVnode.sel) {
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    if (oldVnode.sel !== newVnode.sel || oldVnode.key !== newVnode.key) {
        let newDom = createElement(newVnode)
        if (oldVnode.el && newDom) {
            oldVnode.el.parentNode.insertBefore(newDom, oldVnode.el)
        }
        oldVnode.el.parentNode.removeChild(oldVnode.el)
    } else {
        patchVnode(oldVnode, newVnode)
    }
}

patchVnode函数手写代码:

import updateChildren from "./updateChildren"

export default function patchVnode(oldVnode, newVnode) {
    // 判断新旧节点内存是否指向统一对象
    if (oldVnode === newVnode) return
    // 新节点有文字且没有children
    if (newVnode.text && (newVnode.children == undefined || newVnode.children.length < 1)) {
        // 直接将oldVnode的innerText改为newVnode.text
        if (newVnode.text !== oldVnode.text) {
            oldVnode.el.innerText = newVnode.text
        }
    } else {
        if (oldVnode.children === undefined || oldVnode.children.length < 1) {
            // newVnode有children oldVnode没有children有text
            oldVnode.el.innerText = ''
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i])
                oldVnode.el.appendChild(dom)
            }
        } else {
            console.log('比较子节点');
            updateChildren(oldVnode.el, oldVnode.children, newVnode.children)
        }
    }
}

updateChildren函数手写就代码:

import createElement from "./createElement"
import patchVnode from "./patchVnode"

export default function updateChildren(parentElm, oldCh, newCh) {
    let newStartIdx = 0
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[newStartIdx]
    let newEndVnode = newCh[newEndIdx]
    let oldStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[oldStartIdx]
    let oldEndVnode = oldCh[oldEndIdx]

    let before = null
    let keyMap = null

    while (newEndIdx >= newStartIdx && oldEndIdx >= oldStartIdx) {
        if (!oldEndVnode) {
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (!oldStartVnode) {
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (!newEndVnode) {
            newEndVnode = newCh[--newEndIdx]
        } else if (!newStartVnode) {
            newStartVnode = newCh[++newStartIdx]
        } else if (isSameNode(newStartVnode, oldStartVnode)) { // 新前与旧前比较
            console.log('1、新前与旧前相同');
            patchVnode(oldStartVnode, newStartVnode)
            newStartVnode = newCh[++newStartIdx]
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (isSameNode(newEndVnode, oldEndVnode)) { // 新后与旧后比较
            console.log('2、新后与旧后相同');
            patchVnode(oldEndVnode, newEndVnode)
            newEndVnode = newCh[--newEndIdx]
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (isSameNode(newEndVnode, oldStartVnode)) { // 新后与旧前比较
            console.log('3、新后与旧前相同');
            patchVnode(oldStartVnode, newEndVnode)
            parentElm.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
            newEndVnode = newCh[--newEndIdx]
            oldStartVnode = undefined
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (isSameNode(oldEndVnode, newStartVnode)) { // 新前与旧后相同
            console.log('4、新前与旧后相同');
            patchVnode(oldEndVnode, newStartVnode)
            before = oldStartVnode.el
            parentElm.insertBefore(oldEndVnode.el, before)
            oldEndVnode = undefined
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            console.log('都不命中');
            if (!keyMap) {
                keyMap = {}
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    if (oldCh[i].key) {
                        keyMap[oldCh[i].key] = i
                    }
                }
            }
            console.log(keyMap);
            let moveIdx = keyMap[newStartVnode.key]
            if (moveIdx) {
                let nodeToMove = oldCh[moveIdx]
                patchVnode(nodeToMove, newStartVnode)
                oldCh[moveIdx] = undefined
                parentElm.insertBefore(nodeToMove.el, oldStartVnode.el)
            } else {
                let newDom = createElement(newStartVnode)
                parentElm.insertBefore(newDom, oldStartVnode.el)
            }
            newStartVnode = newCh[++newStartIdx]
        }
    }

    if (newEndIdx < newStartIdx) {
        console.log('新节点循环结束');
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            parentElm.removeChild(oldCh[i].el)
        }
    } else if (oldEndIdx < oldStartIdx) {
        console.log('旧节点循环结束');
        before = oldCh[oldEndIdx + 1] == null ? null : oldCh[oldEndIdx + 1].el
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            let newDom = createElement(newCh[i])
            parentElm.insertBefore(newDom, before)
        }
    }
}

function isSameNode(node1, node2) {
    return node1.sel === node2.sel && node1.key === node2.key
}

你可能感兴趣的:(javascript,前端,vue.js)