编程

Laravel 新的 defer() 助手背后的魔力

110 2024-10-19 01:13:00

Laravel 最近发布了一个名为 defer() 的新助手函数,可用于将回调的执行推迟到响应成功发送之后。

通过这种方式,你可以将耗时的工作(比如调用外部 API)卸载到回调,并尽快向用户返回响应。
这是它的样子。

Route::get('/defer', function () {
    defer(function () {
        // do time-consuming work here
        sleep(10);
    });

    return "Hello world";
});

如你所见,defer() 辅助函数将回调作为其参数,并在响应发送后执行。因此,当调用此路由时,即使回调需要 10 秒才能执行完,你也能立即看到响应。

如果请求导致 4xx 或 5xx HTTP 响应,则不会执行延迟回调。通过将 true 作为第三个参数传递给 defer() 辅助函数或在其返回的对象调用 always() 方法,可以改变这种行为。无论响应状态代码如何,都将执行回调。

Route::get('/defer', function () {
    defer(function () {
        // do time-consuming work here
        sleep(10);
    })->always();

    return "Hello world";
});

接下来让我们看看这在幕后是如何运作的,泰勒提到过它没有使用任何队列或类似的东西。

所以,我决定深入研究代码,看看发生了什么。在源代码中来回切换了很多之后,我终于找到了答案。
如果你也像我一样想知道答案,请继续阅读。

defer() 辅助函数

首先,我们来看看一下 defer() 函数。

use Illuminate\Foundation\Defer\DeferredCallback;
use Illuminate\Foundation\Defer\DeferredCallbackCollection;

/**
 * Defer execution of the given callback.
 *
 * @param  callable|null  $callback
 * @param  string|null  $name
 * @param  bool  $always
 * @return \Illuminate\Foundation\Defer\DeferredCallback
 */
function defer(?callable $callback = null, ?string $name = null, bool $always = false)
{
    if ($callback === null) {
        return app(DeferredCallbackCollection::class);
    }

    return tap(
        new DeferredCallback($callback, $name, $always),
        fn ($deferred) => app(DeferredCallbackCollection::class)[] = $deferred
    );
}

如你所见,这里没什么太花哨的东西。助手函数主要做的是创建 DeferredCallback 类的实例,然后将其添加到已从容器解析的 DeferredCallbackCollection 类中。

值得注意的是,DeferredCallback 类的对象将被赋值给 DeferredCallbackCollection 类实例中的一个数组。这意味着它将累积当前请求中延迟的所有回调。

因此,在请求结束时,此数组将包含需要执行的所有回调。

DeferredCallback 类

DeferredCallback 类负责定义具有唯一名称和一些方法(如 always()name()__invoke())的回调。

所以,如果我们打印 defer 回调…

$d = defer(function () {
    sleep(10);
});

dd($d);

它将会产生类似这样的输出:

这就是 DeferredCallback 类。

真正的魔法

我们看到了 defer() 辅助函数和 DeferredCallback 类的定义,但它仍然没有解释之后如何执行回调。

真正的魔法发生在在核心中添加的新的全局中间件里。

\Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class

如果你看看这个中间件的源代码,乍一看它看起来很简单。但请注意 terminate() 方法。

是的!Laravel 采用这种方法来实现最终结果。

中间件中的 terminate() 方法用于在向用户发送响应后执行操作。

以下是 InvokeDeferredCallbacks 中间件中的 terminate() 方法的代码:

/**
 * Invoke the deferred callbacks.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Symfony\Component\HttpFoundation\Response  $response
 * @return void
 */
public function terminate(Request $request, Response $response)
{
    Container::getInstance()
        ->make(DeferredCallbackCollection::class)
        ->invokeWhen(fn ($callback) => $response->getStatusCode() < 400 || $callback->always);
}

如你所见,terminate() 方法调用 DeferredCallbackCollection 类,该类包含当前请求中累积的所有延迟回调,如我们之前所见。

invokeWhen() 告诉容器,只有当响应状态代码小于 400 或回调的 always 属性设置为 true 时,才应调用和执行回调。

这就是 Laravel 如何实现 defer() 辅助函数背后的魔法。
正如有人所说,“魔法就在简单的事情中。

 terminate() 方法的一些

可能很多人想知道 Laravel 是如何神奇地知道如何在 defer() 回调执行后终止请求的。所以,这里有一个快速的解释。

本质上讲,Laravel 依赖于一种名为 FastCGI 的协议(首先需要在服务器上启用)。FastCGI 维护了一个 PHP 进程池,该进程池在执行单个请求之外仍然存在。

这意味着 PHP 进程在发送响应后不会立即终止,从而允许 Laravel 在响应发出后执行其他代码(如 terminate() 方法)。
Laravel 使用请求生命周期来确保所有终止方法的调用顺序与中间件注册的顺序相反。这发生在 Laravel 的内核级别上。