一步一步帶你實現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

相關文章
相關標籤/搜索