资讯专栏INFORMATION COLUMN

vue2源码学习开胃菜——snabbdom源码学习(一)

betacat / 3322人阅读

摘要:前言最近在学习的源码,刚开始看其源码,着实找不到方向,因为其在的实现上还加入了很多本身的钩子,加大了阅读难度。

前言

最近在学习vue2.0的源码,刚开始看其vdom源码,着实找不到方向,因为其在vdom的实现上还加
入了很多vue2.0本身的钩子,加大了阅读难度。于是看到第一行尤大说vue2.0的vdom是在snabbdom
的基础上改过来的,而snabbdom只有不到300sloc,那不妨先从snabbdom入手,熟悉其中的原理,
再配合vue2.0的vdom看,效果可能更好。

什么是virtual-dom

virtual-dom可以看做一棵模拟了DOM树的JavaScript树,其主要是通过vnode,实现一个无
状态的组件,当组件状态发生更新时,然后触发virtual-dom数据的变化,然后通过virtual-dom
和真实DOM的比对,再对真实dom更新。

为什么是virtual-dom

我们知道,当我们希望实现一个具有复杂状态的界面时,如果我们在每个可能发生变化的组件上都绑定
事件,绑定字段数据,那么很快由于状态太多,我们需要维护的事件和字段将会越来越多,代码也会
越来越复杂,于是,我们想我们可不可以将视图和状态分开来,只要视图发生变化,对应状态也发生
变化,然后状态变化,我们再重绘整个视图就好了。这样的想法虽好,但是代价太高了,于是我们又
想,能不能只更新状态发生变化的视图?于是virtual-dom应运而生,状态变化先反馈到vdom上,
vdom在找到最小更新视图,最后批量更新到真实DOM上,从而达到性能的提升。

除此之外,从移植性上看,virtual-dom还对真实dom做了一次抽象,这意味着virtual-dom对应
的可以不是浏览器的dom,而是不同设备的组件,极大的方便了多平台的使用。

snabbdom目录结构

好了,说了这么多,我们先来看看snabbdom吧,我看的是这个版本的snabbdom
(心塞,typescript学的不深,看最新版的有点吃力,所以选了ts版本前的一个版本)。好了我们先
看看snabbdom的主要目录结构。

名称 类型 解释
dist 文件夹 里面包含了snabddom打包后的文件
examples 文件夹 里面包含了使用snabbdom的例子
helpers 文件夹 包含svg操作需要的工具
modules 文件夹 包含了对attribute,props,class,dataset,eventlistner,style,hero的操作
perf 文件夹 性能测试
test 文件夹 测试
h 文件 把状态转化为vnode
htmldomapi 文件 原生dom操作的抽象
is 文件 判断类型
snabbdom.bundle 文件 snabbdom本身依赖打包
snabbdom 文件 snabbdom 核心,包含diff,patch等操作
thunk 文件 snabbdom下的thunk功能实现
vnode 文件 构造vnode
snabbdom源码之旅 第一站 vnode

首先,我们从最简单的vnode开始入手,vnode实现的功能非常简单,就是讲输入的数据转化为vnode
对象的形式

    //VNode函数,用于将输入转化成VNode
    /**
     *
     * @param sel    选择器
     * @param data    绑定的数据
     * @param children    子节点数组
     * @param text    当前text节点内容
     * @param elm    对真实dom element的引用
     * @returns {{sel: *, data: *, children: *, text: *, elm: *, key: undefined}}
     */
    module.exports = function ( sel, data, children, text, elm ) {
        var key = data === undefined ? undefined : data.key;
        return {
            sel: sel, data: data, children: children,
            text: text, elm: elm, key: key
        };
    };

vnode主要有5大属性:

sel 对应的是选择器,如"div","div#a","div#a.b.c"的形式

data 对应的是vnode绑定的数据,可以有以下类型:attribute、props、eventlistner、
class、dataset、hook

children 子元素数组

text 文本,代表该节点中的文本内容

elm 里面存储着对对应的真实dom element的引用

key 用于不同vnode之间的比对

第二站 h

说完vnode,就到h了,h也是一个包装函数,主要是在vnode上再做一层包装,实现功能如下

如果是svg,则为其添加命名空间

将children中的text包装成vnode形式

var VNode = require ( "./vnode" );
var is = require ( "./is" );
//添加命名空间(svg才需要)
function addNS ( data, children, sel ) {
    data.ns = "http://www.w3.org/2000/svg";
//如果选择器
    if ( sel !== "foreignObject" && children !== undefined ) {
        //递归为子节点添加命名空间
        for (var i = 0; i < children.length; ++i) {
            addNS ( children[ i ].data, children[ i ].children, children[ i ].sel );
        }
    }
}
//将VNode渲染为VDOM
/**
 *
 * @param sel 选择器
 * @param b    数据
 * @param c    子节点
 * @returns {{sel, data, children, text, elm, key}}
 */
module.exports = function h ( sel, b, c ) {
    var data = {}, children, text, i;
    //如果存在子节点
    if ( c !== undefined ) {
        //那么h的第二项就是data
        data = b;
        //如果c是数组,那么存在子element节点
        if ( is.array ( c ) ) {
            children = c;
        }
        //否则为子text节点
        else if ( is.primitive ( c ) ) {
            text = c;
        }
    }
    //如果c不存在,只存在b,那么说明需要渲染的vdom不存在data部分,只存在子节点部分
    else if ( b !== undefined ) {
        if ( is.array ( b ) ) {
            children = b;
        }
        else if ( is.primitive ( b ) ) {
            text = b;
        }
        else {
            data = b;
        }
    }
    if ( is.array ( children ) ) {
        for (i = 0; i < children.length; ++i) {
            //如果子节点数组中,存在节点是原始类型,说明该节点是text节点,因此我们将它渲染为一个只包含text的VNode
            if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] );
        }
    }
    //如果是svg,需要为节点添加命名空间
    if ( sel[ 0 ] === "s" && sel[ 1 ] === "v" && sel[ 2 ] === "g" ) {
        addNS ( data, children, sel );
    }
    return VNode ( sel, data, children, text, undefined );
};

第三站 htmldomapi

htmldomapi中提供了对原生dom操作的一层抽象,这里就不再阐述了

第四站 modules

modules中主要包含attributes,class,props,dataset,eventlistener,hero,style
这些模块,其中attributes,class,props,dataset,eventlistener,style这些模块是我们
日常所需要的,也是snabbdom.bundle默认注入的也是这几个,这里就详细介绍这几个模块

attributes

主要功能如下:

从elm的属性中删除vnode中不存在的属性(包括那些boolean类属性,如果新vnode设置为false,同样删除)

如果oldvnode与vnode用同名属性,则在elm上更新对应属性值

如果vnode有新属性,则添加到elm中

如果存在命名空间,则用setAttributeNS设置

var NamespaceURIs = {
  "xlink": "http://www.w3.org/1999/xlink"
};

var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare",
                "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable",
                "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple",
                "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly",
                "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate",
                "truespeed", "typemustmatch", "visible"];

var booleanAttrsDict = Object.create(null);

//创建属性字典,默认为true
for(var i=0, len = booleanAttrs.length; i < len; i++) {
  booleanAttrsDict[booleanAttrs[i]] = true;
}

function updateAttrs(oldVnode, vnode) {
  var key, cur, old, elm = vnode.elm,
      oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs, namespaceSplit;


  //如果旧节点和新节点都不包含属性,立刻返回
  if (!oldAttrs && !attrs) return;
  oldAttrs = oldAttrs || {};
  attrs = attrs || {};

  // update modified attributes, add new attributes
  //更新改变了的属性,添加新的属性
  for (key in attrs) {
    cur = attrs[key];
    old = oldAttrs[key];
    //如果旧的属性和新的属性不同
    if (old !== cur) {
    //如果是boolean类属性,当vnode设置为falsy value时,直接删除,而不是更新值
      if(!cur && booleanAttrsDict[key])
        elm.removeAttribute(key);
      else {
        //否则更新属性值或者添加属性
        //如果存在命名空间
        namespaceSplit = key.split(":");
        if(namespaceSplit.length > 1 && NamespaceURIs.hasOwnProperty(namespaceSplit[0]))
          elm.setAttributeNS(NamespaceURIs[namespaceSplit[0]], key, cur);
        else
          elm.setAttribute(key, cur);
      }
    }
  }
  //remove removed attributes
  // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
  // the other option is to remove all attributes with value == undefined
  //删除不在新节点属性中的旧节点的属性
  for (key in oldAttrs) {
    if (!(key in attrs)) {
      elm.removeAttribute(key);
    }
  }
}

module.exports = {create: updateAttrs, update: updateAttrs};

class

主要功能如下:

从elm中删除vnode中不存在的或者值为false的类

将vnode中新的class添加到elm上去

function updateClass(oldVnode, vnode) {
  var cur, name, elm = vnode.elm,
      oldClass = oldVnode.data.class,
      klass = vnode.data.class;
  //如果旧节点和新节点都没有class,直接返回
  if (!oldClass && !klass) return;
  oldClass = oldClass || {};
  klass = klass || {};
  //从旧节点中删除新节点不存在的类
  for (name in oldClass) {
    if (!klass[name]) {
      elm.classList.remove(name);
    }
  }
  //如果新节点中对应旧节点的类设置为false,则删除该类,如果新设置为true,则添加该类
  for (name in klass) {
    cur = klass[name];
    if (cur !== oldClass[name]) {
      elm.classList[cur ? "add" : "remove"](name);
    }
  }
}

module.exports = {create: updateClass, update: updateClass};

dataset

主要功能如下:

从elm中删除vnode不存在的属性集中的属性

更新属性集中的属性值

function updateDataset(oldVnode, vnode) {
  var elm = vnode.elm,
    oldDataset = oldVnode.data.dataset,
    dataset = vnode.data.dataset,
    key

  //如果新旧节点都没数据集,则直接返回
  if (!oldDataset && !dataset) return;
  oldDataset = oldDataset || {};
  dataset = dataset || {};
 //删除旧节点中在新节点不存在的数据集
  for (key in oldDataset) {
    if (!dataset[key]) {
      delete elm.dataset[key];
    }
  }
  //更新数据集
  for (key in dataset) {
    if (oldDataset[key] !== dataset[key]) {
      elm.dataset[key] = dataset[key];
    }
  }
}

module.exports = {create: updateDataset, update: updateDataset}

eventlistener

snabbdom中对事件处理做了一层包装,真实DOM的事件触发的是对vnode的操作,主要途径是:

createListner => 返回handler作事件监听生成器 =>handler上绑定vnode =>将handler作真实DOM的事件处理器
真实DOM事件触发后 => handler获得真实DOM的事件对象 => 将真实DOM事件对象传入handleEvent => handleEvent找到
对应的vnode事件处理器,然后调用这个处理器从而修改vnode

//snabbdom中对事件处理做了一层包装,真实DOM的事件触发的是对vnode的操作
//主要途径是
// createListner => 返回handler作事件监听生成器 =>handler上绑定vnode =>将handler作真实DOM的事件处理器
//真实DOM事件触发后 => handler获得真实DOM的事件对象 => 将真实DOM事件对象传入handleEvent => handleEvent找到
//对应的vnode事件处理器,然后调用这个处理器从而修改vnode

//对vnode进行事件处理
function invokeHandler ( handler, vnode, event ) {
    if ( typeof handler === "function" ) {
        // call function handler
        //将事件处理器在vnode上调用
        handler.call ( vnode, event, vnode );
    }
    //存在事件绑定数据或者存在多事件处理器
    else if ( typeof handler === "object" ) {

        //说明只有一个事件处理器
        if ( typeof handler[ 0 ] === "function" ) {
            //如果绑定数据只有一个,则直接将数据用call的方式调用,提高性能
            //形如on:{click:[handler,1]}
            if ( handler.length === 2 ) {
                handler[ 0 ].call ( vnode, handler[ 1 ], event, vnode );
            }
            //如果存在多个绑定数据,则要转化为数组,用apply的方式调用,而apply性能比call差
            //形如:on:{click:[handler,1,2,3]}
            else {
                var args = handler.slice ( 1 );
                args.push ( event );
                args.push ( vnode );
                handler[ 0 ].apply ( vnode, args );
            }
        } else {
            //如果存在多个相同事件的不同处理器,则递归调用
            //如on:{click:[[handeler1,1],[handler,2]]}
            for (var i = 0; i < handler.length; i++) {
                invokeHandler ( handler[ i ] );
            }
        }
    }
}

/**
 *
 * @param event 真实dom的事件对象
 * @param vnode
 */
function handleEvent ( event, vnode ) {
    var name = event.type,
        on = vnode.data.on;

    // 如果找到对应的vnode事件处理器,则调用
    if ( on && on[ name ] ) {
        invokeHandler ( on[ name ], vnode, event );
    }
}
//事件监听器生成器,用于处理真实DOM事件
function createListener () {
    return function handler ( event ) {
        handleEvent ( event, handler.vnode );
    }
}
//更新事件监听
function updateEventListeners ( oldVnode, vnode ) {
    var oldOn = oldVnode.data.on,
        oldListener = oldVnode.listener,
        oldElm = oldVnode.elm,
        on = vnode && vnode.data.on,
        elm = vnode && vnode.elm,
        name;

    // optimization for reused immutable handlers
    //如果新旧事件监听器一样,则直接返回
    if ( oldOn === on ) {
        return;
    }

    // remove existing listeners which no longer used
    //如果新节点上没有事件监听,则将旧节点上的事件监听都删除
    if ( oldOn && oldListener ) {
        // if element changed or deleted we remove all existing listeners unconditionally
        if ( !on ) {
            for (name in oldOn) {
                // remove listener if element was changed or existing listeners removed
                oldElm.removeEventListener ( name, oldListener, false );
            }
        } else {
            //删除旧节点中新节点不存在的事件监听
            for (name in oldOn) {
                // remove listener if existing listener removed
                if ( !on[ name ] ) {
                    oldElm.removeEventListener ( name, oldListener, false );
                }
            }
        }
    }

    // add new listeners which has not already attached
    if ( on ) {
        // reuse existing listener or create new
        //如果oldvnode上已经有listener,则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‘
        //如果oldvnode上没有事件处理器
        if ( !oldOn ) {
            for (name in on) {
                // add listener if element was changed or new listeners added
                //直接将vnode上的事件处理器添加到elm上
                elm.addEventListener ( name, listener, false );
            }
        } else {
            for (name in on) {
                // add listener if new listener added
                //否则添加oldvnode上没有的事件处理器
                if ( !oldOn[ name ] ) {
                    elm.addEventListener ( name, listener, false );
                }
            }
        }
    }
}

module.exports = {
    create: updateEventListeners,
    update: updateEventListeners,
    destroy: updateEventListeners
};
props

主要功能:

从elm上删除vnode中不存在的属性

更新elm上的属性

function updateProps(oldVnode, vnode) {
  var key, cur, old, elm = vnode.elm,
      oldProps = oldVnode.data.props, props = vnode.data.props;
 //如果新旧节点都不存在属性,则直接返回
  if (!oldProps && !props) return;
  oldProps = oldProps || {};
  props = props || {};
  //删除旧节点中新节点没有的属性
  for (key in oldProps) {
    if (!props[key]) {
      delete elm[key];
    }
  }
  //更新属性
  for (key in props) {
    cur = props[key];
    old = oldProps[key];
    //如果新旧节点属性不同,且对比的属性不是value或者elm上对应属性和新属性也不同,那么就需要更新
    if (old !== cur && (key !== "value" || elm[key] !== cur)) {
      elm[key] = cur;
    }
  }
}

module.exports = {create: updateProps, update: updateProps};

style

主要功能如下:

将elm上存在于oldvnode中但不存在于vnode中不存在的style置空

如果vnode.style中的delayed与oldvnode的不同,则更新delayed的属性值,并在下一帧将elm的style设置为该值,从而实现动画过渡效果

非delayed和remove的style直接更新

vnode被destroy时,直接将对应style更新为vnode.data.style.destory的值

vnode被reomve时,如果style.remove不存在,直接调用全局remove钩子进入下一个remove过程
如果style.remove存在,那么我们就需要设置remove动画过渡效果,等到过渡效果结束之后,才调用
下一个remove过程

//如果存在requestAnimationFrame,则直接使用,以优化性能,否则用setTimeout
var raf = (typeof window !== "undefined" && window.requestAnimationFrame) || setTimeout;
var nextFrame = function(fn) { raf(function() { raf(fn); }); };

//通过nextFrame来实现动画效果
function setNextFrame(obj, prop, val) {
  nextFrame(function() { obj[prop] = val; });
}

function updateStyle(oldVnode, vnode) {
  var cur, name, elm = vnode.elm,
      oldStyle = oldVnode.data.style,
      style = vnode.data.style;
  //如果oldvnode和vnode都没有style,直接返回
  if (!oldStyle && !style) return;
  oldStyle = oldStyle || {};
  style = style || {};
  var oldHasDel = "delayed" in oldStyle;
  //遍历oldvnode的style
  for (name in oldStyle) {
    //如果vnode中无该style,则置空
    if (!style[name]) {
      elm.style[name] = "";
    }
  }
  //如果vnode的style中有delayed且与oldvnode中的不同,则在下一帧设置delayed的参数
  for (name in style) {
    cur = style[name];
    if (name === "delayed") {
      for (name in style.delayed) {
        cur = style.delayed[name];
        if (!oldHasDel || cur !== oldStyle.delayed[name]) {
          setNextFrame(elm.style, name, cur);
        }
      }
    }
    //如果不是delayed和remove的style,且不同于oldvnode的值,则直接设置新值
    else if (name !== "remove" && cur !== oldStyle[name]) {
      elm.style[name] = cur;
    }
  }
}

//设置节点被destory时的style
function applyDestroyStyle(vnode) {
  var style, name, elm = vnode.elm, s = vnode.data.style;
  if (!s || !(style = s.destroy)) return;
  for (name in style) {
    elm.style[name] = style[name];
  }
}
//删除效果,当我们删除一个元素时,先回调用删除过度效果,过渡完才会将节点remove
function applyRemoveStyle(vnode, rm) {
  var s = vnode.data.style;
  //如果没有style或没有style.remove
  if (!s || !s.remove) {
    //直接调用rm,即实际上是调用全局的remove钩子
    rm();
    return;
  }
  var name, elm = vnode.elm, idx, i = 0, maxDur = 0,
      compStyle, style = s.remove, amount = 0, applied = [];
  //设置并记录remove动作后删除节点前的样式
  for (name in style) {
    applied.push(name);
    elm.style[name] = style[name];
  }
  compStyle = getComputedStyle(elm);
  //拿到所有需要过渡的属性
  var props = compStyle["transition-property"].split(", ");
  //对过渡属性计数,这里applied.length >=amount,因为有些属性是不需要过渡的
  for (; i < props.length; ++i) {
    if(applied.indexOf(props[i]) !== -1) amount++;
  }
  //当过渡效果的完成后,才remove节点,调用下一个remove过程
  elm.addEventListener("transitionend", function(ev) {
    if (ev.target === elm) --amount;
    if (amount === 0) rm();
  });
}

module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};

第五站 is

啃完modules这些大部头,总算有个比较好吃的甜品了,他主要功能就是判断是否为array类型或者原始类型

//is工具库,用于判断是否为array或者原始类型
module.exports = {
  array: Array.isArray,
  primitive: function(s) { return typeof s === "string" || typeof s === "number"; },
};
中途休息

看了这么多源码,估计也累了吧,毕竟一下完全理解可能有点难,不妨先休息一下,消化一下,下一章将会见到最大的boss——snabbdom本身!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88234.html

相关文章

  • vue2源码学习开胃——snabbdom源码学习(二)

    摘要:前言在上一章我们学习了,等模块,在这一篇我们将会学习到的核心功能和功能。如果父节点没变化,我们就比较所有同层的子节点,对这些子节点进行删除创建移位操作。只需要对两个进行判断是否相似,如果相似,则对他们进行操作,否则直接用替换。 前言 在上一章我们学习了,modules,vnode,h,htmldomapi,is等模块,在这一篇我们将会学习到snabbdom的核心功能——patchVno...

    BetaRabbit 评论0 收藏0
  • Snabbdom.js(

    摘要:闲聊在学的过程中,虚拟应该是听的最多的概念之一,得知其是借鉴进行开发,故习之。以我的观点来看,多个相同元素渲染时,则需要为每个元素添加值。 闲聊:在学vue的过程中,虚拟dom应该是听的最多的概念之一,得知其是借鉴snabbdom.js进行开发,故习之。由于我工作处于IE8的环境,对ES6,TS这些知识的练习也只是浅尝辄止,而snabbdom.js从v.0.5.4这个版本后开始使用TS...

    mating 评论0 收藏0
  • Luy 1.0 :个React-like轮子的诞生

    摘要:司徒正美的一款了不起的化方案,支持到。行代码内实现一个胡子大哈实现的作品其实就是的了源码学习个人文章源码学习个人文章源码学习个人文章源码学习个人文章这几片文章的作者都是司徒正美,全面的解析和官方的对比。 前言 在过去的一个多月中,为了能够更深入的学习,使用React,了解React内部算法,数据结构,我自己,从零开始写了一个玩具框架。 截止今日,终于可以发布第一个版本,因为就在昨天,我...

    codecook 评论0 收藏0
  • snabbdom源码解析() 准备工作

    摘要:阅读源码的时候,想了解虚拟结构的实现,发现在的地方。然而慢慢的人们发现,在我们的代码中布满了一系列操作的代码。源码解析系列源码解析一准备工作源码解析二函数源码解析三对象源码解析四方法源码解析五钩子源码解析六模块源码解析七事件处理个人博客地址 前言 虚拟 DOM 结构概念随着 react 的诞生而火起来,之后 vue2.0 也加入了虚拟 DOM 的概念。 阅读 vue 源码的时候,想了解...

    defcon 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<