В redux-saga эквивалент приведенного выше примера будет
export function* loginSaga() {
while(true) {
const { user, pass } = yield take(LOGIN_REQUEST)
try {
let { data } = yield call(request.post, '/login', { user, pass });
yield fork(loadUserData, data.uid);
yield put({ type: LOGIN_SUCCESS, data });
} catch(error) {
yield put({ type: LOGIN_ERROR, error });
}
}
}
export function* loadUserData(uid) {
try {
yield put({ type: USERDATA_REQUEST });
let { data } = yield call(request.get, `/users/${uid}`);
yield put({ type: USERDATA_SUCCESS, data });
} catch(error) {
yield put({ type: USERDATA_ERROR, error });
}
}
Первое, что следует заметить, это то, что мы вызываем функции api, используя форму yield call(func, ...args)
. call
не выполняет эффект, а просто создает простой объект, например {type: 'CALL', func, args}
. Выполнение делегируется промежуточному программному обеспечению redux-saga, которое заботится о выполнении функции и возобновлении работы генератора с ее результатом.
Основное преимущество заключается в том, что вы можете протестировать генератор вне Redux, используя простые проверки равенства.
const iterator = loginSaga()
assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))
// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
iterator.next(mockAction).value,
call(request.post, '/login', mockAction)
)
// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
iterator.throw(mockError).value,
put({ type: LOGIN_ERROR, error: mockError })
)
Обратите внимание, что мы имитируем результат вызова API, просто вводя фиктивные данные в next
метод итератора. Имитация данных намного проще, чем имитирующие функции.
Второе, на что следует обратить внимание, - это звонок на yield take(ACTION)
. Преобразователи вызываются создателем действия для каждого нового действия (например, LOGIN_REQUEST
). т.е. действия постоянно передаются в преобразователи, и преобразователи не могут контролировать, когда прекратить обработку этих действий.
В саге о сокращении генераторы тянут следующее действие. то есть у них есть контроль, когда прислушиваться к определенному действию, а когда нет. В приведенном выше примере инструкции потока помещаются внутри цикла while(true)
, поэтому он будет прослушивать каждое входящее действие, что в некоторой степени имитирует поведение отправки преобразователя.
Подход по запросу позволяет реализовать сложные потоки управления. Предположим, например, мы хотим добавить следующие требования
Обработка действий пользователя LOGOUT
при первом успешном входе в систему сервер возвращает токен, срок действия которого истекает с некоторой задержкой, сохраненной в поле expires_in
. Придется обновлять авторизацию в фоновом режиме каждые expires_in
миллисекунд.
Учтите, что при ожидании результата вызовов api (первоначального входа в систему или обновления) пользователь может выйти из системы между ними.
Как бы вы реализовали это с помощью thunks; в то же время обеспечивая полное тестовое покрытие для всего потока? Вот как это может выглядеть с Сагами:
function* authorize(credentials) {
const token = yield call(api.authorize, credentials)
yield put( login.success(token) )
return token
}
function* authAndRefreshTokenOnExpiry(name, password) {
let token = yield call(authorize, {name, password})
while(true) {
yield call(delay, token.expires_in)
token = yield call(authorize, {token})
}
}
function* watchAuth() {
while(true) {
try {
const {name, password} = yield take(LOGIN_REQUEST)
yield race([
take(LOGOUT),
call(authAndRefreshTokenOnExpiry, name, password)
])
// user logged out, next while iteration will wait for the
// next LOGIN_REQUEST action
} catch(error) {
yield put( login.error(error) )
}
}
}
В приведенном выше примере мы выражаем наше требование параллелизма с помощью race
. Если take(LOGOUT)
выиграет гонку (т.е. пользователь нажал кнопку выхода). Гонка автоматически отменит authAndRefreshTokenOnExpiry
фоновую задачу. И если authAndRefreshTokenOnExpiry
был заблокирован во время call(authorize, {token})
вызова, он также будет отменен. Отмена автоматически распространяется вниз.
Вы можете найти работающую демонстрацию вышеуказанного потока
person
Yassine Elouafi
schedule
21.01.2016
::
перед тем, как сделатьthis.onClick
? - person Downhillski   schedule 20.07.2016this
), иначеthis.onClick = this.onClick.bind(this)
. Более длинную форму обычно рекомендуется делать в конструкторе, так как сокращенная форма повторно привязывается при каждом рендеринге. - person hampusohlsson   schedule 20.07.2016bind()
, чтобы передатьthis
функции, но я начал использовать() => method()
сейчас. - person Downhillski   schedule 20.07.2016