异步函数发展史:Promise对象,Generator函数,Thunk函数和自动流程管理

1、回调地狱

下面的回调应该是大部分人都见到的,但是可能没有几个人对回调这种写法有好感的。因为一层层的嵌套让逻辑的代码的理解性开始变差,最外层的代码明明没有做错什么,但是头和尾越来越远,怎么都不能相见。如果遇到十几层的回调,可能一个函数就要几百行了,简直反人类。

所以如何简单又好理解的实现异步函数是前端人员的必修课。相信大部分前端已经学习过了异步函数的写法就是使用Promise和async函数,可以比较好的解决异步函数的问题。

那么今天,我们就了解一下异步函数经过了哪些变化才变成今天这个样子的?

// 一般的异步编程
getFileCallback('path1', data => {
    
    
  console.log('data :>> ', data)
  getFileCallback('/path2', data2 => {
    
    
    console.log('data2 :>> ', data2);
    // 继续回调...
  })
})

2、Promise对象

第一步,我们使用了Promise对象,把回调封装在了Pormise函数里面,这样我们使用Pormise函数时,就可以使用链式调用,减少了一部分回调的层数。但它更重要的意义是:Promise函数统一了异步函数使用方法的API。使得各种各样的异步操作都有了一个统一的规范。

Promise函数缺点也很明显:没有从根本上解决回调的问题,then函数里面仍然还是使用回调进行下一步操作。而且使用then函数之后,函数的逻辑臃肿,不是很直观。

所以下一步我们的目标就很明确,就是彻底干掉回调函数,让异步函数的执行逻辑回归线性。

// Promise
let getFilePromise = (path) => {
    
    
  return new Promise((resolve, reject) => {
    
    
    getFileCallback(path, (data) => {
    
    
      resolve(data)
    })
  })
}
getFilePromise('/path1').then((data) => {
    
    
  console.log('data :>> ', data)
  return getFilePromise('/path2')
}).then((data2) => {
    
    
  console.log('data2 :>> ', data2)
})

3、Promise对象 + Generator函数

所以,我们又引入了一种新的函数:Generator函数,通过Generator函数的next方法,可以依次执行每一个的yield语句。每一个yield语句就是一个异步函数,等异步函数返回就使用next方法继续向下执行。在异步函数里面我们终于将回调彻底移除。

但是我们调用方法开始变得复杂了起来,而且回调依然存在。我们需要在每个next方法执行后返回的Promise继续使用next方法,来得到异步函数的返回值。这不就是把回调换了一个地方吗?

所以下一步我们将在调用方法中也干掉回调函数,让回调函数的使用进一步方便。

// Generator函数 + Promise
let getFilePromise = (path) => {
    
    
  return new Promise((resolve, reject) => {
    
    
    getFileCallback(path, (data) => {
    
    
      resolve(data)
    })
  })
}
function* getFileGenerator(path1, path2) {
    
    
  const data = yield getFilePromise(path1)
  console.log('data :>> ', data)
  const data2 = yield getFilePromise(path2)
  console.log('data2 :>> ', data2)
}
let result = getFileGenerator('path1', 'path2')
result.next().value.then(data => {
    
    
  result.next(data).value.then(data2 => {
    
    
    result.next(data2)
  })
})

4、Promise对象 + Generator函数 + 基于Thunk函数的自动化流程管理

之前的调用函数中,我们仔细观察可以发现,因为使用了Promise所以调用方法里面的每一层回调都是相同的,

4.1、Thunk函数

这次还要引用一种新的函数Thunk函数,可以把多参数函数改成多层单参数函数,就是把 func(a, b, c) => func(a)(b)(c) 这种形式的函数。概念上感觉很像函数式编程里面的柯里化。

有了它,我们就可以把异步方法参数拆分成单参数的函数。在yield语句执行的时候得到只能callback参数的函数。执行器回调逻辑与Promise对象的结果很相似。

// Generator函数 + Promise + 基于Thunk函数的自动化流程管理
const Thunk = function(fn) {
    
    
  return function (...args) {
    
    
    return function (callback) {
    
    
      return fn.call(this, ...args, callback);
    }
  };
};
// getFileThunk(path)(callback)
let getFileThunk = Thunk(getFileCallback)
function* getFileGenerator(path1, path2) {
    
    
  const data = yield getFileThunk(path1)
  console.log('data :>> ', data)
  const data2 = yield getFileThunk(path2)
  console.log('data2 :>> ', data2)
}
var g = getFileGenerator('path1', 'path2');

g.next().value((data) => {
    
    
  g.next(data).value((data2) => {
    
    
    g.next(data2);
  });
});

4.2、基于Thunk函数的Generator函数自动化流程管理

到现在我们的问题还是没有解决,只是用Thunk函数替换了之前的Promise对象。那为什么要它呢?因为利用Thunk函数我们可以实现流程的自动化管理。

我们可以看出每一层的回调都非常相似,所以,我们其实可以使用递归函数实现相同的效果。这种无论多少层的回调都可以完成。

// 基于Thunk函数的自动化流程管理
function autoThunk(callback) {
    
    
  const gen = callback()
  function next(err, data) {
    
    
    let result = gen.next(data)
    if (result.done) return
    result.value(next)
  }
  next()
}
autoThunk(getFileGenerator('path1', 'path2'))

5、Promise对象 + Generator函数 + 基于Promise的自动化流程管理

上一步我们使用Thunk函数实现了自动化的流程管理,问题的关键是其实不在于Thunk函数,而是有这样一种机制,可以自动接收和交还程序的执行权。其实Promise也可以实现一样的效果。

// Generator函数 + Promise + Promise自动执行器
let getFilePromise = (path) => {
    
    
  return new Promise((resolve, reject) => {
    
    
    getFileCallback(path, (data) => {
    
    
      resolve(data)
    })
  })
}
function* getFileGenerator(path1, path2) {
    
    
  const data = yield getFilePromise(path1)
  console.log('data :>> ', data)
  const data2 = yield getFilePromise(path2)
  console.log('data2 :>> ', data2)
}
function autoPromise(callback) {
    
    
  const gen = callback()
  function next(err, data) {
    
    
    let result = gen.next(data)
    if (result.done) return result.value
    result.value.then(data =>{
    
    
      next(data)
    })
  }
  next()
}
autoPromise(getFileGenerator('path1', 'path2'))

6、async函数 + JS内置基于Promise的自动化流程管理

最后将Generator函数使用async关键字代替,yield关键字使用await代替。自动化流程管理内置到语法层面,就构成了如今成熟的异步编程解决方案。

// async + await + 内置Promise自动执行器
let getFilePromise = (path) => {
    
    
  return new Promise((resolve, reject) => {
    
    
    getFileCallback(path, (data) => {
    
    
      resolve(data)
    })
  })
}
async function getFileAsync(path1, path2) {
    
    
  let data = await getFilePromise(path1)
  console.log('data :>> ', data)
  let data2 = await getFilePromise(path2)
  console.log('data2 :>> ', data2)
}
getFileAsync('path1', 'path2')

7、总结

// 最终结果
async function getFileAsync(path1, path2) {
    
    
  let data = await getFilePromise(path1)
  console.log('data :>> ', data)
  let data2 = await getFilePromise(path2)
  console.log('data2 :>> ', data2)
}
// 最开始
getFileCallback('path1', data => {
    
    
  console.log('data :>> ', data)
  getFileCallback('/path2', data2 => {
    
    
    console.log('data2 :>> ', data2);
  })
})

对比最开始的回调方法,看到虽然只是进行了扁平化处理和添加了两个关键字。但是背后的实现原理却使用了多项技术(Promise对象,Generator函数,Thunk函数,自动流程管理),而且经过了曲折而漫长的发展。每一步的都对当前方案进行不断优化与改进,最后才有了我们现在这个简洁又优雅的异步函数实现。

每一步中使用的技术可能在表面上看不到了,比如说自动执行器。但是不并不能说明这项技术就过时被淘汰了。它可能就隐藏在我们简单的代码里面,还在持续的为我们的发光发热。

所以了解编程技术的发展历史,可以更好的帮助我们理解为什么技术要这么设计,以及它是如何一步一步走到了今天。

最好的技术就是感觉不到它的存在。

参考资料:

阮一峰-ES6 入门教程-Generator 函数的语法
阮一峰-ES6 入门教程-Generator 函数的异步应用
阮一峰-ES6 入门教程-async 函数
阮一峰-ES6 入门教程-Promise 对象

おすすめ

転載: blog.csdn.net/cuipp0509/article/details/109849102