0
点赞
收藏
分享

微信扫一扫

Taro源码 Taro DOM

浏览器的事件系统是 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目录结构

Taro源码 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类有以下三个方法,addEventListenerremoveEventListener控制监听事件的增删,与浏览器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 方法将其定义为可枚举的样式属性。

举报

相关推荐

0 条评论