协程 是一种函数,其执行可以被暂停和恢复,可能传递一些数据。它们恰好适用于实现涉及不同任务/函数之间合作的各种模式,比如异步流。
在 JavaScript 中
在 JavaScript 中,你可以使用生成器函数来实现(类似)协程。你可能已经使用生成器函数来实现迭代器和序列。
function *integers(){
let n = 0;
while(true){
yield ++n;
}
}
const sequence = integers();
console.log(sequence.next().value); // > 1
console.log(sequence.next().value); // > 2
console.log(sequence.next().value); // > 3
console.log(sequence.next().value); // > 4
这里的 while(true)
很有趣(而且完全没问题),因为它证明了生成器正在惰性地被评估。当你调用 next
函数时,实际上是执行生成器,直到下一个 yield
语句。yield
右侧表达式的结果成为迭代器结果的 value
,然后生成器函数暂停。
我们通常不知道的是,当恢复例程的执行时,你可以向 next
函数传递数据,这样做的效果是将该数据分配给语句“左侧”的任何变量:
function *generator() {
while(true){
const action = yield;
console.log(action)
}
}
const routine = generator();
routine.next();
routine.next('increment'); // > 'increment'
routine.next('go-left'); // > 'go-left'
首次调用 next
显然不能接收任何数据,因为例程尚未暂停。
双向示例
虽然通常你会将生成器用作数据的生产者或接收者,但你可以同时在两个方向上使用它。请注意,管理起来可能会令人困惑和复杂,但它非常方便用于实现某些模式。
看以下类似于 “Redux” 的状态机示例:
function* EventLoop({reducer, state}) {
while (true) {
const action = yield state; // 哇!
state = reducer(state, action);
}
}
const createEventLoop = ({reducer, state}) => {
const eventLoop = EventLoop({reducer, state});
eventLoop.next();
return (action) => eventLoop.next(action).value;
};
const createSubscribable = () => {
const eventName = 'state-changed';
const eventTarget = new EventTarget();
const notify = () => eventTarget.dispatchEvent(new CustomEvent(eventName));
const subscribe = (listener) => {
eventTarget.addEventListener(eventName, listener);
return () => unsubscribe(listener);
};
const unsubscribe = (listener) =>
eventTarget.removeEventListener(eventName, listener);
return {
unsubscribe,
subscribe,
notify
};
};
const createStore = ({reducer, initialState}) => {
let state = initialState;
const {notify, ...subscribable} = createSubscribable();
const dispatch = createEventLoop({reducer, state});
return {
...subscribable,
getState() {
return structuredClone(state);
},
dispatch(action) {
state = dispatch(action);
notify();
}
};
};
const store = createStore(
{
reducer: (state, action) => {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1,
};
case 'decrement':
return {
...state,
count: state.count - 1,
};
default:
return state;
}
},
initialState: {
count: 0,
}
}
);
store.subscribe(() => console.log(store.getState()));
store.dispatch({
type: 'increment'
}); // 记录 { count: 1 }
store.dispatch({
type: 'increment'
}); // 记录 { count: 2 }
store.dispatch({
type: 'decrement'
}); // 记录 { count: 1 }
对我们来说有趣的部分是 EventLoop
例程,当暂停时,它返回当前状态,并在恢复时接收下一个要处理的动作。createEventLoop
函数隐藏了我们使用协程来实现状态机的事实,使其成为实现的一个细节。然而,由于协程的存在,整体解决方案保持简洁而相当简单。
异步流示例
在前面的示例中,我们看到了如何使用协程模拟事件循环。在下面的示例中,我们将看到一种不同类型的 “协同多任务”,使用相同语义的异步工作流程,正如常规的 async
函数一样(使用 await
关键字)。
const co = (genFn) => (...args) => {
const gen = genFn(...args);
// 没有数据传递给 next,因为例程尚未暂停
return next();
function next(data) {
const { value, done } = gen.next(data);
if (done) {
return value;
}
// 非 Promise 值
if (value?.then === undefined) {
return next(value);
}
// 我们恢复例程,并将解决的值赋给 "yield"
return value.then(next);
}
};
const fn = co(function* (arg) {
let value = yield asyncTask(arg);
value = yield otherAsyncTask(value);
return value;
});
fn(42).then(console.log);
其背后的思想非常简单:每当主要异步函数委托任务给另一个函数时,它就会被暂停。如果该函数本身是异步的,我们会等待挂起的 Promise 解析,然后使用解析后的值恢复主例程。这与 async
函数非常相似,只是你用 yield
替换了内置的 await
关键字。
如果你看完还是对coroutine一知半解,不急,关注博主以后会更新更多示例让你弄明白如何高效的使用javascript中的coroutine.