Snabbdom.js(一)

12次阅读

共计 7034 个字符,预计需要花费 18 分钟才能阅读完成。

闲聊:在学 vue 的过程中,虚拟 dom 应该是听的最多的概念之一,得知其是借鉴 snabbdom.js 进行开发,故习之。由于我工作处于 IE8 的环境,对 ES6,TS 这些知识的练习也只是浅尝辄止,而 snabbdom.js 从 v.0.5.4 这个版本后开始使用 TS,所以我下载了 0.5.4 这个版本进行学习(后来才发现可以直接下载最新的版本,去 dist 目录找编译好的文件即可,而且这个版本还有 BUG,在新版本中得到了修改,建议大家还是下载最新版本进行学习)

总共写了四篇文章(都是自己的一些拙见,仅供参考,请多多指教,我这边也会持续修正加更新)

介绍一下 snabbdom 基本用法
介绍一下 snabbdom 渲染原理
介绍一下 snabddom 的 diff 算法和对 key 值的认识
介绍一下对于兼容 IE8 的修改

githubps: 学习的目的是希望将 snabbdom.js 实践到工作中去,思前想后,决定拿表格渲染来开刀,而且兼容了 IE8
当然我也是站在巨人肩膀上进行学习,参考文章:
snabbdom 入门使用
vue2 源码学习开胃菜——snabbdom 源码学习(一)
vue2 源码学习开胃菜——snabbdom 源码学习(二)

好了,前面说了那么多‘废话’,现在切入主题。
开门见山,先总结一下,通过自己的实践,个人认为虚拟 dom 的实现思路为:
通过 js 对象模拟出一个我们需要渲染到页面上的 dom 树的结构,实现了一个修改 js 对象即可修改页面 dom 的快捷途径,避免了我们手动再去一次次操作 dom-api 的繁琐,而且其提供了算法可以使得用最少的 dom 操作进行修改。
对于基础用法的介绍,英语好的完全可以去看一下它 github 的内容 snabbdom.js,我这边主要是记录自己在实践过程中的一些笔记及踩坑。
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 函数实现,细分的话可以分为初始化渲染和对比渲染;

第一次是初始化的时候,vnode=patch(app,vnode),app 作为一个被替换的真实 dom 传入,返回一个当前页面的 vnode,作为下一次渲染的对比虚拟 dom。(这里需要注意的是,app 是在这里作为一个替换 dom,渲染后 app 将会被替换);
第二次是对比渲染,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)

它的第一个参数是元素的选择器,这里可以参考 jq 的写法,# 和. 分别代表了 id 和 class, 对于多个 class,可以 div#divId.red.blue.black 这样去写;
它的第二个参数是模块数据的定义,没有可以省略;

它的第三个参数是其子节点的形式;
如果它无子节点,则为空,不写: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: 绑定的事件类型
对于绑定事件的实践:

绑定 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);
}

dataset:data 属性
hook: 钩子函数

这些钩子函数在模块中使用的: pre, create, update, destroy, remove, post.
这些钩子函数是我们自己定义在虚拟 dom 中的: init, create, insert, prepatch, update, postpatch, destroy, remove.
自己实践钩子函数的时候遇到的一些情况:

如果你的 vnode 进行 patch 的时候 sel 值不同时,只会触发 init create destroy remove insert,因为这理会将旧的 vnode 全部删创建新的 vnode sel:div –> sel:p

如果你的 vnode 进行 patch 的时候 sel 值相同时,只会触发 beforePatch update postPatch,因为这里只是在旧的 vnode 上进行更新
在使用 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);

//var vnode = patch(app, render());

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>1</li> <li>2</li>
<li>2</li> –> <li>3</li>
<li>3</li> <li>4</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 来赋值。
希望大家看完能有收获,欢迎指正!

正文完
 0