Dio’s CancelToken is used. This article examines how the CancelToken is used to cancel network requests from the source code. The relevant contents are as follows:

  • Implementation of the CanelToken class
  • CancelToken How to cancel a network request

CancelToken class

The CalcelToken class doesn’t have much code, so let’s just copy it and go through it one by one.

import 'dart:async';
import 'dio_error.dart';
import 'options.dart';

/// You can cancel a request by using a cancel token.
/// One token can be shared with different requests.
/// when a token's [cancel] method invoked, all requests
/// with this token will be cancelled.
class CancelToken {
  CancelToken() {
    _completer = Completer<DioError>();
  }

  /// Whether is throw by [cancel]
  static bool isCancel(DioError e) {
    return e.type == DioErrorType.cancel;
  }

  /// If request have been canceled, save the cancel Error.
  DioError? _cancelError;

  /// If request have been canceled, save the cancel Error.
  DioError? get cancelError => _cancelError;

  late Completer<DioError> _completer;

  RequestOptions? requestOptions;

  /// whether cancelled
  bool getisCancelled => _cancelError ! =null;

  /// When cancelled, this future will be resolved.
  Future<DioError> get whenCancel => _completer.future;

  /// Cancel the request
  void cancel([dynamic reason]) {
    _cancelError = DioError(
      type: DioErrorType.cancel,
      error: reason,
      requestOptions: requestOptions ?? RequestOptions(path: ' ')); _cancelError! .stackTrace = StackTrace.current; _completer.complete(_cancelError); }}Copy the code

First of all, looking at the comments, we can see that a very useful thing about a CancelToken is that a CancelToken can be associated with multiple requests, and a CancelToken can cancel multiple associated requests simultaneously. This is useful when we have multiple requests for a page. Most of them are attributes:

  • _cancelError: Indicates the cancellation error information stored after the cancellation. You can obtain the cancellation error information in GET mode.
  • _completerA:Completer<DioError>Object,CompleterThis is an abstract class for managing asynchronous operation events. Build time returns oneFutureObject, you can call the correspondingcomplete(corresponds to normal completion) orcompleteError(For error handling). This property is private and cannot be accessed externally.
  • requestOptions:RequestOptionsObjects are optional properties of the request (e.gheaders, request parameters, request mode, etc.), can be empty. This property is public, which means it can be modified externally.
  • isCancelled: Boolean value, used to indicate whether to cancel, actually pass_cancelErrorNull or not. If it is not null, it is cancelled.
  • whenCancel: In fact_completerthefutureObject that can be used to process the response to an operation_completerMade a wrapper that only exposed itfutureObject.
  • cancelThe cancellation method, also known as the core method, builds aDioErrorObject (used to store cancelled errors), here if passed when calledreasonObject, will alsoreasonPassed to theerrorParameter, and then therequestOptionsParameter, ifrequestOptionsIf empty, an empty one is builtRequestOptionsObject. The current stack information is also stored in the _cancelError stackTrace to facilitate stack tracing. And finally call_completer.completeAsynchronous methods. So this is the key method, so let’s see what this method does.

Completer class

Let’s go into the Completer class and see what the complete method does:

/// All listeners on the future are informed about the value.
void complete([FutureOr<T>? value]);
Copy the code

You can see that this method is an abstract method, meaning it should be implemented by the concrete implementation class of the Completer. Also, as you can see from the comment, this method is a generic value object that calls the listener to tell the complete method. This can be interpreted as telling the observer to process the object. If the cancelToken parameter is used at the time of the request, a listener is added to the cancelToken. Moving on to Dio’s implementation of the request code.

Dio request code

To the source code of Dio Dio. The dart to see, found that all the request is actually fetch < T > (RequestOptionsrequestOptions) alias, all is the actual request is done by this method. Let’s look at the source code for this method. The code is quite long, so if you are interested you can read it carefully, but we only find the code related to cancelToken here.

@override
Future<Response<T>> fetch<T>(RequestOptions requestOptions) async {
  if(requestOptions.cancelToken ! =null) { requestOptions.cancelToken! .requestOptions = requestOptions; }if(T ! =dynamic &&
      !(requestOptions.responseType == ResponseType.bytes ||
          requestOptions.responseType == ResponseType.stream)) {
    if (T == String) {
      requestOptions.responseType = ResponseType.plain;
    } else{ requestOptions.responseType = ResponseType.json; }}// Convert the request interceptor to a functional callback in which
  // we can handle the return value of interceptor callback.
  FutureOr Function(dynamic) _requestInterceptorWrapper(
    void Function(
      RequestOptions options,
      RequestInterceptorHandler handler,
    )
        interceptor,
  ) {
    return (dynamic _state) async {
      var state = _state as InterceptorState;
      if (state.type == InterceptorResultType.next) {
        return listenCancelForAsyncTask(
          requestOptions.cancelToken,
          Future(() {
            return checkIfNeedEnqueue(interceptors.requestLock, () {
              var requestHandler = RequestInterceptorHandler();
              interceptor(state.data, requestHandler);
              returnrequestHandler.future; }); })); }else {
        returnstate; }}; }// Convert the response interceptor to a functional callback in which
  // we can handle the return value of interceptor callback.
  FutureOr<dynamic> Function(dynamic) _responseInterceptorWrapper(
      interceptor) {
    return (_state) async {
      var state = _state as InterceptorState;
      if (state.type == InterceptorResultType.next ||
          state.type == InterceptorResultType.resolveCallFollowing) {
        return listenCancelForAsyncTask(
          requestOptions.cancelToken,
          Future(() {
            return checkIfNeedEnqueue(interceptors.responseLock, () {
              var responseHandler = ResponseInterceptorHandler();
              interceptor(state.data, responseHandler);
              returnresponseHandler.future; }); })); }else {
        returnstate; }}; }// Convert the error interceptor to a functional callback in which
  // we can handle the return value of interceptor callback.
  FutureOr<dynamic> Function(dynamic, StackTrace stackTrace)
      _errorInterceptorWrapper(interceptor) {
    return (err, stackTrace) {
      if (err is! InterceptorState) {
        err = InterceptorState(assureDioError(
          err,
          requestOptions,
          stackTrace,
        ));
      }

      if (err.type == InterceptorResultType.next ||
          err.type == InterceptorResultType.rejectCallFollowing) {
        return listenCancelForAsyncTask(
          requestOptions.cancelToken,
          Future(() {
            return checkIfNeedEnqueue(interceptors.errorLock, () {
              var errorHandler = ErrorInterceptorHandler();
              interceptor(err.data, errorHandler);
              returnerrorHandler.future; }); })); }else {
        throwerr; }}; }// Build a request flow in which the processors(interceptors)
  // execute in FIFO order.

  // Start the request flow
  var future = Future<dynamic>(() => InterceptorState(requestOptions));

  // Add request interceptors to request flow
  interceptors.forEach((Interceptor interceptor) {
    future = future.then(_requestInterceptorWrapper(interceptor.onRequest));
  });

  // Add dispatching callback to request flow
  future = future.then(_requestInterceptorWrapper((
    RequestOptions reqOpt,
    RequestInterceptorHandler handler,
  ) {
    requestOptions = reqOpt;
    _dispatchRequest(reqOpt).then(
      (value) => handler.resolve(value, true),
      onError: (e) {
        handler.reject(e, true); }); }));// Add response interceptors to request flow
  interceptors.forEach((Interceptor interceptor) {
    future = future.then(_responseInterceptorWrapper(interceptor.onResponse));
  });

  // Add error handlers to request flow
  interceptors.forEach((Interceptor interceptor) {
    future = future.catchError(_errorInterceptorWrapper(interceptor.onError));
  });

  // Normalize errors, we convert error to the DioError
  return future.then<Response<T>>((data) {
    return assureResponse<T>(
      data is InterceptorState ? data.data : data,
      requestOptions,
    );
  }).catchError((err, stackTrace) {
    var isState = err is InterceptorState;

    if (isState) {
      if ((err as InterceptorState).type == InterceptorResultType.resolve) {
        returnassureResponse<T>(err.data, requestOptions); }}throw assureDioError(
      isState ? err.data : err,
      requestOptions,
      stackTrace,
    );
  });
}
Copy the code

If the cancelToken is not null, set the cancelToken to the requestOptions of the current request. All request parameters are cached in cancelToken.

Next comes the handling of interceptors, including request interceptors, response interceptors, and error interceptors. Each defines a built-in interceptor wrapper method that wraps interceptors into functional callbacks for uniform interception handling. We’ll skip this, but the point is that each interceptor wrapper method has a listenCancelForAsyncTask method that is called when the interceptor state is next (indicating that there are interceptors to handle) and returns its return value. The first argument to this method is the cancelToken. Listen for cancellation events for asynchronous tasks, so let’s see what this method does.

Asynchronous task cancellations event listening

The listenCancelForAsyncTask method simply returns a Future.any object and then performs subsequent processing in response to the cancelToken cancellation event if the cancelToken is not empty. The feature of Future.any is to assemble a series of asynchronous functions with a uniform interface and execute them in order (after the previous interceptor’s processing is completed, it is wheeled to the next interceptor for execution) to execute onValue (normal) and onError (exception) methods.

If the cancelToken is not empty, the cancelToken’s cancellation event method is placed in the interceptor and an exception is thrown when it occurs. This is essentially pre-intercepting, meaning that if the request has not been processed (not queued for processing), the interceptor is used to intercept it. If the request has been queued for processing, it needs to be processed in the queue scheduler.

static Future<T> listenCancelForAsyncTask<T>(
      CancelToken? cancelToken, Future<T> future) {
  return Future.any([
    if(cancelToken ! =null) cancelToken.whenCancel.then((e) => throw e),
    future,
  ]);
}
Copy the code
/// Returns the result of the first future in [futures] to complete.
///
/// The returned future is completed with the result of the first
/// future in [futures] to report that it is complete,
/// whether it's with a value or an error.
/// The results of all the other futures are discarded.
///
/// If [futures] is empty, or if none of its futures complete,
/// the returned future never completes.
static Future<T> any<T>(可迭代<Future<T>> futures) {
  var completer = new Completer<T>.sync(a);void onValue(T value) {
    if(! completer.isCompleted) completer.complete(value); }void onError(Object error, StackTrace stack) {
    if(! completer.isCompleted) completer.completeError(error, stack); }for (var future in futures) {
    future.then(onValue, onError: onError);
  }
  return completer.future;
}
Copy the code

Request scheduling

The actual request scheduling is done in the _dispatchRequest method in dio_mixin.dart, which is actually called in the fetch method above. This method uses the canlToken in two ways. The first is the whenCancel property of cancelToken passed into the FETCH method of httpClientAdapter. The httpClientAdapter reference is for a callback to tell the listener that the request was cancelled after the request is cancelled. The other is called checkCancelled, which checks to see if the request is to be stopped.

responseBody = awaithttpClientAdapter.fetch( reqOpt, stream, cancelToken? .whenCancel, );// If the request has been cancelled, stop request and throw error.
static void checkCancelled(CancelToken? cancelToken) {
  if(cancelToken ! =null&& cancelToken.cancelError ! =null) {
    throw cancelToken.cancelError!;
  }
}
Copy the code

From here we can get a general idea of the basic mechanism. In fact we call cancelToken cancel method, marked the error information of the cancelToken cancelError, to let _dispatchRequest is scheduled to test whether cancel. If cancelError is not detected in _dispatchRequest, a cancelError will be raised to abort the current and subsequent requests.

conclusion

In terms of source code, there are a bunch of Futuresand various wrapping methods that are pretty hard to read, which is what makes Dio great, and it makes the average person want to blow up these web request headers. The mechanism of CancelToken is as follows:

  • Pre-cancellation: If the requested cancelToken is not empty, the asynchronous processing of the cancelToken will be added to the interceptor. After cancellation, the request will be blocked at the interceptor link instead of the subsequent scheduling link.
  • In-process cancellation: If the request has already been queued, cancellation will throw an exception that will abort the request.
  • Later cancellation: The request has been sent, and when the server returns the result, it intercepts the response processing (including the error), aborting subsequent response processing. Even if the server does return data, it will be intercepted, but this does not reduce the load on the server, only to avoid subsequent data processing.