React와 Vue 모두 가상 DOM을 가지고 있는데 가상 DOM의 본질을 어떻게 이해하고 마스터해야 할까요? 다음 기사는 여러분에게 가상 DOM에 대한 심층적인 이해를 제공할 것입니다. 여러분에게 도움이 되기를 바랍니다.
가상 DOM의 본질을 이해하고 익히는 방법은 무엇입니까? 나는 모든 사람에게 Snabbdom 프로젝트를 배울 것을 권장합니다.
Snabbdom은 가상 DOM 구현 라이브러리입니다. 첫째, 코드가 상대적으로 작고 핵심 코드가 몇 백 줄에 불과합니다. 둘째, Vue는 이 프로젝트의 아이디어를 활용하여 가상 DOM을 구현합니다. 셋째, 이 프로젝트의 설계/구현과 확장 아이디어는 참고할만한 가치가 있습니다.
snabb /snab/, 스웨덴어는 빠르다는 뜻입니다.
편안한 앉은 자세로 적응하고 힘내서 시작해보세요~ 가상 DOM을 배우기 위해서는 먼저 DOM에 대한 기본 지식과 JS로 DOM을 직접 조작할 때의 문제점을 알아야 합니다.
DOM(문서 개체 모델)은 개체 트리 구조를 사용하여 HTML/XML 문서를 나타내는 문서 개체 모델입니다. 트리의 각 분기 끝은 노드입니다. (노드), 각 노드에는 개체가 포함됩니다. DOM API의 메소드를 사용하면 특정 방식으로 이 트리를 조작할 수 있으며, 문서의 구조, 스타일 또는 컨텐츠를 변경할 수 있습니다.
DOM 트리의 모든 노드는 먼저 노드
이고 노드
는 기본 클래스입니다. 요소
, 텍스트
및 설명
은 모두 여기에서 상속됩니다.
즉, Element
, Text
및 Comment
는 세 가지 특수 노드
입니다. 요소 노드(HTML 태그), 텍스트 노드 및 주석 노드를 나타내는 ELEMENT_NODE
, TEXT_NODE
및 COMMENT_NODE
라고 합니다. 그중 Element
에는 HTMLElement
라는 하위 클래스도 있습니다. 그러면 HTMLElement
와 Element
의 차이점은 무엇인가요? HTMLElement
는 <span></span>
, <img alt="가상 DOM을 이해하는 방법은 무엇입니까? 이 기사를 확인해보세요!" >
등과 같은 HTML의 요소를 나타냅니다. 그러나 일부 요소는 HTML 표준이 아닙니다. 예를 들어 <svg>
입니다. 다음 방법을 사용하여 이 요소가 HTMLElement
인지 확인할 수 있습니다. Node
,Node
是一个基类。Element
,Text
和 Comment
都继承于它。
换句话说,Element
,Text
和 Comment
是三种特殊的 Node
,它们分别叫做 ELEMENT_NODE
,TEXT_NODE
和 COMMENT_NODE
,代表的是元素节点(HTML 标签)、文本节点和注释节点。其中 Element
还有一个子类是 HTMLElement
,那 HTMLElement
和 Element
有什么区别呢?HTMLElement
代表 HTML 中的元素,如:<span>
、<img>
等,而有些元素并不是 HTML 标准的,比如 <svg>
。可以用下面的方法来判断这个元素是不是 HTMLElement
:
document.getElementById('myIMG') instanceof HTMLElement;
浏览器创建 DOM 是很“昂贵”的。来一个经典示例,我们可以通过 document.createElement('p')
创建一个简单的 p 元素,将属性都打印出来康康:
可以看到打印出来的属性非常多,当频繁地去更新复杂的 DOM 树时,会产生性能问题。虚拟 DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以创建一个 JS 对象比创建一个 DOM 对象的代价要小很多。
VNode 就是 Snabbdom 中描述虚拟 DOM 的一个对象结构,内容如下:
type Key = string | number | symbol; interface VNode { // CSS 选择器,比如:'p#container'。 sel: string | undefined; // 通过 modules 操作 CSS classes、attributes 等。 data: VNodeData | undefined; // 虚拟子节点数组,数组元素也可以是 string。 children: Array<VNode | string> | undefined; // 指向创建的真实 DOM 对象。 elm: Node | undefined; /** * text 属性有两种情况: * 1. 没有设置 sel 选择器,说明这个节点本身是一个文本节点。 * 2. 设置了 sel,说明这个节点的内容是一个文本节点。 */ text: string | undefined; // 用于给已存在的 DOM 提供标识,在同级元素之间必须唯一,有效避免不必要地重建操作。 key: Key | undefined; } // vnode.data 上的一些设置,class 或者生命周期函数钩子等等。 interface VNodeData { props?: Props; attrs?: Attrs; class?: Classes; style?: VNodeStyle; dataset?: Dataset; on?: On; attachData?: AttachData; hook?: Hooks; key?: Key; ns?: string; // for SVGs fn?: () => VNode; // for thunks args?: any[]; // for thunks is?: string; // for custom elements v1 [key: string]: any; // for any other 3rd party module }
例如这样定义一个 vnode 的对象:
const vnode = h( 'p#container', { class: { active: true } }, [ h('span', { style: { fontWeight: 'bold' } }, 'This is bold'), ' and this is just normal text' ]);
我们通过 h(sel, b, c)
函数来创建 vnode 对象。h()
代码实现中主要是判断了 b 和 c 参数是否存在,并处理成 data 和 children,children 最终会是数组的形式。最后通过 vnode()
函数返回上面定义的 VNode
类型格式。
先来一张运行流程的简单示例图,先有个大概的流程概念:
diff 处理是用来计算新老节点之间差异的处理过程。
再来看一段 Snabbdom 运行的示例代码:
import { init, classModule, propsModule, styleModule, eventListenersModule, h, } from 'snabbdom'; const patch = init([ // 通过传入模块初始化 patch 函数 classModule, // 开启 classes 功能 propsModule, // 支持传入 props styleModule, // 支持内联样式同时支持动画 eventListenersModule, // 添加事件监听 ]); // <p id="container"></p> const container = document.getElementById('container'); const vnode = h( 'p#container.two.classes', { on: { click: someFn } }, [ h('span', { style: { fontWeight: 'bold' } }, 'This is bold'), ' and this is just normal text', h('a', { props: { href: '/foo' } }, "I'll take you places!"), ] ); // 传入一个空的元素节点。 patch(container, vnode); const newVnode = h( 'p#container.two.classes', { on: { click: anotherEventHandler } }, [ h( 'span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'This is now italic type' ), ' and this is still just normal text', h('a', { props: { href: ''/bar' } }, "I'll take you places!"), ] ); // 再次调用 patch(),将旧节点更新为新节点。 patch(vnode, newVnode);
从流程示意图和示例代码可以看出,Snabbdom 的运行流程描述如下:
首先调用 init()
进行初始化,初始化时需要配置需要使用的模块。比如 classModule
模块用来使用对象的形式来配置元素的 class 属性;eventListenersModule
模块用来配置事件监听器等等。init()
调用后会返回 patch()
函数。
通过 h()
函数创建初始化 vnode 对象,调用 patch()
函数去更新,最后通过 createElm()
创建真正的 DOM 对象。
当需要更新时,创建一个新的 vnode 对象,调用 patch()
函数去更新,经过 patchVnode()
和 updateChildren()
h('p.row', { key: 'myRow', hook: { insert: (vnode) => { console.log(vnode.elm.offsetHeight); }, }, });🎜가상 DOM이 필요한 이유는 무엇입니까? 🎜🎜🎜브라우저가 DOM을 생성하는 것은 매우 "비용이 많이 듭니다". 전형적인 예를 들어보겠습니다.
document.createElement('p')
를 통해 간단한 p 요소를 생성하고 모든 속성을 인쇄할 수 있습니다: 🎜🎜import { VNode, VNodeData } from "../vnode"; import { Module } from "./module"; export type Classes = Record<string, boolean>; function updateClass(oldVnode: VNode, vnode: VNode): void { // 这里是更新 class 属性的细节,先不管。 // ... } export const classModule: Module = { create: updateClass, update: updateClass };🎜예를 들어 vnode 객체를 다음과 같이 정의합니다. 🎜
import { PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook, } from "../hooks"; export type Module = Partial<{ pre: PreHook; create: CreateHook; update: UpdateHook; destroy: DestroyHook; remove: RemoveHook; post: PostHook; }>;🎜우리는
h(sel, b, c) 함수를 사용하여 vnode 객체를 생성합니다. <code>h()
코드 구현은 주로 b 및 c 매개변수가 존재하는지 확인하고 이를 데이터로 처리하며 하위 항목은 결국 배열 형태가 됩니다. 마지막으로 위에서 정의한 VNode
유형 형식이 vnode()
함수를 통해 반환됩니다. 🎜🎜🎜Snabbdom의 실행 프로세스🎜🎜🎜실행 프로세스에 대한 간단한 예제 다이어그램으로 시작하여 프로세스의 일반적인 개념을 살펴보겠습니다.🎜🎜🎜🎜diff 처리를 사용하여 이전 버전과 새 버전의 차이를 계산합니다. 노드. 🎜🎜Snabbdom 연산의 샘플 코드를 살펴보겠습니다: 🎜// 模块中可能定义的钩子有哪些。 const hooks: Array<keyof Module> = [ "create", "update", "remove", "destroy", "pre", "post", ]; export function init( modules: Array<Partial<Module>>, domApi?: DOMAPI, options?: Options ) { // 模块中定义的钩子函数最后会存在这里。 const cbs: ModuleHooks = { create: [], update: [], remove: [], destroy: [], pre: [], post: [], }; // ... // 遍历模块中定义的钩子,并存起来。 for (const hook of hooks) { for (const module of modules) { const currentHook = module[hook]; if (currentHook !== undefined) { (cbs[hook] as any[]).push(currentHook); } } } // ... }🎜프로세스 다이어그램과 샘플 코드에서 볼 수 있듯이 Snabbdom의 연산 프로세스는 다음과 같습니다. 🎜
init()
를 호출합니다. 초기화하는 동안 사용할 모듈을 구성해야 합니다. 예를 들어, classModule
모듈은 객체 형태로 요소의 클래스 속성을 구성하는 데 사용됩니다. eventListenersModule
모듈은 이벤트 리스너 등을 구성하는 데 사용됩니다. init()
는 호출된 후 patch()
함수를 반환합니다. 🎜h()
함수를 통해 초기화된 vnode 객체를 생성하고 patch()
함수를 호출하여 업데이트한 다음 마지막으로 createElm() 실제 DOM 객체를 생성합니다. 🎜patchVnode()
후에 patch()
함수를 호출하여 업데이트하세요. updateChildren()
이 노드와 해당 하위 노드의 차등 업데이트를 완료합니다. 🎜Snabbdom 是通过模块这种设计来扩展相关属性的更新而不是全部写到核心代码中。那这是如何设计与实现的?接下来就先来康康这个设计的核心内容,Hooks——生命周期函数。
Snabbdom 提供了一系列丰富的生命周期函数也就是钩子函数,这些生命周期函数适用在模块中或者可以直接定义在 vnode 上。比如我们可以在 vnode 上这样定义钩子的执行:
h('p.row', { key: 'myRow', hook: { insert: (vnode) => { console.log(vnode.elm.offsetHeight); }, }, });
全部的生命周期函数声明如下:
名称 | 触发节点 | 回调参数 |
---|---|---|
pre |
patch 开始执行 | none |
init |
vnode 被添加 | vnode |
create |
一个基于 vnode 的 DOM 元素被创建 | emptyVnode, vnode |
insert |
元素被插入到 DOM | vnode |
prepatch |
元素即将 patch | oldVnode, vnode |
update |
元素已更新 | oldVnode, vnode |
postpatch |
元素已被 patch | oldVnode, vnode |
destroy |
元素被直接或间接得移除 | vnode |
remove |
元素已从 DOM 中移除 | vnode, removeCallback |
post |
已完成 patch 过程 | none |
其中适用于模块的是:pre
, create
,update
, destroy
, remove
, post
。适用于 vnode 声明的是:init
, create
, insert
, prepatch
, update
,postpatch
, destroy
, remove
。
我们来康康是如何实现的,比如我们以 classModule
模块为例,康康它的声明:
import { VNode, VNodeData } from "../vnode"; import { Module } from "./module"; export type Classes = Record<string, boolean>; function updateClass(oldVnode: VNode, vnode: VNode): void { // 这里是更新 class 属性的细节,先不管。 // ... } export const classModule: Module = { create: updateClass, update: updateClass };
可以看到最后导出的模块定义是一个对象,对象的 key 就是钩子函数的名称,模块对象 Module
的定义如下:
import { PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook, } from "../hooks"; export type Module = Partial<{ pre: PreHook; create: CreateHook; update: UpdateHook; destroy: DestroyHook; remove: RemoveHook; post: PostHook; }>;
TS 中 Partial
表示对象中每个 key 的属性都是可以为空的,也就是说模块定义中你关心哪个钩子,就定义哪个钩子就好了。钩子的定义有了,在流程中是怎么执行的呢?接着我们来看 init()
函数:
// 模块中可能定义的钩子有哪些。 const hooks: Array<keyof Module> = [ "create", "update", "remove", "destroy", "pre", "post", ]; export function init( modules: Array<Partial<Module>>, domApi?: DOMAPI, options?: Options ) { // 模块中定义的钩子函数最后会存在这里。 const cbs: ModuleHooks = { create: [], update: [], remove: [], destroy: [], pre: [], post: [], }; // ... // 遍历模块中定义的钩子,并存起来。 for (const hook of hooks) { for (const module of modules) { const currentHook = module[hook]; if (currentHook !== undefined) { (cbs[hook] as any[]).push(currentHook); } } } // ... }
可以看到 init()
在执行时先遍历各个模块,然后把钩子函数存到了 cbs
这个对象中。执行的时候可以康康 patch()
函数里面:
export function init( modules: Array<Partial<Module>>, domApi?: DOMAPI, options?: Options ) { // ... return function patch( oldVnode: VNode | Element | DocumentFragment, vnode: VNode ): VNode { // ... // patch 开始了,执行 pre 钩子。 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // ... } }
这里以 pre
这个钩子举例,pre
钩子的执行时机是在 patch 开始执行时。可以看到 patch()
函数在执行的开始处去循环调用了 cbs
中存储的 pre
相关钩子。其他生命周期函数的调用也跟这个类似,大家可以在源码中其他地方看到对应生命周期函数调用的地方。
这里的设计思路是观察者模式。Snabbdom 把非核心功能分布在模块中来实现,结合生命周期的定义,模块可以定义它自己感兴趣的钩子,然后 init()
执行时处理成 cbs
对象就是注册这些钩子;当执行时间到来时,调用这些钩子来通知模块处理。这样就把核心代码和模块代码分离了出来,从这里我们可以看出观察者模式是一种代码解耦的常用模式。
接下来我们来康康核心函数 patch()
,这个函数是在 init()
调用后返回的,作用是执行 VNode 的挂载和更新,签名如下:
function patch(oldVnode: VNode | Element | DocumentFragment, vnode: VNode): VNode { // 为简单起见先不关注 DocumentFragment。 // ... }
oldVnode
参数是旧的 VNode 或 DOM 元素或文档片段,vnode
参数是更新后的对象。这里我直接贴出整理的流程描述:
调用模块上注册的 pre
钩子。
如果 oldVnode
是 Element
,则将其转换为空的 vnode
对象,属性里面记录了 elm
。
这里判断是不是 Element
是判断 (oldVnode as any).nodeType === 1
是完成的,nodeType === 1
表明是一个 ELEMENT_NODE,定义在 这里。
然后判断 oldVnode
和 vnode
是不是相同的,这里会调用 sameVnode()
来判断:
function sameVnode(vnode1: VNode, vnode2: VNode): boolean { // 同样的 key。 const isSameKey = vnode1.key === vnode2.key; // Web component,自定义元素标签名,看这里: // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElement const isSameIs = vnode1.data?.is === vnode2.data?.is; // 同样的选择器。 const isSameSel = vnode1.sel === vnode2.sel; // 三者都相同即是相同的。 return isSameSel && isSameKey && isSameIs; }
patchVnode()
做 diff 更新。createElm()
创建新的 DOM 节点;创建完毕后插入 DOM 节点并删除旧的 DOM 节点。调用上述操作中涉及的 vnode 对象中注册的 insert
钩子队列, patchVnode()
createElm()
都可能会有新节点插入 。至于为什么这样做,在 createElm()
中会说到。
最后调用模块上注册的 post
钩子。
流程基本就是相同的 vnode 就做 diff,不同的就创建新的删除旧的。接下来先看下 createElm()
是如何创建 DOM 节点的。
createElm()
是根据 vnode 的配置来创建 DOM 节点。流程如下:
调用 vnode 对象上可能存在的 init
钩子。
然后分一下几种情况来处理:
如果 vnode.sel === '!'
,这是 Snabbdom 用来删除原节点的方法,这样会新插入一个注释节点。因为在 createElm()
后会删除老节点,所以这样设置就可以达到卸载的目的。
如果 vnode.sel
选择器定义是存在的:
解析选择器,得到 id
、tag
和 class
。
调用 document.createElement()
或 document.createElementNS
创建 DOM 节点,并记录到 vnode.elm
中,并根据上一步的结果来设置 id
、tag
和 class
。
调用模块上的 create
钩子。
children
하위 노드 배열 처리: children
子节点数组:
如果 children
是数组,则递归调用 createElm()
创建子节点后,调用 appendChild
挂载到 vnode.elm
下。
如果 children
不是数组但 vnode.text
存在,说明这个元素的内容是个文本,这个时候调用 createTextNode
创建文本节点并挂载到 vnode.elm
下。
调用 vnode 上的 create
钩子。并将 vnode 上的 insert
钩子加入到 insert
钩子队列。
剩下的情况就是 vnode.sel
不存在,说明节点本身是文本,那就调用 createTextNode
创建文本节点并记录到 vnode.elm
。
最后返回 vnode.elm
。
整个过程可以看出 createElm()
是根据 sel
选择器的不同设置来选择如何创建 DOM 节点。这里有个细节是补一下: patch()
中提到的 insert
钩子队列。需要这个 insert
钩子队列的原因是需要等到 DOM 真正被插入后才执行,而且也要等到所有子孙节点都插入完成,这样我们可以在 insert
中去计算元素的大小位置信息才是准确的。结合上面创建子节点的过程,createElm()
创建子节点是递归调用,所以队列会先记录子节点,再记录自身。这样在 patch()
的结尾执行这个队列时就可以保证这个顺序。
接下来我们来看 Snabbdom 如何用 patchVnode()
来做 diff 的,这是虚拟 DOM 的核心。patchVnode()
的处理流程如下:
首先执行 vnode 上 prepatch
钩子。
如果 oldVnode 和 vnode 是同一个对象引用,则不处理直接返回。
调用模块和 vnode 上的 update
钩子。
如果没有定义 vnode.text
,则处理 children
的几种情况:
如果 oldVnode.children
和 vnode.children
均存在并且不相同。则调用 updateChildren
去更新。
vnode.children
存在而 oldVnode.children
不存在。如果 oldVnode.text
存在则先清空,然后调用 addVnodes
去添加新的 vnode.children
。
vnode.children
不存在而 oldVnode.children
存在。调用 removeVnodes
移除 oldVnode.children
。
如果 oldVnode.children
和 vnode.children
均不存在。如果 oldVnode.text
存在则清空。
如果有定义 vnode.text
并且与 oldVnode.text
不同。如果 oldVnode.children
存在则调用 removeVnodes
清除。然后通过 textContent
来设置文本内容。
最后执行 vnode 上的 postpatch
钩子。
从过程可以看出,diff 中对于自身节点的相关属性的改变比如 class
、style
之类的是依靠模块去更新的,这里不过多展开了大家有需要可以去看下模块相关的代码。diff 的主要核心处理是集中在 children
上,接下来康康 diff 处理 children
的几个相关函数。
这个很简单,先调用 createElm()
创建,然后插入到对应的 parent 中。
移除的时候会先调用 destory
和 remove
钩子,这里重点讲讲这两个钩子的调用逻辑和区别。
destory
,首先调用这个钩子。逻辑是先调用 vnode 对象上的这个钩子,再调用模块上的。然后对 vnode.children
也按照这个顺序递归调用这个钩子。remove
,这个 hook 只有在当前元素从它的父级中删除才会触发,被移除的元素中的子元素则不会触发,并且模块和 vnode 对象上的这个钩子都会调用,顺序是先调用模块上的再调用 vnode 上的。而且比较特殊的是等待所有的 remove
都会调用后,元素才会真正被移除,这样做可以实现一些延迟删除的需求。以上可以看出这两个钩子调用逻辑不同的地方,特别是 remove
children
이 배열인 경우 재귀적으로 호출createElm()appendChild
를 호출하여 vnode.elm
아래에 마운트합니다. 🎜🎜🎜🎜 children
이 배열은 아니지만 vnode.text
가 존재한다면 이 요소의 내용이 텍스트라는 뜻입니다. 이때 createTextNode를 호출하세요.
를 사용하여 텍스트 노드를 만들고 vnode.elm
아래에 마운트합니다. 🎜🎜create
후크를 호출하세요. 그리고 vnode의 insert
후크를 insert
후크 대기열에 추가하세요. 🎜🎜🎜🎜🎜남은 상황은 vnode.sel
이 존재하지 않아 노드 자체가 텍스트임을 나타내는 것입니다. 그런 다음 createTextNode
를 호출하여 텍스트를 생성합니다. 노드를 vnode.elm
에 기록합니다. 🎜🎜🎜🎜🎜마지막으로 vnode.elm
을 반환합니다. 🎜🎜🎜createElm()
이 sel
선택기의 다양한 설정에 따라 DOM 노드를 생성하는 방법을 선택하는 것을 전체 프로세스에서 볼 수 있습니다. 여기에 추가할 세부정보가 있습니다. patch()
에 언급된 insert
후크 대기열입니다. 이 insert
후크 큐가 필요한 이유는 DOM을 실행하기 전에 실제로 DOM이 삽입될 때까지 기다려야 하고 모든 하위 노드가 삽입될 때까지 기다려야 하기 때문입니다. insert
에서 요소의 크기와 위치 정보를 정확하게 계산합니다. 위의 하위 노드 생성 프로세스와 결합된 createElm()
은 하위 노드를 생성하는 재귀 호출이므로 대기열은 하위 노드를 먼저 기록한 다음 자체적으로 기록합니다. 이렇게 하면 patch()
끝에서 대기열을 실행할 때 순서가 보장됩니다. 🎜patchVnode()
를 사용하여 가상 DOM의 핵심인 diff를 수행하는 방법을 살펴보겠습니다. patchVnode()
의 처리 흐름은 다음과 같습니다. 🎜prepatch
후크를 실행합니다. . 🎜🎜🎜🎜oldVnode와 vnode가 동일한 객체 참조인 경우 처리 없이 직접 반환됩니다. 🎜🎜🎜🎜모듈과 vnode에서 update
후크를 호출하세요. 🎜🎜🎜🎜 vnode.text
가 정의되지 않은 경우 하위 항목
의 여러 사례를 처리합니다. 🎜oldVnode.children
과 vnode.children
이 모두 존재하고 서로 다른 경우. 그런 다음 updateChildren
을 호출하여 업데이트하세요. 🎜🎜🎜🎜vnode.children
은 존재하고 oldVnode.children
은 존재하지 않습니다. oldVnode.text
가 존재하는 경우 먼저 이를 지운 다음 addVnodes
를 호출하여 새 vnode.children
을 추가하세요. 🎜🎜🎜🎜vnode.children
은 존재하지 않고 oldVnode.children
은 존재합니다. oldVnode.children
을 제거하려면 removeVnodes
를 호출하세요. 🎜🎜🎜🎜oldVnode.children
이나 vnode.children
이 모두 존재하지 않는 경우. oldVnode.text
가 존재하는 경우 비어 있습니다. 🎜🎜vnode.text
가 정의되어 있고 oldVnode.text
와 다른 경우. oldVnode.children
이 존재하는지 지우려면 removeVnodes
를 호출하세요. 그런 다음 textContent
를 통해 텍스트 콘텐츠를 설정하세요. 🎜🎜🎜🎜마지막으로 vnode에서 postpatch
후크를 실행합니다. 🎜🎜class
, style
등과 같은 diff에서 자체 노드의 관련 속성이 변경되는 과정을 볼 수 있습니다. , 모듈별로 업데이트됩니다. 필요한 경우 모듈 관련 코드를 살펴볼 수 있습니다. diff의 주요 핵심 처리는 children
에 중점을 둡니다. 다음으로 Kangkang diff는 children
의 여러 관련 기능을 처리합니다. 🎜createElm()
을 호출하여 생성한 다음 해당 상위 항목에 삽입하세요. 🎜destory
및 remove
후크가 먼저 호출됩니다. this 두 후크 간의 호출 논리 및 차이점입니다. 🎜destroy
, 먼저 이 후크를 호출하세요. 논리는 먼저 vnode 객체에서 후크를 호출한 다음 모듈에서 후크를 호출하는 것입니다. 그런 다음 이 후크는 vnode.children
에서 이 순서대로 재귀적으로 호출됩니다. 🎜🎜remove
, 이 후크는 현재 요소가 상위 요소에서 제거될 때만 트리거되며 제거된 요소의 하위 요소는 트리거되지 않으며 모듈 및 vnode 개체에 대한 이 후크는 모두 후크가 호출되며 순서는 모듈에 있는 후크를 먼저 호출한 다음 vnode에 있는 후크를 호출하는 것입니다. 더 특별한 점은 모든 remove
가 호출될 때까지 요소가 실제로 제거되지 않는다는 것입니다. 이로 인해 일부 지연된 삭제 요구 사항이 충족될 수 있습니다. 🎜remove
는 상위 요소와 직접 분리된 요소에서만 호출됩니다. 🎜updateChildren()
是用来处理子节点 diff 的,也是 Snabbdom 中比较复杂的一个函数。总的思想是对 oldCh
和 newCh
各设置头、尾一共四个指针,这四个指针分别是 oldStartIdx
、oldEndIdx
、newStartIdx
和 newEndIdx
。然后在 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
循环中对两个数组进行对比,找到相同的部分进行复用更新,并且每次比较处理最多移动一对指针。详细的遍历过程按以下顺序处理:
如果这四个指针有任何一个指向的 vnode == null,则这个指针往中间移动,比如:start++ 或 end--,null 的产生在后面情况有说明。
如果新旧开始节点相同,也就是 sameVnode(oldStartVnode, newStartVnode)
返回 true,则用 patchVnode()
执行 diff,并且两个开始节点都向中间前进一步。
如果新旧结束节点相同,也采用 patchVnode()
处理,两个结束节点向中间后退一步。
如果旧开始节点与新结束节点相同,先用 patchVnode()
处理更新。然后需要移动 oldStart 对应的 DOM 节点,移动的策略是移动到 oldEndVnode
对应 DOM 节点的下一个兄弟节点之前。为什么是这样移动呢?首先,oldStart 与 newEnd 相同,说明在当前循环处理中,老数组的开始节点是往右移动了;因为每次的处理都是首尾指针往中间移动,我们是把老数组更新成新的,这个时候 oldEnd 可能还没处理,但这个时候 oldStart 已确定在新数组的当前处理中是最后一个了,所以移动到 oldEnd 的下一个兄弟节点之前是合理的。移动完毕后,oldStart++,newEnd--,分别向各自的数组中间移动一步。
如果旧结束节点与新开始节点相同,也是先用 patchVnode()
处理更新,然后把 oldEnd 对应的 DOM 节点移动 oldStartVnode
对应的 DOM 节点之前,移动理由同上一步一样。移动完毕后,oldEnd--,newStart++。
如果以上情况都不是,则通过 newStartVnode 的 key 去找在 oldChildren
的下标 idx,根据下标是否存在有两种不同的处理逻辑:
如果下标不存在,说明 newStartVnode 是新创建的。通过 createElm()
创建新的 DOM,并插入到 oldStartVnode
对应的 DOM 之前。
如果下标存在,也要分两种情况处理:
如果两个 vnode 的 sel 不同,也还是当做新创建的,通过 createElm()
创建新的 DOM,并插入到 oldStartVnode
对应的 DOM 之前。
如果 sel 是相同的,则通过 patchVnode()
处理更新,并把 oldChildren
对应下标的 vnode 设置为 undefined,这也是前面双指针遍历中为什么会出现 == null 的原因。然后把更新完毕后的节点插入到 oldStartVnode
对应的 DOM 之前。
以上操作完后,newStart++。
遍历结束后,还有两种情况要处理。一种是 oldCh
已经全部处理完成,而 newCh
中还有新的节点,需要对 newCh
剩下的每个都创建新的 DOM;另一种是 newCh
全部处理完成,而 oldCh
中还有旧的节点,需要将多余的节点移除。这两种情况的处理在 如下:
function updateChildren( parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue ) { // 双指针遍历过程。 // ... // newCh 中还有新的节点需要创建。 if (newStartIdx <= newEndIdx) { // 需要插入到最后一个处理好的 newEndIdx 之前。 before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes( parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); } // oldCh 中还有旧的节点要移除。 if (oldStartIdx <= oldEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
我们用一个实际例子来看一下 updateChildren()
的处理过程:
初始状态如下,旧子节点数组为 [A, B, C],新节点数组为 [B, A, C, D]:
第一轮比较,开始和结束节点都不一样,于是看 newStartVnode 在旧节点中是否存在,找到了在 oldCh[1] 这个位置,那么先执行 patchVnode()
进行更新,然后把 oldCh[1] = undefined,并把 DOM 插入到 oldStartVnode
之前,newStartIdx
向后移动一步,处理完后状态如下:
두 번째 비교인 oldStartVnode
와 newStartVnode
는 동일합니다. patchVnode()
를 실행하여 업데이트하세요. oldStartIdx
및 newStartIdx가 가운데로 이동하며 처리 후 상태는 다음과 같습니다. oldStartVnode
和 newStartVnode
相同,执行 patchVnode()
更新,oldStartIdx
和 newStartIdx
向中间移动,处理完后状态如下:
第三轮比较,oldStartVnode == null
,oldStartIdx
向中间移动,状态更新如下:
第四轮比较,oldStartVnode
和 newStartVnode
相同,执行 patchVnode()
更新,oldStartIdx
和 newStartIdx
向中间移动,处理完后状态如下:
此时 oldStartIdx
大于 oldEndIdx
,循环结束。此时 newCh
中还有没处理完的新节点,需要调用 addVnodes()
oldStartVnode == null code>, <code>oldStartIdx 가운데로 이동하면 상태가 다음과 같이 업데이트됩니다: <strong></strong>
4차 비교, oldStartVnode
및 newStartVnode가 동일하고 patchVnode()
업데이트를 실행하면 oldStartIdx
와 newStartIdx
가 가운데로 이동하고 그 이후의 상태 처리는 다음과 같습니다:
oldEndIdx
보다 크며 루프가 종료됩니다. 현재 newCh
에는 아직 처리되지 않은 새 노드가 있는데, addVnodes()
를 호출하여 삽입해야 합니다. 최종 상태는 다음과 같습니다. 🎜🎜🎜🎜🎜 🎜🎜🎜Summary🎜🎜🎜 이제 가상 DOM의 핵심 내용이 정리되었습니다. 시간이 되시면 Kangkang 소스 코드의 세부 사항을 살펴보실 수 있습니다. 아이디어를 자세히 살펴보세요. 🎜🎜더 많은 프로그래밍 관련 지식을 보려면 🎜프로그래밍 비디오🎜를 방문하세요! ! 🎜위 내용은 가상 DOM을 이해하는 방법은 무엇입니까? 이 기사를 확인해보세요!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!