Study Notes
本博主會持續(xù)更新各種前端的技術(shù)怯邪,如果各位道友喜歡绊寻,可以關(guān)注、收藏悬秉、點贊下本博主的文章榛斯。
Vue.js 源碼剖析-虛擬 DOM
什么是虛擬 DOM
虛擬 DOM(Virtual DOM) 是使用 JavaScript 對象來描述 DOM,虛擬 DOM 的本質(zhì)就是 JavaScript 對象搂捧,使用 JavaScript 對象來描述 DOM 的結(jié)構(gòu)。應(yīng)用的各種狀態(tài)變化首先作用于虛擬 DOM懂缕,最終映射到 DOM允跑。Vue.js 中的虛擬 DOM 借鑒了 Snabbdom,并添加了一些 Vue.js 中的特性搪柑,例如:指令和組件機制聋丝。
Vue 1.x 中細(xì)粒度監(jiān)測數(shù)據(jù)的變化,每一個屬性對應(yīng)一個 watcher工碾,開銷太大 Vue 2.x 中每個組件對應(yīng)一個 watcher弱睦,狀態(tài)變化通知到組件,再引入虛擬 DOM 進(jìn)行比對和渲染
為什么要使用虛擬 DOM
使用虛擬 DOM渊额,可以避免用戶直接操作 DOM况木,開發(fā)過程關(guān)注在業(yè)務(wù)代碼的實現(xiàn)垒拢,不需要關(guān)注如何操作 DOM,從而提高開發(fā)效率
作為一個中間層可以跨平臺火惊,除了 Web 平臺外求类,還支持 SSR、Weex屹耐。
關(guān)于性能方面尸疆,在首次渲染的時候肯定不如直接操作 DOM,因為要維護(hù)一層額外的虛擬 DOM惶岭,如果后續(xù)有頻繁操作 DOM 的操作寿弱,這個時候可能會有性能的提升,虛擬 DOM 在更新真實 DOM 之前會通過 Diff 算法對比新舊兩個虛擬 DOM 樹的差異按灶,最終把差異更新到真實 DOM
vue 虛擬 DOM 使用
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 標(biāo)簽名稱
this.$slots.default, // 子節(jié)點數(shù)組
);
},
props: {
level: {
type: Number,
required: true,
},
},
});
createElement
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標(biāo)簽名症革、組件選項對象,或者
// resolve 了上述任何一種的一個 async 函數(shù)兆衅。必填項地沮。
'div',
// {Object}
// 一個與模板中 attribute 對應(yīng)的數(shù)據(jù)對象∠勰叮可選摩疑。
{
// (詳情見下一節(jié))
},
// {String | Array}
// 子級虛擬節(jié)點 (VNodes),由 `createElement()` 構(gòu)建而成畏铆,
// 也可以使用字符串來生成“文本虛擬節(jié)點”雷袋。可選辞居。
[
'先寫一些文字',
createElement('h1', '一則頭條'),
createElement(MyComponent, {
props: {
someProp: 'foobar',
},
}),
],
);
虛擬 DOM 創(chuàng)建過程
vue 虛擬 DOM 源碼分析
createElement
在 vm._render() 中調(diào)用了用戶傳遞的(或者編譯生成的) render 函數(shù)楷怒,這個時候傳遞了 createElement
src/core/instance/render.js
// 對編譯生成的render進(jìn)行渲染的方法
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// 對手寫render函數(shù)進(jìn)行渲染的方法
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
src/core/vdom/create-element.js
使用 createElement 創(chuàng)建 VNode,并返回給 vm._update瓦灶,最終傳遞給 Watcher 對象
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number,
): VNode {
// 如果data存在鸠删,且存在__ob__屬性,創(chuàng)建一個空虛擬DOM節(jié)點
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data,
)}\n` + 'Always create fresh vnode data objects in each render!',
context,
);
return createEmptyVNode();
}
// object syntax in v-bind
// 如果data存在贼陶,并且存在is屬性刃泡,將其賦值給tag(標(biāo)簽)
// <component v-bind:is="currentTabComponent"></component>
if (isDef(data) && isDef(data.is)) {
tag = data.is;
}
// 如果不存在tag,創(chuàng)建一個空虛擬DOM節(jié)點
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode();
}
// warn against non-primitive key
if (
process.env.NODE_ENV !== 'production' &&
isDef(data) &&
isDef(data.key) &&
!isPrimitive(data.key)
) {
// 避免使用非原始值作為key
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context,
);
}
// support single function children as default scoped slot
// 如果children是一個數(shù)組碉怔,并且數(shù)組的第一位元素是一個函數(shù)
if (Array.isArray(children) && typeof children[0] === 'function') {
data = data || {};
data.scopedSlots = { default: children[0] };
children.length = 0;
}
// ALWAYS_NORMALIZE代表用戶傳入的render
if (normalizationType === ALWAYS_NORMALIZE) {
// 當(dāng)手寫 render 函數(shù)的時候調(diào)用
// 判斷 children 的類型烘贴,如果是原始值的話轉(zhuǎn)換成 VNode 的數(shù)組
// 如果是數(shù)組的話,繼續(xù)處理數(shù)組中的元素
// 如果數(shù)組中的子元素又是數(shù)組(slot template)撮胧,遞歸處理
// 如果連續(xù)兩個節(jié)點都是字符串會合并文本節(jié)點
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 將二維數(shù)組轉(zhuǎn)換為一維數(shù)組并返回
// 如果 children 中有函數(shù)組件的話桨踪,函數(shù)組件會返回數(shù)組形式
// 這時候 children 就是一個二維數(shù)組,只需要把二維數(shù)組轉(zhuǎn)換為一維數(shù)組
children = simpleNormalizeChildren(children);
}
let vnode, ns;
if (typeof tag === 'string') {
let Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
// 如果是瀏覽器的保留標(biāo)簽芹啥,創(chuàng)建對應(yīng)的 VNode
if (config.isReservedTag(tag)) {
// platform built-in elements
// 創(chuàng)建vnode對象
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context,
);
} else if (
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
) {
// component
// 如果是自定義組件
// 查找自定義組件構(gòu)造函數(shù)的聲明
// 根據(jù)Ctor創(chuàng)建組件的VNode
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(tag, data, children, undefined, undefined, context);
}
} else {
// direct component options / constructor
// 如果tag不是字符串锻离,即代表其是一個組件
// 創(chuàng)建組件的VNode
vnode = createComponent(tag, data, context, children);
}
if (isDef(vnode)) {
if (ns) applyNS(vnode, ns);
return vnode;
} else {
return createEmptyVNode();
}
}
normalizeChildren
export function normalizeChildren(children: any): ?Array<VNode> {
// 如果children是原始值铺峭,則創(chuàng)建文本虛擬DOM節(jié)點,并返回
// 如果是數(shù)組纳账,使用normalizeArrayChildren方法逛薇,遞歸children并創(chuàng)建的文本虛擬DOM節(jié)點的一維數(shù)組,并返回
// 否則返回undefined
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined;
}
normalizeArrayChildren
function normalizeArrayChildren(
children: any,
nestedIndex?: string,
): Array<VNode> {
const res = [];
let i, c, lastIndex, last;
for (i = 0; i < children.length; i++) {
c = children[i];
// 如果c為undefined或者null疏虫,或者是Boolean類型永罚,則跳過該循環(huán),執(zhí)行下一次循環(huán)
if (isUndef(c) || typeof c === 'boolean') continue;
lastIndex = res.length - 1;
last = res[lastIndex];
// nested
// 如果c是一個數(shù)組卧秘,遞歸
if (Array.isArray(c) && c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`);
// merge adjacent text nodes
// 合并相鄰的文本節(jié)點
if (isTextNode(c[0]) && isTextNode(last)) {
// 創(chuàng)建文本虛擬DOM節(jié)點
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text);
// 刪除數(shù)組的第一個元素
c.shift();
}
// 合并兩個數(shù)組
res.push.apply(res, c);
} else if (isPrimitive(c)) {
// 如果c是原始值
// r如果last是文本節(jié)點
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
// 合并相鄰的文本節(jié)點
// 這對于SSR水化來說是必需的
// 因為文本節(jié)點在呈現(xiàn)為HTML字符串時基本上已經(jīng)合并
res[lastIndex] = createTextVNode(last.text + c);
} else if (c !== '') {
// convert primitive to vnode
// 創(chuàng)建文本虛擬DOM節(jié)點呢袱,并添加到res數(shù)組中
res.push(createTextVNode(c));
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
// 合并相鄰的文本節(jié)點
res[lastIndex] = createTextVNode(last.text + c.text);
} else {
// default key for nested array children (likely generated by v-for)
if (
isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)
) {
c.key = `__vlist${nestedIndex}_${i}__`;
}
res.push(c);
}
}
}
return res;
}
update
update 方法的作用是通過 patch 方法把 VNode 渲染成真實的 DOM
首次渲染和數(shù)據(jù)更新都會調(diào)用_update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
const prevEl = vm.$el;
const prevVnode = vm._vnode;
const prevActiveInstance = activeInstance;
activeInstance = vm;
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 如果當(dāng)前vue實例不存在vnode,則代表是首次渲染
if (!prevVnode) {
// initial render
// 這時使用vm.__patch__方法傳入真實DOM(vm.$el)翅敌,并轉(zhuǎn)換為虛擬DOM羞福,與傳入的vnode進(jìn)行比較
// 返回真實DOM賦值給vm.$el
vm.$el = vm.__patch__(
vm.$el,
vnode,
hydrating,
false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm,
);
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null;
} else {
// updates
// 使用vm.__patch__方法傳入新舊vnode進(jìn)行比較
// 返回真實DOM賦值給vm.$el
vm.$el = vm.__patch__(prevVnode, vnode);
}
activeInstance = prevActiveInstance;
// 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;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
patch
功能
- 如果是首次渲染的話,會把真實 DOM 先轉(zhuǎn)換成
VNode - 傳入新舊 VNode蚯涮,對比差異治专,把差異渲染到 DOM
- 返回新的 VNode 的真實 DOM
patch 初始化過程
// __patch__方法將虛擬dom轉(zhuǎn)換為真實dom
Vue.prototype.__patch__ = inBrowser ? patch : noop;
/* @flow */
// nodeOps里是各種DOM操作函數(shù)
import * as nodeOps from 'web/runtime/node-ops';
import { createPatchFunction } from 'core/vdom/patch';
// 指令和鉤子函數(shù)
import baseModules from 'core/vdom/modules/index';
// DOM節(jié)點的屬性/事件/樣式的操作
import platformModules from 'web/runtime/modules/index';
// the directive module should be applied last, after all
// built-in modules have been applied.
// 合并指令和鉤子函數(shù)和DOM節(jié)點的屬性/事件/樣式的操作
const modules = platformModules.concat(baseModules);
export const patch: Function = createPatchFunction({ nodeOps, modules });
export function createPatchFunction(backend) {
let i, j;
const cbs = {};
// modules 節(jié)點的屬性/事件/樣式的操作
// nodeOps 節(jié)點操作
const { modules, nodeOps } = backend;
for (i = 0; i < hooks.length; ++i) {
// 初始化create、activate遭顶、update张峰、remove、destroy鉤子函數(shù)回調(diào)數(shù)組
// cbs['update'] = []
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
// 把模塊中的鉤子函數(shù)全部設(shè)置到 cbs 中棒旗,將來統(tǒng)一觸發(fā)
// cbs['update'] = [updateAttrs, updateClass, update...]
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
}
...
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {...}
}
patch 執(zhí)行過程
function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 如果新的虛擬節(jié)點不存在喘批,并且舊的虛擬節(jié)點存在,則執(zhí)行destroy鉤子函數(shù)铣揉,并直接返回饶深,阻止向下執(zhí)行
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
// 新插入虛擬節(jié)點隊列數(shù)組
const insertedVnodeQueue = [];
// 如果舊的虛擬節(jié)點不存在
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 標(biāo)記當(dāng)前虛擬節(jié)點已創(chuàng)建,只存儲在內(nèi)存中逛拱,未掛載到DOM樹上
isInitialPatch = true;
// 將新的虛擬節(jié)點轉(zhuǎn)換為真實DOM
createElm(vnode, insertedVnodeQueue, parentElm, refElm);
} else {
// 如果存在nodeType敌厘,則是真實DOM
const isRealElement = isDef(oldVnode.nodeType);
// 如果不是真實DOM,并且新舊虛擬節(jié)點相同
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 調(diào)用patchVnode朽合,通過diff算法俱两,對比新舊節(jié)點的差異,并更新
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
} else {
// 否則
// 如果是真實DOM
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
// 如果是元素節(jié)點旁舰,并且該節(jié)點存在data-server-rendered屬性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
// 移除data-server-rendered屬性
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode;
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.',
);
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 將真實DOM轉(zhuǎn)換為虛擬節(jié)點并賦值給舊的虛擬節(jié)點
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
// 獲取舊的虛擬節(jié)點的真實DOM元素
const oldElm = oldVnode.elm;
// 獲取舊的虛擬節(jié)點的父元素節(jié)點
const parentElm = nodeOps.parentNode(oldElm);
// 調(diào)用createElm方法將新的虛擬節(jié)點轉(zhuǎn)換為真實DOM,并掛載到舊的虛擬節(jié)點的父元素節(jié)點上
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm),
);
if (isDef(vnode.parent)) {
// component root element replaced.
// update parent placeholder node element, recursively
let ancestor = vnode.parent;
const patchable = isPatchable(vnode);
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// 如果存在舊的虛擬節(jié)點的父元素節(jié)點
if (isDef(parentElm)) {
// 移除DOM樹上對應(yīng)的舊虛擬節(jié)點的真實DOM節(jié)點
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
// 如果不存在舊的虛擬節(jié)點的父元素節(jié)點嗡官,并且存在tag
// 觸發(fā)destroy鉤子函數(shù)
invokeDestroyHook(oldVnode);
}
}
}
// 遍歷insertedVnodeQueue數(shù)組箭窜,執(zhí)行insert鉤子函數(shù)
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
}
createElm
將虛擬節(jié)點轉(zhuǎn)換為真實 DOM,并掛載到 DOM 樹上
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;
// 如果是標(biāo)簽節(jié)點
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
inPre++;
}
if (
!inPre &&
!vnode.ns &&
!(
config.ignoredElements.length &&
config.ignoredElements.some((ignore) => {
return isRegExp(ignore) ? ignore.test(tag) : ignore === tag;
})
) &&
config.isUnknownElement(tag)
) {
warn(
'Unknown custom element: <' +
tag +
'> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context,
);
}
}
// 如果存在ns屬性衍腥,創(chuàng)建一個具有指定的命名空間URI和限定名稱的元素磺樱,否則創(chuàng)建創(chuàng)建元素纳猫,并賦值給vnode.elm
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
// 設(shè)置樣式的作用域
setScope(vnode);
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree);
if (!appendAsTree) {
if (isDef(data)) {
// 觸發(fā)create鉤子函數(shù)
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// 將元素節(jié)點掛載到父元素節(jié)點上
insert(parentElm, vnode.elm, refElm);
}
// 創(chuàng)建子節(jié)點真實DOM元素
createChildren(vnode, children, insertedVnodeQueue);
if (appendAsTree) {
if (isDef(data)) {
// 觸發(fā)create鉤子函數(shù)
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// 將元素節(jié)點掛載到父元素節(jié)點上
insert(parentElm, vnode.elm, refElm);
}
} else {
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
inPre--;
}
} else if (isTrue(vnode.isComment)) {
// 如果是注釋節(jié)點
// 創(chuàng)建注釋節(jié)點,并賦值給vnode.elm
vnode.elm = nodeOps.createComment(vnode.text);
// 將元素節(jié)點掛載到父元素節(jié)點上
insert(parentElm, vnode.elm, refElm);
} else {
// 否則
// 創(chuàng)建文本節(jié)點竹捉,并賦值給vnode.elm
vnode.elm = nodeOps.createTextNode(vnode.text);
// 將元素節(jié)點掛載到父元素節(jié)點上
insert(parentElm, vnode.elm, refElm);
}
}
patchVnode
對比新舊節(jié)點的差異芜辕,并更新
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新舊虛擬節(jié)點相等,直接返回块差,阻止向下執(zhí)行
if (oldVnode === vnode) {
return;
}
const elm = (vnode.elm = oldVnode.elm);
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return;
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
let i;
const data = vnode.data;
// 執(zhí)行用戶傳過來的鉤子函數(shù)
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
// 獲取舊虛擬節(jié)點的子節(jié)點
const oldCh = oldVnode.children;
// 獲取新虛擬節(jié)點的子節(jié)點
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
// 執(zhí)行update鉤子函數(shù)侵续,操作節(jié)點的屬性/樣式/事件....
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
// 執(zhí)行用戶自定義的鉤子函數(shù)
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
// 如果新虛擬節(jié)點不存在text屬性
if (isUndef(vnode.text)) {
// 新舊虛擬節(jié)點都存在子節(jié)點
if (isDef(oldCh) && isDef(ch)) {
// 如果新舊虛擬節(jié)點的子節(jié)點不一致,調(diào)用 updateChildren方法憨闰,對子節(jié)點進(jìn)行 diff 操作状蜗,并更新
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
} else if (isDef(ch)) {
// 如果新虛擬節(jié)點存在子節(jié)點,舊虛擬節(jié)點不存在子節(jié)點
// 如果舊虛擬節(jié)點存在text屬性鹉动,清空舊節(jié)點 DOM 的文本內(nèi)容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
// 為子節(jié)點創(chuàng)建真實DOM元素轧坎,并掛載到DOM樹上
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 如果舊虛擬節(jié)點存在子節(jié)點,新虛擬節(jié)點不存在子節(jié)點
// 移除舊虛擬節(jié)點的子節(jié)點
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 如果新舊虛擬節(jié)點不存在子節(jié)點泽示,并且舊虛擬節(jié)點存在text屬性
// 清空舊節(jié)點 DOM 的文本內(nèi)容
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 如果新舊虛擬節(jié)點的text屬性都存在缸血,并且不一致
// 修改文本內(nèi)容
nodeOps.setTextContent(elm, vnode.text);
}
// 觸發(fā)用戶傳入的postpatch鉤子函數(shù)
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}
}
updateChildren
該方法與 Snabbdom 中的 updateChildren 整體算法 一致。
在 patch 函數(shù)中械筛,調(diào)用 patchVnode 之前捎泻,會首先調(diào)用 sameVnode()判斷當(dāng)前的新舊虛擬節(jié)點是否是相同節(jié)點,sameVnode() 中會首先判斷 key 是否相同变姨。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>key</title>
</head>
<body>
<div id="app">
<button @click="handler">按鈕</button>
<ul>
<!-- <li v-for="value in arr">{{value}}</li> -->
<li v-for="value in arr" :key="value">{{value}}</li>
</ul>
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
arr: ['a', 'b', 'c', 'd'],
},
methods: {
handler() {
this.arr.splice(1, 0, 'x');
// this.arr = ['a', 'x', 'b', 'c', 'd']
},
},
});
</script>
</body>
</html>
當(dāng)沒有設(shè)置 key 的時候族扰,在 updateChildren 中比較子節(jié)點的時候,會做三次更新 DOM 操作和一次插入 DOM 的操作
當(dāng)設(shè)置 key 的時候定欧,在 updateChildren 中比較子節(jié)點的時候渔呵,因為 oldVnode 的子節(jié)點的 b,c,d 和 newVnode 的 b,c,d 的 key 相同,所以只做比較砍鸠,沒有更新 DOM 的操作扩氢,當(dāng)遍歷完畢后,會再把 x 插入到 DOM 上爷辱,DOM 操作只有一次插入操作录豺。