虚拟 DOM

  • 为什么需要虚拟DOM
    浏览器的引擎工作流程,大致分5步:

    1. 创建DOM tree:用HTML分析器,分析HTML元素,构建一颗DOM树。
    2. 创建Style Rules:用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。
    3. 构建Render tree:将上面的DOM树和样式表,关联起来,构建一颗Render树。这一过程又称为Attachment。每个DOM节点都有attach方法,接受样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。
    4. 布局Layout:浏览器开始布局,会为每个Render树上的节点确定一个在显示屏上出现的精确坐标值。
    5. 绘制Painting:调用每个节点的paint方法,让它们显示出来。

    当用传统的源生apijQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。比如当你在一次操作时,需要更新10个DOM节点,理想状态是一次性构建完DOM树,再执行后续操作。但浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。因此操作DOM的代价是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验。
    真实的DOM节点,哪怕一个最简单的div也包含着很多属性,虚拟DOM就是为了解决这个浏览器性能问题而被设计出来的。

    let div = document.createElement('div');
    for(let key in div) {
         console.log(key)
    }
    
  • 虚拟DOM是什么?
    由于浏览器的标准过于复杂,自己使用js的对象来描述真实dom,这个js对象,称为虚拟dom

      <div id="app">
          <p class="item">节点1</p>
          <div class="item">节点2</div>
      </div>
    
      {
              tag: 'div',
              data: { id: 'app' },
              children: [
                  {
                      tag: 'p',
                      data: { class: 'item' },
                      children: ['节点1']
                  },
                  {
                      tag: 'div',
                      data: { class: 'item' },
                      children: ['节点2']
                  }
              ]
       }
    
  • 为什么需要虚拟DOM,它有什么好处?
    Web界面由DOM树(数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化。
    虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attchDOM树上,再进行后续操作,避免大量无谓的计算量。
    所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。

  • 如何实现虚拟DOM
    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>
   <style>
       .item {
           font-size: 30px;
           color: red;
       }
   </style>
</head>
<body>
   <div id="app"></div>

   <script src="vdom.js"></script>
   <script>
       // 旧的 VNode
       const prevVNode = createElement('div', null, [
           createElement('div', { key: 'a' , style: {color:'blue', 'text-align': 'left'} }, '节点1'),
           createElement('p', { key: 'b' ,'@click': () => alert('呵呵') }, '节点2'),
           createElement('div', { key: 'c' }, '节点3'),
           createElement('p', { key: 'd'}, '节点4'),
       ])

       // 新的 VNode
       const nextVNode = createElement('div', null, [
           createElement('p', { key: 'd' }, '节点4'),
           createElement('p', { key: 'a' , style: {color:'red'} }, '节点1'),
           createElement('p', { key: 'f' }, '节点6'),
           createElement('p', { key: 'e' , class: "item box" }, '节点5'),
           createElement('div', { key: 'b' , '@click':() => alert('哈哈') }, '节点2'),
       ])

       render(prevVNode, document.getElementById('app'))

       // 2秒后更新
       setTimeout(() => {
           render(nextVNode, document.getElementById('app'))
       }, 1000)

   </script>
</body>
</html>

vdom.js:

const VNodeType = {
    HTML: 'HTML',
    COMPONENT: 'COMPONENT',
    TEXT: 'TEXT'
}

const ChildType = {
    EMPTY: 'EMPTY',
    SINGLE: 'SINGLE',
    MULTIPLE: 'MULTIPLE'
}

// 新建虚拟dom
// tag: 标签名;data: 属性;children: 子元素
function createElement(tag, data, children) {
    // vnode 类型
    let flags = null;
    if (typeof tag === 'string') {
        flags = VNodeType.HTML;
    } else if (typeof tag === 'function') {
        flags = VNodeType.COMPONENT;
    } else {
        flags = VNodeType.TEXT;
    }

    // children 类型
    let childFlags = null;
    if (Array.isArray(children)) {
        if (!children.length) {
            childFlags = ChildType.EMPTY;
        } else if (children.length) {
            // 多个子节点,且子节点使用key
            childFlags = ChildType.MULTIPLE;
        }
    } else if (!children) {
        // 没有子节点
        childFlags = ChildType.EMPTY;
    } else {
        // 其他情况都作为文本节点处理,即单个子节点,会调用 createTextVNode 创建纯文本类型的 VNode
        childFlags = ChildType.SINGLE;
        children = createTextVnode(children);
    }

    // 返回 vnode,key 用来标识节点的唯一性
    return { flags, tag, data, key: data && data.key, children, childFlags, el: null }
}

// 新建文本类型的 vnode
function createTextVnode(text) {
    return {
        flags: VNodeType.TEXT,
        tag: null,
        data: null,
        children: text,
        childFlags: ChildType.EMPTY
    }
}

// 渲染虚拟dom
function render(vnode, container) {
    // 区分首次渲染和再次渲染
    const prevVNode = container.vnode;
    if (prevVNode) {
        // 有旧的 VNode,则调用 `patch` 函数打补丁
        patch(prevVNode, vnode, container);
    } else {
        // 没有旧的 VNode,使用 `mount` 函数挂载全新的 VNode
        mount(vnode, container);
    }
    // 将新的 VNode 添加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
    container.vnode = vnode;
}

// 首次渲染(首次挂载)
function mount(vnode, container, flagNode) {
    let { flags } = vnode;
    if (flags === VNodeType.HTML) {
        // 挂载普通标签
        mountElement(vnode, container, flagNode);
    } else if (flags === VNodeType.TEXT) {
        // 挂载纯文本
        mountText(vnode, container);
    }
}

// 挂载普通标签
function mountElement(vnode, container, flagNode) {
    let { tag, data, children, childFlags } = vnode;
    let el = document.createElement(tag);
    vnode.el = el;
    if (data) {
        for(let key in data) {
            patchData(el, key, null, data[key])
        }
    }

    if (childFlags === ChildType.SINGLE) {
        mount(children, el);
    } else if (childFlags === ChildType.MULTIPLE) {
        children.forEach(item => {
            mount(item, el);
        });
    }
    flagNode ? container.insertBefore(el, flagNode) : container.appendChild(el);
}

// 挂载纯文本
function mountText(vnode, container) {
    const el = document.createTextNode(vnode.children)
    vnode.el = el
    container.appendChild(el)
}

// 更新data
function patchData(el, key, prevValue, nextValue) {
    switch (key) {
        case 'style':
            // 先增加新值
            for(let k in nextValue) {
                el.style[k] = nextValue[k];
            }
            for(let k in prevValue) {
                // 再去掉旧值
                if (!nextValue.hasOwnProperty(k)) {
                    el.style[k] = '';
                }
            }
            break;
        case 'className':
            el.className = nextValue;
            break;
        default:
            if (key[0] === '@') {
                // 事件
                // 移除旧事件
                if (prevValue) {
                    el.removeEventListener(key.slice(1), prevValue);
                }
                // 添加新事件
                if (nextValue) {
                    el.addEventListener(key.slice(1), nextValue);
                }
            } else {
                // attr
                el.setAttribute(key, nextValue);
            }
            break;
    }
}

// 打补丁
function patch(prevVNode, nextVNode, container) {
    const prevFlags = prevVNode.flags;
    const nextFlags = nextVNode.flags;

    if (prevFlags !== nextFlags) {
        // 直接替换
        replaceVNode(prevVNode, nextVNode, container);
    } else if (nextFlags === VNodeType.HTML) {
        // 普通标签
        patchElement(prevVNode, nextVNode, container);
    } else if (nextFlags === VNodeType.TEXT) {
        // 纯文本
        patchText(prevVNode, nextVNode, container);
    }
}

// 替换节点
function replaceVNode(prevVNode, nextVNode, container) {
    container.removeChild(prevVNode.el)
    mount(nextVNode, container)
}

// 普通标签
function patchElement(prevVNode, nextVNode, container) {
    // 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数使用新的 VNode 替换旧的 VNode
    if (prevVNode.tag !== nextVNode.tag) {
        // 直接替换
        replaceVNode(prevVNode, nextVNode, container);
        return;
    }
    // 拿到 el 元素,且让 nextVNode.el 也引用该元素
    const el = (nextVNode.el = prevVNode.el);
    const prevData = prevVNode.data;
    const nextData = nextVNode.data;
    // 更新新的数据
    if (nextData) {
        for(let key in nextData) {
            patchData(el, key, prevData[key], nextData[key])
        }
    }
    // 删除旧的数据
    if (prevData) {
        for(let key in prevData) {
            if (prevData[key] && !nextData.hasOwnProperty(key)) {
                patchData(el, key, prevData[key], null)
            }
        }
    }

    // 调用 patchChildren 函数,递归更新的子节点
    patchChildren(prevVNode.childFlags, nextVNode.childFlags, prevVNode.children, nextVNode.children, el);
}

// 更新子节点
// 旧的 VNode 子节点的类型; 新的 VNode 子节点的类型; 旧的 VNode 子节点; 新的 VNode 子节点; 当前标签元素,即这些子节点的父节点
function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {
    switch (prevChildFlags) {
        // 旧的 children 是单个子节点
        case ChildType.SINGLE:
            switch (nextChildFlags) {
                // 新的 children 是单个子节点
                case ChildType.SINGLE:
                    patch(prevChildren, nextChildren, container);
                    break;
                // 新的 children 没有子节点
                case ChildType.EMPTY:
                    container.removeChild(prevChildren.el);
                    break;
                // 新的 children 中有多个子节点
                default:
                    container.removeChild(prevChildren.el);
                    nextChildren.forEach(item => {
                        mount(item, container);
                    })
                    break;
            }
            break;
        // 旧的 children 没有子节点
        case ChildType.EMPTY:
            switch (nextChildFlags) {
                // 新的 children 是单个子节点
                case ChildType.SINGLE:
                    mount(nextChildren, container);
                    break;
                // 新的 children 没有子节点
                case ChildType.EMPTY:
                    break;
                // 新的 children 中有多个子节点
                default:
                    nextChildren.forEach(item => {
                        mount(item, container);
                    })
                    break;
            }
            break;
        // 旧的 children 中有多个子节点
        default:
            switch (nextChildFlags) {
                // 新的 children 是单个子节点
                case ChildType.SINGLE:
                    prevChildren.forEach(item => {
                        container.removeChild(item.el);
                    })
                    mount(nextChildren, container);
                    break;
                // 新的 children 没有子节点
                case ChildType.EMPTY:
                    prevChildren.forEach(item => {
                        container.removeChild(item.el);
                    })
                    break;
                // 新的 children 中有多个子节点
                default:
                    patchMultipleToMultiple(prevChildren, nextChildren, container);
                    break;
            }
            break;
    }
}

// 纯文本
function patchText(prevVNode, nextVNode, container) {
    // 拿到文本节点 el,同时让 nextVNode.el 指向该文本节点
    const el = (nextVNode.el = prevVNode.el);
    // 只有当新旧文本内容不一致时才有必要更新
    if (prevVNode.children !== nextVNode.children) {
        el.nodeValue = nextVNode.children;
    }
}

function patchMultipleToMultiple(prevChildren, nextChildren, container) {
    let lastIndex = 0;
    let hasFind = false;
    let prevVNode = null;
    // 处理新节点的节点
    nextChildren.forEach((nextVNode, nextIndex) => {
        hasFind = false;
        for(let prevIndex = 0; prevIndex < prevChildren.length; prevIndex++) {
            prevVNode = prevChildren[prevIndex];
            if (nextVNode.key === prevVNode.key) {
                hasFind = true;
                if (prevIndex < lastIndex) {
                    // 需要移动
                    const flagNode = nextChildren[nextIndex - 1].el.nextSibling;
                    container.insertBefore(prevVNode.el, flagNode);
                } else {
                    lastIndex = prevIndex;
                }
                patch(prevVNode, nextVNode, container);
                break;
            }
        }
        if (!hasFind) {
            // 挂载新节点
            const flagNode = !nextIndex ? prevChildren[0].el : nextChildren[nextIndex - 1].el.nextSibling;
            mount(nextVNode, container, flagNode);
        }
    });

    // 移除已经不存在的节点
    prevChildren.forEach(prevVNode => {
        let hasFind = nextChildren.find(nextVNode => nextVNode.key === prevVNode.key);
        if (!hasFind) {
            container.removeChild(prevVNode.el);
        }
    });
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容