上一篇Flutter 入門與實戰(三十):Dio之戛然而止講了 Dio 的 CancelToken 的使用,本篇來從原始碼解析 CancelToken 是如何實現取消網路請求的。相關的內容如下:
- CanelToken 類的實現
- CancelToken 如何取消網路請求
CancelToken 類
CalcelToken類的程式碼並不多,我們直接複製下來一個個過一遍。
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 get isCancelled => _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);
}
}
複製程式碼
首先看註釋,我們可以瞭解到 CancelToken 的一個非常有用的地方,一個 CancelToken 可以和多個請求關聯,取消時可以同時取消多個關聯的請求。這對於我們一個頁面有多個請求時非常有用。大部分的是一些屬性:
_cancelError
:被取消後儲存的取消錯誤資訊,對外可以通過 get 方式可以獲取。_completer
:一個Completer<DioError>
物件,Completer
本是一個抽象類,用於管理非同步操作事件。構建時返回的是一個Future
物件,可以呼叫對應的complete
(對應正常完成) 或completeError
(對應錯誤處理)。該屬性為私有屬性,外部不可訪問。requestOptions
:RequestOptions
物件,是請求的一些可選屬性(比如headers
,請求引數,請求方式等等),可以為空。該屬性是公共屬性,說明可以在外部修改。isCancelled
:布林值,用於標識是否取消,實際是通過_cancelError
是否為空判斷的,如果不為空說明是被取消了。whenCancel
:實際就是_completer
的future
物件,可以用來處理操作的響應,這樣也相當於對_completer
做了一個封裝,只暴露了其future
物件。cancel
:取消方法,也就是核心方法了,這個方法構建了一個DioError
物件(用於儲存取消的錯誤),這裡如果呼叫時傳了reason
物件,也會將reason
傳遞到error
引數中,然後就是requestOptions
引數,如果requestOptions
為空則構建一個空的RequestOptions
物件。同時還會將當前的堆疊資訊存入到_cancelError 的 stackTrace 中,方便跟蹤堆疊資訊。最後是呼叫_completer.complete
非同步方法。這個是關鍵方法,我們看一下這個方法做了什麼事情。
Completer類
我們進入 Completer 類來看一下 complete方法做了什麼事情:
/// All listeners on the future are informed about the value.
void complete([FutureOr<T>? value]);
複製程式碼
可以看到這個方法是一個抽象方法,意味著應該是由 Completer
的具體實現類來實現的。同時從註釋可以看到,這個方法是會呼叫監聽器來告知 complete
方法的 value
泛型物件。可以理解為是通知觀察者處理該物件。那我們就可以猜測是在請求的時候,如果有 cancelToken
引數時,應該是給 cancelToken
增加了一個監聽器。繼續來看 Dio 的請求程式碼的實現。
Dio 的請求程式碼
到 Dio 的原始碼 dio.dart看的時候,發現全部請求其實是fetch<T>(RequestOptionsrequestOptions)
的別名,也就是實際全部的請求都是通過該方法完成的。我們看一下這個方法的原始碼。程式碼很長,如果有興趣的可以仔細閱讀一下,我們這裡只找出與 cancelToken 相關的程式碼。
@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);
return requestHandler.future;
});
}),
);
} else {
return state;
}
};
}
// 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);
return responseHandler.future;
});
}),
);
} else {
return state;
}
};
}
// 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);
return errorHandler.future;
});
}),
);
} else {
throw err;
}
};
}
// 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) {
return assureResponse<T>(err.data, requestOptions);
}
}
throw assureDioError(
isState ? err.data : err,
requestOptions,
stackTrace,
);
});
}
複製程式碼
首先在一開始就檢查了當前請求 requestOptions
的 cancelToken
是不是為空,如果不為空,就設定 cancelToken
的 requestOptions
為當前請求的requestOptions
,相當於在 cancelToken
快取了所有的請求引數。
接下來是攔截器的處理,包括了請求攔截器,響應攔截器和錯誤攔截器。分別定義了一個內建的攔截器包裝方法,用於將攔截器封裝為函式式回撥,以便進行統一的攔截處理。這個我們跳過,關鍵是每個攔截器的包裝方法都有一個listenCancelForAsyncTask
方法,在攔截器狀態是 next
(說明還有攔截要處理)的時候,會呼叫該方法並返回其返回值。這個方法第一個引數就是 cancelToken
。從方法名看就是監聽非同步任務的取消事件,看看這個方法做了什麼事情。
非同步任務取消事件監聽
listenCancelForAsyncTask
方法很簡單,其實就是返回了一個 Future.any 物件,然後在這個 Future裡,如果 cancelToken
不為空的話,在響應 cancelToken
的取消事件時執行後續的處理。Future.any
的特性是將一系列的非同步函式按統一的介面組裝起來,按次序執行(上一個攔截器的處理完後輪到下一個攔截器執行),以執行 onValue
(正常情況)和onError
(異常情況) 方法。
這裡如果 cancelToken
不為空,就會把cancelToken
的取消事件方法放到攔截器中,然後出現異常的時候會將異常丟擲。這其實相當於是前置攔截,就是說如果請求還沒處理(未加入處理佇列)的時候,直接使用攔截器攔截。而如果請求已經加入到了處理佇列,就需要在佇列排程中處理了。
static Future<T> listenCancelForAsyncTask<T>(
CancelToken? cancelToken, Future<T> future) {
return Future.any([
if (cancelToken != null) cancelToken.whenCancel.then((e) => throw e),
future,
]);
}
複製程式碼
/// 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>(Iterable<Future<T>> futures) {
var completer = new Completer<T>.sync();
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;
}
複製程式碼
請求排程
實際的請求排程是在 dio_mixin.dart 中的_dispatchRequest
方法完成的,該方法實際在上面的 fetch 方法中呼叫。這個方法有兩個地方用到了 canlToken,一個是使用 httpClientAdapter 的 fetch方法時傳入了 cancelToken 的whenCancel 屬性。在 httpClientAdapter引用是為了在取消請求後,能夠回撥告知監聽器請求被取消。另外就是呼叫了一個 checkCancelled
方法,用於檢查是否要停止請求。
responseBody = await httpClientAdapter.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!;
}
}
複製程式碼
從這裡我們就能夠大致明白基本的機制了。實際上我們呼叫 cancelToken
的 cancel
方法的時候,標記了 cancelToken
的錯誤資訊 cancelError
,以讓_dispatchRequest
被排程的時候來檢測是否取消。在_dispatchRequest
中如果檢測到cancelError
不為空,就會丟擲一個 cancelError
,中止當前以及接下來的請求。
總結
從原始碼來看,有一堆的 Future,以及各類包裝方法,閱讀起來相當費勁,這也看出來 Dio 的厲害之處,讓一般人想這些網路請求頭都能想炸去。實際我們從原始碼以及除錯跟蹤來看,CancelToken 的機制是:
- 事前取消:如果請求的 cancelToken 不為空,就會將 cancelToken 的非同步處理加入到攔截器,取消後直接在攔截器環節就把請求攔住,不會到後續的排程環節。
- 事中取消:如果請求已經加入到了排程佇列,此時取消的話會丟擲異常,中止請求的發出。
- 事後取消:請求已經發出了,當服務端返回結果的時候,會在響應處理環節(包括出錯)攔截,中止後續的響應處理。即便是服務端返回了資料,也會被攔截,不過這個實際意義不太大,無法降低服務端的負荷,只是避免後續的資料處理過程了。