原本打算看下virtualDOM的實現原理,但看到許多文章都只是在講原理,不多有對vDOM庫的源碼的分析,今天打算嘗試着從本身的角度出發,寫一篇源碼解析的文章css
首先請出今天的主角——Vue2的vDOM所基於的庫,snabbdom,github地址以下html
GitHub: github.com/snabbdom/sn…vue
首先咱們來看下他的類型定義node
vNode類型react
VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
hero?: Hero;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: Array<any>; // for thunks
[key: string]: any; // for any other 3rd party module
}
// Recode的含義(至關於定義了key和value的類型)
// const user: Record<'name'|'email', string> = {
// name: '',
// email: ''
// }
type Props = Record<string, any>;
type Classes = Record<string, boolean>
type Attrs = Record<string, string | number | boolean>
interface Hooks {
pre?: PreHook;
init?: InitHook;
create?: CreateHook;
insert?: InsertHook;
prepatch?: PrePatchHook;
update?: UpdateHook;
postpatch?: PostPatchHook;
destroy?: DestroyHook;
remove?: RemoveHook;
post?: PostHook;
}
複製代碼
能夠看到snabbdom定義的虛擬dom節點並不像許多Vue裏面所定義的同樣, 他有一系列的符合咱們認知的諸如class,attrs等屬性,但同時他又給咱們提供了hook,讓咱們能夠在更新節點是對他進行操做git
先看下官方給咱們的示例github
var snabbdom = require('snabbdom')
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var toVNode = require('snabbdom/tovnode').default;
var newVNode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
patch(toVNode(document.querySelector('.container')), newVNode)
複製代碼
很方便,定義一個節點以及一個更新時函數就能夠正常使用了,下面咱們來看下具體這些方法都作了什麼api
function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}, children: any, text: any, i: number;
if (c !== undefined) {
data = b;
if (is.array(c)) { children = c; }
else if (is.primitive(c)) { text = c; }
else if (c && c.sel) { children = [c]; }
} else if (b !== undefined) {
if (is.array(b)) { children = b; }
else if (is.primitive(b)) { text = b; }
else if (b && b.sel) { children = [b]; }
else { data = b; }
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined);
};
// addNs
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg';
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
}
// vnode
function vnode(sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode {
let key = data === undefined ? undefined : data.key;
return {sel, data, children, text, elm, key};
}
複製代碼
能夠看到所作的無非就是對你的輸入作一些判斷(可變參數),以及對一些擁有本身特殊的命名空間(svg)的元素的處理數組
init接受插件和可選的domAPI屬性,返回一個函數用於更新dombash
init(modules: Array<Partial<Module>>, domApi?: DOMAPI)
複製代碼
第一個參數接受一系列插件用於更新dom
// Partial 將全部類型標記爲可選屬性
interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
複製代碼
看一個插件的源碼
import {VNode, VNodeData} from '../vnode';
import {Module} from './module';
export type Classes = Record<string, boolean>
function updateClass(oldVnode: VNode, vnode: VNode): void {
var cur: any, name: string, elm: Element = vnode.elm as Element,
oldClass = (oldVnode.data as VNodeData).class,
klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
for (name in oldClass) {
if (!klass[name]) {
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? 'add' : 'remove'](name);
}
}
}
export const classModule = {create: updateClass, update: updateClass} as Module;
export default classModule;
複製代碼
插件是在patch函數運行時的提供的各個hook對dom進行實際操做的動做 那麼插件是怎麼裝載進patch的呢?咱們再來看一下init函數具體操做了什麼
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number, j: number, cbs = ({} as ModuleHooks);
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
for (i = 0; i < hooks.length; ++i) { // 把鉤子函數放進一個數組,用閉包存起來
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
}
// ...
return function patch() {
// ...
}
}
複製代碼
就是用閉包把這些方法存起來,在運行時再一一調用
再看patch函數(由init方法返回的用於更新dom的函數)
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 執行鉤子: pre
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 執行鉤子: post
return vnode;
};
複製代碼
以後咱們再看下pathVnode進行了什麼操做
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, hook: any;
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
i(oldVnode, vnode); // 執行鉤子: prepatch(定義在VNode上)
}
const elm = vnode.elm = (oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); // 執行鉤子: update
i = vnode.data.hook;
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
}
//
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
}
api.setTextContent(elm, vnode.text as string);
}
if (isDef(hook) && isDef(i = hook.postpatch)) { // 執行鉤子: postpatch(定義在VNode上)
i(oldVnode, vnode);
}
}
複製代碼
咱們最後來看下snabbdom是怎麼處理子元素的更新的,能夠總結爲:
function updateChildren(parentElm: Node,
oldCh: Array<VNode>,
newCh: Array<VNode>,
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0, 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: any;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]; // 以上四個都是對空元素的處理
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx]; // 以上兩個則是對元素移動狀況的處理
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) { // 判斷新的vNode是否存在舊的vNode的中,執行新增或者移動的操做
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
newStartVnode = newCh[++newStartIdx];
} else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
}
newStartVnode = newCh[++newStartIdx];
}
}
// ...
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
複製代碼
以上就是snabbdom在對節點進行更新時的主要操做,能夠概括爲
瞭解snabbdom的行爲後,咱們能夠進行簡單(不考慮特殊狀況,只簡單實現功能)的仿寫來練練手以及加深理解
const NODE_KEY = Symbol('vNode')
type Style = {
[key: string]: string
}
export type vNodeModal = {
tag: string
class?: string
id?: string
style?: Style
[NODE_KEY]: string
elem?: Element
children?: Array<vNodeModal | string>
}
複製代碼
這裏我用symbol來作惟一的標示方便準確判斷是不是vNode以及vNode是否相同
export const isVNode = (elem: vNodeModal | Element) => Boolean(elem && elem[NODE_KEY])
export const isSameNode = (node: vNodeModal, otcNode: vNodeModal) => node[NODE_KEY] === otcNode[NODE_KEY]
複製代碼
我把tag定義爲的必填屬性,key爲的私有屬性,由我來幫它建立
const constructVNode = function(data: Partial<vNodeModal> & { tag: string }) {
return {
...data,
[NODE_KEY]: uuid()
}
}
複製代碼
我把的更新處理函數稱爲plugin,好理解一些,因此plugin和這個簡單的vNode庫是毫無關係的,純粹由外部提供
const init = function (plugins = []) {
if (!plugins || !plugins.length) return null
// 把hook存起來
hooks.forEach(function(hook) {
plugins.forEach(function(plugin) {
if (plugin[hook]) {
handler[hook] ? handler[hook].push(plugin[hook]) : handler[hook] = [plugin[hook]]
}
})
})
return function(ctrlNode: Element | vNodeModal, newVNode: vNodeModal) {
let oldVNode = ctrlNode
if (!isVNode(ctrlNode)) oldVNode = transformToVNode(ctrlNode as Element)
if (handler.pre) {
handler.pre.map((preHandle) => { preHandle(oldVNode, newVNode) })
}
updateNode(oldVNode as vNodeModal, newVNode)
if (handler.finish) {
handler.finish.map((finishHandle) => { finishHandle(oldVNode, newVNode) })
}
return newVNode
}
}
複製代碼
接下來是更新處理判斷的函數
// 簡單判斷不是同一個vNode節點或者tag變動了就直接所有更新
const updateNode = function(oldVNode: vNodeModal, newVNode: vNodeModal) {
if (!isSameVNode(oldVNode as vNodeModal, newVNode) || isTagChange(oldVNode, newVNode)) {
const newElement = createDOMByVNode(newVNode)
oldVNode.elem.replaceWith(newElement)
} else {
updateVNodeByModal(oldVNode, newVNode)
}
}
// 根據VNode去更新dom
const updateVNodeByModal = function(oldVNode: vNodeModal, newVNode: vNodeModal) {
if (handler.update.length) {
handler.update.forEach((updateHandle) => { updateHandle(oldVNode, newVNode) })
}
// 更新完元素自己後對子元素進行處理
const oCh = oldVNode.children || []
const nCh = newVNode.children || []
if (oCh.length && !nCh.length) {
removeAllChild(oldVNode.elem)
} else if (!oCh.length && nCh.length) {
inertNode(newVNode.elem, nCh)
} else if (oCh.length && nCh.length) {
diff(oldVNode, newVNode)
for(let i = 0; i < nCh.length; i++) {
if (isVNode(nCh[i])) {
const idx = oCh.findIndex((oChild) => isSameVNode(nCh[i], oChild))
if (idx > - 1) updateNode(oCh[idx] as vNodeModal, nCh[i] as vNodeModal)
}
}
}
}
// 對子元素的diff
const diff = function(oldVNode: vNodeModal, newVNode: vNodeModal) {
// 具體處理
const oCh = oldVNode.children
const nCh = newVNode.children
const nLen = nCh.length
let lastIdx = 0
const getIndex = function(checkArray: Array<vNodeModal | string>, item: vNodeModal | string) {
if (isVNode(item)) {
return checkArray.findIndex(o => isSameVNode(o as vNodeModal, item as vNodeModal))
} else {
return checkArray.findIndex(o => o === item)
}
}
// 參考react的diff策略,但字符串不考慮
for (let i = 0; i < nLen; i++) {
const oldIdx = getIndex(oCh, nCh[i])
if (oldIdx > -1) {
if (oldIdx < lastIdx) {
if (typeof oCh[oldIdx] === 'string') {
oldVNode.elem.childNodes[oldIdx].remove()
}
getElement(oCh[i]).after(getElement(oCh[oldIdx]))
}
lastIdx = Math.max(oldIdx, lastIdx)
} else {
const newElem = createDOMByVNode(nCh[i])
if (i === 0) (oldVNode as vNodeModal).elem.parentElement.prepend(newElem)
else {
if (typeof nCh[i] === 'string') (oldVNode as vNodeModal).elem.childNodes[i].after(newElem)
else getElement(nCh[i]).after(newElem)
}
}
}
for (let i = 0; i < oldVNode.children.length; i++) {
const idx = getIndex(nCh, oCh[i])
if (idx < 0) {
if (typeof oCh[i] === 'string') {
oldVNode.elem.childNodes[i].remove()
} else {
(oCh[i] as vNodeModal).elem.remove()
}
}
}
}
複製代碼
再來寫一個用於更新class的插件
const getClassList = (className: string) => className ? className.split('.') : []
const updateClassName = function (oldVNode: vNodeModal, newVNode: vNodeModal) {
const elem = newVNode.elem
if (!elem) return
const oldClassList = getClassList(oldVNode.class)
const newClassList = getClassList(newVNode.class)
if (!newClassList.length) return
oldClassList.forEach((className) => {
if (!newClassList.includes(className)) {
elem.classList.remove(className)
} else {
newClassList.splice(newClassList.indexOf(className), 1)
}
})
newClassList.forEach((className) => elem.classList.add(className))
}
const updateClassPlugin = {
update: updateClassName
}
複製代碼
使用的時候這麼寫
import init from './tools/init'
import transFromClass from './tools/plugins/class'
import './style.css'
const inp1 = document.querySelector('#first')
const newV = constructVNode({
tag: 'div',
class: 'haha.mama',
id: 'no',
children: [
'lalala',
constructVNode({
tag: 'input',
class: 'asdad',
id: '123'
})
]
})
// 插入子元素
const patch = init([transFromClass])
let newModal = patch(inp1, newV)
// 交換子元素位置
setTimeout(() => {
const changPosModal = {
...newModal,
children: [newModal.children[1], newV.children[0]]
}
newModal = patch(newModal, changPosModal)
}, 500)
// 修改子元素屬性
setTimeout(() => {
const newChildren0 = {
...newModal.children[0] as vNodeModal,
class: 'newChildren0'
}
const changClassModal = {
...newModal,
children: [newChildren0, newModal.children[1] + 'juejin']
}
newModal = patch(newModal, changClassModal)
}, 1000)
// 刪除子元素
setTimeout(() => {
const deleteChildrenModal = {
...newModal,
children: []
}
newModal = patch(newModal, deleteChildrenModal)
}, 1500)
複製代碼
最後看看結果:
這樣,就實現了一個很是簡單vDOM的處理(缺失對邊界的處理,特殊元素處理等)
snabbdom作的最主要的事情就是使dom的結構變得更加清晰容易掌控,在咱們更新dom元素時,幫助咱們進行了一系列操做優化處理,封裝了實際操做邏輯。以及提供了一系列插件可供咱們使用。
這是本人的第一次寫這樣的文章,寫得有很差的地方歡迎你們批評指證!😄