The component life cycle is usually where our business logic begins. If the business scenario is complex and the component life cycle does not behave as expected, it can lead to weird business bugs that are extremely difficult to reproduce and fix.

Number of times the component Attached life cycle is executed

It is generally understood that life cycles that are strictly related to component loading, such as created, Attached, Ready, etc. should only be executed once per component instance, except that life cycles like Moved/Show /hide may be executed multiple times. But is this really the case?

background

We discovered this problem when we received a large number of errors like Cannot Re-define Property: isComponent in the applet’s error log.

Cause analysis,

The variable name can be traced back to the way we defined it in our code:

Component({
  lifetimes: {
    attached() {
      Object.defineProperty(this.'isComponent', {
        enumerable: true.get() { return true}}); ,}}});Copy the code

This error is easily caused by an attempt to redefine an object as a non-configurable property, as described in the MDN.

But this definition is written in the Attached life cycle. Is the component’s Attached life cycle triggered twice?

Oh, my God. How is that possible?

Yes, it is!

Scenario reduction

The problem is not easy to repeat, but by pruning it down, we finally get to the root of the problem:

Before the page onLoad, the child component rendering is triggered by changing state via setData, and the attached life cycle of that child component is triggered twice.

You can reproduce the scenario with the following code, or access the snippet of the applet code directly.

page

// page.js
Page({
  data: {
    showChild2: false,},onChild1Attached() {
    this.setData({ showChild2: true}); }});Copy the code
<! -- page.wxml -->
<child1 bind:attached="onChild1Attached"></child1>
<child2 wx:if="{{ showChild2 }}"></child2>
Copy the code

The child component 1

Render with the page and, while attached, notify the page to update its state and render the child component 2 via triggerEvent.

// child1.js
Component({
  lifetimes: {
    attached() {
      this.triggerEvent('attached'); ,}}});Copy the code
<! -- child1.wxml -->
<view>child1</view>
Copy the code

The child component 2

The attached life cycle was executed twice, resulting in an error.

// child2.js
Component({
  lifetimes: {
    attached() {
      Object.defineProperty(this.'isComponent', {
        enumerable: true.get() { return true}}); ,}}});Copy the code
<! -- child2.wxml -->
<view>child2</view>
Copy the code

The execution time of the component’s ready life cycle

The applets’ official documentation does not specify the execution order of the component life cycle, but it can be easily found by printing logs:

  • During the loading phase, the sequence is created -> Attached -> Ready
  • In the detached phase, this is followed by: detached

Created -> attached -> Ready -> detached. But is this really the case?

background

Some time, customer service often feedback, our small program exists string data phenomenon. For example, merchant A’s live broadcast shows the commodities of merchant B.

Cause analysis,

String data occurs in multiple scenarios, considering that the data is pushed to the applet side by message, it is suspected that the problem is WebSocket communication.

In the small program side, we encapsulated a WebSocket communication component, the core logic is as follows:

// socket.js
Component({
  lifetimes: {
    ready() {
      this.getSocketConfig().then(config= > {
        this.ws = wx.connectSocket(config);
        this.ws.onMessage(msg= > {
          const data = JSON.parse(msg.data);
          this.onReceiveMessage(data);
        });
      });
    },
    detached() {
      this.ws && this.ws.close({}); }},methods: {
    getSocketConfig() {
      // Request socket connection configuration from the server
      return new Promise(() = > {});
    },
    onReceiveMessage(data) {
      event.emit('message', data); ,}}});Copy the code

Simply put, you initialize a WebSocket connection and listen for a push message when the component is ready, and then close the connection in the detached phase.

It doesn’t seem to be a problem, so you have to work backwards from the results to what might not make sense.

Detached data string -> WebSocket message string -> WebSocket not closed properly -> close problem /detached not executed /ready executed after detached

Scenario reduction

The actual business logic here is complex and can only be verified with simplified code. Through continuous experiments, we finally found that:

The ready and detached execution order of components is not clear.

You can reproduce the scenario with the following code, or access the snippet of the applet code directly.

page

// page.js
Page({
  data: {
    showChild: true,},onLoad() {
    this.setData({ showChild: false}); }});Copy the code
<! -- page.wxml -->
<child wx:if="{{ showChild }}" />
Copy the code

component

Detached Destroying a component that is not ready is done synchronously and asynchronously with ready.

// child.js
Component({
  lifetimes: {
    created() {
      console.log('created');
    },
    attached() {
      console.log('attached');
    },
    ready() {
      console.log('ready');
    },
    detached() {
      console.log('detached'); }}});Copy the code

expand

Even if you take the initialization work from the ready front-attached phase, as long as there are asynchronous operations, there may still be cases where detached execution precedes asynchronous callbacks.

Therefore, don’t put all your faith in the destruction operation in the component detached phase.