Snabbdom

吐槽君 分类:javascript

前情提要

  • 刀耕火种时代:手动操作dom,不需要考虑兼容性;随着时间推移,前端需求越来越复杂,需要处理更复杂的数据和dom结构。
  • MVVM框架:解决视图和状态同步问题,当视图发生变化,自动更新数据;当数据发生变化,自动更新视图。
  • 模板引擎:利于seo,数据安全性高,无须担心js被用户禁用,但是无法跟踪dom状态。

Snabbdom

一个高性能的虚拟DOM库

vue中snabbdom.png

在vue的仓库中可以看到Snabbdom的身影,vue使用的虚拟dom基于Snabbdom改造。

vnode

一个普通的js对象,是一个节点描述对象,描述如何创建真实节点

export interface VNode {
    // 选择器
    sel: string | undefined;  
    data: VNodeData | undefined;
    elm: Node | undefined;
    // text和children为互斥关系
    text: string | undefined;  
    children: Array<VNode | string> | undefined;
    key: Key | undefined;
}
// 在patch过程中,通过vnode的key和sel选择器字段判断俩个节点是否相同
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
// 通过对象是否存在sel节点判断是vnode节点还是普通的dom节点
function isVnode (vnode: any): vnode is VNode {
  // 通过是否有sel属性识别Vnode节点
  return vnode.sel !== undefined
}
 

虚拟dom

  • 解决的问题:描述应用的状态与视图的关系,将状态渲染成视图。jquery是命令式操作dom,状态不好管理,逻辑混乱;angular/react/vue都是声明式操作dom,只需要把状态维护好,如何操作dom由框架实现,虚拟dom就是描述状态的一种方式。

  • 手动DOM操作比较麻烦,并且很难跟踪以前的DOM状态。每次更改网页状态时实际上都重新创建了整个DOM,则您的网页将非常缓慢,并且输入文本将失去焦点。js的运算速度比dom操作的执行速度快,使用patch计算出真正需要更新的节点,大大减少dom操作,提升性能。

  • 跨平台,以JavaScript对象为基础而不依赖真实平台环境。

    浏览器渲染dom
    服务端渲染SSR
    原生应用
    小程序

  • 虚拟dom过程:渲染函数——>vnode——>真实节点

Snabbdom小例子

创建虚拟dom,通过patch更新节点
import { init } from "Snabbdom/build/package/init"
import { h } from "Snabbdom/build/package/h"

const patch = init([])

// 参数1:标签+选择器
// 参数2:标签的文本内容
let vnode = h('div#container.cls', 'hello world')
let app = document.querySelector('#app')
// 参数1:旧的vnode或者dom元素
// 参数2:新的vnode
// 返回新的vnode
let oldnode = patch(app, vnode)
 

修改前
image.png
修改后
image.png

Snabbdom模块的作用

  • Snabbdom的核心库不能处理dom元素的属性、样式、事件等,需要通过Snabbdom提供的模块实现

官方模块

  • attributes

    <input type="button" id="p" value="prop全选" test="自定义属性" />
     

    image.png

    html标签的自定义与预定义属性。预定义(标签自带的)属性有:type,id,value,name等

  • props

    js原生对象的直接属性。

  • dataset,处理html中data-xx属性

  • class

  • style

  • eventlisteners

使用模块功能演示prop和attr的区别

  • 导入模块
  • init方法中注册模块
  • h函数的第二个参数中使用模块
import { init } from "snabbdom/build/package/init"
import { h } from "snabbdom/build/package/h"

import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
import { attributesModule } from "snabbdom/build/package/modules/attributes";
import { propsModule } from "snabbdom/build/package/modules/props";
// init中注册模块
const patch = init([
  styleModule,
  eventListenersModule,
  attributesModule,
  propsModule
]);

let vnode
function render(attrCheck, propCheck) {
  vnode = patch(vnode, view(attrCheck, propCheck));
}
function view (attrCheck, propCheck, propBox) {
  const attrCheckOps = {
    name: "attr1",
    type: "checkbox",
    checked: attrCheck
  };
  if (attrCheck) attrCheckOps.checked = "checked";
  if (propBox) propCheck = propBox;
    // h函数第二个参数为对象是表示模块配置,字符串为文本内容
    return h("div", [
      h(
        "h1",
        {
          style: {
            backgroundColor: "yellow",
          },
        },
        "attr与prop的区别"
      ),
      h("input", {
        attrs: attrCheckOps,
      }),
      "attr控制的勾选框",
      h("br"),
      h("input#attrBtn", {
        attrs: {
          type: "button",
          value: "attr按钮选中",
        },
        on: {
          click: attrEventHandler,
        },
      }),
      h("hr"),
      h("input", {
        attrs: {
          name: "prop1",
          type: "checkbox",
        },
        on: {
          click: checkboxHandler,
        },
        props: {
          checked: !!propCheck,
        },
      }),
      "prop控制的勾选框",
      h("br"),
      h("input#propBtn", {
        props: {
          type: "button",
          value: "prop按钮选中",
        },
        on: {
          click: propEventHandler,
        },
      }),
    ]);
}
function attrEventHandler () {
  console.log("点击触发attrClick事件");
  render(true);
}
function propEventHandler () {
  console.log("点击触发propClick事件");
  render(null, true);
}
function checkboxHandler (e) {
  render(null, null, !e.target.checked);
}
window.addEventListener("DOMContentLoaded", () => {
  vnode = document.getElementById("app");
  render();
});

 

操作1,分别通过俩个按钮去控制checkbox,可以选中,attr方式实现选中会在html标签上添加“checked”属性

image.png

image.png

操作2,手动点击勾选框取消选中,可以看到attr方式的“checked”属性依然存在,但是勾选框状态为未选中。手动点击勾选框修改的实际是dom对象的checked属性(prop),不是attributes中的checked(attr)。

  • 当prop和attr的值为Boolean
    同时存在时,浏览器以prop的值为准。
    初始化时修改attr的值,prop会更新,一旦手动触发prop的值改变,再修改attr的值不会更新prop;
    修改prop值不会触发attr更新
  • 非Boolean值,attr和prop会同步更新

操作3,点击prop勾选框取消勾选,再点击prop按钮,无法选中勾选框

image.png

点击prop按钮,成功让props的checked设为true

image.png

dom元素的对象属性checked仍为false

为什么我们在代码中根据props模块提供的能力使prop属性为true,勾选框的checked值仍为false呢?

image.png

查看Snabbdom源码的props模块,可以看到内部是通过对比Vnode节点的新旧prop值来判断是否更新dom的prop值,导致它仍为dom元素的prop值没有变化。

所以,我们只要把勾选框的响应的状态变更都更新到prop对象中,Snabbdom才能知道dom状态已经发生变化。

Snabbdom还提供了hooks钩子方便我们操作更复杂的dom结构,里面最复杂的是patch方法的diff流程,同层对比新旧节点。

回复

我来回复
  • 暂无回复内容