前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

虚拟DOM原理分析 Snabbdom源码与diff算法拆解

首页2021-03-29 08:35:32Front-End
Vue虚拟DOMSnabbdomdiff算法

面试被问「虚拟 DOM 为什么快」,很多人会条件反射地答「因为操作 JS 对象比操作 DOM 快」。这个答案经不起追问。真要比单次操作,直接改一次 textContent 显然比走一整套 diff 流程省事。虚拟 DOM 真正解决的不是快慢,是在「状态变了」和「DOM 该怎么改」这两件事之间插了一层,让你只描述结果,不用手写变更过程。这篇把 Vue 2 底层用的 Snabbdom 从头拆一遍,h 函数怎么造 VNode,patch 怎么打补丁,diff 的双端比较到底比了哪四种情况,key 在其中起什么作用。读完你对「v-for 为什么别拿 index 当 key」会有一个能自己推导出来的解释。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 虚拟 DOM 是什么,用一个 JS 对象描述 DOM 到底描述了哪几个字段
  • 为什么要有虚拟 DOM,它解决的是性能问题还是别的问题
  • Snabbdom 上手,从建项目到用模块处理属性、样式、事件
  • h 函数的重载怎么实现,VNode 的六个字段各管什么
  • init 为什么写成高阶函数,patch 的完整分支走向
  • createElm 和 patchVnode 的执行顺序,钩子函数在哪几个点被触发
  • updateChildren 的双端比较,四种命中情况加两种收尾情况
  • 这套算法反过来对写业务代码有什么约束,比如 key 该怎么选

# 一、虚拟 DOM 到底是什么

虚拟 DOM 就是用普通的 JavaScript 对象来描述真实 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,aut ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown ,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup, onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend ,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl eave,onselectstart,onselectionchange,onanimationend,onanimationiteration ,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click ,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight ,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme ntTiming,previousElementSibling,nextElementSibling,children,firstElement Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture ,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames ,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute ,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_ NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION _NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace ,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo veEventListener,dispatchEvent
@前端进阶之旅: 代码已经复制到剪贴板

这一坨输出还只是 Chrome 里一个 div 的属性名列表,几百个。如果每次状态变化都要在这样一个庞然大物上做增删改查,还要考虑各家浏览器的行为差异,代码很快就会变成一团乱麻。

所以换个思路,我不描述「怎么改」,我描述「现在应该长什么样」。用一个对象就够了:

{
  sel: "div",
  data: {},
  children: undefined,
  text: "Hello Virtual DOM",
  elm: undefined,
  key: undefined
}
@前端进阶之旅: 代码已经复制到剪贴板

六个字段,一个不多。sel 是选择器,data 装属性样式事件,children 和 text 互斥(一个节点要么有子节点要么有文本),elm 指向它对应的真实 DOM,key 用来做同一层节点的身份标识。一棵这样的对象树,就是一份 DOM 的快照。

# 二、为什么要有虚拟 DOM

先说结论,虚拟 DOM 解决的核心问题是状态跟踪,性能只是它顺带换来的东西。

顺着历史看一遍就清楚了。最早我们手动操作 DOM,麻烦不说,还得处理浏览器兼容,jQuery 把这层抹平了一部分,但项目一复杂,「哪个状态变了要改哪几个节点」这件事依然全靠人脑记,DOM 操作的复杂度随着页面复杂度一起涨。

再往后各种 MVVM 框架出现,把视图和状态的同步问题接管了。模板引擎也简化了视图的书写,但模板引擎有个致命短板,它不知道这次和上次相比哪里变了,只能整个重新渲染一遍。页面一大,整块重渲的代价就上来了,输入框失焦、滚动位置丢失这些副作用还得另外补。

fe
  • 一、虚拟 DOM 到底是什么
  • 二、为什么要有虚拟 DOM
    • 有哪些现成的虚拟 DOM 库
  • 三、Snabbdom 上手
    • 建项目
    • 先跑几个例子找感觉
    • 模块,Snabbdom 保持小巧的关键
  • 四、源码主线,从 h 到 patch
    • 整条链路只有四步
    • h 函数
    • VNode
    • patch 的整体走向
    • init
    • patch
    • createElm
    • patchVnode
  • 五、updateChildren,diff 算法的核心
    • 先算一笔账,为什么只比同层
    • 双端比较,四种命中情况
    • 四种都不命中怎么办
    • 循环怎么结束
  • 六、这套算法反过来约束了什么
  • 总结
  • 参考

← Serverless 入门实战 从运行原理到四个落地案例Vue响应式原理模拟 手写一个迷你版Vue →