This is the second day of my participation in the August More text Challenge. For details, see: August More Text Challenge

preface

When I read the Channel chapter, I was deeply inspired. It turned out that Redux-Saga designed different types of channels according to different requirements and scenarios. Besides learning the usage of these channels, I also took a look at the source code for these apis in Redux-Saga and summarized it into the following article.

Reading the Prophet:

Reading the following requires experience with Redux-Saga and an understanding of how it works. Take a quick look at my previous article redux-Saga: Using ~ principles to analyze ~ understand design purpose.

By reading the following, you will learn:

  1. takeDesign principles of
  2. channelUsage and design principles, as well as use scenarios
  3. actionChannelUsage and design principles, as well as use scenarios
  4. eventChannelUsage and design principles, as well as use scenarios

The design principle of TAKE

Here’s some code to show how to use ‘take’ :

import { take, fork } from 'redux-saga/effects'

function* watchRequests() {
  while (true) {
    const {payload} = yield take('REQUEST')
    yield fork(handleRequest, payload)
  }
}

function* handleRequest(payload) {... }Copy the code

The watchRequests saga uses the classic take and fork code pattern:

  1. Calling take generates Effect(type:’ take ‘) instructs sagaMiddleware to listen for an action of type ‘REQUEST’. The watchRequests then block until the specific action is dispatched.

  2. After sagaMiddleware captures an action of type ‘REQUEST’, the watchRequests are desblocked and the action is taken. Fork is called after the payload is taken from the action to execute handleRequest non-blocking.

Understand the usage is not good, because this chapter to explain the principle of operation, then directly through the source code to understand the principle:

First look at the code for take:

packages/core/src/internal/io.js

/** * take can be either pattern (a string used to match action.type), or channel (the channel passed in, used to listen for changes in the channel). Here, we first analyze */ for take(pattern)
export function take(patternOrChannel = The '*', multicastPattern) {
  / / processing take (pattern)
  if (is.pattern(patternOrChannel)) {
    // If there is a second argument, a warning is printed
    if (is.notUndef(multicastPattern)) {
      console.warn(`take(pattern) takes one argument but two were provided. Consider passing an array for listening to several action types`)}* {* [IO]: true, * {* [IO]: true, * {* [IO]: true; false, * type:'TAKE', * payload:{pattern: patternOrChannel} * } */
    return makeEffect(effectTypes.TAKE, { pattern: patternOrChannel })
  }
  ...
  / / processing take (channel)
  if(is.channel(patternOrChannel)) { ... }}const makeEffect = (type, payload) = > ({
  [IO]: true.// this property makes all/race distinguishable in generic manner from other effects
  // currently it's not used at runtime at all but it's here to satisfy type systems
  combinator: false,
  type,
  payload,
})
Copy the code

The Effect generated by take is handed to sagaMiddleware after yield. SagaMiddleware calls the corresponding function according to the type of Effect (called EffectRunner in the redux-saga concept, For EffectRunner: runTakeEffect (type:’TAKE’), let’s look at the EffectRunner: runTakeEffect code for Effect(type:’TAKE’) :

packages/core/src/internal/effectRunnerMap.js

function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
  const takeCb = input= >{... cb(input) }try {
    channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)}catch (err) {
    cb(err, true)
    return
  }
  cb.cancel = takeCb.cancel
}
Copy the code

RunTakeEffect does two things that we don’t know why they do:

  1. The statementtakeCbBut why declare onetakeCb
  2. Call thechannel.takeTo deal withtakeCb, butchannelFrom what,channel.takeWhat does it do

With that in mind, env is an environment variable that is declared when sagaMiddleware is instantiated. That is, we create the Redux store in the following statement:

const store = createStore(reducer, {}, applyMiddleware(sagaMiddleware()));
Copy the code

In sagaMiddleware(), env.channel is declared as an instance of stdChannel. Next, look at the code for stdChannel:

packages/core/src/internal/channel.js

export function stdChannel() {
  // Declare chan as an instance of multicastChannel
  const chan = multicastChannel()
  Chan.put is enhanced. Put (input) is executed regardless of the condition, so this enhancement does not affect the basic logic
  const { put } = chan
  chan.put = input= > {
    // Check whether input is an action for the Saga internal Dispatch
    if (input[SAGA_ACTION]) {
      put(input)
      return
    }
    asap(() = > {
      put(input)
    })
  }
  return chan
}
Copy the code

StdChannel is actually an enhancement of multicastChannel. Let’s look directly at the multicastChannel code:

export function multicastChannel() {
  // There is a flag bit indicating that the channel is closed, but we do not need to consider this for this analysis
  //, I will also delete the code involving close to simplify
  // let closed = false
  /** * makes currentTakers and nextTakers statements. */ is explained below
  let currentTakers = []
  let nextTakers = currentTakers

  // This method makes sure that nextTakers and currentTakers don't point to the same array
  const ensureCanMutateNextTakers = () = > {
    if(nextTakers ! == currentTakers) {return
    }
    nextTakers = currentTakers.slice()
  }

  return {
    [MULTICAST]: true./** * sagaMiddleware executes channel.put(action) when an action is dispatched, * so input is action */
    put(input) {
      // Make currentTakers the name of the string that points to nextTakers before traversal
      const takers = (currentTakers = nextTakers)
      // Select the taker that matches action.type
      for (let i = 0, len = takers.length; i < len; i++) {
        const taker = takers[i]
	// MATCH is detected by the MATCH attribute in taker
        if (taker[MATCH](input)) {
          ** Take taker out of nextTakers at the end of the list ** take taker out of nextTakers at the end of the list ** take taker out of nextTakers at the end of the list Will invoke the above ensureCanMutateNextTakers, * guarantee currentTakers with nextTakers point is not the same array, then after nextTakers change, * of currentTakers traversal unaffected * /
          taker.cancel()
          taker(input)
        }
      }
    },
    /** * When runTakeEffect is executed, the channel. Take is executed to pass taker into runTakeEffect */
    take(cb, matcher = matchers.wildcard) {
      // Mount matcher (the MATCH function that checks if the action matches) to the MATCH attribute
      cb[MATCH] = matcher
      // Make sure currentTakers and nextTakers point to different arrays before changing nextTakers
      ensureCanMutateNextTakers()
      // Save the CB into nextTakers
      nextTakers.push(cb)
      // Define the cancel property of cb to be called before the execution of CB, thus removing CB from nextTakers during the call
      cb.cancel = once(() = > {
	// Make sure currentTakers and nextTakers point to different arrays, whether adding or deleting them
        ensureCanMutateNextTakers()
	// Remove cb from nextTakers
        remove(nextTakers, cb)
      })
    },
  }
}
Copy the code

The two important functions in a channel are take and put. The former is the function that stores the callback, and the latter is the function that executes the stored callback.

We know that runTakeEffect calls channel.take internally, so where does channel.put get called? SagaMiddleware is called internally when an action is issued. Let’s take a look at the following part of the source code for sagaMiddlewareFactory (factory function that instantiates sagaMiddlewareFactory) :

packages/core/src/internal/middleware.js

export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ... options } = {}) {...// Declare sagaMiddleware in factory mode and go back out
  function sagaMiddleware({ getState, dispatch }) {...// Redux middleware paradigm
    return next= > action= > {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      // Call Next to transfer the action to the next middleware or store.dispatch
      const result = next(action) // hit reducers
      // Here we go!! Whenever there is an action, it is executed using channel.put
      channel.put(action)
      return result
    }
  }

  sagaMiddleware.run = (. args) = >{... }return sagaMiddleware
}

Copy the code

The above analysis of a large list of source code, the following we can summarize:

  1. When take is called in Saga, sagaMiddleware calls runTakeEffect, which generates Taker and calls channel.take(taker). Channel. take puts the passed callback into the takers array. The Saga will be blocked until the generated Taker is executed.

  2. SagaMiddleware calls channel.put(Action) when an action is dispatched. Channel. put traverses takers to retrieve the matching callback function for execution. After execution, Taker will let the corresponding saga exit the block caused by the take and get the distributed action.

The diagram is shown as follows:

channel

use

Let’s continue with the watchRequests that we started with:

import { take, fork } from 'redux-saga/effects'

function* watchRequests() {
  while (true) {
    const {payload} = yield take('REQUEST')
    yield fork(handleRequest, payload)
  }
}

function* handleRequest(payload) {... }Copy the code

watchRequestsThere’s a catch:forkIs a non-blockingAPI. Therefore, if there is a large number of correspondingactionIs captured, thenhandleRequestWill be called over and over again ifhandleRequestWith network request logic, a large number of network requests will be executed at the same time.

Suppose our solution to the above disadvantage is to fork a new handleRequest until one of the three handlerequests has finished executing if there are at most three handlerequests executing at the same time.

But what about the above solution? Redux-saga provides a channel to make this logic very easy. Look directly at the following code:

import { channel } from 'redux-saga'
import { take, fork, call } from 'redux-saga/effects'

function* watchRequests() {
  // create a channel to queue incoming requests
  // Create a channel to store incoming information
  const chan = yield call(channel)

  // create 3 worker 'threads'
  // Create 3 'worker threads', which are not really threads, just because' fork 'is a non-blocking API for non-blocking calls to FN.
  for (var i = 0; i < 3; i++) {
    yield fork(handleRequest, chan)
  }

  while (true) {
    const {payload} = yield take('REQUEST')
    // When a corresponding action is sent, the action.payload is stored in the chan
    yield put(chan, payload)
  }
}

function* handleRequest(chan) {
  while (true) {
    // Run the following logic to see if any information has been stored in chan
    const payload = yield take(chan)
    // process the request}}Copy the code

It’s very short, and compared to writing your own limiting function. Using a channel allows us to test better.

Another nice feature of the channel API is that the default channel generates an unlimited number of channels to store any input, but if you want to limit the number, you can call a channel with a buffer parameter in redux-saga, such as:

import { buffers,channel } from 'redux-saga'

function* watchRequests() {
  // Accept a maximum of 5 incoming messages.
  Sliding means that if a new action is assigned, the earliest action passed in will be discarded.
  // In fact, it is to save the new and discard the old.
  const chan = yield call(channel, buffers.sliding(5))... }Copy the code

In fact, buffers have several modes besides sliding:

  • Buffers.None (): Any input information stored is simply discarded and will not be cached.
  • buffers.fixed(limit): The input informationWill be cached until the number exceeds the upper limit, and then an error will be thrown. herelimitThe default value of is 10.
  • Buffers. Expanding (initialSize): Input information is cached until the number exceeds the upper limit.
  • buffers.dropping(limit): actionWill be cached until the number exceeds the upper limit, neither throwing errors nor caching new onesThe input information.
  • Buffers. Sliding (limit): The input will be cached until the number of inputs exceeds the upper limit. The first input will be removed and the latest input will be cached.

Now that the usage is over, let’s take a look at how the source code for this channel works.

Source code analysis

Instead of looking at the source code for channel, let’s look at the source code for put and take, because there are two new uses we haven’t seen in the example above:

  1. handleRequestIn theyield take(chan)Here,takeThe parameter of is a channel.
  2. watchRequestsIn theyield put(chan, payload)With before,putThe parameters of are synchronizedaction. This is the passageway andaction.payload.

Let’s look at put and its runPutEffect in turn:

packages\core\src\internal\io.js

export function put(channel, action) {
  // When an action is called in the form put(action)
  if (is.undef(action)) {
    action = channel
    channel = undefined
  }
  /** * generates the corresponding Effect with the following structure: * {* [IO]: true, * combinator: false, * type:'PUT', * payload:{channel, action} *} */
  return makeEffect(effectTypes.PUT, { channel, action })
}
Copy the code

packages\core\src\internal\effectRunnerMap.js

function runPutEffect(env, { channel, action, resolve }, cb) {
  /** * as an aside, you can skip: * ASAP is used to make incoming callbacks execute in sequence without preemption. * A program dispatches two actions in turn, which are captured by two different saga. * Then there is a call to put the EffectCreator in both saga. * At this point, if not wrapped in ASAP, it is possible that the Effect from the first saga is being processed (possibly because the asynchronous request is not finished yet) * Then the Effect from the second saga is yielded and processed while the first Effect is still waiting. * This way, if the above two actions must be delivered in order (the first saga affects the parameters of the second saga), the absence of the ASAP package will result in different results for each run. * / 
  asap(() = > {
    let result
    try {
      * 2. Put (channel, action) : * 2. Put (channel, action) : Call the channel. The put (action) * /
      result = (channel ? channel.put : env.dispatch)(action)
    } catch (error) {
      cb(error, true)
      return
    }
    // If the result is a Promise instance, call cb after the Promise state changes from pending to fulfilled
    if (resolve && is.promise(result)) {
      resolvePromise(result, cb)
    } else {
      cb(result)
    }
  })
  // Put effects are not cancelable
}
Copy the code

A call to put(chan, payload) in a saga triggers channel.put(payload).

Now let’s look again at take and runTakeEffect with the question of exploring take(channel) :

export function take(patternOrChannel = The '*', multicastPattern) {
  // Processing when take(pattern)
  if (is.pattern(patternOrChannel)) {
   if (is.notUndef(multicastPattern)) {
      console.warn(`take(pattern) takes one argument but two were provided. Consider passing an array for listening to several action types`)}return makeEffect(effectTypes.TAKE, { pattern: patternOrChannel })
  }
  ...
  // Take (channel)
  if (is.channel(patternOrChannel)) {
    // Prints a warning if there is a second parameter
    if (is.notUndef(multicastPattern)) {
      console.warn(`take(channel) takes one argument but two were provided. Second argument is ignored.`)}/ / generated Effect
    return makeEffect(effectTypes.TAKE, { channel: patternOrChannel })
  }
}
Copy the code

It can be seen from the above that the Effect payload structure generated by different parameter types is different:

  • take(pattern)Generated:Effect.payloadfor{pattern:pattern}
  • take(channel)Generated:Effect.payloadfor{channel:channel}
/** * Different Effect will result in different channel of this function. If the channel of the Effect payload exists, * then the channel of the following function will take effe.payload. * If it does not exist, it simply takes the channel in the environment variable, the instance of the stdChannel that manages the take */
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
  const takeCb = input= >{... cb(input) }try {
    channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)}catch (err) {
    cb(err, true)
    return
  }
  cb.cancel = takeCb.cancel
}
Copy the code

Thus, we know that yield take(channel) in saga triggers the execution of channe.take (takeCb,matcher(pattern)).

Now that we knowchannelThe main call in theputandtake. Well, we’ll seechannelSource code, the main understanding of thetakeandputThe method will do.

Note: The channel here is not the same as multicastChannel as described in the design principles of Take in the previous chapter.

packages/core/src/internal/channel.js

// In the following channel, we only analyze take and put and the parts of the code involved. The rest is omitted
export function channel(buffer = buffers.expanding()) {
  let takers = []
  Takers is not the same as multicastChannel in that it's not the decision that makes a loop around the match. Takers, if present, are removed from the beginning of the array and executed. * /
  function put(input) {
    But it's the name of the buffer that makes the array empty. * Given that channel.take is used to store the callback into takers, but channel.take is called at the end of runTakeEffect. * Saga exit take block when channel-put is called. At this point, there is a CB inside channel-. takers, so take it out and execute * Saga continues, assuming it gets stuck in a call-caused block. But there's a need for the action to be distributed, and * channel.put is called again, but channel.takers is empty, so the input is stored in the buffer. * When channell. take is called, the buffer is checked to see if it is empty. If it is not empty, the buffer input is directly fetched and executed, i.e. Cb (input). * If empty, save cb at channel.takers. Wait for the next call to channel.put to execute. * /
    if (takers.length === 0) {
      return buffer.put(input)
    }
    const cb = takers.shift()
    cb(input)
  }

  function take(cb) {
    // If the buffer is not empty, the buffer is fetched
    if(! buffer.isEmpty()) { cb(buffer.take())// Make it at the end of the box
    } else {
      takers.push(cb)
      Cancel is used to remove the name of the cb. This method is used when closing the pipe
      cb.cancel = () = > {
        remove(takers, cb)
      }
    }
  }

  return{ take, put, ... }}Copy the code

From the above source code, we know how take and put are complementary to each other in a channel. In the form of a flowchart, it is shown as follows:

With that in mind, let’s go back to the example we started with with a channel, where the question can be solved instantly:

  1. handleRequestIn theyield take(chan)The resultingTake EffectWill triggerchannel.takeThe execution of the,channel.takeWill do what, I also need not write more, are above the content.
  2. watchRequestsIn theyield put(chan, payload)The resultingPut EffectWill triggerchannel.putThe execution of the,channel.putWill do what, I also need not write more, are above the content.

actionChannel

use

Let’s look again at watchRequests in the Design Principles section of Take:

import { take, fork } from 'redux-saga/effects'

function* watchRequests() {
    while (true) {
        const {payload} = yield take('REQUEST')
        yield fork(handleRequest, payload)
    }
}

function* handleRequest(payload) {... }Copy the code

In channell. usage we mentioned one of the pitfalls in watchRequests: if the matched action is dispatched at high frequency over a short period of time, many handleRequest tasks may be executing at the same time. In the channel. usage section, the solution used is to limit the number of handlerequests executed at a time.

Let’s say we want a different solution: we want to execute handleRequest sequentially. Limit one handleRequest to be running at a time. Let’s change the number of “worker threads” created to 1, as shown below:

import { channel } from 'redux-saga'
import { take, fork, call } from 'redux-saga/effects'

function* watchRequests() {
  // create a channel to queue incoming requests
  // Create a channel to store incoming information
  const chan = yield call(channel)

  // Create a 'worker' thread
  yield fork(handleRequest, chan)

  while (true) {
    const {payload} = yield take('REQUEST')
    // When a corresponding action is sent, the action.payload is stored in the chan
    yield put(chan, payload)
  }
}

function* handleRequest(chan) {
  while (true) {
    // Run the following logic to see if any information has been stored in chan
    const payload = yield take(chan)
    // process the request}}Copy the code

But actually, for serial processing. Redux-saga provides a much better API: actionChannel. Let’s see how the above requirement would look if implemented using actionChannel:

import { take, actionChannel, call } from 'redux-saga/effects'

function* watchRequests() {
    // 1- Issue an ActionChannel Effect to create a pipe for storing actions that are too late to be processed
    const requestChan = yield actionChannel('REQUEST')
    while (true) {
        // 2- Remove the action from the pipe
        const {payload} = yield take(requestChan)
        // 3- Call handleRequest to handle the action
        // Note that the call blocking API is invoked. If the fork non-blocking API is called, the serialization effect is not achieved
        yield call(handleRequest, payload)
    }
}

function* handleRequest(payload) {... }Copy the code

How, compared to the two implementation of the code, using actionChannel is much cleaner. Also, as with channels, we can restrict the mode in which the generated channel stores the action by passing a buffer to the second parameter of the actionChannel, as shown below:

import { buffers } from 'redux-saga'
import { actionChannel } from 'redux-saga/effects'

function* watchRequests() {
    const requestChan = yield actionChannel('REQUEST', buffers.sliding(5))... }Copy the code

Source code analysis

From the above usage, the actionChannel is also used to generate Effect. For each new Effect, we analyze not only the corresponding EffectCreator (the external API that generates the Effect, such as take and actionChannel), but also the EffectRunner (the internal function that processes the Effect).

Look at the actionChannel:

packages/core/src/internal/io.js

export function actionChannel(pattern, buffer) {
  * {* [IO]: true, * combinator: false, * type: "ACTION_CHANNEL", * payload: {pattern,buffer} * } */
  return makeEffect(effectTypes.ACTION_CHANNEL, { pattern, buffer })
}
Copy the code

Next, look at the EffectRunner: runChannelEffect of actionChannel

packages/core/src/internal/effectRunnerMap.js

function runChannelEffect(env, { pattern, buffer }, cb) {
  / / generated channel
  const chan = channel(buffer)
  // Generate a function to match the pattern
  const match = matcher(pattern)
  /** * generate taker and store it in env.channel via env.channel.take. * taker is executed when action matching match is distributed * Note: Env.channel and the chan generated by this function are two different channels * env.channel is generated when sagaMiddleware is instantiated to handle the Effect generated by take, The chan generated here is dedicated to handling the Effect of the actionChannel, which is an instance of the channel */ 
  const taker = action= > {
    /** * If the actionChannel is not disabled, * store Taker again in env.channel when taker is taken out of env.channel. This ensures that whenever a match action * is distributed, Taker will be taken out and executed */
    if(! isEnd(action)) { env.channel.take(taker, match) }// Insert action into chan via put
    chan.put(action)
  }

  const { close } = chan
  // The wrap mode is changed to change the chan.close function to remove taker from env.channel after executing taker
  chan.close = () = > {
    taker.cancel()
    close()
  }
  // Save Taker to env.channel
  env.channel.take(taker, match)
  // This is equivalent to next(chan). At this point saga can continue execution after obtaining chan
  cb(chan)
}
Copy the code

As seen above, actionChannel is implemented in conjunction with env.channel and channel.

Since the example above uses take, and is called through the take(channel) method, here is the runTakeEffect source code again for easy understanding:

/** * When channel is called by take(channel), the * channel in runTakeEffect's second parameter is the channel passed in take. * /
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
  const takeCb = input= >{... cb(input) }try {
    channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)}catch (err) {
    cb(err, true)
    return
  }
  cb.cancel = takeCb.cancel
}
Copy the code

To sum up, there is the following flowchart:

But for the calls channel.take and channel.put in the above flow chart, I only made the case of CB inside channel.takers. To see how it works without CB, refer to the flowchart I drew in channel source analysis.

eventChannel

Method of use

EventChannel is a factory function (unlike actionChannel, eventChannel is not an EffectCreator) that creates a channel whose event source is separate from the Redux Store. A simple example shows the following:

import { eventChannel, END } from 'redux-saga'
import { take, put, call } from 'redux-saga/effects'

function countdown(secs) {
  / * * * the eventChannel parameter for subscription function, the paradigm of emitter = > (() = > {} | | void) * each call emitter is triggered capture the channel (namely take (chan)) of the saga continues executing * /
  return eventChannel(emitter= > {
      const iv = setInterval(() = > {
        secs -= 1
        if (secs > 0) {
          * It is recommended that the data structures for incoming data be pure functions, i.e., ** is better than emitter(number), {number} */
          emitter(secs)
        } else {
          END is an action defined by redux-saga to close a channel * after closing the channel, no information is passed to the channel */
          emitter(END)
        }
      }, 1000);
      /** * This function is used to unsubscribe if the result is one. * this function is used to close the channel. */ is called inside redux-saga
      return () = > {
        clearInterval(iv)
      }
    }
  )
}

export function* saga() {
  const chan = yield call(countdown, 5)
  try {    
    while (true) {
      // Closing the channel causes saga to jump directly to the finally block
      let seconds = yield take(chan)
      console.log(`countdown: ${seconds}`)}}finally {
    console.log('countdown terminated')}}Copy the code

After the above saga is sagamiddleware.run (saga), the page console will have the following effect:

Similarly, eventChannel supports a caching pattern that controls incoming information using buffers. The second parameter of eventChannel can be passed as a buffer parameter.

The official website provides an example of eventChannel-based socket communication processing saga, interested to see.

Source code analysis

The eventChannel API is a factory function that generates channel instances.

packages\core\src\internal\channel.js

export function eventChannel(subscribe, buffer = buffers.none()) {
  let closed = false
  let unsubscribe
  // Initialize the channel instance
  const chan = channel(buffer)
  // Define the close function
  const close = () = > {
    if (closed) {
      return
    }

    closed = true

    if (is.func(unsubscribe)) {
      unsubscribe()
    }
    chan.close()
  }
  /** * When an eventChannel is instantiated, it will call the SUBSCRIBE function * with input=>{} which is emitter *. You can see that when an Emitter executes, it will call channel.put */ 
  unsubscribe = subscribe(input= > {
    if (isEnd(input)) {
      close()
      return
    }
    chan.put(input)
  })

  // Enable unsubscribe to be called only once, similar to once in Lodash
  unsubscribe = once(unsubscribe)
  /** * If the channel is closed during subscribe because of a call to the emitter(END) * then closed is true */
  if (closed) {
    unsubscribe()
  }

  return {
    take: chan.take,
    flush: chan.flush,
    close,
  }
}
Copy the code

Combining the above source code with the channel we learned in the previous section, we can describe the eventChannel. The running process using the examples in the method:

Afterword.

After so much writing, it’s finally done. Writing isn’t easy, so if you find it rewarding and in a good mood, please give it a thumbs up.