精华内容
下载资源
问答
  • 虚拟DOM 之 Snabbdom 一、基本介绍
    千次阅读
    2020-07-26 17:41:19

    Snabbdom

    接口介绍(Snabbdom@1.0.1)

    官方文档

    当前snabbdom版本为 @1.0.1。接口介绍在官方文档的基础上做扩展,新版本接口使用基本和@0.7.4差不多。

    Snabbdom的核心只提供最基本的功能。更多功能可以通过“模块”扩展。

    Snabbdom用于扩展的“模块”,类似于插件。可以自定义。

    导入

    官方提供的导入snabbdom的示例是:

    import { init } from 'snabbdom/init'
    import { classModule } from 'snabbdom/modules/class'
    import { propsModule } from 'snabbdom/modules/props'
    import { styleModule } from 'snabbdom/modules/style'
    import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
    import { h } from 'snabbdom/h'
    

    但目前在parcel运行时会报错,找不到对应模块,查看node_modules/snabbdom/package.json配置,找到一个exports字段(可能是新字段,npmjs官方没有介绍该字段)。

    exports字段中配置了snabbdom下每个模块的路径,直接使用这个路径导入即可。

    import { init } from 'snabbdom/build/package/init'
    import { classModule } from 'snabbdom/build/package/modules/class'
    import { propsModule } from 'snabbdom/build/package/modules/props'
    import { styleModule } from 'snabbdom/build/package/modules/style'
    import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
    import { h } from 'snabbdom/build/package/h'
    

    init()

    Snabbdom核心只公开了一个init函数。(不懂官方对“核心”的定义)

    init 的作用就是创建一个使用指定模块集的patch(补丁)函数。

    语法:init: (modules: Array[Module]) => patch:Function

    参数:

    • modules(必选):一个数组,存放需要注册的模块,默认必须传个[]
      • 模块指的是扩展Snabbdom功能的模块(插件),可以是内置模块,或自定义的模块。
    • domApi(可选):操作虚拟DOM的API对象
      • 默认是snabbdom封装好的一些方法,包括:
        • 将虚拟DOM转化为真实DOM
        • 删除、添加、插入DOM等操作
      • 可以通过传递 domApi,传入一些自定义操作,把虚拟DOM转化为具体想要的内容
        • 如 HTML字符串 或 其他类型的内容
      • 具体使用需要查看源码中init()函数中对domApi的使用

    返回:

    • patch:一个使用指定模块集的补丁函数
    import { init } from 'snabbdom/build/package/init'
    import { classModule } from 'snabbdom/build/package/modules/class'
    import { propsModule } from 'snabbdom/build/package/modules/props'
    import { styleModule } from 'snabbdom/build/package/modules/style'
    import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
    
    // 不使用模块,必须传个空[]
    // var patch = init([])
    
    var patch = init([
      classModule, // 切换类的模块
      propsModule, // 设置DOM元素属性的模块
      styleModule, // 设置行内样式或动画的模块
      eventListenersModule, // 事件监听模块
    ])
    

    patch()

    patch 的作用就是对比新旧两个vnode的差异,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点。

    它是整个snabbdom中的核心函数。

    语法:patch: (oldVnode: Element | VNode, newVnode: VNode) => VNode: newVnode

    参数:

    • oldVnode:接收一个真实的DOM 或 一个vnode
      • 真实的DOM:首次调用patch时往往传入页面上的一个占位符,例如Vue的el选项(#app)表示的DOM对象。
        • 第一个参数是真实DOM的情况,会将它转化成一个空的Vnode,然后经过判断新旧vnode不相同(key和sel相同),直接把newVnode转化成真实DOM,插入DOM树中,再从DOM树种移除参数1传入的DOM元素。
        • 所以这个情况不会去对比两个Vnode的差异
      • oldVnode:旧的VNode,它应该是上一次patch返回的结果。
        • 虽然使用手动获取的真实DOM,或历史用过的VNode,也能实现替换。但是应该使用最近一次的VNode去对比。
        • 因为snabbdom已经将信息存储在VNode中,所以为保证对比差异更少更精确,使用最近一次的结果去对比,避免进行多余的没有意义的对比、更新甚至创建新的用于对比的VNode,才是Snabbdom的使用目的。
    • newVnode:表示更新后的新视图的VNode
      • 它是VNode对象,而不是真实DOM对象

    返回:

    • newVnode

    卸载DOM官方方案:用一个【注释】节点作为newValue替换oldVnode,实现视觉上卸载DOM的效果。

    import { init } from 'snabbdom/build/package/init'
    import { h } from 'snabbdom/build/package/h'
    
    let patch = init([])
    let element = document.querySelector('#app')
    let newValue = h('div', 'Hello world')
    
    let oldValue = patch(element, newValue)
    console.log(oldValue === newValue) // true
    
    // oldValue应该是上一次patch返回的结果
    let endVnode = patch(oldValue, h('div', 'Hello Snabbdom'))
    
    // 卸载DOM
    let commentsVnode = patch(endVnode, h('!'))
    

    h()

    Snabbdom建议使用h 函数创建描述真实DOM的Virtual DOM(VNode)。

    语法:h: (sel: string, [data: VNodeData || null], [children: VNodeChildren]) => VNode

    参数:

    h 函数可以接收3个参数:

    • sel(必选):必须包含标签的名的css选择器 或 表示注释的!
      • css选择器由两个内容组成:
        • 标签名(必选):divh1p
        • id/class选择器(可选):#app.cls
          • id选择器必须在class前面,因为snabbdom内部解析sel的时候,默认把id选择器当作最前面的去解析。[详细查看源码解析-createElm()函数](#createElm() 函数)
    • data(可选):VNode数据对象 VNodeData,用来设置sel表示的标签的内容、属性、样式、事件绑定、key、hook等
      • 需要注册对应模块(init)才能在patch时生效,例如注册 styleModule 模块 style 样式才会生效
    • children(可选):子节点内容,允许接收的类型:
      • String:字符串会转化成文本节点
      • Number:数字也会转化成文本节点
      • VNode:使用h函数创建的VNode
      • Array:包含多个子节点的数组,子节点类型可以是 String Number VNode

    data 和 children可选参数可以单独使用,也可以同时使用,同时使用时,data在children前面。

    使用方式示例汇总:

    h('div') // h(sel)
    h('div', 'Hello world') // h(sel, children)
    h('div', { style: '#333' }) // h(sel, data)
    h('div', { style: '#333' }, 'Hello World') // h(sel, data, children)
    

    基本使用

    创建项目

    打包工具为了方便使用 parcel

    # 在项目目录下创建package.json
    npm init -y
    # 本地安装parcel
    npm install parcel
    

    配置 package.json 的scripts

    {
      "scripts": {
        "dev": "parcel inxex.html --open",
        "build": "parcel build index.html"
      }
    }
    

    创建目录结构

    │	index.html # parcel的入口文件
    │ package.json
    └-src
    		01-basicusage.js
    

    导入Snabbdom@0.7.4

    ESM 导入

    安装snabbdom@0.7.4(注意代码演示中使用不是新版本@1.x)

    npm install snabbdom@0.7.4
    

    官方示例使用的是CommonJS导入的snabbdom,如果用ES Module形式直接导入会返回undefined。

    通过查看node_modules/snabbdom中的源码发现,当使用ESM的时候,导入的是es/snabbdom.js,即src/snabbdom.ts编译后的文件。

    可以查看它们之一的源码,搜索export,发现只导出了3个成员,并没有导出default默认成员。

    使用CommonJS的require导入模块,会将所有成员打包成一个对象。

    使用ESM直接导入模块,只能接收默认成员,所以直接导入获取不到。

    ESM也可以通过*接收全部成员。

    // 官方示例使用CommonJS方式导入
    // var snabbdom = require('snabbdom');
    
    // 使用ESM方式导入 snabbdom
    
    // ESM方式导入的是node_modules/ex/snabbdom.js文件
    // 该文件只道导出了 h thunk init 3个具名成员,并没有导出默认成员
    // 所以直接导入接收不到
    // import snabbdom from 'snabbdom'
    // console.logo(snabbdom) // undefined
    
    // 导入时指定接收的成员
    import { h, thunk, init } from 'snabbdom'
    console.log(h, thunk, init)
    
    // 导入时指定一个对象名,用于接收全部成员
    // import * as snabbdom from 'snabbdom'
    // console.log(snabbdom)
    

    Snabbdom 的核心仅提供最近本的功能,只导出了三个函数:

    • init() 是一个高阶函数,返回patch()
    • h() 返回虚拟节点 VNode,Vue的render中使用了这个函数
      • h()函数用于创建虚拟DOM,在Snabbdom中用VNode描述虚拟节点,也就是虚拟DOM。
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
    
    • thunk() 是一种优化策略,可以在处理不可变数据时使用(用于优化复杂的视图)

    模块化语法参考

    模块化语法可以参考阮一峰老师的文章:

    代码演示

    演示功能:

    1. hello world
    2. div中放置子元素:h1 p

    流程:

    1. 首先使用init()创建patch函数
    2. 然后使用h()创建VNode(虚拟DOM)

    功能1代码:

    // 代码演示
    import { h, init } from 'snabbdom'
    
    // 1. hello world
    
    let patch = init([])
    
    let vnode = h('div#container.cls', 'hello world')
    
    // 获取占位DOM
    let app = document.querySelector('#app')
    
    let oldVnode = patch(app, vnode)
    
    // 对比差异更新视图
    // 假设有个操作要更新DOM
    setTimeout(() => {
      vnode = h('div', 'Hello Snabbdom')
      patch(oldVnode, vnode)
    }, 1000)
    
    

    功能2代码:

    // 2. div中放置子元素 h1 p
    import { h, init } from 'snabbdom'
    
    let patch = init([])
    
    let vnode = h('div#container.cls', [
      h('h1', 'Hello Sabbdom'),
      h('p', '这是一个p标签')
    ])
    
    let app = document.querySelector('#app')
    
    let oldVnode = patch(app, vnode)
    
    // 对比差异更新视图
    // 假设有个操作要更新DOM
    // 更新DOM子元素
    setTimeout(() => {
      vnode = h('div#container.cls', [
        h('h1', 'Hello world'),
        h('p', 'Hello p')
      ])
      vnode = patch(oldVnode, vnode)
    
      // 删除DOM(官方示例是个错误示例,已经被删掉)
      // 报错:Cannot read property 'key' of null
      // patch(endVnode, null)
    
      // 通过创建注释节点来实现
      // vnode = patch(vnode, h('!'))
      
      // Vnode节点仍然存在
      // patch(vnode, h('div', '又在原位置出现'))
    }, 1000)
    
    

    删除DOM节点的方法

    Snabbdom 旧版本中的官方文档曾经介绍卸载DOM的方法:

    patch(old, null)
    

    但是这个方式并不成功。

    然后有网友提出了issues:

    Unmounting by patch(vnode, null) documented but not implemented #461

    其中另一个网友提出可以通过用一个注释节点,替换旧节点实现卸载DOM。

    最终Snabbdom作者更新了文档:

    Unmounting / 卸载

    While there is no API specifically for removing a VNode tree from its mount point element, one way of almost achieving this is providing a comment VNode as the second argument to patch, such as:

    虽然没有专门用于用挂载元素中删除VNode树的API。

    但可以通过提供注释VNode作为patch的第二个参数的方法,几乎实现这一点。

    例如:

    patch(oldVnode, h('!', { hooks: { post: () => { /* patch complete */ } } }))
    

    当然,挂载元素上仍然会有一个注释节点(<!---->)。

    注意虽然使用这个方法删除了DOM,好像页面中找不到原来的位置。

    但其实patch返回的vnode仍然记录了信息,因为它返回了一个注释节点。

    重新用它更新视图,依然能在原位置渲染。

    模块

    Snabbdom的核心库并不能处理元素的 属性 / 样式 / 事件 等,如果需要处理的话,可以使用模块。

    常用模块

    官方提供了 6 个模块,也可以自定义模块。

    • attributes
      • 设置DOM元素的属性
      • 内部使用 DOM setAttribute() 方法
      • 处理布尔类型的属性时也会做相应的判断 selected checked等
    • props
      • 和 attributes 模块类似,设置DOM元素的属性
      • 内部使用的是 element[attr] = value 方式
      • 并且不会处理布尔类型的属性
    • class
      • 用于 切换 类样式
      • 注意:设置 元素的类样式是通过 sel 选择器
    • dataset
      • 设置 HTML5 中的 data-* 的自定义属性
    • eventlisteners
      • 注册和移除事件
    • style
      • 设置行内样式,支持动画(内部创建transitionend事件)
      • 额外的属性:delayed / remove / destroy

    模块使用

    步骤:

    1. 导入模块
    2. init() 中注册模块
    3. 使用 h() 函数创建VNode的时候,第二个参数可以传对象,对象中是模块需要的数据,可以设置行内样式、事件等
    import { init, h } from 'snabbdom'
    // 1. 导入模块
    import style from 'snabbdom/modules/style'
    import eventlisteners from 'snabbdom/modules/eventlisteners'
    
    // 2. 注册模块
    let patch = init([style, eventlisteners])
    
    // 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
    let vnode = h('div', {
      style: {
        backgroundColor: 'red'
      },
      on: {
        click: eventHandler
      }
    }, [
      h('h1', '这是h1标签'),
      h('p', '这是p标签')
    ])
    
    function eventHandler(event) {
      console.log('点击触发:',event.target.textContent)
    }
    
    let element = document.querySelector('#app')
    patch(element, vnode)
    
    更多相关内容
  • snabbdom的基本使用

    千次阅读 2020-08-11 21:32:16
    我们上一篇博客中介绍了虚拟DOM的基本概念及常见类库,其中常见类库有2中,snabbdom和virtual-dom,而snabbdom是vue2.x版本中所使用的,所以我们这片文章就主要来记录一下snabbdom的基本使用。 创建项目 打包工具...

    前言

    我们上一篇博客中介绍了虚拟DOM的基本概念及常见类库,其中常见类库有2种,snabbdom和virtual-dom,而snabbdom是vue2.x版本中所使用的,所以我们这片文章就主要来记录一下snabbdom的基本使用。

    创建项目

    • 打包工具为了方便使用比较简单的parcel

    • 创建项目 并安装parcel

          mkdir snabb-demo // 在工作目录下建立snabb-demo子目录
          cd snabb-demo //进入snabb-demo 目录
          yarn init --yes //初始化一个package.json文件,也可以用npm init
          yarn add parcel-bundler --dev // 安装parcel
          
      
    • 创建目录结构,根目录下创建一个index.html,src目录下创建一个basicuse.js
      其中index.html内容如下:

      其中html代码如下:
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title>snabbdom demo</title>
        </head>
        <body>
          <div id="app"></div>
        </body>
        <script src="./src/basicuse.js"></script>
        
      
    • 在package.json中配置脚本

      "scripts":{
          "dev": "parcel index.html --open", 
          "build":"parcel build index.html"
      }
      

    导入snabbdom

    snabbdom文档

    • 英文文档:https://github.com/snabbdom/snabbdom
    • 中文文档:https://github.com/coconilu/Blog/issues/152

    安装snabbdom

    ```
    yarn add snabbdom //即可安装sanbbdom,也可以安装指定版本0.7.4
    
    ```
    

    导入snabbdom

    • 采用es module 方式导入snabbdom

    文档给出的示例是 var snabbdom = require(‘snabbdom’)采用的是commonJS的模块化方式引入的;我们此处需要使用es module来进行导入,所以我们去node_modules中找到snabbdom中的snabbdom.js,发现它只导出了h、thunk、init三个函数,并且都没有采用export default导出,我们学习es6知道,如果没有采用 export default,我们用es module导入的时候就要用对象的形式 {} 去接收
    所以我们这样导入:import {h,thunk,init} from 'snabbdom’即可完成导入

    import {h,thunk,init} from 'snabbdom'
    
    • h函数的作用是生成一个虚拟dom,我们在vue中也在使用
    • init() 是一个高阶函数,返回一个patch()
    • thunk()是一种优化策略,主要优化复杂视图

    snabbdom使用示例

    创建一个hello world

    basicuse.js代码如下:

    
    import { h, init } from 'snabbdom'
    // 此例主要实现一个hello world 实例
    
    // init接收一个数组作为参数,该参数主要指定使用的模块列表,也可以为空
    // 返回一个使用指定模块集的patch函数,此函数的作用主要是对比两个vnode的差异更新到真实DOM
    let patch = init([])
    
    // h函数
    // 第一个参数:标签+选择器
    // 第二个参数:如果是字符串的话就是标签中的内容
    // 返回值:vnode虚拟dom
    let vnode = h('div#container.cls','hello world')
    
    let appDom = document.querySelector('#app')
    
    // 然后利用patch函数进行内容比较与替换
    
    // patch()函数
    // 第一个参数:可以是dom元素,如果是dom则会自动将dom转换成vnode
    // 第二个参数:vnode
    // 返回值:vnode
    
    let oldVnode = patch(appDom,vnode)
    
    

    index.html内容

       <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8">
            <title>snabbdom demo</title>
          </head>
          <body>
            <div id="app"></div>
          </body>
          <script src="./src/basicuse.js"></script>
         </html>
    

    执行yarn run dev 之后,可以看到浏览器页面中输出了hello world

    效果图如下:

    在这里插入图片描述

    假设我们从服务器拿到了内容想要进行替换,可以进行如下操作即可完成

    let ser = h('div',"hello snabbdom")
    patch(oldVnode,ser)
    

    页面就会输出hello snabbdom

    创建一个带子元素的节点并进行替换

    创建02-basicuse.js,内容如下

    import { h, init } from 'snabbdom'
    
    let patch = init([])
    
    // h()中第二个参数如果是字符串,被认为是内容,如果是数组被任务是子元素
    let vnode = h('div#container',[
        h('h1','hello snabbdom'),
        h('p','这是一个P标签')
    
    ])
    let app = document.querySelector('#app')
    let oldVnode = patch(app,vnode)
    
    setTimeout(()=>{
        vnode = h('div#container',[
            h('h1','hello world'),
            h('p','这是一个新的p标签内容')
        
        ]);
        patch(oldVnode,vnode)
    },2000)
    
    

    index.html内容

       <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8">
            <title>snabbdom demo</title>
          </head>
          <body>
            <div id="app"></div>
          </body>
          <script src="./src/02-basicuse.js"></script>
         </html>
    

    可以通过yarn run dev查看效果,最开始页面将id为app的div替换为一个含H1与P把标签的div,2秒后值改变为我们新设的值

    如果想要清空节点内容,注意官网给出的方式是错误的

    patch(oldVnode,null) //官网给出的错误清空方法
    
    patch(oldVnode,h('!')) //正确清空节点的方法
    //h('!') 创建一个注释节点替换之前节点内容
    
    

    snabbdom 模块

    snabbdom的核心库并不能处理元素的属性、样式、事件等,如果需要处理的话,可以使用模块来处理

    常用模块

    官方提供的模块有以下6个:

    • attributes
      • 设置dom元素的属性,使用setAttribute()
      • 会对布尔类型的属性进行判断
    • props
      • 和attributes模块类似,设置dom的属性,但是是以element[attr] = value的形式设置的
      • 不会处理布尔类型的属性
    • class
      • 切换类样式
    • dataset
      • 设置data-*的自定义属性
    • eventlisteners
      • 注册和移除事件
    • style
      • 设置行内样式、支持动画

    模块使用

    模块使用步骤

    • 导入需要的模块
    • 在init()中注册模块
    • 使用h()函数创建vnode时,可以把第二个参数设置为对象,其他参数后移

    代码演示

    创建03-basicuse.js,内容如下:

    
    
    
    import { h, init } from 'snabbdom'
    // 1、导入模块
    import style from 'snabbdom/modules/style'
    import eventlisteners from 'snabbdom/modules/eventlisteners'
    // 2、注册模块
    var patch = init([style,eventlisteners])
    
    // 3、使用h()函数的第二个参数存放样式、事件等,其他参数后移
    let vnode = h('div',{
        style:{
            backgroundColor:'red' // 如果两个单词采用驼峰写法
        },
        on:{
            click:addCount // 所有事件都写在on里
        }
    },[
        h('h1','h1的内容,增加了背景色和click事件'),
        h('p','这是一个p标签')
    ])
    // 创建一个div,背景色为红色且绑定click事件addCount
    // 包含2个孩子节点,一个是p标签,一个是h1标签
    function addCount(){
        alert('方法增加~')
    }
    let app = document.querySelector('#app')
    
    let oldvNode = patch(app,vnode)
    
    

    效果图如下:

    在这里插入图片描述

    总结一下:

    • 先引入模块 import x from ‘snabbdom/modules/xx’
    • init()初始化的时候是一个数组,可以传入我们需要的模块,也可不传
    • 我们在h()函数的第二个参数中,可以增加相关的配置,第二个参数是一个对象。
    • 如果我们需要给该节点增加子节点,则第三个参数为数组,若只是想添加节点内容则需要是字符串。
    展开全文
  • Snabbdom学习笔记

    2020-11-20 23:49:35
    snabbdom github文档 snabbdom 中文文档 安装Snabbdom npm install snabbdom--save 导入snabbdom 官网使用的是commonjs模块化语法,这里使用更为流行的ES6模块化语法import 关于模块化的语法请参考阮一峰...

    目录

    创建项目

    导入Snabbdom

    文档

    安装Snabbdom

    导入snabbdom

    HelloWorld案例

    Snabbdom模块

    attributes

    props

    class

    dataset

    eventlisteners

    style

    模块使用示例


    创建项目

    导入Snabbdom

    文档

    看文档的意义

    • 学习任何一个库都要先看文档
    • 通过文档了解库的作用
    • 看文档中提供的示例,快速实现一个demo
    • 通过文档查看API的使用

    文档地址

    snabbdom github文档

    snabbdom 中文文档

    安装Snabbdom

    npm install snabbdom@0.7.0 --save
    //注意:新版本运行之后会有问题,引入以后报错,这里安装0.7版本的
    

    导入snabbdom

    官网使用的是commonjs模块化语法,这里使用更为流行的ES6模块化语法import

    关于模块化的语法请参考阮一峰老师的module语法

    import {init, h, thunk} from 'snabbdom'

    Snabbdom仅提供最基本的功能,只导出了三个函数init()、h()、thunk()

    • init()是一个高阶函数,返回patch()
    • h()返回虚拟节点VNode,这个函数在Vue.js中见过
    new Vue({
      router,
      store,
      render:h => h(App)
    }).$mount('#app')
    • thunk()是一种优化策略,可以在处理不可变数据使用

    注意:导入的时候不能使用

    import snabbdom from 'snabbdom'

    原因:node_modules/src/snabbdom.ts末尾导出使用的语法是export导出API,没有使用export default导出默认输出

    HelloWorld案例

    1.

    import {h, init} from 'snabbdom'
    // 1.hello world
    // 参数:数组,模块
    // 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
    let patch = init([])//初始化patch
    //第一个参数:标签+选择器
    // 第二个参数:如果是字符串的话就是标签中的内容
    let vnode = h('div#container.cls', 'Hello World')
    
    let app = document.querySelector('#app')
    // 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
    // 第二个参数:VNode
    // 返回值:VNode
    
    // 作用:对比两个虚拟dom,若app是真实dom,会将其转化成虚拟dom,然后对比app的虚拟dom和vnode的差异,更新到真实dom,并返回虚拟dom 
    let oldVNode = patch(app, vnode)
     
    //假设的时刻
    vnode = h('div', 'Hello Snabbdom')
    
    patch(oldVNode, vnode)
    

    2.

    // 2.div防止子元素h1,p
    import {h,init} from 'snabbdom'
    
    let patch = init([])
    
    let vnode = h('div#container',[
    h('h1', 'Hello Snabbdom'),
    h('p', '这是一个p标签')
    ])
    
    let app = document.querySelector('#app')
    
    let oldVnode = patch(app, vnode)//更新vnode
    
    setTimeout(()=>{
      vnode = h('div#container',[
        h('h1', 'Hello World'),
        h('p', 'Hello P')
      ])
      patch(oldVnode, vnode)
    
      // 清空页面元素 --- 官网错误做法
      // patch(oldVnode, null)
      patch(oldVnode, h('!'))//使用注释<!-- -->替换oldVnode
    }, 2000);
    
    
    

    Snabbdom模块

    Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要可以使用模块。官方提供的常用模块,有6个:

    attributes

    • 设置DOM元素属性,设置setAttribute(),
    • 设置布尔类型的属性

    props

    • 和attributes相似,设置DOM元素的属性element[attr] = value
    • 不处理布尔类型的属性

    class

    • 切换类样式
    • 注意:给元素设置类样式是通过sel选择器

    dataset

    • 设置data-*的自定义属性

    eventlisteners

    • 注册和移除事件

    style

    • 设置行内样式,支持动画
    • delayed/remove/destroy

    模块使用示例

    import {init,h} from 'snabbdom'
    // 1.导入模块
    import style from 'snabbdom/modules/style'
    import eventlisteners from 'snabbdom/modules/eventlisteners'
    
    // 2.注册模块
    let patch = init([
      style,
      eventlisteners
    ])
    
    // 3.使用h()函数的第二个参数传入模块需要的数据(对象)
    let vnode = h('div',{
      style:{
        backgroundColor:'red'
      },
      on:{
        click:eventHandler
      }
    },[
      h('h1','Hello Snabbdom'),
      h('p', '这是p标签')
    ])
    
    function eventHandler(){
      console.log('点击我了')
    }
    
    let app = document.querySelector('#app')
    patch(app, vnode)

     

     

     

    展开全文
  • Snabbdom && Virtual DOM

    2021-11-02 09:21:41
    Snabbdom 的基本使用 Snabbdom 的源码解析 什么是 Virtual DOM Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM 真实 DOM 成员 let element = ...

    Virtual DOM

    课程目标

    • 了解什么是虚拟 DOM,以及虚拟 DOM 的作用
    • Snabbdom 的基本使用
    • Snabbdom 的源码解析

    什么是 Virtual DOM

    • Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM

    • 真实 DOM 成员

      let element = document.querySelector('#app')
      let s = ''
      for (var key in element) {
        s += key + ','
      }
      console.log(s)
      // 打印结果
      align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,autocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,offsetTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,oncopy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onchange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondragend,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchange,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypress,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,onresize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongotpointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointerup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerleave,onselectstart,onselectionchange,onanimationend,onanimationiteration,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,namespaceURI,prefix,localName,tagName,id,className,classList,slot,part,attributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLeft,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,elementTiming,previousElementSibling,nextElementSibling,children,firstElementChild,lastElementChild,childElementCount,onfullscreenchange,onfullscreenerror,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttributeNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAttributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElementsByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentElement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClientRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scrollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,remove,prepend,append,querySelector,querySelectorAll,requestFullscreen,webkitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestinationInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMMENT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION_NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMENT_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAINED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseURI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstChild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChildNodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocumentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace,insertBefore,appendChild,replaceChild,removeChild,addEventListener,removeEventListener,dispatchEvent
      
    • 可以使用 Virtual DOM 来描述真实 DOM,示例

      {
        sel: "div",
        data: {},
        children: undefined,
        text: "Hello Virtual DOM",
        elm: undefined,
        key: undefined
      }
      

    为什么使用 Virtual DOM

    • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升

    • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题

    • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是 Virtual DOM 出现了

    • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM

    • 参考 github 上 virtual-dom 的描述

      • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
      • 通过比较前后两次状态的差异更新真实 DOM

    虚拟 DOM 的作用

    • 维护视图和状态的关系

    • 复杂视图情况下提升渲染性能

    • 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

      在这里插入图片描述

    Virtual DOM 库

    • Snabbdom
      • Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
      • 大约 200 SLOC(single line of code)
      • 通过模块可扩展
      • 源码使用 TypeScript 开发
      • 最快的 Virtual DOM 之一
    • virtual-dom

    案例演示

    Snabbdom 基本使用成

    创建项目

    • 打包工具为了方便使用 parcel

    • 创建项目,并安装 parcel

      # 创建项目目录
      md snabbdom-demo
      # 进入项目目录
      cd snabbdom-demo
      # 创建 package.json
      npm init -y
      # 本地安装 parcel
      npm install parcel-bundler -D
      
    • 配置 package.json 的 scripts

      "scripts": {
        "dev": "parcel index.html --open",
        "build": "parcel build index.html"
      }
      
    • 创建目录结构

      │  index.html
      │  package.json
      └─src
           		01-basicusage.js
      

    导入 Snabbdom

    Snabbdom 文档

    • 看文档的意义

      • 学习任何一个库都要先看文档
      • 通过文档了解库的作用
      • 看文档中提供的示例,自己快速实现一个 demo
      • 通过文档查看 API 的使用
    • 文档地址

      • https://github.com/snabbdom/snabbdom
      • 当前版本 v2.1.0
      # --depth 表示克隆深度, 1 表示只克隆最新的版本. 因为如果项目迭代的版本很多, 克隆会很慢
      git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
      

    安装 Snabbdom

    • 安装 Snabbdom

      npm install snabbdom@2.1.0
      

    导入 Snabbdom

    • Snabbdom 的两个核心函数 init 和 h()
      • init() 是一个高阶函数,返回 patch()
      • h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
    import { init } from 'snabbdom/init'
    import { h } from 'snabbdom/h'
    const patch = init([])
    

    注意:此时运行的话会告诉我们找不到 init / h 模块,因为模块路径并不是 snabbdom/int,这个路径是在 package.json 中的 exports 字段设置的,而我们使用的打包工具不支持 exports 这个字段,webpack 4 也不支持,webpack 5 支持该字段。该字段在导入 snabbdom/init 的时候会补全路径成 snabbdom/build/package/init.js

    "exports": {
        "./init": "./build/package/init.js",
        "./h": "./build/package/h.js",
        "./helpers/attachto": "./build/package/helpers/attachto.js",
        "./hooks": "./build/package/hooks.js",
        "./htmldomapi": "./build/package/htmldomapi.js",
        "./is": "./build/package/is.js",
        "./jsx": "./build/package/jsx.js",
        "./modules/attributes": "./build/package/modules/attributes.js",
        "./modules/class": "./build/package/modules/class.js",
        "./modules/dataset": "./build/package/modules/dataset.js",
        "./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
        "./modules/hero": "./build/package/modules/hero.js",
        "./modules/module": "./build/package/modules/module.js",
        "./modules/props": "./build/package/modules/props.js",
        "./modules/style": "./build/package/modules/style.js",
        "./thunk": "./build/package/thunk.js",
        "./tovnode": "./build/package/tovnode.js",
        "./vnode": "./build/package/vnode.js"
      }
    
    • 如果使用不支持 package.json 的 exports 字段的打包工具,我们应该把模块的路径写全
      • 查看安装的 snabbdom 的目录结构
    import { h } from 'snabbdom/build/package/h'
    import { init } from 'snabbdom/build/package/init'
    import { classModule } from 'snabbdom/build/package/modules/class'
    
    • 回顾 Vue 中的 render 函数
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
    
    • thunk() 是一种优化策略,可以在处理不可变数据时使用

    代码演示

    基本使用

    import { h } from 'snabbdom/build/package/h'
    import { init } from 'snabbdom/build/package/init'
    
    // 使用 init() 函数创建 patch()
    // init() 的参数是数组,将来可以传入模块,处理属性/样式/事件等
    let patch = init([])
    
    // 使用 h() 函数创建 vnode
    let vnode = h('div.cls', [
      h('h1', 'Hello Snabbdom'),
      h('p', '这是段落')
    ])
    
    const app = document.querySelector('#app')
    // 把 vnode 渲染到空的 DOM 元素(替换)
    // 会返回新的 vnode
    let oldVnode = patch(app, vnode)
    
    setTimeout(() => {
      vnode = h('div.cls', [
        h('h1', 'Hello World'),
        h('p', '这是段落')
      ])
      // 把老的视图更新到新的状态
      oldVnode = patch(oldVnode, vnode)
      // h('!') 是创建注释
      patch(oldVnode, h('!'))
    }, 2000)
    

    模块

    Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,如果需要处理的话,可以使用模块

    常用模块

    • 官方提供了 6 个模块

      • attributes
        • 设置 DOM 元素的属性,使用 setAttribute()
        • 处理布尔类型的属性
      • props
        • attributes 模块相似,设置 DOM 元素的属性 element[attr] = value
        • 不处理布尔类型的属性
      • class
        • 切换类样式
        • 注意:给元素设置类样式是通过 sel 选择器
      • dataset
        • 设置 data-* 的自定义属性
      • eventlisteners
        • 注册和移除事件
      • style
        • 设置行内样式,支持动画
        • delayed/remove/destroy

    模块使用

    • 模块使用步骤:
      • 导入需要的模块
      • init() 中注册模块
      • 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移

    代码演示

    import { h } from 'snabbdom/build/package/h'
    import { init } from 'snabbdom/build/package/init'
    // 导入需要的模块
    import { styleModule } from 'snabbdom/build/package/modules/style'
    import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
    
    // 使用 init() 函数创建 patch()
    // init() 的参数是数组,将来可以传入模块,处理属性/样式/事件等
    let patch = init([
      // 注册模块
      styleModule,
      eventListenersModule
    ])
    
    // 使用 h() 函数创建 vnode
    let vnode = h('div.cls', {
      // 设置 DOM 元素的行内样式
      style: { color: '#DEDEDE', backgroundColor: '#181A1B' },
      // 注册事件
      on: { click: clickHandler }
    }, [
      h('h1', 'Hello Snabbdom'),
      h('p', '这是段落')
    ])
    
    function clickHandler () {
      // 此处的 this 指向对应的 vnode
      console.log(this.elm.innerHTML)
    }
    

    Snabbdom 源码解析

    概述

    如何学习源码

    • 先宏观了解
    • 带着目标看源码
    • 看源码的过程要不求甚解
    • 调试
    • 参考资料

    Snabbdom 的核心

    • 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
    • init() 设置模块,创建 patch()
    • patch() 比较新旧两个 VNode
    • 把变化的内容更新到真实 DOM 树上

    Snabbdom 源码

    • 源码地址:

      • https://github.com/snabbdom/snabbdom
    • src 目录结构

      ├── package
      │   ├── helpers
      │   │   └── attachto.ts		定义了 vnode.ts 中 AttachData 的数据结构
      │   ├── modules
      │   │   ├── attributes.ts
      │   │   ├── class.ts
      │   │   ├── dataset.ts
      │   │   ├── eventlisteners.ts
      │   │   ├── hero.ts				example 中使用到的自定义钩子
      │   │   ├── module.ts			定义了模块中用到的钩子函数
      │   │   ├── props.ts
      │   │   └── style.ts
      │   ├── h.ts							h() 函数,用来创建 VNode
      │   ├── hooks.ts					所有钩子函数的定义
      │   ├── htmldomapi.ts			对 DOM API 的包装
      │   ├── init.ts						加载 modules、DOMAPI,返回 patch 函数
      │   ├── is.ts							判断数组和原始值的函数
      │   ├── jsx-global.ts			jsx 的类型声明文件
      │   ├── jsx.ts						处理 jsx
      │   ├── thunk.ts					优化处理,对复杂视图不可变值得优化
      │   ├── tovnode.ts				DOM 转换成 VNode
      │   ├── ts-transform-js-extension.cjs
      │   ├── tsconfig.json			ts 的编译配置文件
      │   └── vnode.ts					虚拟节点定义
      

    h 函数

    • h() 函数介绍

      • 在使用 Vue 的时候见过 h() 函数

        new Vue({
          router,
          store,
          render: h => h(App)
        }).$mount('#app')
        
      • h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本

      • Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode

    • 函数重载

      • 概念

        • 参数个数类型不同的函数
        • JavaScript 中没有重载的概念
        • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
      • 重载的示意

        function add (a: number, b: number) {
          console.log(a + b)
        }
        function add (a: number, b: number, c: number) {
          console.log(a + b + c)
        }
        add(1, 2)
        add(1, 2, 3)
        
        function add (a: number, b: number) {
          console.log(a + b)
        }
        function add (a: number, b: string) {
          console.log(a + b)
        }
        add(1, 2)
        add(1, '2')
        
    • 源码位置:src/package/h.ts

      // h 函数的重载
      export function h (sel: string): VNode
      export function h (sel: string, data: VNodeData | null): VNode
      export function h (sel: string, children: VNodeChildren): VNode
      export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
      export function h (sel: any, b?: any, c?: any): VNode {
        var data: VNodeData = {}
        var children: any
        var text: any
        var i: number
        // 处理参数,实现重载的机制
        if (c !== undefined) {
          // 处理三个参数的情况
          // sel、data、children/text
          if (b !== null) {
            data = b
          }
          if (is.array(c)) {
            children = c
          } else if (is.primitive(c)) {
            text = c
          } else if (c && c.sel) {
            children = [c]
          }
        } else if (b !== undefined && b !== null) {
          if (is.array(b)) {
            children = b
          } else if (is.primitive(b)) {
            // 如果 c 是字符串或者数字
            text = b
          } else if (b && b.sel) {
            // 如果 b 是 VNode
            children = [b]
          } else { data = b }
        }
        if (children !== undefined) {
          // 处理 children 中的原始值(string/number)
          for (i = 0; i < children.length; ++i) {
            // 如果 child 是 string/number,创建文本节点
            if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
          }
        }
        if (
          sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
          (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
        ) {
          // 如果是 svg,添加命名空间
          addNS(data, children, sel)
        }
        // 返回 VNode
        return vnode(sel, data, children, text, undefined)
      };
      

    VNode

    • 一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是 Virtual DOM

    • 源码位置:src/package/vnode.ts

    export interface VNode {
      // 选择器
      sel: string | undefined;
      // 节点数据:属性/样式/事件等
      data: VNodeData | undefined;
      // 子节点,和 text 只能互斥
      children: Array<VNode | string> | undefined;
      // 记录 vnode 对应的真实 DOM
      elm: Node | undefined;
      // 节点中的内容,和 children 只能互斥
      text: string | undefined;
      // 优化用
      key: Key | undefined;
    }
    
    export function vnode (sel: string | undefined,
                          data: any | undefined,
                          children: Array<VNode | string> | undefined,
                          text: string | undefined,
                          elm: Element | Text | undefined): VNode {
      const key = data === undefined ? undefined : data.key
      return { sel, data, children, text, elm, key }
    }
    

    snabbdom

    • patch(oldVnode, newVnode)
    • 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
    • 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
    • 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
    • diff 过程只进行同层级比较

    在这里插入图片描述

    init

    • **功能:**init(modules, domApi),返回 patch() 函数(高阶函数)

    • 为什么要使用高阶函数?

      • 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
      • 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建
    • init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中

    • 源码位置:src/package/init.ts

      const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
      export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
        let i: number
        let j: number
        const cbs: ModuleHooks = {
          create: [],
          update: [],
          remove: [],
          destroy: [],
          pre: [],
          post: []
        }
        // 初始化 api
        const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
        // 把传入的所有模块的钩子方法,统一存储到 cbs 对象中
        // 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ... ]
      	for (i = 0; i < hooks.length; ++i) {
          // cbs['create'] = []
          cbs[hooks[i]] = []
          for (j = 0; j < modules.length; ++j) {
            // const hook = modules[0]['create']
            const hook = modules[j][hooks[i]]
            if (hook !== undefined) {
              (cbs[hooks[i]] as any[]).push(hook)
            }
          }
        }
        ……
        return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
          ……
        }
      }
      

    patch

    • 功能:

      • 传入新旧 VNode,对比差异,把差异渲染到 DOM
      • 返回新的 VNode,作为下一次 patch() 的 oldVnode
    • 执行过程:

      • 首先执行模块中的钩子函数 pre
      • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
        • 调用 patchVnode(),找节点的差异并更新 DOM
      • 如果 oldVnode 是 DOM 元素
        • 把 DOM 元素转换成 oldVnode
        • 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
        • 把刚创建的 DOM 元素插入到 parent 中
        • 移除老节点
        • 触发用户设置的 create 钩子函数
    • 源码位置:src/package/init.ts

      return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
        let i: number, elm: Node, parent: Node
        // 保存新插入节点的队列,为了触发钩子函数
        const insertedVnodeQueue: VNodeQueue = []
        // 执行模块的 pre 钩子函数
        for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
      	// 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm 
        if (!isVnode(oldVnode)) {
          // 把 DOM 元素转换成空的 VNode
          oldVnode = emptyNodeAt(oldVnode)
        }
      	// 如果新旧节点是相同节点(key 和 sel 相同)
        if (sameVnode(oldVnode, vnode)) {
          // 找节点的差异并更新 DOM
          patchVnode(oldVnode, vnode, insertedVnodeQueue)
        } else {
          // 如果新旧节点不同,vnode 创建对应的 DOM
          // 获取当前的 DOM 元素
          elm = oldVnode.elm!
          parent = api.parentNode(elm) as Node
      		// 触发 init/create 钩子函数,创建 DOM
          createElm(vnode, insertedVnodeQueue)
      
          if (parent !== null) {
            // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
            api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
            // 移除老节点
            removeVnodes(parent, [oldVnode], 0, 0)
          }
        }
      	// 执行用户设置的 insert 钩子函数
        for (i = 0; i < insertedVnodeQueue.length; ++i) {
          insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
        }
        // 执行模块的 post 钩子函数
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
        return vnode
      }
      

    createElm

    • 功能:

      • createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
      • 创建 vnode 对应的 DOM 元素
    • 执行过程:

      • 首先触发用户设置的 init 钩子函数
      • 如果选择器是!,创建评论节点
      • 如果选择器为空,创建文本节点
      • 如果选择器不为空
        • 解析选择器,设置标签的 id 和 class 属性
        • 执行模块create 钩子函数
        • 如果 vnode 有 children,创建子 vnode 对应的 DOM,追加到 DOM 树
        • 如果 vnode 的 text 值是 string/number,创建文本节点并追击到 DOM 树
        • 执行用户设置的 create 钩子函数
        • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
    • 源码位置:src/package/init.ts

        function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
          let i: any
          let data = vnode.data
          
          if (data !== undefined) {
            // 执行用户设置的 init 钩子函数
            const init = data.hook?.init
            if (isDef(init)) {
              init(vnode)
              data = vnode.data
            }
          }
          const children = vnode.children
          const sel = vnode.sel
          if (sel === '!') {
            // 如果选择器是!,创建注释节点
            if (isUndef(vnode.text)) {
              vnode.text = ''
            }
            vnode.elm = api.createComment(vnode.text!)
          } else if (sel !== undefined) {
            // 如果选择器不为空
            // 解析选择器
            // Parse selector
            const hashIdx = sel.indexOf('#')
            const dotIdx = sel.indexOf('.', hashIdx)
            const hash = hashIdx > 0 ? hashIdx : sel.length
            const dot = dotIdx > 0 ? dotIdx : sel.length
            const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
            const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
              ? api.createElementNS(i, tag)
              : api.createElement(tag)
            if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
            if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
            // 执行模块的 create 钩子函数
            for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
            // 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
            if (is.array(children)) {
              for (i = 0; i < children.length; ++i) {
                const ch = children[i]
                if (ch != null) {
                  api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
                }
              }
            } else if (is.primitive(vnode.text)) {
              // 如果 vnode 的 text 值是 string/number,创建文本节点并追加到 DOM 树
              api.appendChild(elm, api.createTextNode(vnode.text))
            }
            const hook = vnode.data!.hook
            if (isDef(hook)) {
              // 执行用户传入的钩子 create
              hook.create?.(emptyNode, vnode)
              if (hook.insert) {
                // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
                insertedVnodeQueue.push(vnode)
              }
            }
          } else {
            // 如果选择器为空,创建文本节点
            vnode.elm = api.createTextNode(vnode.text!)
          }
          // 返回新创建的 DOM                                
          return vnode.elm
        }
      

    patchVnode

    • 功能:

      • patchVnode(oldVnode, vnode, insertedVnodeQueue)
      • 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
    • 执行过程:

      • 首先执行用户设置的 prepatch 钩子函数
      • 执行 create 钩子函数
        • 首先执行模块create 钩子函数
        • 然后执行用户设置的 create 钩子函数
      • 如果 vnode.text 未定义
        • 如果 oldVnode.childrenvnode.children 都有值
          • 调用 updateChildren()
          • 使用 diff 算法对比子节点,更新子节点
        • 如果 vnode.children 有值,oldVnode.children 无值
          • 清空 DOM 元素
          • 调用 addVnodes(),批量添加子节点
        • 如果 oldVnode.children 有值,vnode.children 无值
          • 调用 removeVnodes(),批量移除子节点
        • 如果 oldVnode.text 有值
          • 清空 DOM 元素的内容
      • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
        • 如果老节点有子节点,全部移除
        • 设置 DOM 元素的 textContentvnode.text
      • 最后执行用户设置的 postpatch 钩子函数
    • 源码位置:src/package/init.ts

      function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
          const hook = vnode.data?.hook
          // 首先执行用户设置的 prepatch 钩子函数
          hook?.prepatch?.(oldVnode, vnode)
          const elm = vnode.elm = oldVnode.elm!
          const oldCh = oldVnode.children as VNode[]
          const ch = vnode.children as VNode[]
        	// 如果新老 vnode 相同返回
          if (oldVnode === vnode) return
          if (vnode.data !== undefined) {
            // 执行模块的 update 钩子函数
            for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
            // 执行用户设置的 update 钩子函数
            vnode.data.hook?.update?.(oldVnode, vnode)
          }
        	// 如果 vnode.text 未定义
          if (isUndef(vnode.text)) {
            // 如果新老节点都有 children
            if (isDef(oldCh) && isDef(ch)) {
              // 调用 updateChildren 对比子节点,更新子节点
              if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
            } else if (isDef(ch)) {
              // 如果新节点有 children,老节点没有 children
            	// 如果老节点有text,清空dom 元素的内容
              if (isDef(oldVnode.text)) api.setTextContent(elm, '')
              // 批量添加子节点
              addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
            } else if (isDef(oldCh)) {
              // 如果老节点有children,新节点没有children
            	// 批量移除子节点
              removeVnodes(elm, oldCh, 0, oldCh.length - 1)
            } else if (isDef(oldVnode.text)) {
              // 如果老节点有 text,清空 DOM 元素
              api.setTextContent(elm, '')
            }
          } else if (oldVnode.text !== vnode.text) {
            // 如果没有设置 vnode.text
            if (isDef(oldCh)) {
              // 如果老节点有 children,移除
              removeVnodes(elm, oldCh, 0, oldCh.length - 1)
            }
            // 设置 DOM 元素的 textContent 为 vnode.text
            api.setTextContent(elm, vnode.text!)
          }
          // 最后执行用户设置的 postpatch 钩子函数
          hook?.postpatch?.(oldVnode, vnode)
        }
      

    updateChildren

    • 功能:

      • diff 算法的核心,对比新旧节点的 children,更新 DOM
    • 执行过程:

      • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,但是这样的时间复杂度为 O(n^3)
      • 在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
      • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为 O(n)

      在这里插入图片描述

      • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
      • 在对开始和结束节点比较的时候,总共有四种情况
        • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
        • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
        • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
        • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)

      在这里插入图片描述

      • 开始节点和结束节点比较,这两种情况类似
        • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
        • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
      • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
        • 调用 patchVnode() 对比和更新节点
        • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++

    在这里插入图片描述

    • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
      • 调用 patchVnode() 对比和更新节点
      • 把 oldStartVnode 对应的 DOM 元素,移动到右边
        - 更新索引

    在这里插入图片描述

    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
      • 调用 patchVnode() 对比和更新节点
      • 把 oldEndVnode 对应的 DOM 元素,移动到左边
      • 更新索引

    在这里插入图片描述

    • 如果不是以上四种情况
      • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
      • 如果没有找到,说明 newStartNode 是新节点
        • 创建新节点对应的 DOM 元素,插入到 DOM 树中
      • 如果找到了
        • 判断新节点和找到的老节点的 sel 选择器是否相同
        • 如果不相同,说明节点被修改了
          • 重新创建对应的 DOM 元素,插入到 DOM 树中
        • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DuqVu7Jf-1635816080015)(assets/image-20200109184822439.png)]

    • 循环结束
      • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
      • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
    • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边

    在这里插入图片描述

    - 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除
    

    在这里插入图片描述

    • 源码位置:src/package/init.ts

      function updateChildren (parentElm: Node,
        oldCh: VNode[],
        newCh: VNode[],
        insertedVnodeQueue: VNodeQueue) {
        let oldStartIdx = 0
        let newStartIdx = 0
        let oldEndIdx = oldCh.length - 1
        let oldStartVnode = oldCh[0]
        let oldEndVnode = oldCh[oldEndIdx]
        let newEndIdx = newCh.length - 1
        let newStartVnode = newCh[0]
        let newEndVnode = newCh[newEndIdx]
        let oldKeyToIdx: KeyToIndexMap | undefined
        let idxInOld: number
        let elmToMove: VNode
        let before: any
      
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          // 索引变化后,可能会把节点设置为空
          if (oldStartVnode == null) {
            // 节点为空移动索引
            oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
          } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
          } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
          } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx]
          // 比较开始和结束节点的四种情况
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // 1. 比较老开始节点和新的开始节点
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // 2. 比较老结束节点和新的结束节点
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            // 3. 比较老开始节点和新的结束节点
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
            api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            // 4. 比较老结束节点和新的开始节点
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
            api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
            // 开始节点和结束节点都不相同
            // 使用 newStartNode 的 key 再老节点数组中找相同节点
            // 先设置记录 key 和 index 的对象
            if (oldKeyToIdx === undefined) {
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            }
            // 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
            idxInOld = oldKeyToIdx[newStartVnode.key as string]
            // 如果是新的vnode
            if (isUndef(idxInOld)) { // New element
              // 如果没找到,newStartNode 是新节点
              // 创建元素插入 DOM 树
              api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
            } else {
              // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
              elmToMove = oldCh[idxInOld]
              if (elmToMove.sel !== newStartVnode.sel) {
                // 如果新旧节点的选择器不同
                // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
                api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
              } else {
                // 如果相同,patchVnode()
                // 把 elmToMove 对应的 DOM 元素,移动到左边
                patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
                oldCh[idxInOld] = undefined as any
                api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
              }
            }
            // 重新给 newStartVnode 赋值,指向下一个新节点
            newStartVnode = newCh[++newStartIdx]
          }
        }
        // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
        if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
          if (oldStartIdx > oldEndIdx) {
            // 如果老节点数组先遍历完成,说明有新的节点剩余
            // 把剩余的新节点都插入到右边
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
          } else {
            // 如果新节点数组先遍历完成,说明老节点有剩余
            // 批量删除老节点
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
          }
        }
      }
      

    调试 updateChildren

    <ul>
      <li>首页</li>
      <li>微博</li>
      <li>视频</li>
    </ul>
    
    <ul>
      <li>首页</li>
      <li>视频</li>
      <li>微博</li>
    </ul>
    
    展开全文
  • vue在官方文档中提到与react的渲染性能对比中,因为其使用了snabbdom而有更优异的性能。 JavaScript 开销直接与求算必要 DOM 操作的机制相关。尽管 Vue 和 React 都使用了 Virtual Dom 实现这一点,但 Vue 的 ...
  • 本文挑选 Snabbdom 模块系统作为主要核心点介绍,其他内容可以查阅官方文档Snabbdom》[2]。 一、Snabbdom 是什么 Snabbdom 是一个专注于简单性、模块化、强大特性和性能的虚拟 DOM 库。其中有几个核心特性: 核心...
  • 什么是 Virtual DOM ...看文档的意义 学习任何一个库都要先看文档 通过文档了解库的作用 看文档中提供的示例,自己快速实现一个 demo 通过文档查看 API 的使用 (以上题外话_~~~下面提供一个地址感兴趣的阔以看看) ...
  • 快速上手Snabbdom

    2020-08-24 15:48:55
    Snabbdom基本使用 一.创建项目 打包工具为了方便使用 parcel # 创建项目目录 md snabbdom-demo # 进入项目目录 cd snabbdom-demo # 创建 package.json yarn init -y # 本地安装 parcel yarn add parcel-bundler ...
  • Snabbdom源码解析

    2020-08-25 15:55:27
    Snabbdom源码解析 一.Snabbdom的核心 1.使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM 2.init() 设置模块,创建 patch() 3.patch() 比较新旧两个 VNode 4.把变化的内容更新到真实 DOM 树上 二.src 目录...
  • 1、Vue的响应式原理 Vue2.0的响应式原理是基于Object.defineProperty函数是实现的,Vue3.0是基于es6的新特性Proxy实现的 2、描述Vue响应式原理中的Vue类、Observer类、Dep类、Watcher类、Compiler类 ...
  • Snabbdom 的基本使用 创建项目 打包工具为了方便使用 parcel 创建项目,并安装 parcel md snabbdom-demo cd snabbdom-demo yarn init -y yarn add parcel-bundler 配置 package.json 的 scripts "scripts": { ...
  • 引言snabbdom是目前最流行的virtual dom库之一。它的github仓库有关于它的一切。本篇博客介绍Snabbdom的一个官方demo => reorder-animation,并解说其基本实现,以方便理解Snabbdom基本用法。废话不多说直接上图...
  • Snabbdom(虚拟dom)

    2021-08-14 16:34:10
    创建项目目录 snabbdom md snabbdom 初始化package.json npm init 或 yarn init 安装打包工具 npm ...
  • 3,看文档中提供的示例,自己快速实现一个 demo 4,通过文档查看 API 的使用 文档地址 1,https://github.com/snabbdom/snabbdom 2,中文翻译 安装 Snabbdom 安装 Snabbdom # 版本 0.7.4 $ yarn add snabbdom 导入 ...
  • 昨天,我们完成了最简单的模板到DOM的实现,以及执行setData时候页面重新渲染工作,只不过比较粗暴还没有引入snabbdom进行了重新渲染,今天我们来完成其中的事件绑定部分代码 这里我们先不去管循环标签这些的解析...
  • // 298 对复杂的文档DOM结构(复杂视图情况下提升渲染性能),提供一种方便的工具,进行最小化地DOM操作。既然我们可以用JS对象表示DOM结构,那么当数据状态发生变化而需要改变DOM结构时,我们先通过JS对象表示的...
  • 一、vue-router 1.1 动态路由传参 路由组价传参 通过path中带有:id routes: [ { path: '/user/:id', component: User,}, ] 通过props属性,让路由和组件解耦 const User = { props: ['id'], ... routes:
  • 文档对象模型 (DOM) 是HTML和XML文档的编程接口,将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合, 简单的说,DOM就是解析文档,将web页面和脚本或程序语言连接起来。更多内容可以参考MDN中对...
  • jQuery-demo snabbdom-demo Snabbdom 中文翻译 Snabbdom 基本使用 为了方便使用parcel打包工具 安装 npm i snabbdom -D 导入 Snabbdom import { init } from 'snabbdom/init'; import { h } from 'snabbdom/h'; // ...
  • virtual dom diff vdom diff是virtual dom的核心算法,snabbdom的实现原理与react官方文档Reconciliation一致 总结起来有: 对两个树结构进行完整的diff和patch,复杂度增长为O(n^3),几乎不可用 对两个数结构进行启发...
  • 对很多人而言,虚拟 DOM 都是一个很高大上而且远不可及的专有名词,以前我也这么认为,后来在学习 Vue 源码的时候发现 Vue 的虚拟 DOM 方案衍生于本文要讲的 snabbdom 工具,经过阅读源码之后才发现,虚拟 DOM 原来...
  • snabbdom 源码阅读分析

    2018-12-06 09:30:28
    随着 React Vue 等框架的流行,Virtual DOM 也越来越火,snabbdom 是其中一种实现,而且 Vue 2.x 版本的 Virtual DOM 部分也是基于 snabbdom 进行修改的。snabbdom 这个库核心代码只有 200 多行,非常适合想要深入...
  • Snabbdom 源码解析 核心源码解析 如何学习源码 先宏观了解:学习库的核心执行过程 带着目标看源码,比如: VNode是如何创建的 VNode是如何渲染成真实DOM的 看源码的过程要不求甚解 看源码的过程要围绕核心目标 ...
  • 这里用Fragment是可以减少DOM操作,文档碎片流对象,不懂得可以去百度。 接着写最复杂的 就是for循环, 我们可以看下源码 这个是做什么的,就是将我们的key值存起来,就是旧前旧后中包含的Key值取出来,方便我们循环...
  • snabbdom 简介 及 准备工作2.1 简介2.2 搭建初始环境1. 安装snabbdom2. 安装webpack5并配置3. 复制官方demo Example3. h函数的介绍与使用3.1 介绍3.2 使用h函数 创建虚拟节点3.3 使用patch函数 将虚
  • vue在官方文档中提到与react的渲染性能对比中,因为其使用了snabbdom而有更优异的性能。 JavaScript 开销直接与求算必要 DOM 操作的机制相关。尽管 Vue 和 React 都使用了 Virtual Dom 实现这一点,但 Vue 的 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 445
精华内容 178
关键字:

snabbdom 文档

友情链接: yeteyete-7245251-ueulvr.rar