Vue.js 框架最为核心的 Virtual DOM 是如何实现的?
1、虚拟DOM
Virtual DOM :由普通的js对象来描述DOM对象,因为不是真实的DOM对象,所以叫虚拟DOM
Snabbdom 就是Vue使用的虚拟DOM框架
1)真实DOM成员
let element =document.querySelector('#app') let s='' for(let key in element){ s += key +',' } //输出的就是真实的DOM console.log(s)
2)可以使用Virtual DOM 来描述真实DOM
示例: { sel:"div", data:{}, children:undefined, text:"Hello Virtual DOM", elm:undefined, key:undefined }
如上对比,创建一个虚拟的DOM的成本远远小于真实的DOM
2、为什么使用Virtual DOM
虚拟DOM的作用
Virtual DOM的开源库
3、Snabbdom的基本使用
因为我使用的是npm 所以和教程步骤不一样 npm install parcel-bundler //snabbdom要安装指定版本才能使用本教程demo npm i snabbdom@0.7.4 安装完成后,根据图示添加 "scripts": { "dev": "parcel index.html --open", //--open 浏览器自动打开 "build": "parcel build index.html" }, 下面是两个解决snabbdom更新后兼容问题 https://www.jianshu.com/p/cff844f497a4 https://blog.csdn.net/tiandao451/article/details/107163507/
Snabbdom的使用
01-basicusage.js // 导入snabbdom import { h,init } from 'snabbdom' // 参数:数组 模块 // 返回值:patch 函数,作用对比两个VNode的差异更新到真实DOM let patch=init([]) // 第一个参数:标签+选择器 // 第二个参数:如果是字符串的话就是标签中的内容 let vnode =h("div#container.cls",'Hello world') //自己创建的vnode let app=document.querySelector('#app') //初始的vnode // 第一个参数:可以是DOM 元素,内部会把DOM元素转换成vnode // 第二个参数:Vnode // 返回值:Vnode let oldVnode =patch(app,vnode) // 假设时刻 vnode=h('div','hello snabbdom') patch(oldVnode,vnode) console.log(patch,vnode,oldVnode)
02-basicusage.js // div中放置子元素h1,p import {h,init} from 'snabbdom' let patch=init([]) let vnode=h('div#container',[ h('h1','hello snabbdom'), h('p','这是一个p标签') ]) let app=document.querySelector('#app') // 使用patch把虚拟DOM更新至真实DOM let oldNode=patch(app,vnode) setTimeout(()=>{ vnode=h("div#container",[ h("h1","3秒后的更新"), h('p',"hello new text") ]) patch(oldNode,vnode) // 清空页面元素 // 1 错误做法 // patch(oldNode,null) // 2 创建一个注释节点 h('!') patch(oldNode,h('!')) },3000)
4、Snabbdom的模块
03-modules.js // 1、导入模块 import {h,init} from 'snabbdom' import style from 'snabbdom/modules/style' import eventlistenters from 'snabbdom/modules/eventlisteners' // 2、注册模块 let patch=init([ style, eventlistenters ]) // 3、使用h()函数的第二个参数传入模块需要的数据(对象) // 设置行内样式和注册事件 let vnode=h('div#container',{ style:{ backgroundColor:'red' }, on:{ click:eventHandler } },[ h('h1','hello snabbdom'), h('p','这是一个p标签') ]) function eventHandler(){ console.log('点击我了') } let app=document.querySelector('#app') // 使用patch把虚拟DOM更新至真实DOM patch(app,vnode)
3、Snabbdom的源码解析
如何学习源码
先宏观了解
带着目标看源码
看源码的过程中要不求甚解
调试
参考资料
Snabbdom的核心
1)使用h()函数创建Javascript对象(VNode)描述真实DOM
2)init()设置模块,创建patch()
3)patch()比较新旧两个VNode (旧的VNode在前,新的VNode在后)
4)把变化的内容更新到真实DOM树上
Snabbdom源码地址
https://github.com/snabbdom/snabbdom
重载的意思就是可以存在同名函数,根据传入参数的个数不同调用不同的函数。
c或者c++才有重载,js没有,ts的重载就和此例子一样的实现方法
function add(a,b,c){ if(c == undefined){ console.log(a+b) }else{ console.log(a+b+c) } } add(1,2) //3 add(1,2,3)//6
h.ts
//h()函数核心是 调用vnode函数,创建虚拟节点 export function h(sel: string): VNode; export function h(sel: string, data: VNodeData): VNode; export function h(sel: string, children: VNodeChildren): VNode; export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode; export function h(sel: any, b?: any, c?: any): VNode { var data: VNodeData = {}, children: any, text: any, i: number; // 处理参数,实现重载的机制 if (c !== undefined) { // 处理三个参数的情况 // sel,data,children/text data = b; // 如果b是数组 if (is.array(c)) { children = c; } // 如果c是字符串或者数字 else if (is.primitive(c)) { text = c; } // 如果c是vnode else if (c && c.sel) { children = [c]; } } else if (b !== undefined) { // 处理两个参数的情况 // 如果b是数组 if (is.array(b)) { children = b; } // 如果b是字符串或者数字 else if (is.primitive(b)) { text = b; } // 如果b是vnode,vnode有sel属性 else if (b && b.sel) { children = [b]; } else { data = b; } } if (children !== undefined) { // 处理children中的原始值(string/number) for (i = 0; i < children.length; ++i) { // 如果child是strin/number,则创建文本节点 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] === '#') ) { // 如果是svg,添加命名空间 addNS(data, children, sel); } return vnode(sel, data, children, text, undefined); };
vnode.ts
export interface VNode { // 选择器 sel: string | undefined; // 节点数据:属性/样式/事件等 data: VNodeData | undefined; // 子节点 和text只能互斥 children: Array<VNode | string> | undefined; // 记录vnode对应的真实DOM elm: Node | undefined; // 节点中的内容,和children只能互斥 text: string | undefined; // 优化用 key: Key | undefined; } export 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}; // vnode最后返回的就是一个描述虚拟DOM的对象 }
vnode渲染真实DOM
init函数
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; export {h} from './h'; export {thunk} from './thunk'; export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) { let i: number, j: number, cbs = ({} as ModuleHooks); // 初始化转换虚拟节点的api const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi; // 把传入的所有模块的钩子函数,统一存储在cbs对象中 // 最终构建的cbs对象的形式 cbs={create:[fn1,fn2],update:[],,,} for (i = 0; i < hooks.length; ++i) { // cbs.create=[],cbs.update=[] cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { // modules传入的模块数组 // 获取模块中的hook函数 // hook=modules[0][create].... const hook = modules[j][hooks[i]]; if (hook !== undefined) { //把获取到的hook函数放入到cbs对应的钩子函数数组中 (cbs[hooks[i]] as Array<any>).push(hook); } } } init 内部返回patch函数,把vnode渲染成真实DOM,并返回vnode return function patch(oldVnode: VNode | Element, vnode: VNode): VNode )
patch函数
// init 内部返回patch函数,把vnode渲染成真实DOM,并返回vnode return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; // 保存新插入节点的队列,为了触发钩子函数 const insertedVnodeQueue: VNodeQueue = []; // 执行模块的pre钩子函数 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 如果oldVnode不是vnode,创建vnode并设置elm if (!isVnode(oldVnode)) { // 把DOM元素转换成空的vnode oldVnode = emptyNodeAt(oldVnode); } // 如果新旧节点是相同节点(key和sel相同) if (sameVnode(oldVnode, vnode)) { // 找节点的差异并更新DOM patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 如果新旧节点不同,vnode创建对应的DOM // 获取当前的DOM元素 elm = oldVnode.elm as Node; parent = api.parentNode(elm); // 创建vnode对应的DOM元素,并触发init/create钩子函数 createElm(vnode, insertedVnodeQueue); if (parent !== null) { // 如果父节点不为空,把vnode对应的DOM插入到文档中 api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm)); // 移除老节点 removeVnodes(parent, [oldVnode], 0, 0); } } // 执行用户设置的insert钩子函数 for (i = 0; i < insertedVnodeQueue.length; ++i) { (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]); } // 执行模块的post钩子函数 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 返回vnode return vnode; };
createElm函数
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any, data = vnode.data; if (data !== undefined) { // 执行用户设置的init钩子函数 if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); data = vnode.data; } } // 把vnode转换成真实DOM对象(没有渲染到页面) let children = vnode.children, sel = vnode.sel; if (sel === '!') { // 如果选择器是!,则创建注释节点 if (isUndef(vnode.text)) { vnode.text = ''; } vnode.elm = api.createComment(vnode.text as string); } else if (sel !== undefined) { // 如果选择器不为空 // 解析选择器 // Parse selector const hashIdx = sel.indexOf('#'); const dotIdx = sel.indexOf('.', hashIdx); const hash = hashIdx > 0 ? hashIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length; const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag) : api.createElement(tag); if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)); if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')); // 执行模块的create钩子函数 for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); // 如果vnode中有子节点,创建子vnode对应的DOM元素并追加到DOM树上。 if (is.array(children)) { for (i = 0; i < children.length; ++i) { const ch = children[i]; if (ch != null) { api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue)); } } } else if (is.primitive(vnode.text)) { // 如果vnode的text值是string/number,创建文本节点并追加到DOM树 api.appendChild(elm, api.createTextNode(vnode.text)); } i = (vnode.data as VNodeData).hook; // Reuse variable if (isDef(i)) { // 执行用户传入的钩子create if (i.create) i.create(emptyNode, vnode); // 把vnode添加到队列中,为后续执行insert钩子做准备 if (i.insert) insertedVnodeQueue.push(vnode); } } else { // 如果选择器为空,创建文本节点 vnode.elm = api.createTextNode(vnode.text as string); } // 返回创建的DOM return vnode.elm; }
addVnodes和removeVnodes函数
function createRmCb(childElm: Node, listeners: number) { // 返回删除元素的回调函数 return function rmCb() { if (--listeners === 0) { const parent = api.parentNode(childElm); api.removeChild(parent, childElm); } }; } function addVnodes(parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); } } } function invokeDestroyHook(vnode: VNode) { let i: any, j: number, data = vnode.data; if (data !== undefined) { // 执行用户设置的destroy的钩子函数 if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode); // 调用模块的destroy钩子函数 for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); // 执行子节点的destroy的钩子函数 if (vnode.children !== undefined) { for (j = 0; j < vnode.children.length; ++j) { i = vnode.children[j]; if (i != null && typeof i !== "string") { invokeDestroyHook(i); } } } } } function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void { for (; startIdx <= endIdx; ++startIdx) { let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx]; if (ch != null) { // 如果sel有值 if (isDef(ch.sel)) { // 执行destroy钩子函数,会执行所有子节点的destroy钩子函数 invokeDestroyHook(ch); listeners = cbs.remove.length + 1; // 创建删除的回调函数 rm = createRmCb(ch.elm as Node, listeners); for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); // 执行用户设置的remove钩子函数 if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) { i(ch, rm); } else { // 如果没有用户钩子函数,直接调用删除元素的方法 rm(); } } else { // Text node // 如果是文本节点,直接调用删除元素的方法 api.removeChild(parentElm, ch.elm as Node); } } } }
patchVnode函数
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { let i: any, hook: any; // 首先执行用户设置的prepatch钩子函数 if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); } const elm = vnode.elm = (oldVnode.elm as Node); let oldCh = oldVnode.children; let ch = vnode.children; // 如果新老vnode相同直接返回 if (oldVnode === vnode) return; if (vnode.data !== undefined) { // 执行模块的update钩子函数 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); } // 如果vnode.text未定义 if (isUndef(vnode.text)) { // 如果新老节点都有children if (isDef(oldCh) && isDef(ch)) { // 使用diff算法对比子节点,更新子节点 if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue); } else if (isDef(ch)) { // 如果新节点有children,老节点没有children // 如果老节点有text,清空dom元素的内容 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) { // 如果没有设置vnode.text if (isDef(oldCh)) { // 如果老节点有children,移出 removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1); } // 设置DOM元素的textContent为vnode.text api.setTextContent(elm, vnode.text as string); } // 最后执行用户设置的postpatch 钩子函数 if (isDef(hook) && isDef(i = hook.postpatch)) { i(oldVnode, vnode); } }