美文网首页
虚拟 DOM

虚拟 DOM

作者: 欢欣的膜笛 | 来源:发表于2020-04-17 22:43 被阅读0次
    • 为什么需要虚拟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);
            }
        });
    }
    

    相关文章

      网友评论

          本文标题:虚拟 DOM

          本文链接:https://www.haomeiwen.com/subject/shynphtx.html