Vue2 剝絲抽繭 - 虛擬 dom 之更新
虛擬 dom 簡介、虛擬 dom 之綁定事件 中我們將虛擬 dom
轉換爲了真實 dom
的結構,介紹了 dom
中 class
、style
、綁定事件的過程。
當數據更新的時候,vue
會重新觸發 render
,此時會通過新的 vdom
來更新視圖。
新的 vdom
結構可能發生改變,就涉及到 dom
的新建、刪除和移動,這篇文章先假設更新的 dom
結構沒有變化,我們來過一下整體更新的過程。
dom 結構
不管是虛擬 dom
,還是真實 dom
,都可以看成一個樹結構。
對應的 render
函數如下:
render(createElement) {
return createElement(
"div",
[
createElement("div", [
createElement("div", {}, "left"),
"hello",
]),
createElement("span", {}, "right"),
]
);
},
生成的 vnode
如下:
{
"tag": "div",
"children": [
{
"tag": "div",
"children": [
{
"tag": "div",
"children": [
{
"text": "left",
}
],
},
{
"text": "hello",
}
],
},
{
"tag": "span",
"data": {},
"children": [
{
"text": "right",
}
],
}
],
}
渲染的 dom
如下:
假設新的 vnode
結構沒有改變,只是 text
進行了更新:
{
"tag": "div",
"children": [
{
"tag": "div",
"children": [
{
"tag": "div",
"children": [
{
"text": "leftupdate",
}
],
},
{
"text": "hello",
}
],
},
{
"tag": "span",
"data": {},
"children": [
{
"text": "rightupdate",
}
],
}
],
}
我們只需要同時遍歷這兩個 vdom
,如果有 tag
屬性就遞歸它們的 children
,如果只有 text
屬性就更新 dom
的 text
即可。
function patchVnode (
oldVnode,
vnode,
) {
const elm = vnode.elm = oldVnode.elm // 拿到對應的 dom
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) { // 如果沒有 text 屬性,遞歸遍歷 children
for(let i = 0; i < oldch.length; i++) {
patchVnode(oldch[i], ch[i])
}
// 如果有 text 屬性,說明是 text 節點
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text) // 更新 text
}
}
上邊就是更新的核心邏輯了,本質上就是對樹的一個深度優先遍歷,下邊我們繼續完善一些細節。
引入響應式
爲了測試數據更新自動更新頁面,相比於 Vue2 剝絲抽繭 - 虛擬 dom 之綁定事件 的測試程序,我們將上一篇章介紹的 響應式系統 引入,當點擊的時候我們修改 data
的數據,然後自動觸發頁面的 update
。
import * as nodeOps from "./node-ops";
import modules from "./modules";
import { createPatchFunction } from "./patch";
import { createElement } from "./create-element";
import { observe } from "./observer/reactive";
import Watcher from "./observer/watcher";
const options = {
el: "#root",
data: {
selected: 1,
},
render(createElement) {
const vnode = createElement(
"div",
{
on: {
click: () => {
this.selected = 3;
},
},
},
[
createElement("div", [
createElement("div", {}, this.selected + "left"), // 使用 data 數據
"hello",
]),
createElement("span", {}, "right"),
]
);
return vnode;
},
};
const _render = function () {
const vnode = options.render.call(options.data, createElement);
return vnode;
};
let $el = document.querySelector(options.el);
const __patch__ = createPatchFunction({ nodeOps, modules });
const _update = (vnode) => {
$el = __patch__($el, vnode);
};
observe(options.data); // 將數據變爲響應式
new Watcher(options.data, () => _update(_render())); // 創建 Watcher
這樣當我們點擊頁面的時候,頁面就會自動刷新了,selected
的值從 1
變成了 3
。
但因爲我們並沒有寫更新 dom
的代碼,此時相當於是用新的 vnode
生成了新 dom
然後直接代替了原 dom
。
在創建 dom
代碼打個斷點來看一下:
下邊來完善下當 vnode
結構不變情況下 dom
的更新代碼。
更新代碼
看一下我們原來的 _update
方法:
const _update = (vnode) => {
$el = __patch__($el, vnode);
};
因爲之前第一次創建 dom
的時候還沒有舊的 vdom
,所以我們直接傳了 $el
,但當第二次更新的時候已經有了 oldvode
,我們第一個參數應該把舊的 vnode
傳入。
const vm = {};
vm.$el = document.querySelector(options.el);
const _update = (vnode) => {
const prevVnode = vm._vnode;
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = __patch__(vm.$el, vnode);
} else {
// updates
vm.$el = __patch__(prevVnode, vnode);
}
};
上邊模擬一個 vm
對象,將 $el
掛到 vm
對象中,同時用 vm._vnode
存儲 vnode
,這樣下一次更新的時候 vm._vnode
就代表的是舊的 vnode
了。
接下來完善 createPatchFunction
返回的 __patch__
方法:
return function patch(oldVnode, vnode) {
const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 通過新舊 vnode 進行更新
patchVnode(oldVnode, vnode);
} else {
// vnode 發生改變或者是第一次渲染
if (isRealElement) {
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm);
// create new node
createElm(vnode, parentElm, nodeOps.nextSibling(oldElm));
removeVnodes([oldVnode], 0, 0);
}
return vnode.elm;
};
上邊的 else
分支中的代碼是 虛擬 dom 之綁定事件 中我們介紹的邏輯。
if
中判斷它不是真實 dom
並且當前的 vnode
沒有改變,然後就調用 pathVnode
方法來更新 dom
。
其中的 sameVnode
我們僅簡單判斷:
// vue 源碼中的 sameVnode 判斷的比較多,這裏我們僅簡單理解爲 key、tag 一致,並且 data 屬性還存在即可
function sameVnode(a, b) {
return (
a.key === b.key && a.tag === b.tag && isDef(a.data) === isDef(b.data)
);
}
接着看一下 patchVnode
的實現:
function isPatchable(vnode) {
return isDef(vnode.tag);
}
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) {
return;
}
const elm = (vnode.elm = oldVnode.elm);
const oldCh = oldVnode.children;
const ch = vnode.children;
const data = vnode.data;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
}
if (isUndef(vnode.text)) { // 不是 text 節點 更新children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch);
} else if (isDef(oldVnode.text)) {
// 更新成了空字符
nodeOps.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
// text 節點
nodeOps.setTextContent(elm, vnode.text);
}
}
這就是文章最開始講的那段邏輯了,不是 text
節點就更新 children
,如果是 text
節點就直接更新 dom
的文本內容。
除此之外,創建 dom
的時候在 虛擬 dom 之綁定事件 我們調用了 cbs.create
,這裏我們調用 cbs.update
來更新 dom
的屬性。
因爲這篇文章我們只考慮 dom
整個結構沒有發生變化的情況,所以我們 updateChilden
簡單的實現爲一個循環即可。
function updateChildren(elm, oldCh, ch) {
for (let i = 0; i < oldCh.length; i++) {
patchVnode(oldCh[i], ch[i]);
}
}
以上就是 dom
更新的整個過程了。
測試
import * as nodeOps from "./node-ops";
import modules from "./modules";
import { createPatchFunction } from "./patch";
import { createElement } from "./create-element";
import { observe } from "./observer/reactive";
import Watcher from "./observer/watcher";
const options = {
el: "#root",
data: {
selected: 1,
},
render(createElement) {
const vnode = createElement(
"div",
{
on: {
click: () => {
this.selected = 3;
},
},
},
[
createElement("div", [
createElement("div", {}, this.selected + "left"),
"hello",
]),
createElement("span", {}, "right"),
]
);
return vnode;
},
};
const _render = function () {
const vnode = options.render.call(options.data, createElement);
return vnode;
};
const __patch__ = createPatchFunction({ nodeOps, modules });
const vm = {};
vm.$el = document.querySelector(options.el);
const _update = (vnode) => {
const prevVnode = vm._vnode;
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = __patch__(vm.$el, vnode);
} else {
// updates
vm.$el = __patch__(prevVnode, vnode);
}
};
observe(options.data);
new Watcher(options.data, () => _update(_render()));
視圖肯定會更新,我們來看一下是刪除原有 dom
插入新 dom
,還是直接在原有 dom
上進行的更新:
可以看到代碼走到了我們的 patchVnode
中,複用了原有 dom
進行更新。
總
這篇文章主要是加深對虛擬 dom
結構的瞭解,然後通過深度優先遍歷對虛擬 dom
樹進行遍歷,因爲我們假設了 dom
樹的結構沒有發生變化,所以遍歷過程中直接進行節點的更新即可。
如果 dom
樹發生了變化,爲了儘可能的複用原有 dom
,就會涉及到 diff
算法了,接下來幾篇文章會講到。
本文相應源碼詳見網站:vue.windliang.wang
windliang 前端,生活,成長
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/m9Xmtvw-TCLwfFn_qbU6wA