Skip to content
微信公众号

组件系统Exparser

小程序中会涉及到的组件共有三种类型:内置组件、自定义组件、原生组件

Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序提供各种各样的组件支撑。内置组件和自定义组件都有Exparser组织管理。

exparser有个核心方法:

  • regiisterBehavior: 注册组件的一些基础行为,供组件继承
  • registerElement:注册组件,跟我们交互接口主要是属性和事件

Exparser的组件模型与WebComponents标准中的Shadow DOM高度相似。Exparser会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的Shadow DOM实现。Exparser的主要特点包括以下几点:

  • 基于Shadow DOM模型:模型上与WebComponents的Shadow DOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
  • 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  • 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。

在理解了WebComponent的概念之后,再理解Exparser就会变得简单。就有了一些可以与之对照的一些概念,比如Shadow DOM模型,属性、事件绑定、slot等等。并且拥有了与WebComponent一样的优秀表现。

在Exparser的组件模型中,组件的节点树称为Shadow Tree,即组件内部的实现;最终拼接成的页面节点树被称为Composed Tree,即将页面所有组件节点树合成之后的树。这里与我们理解WebComponent时候是有一点点区别的。可以想像小程序自带的组件都是自定义组件,而Web中我们通过WebComponent写的才是自定义组件。并且在Web中,页面所有组件节点合成的树为HTML Tree。这里的概念要进行区分。

内置组件

Exparser内置了很多基础组件,比如说视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,我们可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。

把一个组件内置到小程序框架里的一个重要原则是:这个组件是基础的。换句话说,没有这个组件的话,在小程序架构里无法实现或者实现不好某类功能。

比如像一些开放类组件,有open-data组件提供展示群名称、用户信息等微信体系下的隐私信息,有button组件里open-type属性所提供分享、跳转App等敏感操作的能力。

还有比如像视图容器类组件movable-view这种因双线程模型导致手势识别不好实现的组件,这是因为手势识别需要高频率捕捉手指的触摸事件,而在双线程模型中,触摸事件从渲染层发出,派发到逻辑层,这中间是有一定的延时而导致视图跟随手指运动这类交互变得有些卡顿。

说到这里可以提及一下小程序js动画效果问题,如果是纯css动画可以在视图层进行处理。如果业务场景为手势识别之类的,监听事件不断的触发,数据不断的改变。这样的业务场景中,我们可以想像,如果坐标值不断改变的话,在逻辑与视图分开的双线程架构中,线程与线程之间的通讯是非常频繁的,会有很大的性能问题。所以我们可以看到微信开放了一个标记<WXS>,可以在渲染层写部分js逻辑。这样话就可以在渲染层单独处理频繁改变的数据,这样的话就避免了线程与线程之间频繁通讯导致的性能和延时问题。

自定义组件

Exparser支持用户自定义组件。我们可以看一下官方给予的自定义组件的例子,通过例子来看一下与WebComponent有什么区别。

js
<view>
    <input-with-label>
        <label>
            TEXT
        </label>
        <input />
    </input-with-label>
</view>

这里如果将input-with-label抽象成一个自定义组件,那么可以将整个节点树拆分成两部分。构建Shadow Tree,如下

js
<label><slot/></label>
<input />

看到了熟悉的<slot>标记,插槽,一看就是上面我们讲的插槽式写法,那里不一样呢?外面少了template标记包裹。

那么来看一下,生成Shadow Tree之后,<input-with-label>这个组件怎么调用了呢?

js
<view>
    <input-with-label>
        TEXT
    </input-with-label>
</view>

这里就可以理解了,TEXT文本会插入到Shadow Tree的<slot>标记处。这里的原理和逻辑与WebComponent是一至的。

在自定义组件的概念基础上,我们可以把所有组件都进行分离,这样,各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用。

整个页面节点树实质上被拆分成了若干个ShadowTree(页面的body实质上也是一个组件,因而也是一个ShadowTree)最终组成了小程序中的Composed Tree。

在这个时候可以看一下官方的这句话,就非常好理解了。

小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。

组件间通信

不同组件实例间的通信有WXML属性值传递、事件系统、selectComponent和relations等方式。其中,WXML属性值传递是从父组件向子组件的基本通信方式,而事件系统是从子组件向父组件的基本通信方式。

Exparser的事件系统完全模仿Shadow DOM的事件系统。在通常的理解中,事件可以分为冒泡事件和非冒泡事件,但在Shadow DOM体系中,冒泡事件还可以划分为在Shadow Tree上冒泡的事件和在Composed Tree上冒泡的事件。如果在Shadow Tree上冒泡,则冒泡只会经过这个组件Shadow Tree上的节点,这样可以有效控制事件冒泡经过的范围。

我们还是通过一个例子理解事件冒泡。

js
<label><slot/></label>
<input />

slot一个button

js
<view>
    <input-with-label>
        <button />
    </input-with-label>
</view>

用上面的例子来说,当在button上触发一个事件时:

  • 如果事件是非冒泡的,那只能在 button 上监听到事件。
  • 如果事件是在 Shadow Tree 上冒泡的,那 button 、 input-with-label 、view 可以依次监听到事件。
  • 如果事件是在 Composed Tree 上冒泡的,那 button 、 slot 、label 、 input-with-label 、 view 可以依次监听到事件。

在自定义组件中使用triggerEvent触发事件时,可以指定事件的bubbles、composed和capturePhase属性,用于标注事件的冒泡性质。这一点和前面讲的自定义事件相互呼应,triggerEvent可以理解为小程序中的自定义事件createEvent。

js
Component({
    methods:{
        customEvent: function(){
            this.triggerEvent('custom',{},{
                bubbles: true,   // Boolean类型,这是一个冒泡事件
                composed: true,  // Boolean类型,这个事件在Composed Tree上冒泡
                capturePhase: false //Boolean类型,这个事件没有捕获阶段
            })
        }
    }
})

小程序基础库自身也会通过这套事件系统提供一些用户事件,如tap、touchstart和form组件的submit等。其中,tap等用户触摸引发的事件是在Composed Tree上的冒泡事件,其他事件大多是非冒泡事件。

Exparser运行原理

小程序基础库对外提供有 Page 和 Component 两个构造器,小程序启动将 properties、data、methods 等定义字段,写入 Exparser 的组件注册表。在初始化页面时,Exparser会创建出页面根组件的一个实例,用到的其他组件也会响应创建组件实例(这是一个递归的过程)。组件创建的过程大致有以下几个要点:

  • 根据组件注册信息,从组件原型上创建出组件节点的JS对象,即组件的this;
  • 将组件注册信息中的data 复制一份,作为组件数据,即this.data;
  • 将这份数据结合组件WXML,据此创建出Shadow Tree,由于Shadow Tree中可能引用有其他组件,因而这会递归触发其他组件创建过程;
  • 将Shadow Tree拼接到Composed Tree上,并生成一些缓存数据用于优化组件更新性能;
  • 触发组件的created生命周期函数;
  • 如果不是页面根组件,需要根据组件节点上的属性定义,来设置组件的属性值;
  • 当组件实例被展示在页面上时,触发组件的attached 生命周期函数,如果Shadw Tree中有其他组件,也逐个触发它们的生命周期函数。

Exparser核心方法

js
// Register
export const registerBehavior = Behavior.create
export const registerElement = Component.register

// Create node
export const createElement = Component.create
export const createTextNode = TextNode.create
export const createVirtualNode = VirtualNode.create

// Dom manipulation
export const appendChild = Element.appendChild
export const insertBefore = Element.insertBefore
export const removeChild = Element.removeChild
export const replaceChild = Element.replaceChild

// Event
export const addListenerToElement = EventManager.addListenerToElement
export const removeListenerFromElement = EventManager.removeListenerFromElement
export const triggerEvent = EventManager.triggerEvent
export const addGlobalErrorListener = Events.addGlobalErrorListener
export const removeGlobalErrorListener = Events.removeGlobalErrorListener
  • registerBehavior 注册组件的一些基础行为,供组件继承
js
export default window.exparser.registerBehavior({
    // 使用 exparser.registerBehavior 和exparser.registerElement 方法注册各种以 wx- 做为标签开头的元素到 exparser
    is: 'wx-base',
    properties: {
        id: {
            type: String,
            public: !0
        },
        hidden: {
            type: Boolean,
            public: !0
        }
    },
    _isDevTools: function () {
        return true
    },
    debounce: function (id, func, waitTime) {
        var _this = this
        this.__debouncers = this.__debouncers || {}
        this.__debouncers[id] && clearTimeout(this.__debouncers[id])
        this.__debouncers[id] = setTimeout(function () {
            typeof func === 'function' && func()
            _this.__debouncers[id] = void 0
        }, waitTime)
    }
})
  • registerElement 为各种内置组件,注册模板,行为,属性,监听器等内容,跟我们交互接口主要是属性和事件
js
// wx-button
export default window.exparser.registerElement({
    is: 'wx-button',
    template: '\n    <slot></slot>\n  ',
    behaviors: ['wx-base', 'wx-hover', 'wx-label-target'],
    properties: {
        type: {
            type: String,
            value: 'default',
            public: !0
        },
        size: {
            type: String,
            value: 'default',
            public: !0
        },
        disabled: {
            type: Boolean,
            public: !0
        },
        plain: {
            type: Boolean,
            public: !0
        },
        loading: {
            type: Boolean,
            public: !0
        },
        formType: {
            type: String,
            public: !0
        },
        hover: {
            type: Boolean,
            value: !0
        }
    },
    listeners: {
        tap: '_preventTapOnDisabled',
        longtap: '_preventTapOnDisabled',
        canceltap: '_preventTapOnDisabled',
        'this.tap': '_onThisTap'
    },
    _preventTapOnDisabled: function () {
        if (this.disabled) return !1
    },
    _onThisTap: function () {
        this.formType === 'submit'
            ? this.triggerEvent('formSubmit', void 0, {bubbles: !0})
            : this.formType === 'reset' &&
            this.triggerEvent('formReset', void 0, {bubbles: !0})
    },
    handleLabelTap: function (event) {
        exparser.triggerEvent(this.shadowRoot, 'tap', event.detail, {
            bubbles: !0,
            composed: !0,
            extraFields: {
                touches: event.touches,
                changedTouches: event.changedTouches
            }
        })
    }
})

Page 和 Component 的创建过程

从上图中我们能够比较明显的看到小程序在 Page 渲染和 Component 渲染时通信方式上存在差别。其中,Page 渲染中 VDOM 的生成和 diff 都是在视图层完成的,逻辑层只负责对 data 数据的发送,来触发渲染层的更新逻辑。而在 Component 的渲染中,逻辑层和视图层需要共同维护一套 VDOM,方式则是在组件初始化时在逻辑层构建组件的 VDOM,然后将其同步到视图层。后续的更新操作则会先在逻辑层进行新旧 VDOM 的 diff,然后仅将 diff 之后的结果进行通信,传递到视图层之后直接进行 VDOM 的更新和渲染。这样做最大的好处就是将视图更新通信的粒度控制成 DOM 级别,只有最终发生改变的 DOM 才会被更新过去(因为有时候 data 的改变并不一定会带来视图的更新),相对于之前 data 级别的更新会更加精准,避免非必要的通信成本,性能更好。

Exparser源码解析

这里以Hera为例分析其源码内容。

VirtualNode

js
import Element from './Element'

const VirtualNode = function () {}
VirtualNode.prototype = Object.create(Element.prototype, {
  constructor: {
    value: VirtualNode,
    writable: true,
    configurable: true
  }
})

// 创建VirtualNode
VirtualNode.create = function (is) {
  const insVirtualNode = Object.create(VirtualNode.prototype)
  insVirtualNode.__virtual = true
  insVirtualNode.is = is
  //调用元素的初始化方法给元素初始化属性:__attached,parentNode,childNodes,__slotParent,__slotChildren,__subtreeObserversCount
  Element.initialize(insVirtualNode, null) // 第二个null参数没用?
  return insVirtualNode
}

export default VirtualNode

TextNode

js
import Observer from './Observer'
const TextNode = function () {}
TextNode.prototype = Object.create(Object.prototype, {
  constructor: {
    value: TextNode,
    writable: true,
    configurable: true
  }
})

// 创建TextNode,并创建文本节点赋值给__domElement属性
TextNode.create = function (txt) {
  const tempObj = Object.create(TextNode.prototype)
  tempObj.$$ = tempObj.__domElement = document.createTextNode(txt || '')
  tempObj.__domElement.__wxElement = tempObj
  tempObj.__subtreeObserversCount = 0
  tempObj.parentNode = null
  return tempObj
}
//获取textContent从_domElement中获取,设置textContent内容将其设置到_domElement文本节点的textContent
Object.defineProperty(TextNode.prototype, 'textContent', {
  get: function () {
    return this.__domElement.textContent
  },
  set: function (txt) {
    this.__domElement.textContent = txt
    if (
      (this.__textObservers && !this.__textObservers.empty) ||
      this.__subtreeObserversCount
    ) {
      Observer._callObservers(this, '__textObservers', {
        type: 'characterData',
        target: this
      })
    }
  }
})
export default TextNode

SlotNode

js
import Element from './Element'
const SlotNode = function () {}

SlotNode.prototype = Object.create(Element.prototype, {
  constructor: {
    value: SlotNode,
    writable: true,
    configurable: true
  }
})

//对dom元素时行封装,返回虚拟dom
SlotNode.wrap = function (ele) {
  let tempObj = Object.create(SlotNode.prototype)
  Element.initialize(tempObj)
  tempObj.__domElement = ele
  ele.__wxElement = tempObj
  tempObj.$$ = ele
  return tempObj
}

export default SlotNode

本站总访问量次,本站总访客数人次
Released under the MIT License.