该帖子最初发布于Codebrahma 。
JavaScript是一种单线程编程语言。 也就是说,当您拥有类似这样的代码时……
…直到第一行完成,第二行才被执行。 通常,这不会有问题,因为客户端或服务器可在一秒钟内执行数百万次计算。 只有在执行昂贵的计算时(我们需要花费大量时间才能完成的任务-需要一些时间才能返回的网络请求),我们才会注意到效果。
为什么我在这里只显示一个API调用(网络请求)? 那其他异步操作呢? API调用是一个非常简单而有用的示例,用于描述如何处理异步操作。 还有其他操作,例如setTimeout()
,性能setTimeout()
计算,图像加载以及任何事件驱动的操作。
在构造应用程序时,我们需要考虑异步执行如何影响构造。 例如,将fetch()
视为从浏览器执行API调用(网络请求)的函数。 (忘了它是AJAX请求。本质上可以将行为视为异步或同步。)在服务器上处理请求的时间不会在主线程上发生。 因此,您的JS代码将继续执行,并且一旦请求返回响应,它将更新线程。
考虑以下代码:
userId = fetch(userEndPoint); // Fetch userId from the userEndpoint
userDetails = fetch(userEndpoint, userId) // Fetch for this particular userId.
在这种情况下,由于fetch()
是异步的,因此当我们尝试获取userDetails
时将没有userId
。 因此,我们需要以确保第二行仅在第一行返回响应时才执行的方式来构造它。
网络请求的大多数现代实现都是异步的。 但这并不总是有帮助的,因为我们依赖于先前的API响应数据来进行后续的API调用。 让我们看一下我们如何在ReactJS / Redux应用程序中构造它。
React是用于制作用户界面的前端库。 Redux是一个状态容器,可以管理应用程序的整个状态。 通过将React与Redux结合使用,我们可以使高效的应用程序很好地扩展。 在这样的React应用程序中,有几种构造异步操作的方法。 对于每种方法,让我们讨论与这些因素有关的利弊:
- 代码清晰
- 可扩展性
- 易于错误处理。
对于每种方法,我们将执行以下两个API调用:
1.从userDetails获取城市 (第一个API响应)
假设端点为/details
。 它将有城市作为回应。 响应将是一个对象:
userDetails : {
…
city: 'city',
…
};
2.根据用户所在的城市,我们将获取该城市中的所有餐厅
假设端点为/restuarants/:city
。 响应将是一个数组:
['restaurant1', 'restaurant2', …]
请记住,只有在完成第一个请求后我们才能执行第二个请求(因为它取决于第一个请求)。 让我们看一下执行此操作的各种方法:
- 通过setState直接使用promise或async等待
- 使用Redux Thunk
- 使用Redux-Saga
- 使用Redux Observables。
我特别选择了上述方法,因为它们是大型项目中最常用的方法。 还有其他方法可能更特定于特定任务,并且不具有复杂应用程序所需的所有功能( redux-async,redux-promise,redux-async-queue仅举几例)。
承诺
一个promise是一个对象,它可能在将来的某个时候产生一个单一值:一个已解决的值,或者一个未解决的原因(例如,发生网络错误)。 — 埃里克·艾略特 ( Eric Elliot)
在我们的例子中,我们将使用axios库来获取数据,当我们发出网络请求时,它会返回一个Promise 。 该承诺可能会解决并返回响应或引发错误。 因此,一旦安装了React Component ,我们就可以像这样直接获取:
componentDidMount() {
axios.get('/details') // Get user details
.then(response => {
const userCity = response.city;
axios.get(`/restaurants/${userCity}`)
.then(restaurantResponse => {
this.setState({
listOfRestaurants: restaurantResponse, // Sets the state
})
})
})
}
这样,当状态更改时(由于获取), Component将自动重新渲染并加载餐厅列表。
Async/await
是我们可以进行异步操作的新实现。 例如,可以通过以下方式实现相同的目的:
async componentDidMount() {
const restaurantResponse = await axios.get('/details') // Get user details
.then(response => {
const userCity = response.city;
axios.get(`/restaurants/${userCity}`)
.then(restaurantResponse => restaurantResponse
});
this.setState({
restaurantResponse,
});
}
这两种方法都是所有方法中最简单的。 由于整个逻辑都在组件内部,因此一旦组件加载,我们就可以轻松获取所有数据。
方法的缺点
问题是基于数据进行复杂的交互时。 例如,考虑以下情况:
- 我们不希望为网络请求阻塞执行JS的线程。
- 以上所有情况都会使代码非常复杂,并且难以维护和测试。
- 同样,可伸缩性将是一个大问题,因为如果我们计划更改应用程序的流程,则需要从组件中删除所有获取的内容。
- 想象一下,如果组件位于父子树的顶部,则执行相同的操作。 然后,我们需要更改所有与数据相关的表示组件。
- 还要注意,整个业务逻辑都在组件内部。
我们如何从这里改善?
1.国家管理
在这些情况下,使用全球商店实际上可以解决我们一半的问题。 我们将使用Redux作为我们的全球商店。
2.将业务逻辑转移到正确的位置
如果我们考虑将业务逻辑移出组件之外,那么到底该在哪里做呢? 在行动? 在减速器中? 通过中间件? Redux的架构本质上是同步的。 当您调度一个动作(JS对象)并到达商店时,Reducer会对它进行操作。
3.确保有一个单独的线程来执行异步代码,并且可以通过订阅检索对全局状态的任何更改
由此,我们可以得出一个想法,如果我们将所有获取逻辑移到reducer之前(即动作或中间件),那么就有可能在正确的时间调度正确的动作。
例如,一旦提取开始,我们就可以dispatch({ type: 'FETCH_STARTED' })
,完成后,我们就可以dispatch({ type: 'FETCH_SUCCESS' })
。
使用Redux Thunk
Redux Thunk是Redux的中间件。 基本上,它使我们可以将function
而不是objects
作为动作返回。 通过提供dispatch
和getState
作为函数的参数,这将getState
帮助。 我们通过在适当的时间调度必要的操作来有效地使用调度。 好处是:
- 允许在函数内部进行多次调度
- 业务逻辑与提取的关系将在React组件之外,并移至操作。
在我们的例子中,我们可以这样重写动作:
export const getRestaurants = () => {
return (dispatch) => {
dispatch(fetchStarted()); // fetchStarted() returns an action
fetch('/details')
.then((response) => {
dispatch(fetchUserDetailsSuccess()); // fetchUserDetailsSuccess returns an action
return response;
})
.then(details => details.city)
.then(city => fetch('/restaurants/city'))
.then((response) => {
dispatch(fetchRestaurantsSuccess(response)) // fetchRestaurantsSuccess(response) returns an action with the data
})
.catch(() => dispatch(fetchError())); // fetchError() returns an action with error object
};
}
如您所见,我们现在可以很好地控制何时dispatch
哪种类型的操作。 每个函数调用(如fetchStarted()
, fetchUserDetailsSuccess()
, fetchRestaurantsSuccess()
和fetchError()
分派一个普通的JavaScript对象,并根据需要分配其他详细信息。 因此,现在简化器的工作就是处理每个动作并更新视图。 我没有讨论过reducer,因为从这里开始它很简单,实现可能有所不同。
为此,我们需要将React组件与Redux连接起来,并使用Redux库将操作与该组件绑定。 一旦完成,我们可以简单地调用this.props.getRestaurants()
,它将依次处理上述所有任务并基于化简器更新视图。
就其可扩展性而言,Redux Thunk可以在不涉及异步动作复杂控制的应用中使用。 而且,它可以与其他库无缝协作,如下一节的主题所述。
但是,使用Redux Thunk执行某些任务仍然有些困难。 例如,我们需要在两次调用之间暂停调用,或者在有多个此类调用的时候暂停调用,并且仅允许最新一次调用,或者如果某些其他API提取此数据并且需要取消,则需要暂停。
我们仍然可以实现这些功能,但是确切地执行起来并不复杂。 与其他库相比,复杂任务的代码清晰度几乎没有什么差劲,并且维护起来很困难。
使用Redux-Saga
使用Redux-Saga中间件,我们可以获得解决上述大多数功能的其他好处。 Redux-Saga是基于ES6 生成器开发的。
Redux-Saga提供了有助于实现以下目标的API:
- 阻塞将线程阻塞在同一行中的事件,直到实现目标为止
- 使代码异步的非阻塞事件
- 处理多个异步请求之间的竞争
- 暂停/限制/取消任何动作。
sagas如何工作?
Sagas使用ES6生成器和异步等待API的组合来简化异步操作。 它基本上是在一个单独的线程上完成工作的,在该线程上我们可以执行多个API调用。 我们可以根据其使用情况使用其API使每个调用同步或异步。 API提供了一些功能,通过这些功能,我们可以使线程在同一行中等待,直到请求返回响应为止。 除此之外,该库还提供了许多其他API,这使得API请求非常易于处理。
考虑我们之前的示例:如果我们按照他们的文档中的说明初始化传奇并使用Redux对其进行配置,我们可以执行以下操作:
import { takeEvery, call } from 'redux-saga/effects';
import request from 'axios';
function* fetchRestaurantSaga() {
// Dispatches this action once started
yield put({ type: 'FETCH_RESTAURANTS_INITIATED '});
try {
// config for fetching details API
const detailsApiConfig = {
method: 'get',
url: '/details'
};
// Blocks the code at this line till it is executed
const userDetails = yield call(request, config);
// config for fetching details API
const restaurantsApiConfig = (city) {
method: 'get',
url: `/restaurants/${city}`,
};
// Fetches all restuarants
const restaurants = yield call(request, restaurantsApiConfig(userDetails.city));
// On success dispatch the restaurants
yield put({
type: 'FETCH_RESTAURANTS_SUCCESS',
payload: {
restaurants
},
});
} catch (e) {
// On error dispatch the error message
yield put({
type: 'FETCH_RESTAURANTS_ERROR',
payload: {
errorMessage: e,
}
});
}
}
export default function* fetchRestaurantSagaMonitor() {
yield takeEvery('FETCH_RESTAURANTS', fetchInitial); // Takes every such request
}
因此,如果我们使用FETCH_RESTAURANTS
类型调度一个简单的动作,那么Saga中间件将监听并做出响应。 实际上,中间件都不会消耗任何动作。 它只是侦听并执行一些其他任务,并在需要时调度新的操作。 通过使用这种架构,我们可以调度多个请求,每个请求描述
- 当第一个请求开始时
- 当第一个请求完成时
- 当第二个请求开始时
… 等等。
此外,您还可以看到fetchRestaurantsSaga()
的美丽之fetchRestaurantsSaga()
。 当前,我们已使用调用API来实现阻止调用。 Sagas提供了其他API,例如fork()
,它实现了非阻塞调用。 我们可以结合使用阻塞调用和非阻塞调用来维护适合我们应用程序的结构。
在可伸缩性方面,使用sagas有利:
- 我们可以根据任何特定任务来构造和分组Sagas。 我们可以通过简单地调度一个动作来触发另一个故事。
- 由于它是中间件,因此我们编写的动作将是普通的JS对象,这与thunk不同。
- 由于我们将业务逻辑移到了Sagas(这是一种中间件)内部,因此,如果我们知道传奇的功能是什么,那么理解它的React部分将变得更加容易。
- 可以通过尝试/捕获模式轻松地监视错误并将其分发到商店。
使用Redux-Observables
如他们的文档中“ 史诗是redux-observable的核心原语 ”中所述:
Epic是一种函数,它接受一系列操作并返回一系列操作。 也就是说,在reducer已接收到它们之后,Epic与正常的Redux调度通道一起运行。
在史诗甚至没有收到史诗之前,行动总是贯穿于你的化身。 史诗级只是接收并输出另一个动作流。 这与Redux-Saga相似,因为中间件都不会消耗Action。 它只是监听并执行一些其他任务。
对于我们的任务,我们可以简单地编写以下代码:
const fetchUserDetails = action$ => (
action$.ofType('FETCH_RESTAURANTS')
.switchMap(() =>
ajax.getJSON('/details')
.map(response => response.userDetails.city)
.switchMap(() =>
ajax.getJSON(`/restaurants/city/`)
.map(response => ({ type: 'FETCH_RESTAURANTS_SUCCESS', payload: response.restaurants })) // Dispatching after success
)
.catch(error => Observable.of({ type: 'FETCH_USER_DETAILS_FAILURE', error }))
)
)
)
刚开始时,这看起来似乎有点混乱。 但是您对RxJS的了解越多,创建Epic就越容易。
与sagas一样,我们可以调度多个动作,每个动作描述线程当前所在的API请求链的哪一部分。
在可伸缩性方面,我们可以拆分Epic或根据特定任务组成Epic。 因此,该库可帮助构建可扩展的应用程序。 如果我们了解编写代码的“可观察”模式,那么代码清晰度就很好。
我的偏好
您如何确定要使用哪个库?
这取决于我们的API请求的复杂程度。
您如何在Redux-Saga和Redux-Observable之间进行选择?
它取决于学习生成器或RxJS。 两者是不同的概念,但同样足够好。 我建议尝试两者,看看哪一个最适合您。
您在哪里处理API的业务逻辑?
最好在减速器之前,但不在组件中。 最好的方法是在中间件中(使用Sagas或可观察物)。
您可以在Codebrahma阅读更多React Development帖子。
From: https://www.sitepoint.com/async-operations-react-redux-applications/