Skip to content

wubing0324/snabbdom-js

Repository files navigation

snabbdom-js

snabbdom源码详解 snabbdom的原生js版本,snabbdom地址:https://github.com/snabbdom/snabbdom

snabbdom封装了一套从数据映射到dom的方法。优点(1.对比差异渲染dom,无需重新渲染整个dom;2.不需要多次封装data到dom的方法)

我们在用jquery渲染一个列表的时候,会自己封装一个方法,去映射数据和dom,可能是这样的:

var data = [1,2,3,4]

function updateDom(data){
  jquery.('body').remove('ul')
  var str;
  for (var i = 0; i < data.length; i++>) {
    str += '<li>' + i + '</li>'
  }
  var ul = jquery.('ul').append(jquery.html(str))
  jquery.('body').append(ul)
}

然后,当data更新时,调用updateDom(data),更新dom,而且只要有一点点变化,我们都要删除原来的dom,重新添加。这样其实不难,但是当页面有大量这样的数据的时候,我们要频繁的操作dom,有人也许会用fragment片段来优化dom节点的插入删除,有人也许创建一个<li></li>就更新一次dom,导致页面频繁渲染(dom的回流和重绘),导致页面卡顿。 我们也许会封装很多类似updateDom这样的方法,去映射数据和dom之间的关系,一旦数据发生变化,调用方法就好了,但是我们每次都要费心费力去封装这样的方法,不同的人封装的还不一样,逻辑简单,可能就不封装了。snabbdom解决了这样的问题,他相当于一个封装好的updateDom的究极版。

snabbdom解决了这一问题,我们不再需要自己去做dom的插入删除操作,只要按照一定的格式,把数据和标签组合在一起,做一次初始化,就可以了。我们只需要关心数据的变化,每当数据变化,调用一下更新的方法(patch),snabbdom会帮我们去更新dom,而且,他会对比然后只去更新需要更新的地方,不会因为增加一个li而删除整个ul然后重绘。那么,snabbdom是如何实现从数据到dom这一操作的呢?

答案就是虚拟节点,snabbdom将数据转化为虚拟节点,然后用虚拟节点去映射dom。假如我有一个数字列表,那么映射是这样的(data -> vnode -> dom):

var data = [1,2,3]
->
var vnode = {
  tagName: ul,
  listeners: [
    {click: function(){alert('clickMe!')}}
  ],
  children: [
    {tagName: li, class: 'numberLi', data: 1}
    {tagName: li, class: 'numberLi', data: 2}
    {tagName: li, class: 'numberLi', data: 3}
  ]
}
->
<ul onclick='function(){alert("clickMe!")}'>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

然后,当data发生变化时,我们根据data重新生成vnode,在比较新老vnode,找出他们的不同,然后去更新dom

var data = [1,2,3,4]
->
var newVnode = {
  tagName: ul,
  listeners: [
  ],
  children: [
    {tagName: li, class: 'numberLi', data: 1}
    {tagName: li, class: 'numberLi', data: 2}
    {tagName: li, class: 'numberLi', data: 3}
    {tagName: li, class: 'numberLi', data: 4}
  ]
}
->
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
</ul>

大致过程就是这样,使用snabbdom,dom的变化只是新增了一个值为4的li,并没有删除整个ul然后重新渲染,我们从主干逻辑入手,一步步揭开snabbdom的面纱(yifu)吧。

1.定义vnode格式

2.数据通过h函数生成vnode,然后将vnode传入patch 例:vnode = h('div#container', {class: {'actives': true}, on: {click: [changeText, 'desc']}}, '我是text文本')

3.初始化patch函数 var patch = snabbdom.init(modules){},初始化snabbdom,返回patch方法,调用patch方法传入oldVnode和vnode,最后更新dom,modules参数是更新当前节点对应dom的class、props、styles等属性的方法。最后会存在cbs对象中,然后在不同的时机以vnode为参数调用cbs中的方法,更新dom属性

4.执行patch方法

(1)判断vnode类型

oldVnode是dom元素,也就是第一次初始化,用dom元素创建vnode,例如:patch(document.getElementById('container'), vnode)

if (!isVnode(oldVnode)) {
  oldVnode = emptyNodeAt(oldVnode);
}

(2)比较oldVnode和vnode是否相同,再比较他们的子级vnode是否相同,然后更新dom。 sameVnode判断新老vnode是否是同一个节点,是的话调用patchVnode更新当前节点对应dom的属性,并比较他们的子节点。关键就在patchVnode方法了,我们最后来讲。不是同一个节点,直接用心vnode新建dom元素,然后删除老节点对应的dom元素

if (sameVnode(oldVnode, vnode)) {
  patchVnode(oldVnode, vnode, insertedVnodeQueue);
}
else {
    elm = oldVnode.elm;
    parent = api.parentNode(elm);
    createElm(vnode, insertedVnodeQueue);
    if (parent !== null) {
        api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
    }
}

最后,我们要注意insertedVnodeQueue,在一次patch过程中,所有新增的vnode都会存储在这里,然后遍历,调用每个新节点的insert钩子函数。

snabbdom中有两种方法,一种是cbs中的方法,是内部使用,用来更新当前vnode对用dom的class、attributes、style等属性的,在snabbdom.init(modules)方法中,根据modules,会生成一个cbs对象,它是方法的集合。一类是data.hook中用户自定义的钩子函数,在某些固定的时机触发,和vue的mounted等钩子函数是一样的。

(3)调用patchVnode(只有当前的oldVnode和vnode是同一节点,才会调用patchVnode)

patchVnode(oldVnode, vnode, insertedVnodeQueue);

patchVnode调用cbs.update方法集更新当前vnode对应的dom属性,同时调用用户在data中自定义的钩子函数,然后调用updateChildren比较新老vnode的子节点。 就这样patchVnode和updateChildren互相调用,遍历了整个虚拟dom树。 patchVnode中,文本节点和子节点是互斥的,如果是文本节点,那么删除vnode的子节点,直接:api.setTextContent(elm, vnode.text),修改当前dom的文本即可。

if (isUndef(vnode.text)) {
  ...
}
else if (oldVnode.text !== vnode.text) {
    if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    }
    api.setTextContent(elm, vnode.text);
}

接下来我们重点分析有子节点的情况:

在新老vnode都存在子节点且不同的情况下,调用updateChildren(elm, oldCh, ch, insertedVnodeQueue); 我们要比较当前两个子节点数组中节点的变化,据此更新dom,这个方法里就是我们总说的diff算法了。

我们只考虑没有key的情况,有key的情况是类似的。注意一点,sameVnode(oldStartVnode, newEndVnode)和sameVnode(oldEndVnode, newStartVnode)这两种情况,我们只在odlCh中移动节点的位置即可。

比较(sameVnode(oldEndVnode, newEndVnode);oEnd--;nEnd--;结果如图2):

   oldVnode                                    newVnode
                                  
oStrat               oEnd                |   nStart                        nEnd
  |                   |                  |    |                             |
  A    B    C    D    E                  |    C    G    A    B    D    F    E

                                图一

比较(isUndef(idxInOld); nStart++;结果如图3):

   oldVnode                                    newVnode
                                  
oStrat          oEnd                     |  nStart                    nEnd
  |              |                       |    |                        |
  A    B    C    D    E                  |    C    G    A    B    D    F    E

                                图二

比较(isUndef(idxInOld); nStart++;结果如图4):

     oStrat         oEnd                 |        nStart                nEnd
       |             |                   |          |                    |
  C    A    B    C   D   E               |    C     G    A    B    D     F    E
 
                                图三

比较(sameVnode(oldStartVnode, newStartVnode);oStrat++;nStart++;结果如图5):

         oStrat         oEnd            |                 nStart                nEnd
           |             |              |                   |                    |
 C    G    A    B   C    D    E         |    C       G      A      B      D      F        E
           
                               图四

比较(sameVnode(oldStartVnode, newStartVnode);oStrat++;nStart++;结果如图6):

              oStrat     oEnd             |                      nStart          nEnd
                 |        |               |                         |             |
  C    G    A    B   C    D    E          |   C       G      A      B      D      F        E
                   
                                图五

比较(sameVnode(oldEndVnode, newStartVnode);oEnd--;nStart++;结果如图7):

                   oStrat  oEnd           |                             nStart    nEnd
                      |    |              |                                |      |
  C    G    A    B    C    D    E         |   C       G      A      B      D      F        E
         
                                图六

比较(isUndef(idxInOld); nStart++;结果如图8):

                    oStrat oEnd           |                                  nStart nEnd
                          ||              |                                       ||
  C    G    A    B    D    C    E         |   C       G      A      B      D      F        E
        
                                图七

比较(跳出whilew循环;oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx,循环删除oldCh中多余的节点,如图9):

                         oStrat oEnd                                          nEnd      nStart  
                               ||                                                |        |
  C    G    A    B    D    F    C     E   |   C       G      A      B      D     F       E
    
                                 图八     

比较结果:

                                          |
                                          |                                   nEnd      nStart  
                                          |                                      |        |
  C    G    A    B    D    F    E         |  C       G      A      B      D      F        E
                          
                                  图九

在比较过程中,当sameVnode(oldEndVnode, newEndVnode)返回true时,会再次调用patchVnode,然后又去比较这两个节点的子节点。

oldVnode = h('a.btn.title', {class: {active: true}, on: {click: [changeSort, 'title']}}, 'oldText')  ---->根据数据(data)生成vnode节点。
        |
        |
        |
vnode = h('a.btn.title', {class: {active: false}, on: {click: []}}, 'newText')  ---->更新后的数据(data)生成vnode节点。
        |
        |
        |
patch(oldVnode, vnode)
        |
        |   sameVnode(oldVnode, vnode) === true
        |
patchVnode(oldVnode, vnode) ---->调用hook.prepatch钩子函数,调用cbs.update更新class等属性更新点前vnode节点对应dom,调用hook.update钩子函数。
        |
        |   isDef(oldCh) && isDef(ch) && (oldCh !== ch) ----> 新老vnode都有子节点且不相等,比较子节点。
        |
updateChildren(oldVnode.children, vnode.children) ----> ��有相同节点,继续比较。无相同节点后,调用addVnodes/removeVnodes新增或删除对应的dom元素并更新事件或属性,当前层级比较结束,回去比较父级节点的下。
        |   true                                             这里可以看出,sameVnode(oldVnode, vnode) === true 的节点,属性和事件是在patchVnode中更新的,而不同的节点则是在updateChildren中更新的。
        |<--------- sameVnode(oldVnode.children[i], vnode.children[j]) === true ---->false  当前层级比较结束,回到上层方法。
        |
patchVnode(oldVnode.children[i], vnode.children[j])


现在写的稍微有点乱,未完待续。。。。

About

snabbdom的原生js版本

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published