虚拟DOM

随处可见的VDOM

VDOM,也叫虚拟DOM,并不是什么高大上的新事物,它是仅存于内存中的DOM,因为还未展示到页面中,所以称为VDOM。

var a = document.createElement("div");

如上所示,大家对此应该不陌生吧?没错,这就是VDOM。

问题来了,如果让VDOM变成真实的DOM呢?

其实很简单……只需将节点append到页面中

var a = document.createElement("div");
document.body.append(a);

所以,请大家不要把VDOM想得太复杂!它随处可见~

React中的VDOM

常见的DOM操作

在讲React中的VDOM前,有必要说下,我们日常中常见的DOM操作有哪些?

事实上,就三类:增、删、改。对应的DOM操作如下:

  1. 增加一个节点 => appendChild
  2. 删除一个节点 => removeChild
  3. 更改一个节点 => replaceChild

现实中,很多前端小伙伴在处理前端模板变动时,是简单粗暴的,不管是哪种情况,都会直接使用类似jQuery的html方法整块替换~(全局搜下你代码,是不是有不少$(...).html())

这样做有什么问题呢?——性能问题。如果页面比较小,问题还不是很大,如果页面庞大,这样做势必会出现卡顿,用户体验绝对是不好的。

如何解决呢?——这就引入了差量更新!

差量更新

什么是差量更新?就是只对局面的HTML片段进行更新。比如你加了一个节点,那么我就只更新这个节点,我无需整个模板替换。

这样一来,效率就提高了。

可问题来了,我怎么知道哪个节点更新了,哪个节点删除了,哪个节点替换了呢?——我们需要对DOM建模!

VDOM建模

说是建模,简单点说就是用一个JS对象来表示VDOM。

如果我们可以用一个JS对象来表示VDOM,那么这个对象上多一个属性(增加节点),少一个属性(删除节点),或者属性值变了(更改节点),就一目了然了!

那如何建模呢?

这个麽!我们就要化繁为简了。思考下,DOM也叫DOM树,是一个树形结构,DOM树上有很多元素节点。

我们要对VDOM进行建模,本质上就是对一个个元素节点进行建模,然后再把节点放回DOM树的指定位置,这样不就完成对DOM树的建模了么?

别把建模想得太复杂,无非就是用JS对象的形式来展示罢了。

如何对元素节点进行建模呢?

这个简单,我们不难发现,每个节点无非都是由以下三部分组成:

  1. type : 元素类型
  2. props : 元素属性
  3. children : 子元素集合

比如<div id="main">test</div>,type是div,props是id="main",children是“test”。

我们希望的结果是:

{type:"div",props:{"id":"main"},children:[
       test
]}

如果是更复杂的结构,比如div中有一个图片,我们可以写成

{type:"div",props:{"style":""},children:[
        {type:"img",props:{"src":"..."}}
    ]}

以上也是React对VDOM建模的结果。是不是很简单呢?

如何快捷建模?

如何把真实的DOM,转化成建模后的VDOM呢?

这个简单,transform-react-jsx已经帮我们实现了,使用webpack或者rollup的朋友可以直接使用这个插件。

以下附加rollup的配置文件,供参考:

import babel from 'rollup-plugin-babel';

export default {
    input : 'src/main.js',
    output : {
        file : 'dist/main.js',
        format : 'cjs'
    },
    banner : "/* fed123.com */",
    plugins : [
        babel({
            'presets' : [[
                'env',
                {
                    modules : false
                }
            ]],
            "plugins" : [
                ["transform-react-jsx" , {
                    "pragma" : "vnode"
                }]
            ]
        })
    ]
}

可能你还是一知半解,下面给出一个例子:

// React 常见的DOM写法
const vdom = (
    <div id="_Q5" style="border:1px solid red">
        <div style="text-align:center;">
            <img src="https://m.baidu.com/static/index/plus/plus_logo.png" height="56"/>
        </div>
        Hello
    </div>
);

// 转义后的
var vdom = vnode(
    "div",
    { id: "_Q5", style: "border:1px solid red" },
    vnode(
        "div",
        { style: "text-align:center;" },
        vnode("img", { src: "https://m.baidu.com/static/index/plus/plus_logo.png", height: "56", onClick: function onClick() {
                alert(1);
            } })
    ),
    "Hello"
);
如何将VDOM变成真实DOM呢?

我们知道,将DOM变成VDOM,是为了差量更新,最终我们还是要把VDOM还原成DOM的!VDOM只是个桥梁,如果不能还原成DOM,VDOM就没意义了!

怎么做呢?

可以参考如下代码:

 // 把vdom挂载到页面上
 function createElement(node) {
    if (typeof node === 'string') {
        return document.createTextNode(node);
    }
    const $el = document.createElement(node.type);
    let appendChild = $el.appendChild.bind($el);
    node.children
        .map(createElement)
        .map(appendChild);
    return $el;
}

我们判断,如果是子节点是个字符串节点,直接插入页面即可,如果子节点是个DOM节点,那么就递归调用~

通过这个思路,我们就可以将VDOM还原成DOM了。

DIFF Virtual DOM & Update

以上,是VDOM的准备工作,主要包括两个步骤:

  1. 对VDOM进行建模,方便后续的差量更新
  2. 将VDOM转成真实的DOM

接下来才是主菜。

我们先看思考下,如何判断DOM发生了变化,并找到这个变化?

DIFF算法

DIFF算法是React框架采用的方法。也就是判断DOM是否发生了变化、然后找到这个变化,这样我们才能实现差量更新。

DOM的变化主要有三种:appendChild、replaceChild、removeChild.

还记得我们对VDOM的建模么?

{type:"div",props:{"style":""},children:[
        {type:"img",props:{"src":"..."}}
    ]}

每个节点都包含一个children,DIFF的过程,其实也是diff children的过程。通过递归children的方式,就可以判断不同的children并对其操作。有以下几种情况:

  1. 没有旧的节点,则创建新的节点,并插入父节点。
  2. 如果没有新的节点,则摧毁旧的节点。
  3. 如果节点发生了变化,则用replaceChild改变节点信息
  4. 如果节点没有变化,则对比该节点的子节点进行判断,使用递归调用

function updateElement($parent, newNode, oldNode, index = 0) {
    if(!oldNode) {
        $parent.appendChild(
            createElement(newNode)
        );
    } else if (!newNode) {
        $parent.removeChild(
            $parent.childNodes[index]
        );
    } else if (changed(newNode, oldNode)) {
        $parent.replaceChild(
            createElement(newNode),
            $parent.childNodes[index]
        );
    } else if(newNode.type) {
        const newLength = newNode.children.length;
        const oldLength = oldNode.children.length;
        for(let i = 0; i < newLength || i < oldLength; i++) {
            updateElement(
                $parent.childNodes[index],
                newNode.children[i],
                oldNode.children[i],
                i
            );
        }
    }
}

为什么要DIFF children呢?因为我们必须DOM树是由一个个元素节点组成的,DOM树变化的最小单位也是元素节点。

通过递归的方式,我们就可以从最底层的children开始,层层遍历,找到变化的节点,然后对这些节点差量更新了。

而所谓的差量更新,就是上述提到的三种操作:appendChild、replaceChild、removeChild。这在上面的代码都有体现到。

Handle Props & Event

通过上述的步骤,我们就可以把DOM树进行差量更新并呈现到页面上,但我们知道,DOM树可不只有节点,还有参数跟事件,所以我们需要把参数跟事件加上。

再看一眼我们对VDOM的建模!

{type:"div",props:{"style":""},children:[
        {type:"img",props:{"src":"..."}}
    ]}

我们要做的,就是把props加载到对应的元素节点上,这个步骤简称:DIFF props。

DIFF props,同DIFF VDOM,找到props的不同,然后setAttribute跟removeAttribute。

这里直接上代码:

function updateProps ($target, newProps, oldProps = {}){
    const props = Object.assign({},oldProps, newProps);
    Object.keys(props).forEach(name => {
        updateProp($target, name, newProps[name], oldProps[name]);
    })
}
function updateProp ($target, name, newVal, oldVal) {
    if (!newVal) {
        removeProp($target, name, oldVal);
    } else if (!oldVal || newVal !== oldVal) {
        setProp($target, name, newVal);
    }
}
function setProp ($target, name, value) {
    if (typeof value === "boolean") {
        handleBooleanProp($target, name, value);
    }
    $target.setAttribute(name, value);
}

function setBooleanProp($target, name, value) {
    if (!!value) {
        $target.setAttribute(name, value);
        $target[name] = true;
    } else {
        $target[name] = false;
    }
}

function removeProp($target, name, value) {
    if (typeof value === 'boolean') {
        $target[name] = false;
    } 
    $target.removeAttribute(name);
}

项目源码地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容