美文网首页
snabbdom源码分析

snabbdom源码分析

作者: nymlc | 来源:发表于2019-12-09 10:56 被阅读0次

前言

源码注释链接
snabbdom就是Virtual DOM实现,简单又纯粹,Vue就是借鉴了这个库。众所周知,真实DOM是很庞大的,属性颇多,那么我们使用JS来定义一个DOM对象(包含一些常用的属性),那么这个花费就会大大减小,自然而然也就提升了性能
Virtual DOM需要解决以下几点:

  • 用JS对象表示DOM
  • 高性能的diff算法,比较俩个DOM节点的差异
  • 更新需要更新的DOM到真实的DOM上

有个前提就是比较俩棵树最小修改时间复杂度 O(n^3),这个很糟糕的性能,所以我们假设我们基本上不做跨层级的修改,那么我们就可以只做本层的对比,时间复杂度就能达到 O(n),也就是不管子节点的变化,整个节点替换即可

正文

目录
|-- snabbdom
    |-- h.js 创建vnode
    |-- hooks.js 钩子接口定义(仅限ts)
    |-- htmldomapi.js dom操作工具方法
    |-- is.js 判断工具方法
    |-- snabbdom.bundle.js 入口文件
    |-- snabbdom.js 初始化函数,核心方法所在
    |-- thunk.js thunk实现优化性能
    |-- tovnode.js 将dom转vnode方法
    |-- vnode.js vnode函数对象定义
    |-- helpers 
    |   |-- attachto.js 装饰vnode,并挂载到指定节点
    |-- modules 模块
        |-- attributes.js 属性,只能用setAttribute设置的
        |-- class.js 类
        |-- dataset.js 数据
        |-- eventlisteners.js 事件
        |-- hero.js  css-transition动效
        |-- module.js 模块钩子接口定义(仅限ts)
        |-- props.js props可以直接.操作符设置,eg: .src = xxx
        |-- style.js 样式
流程图
流程图
hook
  • 全局钩子对应modules上,所有节点共享
  • 节点钩子对应每一个节点,节点上定义了才有,如下例所示
h('div', {
    hook: {
        init(vnode) {

        }
    }
}, 'abc')
// 全局钩子只有这六种
// 节点钩子有8种 init create insert prepatch update postpatch destroy remove 
const hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
Name Triggered when Arguments to callback
pre patch开始的时候(patch) none
init vnode要被创建为DOM的时候(createElm) vnode
create vnode已经被创建为DOM的时候(createElm) emptyVnode, vnode
insert vnode已经被插入到DOM的时候,收集要插入的节点,patch要完成时触发(patch) vnode
prepatch 俩节点比对前(patchNode) oldVnode, vnode
update 节点更新过程中(patchNode) oldVnode, vnode
postpatch 节点对比完成(patchNode) oldVnode, vnode
destroy 节点被删除时调用,子节点也会被触发(removeVnodes) vnode
remove 节点被删除时触发,不影响子节点,和destroy类似(removeVnodes) vnode, removeCallback
post patch结束的时候触发(patch) none
updateChildren

五条比对规则:

  1. 旧新首首比对,旧新首索引右移
  2. 旧新尾尾比对,旧新尾索引左移
  3. 旧新首尾比对,旧首索引右移、新尾索引左移
  4. 旧新尾首比对,旧尾索引左移、新首索引右移
  5. 旧新通过key找寻在旧节点组相同的新首节点,新首索引右移(因为新首节点已经比对过了)

demo,如下例、图示

const vnode = h('ul', [h('li', {
    key: 0
}, 'A'), h('li', {
    key: 1
}, 'B'), h('li', {
    key: 2
}, 'C'), h('li', {
    key: 3
}, 'D')])
patch(container, vnode)

const nVnode = h('ul', [h('li', {
    key: 3
}, 'D'), h('li', {
    key: 1
}, 'B'), h('li', {
    key: 2
}, 'C'), h('li', {
    key: 0
}, 'A'), h('li', {
    key: 4
}, 'E')])
patch(vnode, nVnode)
updateChildren.png
thunk

这个其实来源于传名调用与传值调用,就像下文,是先把1+2算出来然后传入fn(传值),还是把1+2整个传入fn(传名)
这俩各有优劣,很明显传值很简单,但是可能有性能影响,毕竟我传入的值fn可能没用。传名能避免这个,但是明显麻烦多了

function fn(a) {
    return a
}
fn(1 + 2)

我们这里thunk就是这意思,如下
demo
这里render方法是返回一个thunk函数创建的vnode(实际上是仿造的),这时候要是通过thunk判定,新旧的vnode一样,那么就没必要创建vnodepath、diff。要是没通过,那么就是不一样的,就会调用prepatch钩子时调用传入的renderNumber(传入的挂在data.fn),创建一个真正的vnodepath

function renderNumber(num) {
    return h('span', num);
}

function render(num) {
    return thunk('div', renderNumber, [num]);
}
// 这里会调用renderNumber创建一个vnode,因为path内肯定走createElement,所以会调用init hook钩子
const vnode1 = patch(container, render(1))
// 这里因为render(1)创建的空壳vnode(vnode2)的fn、args和vnode的一样,所以会把vnode的属性赋值(copyToThunk)给vnode2
// 这样子自然不会调用renderNumber
const vnode2 = patch(vnode1, render(1))
attachTo

demo
这个方法其实是用于挂载vnode,被该方法处理的vnode会被挂载到指定的target节点,也就是会脱离原本该在的位置而在原地留下一个空span节点

props

demo
demo是修改input文本之后点击button,会跳过此语句。就是因为value太容易被改变了,也就是old没变elm[key]变了

if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
    elm[key] = cur;
}
eventlisteners

其实看懂了以上到这里就比较简单了。有点提一下,这里的复用旧listener有点技巧

function createListener() {
    return function handler(event) {
        // 事件触发,触发到此函数
        // 这里的handler就是之前的listener,挂载了vnode数据
        handleEvent(event, handler.vnode);
    };
}

function updateEventListeners(oldVnode, vnode) {
    // ...


    // 复用老监听器,这里很巧妙。因为这个listener都是createListener创建的,他返回一个函数对象,
    // 下面赋值给它vnode,那么触发到这个函数的时候,在函数内部只需要取本函数就可以取到挂载到上面的vnode
    var listener = vnode.listener = oldVnode.listener || createListener();
    // update vnode for listener
    // 更新监听器的vnode
    listener.vnode = vnode;
    // if element changed or added we add all needed listeners unconditionally
    if (!oldOn) {
        for (name in on) {
            // add listener if element was changed or new listeners added
            elm.addEventListener(name, listener, false);
        }
    }

    // ...
}
style

demo

  • 值得注意的是nextFrame说的是下一帧,其实需要两帧,即32ms
// 下两帧更改样式,浏览器16帧/ms来渲染,两帧才能看出变化
var nextFrame = function (fn) {
    raf(function () {
        raf(fn);
    });
};
  • 应用样式之前需要强制页面回流,不然transform样式会直接到结束状态
function applyRemoveStyle(vnode, rm) {
    // ...

    // 首次remove的时候需要强制回流
    if (!reflowForced) {
        // elm.offsetLeft需要返回最新的布局信息,因此浏览器不得不触发回流重绘来返回正确的值
        // 若是不强制回流,那么transform 3s将直接到最终,自然而然的也就没了transitionend事件的回调触发,也就节点不会被remove掉
        vnode.elm.offsetLeft;
        // 若是同时删除俩个节点,那么回流一次就可以了
        /**
            var vnode1 = h('div.parent', {}, [btn1, btn2]);
            var vnode2 = h('div.parent', {}, [null, null]);
         */
        reflowForced = true;
    }
}
function forceReflow() {
    reflowForced = false;
}

相关文章

网友评论

      本文标题:snabbdom源码分析

      本文链接:https://www.haomeiwen.com/subject/vztrwctx.html