从零开始学_JavaScript_系列(58)——Thunk函数

1、Thunk函数基本定义

1.1、缘由

与函数的求值策略有关,求值策略分为两种:

  1. 传名策略:在需要求值的时候才求值;
  2. 传值策略:在传入函数的时候就求值;(又有说法叫按值传递)

如以下的示例代码:

function foo(a) {
    return a * 10   // 1+2在这里计算结果叫做【传名策略】
}
foo(1 + 2)  // 1+2在这里计算结果叫做【传值策略】

JavaScript是采用【传值策略】的语言。

注意:

基本类型传的是值,引用类型传的是地址。

1.2、传值策略的优点

传值策略有其优点,比如说:

foo这个函数里,对于传入进来的参数只调用了一次a * 10,因此似乎并没有什么区别。但假如代码改成如下形式:

function foo(a) {
    let count = 0;
    for (let i = 0; i < 10000; i++) {
        count += a * i
    }
    return count;
}
foo(1 + 2

在这种情况下,【传值策略】的1+2只计算了一次,而若是【传名策略】,那么1+2显然要计算10000次,性能消耗明显大于前者。

1.3、传名策略的优点

而传名函数自然也有其优点。

foo的参数a在以上代码中,是必然执行的,假如参数a不一定执行呢?那么这次计算显然是没有意义的。

并且,若是参数里的表达式极为复杂,性能消耗巨大,那么只有在使用的时候,才会去执行和计算的传名策略,优点自然十分明显了。

如代码:

function foo(a, b) {
    if (b) {
        return a + 1;
    } else {
        return 0;
    }
}
// 阶乘求值函数
function bar(i) {
    if (i === 1) {
        return 1
    } else {
        return i * bar(i - 1)
    }
}
foo(bar(100))

在以上代码中,foo并没有传第二个参数,所以作为foo参数的表达式:bar(100)显然是白计算了。假如bar的参数的值比较大的时候,那么不必要的性能消耗显然十分严重。

如果类似的可能不会被使用的参数很多,那么不要的性能消耗显然就更多了。

这个时候,传名策略的优点即显现了,只有当foo有第二个参数(并且为true)时,执行到return a + 1这一段代码的时候,才会执行阶乘函数,也就是说,无论你参数多么复杂,我不关心,不执行这个参数的时候,对我来说就没有影响。

2、在JavaScript中实现传名策略

2.1、代码改造

js是函数式编程,所以可以把函数作为参数传给另外一个函数。

假如我把复杂的表达式放在一个函数里,然后将这个函数作为参数传给目标函数,只有当我需要执行的时候,直接执行这个函数,不就做到变相的按名传递咯。

对1.3中的代码进行改造,结果如下:

function foo(a, b) {
    if (b) {
        return a() + 1; // 改造点1:从a -> a()
    } else {
        return 0;
    }
}
// 阶乘求值函数
function bar(i) {
    if (i === 1) {
        return 1
    } else {
        return i * bar(i - 1)
    }
}
function thunk(){   // 第二个改造点,将bar(100)放置在thunk函数中
    bar(100)
}
foo(thunk)  //并用thunk函数替代原有的参数

当改造完毕后,再也不用担心在foo的第二个参数不是true的时候,会去执行第一个参数了。

这里的thunk函数,就是一般实现传名策略的编译器的做法,即将参数放在临时函数中,在需要的时候再去执行。

这个临时函数,就称为【Thunk函数

3、JS中的Thunk函数

3.1、JS中Thunk函数的不同

上面实现的是一般Thunk函数,但是js中的Thunk函数有所不同。

通常的Thunk函数:

–》在参数需要执行的时候才执行;

js的Thunk函数:

–》在需要对结果进行处理的时候才执行,特点是只有一个参数,并且该参数是回调函数。

也就是说,在js的Thunk函数中,具有2+1个特点:

1. 有且只有一个参数是callback的函数;
2. 原函数是多参数函数;(如果只有一个参数是没有改造必要的)
3. 通常情况下,callback的第一个参数是error

一般来说,经过Thunk函数改造之后,函数会具有几个优点:

  1. 只关心回调函数的实现,不关心其他参数;
  2. 实现了柯里化,具体参照这篇博文。简单来说,假如我引用了一个函数,他有3个参数,但我每次使用它,有2个参数的值都是固定的,只有第3个参数的值不固定,那么我每次都这么写2个固定参数是很麻烦的,柯里化可以解决这个问题。
  3. 和Generator结合后,异步函数的写法与同步函数的写法十分相似;
3.2、示例
function Foo(callback, time) {
    setTimeout(function () {
        callback()  // 为了方便理解,没有简写
    }, time)
}
function Thunk(time) {
    return function (callback) {
        Foo(callback, time)
    }
}
let foo = Thunk(1000)
foo(function () {
    console.log("test")
})
// 延迟1秒后
// 'test'

解释:

  1. Foo函数是一个异步函数,他有2个参数,分别是延迟后执行的函数,以及延迟时间;
  2. 在通过Thunk函数处理后,我们在Thunk函数中传了延迟时间,返回了一个新函数,这个函数只有一个参数,那就是处理函数;
  3. 我们在执行的时候,只需要传处理函数即可(代码中是打印’test’);
3.3、Thunk函数与异步函数

单纯的Thunk函数有优点,但不明显,但当和ES6的Generator函数结合之后,Thunk函数将十分好用。

先看示例代码:

// 一个普通的异步函数,接受3个参数,分别是延迟,执行处理函数,和回调函数
function delay(time, dealCallback, callback) {
    setTimeout(function () {
        dealCallback()
        callback()
    }, time ? time : 1000)
}
// 一个标准的thunk函数,声明时暴露打印内容和时间的配置,但不会立即执行
// 返回值是一个参数为回调函数的函数(执行本函数时,异步不会执行)
function thunkForAsync(time, dealCallback) {
    return function (callback) {
        delay(time, dealCallback, callback)
    }
}

// 一个Generator函数,目的是连续发起2次异步请求,但打印内容不同
function *g() {
    yield thunkForAsync(null, function () {
        console.log('first')
    })
    yield thunkForAsync(500, function () {
        console.log('second')
    })
}
// 一个用于自动执行(管理)Generator函数的Thunk函数
// 自动控制 Generator 函数的流程,接收和交还程序的执行权
// 但要求Generator函数的每个yield表达式,都是Thunk函数(不然next没有办法作为参数传过去)
function thunkForGenerator(callback) {
    let g = callback()

    function next() {
        let result = g.next()
        if (result.done) {
            return
        }
        result.value(next)
    }

    // 这里启动函数
    next()
}
// 执行管理函数,将Generator函数作为参数传给他即可
thunkForGenerator(g)

这个代码比较复杂,初次接触时可能无法理解,这里慢慢解释:

  1. 首先,需要有1个Thunk函数,用于管理Generator函数,他的作用是自动控制 Generator 函数的流程,接收和交还程序的执行权;
  2. 在以上代码中,这个函数是thunkForGenerator
  3. 其次,thunkForGenerator这个函数要求g这个Generator函数里,每个异步请求都是Thunk函数;
  4. 具体来说,就是yield表达式的值是一个函数,该函数只有一个参数,并且该参数是回调函数;
  5. thunkForAsync函数对delay函数进行封装后,满足了这一点;
  6. 于是thunkForGenerator函数可以管理g函数了

thunkForGenerator这个函数的管理是这么做的:

  1. 当Generator函数传入函数内后,执行他,获得一个遍历器;
  2. 执行这个遍历器的next()方法,获取返回值result;
  3. 假如result的done属性为true时,返回(说明遍历完毕了);
  4. 否则说明该result.value有值,且该值是yield表达式的值;
  5. 由于yield表达式的值是一个Thunk函数,因此可以将传一个函数给他,而这个函数是重新循环本流程的第二至五步(thunkForGeneratornext方法);

控制权的交还流程如下:

  1. 虽然thunkForGenerator函数将next方法传给了Thunk函数,但该函数不会立即执行,而是传给了delay函数的参数三;
  2. 而delay函数的参数三是一个函数,但该函数只会在异步执行完毕后去执行;
  3. 也就是说,当第一次执行遍历器的next方法后,控制权从thunkForGenerator函数交给了g函数
  4. 然后g函数会触发一次1000ms延迟的异步(thunkForAsync函数),等异步执行完毕后,g函数执行了thunkForGenerator函数的next方法,将控制权重新还给了他;
  5. 然后thunkForGenerator函数发现done为false,于是又执行了遍历器的next方法,将控制权交给了g函数
  6. g函数执行完代码,通过执行thunkForGenerator函数的next方法,将控制权还了回去;
  7. 此时thunkForGenerator函数再次执行遍历器的next,但g函数发现此时没代码可执行了,于是返回结果,done为true;
  8. 因为done为true,所以thunkForGenerator函数不再继续下去,也返回了。

4、Thunk函数的通用处理

4.1、es6版的Thunk函数转换器

Thunk函数还是有用的,但每次都要封装一个Thunk函数,还是有点麻烦。

由于Thunk函数的特点鲜明,即:

  1. 只有一个回调函数作为参数;

因此可以进行固定的封装处理,写一个转换器就好啦,具体方法如下:

1、最后需要执行目标函数。可以用call或者apply来执行,执行的时候需要指定this,其他参数,以及回调函数:

fn.call(this, ...args, callback);
// 分别是this,其他参数(扩展运算符展开),执行的回调函数

2、执行上面这段代码的时候,应该是将回调函数传入函数,那么应在上面代码外面加一层壳。
由于同时要获取该函数执行完毕后的返回值,因此应返回fn.call()的返回值

let foo = function (callback) {
    return fn.call(this, ...args, callback)
}

3、回调函数的参数处理了,但我们也得配置一下其他参数吧,用一个函数搞定他。

let foo = function (...args) {
    return function (callback) {
        return fn.call(this, ...args, callback)
    }
}

这个的核心就是利用扩展运算符,即es6新增的函数的rest参数,具体可以参考链接里的博客。
简单来说,就是将传的所有参数取出来,然后放到一个数组里并赋值给args。在fn.call里面,再将这个数组展开作为fn调用时的前n个参数。

4、等等,我们配好了fn的参数,也配好了他的回调函数,但fn函数呢?

const Thunk = function (fn) {
    return function (...args) {
        return function (callback) {
            return fn.call(this, ...args, callback)
        }
    }
}

5、Thunk通用转换函数写好了——es6版本的!老版本的懒得写了,反正就是把不支持的新特性用老的方法实现一遍而已啦。

4.2、实战利用转换器转换函数变为Thunk函数

为了方便,原函数的回调函数应作为函数的最后一个参数使用。

不然还得改写Thunk函数转换器,没必要,所以原函数如果不符合的话,调整一下原函数的参数就行。

1、被处理的函数,为了方便理解,具有以下特点:
①console.log(this),显示this指向目标;
②有两个参数,对两个参数进行计算,并将值传给身为第三个参数的回调函数;
③执行第三个参数的回调函数;
④有返回值;
如代码:

function foo(x, y, callback) {
    console.log(this)   // ①
    let result = x + y; // ②
    callback(result)    // ③
    return result       // ④
}

2、将foo函数传入Thunk函数转换器进行第一步处理:

let setting = Thunk(foo)
// setting = function (...args) {
//     return function (callback) {
//         return foo.call(this, ...args, callback)
//     }
// }

为了帮助理解,处理后的结果见注释

3、传入foo函数需要的其他参数,为了帮助理解,特通过bind绑定this指向的对象:

let fn = setting(1, 2).bind("abc")  //在这步绑定this指向的目标
// fn = (function (callback) {
//     return foo.call(this, 1, 2, callback)
// }).bind("abc")

此时只是改变了this指向的目标,和传入了参数,但尚未执行foo

4、传入回调函数,执行foo函数:

let result = fn(data => {
    console.log(data)
})
// result = foo.call(this, 1, 2, data => {
//     console.log(data)
//     console.log(this)
// })

// 输出:
// "abc"
// 3

5、别忘了,为了测试,我们还添加了返回值,如果运转正确的话,返回值被赋值给了result(毫无疑问会运转正确):

result; //3

6、如果简化,也可以这么写,即第二三步合并。

let fn = Thunk(foo)(1, 2).bind("abc")

7、假如不用bind来修改this指向对象,然后改变this指向目标呢?

两种解决办法:
①Thunk函数再封装一层,即执行callback的时候尚不执行foo函数,等调用call的时候再执行。
但这种缺点很明显,增加了复杂度,执行回调函数的时候,每次都需要加一个call、apply,或者加一对括号()

const Thunk = function (fn) {
    return function (...args) {
        return function (callback) {
            return function () {        //额外增加的一层
                return fn.call(this, ...args, callback)
            }
        }
    }
}

②在绑定参数的时候,让this指向第一个参数。这种方法需要修改Thunk函数转换器。
但缺点也有,即利用此Thunk转换器转换函数的时候,每次绑定参数,都必须指定this指向的目标,如果忘记的话,代码执行将出现问题。

修改方式如下:

const Thunk = function (fn) {
    return function (...args) {
        return function (callback) {
            return fn.call(...args, callback)   // 这里去掉原本的第一个参数this
        }
    }
}

function foo(x, y, callback) {
    // 略
}

let fn = Thunk(foo)("abc", 1, 2)    // 这里的第一个参数就是this指向的目标
let result = fn(data => {
    // 略
})

修改的地方参照注释

4.3、实战演示改造3.3中的代码

改造后的代码如下:

// delay函数略
const Thunk = function (fn) {
    return function (...args) {
        return function (callback) {
            return fn.call(this, ...args, callback)
        }
    }
}
function *g() {
    yield Thunk(delay)(null, function () {
        console.log('first')
    })
    yield Thunk(delay)(500, function () {
        console.log('second')
    })
}
// thunkForGenerator函数略

5、Promise对象的Generator函数自动执行器

5.1、改造管理自动执行的函数

上面用的是Thunk函数自动执行Generator函数,让其可以依次执行。

原理是:

  1. 将管理Generator函数自动执行的Thunk函数称之为(T函数),并且要求每个Generator函数的yield表达式都是一个Thunk函数。
  2. T函数会判断遍历器的next()的返回值的done是否为true,不是true的话将下一次判断的函数作为回调函数传给yield表达式的Thunk函数;
  3. 而yield表达式因为是Thunk函数,因此在执行完毕后,会执行这个回调函数,让T函数的判断函数继续执行;
  4. 然后循环,直到done为true的时候终止

关键点是什么?

由于Generator函数的yield表达式是Thunk函数,那么只要T函数传过去的回调函数,能判断结果,再次执行判断并传递同样的回调函数,那么就可以做到。

假如我们只改造管理管理Generator函数的T函数,使用Promise来实现,那么代码改如何改?

很简单,只需要改上面thunkForGenerator函数的next函数即可

function promiseForGenerator(callback) {
    let g = callback()

    let next = function () {
        let result = g.next()
        if (result.done) {
            return
        }
        return new Promise((resolve, reject) => {
            resolve(result.value)
        }).then(fn => {
            fn(next)
        })
    }
    next()
}
promiseForGenerator(g)

核心思想就一点,在没结束的时候返回一个Promise对象,将result.value作为resolve的参数,然后在then里面执行。

讲道理说,如果只是这样,这种改造方法没啥意义,只是提供一种思路——我们还会面临使用Promise的情况。

例如:

yield表达式的值不是Thunk函数,而是Promise对象呢,显然之前的thunkForGenerator函数是不能解决这个问题的。

5.2、使用promise管理Generator函数自动执行

这时,假设yield表达式不是Thunk函数了,而是一个Promise对象呢(这显然很常见,异步请求用Promise对象来实现,在es6出来后是很正常的事情)。

显然,上面的thunkForGenerator不能满足要求,我们需要改造他。

首先,我们需要制造一个转换器,用于将普通的异步请求转换为Promise对象。

const bePromise = function (fn) {
    return function (...args) {
        return new Promise((resolve, reject) => {
            fn.call(this, ...args, resolve)
        })
    }
}

Generator函数和3.3比起来有变化,但和4.3的相同:

function *g() {
    yield bePromise(delay)(null, function () {
        console.log('first')
    })
    yield bePromise(delay)(500, function () {
        console.log('second')
    })
}

因为改成了Promise对象,所以处理函数也需要做相应的改变

function thunkForGenerator(callback) {
    let g = callback()
    function next() {
        // 这个时候,假如是Promise对象,已经开始执行异步请求了
        let result = g.next()
        if (result.done) {
            return
        }
        // 当Promise的对象改变时,会触发then
        result.value.then(() => {
            next()
        })
    }
    next()
}

完整代码如下:

function delay(time, dealCallback, callback) {
    setTimeout(function () {
        dealCallback()
        callback()
    }, time ? time : 1000)
}

const bePromise = function (fn) {
    return function (...args) {
        return new Promise((resolve, reject) => {
            fn.call(this, ...args, resolve)
        })
    }
}

function *g() {
    yield bePromise(delay)(null, function () {
        console.log('first')
    })
    yield bePromise(delay)(500, function () {
        console.log('second')
    })
}

function thunkForGenerator(callback) {
    let g = callback()

    function next() {
        // 这个时候,假如是Promise对象,已经开始执行异步请求了
        let result = g.next()
        if (result.done) {
            return
        }
        // 当Promise的对象改变时,会触发then
        result.value.then(() => {
            next()
        })
    }

    next()
}
thunkForGenerator(g)
5.3、Co模块

Co模块的具体介绍参照阮一峰的博文,我这里只讲一些大概内容。

首先,Co模块的实现原理,与5.2中的thunkForGenerator函数相近。

5.2代码中的缺陷(Co模块改进内容):

  1. 5.2中代码是没有返回值的,因此只能假定不会出错,但实际中显然是不可能的;
  2. yield表达式,虽然使用了Promise对象,但没考虑到对出错情况的捕获和管理。即,假如在执行next的时候出错了,会导致代码中断运行;
  3. 没有考虑到thunkForGenerator函数的参数不符合要求的情况;
  4. 没有考虑到yield表达式并非Promise对象的情况;

Co模块的github地址

但基于仿其原理,进行改造并不难,当然,不会像Co模块那样完善。

另外,为了处理对错误的捕获,需要修改delay函数为模拟真正的Promise异步函数(而非取巧的)

function delay(time, dealCallback) {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            try {
                dealCallback()
                throw new Error("abc")
                resolve()
            } catch (err) {
                reject(err)
            }
        }, time ? time : 1000)
    })
}

const byPromise = function (fn) {
    return function (...args) {
        return fn.call(this, ...args)
    }
}

function *g() {
    yield byPromise(delay)(null, function () {
        console.log('first')
    })
    yield byPromise(delay)(500, function () {
        console.log('second')
    })
}

function co(gen) {
    return new Promise((resolve, reject) => {
        // 略去判断gen不是generator函数的代码

        let g = gen()

        function onSuccess(res) {
            let result = g.next(res)
            // 增加错误捕获
            try {
                next(result)
            } catch (err) {
                reject(err)
            }
        }

        function onError(err) {
            let ret;
            try {
                ret = g.throw(err)
            } catch (e) {
                return reject(e)
            }
            next(ret)
        }

        function next(result) {
            if (result.done) {
                return resolve(result.value)
            }
            // 略去对每一步的value属性是不是Promise对象的检查
            result.value.then(onSuccess, onError)
        }

        onSuccess()
    })
}

co(g).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})

优化改造完毕

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Generator thunk 函数是一个返回 generator 对象的函数,这个 generator 对象可以用来执行异步的操作。 在 JavaScript 中,generator 函数是一种特殊的函数,它可以暂停执行并在需要时恢复执行。Generator 函数可以通过 yield 关键字来暂停执行,并通过 next() 方法来恢复执行。 Generator thunk 函数可以用来处理异步操作的回调函数,它会将回调函数转换成 generator 函数,并将 generator 对象返回给调用者。调用者可以通过调用 generator 对象的 next() 方法来异步执行操作,并通过 yield 关键字来暂停执行。当异步操作完成时,回调函数会将结果作为参数传递给 generator 函数,generator 函数再通过调用 next() 方法来恢复执行。 以下是一个示例代码,展示了如何使用 generator thunk 函数来处理异步操作的回调函数: ``` function* asyncOperationThunk(callback) { const result = yield callback; // 暂停执行,等待异步操作完成 return result; } // 异步操作的回调函数 function asyncOperation(callback) { setTimeout(() => { callback('异步操作完成'); }, 1000); } // 使用 generator thunk 函数来执行异步操作 const thunk = asyncOperationThunk(asyncOperation); const iterator = thunk(); // 获取 generator 对象 const next = iterator.next(); // 启动 generator 函数 next.value((result) => { console.log(result); // 输出:异步操作完成 iterator.next(result); // 恢复执行 generator 函数 }); ``` 在上面的代码中,我们首先定义了一个 generator thunk 函数 asyncOperationThunk,它接受一个回调函数作为参数。在 asyncOperationThunk 中,我们使用 yield 关键字来暂停执行,并等待异步操作完成。当异步操作完成时,回调函数会将结果作为参数传递给 generator 函数,generator 函数再通过调用 next() 方法来恢复执行。 接下来,我们定义了一个异步操作的回调函数 asyncOperation,并将它传递给 asyncOperationThunk 函数。我们使用 asyncOperationThunk 函数来创建一个 thunk 函数,并通过调用它来获取 generator 对象和启动 generator 函数。 最后,我们通过调用 generator 对象的 next() 方法来异步执行操作,并通过回调函数来获取操作结果。当操作完成时,我们再次调用 generator 对象的 next() 方法来恢复执行 generator 函数,并将操作结果作为参数传递给它。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值