深度解析Vue渲染器模块

本文将以一个轻量级渲染器为例,介绍vue渲染器模块原理。

在这篇文章,首先将介绍 VDOM 与其结构,然后解释如何将 VDOM 转换为 真实 DOM,接下来展示vue如何根据新旧 VDOM 来修补 DOM,最后本文例举了使用 VDOM的优点。

源代码与在线演示: CodePen - Renderer Module of Vue.js

什么是 VDOM?

VDOM是指Virtual DOM (虚拟DOM)

这是个真正的DOM

<div class="red">
    <h1 id="title">Hello, world!</h1>
    <p>How are you</p>
</div>

这是一个与该DOM对应的虚拟DOM

{
    tag: 'div',
    props: {
        class: 'red',
    },
    children: [
        {
            tag: 'h1',
            props: {
                id: 'title',
            },
            children: 'Hello, world!'
        }, {
            tag: 'p',
            children: ['How are you']
        }
    ]
}

那么vue如何将这个 VDOM 转化为一个真正的 DOM

VDOM 的结构

VDOM 可以简单为一个字符串。

'div'

VDOM 也可以是一个有三个键的对象:

  • tag: String(required)
  • props: Object | null
  • children: List<Object|String>

Tag

Tag是必须的,Tag的类型应该是一个字符串。

tag: 'div'

Props

Props 有两种情况

  • 类型是 Object
    props: {
        class: 'red',
    }
    
  • 不存在(为null)
    props: null
    

Children

Children 有三种情况

  • 每项类型是 String
    children: 'Hello, world!'
    
  • 每项类型是 Array
    children: [
        'Hello, world!',
        {
            tag: 'p',
            children: ['How are you']
        }
    ]
    
  • 不存在(为null)
    children: null
    

如何将 VDOM 转换为 真实DOM

我们需要根据之前的讨论,在代码中列举所有可能的情况。

Vue.js 中的 VDOM

这里有一个vue中的vdom

⚠️ 请注意这不同于之前提到的VDOM,这里的vnode对象被h函数包裹。

const vdom = h(
    "div", // tag
    { // props
        class: "red"
    },
    [ // children
        h("div", null, ["div one"]),
        h("div", null, [
            "div two",
            h("h1", null, "headline one")
            ]
        ),
    ]);

而现在的 HTML body 是这样的

<div id="app"></div>

为什么 vdom节点 被包裹在一个 h 函数中?

h表示Hyperscript,代表 "生成HTML结构的脚本"

h函数将帮助我们创建 VDOM 对象,这样我们就不必反复写 tagpropschildren 的键名。

h函数的简单实现

function h(tag, props, children) {
    return {
        tag,
        props,
        children,
    };
}

挂载 VDOM 到目标 DOM

我们需要一个根DOM节点来挂载由VDOM构建的DOM节点

const root = document.getElementById("app")

接下来我们需要一个 mount 函数来帮助我们创建真正的DOM节点,并将它们挂载到容器DOM节点。

function mount(vnode, container) {
    let element;

    // is vnode a string
    if (typeof vnode === "string") {
        container.textContent = vnode;
        return;
    } else {
        // if not, create a real dom with value of vnode.tag
        // and store the dom to the vnode object (keep for patch)
        element = (vnode.element = document.createElement(vnode.tag));
    }

    // is props exist
    if (vnode.props) {
        // if exist, set attribute to the dom
        for (let attr in vnode.props) {
            const value = vnode.props[attr];
            element.setAttribute(attr, value);
        }
    }

    // is children exist
    if (vnode.children) {
        if (typeof vnode.children === "string") {
            // The value of children is string
            element.textContent = vnode.children;
        } else if (Array.isArray(vnode.children)) {
            // The value of children is array
            for (let child of vnode.children) {
                // Every item of children is a vnode, recurse them with mount function
                mount(child, element);
            }
        }
        container.appendChild(element);
    }
}

VDOM 更新时对 DOM 进行 Patch操作

将新的VDOM与旧的VDOM进行比较,并patch处理真实的DOM。

比较 tag

如果新的DOM和旧的DOM的tag不同,就替换整个DOM节点。

比较 props

  • 找出新添加的prop,并将其添加到DOM中。
  • find out the prop no longer exists, and remove it from the dom

比较 children

新与旧VDOM的children有四种类型组合

new children old children
String String
String Array
Array Array
Array String

我们需要处理所有可能的组合。

patch 函数

function patch(older, newer) {
    let element = newer.element = older.element;

    // compare the tag
    if (older.tag !== newer.tag) {
        // update the old dom node
        element.innerHTML = ''
        mount(newer, element)
        return
    }

    // compare the props
    const oldProps = older.props || {};
    const newProps = newer.props || {};
    for (let prop in newProps) {
        // find out the newly added prop
        oldValue = oldProps[prop];
        newValue = newProps[prop];
        if (oldValue !== newValue) {
            // if the prop is changed in newer DOM or the prop is newly added
            element.setAttribute(prop, newValue);
        }
    }
    
    for (let prop in oldProps) {
        // find out the prop no longer exists
        if (!(prop in newProps)) {
            // if the prop is not existed in the newer DOM
            element.removeAttribute(prop);
        }
    }

    // compare the children
    const oldChildren = older.children;
    const newChildren = newer.children;

    if (typeof newChildren === "string") {
        if (typeof oldChildren === "string") {
            // both new children and old children are string
            if (oldChildren !== newChildren) {
                // they are not equal
                element.textContent = newChildren;
            }
        } else if (Array.isArray(oldChildren)) {
            // the new children is string but old children is array
            element.textContent = newChildren; // overwrite the DOM
        }
    } else if (Array.isArray(newChildren)) {
        if (typeof oldChildren === "string") {
            // the new children is array but old children is string
            element.innerHTML = '' // reset this dom node
            for (let child of newChildren) {
                mount(child, element); // recreate the dom nodes with the vnodes in new children
            }
        } else if (Array.isArray(oldChildren)) {
            // both new children and old children are array
            const commonLength = Math.min(oldChildren.length, newChildren.length);
            for (let i = 0; i < commonLength; i++) {
                // Iterate the part that they all have
                patch(oldChildren[i], newChildren[i]);
            }
            if (newChildren.length > oldChildren.length) {
                // if the new children is longer, add the rest vdom node to the dom node
                newChildren.slice(oldChildren.length).forEach(child => {
                    mount(child, element);
                })
            } else {
                oldChildren.slice(newChildren.length).forEach(child => {
                    element.removeChild(child);
                })
            }
        }
    }
}

使用 VDOM 的优点

为什么Vue3仍然使用VDOM?

vue3 对patch函数做了很多优化。
与我们的轻量级演示相比,Vue3用v-for模板语法中的:key属性来优化vnode(对应我们的patch函数的第56至71行)

而且Vue3如果没必要的话,可以跳过propschildren部分。同时块优化基本上避免了在大多数节点上调用这部分函数。

Evan You: In reality, the update is so performant

另一方面,vdom让我们可以直接使用render函数语法,这比模板语法更灵活,而且这个能力对一些库的作者来说是非常重要。

所以Vue3决定坚持使用虚拟DOM,因为它提供了很多好处,同时vue团队还在努力利用编译器使差分尽可能的高效。

总之,与直接生成命令式DOM操作相比,Vue提供了相同级别的性能,同时还提供了降级的能力来使用更灵活的方式表达逻辑。

Evan You: The goal to have the best of both worlds is achieved in Vue3 that into a certain degree.

获取源码

源代码与在线演示:

CodePen - Renderer Module of Vue.js

所有知识点都是提取自 视频 - 尤雨溪深度解析vue3, 所以如果本文有错误的话, 那一定是尤雨溪的错 请在评论区指出。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 9,041评论 0 4
  • 公元:2019年11月28日19时42分农历:二零一九年 十一月 初三日 戌时干支:己亥乙亥己巳甲戌当月节气:立冬...
    石放阅读 7,696评论 0 2
  • 年纪越大,人的反应就越迟钝,脑子就越不好使,计划稍有变化,就容易手忙脚乱,乱了方寸。 “玩坏了”也是如此,不但会乱...
    玩坏了阅读 2,387评论 2 1
  • 感动 我在你的眼里的样子,就是你的样子。 相互内化 没有绝对的善恶 有因必有果 当你以自己的价值观幸福感去要求其他...
    周粥粥叭阅读 1,761评论 1 5
  • 昨天考过了阿里规范,心里舒坦了好多,敲代码也犹如神助。早早完成工作回家喽
    常亚星阅读 3,390评论 0 1

友情链接更多精彩内容