一步一步带你实现virtual dom(二) -- Props和事件

一步一步带你实现virtual dom(一)
一步一步带你实现virtual dom(二)--Props和事件html

很高兴咱们能够继续分享编写虚拟DOM的知识。此次咱们要讲解的是产品级的内容,其中包括:设置和DOM一致性、以及事件的处理。node

使用Babel

在继续以前,咱们须要弥补前一篇文章中没有详细讲解的内容。假设有一个没有任何属性(props)的节点:web

<div></div>

Babel,在处理这个节点的时候会把节点的props属性设置为“null”,由于它没有任何的属性。所以咱们会获得这样的结果:面试

function h(type, props, ...children) {
    return {type, props: props || {}, children};
}

设置props

设置props很是简单,记得DOM显示吗?咱们把props做为简单的js对象来存储,因此这样的标签:babel

<ul className="list", style="list-style: none;"></ul>

内存里就会有这样的对象:app

{
    type: 'ul',
    props: {className: 'list', style: 'list-style:none;'}
}

所以每个props的字段就是一个属性名,这个字段的值就是属性值。因此,咱们只要把这些值给真正的DOM节点设置了就能够了。咱们写一个方法包装一个setAttribute()方法:dom

function setProp($target, name, value) {
    $target.setAttribute(name, value);
}

那么如今咱们知道如何设置属性了(prop)--咱们以后能够所有都设置上,只要遍历prop对象的属性就能够:性能

function setProps($target, props) {
    Object.keys(props).forEach(name => {
        setProp($target, name, props[name]);
    })
}

还记得createElement()方法么?咱们只须要在真正的DOM节点建立以后调用setProp方法给它设置便可:测试

function createElement(node) {
    if(typeof node === 'string) {
        return document.createTextNode(node);
    }
    const $el = document.createElement(node.type);
    setProps($el, node.props);
    node.children
        .map(createElement)
        .forEach($el.appendChild.bind($el));
    return $el;
}

可是,这尚未完。咱们忘记了一些小细节。首先,‘class’是js的保留字。因此不能把它用做属性名称。咱们会使用‘className’:.net

<nav className="navbar light">
    <ul></ul>
</nav>

可是在真正的DOM里并无‘className’,因此咱们应该在setProp方法里处理这个问题。

另一个事情是,设置布尔型的属性的时候最好使用布尔值:

<input type="checkbox" checked={false} />

在这个例子里,我并不但愿这个'checked'属性值设置在真正的DOM节点上。可是事实上这个值足够设置DOM节点了,固然这同时还须要给对应的虚拟DOM节点也设置这个值:

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

如今咱们就来看看如何自定义属性。此次彻底是咱们本身的实现,所以后面咱们会有不一样做用的属性,而且不是全都要在DOM节点上显示的。因此要写一个方法来检查这个属性是否是自定义的。如今它是空的,因此咱们尚未任何的自定义属性:

function isCustomProp(name) {
    return false;
}

下面就是咱们完整的setProp()方法,把全部的问题都处理了:

function setProp($target, name, value) {
    if(isCustomProp(name)) {
        return;
    } else if(name === 'className') {
        $target.setAttribute('class', value);
    } else if(typeof value === 'boolean') {
        setBooleanProp($target, name, value);
    } else {
        $target.setAttribute(name, value);
    }
}

如今在JSFiddle里面试试吧.

属性区分(Diff Props)

如今咱们已经可使用prop来建立元素了,如今要处理的就是如何区分元素的props了。最终要么是设置属性,要么是删除它。咱们已经有方法能够设置属性了,如今来写一个方法来删除它们吧。事实上这很是简单:

function removeBooleanProp($target, name) {
    $target.removeAttribute(name);
    $target[name] = false;
}

function removeProp($target, name, value) {
    if(isCustomProp(name)) {
        return;
    } else if(name === 'className') {
        $target.removeAttribute('class');
    } else if(typeof === 'boolean') {
        removeBooleanProp($target, name);
    } else {
        $target.removeAttribute(name);
    }
}

咱们再来写一个updateProp()方法来比较两个属性--就的和新的,并根据比较的结果来更新DOM元素的属性:

  • 在DOM里没有这个属性的话,就删除掉
new                                     old 
<nav></nav>     <nav className='navbar'></nav>
  • 在新的节点里包含了某个属性,那么就须要在DOM上设置这个属性
new                                                                         old
<nav style='background: blue'></nav>   <nav></nav>
  • 某个属性在新的和旧的节点里都存在,那么咱们就须要比较他们的值。若是他们不相等咱们就须要根据结果给新的节点设置属性值了。
new                                                                                 old 
<nav className='navbar default'></nav>   <nav className='navbar'></nav>
  • 在其余状况下,属性并无改变咱们什么都不须要作。

下面这个方法就是专门处理prop的:

function updateProp($target, naem, newVal, oldVal) {
    if(!newVal) {
        removeProp($target, name, oldVal);
    } else if(!oldVal || newVal != oldVal) {
        setProp($target, name, newVal);
    }
}

是否是很简单?可是一个节点会有不止一个属性--因此咱们要写一个方法能够遍历所有的属性,而后调用updateProp()方法来一对一对的处理:

function updateProps($target, newProps, oldProps = {}) {
    const props = Object.assign({}, newProps, oldProps);
    Object.leys(props).forEach(name => {
        updateProp($target, name, newProps[name], oldProps[name]);
    });
}

这里须要注意咱们建立的组合对象。它包含了新、旧节点的属性。所以,在遍历的时候咱们会遇到undefined,不过这没有关系,咱们的方法能够处理这个问题。

最后一件事就是把这个方法放到咱们的updateElement()方法里。咱们应该放在哪里呢?若是节点自己没有改变,那么它的子节点呢?这个问题咱们也须要处理。因此咱们把那个方法放在最后一个if语句块里。

function updateElement($parent, newNode, oldNode, index=0) {
    if() {
        ...
    }   else if(newNode.type) {
        updateProps(
            $parent.childNodes[index],
            newNode.props,
            oldNode.props,
        );

        ...
    }
}

接着在这里测试一下吧。

事件

固然一个动态的应用是免不了会有事件的。咱们可使用querySelector()来处理节点,而后用addEventListener()来给节点添加事件的listener。可是,这样没啥意思。咱们要像React同样来处理事件。

<button onClick={() => alert('hi')}></button>

这样看起来就像那么回事儿了。你看到了,咱们是用了props来声明一个事件监听器的。咱们的属性名都是on开头的。

function isEventProp(name) {
    return /^on/.test(name);
}

咱们来写一个方法,从属性里获取事件名称。记住事件的名称都是以on为前缀的。

function extractEventName(name) {
    return name.slice(2).toLowerCase();
}

看起来,若是咱们在属性里声明了事件,那么咱们就须要在setProps()或者updateProps()方法里处理。可是如何处理方法的不一样呢?

你不能用相等操做符来比较两个方法。固然你能够用toString()方法,而后比较两个方法。可是有个问题,方法里可能会包含native code,这就给比较带来了问题。

"function () { [native code] }"

固然咱们可使用时间冒泡的方式来处理。咱们能够写咱们本身的事件处理管理器,这个管理器会附加到body或者绘制咱们节点的容器节点上。所以,咱们能够在每次更新的时候添加一次事件处理器,这样也不会形成多大的资源浪费。

可是,咱们不会这么作。由于这样会增长不少的问题,并且事实上咱们的时间处理器不会频繁的改变。因此,咱们只要在建立咱们的节点的时候添加一次事件监听器就能够。那么不会在setProps方法里设置事件属性。咱们本身处理添加事件的问题。怎么实现呢?记得咱们的方法能够检测自定义的属性吗?如今它不会是空的了:

function isCustomProp(name) {
    return isEventProp(name);
}

当咱们知道了一个真的DOM节点的时候添加事件监听器,这时属性对象也很是清晰的。

function addEventListeners($target, props) {
    Object.keys(props).forEach(name => {
        if(isEventProp(name)){
            $target.addEventListener(
                exteactEventName(name),
                props[name]
            );
        }
    }); 
}

把上面的代码加入到createElement方法里:

function createElement(node) {
    if(typeof node === 'string') {
        return document.createTextNode('node');
    }
    const $el = document.createElement(node.type);
    setProps($el, node.props);
    addEventListeners($el, node.props);
    node.children
        .map(createElement)
        .forEach($el.appendChild.bind($el));
    return $el;
}

再次添加事件

若是你必需要再次添加事件监听器呢?咱们来简单理解处理一下这个问题。只是这样的话性能会受到印象。咱们会引入一个自定义属性:forceUpdate。记住,咱们怎么检查节点的更改的:

function changed(node1, node2) {
    return typeof node1 ~== typeof node2 ||
                 typeof node1 === 'string' && node1 !== node2 ||
                 node1.type !== node2.type ||
                 node.props.forceUpdate;
}

若是forceUpdate为true的话,节点就会整个的从新建立而且新的事件监听器也会被添加进去。整个属性也不是不该该加到实际的DOM节点的,因此须要处理一下:

function isCustomProp(name) {
    return isEventProp(name) || name === 'forceUpdate';
}

这基本就是所有了。是的,整个解决的方法会影响性能,可是很简单。

结语

这就基本是所有了。但愿你以为有趣。若是你知道更简单的解决方法处理事件处理器的不一样的方法的话,能分享到评论里就太感谢了。

原文地址:https://medium.com/@deathmood/write-your-virtual-dom-2-props-events-a957608f5c76

相关文章
相关标签/搜索