闲聊:在学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来赋值。
但愿你们看完能有收获,欢迎指正!