博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
通过 Snabbdom 理解 Virtual DOM
阅读量:7070 次
发布时间:2019-06-28

本文共 4540 字,大约阅读时间需要 15 分钟。

前言

作为一名使用前端 UI 框架 React 的开发者,不了解 Virtual DOM、不看下 diff 算法,总感觉不够称职。于是最近花了几节课的时间把 整体过了一遍,终于把 Virtual Dom 和 diff 算法给理解了下,也算解了耽搁许久的事情。

介绍

Snabbdom: A virtual DOM library with focus on simplicity, modularity, powerful features and performance.

整体看下项目组织,Snabbdom 项目并不是很复杂,目录结构异常清晰,其中关于 Virtual Dom 和 diff 核心代码感觉不超过300行,原来如此简洁。

简而化之,Snabbdom 的几个核心API:

  • snabbdom.init()
    • 通过加载如 class/style/props/eventlisteners 等 modules 来初始化 patch,该方法返回 patch
    • 其中 modules 的工作是通过为 hooks 注册全局的 listeners
  • patch()
    • diff 之所在,核心中核心
    • 其中在 patch 过程中会通过 hooks 来 hook 进 DOM node 生命周期,能够做到在virtual node生命周期中的特定钩子点执行任意代码,即恰当的时机去做恰当的事情
    • 比如 patch(oldVnode, newVnode);
  • h()
    • 通过既定传参方式,生成 virtual node
    • 比如 const vnode = h('div', {style: {color: '#000'}}, 'Hello, World!');
  • tovnode()
    • 将一个 DOM node 转化为 Vnode
    • 更适用于服务端渲染
    • 比如 patch(toVNode(document.querySelector('.container')), newVNode)

试用

index.html

      
Document
这是临时预留的外层容器
复制代码

index.js

import { init, h } from 'snabbdom'; // helper function for creating vnodesconst patch = init([  // Init patch function with chosen modules  require('snabbdom/modules/class').default, // makes it easy to toggle classes  require('snabbdom/modules/props').default, // for setting properties on DOM elements  require('snabbdom/modules/style').default, // handles styling on elements with support for animations  require('snabbdom/modules/eventlisteners').default, // attaches event listeners]);const container = document.getElementById('container');function handleClick() {  console.log('click...');}// 注意规范,写成:div.two.classes#container 是不可以的,渲染后 
test
const vnode = h( 'div#first.two.classes', { on: { click: handleClick }, style: { height: '100px', width: '100px', border: '1px solid red' } }, 'test');// Patch into empty DOM element – this modifies the DOM as a side effectpatch(container, vnode);function anotherEventHandler() { console.log('another...');}var newVnode = h('div#first.two.classes', { on: { click: anotherEventHandler } }, [ h('span#c1', { style: { color: 'red' } }, 'This is now red'), ' and this is still just normal text']);// Second `patch` invocationpatch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state复制代码

Parcel 构建

npm initcnpm i parcel --save-dev# package.json 中 { "scripts": { "start": "parcel index.html" } }npm start复制代码

以上,就可以随意把玩了。

核心

通过之前的例子,大致浏览了调用关系。那针对 Virtual DOM 算法,只需要关注两个核心方法即可:

  1. 用 object 来表示 Vnode 的 h(sel: any, b?: any, c?: any): VNode
  2. diff算法的 patch(oldVnode: VNode | Element, vnode: VNode): VNode

h

export interface VNode {  sel: string | undefined;  data: VNodeData | undefined;  children: Array
| undefined; elm: Node | undefined; text: string | undefined; key: Key | undefined;}// 直接返回export function vnode( sel: string | undefined, data: any | undefined, children: Array
| undefined, text: string | undefined, elm: Element | Text | undefined): VNode { let key = data === undefined ? undefined : data.key; return { sel: sel, data: data, children: children, text: text, elm: elm, key: key, };}export function h(sel: string): VNode;export function h(sel: string, data: VNodeData): VNode;export function h(sel: string, children: VNodeChildren): VNode;export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;export function h(sel: any, b?: any, c?: any): VNode { // ... // 返回 Vnode return vnode(sel, data, children, text, undefined);}复制代码

patch

其核心算法逻辑如下

  • 如果不是同一 Vnode,那么
    • 根据 newVnode 创建 DOM
    • 插入该DOM
    • 删除原DOM
  • 如果是同一 Vnode,则进行 patchVnode(oldVnode: VNode, vnode: VNode)
    • newVnode 是 Text 节点
      • 清空 oldVnodes
      • 用 newVnodeText  更新
    • newVnode 不是 Text 节点时,判断是否有 children
      • 都有 children -- updateChildren
        • 首首比较首相同 -- patchVnode
        • 尾尾比较尾相同 -- patchVnode
        • 首尾比较首尾同 -- patchVnode + insertBefore
        • 尾首比较尾首同 -- patchVnode + insertBefore
        • 我中有你首 -- patchVnode(sel等时) + insertBefore
        • 我中没你首 -- insertBefore
        • 最后剩余 newVnodes -- 添加 newVnodes
        • 最后剩余 oldVnodes -- 清空 oldVnodes
      • newVnode 有 children -- 添加 newVnodes
      • oldVnode 有 children -- 清空 oldVnodes
      • oldVnode 是 Text 节点 -- 清空 Text

一层树的比较

最后

最后写几句自己的感受。

有些时候,有些事情只是想做而不行动,那就永远只停留在还没做,整的自己还不爽。其实真的做起来,又不一定那么难。所以,为什么想做而不做?要知道,知道和不知道之间存在着巨大的鸿沟。

另外,在阅读源码这件事情上,“知其然,亦知其所以然”固然是好事儿,但是,需要有更高层次的认知。不能为了读源码而读源码,也不是非得读多了源码就一定好。就像不无意义的,使用的工具有很多,如果都去读起源码,不现实、也不划算。更好的动机应该是明确自己为什么去读,是为了解某些疑惑吗?是为了学习其中的优秀架构、思想、算法实现?再或者是用了其而不懂,项目就不能前行?带着目的去读,可能更高效、更有意义。

本来想把注释后的源码直接贴出来分享来着,不过已经有了,可以

本来也想好生画下 diff算法 中的核心比较来着,不过也较早就有了,可以

最后,结合最近的工作和学习,顺便分享下体会:在既定的目标和问题面前,可以通过 拆解目标,循序渐进,逐个击破 的方式来完成。

转载于:https://juejin.im/post/5c9e3e8a6fb9a05e3527c63a

你可能感兴趣的文章
基于JavaMail的Java邮件发送:简单邮件发送
查看>>
Atitit 软件体系的进化,是否需要一个处理中心
查看>>
MVC ---- Lambda表达式
查看>>
SQL语句的一些基本使用以及一些技巧
查看>>
BZOJ 3585: mex [主席树]
查看>>
python 处理命令行参数--转载
查看>>
未来什么行业最赚钱
查看>>
ORM框架为什么不流行了
查看>>
java--- 使用interrupte中断线程的真正用途
查看>>
024 关于spark中日志分析案例
查看>>
[web] spring boot 整合MyBatis
查看>>
图片提交按钮各浏览器不兼容问题
查看>>
Win7如何改变用户文件夹位置
查看>>
Highchart
查看>>
git mergetool 解决冲突的问题
查看>>
jquery 给每个li增加事件
查看>>
Visual Studio VS如何切换代码自动换行
查看>>
近期(17年三月至四月) TODOlist
查看>>
Win10系列:C#应用控件基础11
查看>>
Swift3.0:NSURLConnection的使用
查看>>