閒聊:在學vue的過程當中,虛擬dom應該是聽的最多的概念之一,得知其是借鑑snabbdom.js進行開發,故習之。因爲我工做處於IE8的環境,對ES6,TS這些知識的練習也只是淺嘗輒止,而snabbdom.js從v.0.5.4這個版本後開始使用TS,因此我下載了0.5.4這個版本進行學習(後來才發現能夠直接下載最新的版本,去dist目錄找編譯好的文件便可,並且這個版本還有BUG,在新版本中獲得了修改,建議你們仍是下載最新版本進行學習)javascript
總共寫了四篇文章(都是本身的一些拙見,僅供參考,請多多指教,我這邊也會持續修正加更新)html
github
ps:學習的目的是但願將snabbdom.js實踐到工做中去,思前想後,決定拿表格渲染來開刀,並且兼容了IE8vue
固然我也是站在巨人肩膀上進行學習,參考文章:
java
snabbdom入門使用node
vue2源碼學習開胃菜——snabbdom源碼學習(一)git
vue2源碼學習開胃菜——snabbdom源碼學習(二)github
好了,前面說了那麼多‘廢話’,如今切入主題。算法
開門見山,先總結一下,經過本身的實踐,我的認爲虛擬dom的實現思路爲:segmentfault
經過js對象模擬出一個咱們須要渲染到頁面上的dom樹的結構,實現了一個修改js對象便可修改頁面dom的快捷途徑,避免了咱們手動再去一次次操做dom-api的繁瑣,並且其提供了算法可使得用最少的dom操做進行修改。
對於基礎用法的介紹,英語好的徹底能夠去看一下它github的內容 snabbdom.js,我這邊主要是記錄本身在實踐過程當中的一些筆記及踩坑。api
我這邊仍是以0.5.4版本進行講解
核心文件是:
有了這幾個文件其實就可使用snabbdom.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;
先從最簡單的例子來看看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函數實現,細分的話能夠分爲初始化渲染和對比渲染;
上面的h函數是一個重點,它裏面的內容其實就是頁面dom元素的一個抽象:
h('div#divId.red', {}, [h('p',{},'2S後改變')]) // <div id="div" class="red> // <p> // 2S後改變 // </p> // </div>
h(sel,data,children)
它的第三個參數是其子節點的形式;
若是它無子節點,則爲空,不寫: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);
下面是代碼的效果:
on:綁定的事件類型
對於綁定事件的實踐:
綁定click事件,不傳自定義參數
var newVnode = h('div', { on: { 'click':clickfn1 }},'div') function clickfn1(e,vnode) { console.log(e) console.log(vnode) }
綁定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) }
爲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); }
這些鉤子函數是在模塊中使用的: pre, create, update, destroy, remove, post.
這些鉤子函數是本身定義在虛擬dom中使用的: init, create, insert, prepatch, update, postpatch, destroy, remove.
在實踐鉤子函數的時候遇到的一些狀況:
舉例說明:
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}}, '按鈕')])
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來賦值。
但願你們看完能有收穫,歡迎指正!