Vue의 가상 DOM에 대한 심층 분석
Virtual DOM 기술을 사용하면 페이지 렌더링의 효율성이 향상되어 노드 작업이 줄어들고 성능이 향상됩니다. 이 글에서는 vue의 Virtual DOM
기술 원리와 Vue
프레임워크의 구체적인 구현에 대한 심층 분석을 제공합니다. (학습 동영상 공유: vue 동영상 튜토리얼Virtual DOM
的技术原理和 Vue
框架的具体实现。(学习视频分享:vue视频教程)
一、真实DOM
和其解析流程
本节我们主要介绍真实 DOM
的解析过程,通过介绍其解析过程以及存在的问题,从而引出为什么需要虚拟DOM
。一图胜千言,如下图为 webkit
渲染引擎工作流程图
所有的浏览器渲染引擎工作流程大致分为5步:创建 DOM
树 —> 创建 Style Rules
-> 构建 Render
树 —> 布局 Layout
-—> 绘制 Painting
。
- 第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
- 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
- 第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
- 第四步,确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
- 第五步,绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。
注意点:
1、DOM
树的构建是文档加载完成开始的? 构建 DOM
树是一个渐进过程,为达到更好的用户体验,渲染引擎会尽快将内容显示在屏幕上,它不必等到整个 HTML
文档解析完成之后才开始构建 render
树和布局。
2、Render
树是 DOM
树和 CSS
样式表构建完毕后才开始构建的? 这三个过程在实际进行的时候并不是完全独立的,而是会有交叉,会一边加载,一边解析,以及一边渲染。
3、CSS
的解析注意点? CSS
的解析是从右往左逆向解析的,嵌套标签越多,解析越慢。
4、JS
操作真实 DOM
的代价? 用我们传统的开发模式,原生 JS
或 JQ
操作 DOM
时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。在一次操作中,我需要更新 10 个 DOM
节点,浏览器收到第一个 DOM
请求后并不知道还有 9 次更新操作,因此会马上执行流程,最终执行10 次。例如,第一次计算完,紧接着下一个 DOM
更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算 DOM
节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作 DOM
的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验
二、Virtual-DOM
基础
2.1、虚拟 DOM
的好处
虚拟 DOM
就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM
的动作,虚拟 DOM
不会立即操作 DOM
,而是将这 10 次更新的 diff
内容保存到本地一个 JS
对象中,最终将这个 JS
对象一次性 attch
到 DOM
树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS
对象模拟 DOM
节点的好处是,页面的更新可以先全部反映在 JS
对象(虚拟 DOM
)上,操作内存中的 JS
对象的速度显然要更快,等更新完成后,再将最终的 JS
对象映射成真实的 DOM
)
1. 실제 DOM
및 해당 구문 분석 프로세스
🎜 이 섹션에서는 주로 실제 를 소개합니다. >DOM
의 구문 분석 프로세스는 구문 분석 프로세스와 기존 문제를 소개함으로써 가상 DOM
이 필요한 이유를 알려줍니다. 천 마디 말보다 한 장의 사진이 좋습니다. 아래 그림은 webkit
렌더링 엔진🎜🎜🎜 모든 브라우저 렌더링 엔진 워크플로는 대략 5단계로 나뉩니다.
DOM
트리 —> 스타일 규칙
생성 -> 렌더링
트리 빌드 —> 그림
을 그립니다. 🎜
- 첫 번째 단계, DOM 트리 구축: HTML 파서를 사용하여 HTML 요소를 분석하고 DOM 트리 구축
- 두 번째 단계, 스타일 시트 생성: CSS 파서 사용, CSS 분석 페이지의 스타일 시트를 생성하기 위한 요소의 파일 및 인라인 스타일
- 세 번째 단계는 렌더 트리를 구축하는 것입니다. DOM 트리를 스타일 시트와 연결하여 렌더 트리(첨부 파일)를 구축합니다. 각 DOM 노드에는 스타일 정보를 받아들이고 렌더 객체(렌더러라고도 함)를 반환하는 연결 메소드가 있습니다. 이러한 렌더 객체는 결국 렌더 트리로 구성됩니다.
- 네 번째 단계는 노드 좌표: 렌더 트리 구조에 따라 렌더 트리의 각 노드에 대한 디스플레이에 나타나는 정확한 좌표를 결정합니다.
- 다섯 번째 단계는 페이지를 그리는 것입니다. 렌더 트리에 따라 좌표를 표시합니다. 트리와 노드를 호출한 다음 각각을 호출합니다. 노드의 페인트 메소드가 이를 그립니다.
DOM
트리 구성이 시작됩니까? DOM
트리 구축은 더 나은 사용자 경험을 달성하기 위해 점진적인 프로세스로, 렌더링 엔진은 가능한 한 빨리 콘텐츠를 화면에 표시합니다. 전체 HTML
렌더링
트리 및 레이아웃 구성은 문서 구문 분석이 완료된 후에만 시작됩니다. 🎜🎜2. DOM
트리와 CSS
스타일 시트가 빌드된 후에 렌더링
트리가 빌드됩니다. 이 세 가지 프로세스는 실제 구현에서 완전히 독립적이지는 않지만 동시에 중첩, 로드, 구문 분석 및 렌더링됩니다. 🎜🎜3. CSS
를 구문 분석할 때 주의해야 할 점은 무엇인가요? CSS
는 오른쪽에서 왼쪽으로 역으로 구문 분석됩니다. 중첩된 태그가 많을수록 구문 분석 속도가 느려집니다. 🎜🎜4. JS
로 실제 DOM
을 운영하는 데 드는 비용은 얼마인가요? 기존 개발 모델인 기본 JS
또는 JQ
를 사용하여 DOM
을 작동하는 경우 브라우저는 DOM 트리 구축부터 시작합니다. 스크래치를 사용하여 프로세스를 한 번 실행합니다. 한 작업에서 10개의 DOM
노드를 업데이트해야 합니다. 브라우저가 첫 번째 DOM
요청을 받은 후에는 9개의 업데이트 작업이 더 있다는 것을 알지 못합니다. 즉시 프로세스를 실행하고 최종적으로 10번 실행합니다. 예를 들어 첫 번째 계산 후 다음 DOM
업데이트 요청 직후 이 노드의 좌표 값이 변경되므로 이전 계산은 쓸모가 없습니다. DOM
노드 좌표 값 등을 계산하는 것은 성능 낭비입니다. 컴퓨터 하드웨어가 반복적으로 업데이트되었음에도 불구하고 DOM
운영 비용은 여전히 높으며 빈번한 작업으로 인해 페이지가 정지되어 사용자 경험에 영향을 미치게 됩니다🎜2. 가상 DOM
기본
2.1. 가상 DOM
이점
🎜 가상 DOM
은 브라우저 성능 문제를 해결하도록 설계되었습니다. 이전과 마찬가지로 한 번의 작업으로 DOM
을 업데이트하는 작업이 10개 있으면 가상 DOM
은 DOM
을 즉시 작동시키지 않고 10개를 업데이트합니다. times 업데이트된 diff
콘텐츠는 로컬 JS
개체에 저장되고 마지막으로 이 JS
개체는 attch
에 저장됩니다. code>DOM 트리를 확인한 다음 불필요한 계산을 피하기 위해 후속 작업을 수행합니다. 따라서 JS
개체를 사용하여 DOM
노드를 시뮬레이션하면 모든 페이지 업데이트가 JS
개체(가상 DOM ), 메모리에서 <code>JS
객체를 작동하는 속도가 확실히 더 빨라졌습니다. 업데이트가 완료된 후 최종 JS
객체가 DOM 객체에 매핑됩니다. 실제 DOM
은 브라우저에 맡겨져 그려집니다. 🎜2.2. 알고리즘 구현
2.2.1. JS
객체를 사용하여 DOM
트리JS
对象模拟 DOM
树
(1)如何用 JS
对象模拟 DOM
树
例如一个真实的 DOM
节点如下:
<div> <p>Virtual DOM</p> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> <div>Hello World</div> </div>
我们用 JavaScript
对象来表示 DOM
节点,使用对象的属性记录节点的类型、属性、子节点等。
element.js
中表示节点对象代码如下:
/** * Element virdual-dom 对象定义 * @param {String} tagName - dom 元素名称 * @param {Object} props - dom 属性 * @param {Array<element>} - 子节点 */ function Element(tagName, props, children) { this.tagName = tagName this.props = props this.children = children // dom 元素的 key 值,用作唯一标识符 if(props.key){ this.key = props.key } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count } function createElement(tagName, props, children){ return new Element(tagName, props, children); } module.exports = createElement;</element>
根据 element
对象的设定,则上面的 DOM
结构就可以简单表示为:
var el = require("./element.js"); var ul = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']), el('li', { class: 'item' }, ['Item 2']), el('li', { class: 'item' }, ['Item 3']) ]), el('div',{},['Hello World']) ])
现在 ul
就是我们用 JavaScript
对象表示的 DOM
结构,我们输出查看 ul
对应的数据结构如下:
(2)渲染用 JS
表示的 DOM
对象
但是页面上并没有这个结构,下一步我们介绍如何将 ul
渲染成页面上真实的 DOM
结构,相关渲染函数如下:
/** * render 将virdual-dom 对象渲染为实际 DOM 元素 */ Element.prototype.render = function () { var el = document.createElement(this.tagName) var props = this.props // 设置节点的DOM属性 for (var propName in props) { var propValue = props[propName] el.setAttribute(propName, propValue) } var children = this.children || [] children.forEach(function (child) { var childEl = (child instanceof Element) ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点 : document.createTextNode(child) // 如果字符串,只构建文本节点 el.appendChild(childEl) }) return el }
我们通过查看以上 render
方法,会根据 tagName
构建一个真正的 DOM
节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。
我们将构建好的 DOM
结构添加到页面 body
上面,如下:
ulRoot = ul.render(); document.body.appendChild(ulRoot);
这样,页面 body
里面就有真正的 DOM
结构,效果如下图所示:
2.2.2、比较两棵虚拟 DOM
树的差异 — diff
算法
diff
算法用来比较两棵 Virtual DOM
树的差异,如果需要两棵树的完全比较,那么 diff
算法的时间复杂度为O(n^3)
。但是在前端当中,你很少会跨越层级地移动 DOM
元素,所以 Virtual DOM
只会对同一个层级的元素进行对比,如下图所示, div
只会和同一层级的 div
对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 O(n)
。
(1)深度优先遍历,记录差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。
// diff 函数,对比两棵树 function diff(oldTree, newTree) { var index = 0 // 当前节点的标志 var patches = {} // 用来记录每个节点差异的对象 dfsWalk(oldTree, newTree, index, patches) return patches } // 对两棵树进行深度优先遍历 function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = [] if (typeof (oldNode) === "string" && typeof (newNode) === "string") { // 文本内容改变 if (newNode !== oldNode) { currentPatch.push({ type: patch.TEXT, content: newNode }) } } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 节点相同,比较属性 var propsPatches = diffProps(oldNode, newNode) if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }) } // 比较子节点,如果子节点有'ignore'属性,则不需要比较 if (!isIgnoreChildren(newNode)) { diffChildren( oldNode.children, newNode.children, index, patches, currentPatch ) } } else if(newNode !== null){ // 新节点和旧节点不同,用 replace 替换 currentPatch.push({ type: patch.REPLACE, node: newNode }) } if (currentPatch.length) { patches[index] = currentPatch } }
从以上可以得出,patches[1]
表示 p
,patches[3]
表示 ul
,以此类推。
(2)差异类型
DOM
操作导致的差异类型包括以下几种:
- 节点替换:节点改变了,例如将上面的
div
换成h1
; - 顺序互换:移动、删除、新增子节点,例如上面
div
的子节点,把p
和ul
顺序互换; - 属性更改:修改了节点的属性,例如把上面
li
的class
样式类删除; - 文本改变:改变文本节点的文本内容,例如将上面
p
节点的文本内容更改为 “Real Dom
”;
以上描述的几种差异类型在代码中定义如下所示:
var REPLACE = 0 // 替换原先的节点 var REORDER = 1 // 重新排序 var PROPS = 2 // 修改了节点的属性 var TEXT = 3 // 文本内容改变
(3)列表对比算法
子节点的对比算法,例如 p, ul, div
的顺序换成了 div, p, ul
。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如 p
和 div
的 tagName
不同,p
会被 div
所替代。最终,三个节点都会被替换,这样 DOM
JS
개체는 DOM
트리를 시뮬레이션합니다. 🎜🎜예를 들어 실제 DOM
노드는 다음과 같습니다. 🎜
var ul1 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']), el('li', { class: 'item' }, ['Item 2']), el('li', { class: 'item' }, ['Item 3']) ]), el('div',{},['Hello World']) ]) var ul2 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 21']), el('li', { class: 'item' }, ['Item 23']) ]), el('p',{},['Hello World']) ]) var patches = diff(ul1,ul2); console.log('patches:',patches);
우리는 JavaScript code> 객체는 DOM
노드를 나타내며 객체의 속성을 사용하여 노드의 유형, 속성, 하위 노드 등을 기록합니다. 🎜
element.js
는 노드 개체 코드를 다음과 같이 나타냅니다. 🎜
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { // 从patches拿出当前节点的差异 var currentPatches = patches[walker.index] var len = node.childNodes ? node.childNodes.length : 0 // 深度遍历子节点 for (var i = 0; i <p><code>element</code> 개체의 설정에 따라 위의 <code>DOM 구조는 다음과 같이 간단하게 표현할 수 있습니다. 🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
이제 ul
은 JavaScript
개체로 표현되는 DOM
구조입니다. 출력 및 보기ul
🎜
🎜 (2) DOM
을 로 표현 >JS
객체 🎜🎜
그러나 페이지에는 그러한 구조가 없습니다. 다음으로 ul
을 실제 DOM
구조로 렌더링하는 방법을 소개하겠습니다. 페이지 관련 렌더링 기능은 다음과 같습니다. 🎜
<div> <p>Virtual DOM</p> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> <div>Hello World</div> </div>
위의 render
메소드를 살펴보면 를 기반으로 실제 <code>DOM
노드를 구축해보겠습니다. tagName을 선택한 다음 이 노드의 속성을 설정하고 마지막으로 자체 하위 노드를 재귀적으로 구축합니다. 🎜
다음과 같이 구성된 DOM
구조를 페이지 body
에 추가합니다. 🎜
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
이런 방식으로 body 실제 <code>DOM
구조가 있고 그 효과는 아래와 같습니다: 🎜
🎜
🎜2.2. 2. 두 트리 비교 가상 DOM
트리의 차이점 - diff
알고리즘 🎜
diff
알고리즘은 두 diff
알고리즘을 사용하여 두 code>가상 DOM 트리 차이, 두 트리의 완전한 비교가 필요한 경우 <code>diff
알고리즘의 시간 복잡도는 O(n^3)
입니다. . 그러나 프런트엔드에서는 DOM
요소를 레벨 간에 이동하는 경우가 거의 없으므로 Virtual DOM
은 아래 그림과 같이 동일한 레벨의 요소만 비교합니다. div는 동일한 수준의 div
와만 비교되고, 두 번째 수준은 두 번째 수준하고만 비교되므로 알고리즘 복잡도는 O(n)에 도달할 수 있습니다. ). 🎜<p style="text-align: center;"><img src="/static/imghw/default1.png" data-src="https://img.php.cn/upload/image/323/191/567/1659956015838460.png" class="lazy" title="165995588713430Vue의 가상 DOM에 대한 심층 분석" alt="Vue의 가상 DOM에 대한 심층 분석">🎜</p>
<p>🎜(1) 깊이 우선 순회, 기록 차이🎜🎜</p>
<p>실제 코드에서는 이전 트리와 새 트리에 대해 깊이 우선 순회가 수행되므로 각 노드에는 고유한 태그가 있습니다: 🎜</p>
<p style="text-align: center;"><img src="/static/imghw/default1.png" data-src="https://img.php.cn/upload/image/323/191/567/1659956015838460.png" class="lazy" title="1659955899825790.png" alt="Vue의 가상 DOM에 대한 심층 분석">🎜</p>
<p>깊이 우선 순회 중에 노드를 순회할 때마다 해당 노드는 새 트리와 비교됩니다. 차이점이 있으면 객체에 기록됩니다. 🎜</p>
<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<vnode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<vnode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
}</vnode></vnode></pre><div class="contentsignin">로그인 후 복사</div></div><div class="contentsignin">로그인 후 복사</div></div>
<p>위에서 <code>patches[1]
는 p
를 나타내고 patches[3]
는 ul을 나타낸다는 결론을 내릴 수 있습니다.
코드> 등. 🎜
🎜(2) 차이 유형🎜🎜
DOM
작업으로 인해 발생하는 차이 유형은 다음과 같습니다. 🎜
- 노드 교체: 노드가 변경됩니다. 예를 들어, 위의
div
는h1
로 대체됩니다. - 시퀀스 교환: 위의
div와 같은 하위 노드 이동, 삭제, 추가
의 하위 노드는p
와ul
의 순서를 교환합니다. - 속성 변경: 노드의 속성을 수정합니다. , 위의
li의 <code>class
스타일 클래스를 삭제합니다. - 텍스트 변경: 텍스트 노드의 텍스트 내용을 변경합니다. 위의
p
노드의 텍스트 콘텐츠를 "Real Dom
"으로;
위에 설명된 여러 가지 차이점 유형은 코드에서 다음과 같이 정의됩니다. 다음: 🎜
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }
🎜(3) 목록 비교 알고리즘 🎜🎜
하위 노드에 대한 비교 알고리즘(예: p, ul, div
순서가 div로 대체됨) , p, ul
. 이것을 어떻게 비교하나요? 동일한 레벨에서 순차적으로 비교하면 모두 교체됩니다. p
와 div
의 tagName
이 다른 경우 p
는 div
로 대체됩니다. >. 결국 세 개의 노드가 모두 교체되므로 DOM
오버헤드가 매우 높습니다. 사실 노드를 교체할 필요는 없고 노드만 이동하면 됩니다. 🎜
将这个问题抽象出来其实就是字符串的最小编辑距离问题(Edition Distance
),最常见的解决方法是 Levenshtein Distance
, Levenshtein Distance
是一个度量两个字符序列之间差异的字符串度量标准,两个单词之间的 Levenshtein Distance
是将一个单词转换为另一个单词所需的单字符编辑(插入、删除或替换)的最小数量。Levenshtein Distance
是1965年由苏联数学家 Vladimir Levenshtein 发明的。Levenshtein Distance
也被称为编辑距离(Edit Distance
),通过动态规划求解,时间复杂度为 O(M*N)
。
定义:对于两个字符串 a、b
,则他们的 Levenshtein Distance
为:
示例:字符串 a
和 b
,a=“abcde” ,b=“cabef”
,根据上面给出的计算公式,则他们的 Levenshtein Distance
的计算过程如下:
本文的 demo
使用插件 list-diff2
算法进行比较,该算法的时间复杂度伟 O(n*m)
,虽然该算法并非最优的算法,但是用于对于 dom
元素的常规操作是足够的。
该算法具体的实现过程这里不再详细介绍,该算法的具体介绍可以参照:https://github.com/livoras/list-diff
(4)实例输出
两个虚拟 DOM
对象如下图所示,其中 ul1
表示原有的虚拟 DOM
树,ul2
表示改变后的虚拟 DOM
树
var ul1 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']), el('li', { class: 'item' }, ['Item 2']), el('li', { class: 'item' }, ['Item 3']) ]), el('div',{},['Hello World']) ]) var ul2 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 21']), el('li', { class: 'item' }, ['Item 23']) ]), el('p',{},['Hello World']) ]) var patches = diff(ul1,ul2); console.log('patches:',patches);
我们查看输出的两个虚拟 DOM
对象之间的差异对象如下图所示,我们能通过差异对象得到,两个虚拟 DOM
对象之间进行了哪些变化,从而根据这个差异对象(patches
)更改原先的真实 DOM
结构,从而将页面的 DOM
结构进行更改。
2.2.3、将两个虚拟 DOM
对象的差异应用到真正的 DOM
树
(1)深度优先遍历 DOM
树
因为步骤一所构建的 JavaScript
对象树和 render
出来真正的 DOM
树的信息、结构是一样的。所以我们可以对那棵 DOM
树也进行深度优先的遍历,遍历的时候从步骤二生成的 patches
对象中找出当前遍历的节点差异,如下相关代码所示:
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { // 从patches拿出当前节点的差异 var currentPatches = patches[walker.index] var len = node.childNodes ? node.childNodes.length : 0 // 深度遍历子节点 for (var i = 0; i(2)对原有
DOM
树进行DOM
操作我们根据不同类型的差异对当前节点进行不同的
DOM
操作 ,例如如果进行了节点替换,就进行节点替换DOM
操作;如果节点文本发生了改变,则进行文本替换的DOM
操作;以及子节点重排、属性改变等DOM
操作,相关代码如applyPatches
所示 :function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }로그인 후 복사로그인 후 복사(3)DOM结构改变
通过将第 2.2.2 得到的两个
DOM
对象之间的差异,应用到第一个(原先)DOM
结构中,我们可以看到DOM
结构进行了预期的变化,如下图所示:
2.3、结语
相关代码实现已经放到 github 上面,有兴趣的同学可以clone运行实验,github地址为:https://github.com/fengshi123/virtual-dom-example%E3%80%82
Virtual DOM
算法主要实现上面三个步骤来实现:
-
用
JS
对象模拟DOM
树 —element.js
<div> <p>Virtual DOM</p> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> <div>Hello World</div> </div>
로그인 후 복사로그인 후 복사로그인 후 복사 比较两棵虚拟
DOM
树的差异 —diff.js
-
将两个虚拟
DOM
对象的差异应用到真正的DOM
树 —patch.js
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
로그인 후 복사로그인 후 복사
三、Vue
源码 Virtual-DOM
简析
我们从第二章节(Virtual-DOM
基础)中已经掌握 Virtual DOM
渲染成真实的 DOM
实际上要经历 VNode
的定义、diff
、patch
等过程,所以本章节 Vue
源码的解析也按这几个过程来简析。
3.1、VNode
模拟 DOM
树
3.1.1、VNode
类简析
在 Vue.js
中,Virtual DOM
是用 VNode
这个 Class
去描述,它定义在 src/core/vdom/vnode.js
中 ,从以下代码块中可以看到 Vue.js
中的 Virtual DOM
的定义较为复杂一些,因为它这里包含了很多 Vue.js
的特性。实际上 Vue.js
中 Virtual DOM
是借鉴了一个开源库 snabbdom 的实现,然后加入了一些 Vue.js
的一些特性。
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<vnode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<vnode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } }</vnode></vnode>
这里千万不要因为 VNode
的这么属性而被吓到,或者咬紧牙去摸清楚每个属性的意义,其实,我们主要了解其几个核心的关键属性就差不多了,例如:
-
tag
属性即这个vnode
的标签属性 -
data
属性包含了最后渲染成真实dom
节点后,节点上的class
,attribute
,style
以及绑定的事件 -
children
属性是vnode
的子节点 -
text
属性是文本属性 -
elm
属性为这个vnode
对应的真实dom
节点 -
key
属性是vnode
的标记,在diff
过程中可以提高diff
的效率
3.1.2、源码创建 VNode
过程
(1)初始化vue
我们在实例化一个 vue
实例,也即 new Vue( )
时,实际上是执行 src/core/instance/index.js
中定义的 Function
函数。
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }
通过查看 Vue
的 function
,我们知道 Vue
只能通过 new
关键字初始化,然后调用 this._init
方法,该方法在 src/core/instance/init.js
中定义。
Vue.prototype._init = function (options?: Object) { const vm: Component = this // 省略一系列其它初始化的代码 if (vm.$options.el) { console.log('vm.$options.el:',vm.$options.el); vm.$mount(vm.$options.el) } }
(2)Vue
实例挂载
Vue
中是通过 $mount
实例方法去挂载 dom
的,下面我们通过分析 compiler
版本的 mount
实现,相关源码在目录 src/platforms/web/entry-runtime-with-compiler.js
文件中定义:。
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) // 省略一系列初始化以及逻辑判断代码 return mount.call(this, el, hydrating) }
我们发现最终还是调用用原先原型上的 $mount
方法挂载 ,原先原型上的 $mount
方法在 src/platforms/web/runtime/index.js
中定义 。
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
我们发现$mount
方法实际上会去调用 mountComponent
方法,这个方法定义在 src/core/instance/lifecycle.js
文件中
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 省略一系列其它代码 let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // 生成虚拟 vnode const vnode = vm._render() // 更新 DOM vm._update(vnode, hydrating) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false return vm }
从上面的代码可以看到,mountComponent
核心就是先实例化一个渲染Watcher
,在它的回调函数中会调用 updateComponent
方法,在此方法中调用 vm._render
方法先生成虚拟 Node,最终调用 vm._update
更新 DOM
。
(3)创建虚拟 Node
Vue
的 _render
方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node
。它的定义在 src/core/instance/render.js
文件中:
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options let vnode try { // 省略一系列代码 currentRenderingInstance = vm // 调用 createElement 方法来返回 vnode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`){} } // set parent vnode.parent = _parentVnode console.log("vnode...:",vnode); return vnode }
Vue.js
利用 _createElement
方法创建 VNode
,它定义在 src/core/vdom/create-elemenet.js
中:
export function _createElement ( context: Component, tag?: string | Class<component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<vnode> { // 省略一系列非主线代码 if (normalizationType === ALWAYS_NORMALIZE) { // 场景是 render 函数不是编译生成的 children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { // 场景是 render 函数是编译生成的 children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // 创建虚拟 vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } }</vnode></component>
_createElement
方法有 5 个参数,context
表示 VNode 的上下文环境,它是 Component
类型;tag
表示标签,它可以是一个字符串,也可以是一个 Component
;data
表示 VNode 的数据,它是一个 VNodeData
类型,可以在 flow/vnode.js
中找到它的定义;children
表示当前 VNode 的子节点,它是任意类型的,需要被规范为标准的 VNode
数组;
3.1.3、实例查看
为了更直观查看我们平时写的 Vue
代码如何用 VNode
类来表示,我们通过一个实例的转换进行更深刻了解。
例如,实例化一个 Vue
实例:
var app = new Vue({ el: '#app', render: function (createElement) { return createElement('div', { attrs: { id: 'app', class: "class_box" }, }, this.message) }, data: { message: 'Hello Vue!' } })
我们打印出其对应的 VNode
表示:
3.2、diff
过程
3.2.1、Vue.js
源码的 diff
调用逻辑
Vue.js
源码实例化了一个 watcher
,这个 ~ 被添加到了在模板当中所绑定变量的依赖当中,一旦 model
中的响应式的数据发生了变化,这些响应式的数据所维护的 dep
数组便会调用 dep.notify()
方法完成所有依赖遍历执行的工作,这包括视图的更新,即 updateComponent
方法的调用。watcher
和 updateComponent
方法定义在 src/core/instance/lifecycle.js
文件中 。
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 省略一系列其它代码 let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // 生成虚拟 vnode const vnode = vm._render() // 更新 DOM vm._update(vnode, hydrating) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false return vm }
完成视图的更新工作事实上就是调用了vm._update
方法,这个方法接收的第一个参数是刚生成的Vnode
,调用的vm._update
方法定义在 src/core/instance/lifecycle.js
中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode if (!prevVnode) { // 第一个参数为真实的node节点,则为初始化 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } }
在这个方法当中最为关键的就是 vm.__patch__
方法,这也是整个 virtual-dom
当中最为核心的方法,主要完成了prevVnode
和 vnode
的 diff
过程并根据需要操作的 vdom
节点打 patch
,最后生成新的真实 dom
节点并完成视图的更新工作。
接下来,让我们看下 vm.__patch__
的逻辑过程, vm.__patch__
方法定义在 src/core/vdom/patch.js
中。
function patch (oldVnode, vnode, hydrating, removeOnly) { ...... if (isUndef(oldVnode)) { // 当oldVnode不存在时,创建新的节点 isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // 对oldVnode和vnode进行diff,并对oldVnode打patch const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } ...... } }
在 patch
方法中,我们看到会分为两种情况,一种是当 oldVnode
不存在时,会创建新的节点;另一种则是已经存在 oldVnode
,那么会对 oldVnode
和 vnode
进行 diff
及 patch
的过程。其中 patch
过程中会调用 sameVnode
方法来对对传入的2个 vnode
进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode
只是局部发生了更新,然后才会对这2个 vnode
进行 diff
,如果2个 vnode
的基本属性存在不一致的情况,那么就会直接跳过 diff
的过程,进而依据 vnode
新建一个真实的 dom
,同时删除老的 dom
节点。
function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) }
diff
过程中主要是通过调用 patchVnode
方法进行的:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { ...... const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children // 如果vnode没有文本节点 if (isUndef(vnode.text)) { // 如果oldVnode的children属性存在且vnode的children属性也存在 if (isDef(oldCh) && isDef(ch)) { // updateChildren,对子节点进行diff if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } // 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 删除elm下的oldchildren removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // oldVnode有子节点,而vnode没有,那么就清空这个节点 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素 nodeOps.setTextContent(elm, vnode.text) } ...... }
从以上代码得知,
diff
过程中又分了好几种情况,oldCh
为 oldVnode
的子节点,ch
为 Vnode
的子节点:
- 首先进行文本节点的判断,若
oldVnode.text !== vnode.text
,那么就会直接进行文本节点的替换; - 在
vnode
没有文本节点的情况下,进入子节点的diff
; - 当
oldCh
和ch
都存在且不相同的情况下,调用updateChildren
对子节点进行diff
; - 若
oldCh
不存在,ch
存在,首先清空oldVnode
的文本节点,同时调用addVnodes
方法将ch
添加到elm
真实dom
节点当中; - 若
oldCh
存在,ch
不存在,则删除elm
真实节点下的oldCh
子节点; - 若
oldVnode
有文本节点,而vnode
没有,那么就清空这个文本节点。
3.2.2、子节点 diff
流程分析
(1)Vue.js
源码
这里着重分析下updateChildren
方法,它也是整个 diff
过程中最重要的环节,以下为 Vue.js
的源码过程,为了更形象理解 diff
过程,我们给出相关的示意图来讲解。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 为oldCh和newCh分别建立索引,为之后遍历的依据 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, idxInOld, vnodeToMove, refElm // 直到oldCh或者newCh被遍历完后跳出循环 while (oldStartIdx oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
在开始遍历 diff
前,首先给 oldCh
和 newCh
分别分配一个 startIndex
和 endIndex
来作为遍历的索引,当oldCh
或者 newCh
遍历完后(遍历完的条件就是 oldCh
或者 newCh
的 startIndex >= endIndex
),就停止oldCh
和 newCh
的 diff
过程。接下来通过实例来看下整个 diff
的过程(节点属性中不带 key
的情况)。
(2)无 key
的 diff
过程
我们通过以下示意图对以上代码过程进行讲解:
(2.1)首先从第一个节点开始比较,不管是 oldCh
还是 newCh
的起始或者终止节点都不存在 sameVnode
,同时节点属性中是不带 key
标记的,因此第一轮的 diff
完后,newCh
的 startVnode
被添加到 oldStartVnode
的前面,同时 newStartIndex
前移一位;
(2.2)第二轮的 diff
中,满足 sameVnode(oldStartVnode, newStartVnode)
,因此对这2个 vnode
进行diff
,最后将 patch
打到 oldStartVnode
上,同时 oldStartVnode
和 newStartIndex
都向前移动一位 ;
(2.3)第三轮的 diff
中,满足 sameVnode(oldEndVnode, newStartVnode)
,那么首先对 oldEndVnode
和newStartVnode
进行 diff
,并对 oldEndVnode
进行 patch
,并完成 oldEndVnode
移位的操作,最后newStartIndex
前移一位,oldStartVnode
后移一位;
(2.4)第四轮的 diff
中,过程同步骤3;
(2.5)第五轮的 diff
中,同过程1;
(2.6)遍历的过程结束后,newStartIdx > newEndIdx
,说明此时 oldCh
存在多余的节点,那么最后就需要将这些多余的节点删除。
(3)有 key
的 diff
流程
在 vnode
不带 key
的情况下,每一轮的 diff
过程当中都是起始
和结束
节点进行比较,直到 oldCh
或者newCh
被遍历完。而当为 vnode
引入 key
属性后,在每一轮的 diff
过程中,当起始
和结束
节点都没有找到sameVnode
时,然后再判断在 newStartVnode
的属性中是否有 key
,且是否在 oldKeyToIndx
中找到对应的节点 :
- 如果不存在这个
key
,那么就将这个newStartVnode
作为新的节点创建且插入到原有的root
的子节点中; - 如果存在这个
key
,那么就取出oldCh
中的存在这个key
的vnode
,然后再进行diff
的过;
通过以上分析,给vdom
上添加 key
属性后,遍历 diff
的过程中,当起始点,结束点的搜寻及 diff
出现还是无法匹配的情况下时,就会用 key
来作为唯一标识,来进行 diff
,这样就可以提高 diff
效率。
带有 Key
属性的 vnode
的 diff
过程可见下图:
(3.1)首先从第一个节点开始比较,不管是 oldCh
还是 newCh
的起始或者终止节点都不存在 sameVnode
,但节点属性中是带 key
标记的, 然后在 oldKeyToIndx
中找到对应的节点,这样第一轮 diff
过后 oldCh
上的B节点
被删除了,但是 newCh
上的B节点
上 elm
属性保持对 oldCh
上 B节点
的elm
引用。
(3.2)第二轮的 diff
中,满足 sameVnode(oldStartVnode, newStartVnode)
,因此对这2个 vnode
进行diff
,最后将 patch
打到 oldStartVnode
上,同时 oldStartVnode
和 newStartIndex
都向前移动一位 ;
(3.3)第三轮的 diff
中,满足 sameVnode(oldEndVnode, newStartVnode)
,那么首先对 oldEndVnode
和newStartVnode
进行 diff
,并对 oldEndVnode
进行 patch
,并完成 oldEndVnode
移位的操作,最后newStartIndex
前移一位,oldStartVnode
后移一位;
(3.4)第四轮的diff
中,过程同步骤2;
(3.5)第五轮的diff
中,因为此时 oldStartIndex
已经大于 oldEndIndex
,所以将剩余的 Vnode
队列插入队列最后。
3.3、patch
过程
通过3.2章节介绍的 diff
过程中,我们会看到 nodeOps
相关的方法对真实 DOM
结构进行操作,nodeOps
定义在 src/platforms/web/runtime/node-ops.js
中,其为基本 DOM
操作,这里就不在详细介绍。
export function createElementNS (namespace: string, tagName: string): Element { return document.createElementNS(namespaceMap[namespace], tagName) } export function createTextNode (text: string): Text { return document.createTextNode(text) } export function createComment (text: string): Comment { return document.createComment(text) } export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { parentNode.insertBefore(newNode, referenceNode) } export function removeChild (node: Node, child: Node) { node.removeChild(child) }
3.4、总结
通过前三小节简析,我们从主线上把模板和数据如何渲染成最终的 DOM
的过程分析完毕了,我们可以通过下图更直观地看到从初始化 Vue
到最终渲染的整个过程。
四、总结
本文从通过介绍真实 DOM
结构其解析过程以及存在的问题,从而引出为什么需要虚拟 DOM
;然后分析虚拟DOM
的好处,以及其一些理论基础和基础算法的实现;最后根据我们已经掌握的基础知识,再一步步去查看Vue.js
的源码如何实现的。从存在问题 —> 理论基础 —> 具体实践,一步步深入,帮助大家更好的了解什么是Virtual DOM
、为什么需要 Virtual DOM
、以及 Virtual DOM
的具体实现,希望本文对您有帮助。
위 내용은 Vue의 가상 DOM에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











Vue에서 ECharts를 사용하면 애플리케이션에 데이터 시각화 기능을 쉽게 추가할 수 있습니다. 특정 단계에는 ECharts 및 Vue ECharts 패키지 설치, ECharts 소개, 차트 구성 요소 생성, 옵션 구성, 차트 구성 요소 사용, Vue 데이터에 반응하는 차트 만들기, 대화형 기능 추가 및 고급 사용법이 포함됩니다.

질문: Vue에서 내보내기 기본값의 역할은 무엇입니까? 자세한 설명: 내보내기 기본값은 구성 요소의 기본 내보내기를 정의합니다. 가져올 때 구성 요소를 자동으로 가져옵니다. 가져오기 프로세스를 단순화하고 명확성을 높이며 충돌을 방지합니다. 개별 구성 요소를 내보내고, 명명된 내보내기와 기본 내보내기를 모두 사용하고, 전역 구성 요소를 등록하는 데 일반적으로 사용됩니다.

Vue.js 맵 함수는 각 요소가 원래 배열의 각 요소를 변환한 결과인 새 배열을 생성하는 내장된 고차 함수입니다. 구문은 map(callbackFn)입니다. 여기서 callbackFn은 배열의 각 요소를 첫 번째 인수로 받고 선택적으로 인덱스를 두 번째 인수로 받아 값을 반환합니다. map 함수는 원래 배열을 변경하지 않습니다.

Vue.js에서 event는 브라우저에 의해 트리거되는 기본 JavaScript 이벤트인 반면, $event는 Vue 구성 요소에서 사용되는 Vue 관련 추상 이벤트 객체입니다. $event는 데이터 바인딩을 지원하도록 형식이 지정되고 향상되었으므로 일반적으로 $event를 사용하는 것이 더 편리합니다. 기본 이벤트 객체의 특정 기능에 액세스해야 하는 경우 이벤트를 사용하세요.

Vue.js에서 모듈을 내보내는 방법에는 내보내기와 기본값 내보내기라는 두 가지 방법이 있습니다. 내보내기는 명명된 엔터티를 내보내는 데 사용되며 중괄호를 사용해야 합니다. 내보내기 기본값은 기본 엔터티를 내보내는 데 사용되며 중괄호가 필요하지 않습니다. 가져올 때 내보내기로 내보낸 엔터티는 해당 이름을 사용해야 하는 반면, 내보내기 기본값으로 내보낸 엔터티는 암시적으로 사용될 수 있습니다. 여러 번 가져와야 하는 모듈에는 내보내기 기본값을 사용하고, 한 번만 내보내는 모듈에는 내보내기를 사용하는 것이 좋습니다.

onMounted는 Vue의 구성 요소 마운팅 라이프 사이클 후크입니다. 해당 기능은 DOM 요소에 대한 참조 가져오기, 데이터 설정, HTTP 요청 전송, 이벤트 리스너 등록 등과 같은 구성 요소가 DOM에 마운트된 후 초기화 작업을 수행하는 것입니다. 구성 요소가 마운트될 때 한 번만 호출됩니다. 구성 요소가 업데이트된 후 또는 삭제되기 전에 작업을 수행해야 하는 경우 다른 수명 주기 후크를 사용할 수 있습니다.

Vue 후크는 특정 이벤트 또는 수명 주기 단계에서 작업을 수행하는 콜백 함수입니다. 여기에는 수명 주기 후크(예: beforeCreate, 마운트, beforeDestroy), 이벤트 처리 후크(예: 클릭, 입력, 키다운) 및 사용자 정의 후크가 포함됩니다. 후크는 구성 요소 제어를 강화하고 구성 요소 수명 주기에 응답하며 사용자 상호 작용을 처리하고 구성 요소 재사용성을 향상시킵니다. 후크를 사용하려면 후크 함수를 정의하고 로직을 실행한 후 선택적 값을 반환하면 됩니다.

Vue.js 이벤트 수정자는 다음을 포함한 특정 동작을 추가하는 데 사용됩니다. 기본 동작 방지(.prevent) 이벤트 버블링 중지(.stop) 일회성 이벤트(.once) 이벤트 캡처(.capture) 수동 이벤트 수신(.passive) 적응형 수정자(.self)키 수정자(.key)
