16.9、Generator与协程、Generator应用


        /*
            协程(coroutine)是一种程序运行方式,可以理解成 “协作的线程” 或 “协作的函数”。
            协程既可以用单线程实现,也可以用多线程实现;前者是一种特殊的子例程,后者是一种特殊的线程。
        */

        /*
            协程与子例程的差异:

                传统的 “子例程” 采用堆栈式 “后进先出” 的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。
                协程与其不同,多个线程(单线程情况下即多个函数)可以并行执行,但只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态,线程(或函数)之间可以交换执行权。
                也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权时再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

                从实现上看,在内存中子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行态。也就是说,协程是以多占用内存为代价实现多任务的并行运行。


            协程与普通线程的差异:

                不难看出,协程适用于多任务运行的环境。
                在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变。
                它们的不同之处在于,同一时间可以有多个线程处于运行态,但是运行的协程只能有一个,其他协程都处于暂停态。
                此外,普通的线程是抢占式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。

                由于 Javascript 是单线程的语言,只能保持一个调用栈。
                引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。
                不至于像异步操作的回调函数那样,一旦出错原始的调用栈早就结束。

                Generator 函数是 es6 对协程的实现,但属于不完全实现。
                Generator 函数被称为 “半协程”,意思是只有 Generator 函数的调用者才能将程序的执行权还给 Generator 函数,
                如果是完全实现的协程,任何函数都可以让暂停的协程继续执行。

                如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用 yield 语句交换控制权。
        */








        /*
            16.10 Generator 的应用


            16.10.1 异步操作的同步化表达

            Genarator 函数的暂停执行效果,意味着可以把异步操作写在 yield 语句里面,等到调用 next 方法时再往后执行。
            这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 语句下面,反正要等到调用 next 方法时再执行。
            所以,Generator 函数的一个重要实际意义就是用于处理异步操作,改写回调函数。       
        */

        function* loadUI() {
            showLoadingScreen()
            yield loadUIDataAsynchronously()
            hideLoadingScreen()
        }

        var loader = loadUI()
        // 加载 UI
        loader.next()

        // 卸载 UI
        loader.next()

        /*
            上面的代码中,第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。
            下一次对该遍历器调用 next 方法,则会显示 Loading 界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。
            等到数据加载完成,再一次使用 next 方法,则会隐藏 loading 界面。
            可以看到,这种写法的好处是所有 Loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰。



            AJAX 是典型的异步操作,通过 Generator 函数部署 AJAX 操作,可以用同步的方式表达。
        */

        function* main() {
            var result = yield request('http://some.url')
            var response = JSON.parse(result)
            console.log(response.value)
        }

        function request(url) {
            makeAjaxCall(url, function(response) {
                it.next(response)
            })
        }

        var it = main()
        it.next()

        // 上面的 main 函数就是通过 AJAX 操作获取数据。可以看到,除了多了一个 yield,它几乎与同步操作的写法完全一样。

        // 注意:makeAjaxCall 函数中的 next 方法必须加上 response 参数,因为 yield 语句构成的表达式本身是没有值的,总是等于 undefined

        // 下面是另一个例子,通过 Generator 函数逐行读取文本文件

        function* numbers() {
            let file = new FileReader('numbers.txt')
            try {
                while(!file.eof) {
                    yield parseInt(file.readLine(), 10)
                }
            } finally {
                file.close()
            }
        }
 
        // 上面的代码打开文本文件,使用 yield 语句可以手动逐行读取文件


        


        /*
            16.10.2 控制流管理

            如果有一个多步操作非常耗时,采用回调函数可能会写成下面这样。
        */

        step1(function (value1) {
            step2(value1, function(value2) {
                step3(value2, function(value3) {
                    step4(value3, function(value4) {
                        // Do something with value4
                    })
                })
            })
        })

        // 采用 Promise 改写上面的代码如下:
        Promise.resolve(step1)
        .then(step2)
        .then(step3)
        .then(step4)
        .then(function (value4) {
            // Do something with value4
        }, function (error) {
            // Handle any error from step1 through step4
        })
        .done()

        // 上面的代码已经把回调函数改成了直线执行的形式,但是加入了大量 Promise 的语法。
        // Generator 函数可以进一步改善代码运行流程
        function* longRunningTask(value1) {
            try {
                var value2 = yield step1(value1)
                var value3 = yield step2(value2)
                var value4 = yield step3(value3)
                var value5 = yield step4(value4)
                // Do something with value4
            } catch (e) {
                // Handle any error from step1 through step4
            }
        }

        // 然后,使用一个函数按次序自动执行所有步骤
        scheduler(longRunningTask(initialValue))

        function scheduler(task) {
            var taskObj = task.next(task.value)
            // 如果 Generator 函数未结束,就继续调用
            if(!taskObj.done) {
                task.value = taskObj.value
                scheduler(task)
            }
        }

        /*
            注意:上面的这种做法只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。
            因为这里的代码一得到返回值就继续往下执行,没有判断异步操作时完成。
        */