浏览器的事件系统是 Web 应用程序中必不可少的部分,可以使开发人员通过编写事件监听器来响应用户操作、处理网络请求等实现交互性和动态性的 Web 应用程序。
Taro作为支持一码多端的跨端框架,支持支付宝小程序、微信小程序、h5等多个平台。为了抹平端上差异,同时支持小程序平台也能够使用dom和bom api,Taro提供了一套精简版DOM和BOM的封装,称之为Taro DOM、Taro BOM。
本文在介绍Taro DOM时,通过和浏览器类比的视角,分析Taro DOM在构思和实现上与浏览器DOM的相似之处,同时总结一些Taro自身特有的性质,来更好的理解框架本身。
为了便于阅读,读者可以查阅下表,mark一下源码中的命名细节。
name | Taro 类 | 浏览器 |
节点 | TaroNode | Node |
事件目标 | TaroEventTarget | EventTarget |
元素节点 | TaroElement | Elemen |
Taro DOM目录结构
应该按照怎样的逻辑顺序看懂这些文件呢?还要从DOM树本身说起。。。
TaroNode
浏览器中支持DOM结构的最小单位是节点,因此,在理解Taro DOM在实现时,可以先从node.ts入手。篇幅原因,无法详细展开每个方法的代码,故先展示TaroNode类的结构
class TaroNode extends TaroEventTarget{
public uid: string
public sid: string
public nodeType: NodeType
public nodeName: string
public parentNode: TaroNode | null = null
public childNodes: TaroNode[] = []
public constructor () {
super()
this.uid = '_' + nodeId() // dom 节点 id,开发者可修改
this.sid = this.uid // dom 节点全局唯一 id,不可被修改
eventSource.set(this.sid, this)
}
hydrate(node:TaroNode){} //用于将节点进行序列化,方便进行后续的渲染。
updateChildNodes (isClean?: boolean){} //更新子节点,将子节点序列化后加入更新队列中,方便进行后续的渲染。
get _root (): TaroRootElement | null{} //获取节点所在的根节点。
findIndex (refChild: TaroNode): number{} //查找子节点在父节点中的位置。
get nextSibling (): TaroNode | null{} //获取节点的下一个兄弟节点。
get previousSibling (): TaroNode | null{} //获取节点的上一个兄弟节点。
get parentElement (): TaroElement | null{} //获取节点的父元素节点。
get firstChild (): TaroNode | null{} //获取节点的第一个子节点。
get lastChild (): TaroNode | null{} //获取节点的最后一个子节点。
set textContent (text: string){} //设置节点的文本内容。
insertBefore<T extends TaroNode> (newChild: T, refChild?: TaroNode | null,
isReplace?: boolean): T{}//在当前节点前插入一个新节点。
appendChild (newChild: TaroNode){} //在当前节点末尾添加一个新节点。
replaceChild (newChild: TaroNode, oldChild: TaroNode){} //替换当前节点中的某一个子节点。
removeChild<T extends TaroNode> (child: T, options: RemoveChildOptions = {}): T{} //移除当前节点中的某一个子节点。
remove (options?: RemoveChildOptions){} //移除当前节点。
hasChildNodes () {} //判断当前节点是否有子节点。
public enqueueUpdate (payload: UpdatePayload){} //将节点序列化后加入更新队列中,方便进行后续的渲染。
public get ownerDocument (): TaroDocument{} //获取节点所在的文档对象。
}
TaroNode类所实现的方法包括了元素节点获取相邻节点、父子节点、增删改查等方法,与浏览器DOM十分类似,很多方法的实现逻辑和业务逻辑简单易懂,本文不再赘述。其中insertBefore方法有着构建运行时Taro DOM树的作用,实现思路也值得细细探讨。
function insertBefore<T extends TaroNode> (newChild: T, refChild?: TaroNode | null, isReplace?: boolean): T {
if (newChild.nodeName === DOCUMENT_FRAGMENT) {
newChild.childNodes.reduceRight((previousValue, currentValue) => {
this.insertBefore(currentValue, previousValue)
return currentValue
}, refChild)
return newChild
}
// Parent release newChild
// - cleanRef: false (No need to clean eventSource, because newChild is about to be inserted)
// - update: true (Need to update parent.childNodes, because parent.childNodes is reordered)
newChild.remove({ cleanRef: false })
// Data structure
newChild.parentNode = this
if (refChild) {
// insertBefore & replaceChild
const index = this.findIndex(refChild)
this.childNodes.splice(index, 0, newChild)
} else {
// appendChild
this.childNodes.push(newChild)
}
// Serialization
if (this._root) {
if (!refChild) {
// appendChild
const isOnlyChild = this.childNodes.length === 1
if (isOnlyChild) {
this.updateChildNodes()
} else {
this.enqueueUpdate({
path: newChild._path,
value: this.hydrate(newChild)
})
}
} else if (isReplace) {
// replaceChild
this.enqueueUpdate({
path: newChild._path,
value: this.hydrate(newChild)
})
} else {
// insertBefore
this.updateChildNodes()
}
}
return newChild
}
插入节点的方法在实现时考虑三个case,其中第一个case DOCUMENT_FRAGMENT(文档碎片,定义与浏览器中类似),代表插入节点无父节点,此时利用reduceRight方法从最右子节点遍历递归插入整个文档碎片,根据传入的参考节点(refChild)以及是否需要替换节点(isReplace)来确定新节点的插入位置。如果参考节点为空,则表示将新节点追加到当前节点的子节点列表末尾;否则根据参考节点的位置将新节点插入到相应位置(在参考节点之前)。如果需要替换旧节点,则需要先找到旧节点的位置并将其从子节点列表中删除,然后再将新节点插入到相应位置。
TaroEventTarget
与浏览器DOM类似,TaroNode继承于TaroEventTarget,意味着每个节点都具有监听事件的能力。TaroEventTarget类有以下三个方法,addEventListener和removeEventListener控制监听事件的增删,与浏览器DOM不同的是,Taro在实现判断是否有监听事件时没有围绕监听事件作过多方法定义(例如浏览器dom中的 getEventListeners ),仅通过isAnyEventBinded方法通过判断__handlers中各个键的EventHandler长度是否都大于零来判断该元素是否存在监听事件。
class TaroEventTarget{
public __handlers: Record<string, EventHandler[]> = {}
//增加监听事件
public addEventListener (type: string, handler: EventHandler,
options?: boolean | AddEventListenerOptions) {}
public removeEventListener (type: string, handler: EventHandler) {} //移除监听事件
public isAnyEventBinded (): boolean { //判断是否有监听事件
const handlers = this.__handlers
const isAnyEventBinded = Object.keys(handlers).find(key => handlers[key].length)
return Boolean(isAnyEventBinded)
}
}
TaroElement
继承于TaroNode,元素节点TaroElement负责承担html结构的属性存储。在属性上除了继承TaroNode节点应用于Taro DOM树的构造信息(例如,parent、child、uid等),还定义了style、props、tagName等html标签信息。
tagName: string
props: Record<string, any> = {}
style: Style
dataset: Record<string, unknown> = EMPTY_OBJ
innerHTML: string
可以看到style属性的特殊性,为了达到小程序和h5之间的一致性,Taro定义了独特的Style类,其中包含了一些方法,例如 setProperty、removeProperty、getPropertyValue 等,用于设置、获取和删除样式属性。Style 类的实例会被绑定到具体的元素节点上,并且在元素节点的属性值发生改变时,会自动更新相应的样式属性。
Style还定义了一些辅助方法,例如 enqueueUpdate、recordCss 等。enqueueUpdate 用于将样式属性变化的更新操作添加到元素节点的更新队列中,以便在下一次更新时更新样式属性;recordCss 则用于记录元素节点的样式属性变化,以便在下一次更新时进行比较,判断是否需要更新样式属性。initStyle 方法用于初始化样式属性,将样式属性定义为对应的 get 和 set 方法,以便在元素节点的属性值发生改变时,能够自动更新相应的样式属性。isCssVariable 方法用于判断样式属性是否为 CSS 变量。如果是 CSS 变量,则用 setCssVariables 方法将其定义为可枚举的样式属性。
标签:Taro,DOM,newChild,TaroNode,public,源码,节点 From: https://blog.51cto.com/u_16116793/6449647