Snabbdom.js(一)

閒聊:在學vue的過程當中,虛擬dom應該是聽的最多的概念之一,得知其是借鑑snabbdom.js進行開發,故習之。

因爲我工做處於IE8的環境,對ES6,TS這些知識的練習也只是淺嘗輒止,而snabbdom.js從v.0.5.4這個版本後開始使用TS,因此我下載了0.5.4這個版本進行學習(後來才發現能夠直接下載最新的版本,去dist目錄找編譯好的文件便可,並且這個版本還有BUG,在新版本中獲得了修改,建議你們仍是下載最新版本進行學習)javascript

總共寫了四篇文章(都是本身的一些拙見,僅供參考,請多多指教,我這邊也會持續修正加更新)html

  1. 介紹一下snabbdom基本用法
  2. 介紹一下snabbdom渲染原理
  3. 介紹一下snabddom的diff算法和對key值的認識
  4. 介紹一下對於兼容IE8的修改

github
ps:學習的目的是但願將snabbdom.js實踐到工做中去,思前想後,決定拿表格渲染來開刀,並且兼容了IE8
圖片描述vue

固然我也是站在巨人肩膀上進行學習,參考文章:java

snabbdom入門使用node

vue2源碼學習開胃菜——snabbdom源碼學習(一)git

vue2源碼學習開胃菜——snabbdom源碼學習(二)github


好了,前面說了那麼多‘廢話’,如今切入主題。算法

開門見山,先總結一下,經過本身的實踐,我的認爲虛擬dom的實現思路爲:segmentfault

經過js對象模擬出一個咱們須要渲染到頁面上的dom樹的結構,實現了一個修改js對象便可修改頁面dom的快捷途徑,避免了咱們手動再去一次次操做dom-api的繁瑣,並且其提供了算法可使得用最少的dom操做進行修改。

對於基礎用法的介紹,英語好的徹底能夠去看一下它github的內容 snabbdom.js,我這邊主要是記錄本身在實踐過程當中的一些筆記及踩坑。api

1.如何引用

我這邊仍是以0.5.4版本進行講解

核心文件是:

  • snabbdom.js
  • h.js
  • vnode.js(最新版本改成tovnode.js)
  • htmldomapi.js
  • is.js(這個文件是用來提供函數來判斷數據是否爲undefined,最新版本已經沒把它單獨拿出來了)
  • polyfill.js(我這邊爲了兼容IE8本身添加的文件)

有了這幾個文件其實就可使用snabbdom.js來渲染咱們的頁面。

固然還有很重要的模塊文件:

  • style.js
  • props.js
  • eventlistener.js
  • class.js
  • attribute.js
  • dataset.js
  • eventlistener.js

這些模塊規定了咱們虛擬dom具有哪些能力,例如很重要的eventlistener.js使得咱們能夠在虛擬dom上添加事件,它們都是咱們不可或缺的。做者將其分離出來應該是想剝離出核心代碼,使得咱們能夠根據本身的需求來定製相應的模塊。

引用的時候各個文件之間仍是有必定順序的,我是這樣引用的:(snabbdom.js是最後引用,輔助型文件polyfill.js is.js得最先引用):

<script type="text/javascript" src="polyfill.js"></script>
<script type="text/javascript" src="is.js"></script>
<script type="text/javascript" src="htmldomapi.js"></script>
<script type="text/javascript" src="eventlistener.js"></script>
<script type="text/javascript" src="class.js"></script>
<script type="text/javascript" src="attributes.js"></script>
<script type="text/javascript" src="props.js"></script>
<script type="text/javascript" src="style.js"></script>
<script type="text/javascript" src="dataset.js"></script>
<script type="text/javascript" src="vnode.js"></script>
<script type="text/javascript" src="h.js"></script>
<script type="text/javascript" src="snabbdom.js"></script>
<script type="text/javascript" src="index.js"></script>

固然你也能夠把全部文件進行壓縮合並,代碼中還可使用模塊化的方式進行引用相關模塊;

ps:因爲咱們這邊尚未使用模塊化,因此我把源碼中使用模塊化的部分簡單的修改了一下;
模塊化也就是將一個功能單獨寫在一個js文件中供其它文件使用,會使用一個對象進行封裝導出,並經過當即執行函數的閉包使得其不會污染其它做用域變量。

舉例:
導出
//a.js

aModule={};

(function(aModule){
     aModule.init=function(){}
})(aModule)

導入

<script type="text/javascript" src="a.js"></script>
var init=aModule.init;

2.如何使用

先從最簡單的例子來看看snabbdom.js是如何使用的;

![圖片描述

代碼以下:

var snabbdom = SnabbdomModule;

var patch = snabbdom.init([ //導入相應的模塊
    DatasetModule,
    ClassModule,
    AttributesModule,
    PropsModule,
    StyleModule,
    EventlistenerModule
]);

var h = HModule.h;

var app = document.getElementById('app');

var newVnode = h('div#divId.red', {}, [h('p', {},'已改變')])

var vnode = h('div#divId.red', {}, [h('p',{},'2S後改變')])

vnode = patch(app, vnode);

setTimeout(function() {
    vnode=patch(vnode, newVnode);
}, 2000)

上面代碼的主要功能就是渲染,經過snabbdom模塊的init方法返回的patch函數實現,細分的話能夠分爲初始化渲染和對比渲染;

  1. 第一次是初始化的時候,vnode=patch(app,vnode),app做爲一個被替換的真實dom傳入,返回一個當前頁面的vnode,做爲下一次渲染的對比虛擬dom。(這裏須要注意的是,app是在這裏做爲一個替換dom,渲染後app將會被替換);
  2. 第二次是對比渲染,vnode=patch(vnode, newVnode);

上面的h函數是一個重點,它裏面的內容其實就是頁面dom元素的一個抽象:

h('div#divId.red', {}, [h('p',{},'2S後改變')])

 // <div id="div" class="red>
 //   <p>
 //     2S後改變
 //   </p>
 //  </div>

h(sel,data,children)

  1. 它的第一個參數是元素的選擇器,這裏能夠參考jq的寫法,#.分別表明了id和class,對於多個class,能夠div#divId.red.blue.black這樣去寫;
  2. 它的第二個參數是模塊數據的定義,沒有能夠省略;
  3. 它的第三個參數是其子節點的形式;

    若是它無子節點,則爲空,不寫:h('p')
         
         若是它的子節點是文本節點,則直接寫其字符串:h('p','2S後改變')
         
         若是它的子節點是包含元素節點,則須要用數組寫入:
         (哪怕只有一個元素,數組裏面還能夠包含文本節點)
            h('div#divId.red', {}, [h('p','2S後改變')])
            h('div#divId.red', {}, ['文本',h('p','2S後改變')])
            h('div#divId.red', {}, ['文本',h('p','2S後改變'),h('p','2S後改變')])

經過從上面的這個例子,咱們知道如何用snabbdom.js來渲染頁面了,不過漏了一個重點,就是h函數的第二個參數,模塊參數的使用,下面咱們改造一下vnode;

vnode = h('div#divId.red', {
    'class': {
        'active': true
    },
    'style': {
        'background': '#fff'
    },
    'on': {
        'click': clickFn
    },
    'dataset': {
        'name': 'liuzj'
    },
    'hook': {
        'init': function() {
            console.log('init')
        },
        'create': function() {
            console.log('create')
        },
        'insert': function() {
            console.log('insert')
        },
        'prepatch': function() {
            console.log('beforePatch')
        },
        'update': function() {
            console.log('update')
        },
        'postpatch': function() {
            console.log('postPatch')
        },
        'destroy': function() {
            console.log('destroy')
        },
        'remove': function(ch, rm) {
            console.log('remove')
            rm();
        }
    }
}, [h('p', {}, '2S後改變')])

function clickFn() {
    console.log('click')
}



vnode = patch(app, vnode);

下面是代碼的效果:
圖片描述

  • class:這裏咱們能夠理解爲動態的類名,sel上的類能夠理解爲靜態的,例如上面class:{active:true}咱們能夠經過控制這個變量來表示此元素是不是當前被點擊
  • style:內聯樣式
  • on:綁定的事件類型

    對於綁定事件的實踐:
  1. 綁定click事件,不傳自定義參數

    var newVnode = h('div', {
       on: {
           'click':clickfn1
       }},'div')
    
       function clickfn1(e,vnode) {
           console.log(e)
           console.log(vnode)
       }
  2. 綁定click事件,傳自定義參數

    var newVnode = h('div', {
       on: {
           'click':[clickfn1,'arg1','arg2']
       }},'div')
    
       function clickfn1(val1,val2,e,vnode) {
           console.log(val1)
           console.log(val2)
           console.log(e)
           console.log(vnode)
       }
  3. 爲click事件綁定多個回調函數

    var newVnode = h('div', {
       on: {
           'click':[[clickfn1,'arg1','arg2'],[clickfn2,'arg1','arg2']]
       }},'div')
    
       function clickfn1(val1,val2,e,vnode) {
           console.log(val1)
           console.log(val2)
           console.log(e)
           console.log(vnode)
       }
       
       function clickfn2(val1,val2,e,vnode) {
           console.log(val1)
           console.log(val2)
           console.log(e)
           console.log(vnode)
       }

在綁定多個回調函數時,源碼存在一個問題,回調參數中的event和vnode獲取不到,修改源碼便可:

eventlistener.js:

for (var i = 0; i < handler.length; i++) {
    invokeHandler(handler[i]);
}

改成:

for (var i = 0; i < handler.length; i++) {
    invokeHandler(handler[i], vnode, event);
}
  • dataset:data屬性
  • hook:鉤子函數

    圖片描述

這些鉤子函數是在模塊中使用的: pre, create, update, destroy, remove, post.

這些鉤子函數是本身定義在虛擬dom中使用的: init, create, insert, prepatch, update, postpatch, destroy, remove.

在實踐鉤子函數的時候遇到的一些狀況:
  1. 若是你的vnode進行patch的時候sel值不一樣時,只會觸發init create destroy remove insert ,由於這理會將舊的vnode所有刪建立新的vnode 好比:sel:div --> sel:p
  2. 若是你的vnode進行patch的時候sel值相同時,只會觸發beforePatch update postPatch,由於這裏只是在舊的vnode上進行更新
  3. 在使用remove鉤子函數的時候須要注意的是,函數會返回一個rm函數參數,咱們須要執行這個函數才能將刪除舊節點。

舉例說明:

var newVnode = h('div#divId', [h('p', '已改變')])

var vnode = h('div#divId.red', {
    'hook': {
        'remove': function() {
            console.log('remove')
        }
    }
}, [h('p', '2S後改變')])


vnode = patch(app, vnode);

setTimeout(function() {
    patch(vnode, newVnode);
}, 2000)

圖片描述

正確使用的方法爲:

'remove': function(ch, rm) {
            console.log('remove')
            rm();
        }
  • props/attribute:設置元素自身的屬性

    h('div#divId.red', [h('a',{ attrs:{ href:'http://baidu.com'}},'百度')])
    h('div#divId.red', [h('a',{ props:{ href:'http://baidu.com'}},'百度')])

    不過對於disabled checked這樣的屬性最好是用props

    h('div#divId.red', [h('button', {props: {disabled: true}}, '按鈕')])

3.對於Key值的使用

key值算是一個snabbdom中diff算法的一個核心內容,關於diff算法的核心思想我會在下一篇介紹,這一篇主要是講一下使用。

以個人觀點來看,多個相同元素渲染時,則須要爲每一個元素添加key值。

例如

<ul>                   <ul>
    <li>li1</li>            <li>li2</li>
    <li>li2</li>   -->      <li>li3</li>
    <li>li3</li>            <li>li4</li>
</ul>                  </ul>
var vnode = h('ul', [h('li', {
    key: 1
}, 'li1'), h('li', {
    key: 2
}, 'li2'), h('li', {
    key: 3
}, 'li3')])

var newVnode = h('ul', [h('li', {
    key: 2
}, 'li2'), h('li', {
    key: 3
}, 'li3'), h('li', {
    key: 4
}, 'li4')])

固然,在實際工做中,咱們確定不會像上面那樣寫,都是利用循環進行動態渲染。

var data1 = [{
    name: 'li1'
}, {
    name: 'li2'
}, {
    name: 'li3'
}]

var data2 = [{
    name: 'li2'
}, {
    name: 'li3'
}, {
    name: 'li4'
}]

var vnode = h('ul', data1.map(function(item) {
    return h('li', {
        key: item.name
    }, item.name)
}))

var newVnode = h('ul', data2.map(function(item) {
    return h('li', {
        key: item.name
    }, item.name)
}))

vnode = patch(app, vnode);

setTimeout(function() {
    patch(vnode, newVnode);
}, 2000)

這裏須要記住的是:這個key值要惟一,並且須要一一對應
不少人喜歡在循環的數組中用index來做爲key值,嚴格意義上來講這樣作是不恰當的,key值不只須要惟一,還須要一一對應(同一個節點舊vnode中和新vnode中的key值要同樣),固然若是你使用key值的元素它不存在增刪排序的需求,那麼index做爲key值沒有影響。
至於緣由,下一篇我會說一下;

前面說到了ul/li須要使用key,還有就是我目前作的表格渲染,也須要使用key,由於表格會涉及到tbody/tr/td,其中tr和td都會存在多個,而tr會有增刪和排序,td只是值的修改,位置不會發生變化,因此我在實際操做的過程當中,tr的key值一一對應的,而td的key值則是用index來賦值。

但願你們看完能有收穫,歡迎指正!
clipboard.png

相關文章
相關標籤/搜索