前言
作为一名使用前端 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 算法,只需要关注两个核心方法即可:
- 用 object 来表示 Vnode 的 h(sel: any, b?: any, c?: any): VNode
- 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
- 都有 children -- updateChildren
- newVnode 是 Text 节点
一层树的比较
最后
最后写几句自己的感受。
有些时候,有些事情只是想做而不行动,那就永远只停留在还没做,整的自己还不爽。其实真的做起来,又不一定那么难。所以,为什么想做而不做?要知道,知道和不知道之间存在着巨大的鸿沟。
另外,在阅读源码这件事情上,“知其然,亦知其所以然”固然是好事儿,但是,需要有更高层次的认知。不能为了读源码而读源码,也不是非得读多了源码就一定好。就像不无意义的,使用的工具有很多,如果都去读起源码,不现实、也不划算。更好的动机应该是明确自己为什么去读,是为了解某些疑惑吗?是为了学习其中的优秀架构、思想、算法实现?再或者是用了其而不懂,项目就不能前行?带着目的去读,可能更高效、更有意义。
本来想把注释后的源码直接贴出来分享来着,不过已经有了,可以
本来也想好生画下 diff算法 中的核心比较来着,不过也较早就有了,可以最后,结合最近的工作和学习,顺便分享下体会:在既定的目标和问题面前,可以通过 拆解目标,循序渐进,逐个击破 的方式来完成。