One of zone.js from getting started to giving up - get to know zone.js through a game

I have written some articles introducing some concepts in Angular before. Next, let's talk about some dependencies in Angular, such as zone.js. It is an execution context that spans multiple asynchronous tasks, and has a particularly powerful ability to intercept or track asynchronous tasks. Let's follow Xiaoda's article and learn about it~

foreword

Recently, due to work arrangements, it is necessary to study some internal mechanisms and modules in Angular. As an excellent framework specially designed for large-scale front-end projects, Angular actually has many advantages worth learning and learning from. I learned that Angular's change detection is essentially different from Vue and React, and Angular's detection system is inseparable from zone.js, so this series will share some of zone.js, and I hope that I can follow the individual to zone.js .js step by step learning to make a learning guide from shallow to deep, welcome everyone to actively get on the bus, study and discuss together.


1_zone_1676015102729.jpeg

Why learn zone.js

This series of articles will contain a lot of conjectures, verifications, demos and source code analysis. In the process of my own learning, there have been many times when I want to give up, or think that it is almost enough. So if you want to stick to one thing, you need to have some clear motives. After all, the motives are not pure, and it is difficult to pretend to be pure. So as far as I am concerned, apart from work needs, I think there are the following two driving forces:

motivation one

In 2017, I was fortunate to participate in an interview with a large factory. During the technical interview, I was asked how Angular handles change detection. My knowledge of this area is very vague, so I answered indiscriminately. I pulled out all the words related to change detection in my mind, but the result became darker and darker, and finally I couldn't justify myself. The interviewer told me that I hope I can straighten out this piece of content in the future . I promised him, but I didn't expect it to be 6 years later.

motivation two

I believe that every Angular developer will have seen error messages like the following, and even some colleagues who are new to Angular feel that the learning curve of Angular is relatively steep and the error messages are extremely unfriendly. In fact, everyone thinks that these unfriendly error messages are the strength of zone.js. Without knowing zone.js, this is indeed a bit anti-human. So I hope that after learning this series, not only do you know what such a mistake means, but you can also understand how such a problem arises.

at HTMLButtonElement.throwError (https://zonejs-basic.stackblitz.io/~/index.js:19:11)
at _ZoneDelegate.invokeTask (https://unpkg.com/zone.js:446:35)
at Zone.runTask (https://unpkg.com/zone.js:214:51)
at ZoneTask.invokeTask [as invoke] (https://unpkg.com/zone.js:528:38)
at invokeTask (https://unpkg.com/zone.js:1730:22)
at globalCallback (https://unpkg.com/zone.js:1761:31)
at HTMLButtonElement.globalZoneAwareCallback (https://unpkg.com/zone.js:1797:20)
at ____________________Elapsed_496_ms__At__Fri_Jan_20_2023_16_20_20_GMT_0800_________ (http://localhost)
at Object.onScheduleTask (https://unpkg.com/[email protected]/dist/long-stack-trace-zone.js:108:22)
at _ZoneDelegate.scheduleTask (https://unpkg.com/zone.js:426:55)
at Zone.scheduleTask (https://unpkg.com/zone.js:257:47)
at Zone.scheduleEventTask (https://unpkg.com/zone.js:283:29)
at HTMLButtonElement.addEventListener (https://unpkg.com/zone.js:2038:37)
at HTMLButtonElement.bindSecondButton (https://zonejs-basic.stackblitz.io/~/index.js:16:8)
at _ZoneDelegate.invokeTask (https://unpkg.com/zone.js:446:35)
at Zone.runTask (https://unpkg.com/zone.js:214:51)
at ____________________Elapsed_1801_ms__At__Fri_Jan_20_2023_16_20_18_GMT_0800_________ (http://localhost)
at Object.onScheduleTask (https://unpkg.com/[email protected]/dist/long-stack-trace-zone.js:108:22)
at _ZoneDelegate.scheduleTask (https://unpkg.com/zone.js:426:55)
at Zone.scheduleTask (https://unpkg.com/zone.js:257:47)
at Zone.scheduleEventTask (https://unpkg.com/zone.js:283:29)
at HTMLButtonElement.addEventListener (https://unpkg.com/zone.js:2038:37)
at main (https://zonejs-basic.stackblitz.io/~/index.js:5:8)
at _ZoneDelegate.invoke (https://unpkg.com/zone.js:412:30)
at Zone.run (https://unpkg.com/zone.js:169:47)

Get to know each other briefly

I'm not very good at abstracting and generalizing. Fortunately, the Angular team has only one sentence for the definition of zone.js, but it is so abstract that it makes no difference whether you read it or not:

A Zone is an execution context that persists across async tasks. You can think of it as thread-local storage for JavaScript VMs.

So far, I don't think you need to pay too much attention to what is described here. I will use other ways to let you gradually understand it later. Here is a little impression of a few words:

  • Execution context: execute context
  • Keep: persist
  • Asynchronous tasks: async tasks
  • Those with a Java background can recall the function and usage of ThreadLocal; those who have used the JS sandbox can also make an analogy. It doesn't matter if you haven't used it before, and it doesn't affect the subsequent reading.

PS: The Angular team also has a video introduction to zone.js. It is recommended that you wait until after reading this article to learn more.

ngZone and zone.js, foolishly confused

Finally, before I really start, I want to add another knowledge point. Some people have seen ngZone when they are learning zone.js, and they think that these two are one thing. To make a simple statement here, the Angular team built the ngZone service based on zone.js. NgZone defines the execution context of Angular, which can be simply understood as a customized zone.js specially used for Angular. So for the knowledge about ngZone, you can pay attention to Series 4 and West Series 5 (if any), where there will be a detailed introduction to the specific change detection methods of ngZone and Angular.

So one sentence summarizes the relationship between the two, ngZone was born in zone.js; it grew up in Angular (born in Si, grew up in Si).

Learn about zone.js from a game

This article is adapted from real stories, any similarities are not accidental

At the end of 2022, the department where I work organized a switch matchup. There are two participating teams, A and B, with 15 people in each team. It is required to have 3 duels between the two teams every day, and the game will last for 5 days in total. The team with the points advantage wins (the losing team will invite the winning team to eat). The sponsor provided 3 games: the first game of boomerang; the second game of horse 8; the third game of Super Smash Bros.

If you just care about where there is such a good department, you can leave a message directly

2_switchgames_1676015798462.png

Since the types of games and the order of competition are fixed, the order of the three contestants sent by each team every day is very important: for example, players who are familiar with a certain game can be asked to compare the corresponding game; potential. So, our example today starts with the captain selection:

First Edition: Brainless Soldiers Being Peeped

Here we have two participating teams, teamA and teamB; here it is assumed that teamA can only be ranked in order, and teamB can only be ranked in reverse order. At the same time, there is also a referee who is responsible for collecting the daily rankings of the two teams, teamA and teamB. The code in the figure below roughly means that, following the referee’s order, teamA and teamB (the code AB is executed sequentially, but readers should not be entangled here) start to rank respectively, and after they are arranged, the referee function prints the ranking :

// demo0/demo0.js

const teamA = {
  name: 'teamA',
  team: [],
  sort: function() {
    this.team.push(1);
    this.team.push(2);
    this.team.push(3);
  }
};

const teamB = {
  name: 'teamB',
  team: [],
  sort: function() {
    // console.log(`${this.name}偷看${teamA.name}排名布阵, ${teamA.name}当前阵容是: `, teamA.team);
    this.team.push(3);
    this.team.push(2);
    this.team.push(1);
  }
};

function judgement() {
  teamA.sort();
  teamB.sort();

  console.log('teamA: ', teamA.team);
  console.log('teamB: ', teamB.team);
}

judgement();

// teamB偷看teamA排名布阵, teamA当前阵容是:  [ 1, 2, 3 ]
// [console.log] teamA:  [ 1, 2, 3 ]
// [console.log] teamB:  [ 3, 2, 1 ]

However, there are some young people who don’t talk about martial arts and have a rat tail, and peep at the opponent’s lineup during the formation of the two teams: As shown in the comment code above, the captain of teamB quietly printed out the formation of teamA during the formation of teamA, causing the captain of teamB to Adjustment of troops can be carried out in a targeted manner to achieve the best results.

Here I really want to name and criticize Captain Wu, you are the one who doesn’t talk about martial arts

3_mouse_1676016163588.jpeg

Then the reason for the above problems is mainly because teamA and teamB are visible to each other, that is, the two teams are completely exposed to each other during the process of arranging troops, which leads to an opportunity for the opponent to take advantage of, so this is also a successor. Down to adjust the focus.

Second Edition: Discussing Military Aircraft in a Small Black Room

In order to prevent the two teams from knowing the opponent's lineup during the formation, the two teams need to be isolated. There are many ways to isolate data in JS, from early closures, to subsequent isolation through modules (files), and now object-oriented programming ideas can also be used in JS. Here, we first isolate the two teams through the module. The file structure is as follows:

    ├─demo1
    │  ├─teamA.js
    │  └─teamB.js
    │  └─judgement.js

The code in teamA is similar to that in tramB:

// demo1/teamA.js

const teamA = {
  name: 'teamA',
  team: [],
  sort: function() {
    this.team.push(1);
    this.team.push(2);
    this.team.push(3);
  }
};

module.exports = teamA
// demo1/teamB.js

const teamB = {
  name: 'teamB',
  team: [],
  sort: function() {
    this.team.push(3);
    this.team.push(2);
    this.team.push(1);
  }
};

module.exports = teamB
// demo1/judgement.js

const teamA = require('./teamA');
const teamB = require('./teamB');

function judgement() {
  teamB.sort();
  teamA.sort();

  console.log('teamA: ', teamA.team);
  console.log('teamB: ', teamB.team);
}

judgement();

// teamA:  [ 1, 2, 3 ]
// teamB:  [ 3, 2, 1 ]

This time, teamA and teamB are imported through the referee program, and the sorting process of each team is relatively independent without interference.

Third Edition: Let Me Think

Although the problem of isolation was solved, the captain of team B felt that the sorting was too hasty every time, and he needed to take the task of sorting personnel back to negotiate with the team. Here we use asynchronous tasks to simulate the effect of the captains taking the tasks back for sorting.

The file structure is as follows:

    ├─demo2
    │  ├─teamA.js
    │  └─teamB.js
    │  └─judgement.js
    │  └─thinking.js

This time, we add a new meditation program: thinking.js, which provides a function to randomly wait for 0 to 3 seconds through the asynchronous setTimeout . The two captains carefully think about the order of players in each game. Here, the thinking.js module with a delay of 0.3 seconds is used to simulate the process of the captain making a decision.

// demo2/thinking.js

// 获取0~3随机数
function getRandomSec() {
  return Math.random() * 3;
}

module.exports = function(cb) {
  const random = getRandomSec() * 1000;
  setTimeout(cb, random);
}

Captain code example:

// demo2/teamA.js

const thinking = require('./thinking');

const teamA = {
  name: 'teamA',
  team: [],
  sort: function() {
    // 此处容我想想
    thinking(() => {
      this.team.push(this.team.length + 1);
    });
    thinking(() => {
      this.team.push(this.team.length + 1);
    });
    thinking(() => {
      this.team.push(this.team.length + 1);
    });
  },
};

module.exports = team
// demo2/teamB.js

const thinking = require('./thinking');

const teamB = {
  name: 'teamB',
  team: [],
  sort: function() {
    thinking(() => {
      this.team.unshift(this.team.length + 1);
    });
    thinking(() => {
      this.team.unshift(this.team.length + 1);
    });
    thinking(() => {
      this.team.unshift(this.team.length + 1);
    });
  },
};

module.exports = team

At this point, the two captains took back the tasks of each group, but there was still a referee in the battle room. Because the lineups of the two groups of A and B were lined up in an instant before, the referee could immediately know the ranking results of each team. Now everyone has gone back to their respective rows, leaving the referee alone. Moreover, the referee does not know how long it will take for the two captains to finish the queue (each captain needs 0~3 seconds), so the referee can only wait for the longest time, that is, the referee has to wait for 3 seconds before returning Collect everyone's results:

// demo2/judgement.js

const teamA = require('./teamA');
const teamB = require('./teamB');

function judgement() {
  teamB.sort();
  teamA.sort();

  setTimeout(() => {
    console.log('teamA: ', teamA.team);
    console.log('teamB: ', teamB.team);
  }, 3000);
}

judgement();

// 苦等3秒出结果
// teamA:  [ 1, 2, 3 ]
// teamB:  [ 3, 2, 1 ]

Version 4: zone.js version

When you think everyone should be satisfied, the referee stepped forward and said he was not satisfied. The referee is unwilling to wait for the result all the time. I hope everyone can notify him as soon as the sorting is completed to avoid wasting time. Next, let's take a look at how zone.js solves these problems.

PS: Don’t worry about the usage of the API and some specific concepts here. The following articles will teach you little by little. First, let’s experience the functions of zone.js through examples.

4_waiting_1676016587223.jpg

Before the demonstration, let’s clarify a few more important requirements:

  • The sorted data for the two teams needs to be segregated
  • The two teams need to have a thinking time when sorting (0~3s)
  • The referee must immediately know that the sorting of the two teams has ended and announce the result

As mentioned in the previous introduction, a key concept of zone.js is the execution context. At that time, we said that this asynchronous context can be compared to Java's LocalThread, that is, data can be shared within a single thread. Then in JS, this execution context is also analogous, it can be imagined as a sandbox - a JS VM. In this sandbox, you can run your JS code in the sandbox, and the sandbox also has a concept of context, which is a shared memory space that can be used by the code running in the sandbox; at the same time Sandbox and sandbox are isolated from each other and cannot interfere with each other.

Mark1: Create a zone, zone.js can create a zone through the fork method, we can first understand that it is a sandbox.
Mark2: There is a static method in zone.js that can get the zone, Zone.current

With these two methods, two zones can be created for teamA and teamB respectively. As can be seen from the following example, two zones are created in the code, they respectively hold the objects of teamA and teamB, and the objects of teamA and B are stored in the properties of the zone.

// demo3/judgement.js

require('zone.js');
const thinking = require('./thinking');

// 创建zone
// Zone.current 获取当前 zone;当前zone为rootZone
// Zone.current.fork  创建一个基于当前zone的子zone
const zoneA = Zone.current.fork({
  // zone的名字
  name: 'teamA',

  // zone中可以通过properties设置一段共享内存
  properties: {
    // teamA对象
    team: {
      name: 'teamA',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
      },
    },
  },
});

const zoneB = Zone.current.fork({
  name: 'teamB',
  properties: {
    team: {
      name: 'teamB',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
      },
    },
  },
});

In the above code, teamA and teamB are no longer defined in two files respectively. In order to verify whether the data of the two teams can be separated by different zones, we execute the same code in zoneA and zoneB respectively (print properties group name). To do this, we also need to look at the other two APIs provided by zone.js.

Mark3: zone.js provides a run method, which can execute a piece of code in the zone
Mark4: zone.js provides a get method, which can obtain the properties attribute of the current zone

// 在zoneA的上下文中执行函数
zoneA.run(() => {
  // 获取当前zone
  const currentZone = Zone.current;
  // 从properties中获取team属性
  const team = currentZone.get('team');
  console.log(team.name); // tramA
});

zoneB.run(() => {
  const currentZone = Zone.current;
  const team = currentZone.get('team');
  console.log(team.name); // tramB
});

It can be seen that the data in the two zones are isolated from each other, and in the scope of run, only the data in its own zone can be obtained.

The first step of transformation

Here, we first achieved the data isolation of teamA and teamB. The two captains save the personnel information of each team in their respective zones, and perform sorting tasks in the context of their respective zones. During the entire mission, the two zones do not interfere with each other.

function judgement() {

  // teamA领任务回去
  zoneA.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });

  // teamB领任务回去
  zoneB.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });

  // 裁判3s后收集结果
  setTimeout(() => {
    // 打印teamA的结果
    zoneA.run(() => {
      const currentZone = Zone.current;
      const team = currentZone.get('team');
      console.log('teamA: ', team.team);
    });
    // 打印teamB的结果
    zoneB.run(() => {
      const currentZone = Zone.current;
      const team = currentZone.get('team');
      console.log('teamB: ', team.team);
    });
  }, 3000);
}

judgement();

But there are two problems with the above code:

  • The referee still has to wait for 3 seconds to know the order of the two captains
  • Due to data segregation, the referee does not know the ranking results of the two captains. The referee can only entrust the two captains to print the sorting results by themselves

Is there a way for the referee to sense when the two captains have finished sorting, and then announce the sorting results as soon as the two captains finish sorting?

In fact, if you look carefully at the definition of the fork method in zone.js, you will know that fork actually just creates a child zone. zone.js will create a root zone when it is initialized, and then a sub-zone will be created under the root zone after everything passes the fork. In other words, zones have an inheritance relationship, which is officially called the composability of zones. And each child zone saves the objects of its parent zone; each parent zone can also listen to the events of the child zone.

Mark5: Composability: Each child zone saves the reference of its parent zone; each parent zone can also listen to the events of the child zone.

It is easy to understand that each child zone saves the reference of its parent zone, so how can each parent zone also monitor the events of the child zone? In fact, this is the most amazing part of zone.js. When zone.js is initialized, it has "tricked" many APIs - Monkey Patch, which encapsulates these asynchronous methods into asynchronous tasks in zone.js. At the same time, because many hook functions are defined in these tasks, zone.js can completely monitor the entire life cycle of these asynchronous tasks.

Mark6: Tracking asynchronous tasks

It is precisely because of this characteristic of zone that zone is often used in tracking and debugging of asynchronous tasks. For example, the incomprehensible error stack shown in motivation 2 above is the result of zone tracking asynchronous exceptions.

5_tracking_1676017737697.png

ultimate makeover

In the last version, we also forked a zone for the referee, and the zones of teamA and tramB are forked from the referee zone. After this processing, the entire life cycle of asynchronous task execution in teamA and tramB can be detected in the referee zone. Among them, the example only uses one of the many hooks in zone.js - onHasTask. This function will be called when a function is added to the execution queue or when there is no function.

In this example, teamA will update the execution result to the referee zone after execution; teamB will do the same. After both teams finish sorting, the referee zone prints the sorting results of the two captains as soon as possible through the configured callback function. So far, this example meets all our above requirements.

The source code is provided:

require('zone.js');
const thinking = require('./thinking');

// 创建一个裁判zone,当做teamA和teamB的父zone
const zoneJudgement = Zone.current.fork({
  name: 'judgement',
  properties: {
    // 存放teamA、teamB的排序结果
    result: [],
  },

  // 异步任务状态改变时的回调
  onHasTask: function (parentZoneDelegate, currentZone, targetZone, hasTaskState) {
    
    // setTimeout属于宏任务,!hasTaskState.macroTask标识有宏任务执行完毕
    if (!hasTaskState.macroTask) {
      // 裁判任务执行结束
      switch (targetZone.name) {
        case 'judgement':
          console.log(currentZone.get('result'));
          break;
        // A组排序任务执行结束
        case 'teamA':
          currentZone.get('result').push({
            teamA: targetZone.get('team').team,
          });
          break;
        // B组排序任务执行结束
        case 'teamB':
          currentZone.get('result').push({
            teamB: targetZone.get('team').team,
          });
          break;
        default:
          break;
      }
    }
    // 事件上抛
    parentZoneDelegate.onHasTask(parentZoneDelegate, currentZone, targetZone, hasTaskState);
  }
});

const zoneA = zoneJudgement.fork({
  name: 'teamA',
  properties: {
    team: {
      name: 'teamA',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
        thinking(() => {
          this.team.push(this.team.length + 1);
        });
      },
    },
  },
});
const zoneB = zoneJudgement.fork({
  name: 'teamB',
  properties: {
    team: {
      name: 'teamB',
      team: [],
      sort: function() {
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
        thinking(() => {
          this.team.unshift(this.team.length + 1);
        });
      },
    },
  },
});

function judgement() {
  zoneA.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });

  zoneB.run(() => {
    const currentZone = Zone.current;
    const team = currentZone.get('team');
    team.sort();
  });
}

zoneJudgement.run(judgement); // [ { teamA: [ 1, 2, 3 ] }, { teamB: [ 3, 2, 1 ] } ]

Summarize

Since then, the function of zone.js has been shown through a small example, and several features of zone.js have been briefly described based on the example. In the previous article, for the convenience of understanding, zone.js has been compared to LocalThread or sandbox. In fact, the capabilities of zone.js are far more than these analog objects, and it is also widely used in processing asynchronous tasks and tracking asynchronous errors. At this point, don't forget to watch the video introduction of zone.js given by the Angular team , which can better deepen your impression of this article.

Next, for the name zone, I personally feel that it is in place (foreigners are always very particular about their names). zone is translated into region, region. Take the regional division of our country as an example, the country, province, city, district, street... Each regional division at the same level is isolated from each other, and the first-level regional divisions can be nested. It has to be said that this nested and isolated feature is fully demonstrated in the above example.

6_map_1676018597784.png

This is the first article in this series, just a brief introduction to zone.js. Later, we will make further analysis and explanation on zone.js API, source code, and how to cooperate with Angular. If you are interested, you can continue~

About OpenTiny

OpenTiny is an enterprise-level component library solution that adapts to multiple terminals such as PC/mobile, covering Vue2/Vue3/Angular multi-technology stacks, and has efficiency-enhancing tools such as theme configuration system/middle-background template/CLI command line, which can help Developers efficiently develop web applications.

Core highlights:

  1. 跨端跨框架: Using the Renderless non-rendering component design architecture, a set of codes supports Vue2/Vue3, PC/Mobile at the same time, and supports function-level logic customization and full template replacement, with good flexibility and strong secondary development capabilities.
  2. 组件丰富: There are 100+ components on the PC side and 30+ components on the mobile side, including high-frequency components Table, Tree, Select, etc., with built-in virtual scrolling to ensure a smooth experience in big data scenarios. In addition to common components in the industry, we also provide Some unique feature components, such as: Split panel divider, IpAddress IP address input box, Calendar calendar, Crop image cropping, etc.
  3. 配置式组件: The component supports both template and configuration methods, suitable for low-code platforms. Currently, the team has integrated OpenTiny into the internal low-code platform, and has made a lot of optimizations for low-code platforms
  4. 周边生态齐全: Provides a TinyNG component library based on Angular + TypeScript, a TinyPro mid-background template with 10+ practical functions and 20+ typical pages, a TinyCLI engineering tool covering the entire process of front-end development, and a powerful online theme configuration platform TinyTheme

contact us:

For more video content, you can also follow the OpenTiny community, Station B/Douyin/Xiaohongshu/Video account.

Guess you like

Origin blog.csdn.net/OpenTiny/article/details/132475437