探索小程序自定义组件的事件系统

246 阅读4分钟

本篇来探索小程序自定义组件的事件系统, 由浅入深的探索事件冒泡 (bubbles), 事件穿越组件边界 (composed) 等问题.

最基础的自定义组件事件

为了探索冒泡, composed 等问题, 我们至少需要两个自定义组件.

假设我们有一个 tree-render 组件可以用来渲染一个树状数据.

tree-render 组件内部会调用一个 tree-node 组件.

然后在页面中引入 tree-render 组件.

graph LR
页面 --> 2[tree-render 组件] --> 3[tree-node 组件]

以下是树状数据

// treeData
{
  type: 'root',
  children: [
   {
     type: 'text', value: '这是一段文本'
   }
  ]
}
<!-- tree-render 组件 -->
<view class='tree-render-body'>
  <block wx:for="{{treeData.children}}" wx:key="index">
    <tree-node node="{{item}}" bindmyclick="handleClick" />
  </block>
</view>
<!-- tree-node 组件 -->
<block wx:if="{{node.type==='text'}}">
  <text>{{node.value}}</text>
</block>

我们以这个例子为切入口, treeData 数据和 tree-node 组件会慢慢变复杂.

最基础的自定义组件事件就是在组件中 triggerEvent, 然后在使用组件的地方 bind 相应的事件.

<!-- tree-node 组件 -->
<block wx:if="{{node.type==='text'}}">
  <text bindtap='handleTap'>{{node.value}}</text>
</block>

在 tree-node 组件中 triggerEvent. 默认情况下 bubbles 和 composed 都为 false.

// tree-node 组件
methods: {
  handleTap(e) {
    const { dataset } = e.currentTarget
    this.triggerEvent('myclick', dataset)
  }
}

我们在 tree-render 组件中放两个监听点, 并且在页面中也放两个监听点.

<!-- tree-render 组件 --> 
<view class='tree-render-body' data-msg='监听于 tree-render 组件中的 view 节点' bindmyclick='log'> 
  <block wx:for="{{treeData.children}}" wx:key="index"> 
    <tree-node node="{{item}}" data-msg='监听于 tree-render 组件中的 tree-node 节点' bindmyclick="log" /> 
  </block>
</view>
<!-- 页面 组件 --> 
<view data-msg='监听于页面中的 view 节点' bindmyclick='log'>
  <table-render data-msg='监听于页面中的 tree-render 节点' bindmyclick='log' treeData="{{treeData}}" />
</view>

监听都准备好后点击文本来触发事件.

光想想都知道控制台只会打印出一个监听:

// log
监听于 tree-render 组件中的 tree-node 节点         tree-render.js:26

在小程序开发者工具中预览效果 - 基础的自定义组件事件

因为选项 bubbles 默认为 false, 也就是说事件不会冒泡, 所以只监听到一个监听, 这是理解的通的.

移除掉 tree-render 组件中绑定在 tree-node 节点上的监听事件

同样的条件下, 当我们移除掉 tree-render 组件中 tree-node 节点上的监听事件, 那么 tree-node 上层的 view 节点能够监听到吗?

一开始我的理解是能够监听到的. tree-node 节点没有监听, 那自然它上层的 view 节点能监听到, 监听到后再阻止事件冒泡.

很显然我把自定义组件中的冒泡跟常规事件里的 catch 混淆在一起了.

在小程序开发者工具中预览效果 - tree-node 节点移除监听事件

结果是什么都监听不到.

因此正确的理解是, 默认情况下自定义组件事件在冒到组件边界的时候就停止冒泡了.

除非开启 bubbles 为 true.

事件的冒泡选项为 true 时 (bubbles: true)

当事件的冒泡选项 bubbles 为 true 后, tree-node 节点以及其上层的 view 节点都能监听到事件了.

那么页面中能够监听到事件吗?

想想既然冒泡了应该能够监听到吧? 但是实际上是监听不到的.

监听于 tree-render 组件中的 tree-node 节点           tree-render.js:26 
监听于 tree-render 组件中的 view 节点                tree-render.js:26 

在小程序开发者工具中预览效果 - bubbles 为 true

现在终于能够理解小程序文档中的那句话了

image.png

事件只会冒到引用组件的节点树上!

除非开启 composed 为 true.

事件的 composed 选项为 true 时

最后我们把 composed 选项设为 true.

可以预见到页面中也能监听到事件了.

监听于 tree-render 组件中的 tree-node 节点                tree-render.js:26 
监听于 tree-render 组件中的 view 节点                     tree-render.js:26
监听于页面中的 tree-render 节点                           VM127:18 
监听于页面中的 view 节点                                  VM127:18 

在小程序开发者工具中预览效果 - bubbles 和 composed 为 true

组件循环引用自身

之所以用树状结构来举例, 是因为树状结构节点的 children 又会套嵌 children.

也就是说 tree-node 组件可以调用自身.

<!-- tree-node 组件 -->
<block wx:if="{{node.type==='text'}}">
  <text bindtap='handleTap'>{{node.value}}</text>
</block>
<block wx:elif="{{node.type === 'element'}}">
  <view class="{{node.tagName}}">
    <block wx:if="{{node.children}}">
      <tree-node wx:for="{{node.children}}" wx:key="index" node="{{item}}" />
    </block>
  </view>
</block>
// tree-node.json
{
  "component": true,
  "usingComponents": {
    "tree-node": "./tree-node"
  }
}

更改 treeData

treeData: {
      type: "root",
      children: [
        {
          type: "element",
          tagName: "ol",
          children: [
            {
              type: "element",
              tagName: "li",
              children: [
                {
                  type: "text",
                  value: "列表项1, 点击会触发事件",
                },
              ],
            },
            {
              type: "element",
              tagName: "li",
              children: [
                {
                  type: "text",
                  value: "列表项2, 点击会触发事件",
                },
              ],
            },
          ],
        },
      ],
    },

正好借这个例子来验证一下对事件的理解.

graph LR
页面 --> 2[tree-render 组件] --> 3[tree-node 组件 ol] --> 4[tree-node 组件 li] --> 5[tree-node text]

当 tree-node 组件内引入自身时没有 bindmyclick

<!-- tree-node 组件引用自身处 -->
<block wx:if="{{node.children}}">
  <tree-node wx:for="{{node.children}}" wx:key="index" node="{{item}}" />
</block>
bubblescomposed结果
falsefalse监听不到事件
truefalse监听不到事件
truetrue页面tree-render 都能监听到事件

在小程序开发者工具中预览效果 - 组件循环引用

当 tree-node 组件内引入自身时有 bindmyclick

<!-- tree-node 组件引用自身处 -->
<block wx:if="{{node.children}}">
  <tree-node wx:for="{{node.children}}" wx:key="index" node="{{item}}" data-msg='监听于 tree-node 组件中的 {{node.tagName}} 节点' bindmyclick='log' />
</block>
bubblescomposed结果
falsefalse监听于 tree-node 组件中的 li 节点
truefalse监听于 tree-node 组件中的 li 节点
truetrue都能监听到事件

在小程序开发者工具中预览效果 - 组件循环引用2

你理解对了吗? 打开小程序开发者工具看得更加清楚.