阅读 1656

JS 异步发展流程(回调函数=>Async/await)

异步编程的语法目标,就是怎样让它更像同步编程

什么是异步?

异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 异步执行的运行机制如下:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。


1、 回调函数

场景: 读取一个文件

let fs = require('fs')
fs.readFile('./1.txt', 'utf8', function(err, data){
    // 回调的特点是第一个参数一般为错误对象
    if (err) { 
        // 如果err有值说明程序出错了
        console.log(err)
    } else { 
        // 否则表示成功获取到数据data
        console.log(data)
    }
})
复制代码

当然回调函数也有它的缺点:

  • 无法捕获错误(使用try catch)
funnction readFile (fileName) {
    fs.readFile(fileName, 'utf8', function (data) {
        if (err) {
            console.log(err)
        } else {
            console.log(data)
        }
    })
}
try {
    readFile('./1.txt')
} catch (e) { 
    // 如果上边读取文件出错,获取不到错误信息
    console.log('err', e)
}
复制代码
  • 不能return readFile 方法中无法返回读取到文件的内容(data)
  • 回调地狱
fs.readFile('./data1.txt', 'utf8', function (err, data1) {
  fs.readFile('./data2.txt', 'utf8', function (err, data2) {
    fs.readFile('./data3.txt', 'utf8', function (err, data3) {
      fs.readFile('./data4.txt', 'utf8', function (err, data4) {
        fs.readFile('./data5.txt', 'utf8', function (err, data5) {
            console.log(data1, data2, data3, data4, data5)
        })
      })
    })
  })
})
// 这样的代码称为恶魔金字塔;且有以下问题
// 1、代码非常难看
// 2、难以维护
// 3、效率比较低,因为它们是串行的;一次只能请求一个文件
复制代码

2、事件发布订阅

为了解决回调嵌套的问题

let EventEmitter = require('events')
// nodejs核心模块之一,包含两个核心方法: 
// on >> 表示注册监听,emit >> 表示发射事件

let eve = new EventEmitter()
let html = {} // 存放页面模板和数据
eve.on('reading', function (key, value) {
    html[key] = value
    if (Object.keys(html).length == 2) {
        console.log(html)
    }
})
fs.readFile('./template.txt', 'utf8', function (err, template) {
    eve.emit('reading', template) 
    // 触发reading事件,执行事件的回调函数向html中填入模板
})
fs.readFile('./data.txt', 'utf8', function (err, template) {
    eve.emit('reading', template) 
    // 触发reading事件,执行事件的回调函数向html中填入数据
})
复制代码

3、哨兵变量

事件发布订阅已经可以解决回调嵌套的问题,但是还需要引入events模块; 利用哨兵变量一样可以解决回调嵌套的问题,且不需要引入其他模块

// 定义一个哨兵函数来处理
function done (key, value) {
    html[key] = value
    if (Object.keys(html).length == 2) {
        console.log(html)
    }
}

fs.readFile('./template.txt', 'utf8', function (err, template) {
    done('template', template)
})
fs.readFile('./data.txt', 'utf8', function (err, template) {
    done('data', data)
})

// 可以封装一个高阶函数去生成哨兵函数
function render (length, cb) {
    let htm = {}
    return function (key, value) {
        html[key] = value
        if (Object.keys(html).length == length) {
            cb(html)
        }
    }
}

let done = render(2, function (html) {
    console.log(html)
})
复制代码

4、Promise

上述方法都是用回调函数来处理异步;我们的目标是把异步往同步的方向靠拢

//promise的用法不再阐述,可自行查阅文档
let promise1 = new Promise(function (resolve, reject) {
    fs.readFile('./1.txt', 'utf8', function (err, data) {
        resolve(data)
    })
})

promise1.then(function (data) {
    console.log(data)
})
复制代码

5、Generator(生成器)

当我们在调用一个函数的时候,它并不会马上执行,而是需要我们手动的去执行迭代操作(next方法);简单来说,调用生成器函数会返回一个迭代器,可以用迭代器来执行遍历每个中断点(yield) 调用next方法会有返回值value,是生成器函数对外输出的数据;next方法还可以接受参数,是向生成器函数内部输入的数据

  • 生成器简单使用
// 方法名前边加*就是生成器函数
function *foo () {
  var index = 0;
  while (index < 2) {
    yield index++; //暂停函数执行,并执行yield后的操作
  }
}
var bar =  foo(); // 返回的其实是一个迭代器

console.log(bar.next());    // { value: 0, done: false }
console.log(bar.next());    // { value: 1, done: false }
console.log(bar.next());    // { value: undefined, done: true }
复制代码
  • 生成器 + Promise解决异步的实现
function readFile (filaName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(filename, function (err, data) {
          if (err) {
            reject(err)
          } else {
            resolve(data)
          }
    })
}
function *read() {
    let template = yield readFile('./template.txt')
    let data = yield readFile('./data.txt')
    return {
        template: template,
        data: data
    }
}
// 生成迭代器r1
let r1 = read()
let templatePromise = r1.next().value
templatePromise.then(function(template) {
    // 将获取到的template的内容传递给生成器函数
    let dataPromsie = r1.next(template).value
    dataPromise.then(function(data) {
        //最后一次执行next传入data的值;最后返回{template, data}
        let result = r1.next(data).value
        console.log(result)
    })
})
复制代码
  • 生成器 + promise的实现已经有了一些同步的样子;借助一些工具(co),可以优雅的编写上述的代码
//实现 co 方法
//参数是一个生成器函数
function co (genFn) {
    let r1 = genFn()
    return new Promise(function(resolev, reject) {
        !function next(lastVal) {
            let p1 = r1.next(lastVal)
            if (p1.done) {
                resolve(p1.value)
            } else {
                p1.value.then(next, reject)
            }
        }()
    })
}

//现在获取上边的result可以这样来取
co(read).then(function(result) {
    console.log(result)
})
复制代码

6、Async/await

Async其实是一个语法糖,它的实现就是将Generator函数和自动执行器(co),包装在一个函数中

//实现 co 方法
//参数是一个生成器函数
async function read() {
  let template = await readFile('./template.txt');
  let data = await readFile('./data.txt');
  return template + '+' + data;
}

// 等同于
function read(){
  return co(function*() {
    let template = yield readFile('./template.txt');
    let data = yield readFile('./data.txt');
    return template + '+' + data;
  });
}
复制代码

异步编程发展的目标就是让异步逻辑的代码看起来像同步一样,发展到Async/await,是处理异步编程的一个里程碑。


参考文章:

文章分类
前端
文章标签