前情提要
上篇地址:[造轮子]在学习过Snabbdom的源码后,自己实现一个虚拟DOM库(上)
原本没有打算分上下篇的,结果写作过程中发现篇幅超出预期了,想想还是分成了2篇。
上篇主要讲 h 函数和 modules 模块相关,那么今天继续来研究Snabbdom中另一个重点 patch 函数。
init.ts
和之前一样,首先在src目录创建init.ts文件。根据之前的案例,我们可以发现 patch 函数其实时由 init 函数返回的。
所以在看 patch 函数之前,我们先了解 init 函数的功能。
在Snabbdom中,init 函数接受需要使用的模块做为参数:
在Terdom中,我们省略了按需引用的功能:
所以,在init.ts直接引入所有Moudules
我们在modules文件夹下创建index.ts文件,在文件中引入所有模块,并统一进行导出
init
回到init.ts中,我们进入 init 函数,首先定义一个变量api用于缓存操作dom的相关api
我们在src目录下创建htmldomapi.ts文件,在init.ts中进行引入
这个htmlDomApi其实就是对DOM API进行了一层封装,新增几个判断方法,这里就不细讲了。
// 暂时不支持SVG相关操作
export interface DOMAPI {
// 创建元素节点
createElement: (tagName: any) => HTMLElement
// 创建文本节点
createTextNode: (text: string | number) => Text
// 创建注释节点
createComment: (text: string | number) => Comment
// 在指定的已有子节点之前插入新的子节点。
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void
// 删除子节点
removeChild: (node: Node, child: Node) => void
// 添加子节点
appendChild: (node: Node, child: Node) => void
// 获取元素父节点
parentNode: (node: Node) => Node | null
// 获取元素紧跟的节点
nextSibling: (node: Node) => Node | null
// 获取元素标签名
tagName: (elm: Element) => string
// 设置元素文本内容
setTextContent: (node: Node, text: string | null) => void
// 获取元素文本内容
getTextContent: (node: Node) => string | null
// 判定是否为元素节点
isElement: (node: Node) => node is Element
// 判定是否为文本节点
isText: (node: Node) => node is Text
// 判定是否为注释节点
isComment: (node: Node) => node is Comment
}
function createElement(tagName: any): HTMLElement {
return document.createElement(tagName)
}
function createTextNode(text: string): Text {
return document.createTextNode(text)
}
function createComment(text: string): Comment {
return document.createComment(text)
}
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
parentNode.insertBefore(newNode, referenceNode)
}
function removeChild(node: Node, child: Node): void {
node.removeChild(child)
}
function appendChild(node: Node, child: Node): void {
node.appendChild(child)
}
function parentNode(node: Node): Node | null {
return node.parentNode
}
function nextSibling(node: Node): Node | null {
return node.nextSibling
}
function tagName(elm: Element): string {
return elm.tagName
}
function setTextContent(node: Node, text: string | null): void {
node.textContent = text
}
function getTextContent(node: Node): string | null {
return node.textContent
}
function isElement(node: Node): node is Element {
return node.nodeType === 1
}
function isText(node: Node): node is Text {
return node.nodeType === 3
}
function isComment(node: Node): node is Comment {
return node.nodeType === 8
}
export const htmlDomApi: DOMAPI = {
createElement,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment,
}
其次定义一个变量cbs用于缓存各个模块中的生命周期函数。遍历cbs获取生命周期name,然后遍历modules获取模块中对应的生命周期,如果生命周期有值,将其加入cbs对应的属性中。
最后返回patch函数
所以 init 函数的作用非常简单,就是将操作 DOM API 的变量api和缓存模块生命周期的变量cbs变成闭包,这样每次每次调用 patch 函数时,我们都可以获取到这两个变量的值。
patch
接下来来看 patch 函数,它接受两个参数:旧VNode对象和新VNode对象。所以还需要引入vnode.ts对VNode对象的类型限制。
进入 patch 函数,首先定义了几个变量用于缓存dom节点和父dom节点
然后,判断旧VNode节点是否为VNode节点,当oldVnode不是VNode时,说明是初次加载,创建一个空白VNode。isVnode 函数和 emptyNodeAt 函数我们之后会详细讲。
接着,判断新旧VNode是否为相同VNode节点,如果是更新VNode的差异。如果不是,先获取VNode的dom以及其父dom节点,然后创建dom元素,最后当父节点不为空时,将新创建的dom插入dom树,删除旧VNode节点。sameVnode、patchVnode、createElm和removeVnodes方法我们之后会详细讲。
在 patch 函数最后会返回新VNode节点
isVnode
了解完 patch 函数的逻辑后,我们开始详细了解其中的方法。
先看 isVnode 函数,如果传入参数中有 sel 属性则认为它是VNode数据
emptyNodeAt
接下来,我们来看 emptyNodeAt 函数,它接受一个dom节点参数,进入函数,先获取dom节点的id并加上“ # ”作为 变量 id ,然后获取dom节点的className并加上“ . ”作为变量 class,最后将 dom的标签名、id 和 class拼接作为参数,调用 vnode 函数,返回其返回结果。vnode 函数的详情我们在上篇文章已经详细讲过了。
sameVnode
我们来看 sameVnode 函数,它的逻辑一样非常简单,如果2个VNode节点的key和sel属性都相同,我们就认为它们是相同的VNode节点。
patchVnode
然后,我们来看 patchVnode,它接受两个参数:旧VNode对象和新VNode对象。进入 patchVnode 函数,判断VNode的data属性是否存在,如果存在触发update钩子函数。
首先定义了几个变量分别缓存dom节点、旧VNode的children和新VNode的children。
最后判断新旧VNode节点是否完全相同,如果是直接返回。再判断VNode是否为文本节点,如果不是文本节点,判断新旧VNode是否同时有children,如果同时有children属性,且children属性不相同就通过 updateChildren 函数更新children。
isUndef && isDef
首先,定义NonUndefined类型
updateChildren
OK,终于来到了 patch 函数的重点了,我们来看VNode中是如何更新子节点组的。updateChildren 函数接受3个参数:父dom节点、旧VNode的子节点组和新VNode的子节点组。
然后定义了几个变量,分别是:
我们来看 oldKeyToIdx 对应的 KeyToIndexMap 类型,就是一个 string 为 key,value 为number 的对象
接着,遍历新旧VNode的 children 数组,直到其中之一遍历完。
在遍历过程中,先判断oldStartVnode、oldEndVnode、newStartVnode、newEndVnode是否为 null,因为我们之后可能会对它们重新赋值,可能会出现等于null的情况,如果等于 null,则将对应的索引前移或后移。
接下来,判断旧头VNode和新头VNode是否为相同VNode节点,如果是,将新头VNode差异更新到旧头VNode上,同时将新旧头索引后移。
再判断旧尾VNode和新尾VNode是否为相同VNode节点,如果是,将新尾VNode差异更新到旧尾VNode上,同时将新旧尾索引前移。
然后再判断旧头VNode和新尾VNode是否为相同VNode节点,如果是,将新尾VNode差异更新到旧头VNode上,然后将旧头VNode插入到旧尾VNode后,最后将旧头索引后移,新尾索引前移。
然后再判断旧尾VNode和新头VNode是否为相同VNode节点,如果是,将新头VNode差异更新到旧尾VNode上,然后将旧尾VNode插入到旧头VNode前,最后将旧尾索引前移,新头索引后移。
如果都不是,则先判断有没有创建Key Map,如果没有则通过 createKeyToOldIdx 函数创建Key Map,用于根据key来更新VNode。createKeyToOldIdx 函数我们之后会了解到。
之后,根据Key Map获取当前新头VNode的对应的旧VNode的index
如果 idxInOld 是 undefined,说明旧VNode节点没有设置key属性,直接根据新头VNode创建dom元素,插入到旧头VNode对应dom之前。
否则说明设置了 key 属性,定义一个变量 elmToMove 缓存key对应的旧VNode节点。判断该旧VNode和新头VNode的 sel属性是否相同。如果sel属性不同,则直接创建新VNode对应的dom,插入到旧头VNode对应dom之前。如果sel属性相同,则将新VNode数据更新到旧VNode上,再删除旧children对应的VNode,代表已经更新过,最后创建VNode对应的dom,插入到旧头VNode对应dom之前。
在遍历的最后,将新头VNode设置为新children数组中新头索引后一位。
当循环完成后,当旧头索引小于等于旧尾索引,或新头索引大于等于新尾索引时,说明新旧children其中之一已经遍历完成。再判断旧头索引是否大于旧尾索引,如果是说明新children中新增了VNode节点,先获取新增VNode后一位的VNode对应的dom,再将新增VNode对应dom插入对应dom树位置。否则直接删除多余dom节点
createKeyToOldIdx
看完 updateChildren 函数,我们来看函数中用到的 createKeyToOldIdx 函数。
这个函数接受三个参数:子VNode节点组、开始索引和结束索引,返回一个KeyToIndexMap类型的数据(之前介绍过,一个key为字符串,value为数字的对象)。
进入函数,先定义一个变量map为空对象。循环从开始索引到结束索引,获取子VNode节点组中对应索引的VNode节点的key属性值,如果属性值不为undefined,将其保存在map中,返回map。
createElm
接着来看同样是在 updateChildren 函数中使用到的 createElm 函数,它是用来根据VNode对象创建真实dom的。它接受一个VNode对象作为参数,返回一个 dom节点
进入函数,首先定义了几个变量:
接着判断是否为注释节点,如果是,再判断VNode对象是否有text属性(文本内容),如果没有设置为空字符串,最后根据文本内容创建注释节点
如果不是注解节点,判断 sel属性是否为空。当 sel属性不为空时,先解析标签和选择器,并将它们缓存到变量中:
当" # "的位置小于" . "的位置时,说明存在id,给dom节点添加id。当" . "的索引大于0时,说明存在class,给dom节点添加class:
然后循环调用 cbs 中的create中的方法,即触发各个模块的create生命周期:
接下来,判断该子VNode节点中是否含有孙VNode节点组,如果有,遍历孙VNode节点组,创建孙vnode对应的DOM元素并追加到DOM树上:
如果该子VNode节点没有孙VNode节点组,判断是否包含文本节点,如果有,同样创建并追加到DOM树上:
如果子VNode节点没有 sel属性,说明是文本节点,创建文本节点
在 createElm 函数最后,返回创建好的dom节点
addVnodes
接着来看同样是在 updateChildren 函数中使用到的 addVnodes 函数,它是用来根据VNode对象创建真实dom并添加到父dom的dom树上。它接受5个参数:父dom节点、要插入位置之后的dom节点、子VNode节点组、开始索引和结束节点。
进入函数,首先根据开始索引和结束索引遍历子VNode节点组,创建对应VNode对象的dom节点,再将dom插入到指定的dom元素之前:
removeVnodes
接着来看同样是在 updateChildren 函数中使用到的 removeVnodes 函数,它是用来根据传入的索引来删除父 dom节点上对应的子节点组。它接受4个参数:父 dom节点、子VNode节点组、开始索引和结束索引。
进入函数,首先根据开始索引和结束索引对子VNode数组进行遍历,定义两个变量 rm 和 ch 分别缓存删除dom的方法和当前要删除的节点,然后判断如果 ch 是否存在。如果存在再判断是否为VNode节点。如果是,先通过 invokeDestroyHook 函数触发 modules 的destroy生命周期(之后会详细看到),再通过 createRmCb 函数创建删除 dom 节点的方法,调用该方法。如果不是VNode节点,直接删除。
invokeDestroyHook
下面我们来看在 removeVnodes 函数中调用的 invokeDestroyHook 函数,它的作用是触发 modules 的destroy生命周期。它接受一个VNode节点作为参数。
进入函数,它先设一个变量 data 来缓存VNode节点的data属性。然后判断当 data 不为空时,先遍历触发 cbs 中 destroy 中的函数,再判断VNode节点是否有子节点,如果有,且子节点不为字符串或数字,触发子节点VNode的destroy钩子函数。
createRmCb
下面我们来看同样在 removeVnodes 函数中调用的 createRmCb 函数,它的作用是创建删除子节点dom的方法。它接受一个dom节点作为参数,返回一个 rmCB 函数作为删除该dom节点的方法。
这里是创建一个闭包,对每一个需要删除dom节点形成单独的删除函数,方便之后调用。
完整代码
// 虚拟dom
import { vnode, VNode } from './vnode'
// 引入操作的dom api的相关方法
import { htmlDomApi, DOMAPI } from './htmldomapi'
// 类型判断
import * as is from './is'
// 导入模块
import modules from "./modules/index"
type NonUndefined<T> = T extends undefined ? never : T
type KeyToIndexMap = { [key: string]: number }
// 如果2个VNode节点的key和sel属性都相同,我们就认为它们是相同的VNode节点
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
// 判断是否为Vnode
function isVnode(vnode: any): vnode is VNode {
return vnode.sel !== undefined
}
// 根据children item的key属性,创建Key Map
function createKeyToOldIdx(children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
const map: KeyToIndexMap = {}
for (let i = beginIdx; i <= endIdx; ++i) {
const key = children[i]?.key
if (key !== undefined) {
map[key] = i
}
}
return map
}
// 创建一个空白VNode,用于createElm中触发create钩子函数时调用
const emptyNode = vnode('', {}, [], undefined, undefined)
export function init() {
// 获取操作html dom的api
const api: DOMAPI = htmlDomApi
// 用于缓存各个模块的钩子函数
const cbs = {
create: [],
update: [],
destroy: [],
}
// 遍历cbs
for (let key in cbs) {
// 初始化为数组
cbs[key] = []
// 遍历moudules
for (let module in modules) {
// 获取模块中对应的生命周期
const hook = modules[module][key]
// 如果生命周期有值
if (hook !== undefined) {
// 将其加入cbs对应的属性中
(cbs[key] as any[]).push(hook)
}
}
}
// 创建一个只有标签名和选择器的空白VNode
function emptyNodeAt(elm: Element) {
const id = elm.id ? '#' + elm.id : ''
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}
// 判断是否为undefined
function isUndef(s: any): boolean {
return s === undefined
}
// 判断是否为defined
function isDef<A>(s: A): s is NonUndefined<A> {
return s !== undefined
}
// 根据VNode创建dom
function createElm(vnode: VNode): Node {
// 缓存循环children中的index
let i: number
// 缓存VNode的children
const children = vnode.children
// 缓存VNode的选择器
const sel = vnode.sel
// 把 vnode 转化成真实DOM对象(没有渲染到页面)
if (sel === '!') {
// 如果选择器是!,创建注释节点
if (isUndef(vnode.text)) {
vnode.text = ''
}
vnode.elm = api.createComment(vnode.text!)
} else if (sel !== undefined) {
// 如果选择器不为空,解析选择器
// 缓存id
const hashIdx = sel.indexOf('#')
// 缓存class
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
// 创建dom
const elm = vnode.elm = api.createElement(tag)
// dom设置id
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
// dom设置class
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))
}
}
} else if (is.primitive(vnode.text)) {
// 如果有子文本节点,也创建并追加到DOM树上
api.appendChild(elm, api.createTextNode(vnode.text))
}
} else {
// 如果选择器为空,创建文本节点
vnode.elm = api.createTextNode(vnode.text!)
}
// 返回新创建的DOM
return vnode.elm
}
// 根据VNode新增dom
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
) {
// 遍历传入的VNode数组,创建对应的dom,再将dom插入到指定的dom元素之前
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (ch != null) {
api.insertBefore(parentElm, createElm(ch), before)
}
}
}
// 触发destroy钩子函数
function invokeDestroyHook(vnode: VNode) {
// 获取VNode的data属性
const data = vnode.data
// 当data不为空时
if (data !== undefined) {
// 触发各个模块的destroy钩子函数
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
// 当VNode有子节点,且子节点不为字符串或数字,触发子节点VNode的destroy钩子函数
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j]
if (child != null && typeof child !== 'string' && typeof child !== 'number') {
invokeDestroyHook(child)
}
}
}
}
}
// 创建删除子节点dom的方法
function createRmCb(childElm: Node) {
return function rmCb() {
// 这里是一个闭包将需要删除的dom节点缓存起来,方便之后调用rmCb方法删除
const parent = api.parentNode(childElm) as Node
api.removeChild(parent, childElm)
}
}
// 删除VNode对应dom
function removeVnodes(parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number): void {
for (; startIdx <= endIdx; ++startIdx) {
// 用于缓存删除dom的方法
let rm: () => void
// 获取当前要删除节点
const ch = vnodes[startIdx]
if (ch != null) {
if (isDef(ch.sel)) {
// 触发destroy钩子函数
invokeDestroyHook(ch)
/*
* 在snabbdom中这里需要判断remove钩子函数是否全部调用,而我们terdom没有remove钩子函数,所以
* 不需要判断
*/
rm = createRmCb(ch.elm!)
// 删除子节点
rm()
} else {
// 删除文本节点
api.removeChild(parentElm, ch.elm!)
}
}
}
}
// 更新children
function updateChildren(parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
) {
// 旧VNode的children头索引
let oldStartIdx = 0
// 新VNode的children头索引
let newStartIdx = 0
// 旧VNode的children尾索引
let oldEndIdx = oldCh.length - 1
// 新VNode的children尾索引
let newEndIdx = newCh.length - 1
// 旧VNode的children的头VNode
let oldStartVnode = oldCh[0]
// 旧VNode的children的尾VNode
let oldEndVnode = oldCh[oldEndIdx]
// 新VNode的children的头VNode
let newStartVnode = newCh[0]
// 新VNode的children的尾VNode
let newEndVnode = newCh[newEndIdx]
// 旧VNode的children的Key Map
let oldKeyToIdx: KeyToIndexMap | undefined
// 用于缓存key相同的新旧VNode
let idxInOld: number
// 用于缓存将要移动的旧VNode
let elmToMove: VNode
// 用于缓存将要插入新增VNode的位置之前的VNode
let before: any
// 遍历新旧children数组,直到其中之一遍历完
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 我们之后可能会对oldStartVnode、oldEndVnode、newStartVnode、newEndVnode重新赋值,可能会出现等于null的情况
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]
} 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)) {
// 当旧头VNode和新头VNode是相同VNode时,将新头VNode差异更新到旧头VNode上,同时将新旧头索引后移
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 当旧尾VNode和新尾VNode是相同VNode时,将新尾VNode差异更新到旧尾VNode上,同时将新旧尾索引前移
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
/**
* 当旧头VNode和新尾VNode是相同节点时,将新尾VNode差异更新到旧头VNode上,然后将旧头VNode插入到旧尾VNode后
* 最后将旧头索引后移,新尾索引前移
*/
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
/**
* 当旧尾VNode和新头VNode是相同节点时,将新头VNode差异更新到旧尾VNode上,然后将旧尾VNode插入到旧头VNode前
* 最后将旧尾索引前移,新头索引后移
*/
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果没有Key Map则创建Key Map,用于根据key来更新VNode
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 根据Key Map获取当前新头VNode的对应的旧VNode的index
idxInOld = oldKeyToIdx[newStartVnode.key as string]
if (isUndef(idxInOld)) {
// 如果没有设置key属性,直接根据新头VNode创建dom元素,插入到旧头VNode对应dom之前
api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm!)
} else {
// 如果设置key属性,获取到key对应的旧VNode
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
// 如果旧VNode和新VNode的sel属性不同,则直接创建新VNode对应的dom,插入到旧头VNode对应dom之前
api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm!)
} else {
// 如果sel属性相同,则将新VNode数据更新到旧VNode上
patchVnode(elmToMove, newStartVnode)
// 删除旧children对应的VNode,代表已经更新过
oldCh[idxInOld] = undefined as any
// 创建VNode对应的dom,插入到旧头VNode对应dom之前
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
// 将新头VNode设置为新children数组中新头索引后一位
newStartVnode = newCh[++newStartIdx]
}
}
// 循环完成后,当旧头索引小于等于旧尾索引,或新头索引大于等于新尾索引时,说明新旧children其中之一已经遍历完成
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
// 如果存在旧头索引大于旧尾索引情况,说明新children中新增了VNode
if (oldStartIdx > oldEndIdx) {
// 获取新增VNode后一位的VNode对应的dom
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
// 将新增VNode对应dom插入对应dom树位置
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
} else {
// 删除多余dom节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
// 对比新旧vnode差异,将差异更新到旧VNode上。
function patchVnode(oldVnode: VNode, vnode: VNode) {
// 当VNode的data属性存在时,触发update钩子函数
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i) {
cbs.update[i](oldVnode, vnode)
}
}
// 由于是相同的VNode节点,使用新旧VNode的真实dom元素相同
const elm = vnode.elm = oldVnode.elm!
// 获取旧VNode的children
const oldCh = oldVnode.children as VNode[]
// 获取新VNode的children
const ch = vnode.children as VNode[]
// 如果新旧节点完全相同,直接返回
if (oldVnode === vnode) return
// 判断VNode是否为文本节点
if (isUndef(vnode.text)) {
// 如果不是文本节点,判断新旧VNode是否同时有children
if (isDef(oldCh) && isDef(ch)) {
// 如果同时有children属性,且children属性不相同就更新children
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch)
}
}
}
}
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
// 缓存dom节点和父dom节点
let elm: Node, parent: Node
// 当oldVnode不是VNode时,说明是初次加载,创建一个空白VNode
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}
// 判断是否为相同VNode节点
if (sameVnode(oldVnode, vnode)) {
// 更新VNode节点差异
patchVnode(oldVnode, vnode)
} else {
// 获取旧VNode的dom
elm = oldVnode.elm!
// 获取父节点dom
parent = api.parentNode(elm) as Node
// 创建dom元素
createElm(vnode)
// 当父节点不为空时
if (parent !== null) {
// 将新创建的dom插入dom树
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
// 删除旧VNode节点
removeVnodes(parent, [oldVnode], 0, 0)
}
}
return vnode
}
}
终于,我们看完了 patch 中所有函数,但这样就万事大吉了吗?
ts-transform-js-extension.cjs
我们终于完成了 Terdom 所有代码的编写,下面让我们打个包体验一下自己写得虚拟DOM库吧。
npm run build
在案例中引入 build 文件夹下的导出的 init 函数和 h 函数
import { init } from '../../build/package/init.js'
import { h } from '../../build/package/h.js'
// 2. 注册模块
let patch = init()
// 3. 使用h()函数的第二个参数传入模块需要的数据(对象)
let content = h('div', [
h('h1', '欢迎使用Terdom'),
h('p', '掘金博文:'),
h('a', {
props: {
href: "https://juejin.cn/user/1011206430663511/posts"
}
}, 'https://juejin.cn/user/1011206430663511/posts'),
h('p', '源码地址:'),
h('a', {
props: {
href: "https://github.com/zhtzhtx/Terdom"
}
}, 'https://github.com/zhtzhtx?tab=repositories')
])
let vnode = h('div', {
style: {
width: "100vw",
height: "100vh",
display: "flex",
"justify-content": "center",
"align-items": "center"
}
}, [content])
let app = document.querySelector('#app')
patch(app, vnode)
但是,等我们加载页面发现出现了报错:
这是为什么呢?我们打开build文件夹下的 init.js,我们发现一个问题,那就是引入的其它模块文件路径没有加后缀名
这是由于TypeScript编译后,并不会帮你在路径后加上后缀名。也就是说,你自己要把所有的引入路径加上“ .js ”。这样非常麻烦,而且我们编写的是" .ts "文件,而路径却要加上“ .js ”,这也不符合编程习惯。
那么怎么解决呢?我们可以看一下Snabbdom的解决方法,他自己编写了一个插件 ts-transform-js-extension.cjs ,可以帮我们自动在引入路径后加上“ .js ”,具体逻辑这里就不深入研究了,代码如下:
const ts = require('typescript')
module.exports.transform = (ctx) => (sf) => ts.visitNode(sf, (node) => {
const visitor = (node) => {
const originalPath = (
ts.isImportDeclaration(node) ||
ts.isExportDeclaration(node)) &&
node.moduleSpecifier
? node.moduleSpecifier.getText(sf).slice(1, -1)
: (
ts.isImportTypeNode(node) &&
ts.isLiteralTypeNode(node.argument) &&
ts.isStringLiteral(node.argument.literal)
)
? node.argument.literal.text
: null
if (originalPath === null) return node
const pathWithExtension = originalPath.endsWith('.js')
? originalPath
: originalPath + '.js'
const newNode = ts.getMutableClone(node)
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
newNode.moduleSpecifier = ts.createLiteral(pathWithExtension)
} else if (ts.isImportTypeNode(node)) {
newNode.argument = ts.createLiteralTypeNode(pathWithExtension)
}
return newNode
}
return ts.visitEachChild(node, visitor, ctx)
})
然后在 tsconfig.json 中进行了设置:
我们也学着一样进行操作,然后重新打包一次。我们会发现引入路径之后依然没有加上“ .js ”,也就是说插件并没有运行,这是为什么呢?
查阅资料后,发现TypeScript本身还没有支持插件功能,而如果我们想要使用TypeScript插件,必须安装 ttypescript
npm install ttypescript -D
然后,将 package.json 中的 build 命令改写成“ ttsc --build src/tsconfig.json ”
再次运行“ npm run build ”,刷新页面发现已经可以正常显示了
结语
都看到这里了,如果觉得文章还不错的,请给个赞吧!
码字不易,求求了!!!