执行时机

Edge 主题由于免初始化,DOM 即组件,因此使用非常方便,唯一可能会给开发带来困扰的就是执行时机的问题,尤其是需要对组件元素进行传参的时候。

要想明白组件的执行时机,就需要了解组件底层的执行原理。

自定义元素的执行时机

在 HTML 规范中,短横线连接命名的 HTML 元素称之为自定义元素。

<ui-drop>下拉</ui-drop>

但是,仅仅是符合规范的 HTML 标签元素是不会有任何额外的行为的,需要使用 JS 对该元素进行继承与扩展,同时要在浏览器中进行注册。

几个比较重要的时间节点是下面这几个(以 Drop 下拉组件举例):

class Drop extends HTMLElement {
    constructor () {},
    connectedCallback () {}
}
if (!customElements.get('ui-drop')) {
    customElements.define('ui-drop', Drop);
}

其中,自定义元素变成真正意义上的组件需要在 customElements.define() 方法成功执行之后。

此时,当页面或者内存中存在 <ui-drop> 元素,就会执行 constructor() 构造函数,表示自定义元素构造完毕,当 <ui-drop> 元素和文档建立联系,就会执行 connectedCallback() 生命周期函数。

所以,下面的代码所示意的对 <ui-drop> 元素的设置是有问题的:

<ui-drop id="d0">下拉</ui-drop>
<script>
d0.target = 'xxx';
</script>
<!-- 后执行 -->
<script>
class Drop extends HTMLElement {
    constructor () {},
    set target (value) {}
}
if (!customElements.get('ui-drop')) {
    customElements.define('ui-drop', Drop);
}
</script>

当执行 d0.target 的时候,<ui-drop> 自定义元素还未在浏览器中注册,因此,直接在 DOM 对象上设置 target 属性不会有预期的行为触发。

因此,在自定义元素组件开发中,一定要确保业务代码在组件注册完毕之后执行。

type="module" 的问题

由于 Edge 主题的大多数组件都采用了 ES6 原生的 import/export 加载,所以,在应用的时候,必须要设置 type="module",例如:

<script type="module" src="./Drop.js"></script>

type="module" 会让 JS 资源异步无阻塞加载,所以,也可能会带来执行时机的问题,例如:

<ui-drop id="d0">下拉</ui-drop>
<script type="module" src="./Drop.js"></script>
<!-- 虽然位置在后,但是先执行 -->
<script>
d0.target = 'xxx';
</script>

此时,执行 d0.target 也不会有预期的效果,因为当 d0.target 执行的时候,Drop.js 尚未加载完毕。

这种情况可以这么处理,给内联的 <script> 也设置 type="module"

<ui-drop id="d0">下拉</ui-drop>
<script type="module" src="./Drop.js"></script>
<!-- 也设置 type="module" -->
<script type="module">
d0.target = 'xxx';
</script>

因为 type="module" 虽然异步,但是可以保证顺序。

如果因为某些原因,业务代码必须先执行,则可以使用生命周期函数调节代码执行的时机,例如下面的代码执行就是没有问题的:

<ui-drop id="d0">下拉</ui-drop>
<script type="module" src="./Drop.js"></script>
<!-- 和页面建立连接后执行 -->
<script>
d0.addEventListener('connected', function () {
    this.target = 'xxx';
});
</script>

在此例子中,我们也可以使用原生的 whenDefined() 方法进行处理:

<ui-drop id="d0">下拉</ui-drop>
<script type="module" src="./Drop.js"></script>
<!-- 自定义组件定义完成后执行 -->
<script>
async function setTarget () {
  await customElements.whenDefined('ui-drop')
  d0.target = 'xxx';
}
setTarget();
</script>

Safari 的问题

由于 Safari 浏览器目前还没支持内置自定义元素,因此,需要引入一段 Polyfill 代码使其支持。

<script href="https://unpkg.com/lu2/theme/edge/js/common/safari-polyfill.js"></script>

在通常情况下,我们是看不出来有什么功能上的问题的,但是,一旦需要对内置自定义元素进行传参或赋值,则就有可能出现错误,例如:

<form id="form" is="ui-form"></form>

<script src="./safari-polyfill.js"></script>
<script type="module" src="./Form.js"></script>
<script type="module">
this.params.avoidSend = () => {};
</script>

要想明白上面代码为什么会出错,就需要了解 safari-polyfill.js 实现的原理以及 type="module" 的执行时机。

Safari 浏览器的 Polyfill 使用了 MutationObserver 接口对页面元素进行观察,会在 customElements.define() 执行完毕后触发。

而 MutationObserver 是个异步处理,这就导致 this.params.avoidSend 执行的时候,Safari 浏览器的 <form> 元素依然是普通元素,并没有 params 对象,导致报错。

事情还没结束,如果我们去掉组件代码中所有的 export 和 import 语句,不再设置 type="module",就会发现执行是没有错误的。例如:

<form id="form" is="ui-form"></form>

<script src="./safari-polyfill.js"></script>
<script src="./all.js"></script>
<!-- 正常执行 -->
<script>
this.params.avoidSend = () => {};
</script>

怎么回事?为什么没有 type="module" 就正常了呢?

这个就不太清楚了。

可能在 Safari 浏览器下,设置了 type="module" 所有 JS 代码由于异步加载,为了提高性能,就全部塞在了一个执行线程上,就好似 this.params.avoidSend 直接就写在了 Form.js 的最后。

而没有设置 type="module"<script> 元素内的 JavaScript 代码是一段一段执行的,执行时机总是比上一个 <script> 元素慢一拍。

自定义属性的执行时机

Edge 主题中,所有的自定义元素组件都支持使用自定义属性实现类似的能力,正常开发都是没什么问题的,但是一旦遇到元素是动态创建的,或者基于 HTML 模板生成的,则可能就会出现执行时机的问题,例如:

xxx.innerHTML = '<button id="b0" data-target="img0" is-drop>下拉</button>';
b0['ui-drop'].show();

上面代码中 b0['ui-drop'].show() 不会有预期的效果,因为此时 #b0 元素还没有完成 is-drop 的组件化处理。

为什么呢?

原因和上面 Safari 执行时机问题类似,自定义属性和自定义元素捆绑也是使用 MutationObserver 接口实现的,这是一个异步过程。

正确做法是使用生命周期事件进行处理,如下所示:

xxx.innerHTML = '<button id="b0" data-target="img0" is-drop>下拉</button>';
b0.addEventListener('connected', function () {
    this['ui-drop'].show();
});

Edge 主题中的 <dialog> 元素虽然使用的是 is="ui-dialog",但是其底层实现并不是内置自定义元素,而是普通的自定义属性观察,因为 Safari、Firefox 浏览器并没有原生支持 <dialog> 元素。

因此,自定义属性的执行时机问题在 <dialog> 弹框组件中也同样存在,在开发的时候,可能需要注意下。

本页贡献者:

zhangxinxu