懒是一个程序员的好习惯,没错就是这样。

问题

假设使用 JQuery 来写一个 Promise

  1. 调用时需要传入参数,比如读取文件的文件名。

  2. 将生成的 Promise 的对象返回出去。

那么这个方法大概是这样的:

var getFile = function (fileName){
    var defer = $.Deferred();
    
    //一些异步的操作,比如读取一个文件
    readFile(fileName, function(file){
        defer.resolve(file);
    })
        
    return defer.promise();
}
getFile('a.txt').then(function(result){
    console.log(result)
})

感觉还是挺简洁干净的,但对于每天要接触 js 中的异步操作的我来说,这在我脑里就是这样:

while(true){
    var defer = $.Deferred();
    defer.resolve(result);
    return defer.promise();
}

能不能把这 3 行给干掉啊,这样我就能少些 n * 3 行代码了。我也不想显示的控制返回,因为需要我脑子参与的应该就仅仅是写个逻辑。

解决

让函数返回函数,内层函数声明 Promise 函数,并在最后返回该 Promise 对象。

需要执行的函数以参数的形式传入到外层函数,在内层函数中进行调用,将 Promise 修改状态的两个函数 resolvereject 以参数形式传入以参数形式传入的函数 func

感觉有点绕,直接上代码吧。

首先不考虑执行的函数需要传递传参的情况,这种情况比较简单。

function q(func){
    return function(){
        var defer = $.Deferred();
        func && func(defer.resolve, defer.reject);
        return defer.promise();
    }
}

然后 getFile 就变成这样了:

var getFile= q(function (resolve, reject){
    readFile('a.txt', function(file){
        resolve(file);
    })
})
getFile().then(function(result){
    console.log(result)
})

// 其实眼尖的应该能看出了,这和原生实现 promise 差不了太多啊
var getFile = new Promise(function (resolve, reject){
    readFile('a.txt', function(file){
        resolve(file);
    })
})
// 但是这是不允许传递参数的,就如例子中的文件名。

是不是有一种酣畅淋漓的感觉,光写主要的代码逻辑就好了,函数返回的就是 Promise

接下来考虑到 getFile 可能需要传入参数,就如同开头说的那样,而且我们要实现的是通用的函数,所以我们不知道实际传入的参数的个数,但是实现起来需要用到 applyarguments 这两个在 js 中不太被了解的东西。

思考后,撸出了这样的代码,供大家参考:

function q(func){
    return function(){
        var args = Array.prototype.slice.call(arguments);
        var defer = $.Deferred();
        args.push(defer.resolve, defer.reject);
        func && func.apply(undefined, args);
        return defer.promise();
    }
}

当然使用 ES6 语法就会更简单

function q(func){
    return () => {
        let defer = $.Deferred();
        func && func.call(undefined, ...arguments, defer.resolve, defer.reject);
        return defer.promise();
    }
}

那么 getFile 就可以这样了:

var getFile = q(function (fileName, resolve, reject){
    readFile(fileName, function(file){
        resolve(file);
    })
})
getFile('a.txt').then(function(result){
    console.log(result);
})

// ES6
// 本来不想放上 ES6 的实现的,但确实 ES6 写起来更加的舒服。

let getFile = q((fileName, resolve, reject) => {
    readFile(fileName, file => resolve(file))
})
getFile('a.txt').then(result => console.log(result))

对于其他的不同实现的 Promise 比如说 $qQ 、原生实现的 Promise 也是一样的。

以下就仅仅放出了 ES5 的写法了,毕竟是一个小小函数,用时临时写就行,这里就不过多的考虑了。

原生的 Promise

function q(func){
    return function(){
        var args = Array.prototype.slice.call(arguments);
        return new Promise(function(resolve, reject) {
            args.push(resolve, reject);
            func && func.apply(undefined, args);
        });
    }
}

angular 中的 $q

function q(func){
    return function(){
        var args = Array.prototype.slice.call(arguments);
        var defer = $q.defer();
        args.push(defer.resolve, defer.reject);
        func && func.apply(undefined, args);
        return defer.promise;
    }
}

无论多少个参数都没用问题,突然感觉如沐春风啊,不多说了我要把剩下来的时间拿去睡觉拿去 happy 了。

最后强调一遍:懒是一个程序员的好习惯,没错就是这样。

你可能感兴趣的文章

9 条评论
function getData(data1, data2) {
  return $.get('http://xxx.xxx.xxx?data1=' + data1 + '&data2=' + data2)
}

getData('foo', 'bar')
  .then(result => console.log(result))

跟你的第一个例子一样一样的,而且你调用 getData 的时候还忘了传俩参数。

即使要返回转换后的结果,也没必要用 deferred

function getData(data1, data2) {
  return $.get('http://xxx.xxx.xxx?data1=' + data1 + '&data2=' + data2)
    .then(response => doSomething(response))
}

getData('foo', 'bar')
  .then(result => console.log(result))  // result 是 doSomething 处理过后的

我不觉得你真的理解 promises 了,你所做的是无意义的封装,滥用/错用 deferred pattern 的典型。

+1 回复

aco 作者 · 1月13日

$.ajax 封装了promise 那要是别的异步调用呢? 想到了吗?

回复

William007 · 1月13日

我只感觉作者的封装过于复杂,这样性能应该会差一些。es6的promise挺简洁的,简单用函数封装一下就可以做通用了

function tool (url, data) {

return new Promise(function (resolve, reject) {
  $.ajax({
    type: 'get',
    url: 'http://xxx.xxx.xxx?' + url,
    data: data,
    success: function (callback) {
      resolve(callback)
    },
    error: function (err) {
      reject(err)
    }
  })
})

}

tool('data=abc', 'haha').then(function (callback) {
console.log(callback)
}).catch(function (err) {
console.log(err)
})

而且Promise.all([promise1, promise2]).then() 这样还可以做到for循环的异步处理。

回复

aco 作者 · 1月13日

我把文章改了改,不用封装了 promise 的 $.ajax 例子,容易导致误解。
我想要的效果是,不去考虑 Promise 怎么实现,怎么返回
我仅仅要做的只是 去 resolve 或是 reject 数据
而且 即使是使用 $.ajax 也是有需要另起一个 Promise 的情况存在的

比如下面这种需要缓存请求数据的 promise

var cache = null;
function getData(data1){
    var defer = $.Deferred();
    if(cache){
        defer.resolve(cache);
    }else{
        $.get('http://xxx.xxx.xxx.xxx?data1='+data1)
            .then(function(data){
                cache = data;
                defer.resolve(data)
            })
    }
    return defer.promise()
}

还有用 es6 去和 es5 比简洁是不公平的。

回复

小兄弟,不要着急,学会心平气和的面对别人的评论对你会更有帮助。很显然,你没有读懂我的评论,我说你没有真的理解 promise,这才是问题的关键之处。

首先我写 es6 的部分根本就不是为了和 es5 比简洁,而是因为这里适合写;时至今日如果还没能把 es6 的精华部分变成日常的本能,这本身就是自己找罪受,何来公平一说?格局要大一点。

至于你具体怎么写你的封装函数,在我看来这个真的不重要。原因主要是两点:

  1. 你应该工作经验并不太长,参与项目的规模和复杂程度也不是太高,所以你才会把“每天写 10 多个 ajax”当成一个事儿来说——其实这根本就不是事儿。因此,你在本文中所描述的封装手法其实只是最最基础的基本功而已,完全不需要像“搞了一个大事情”一样的来描述。

  2. Promise 只是处理异步的一种模式,除此之外还有很多其他的手法。我很担心你过早的满足于这样一种封装而失去了探索更广阔世界的机会与动力。

说得难听一点(希望你不要介意,我对事不对人的说),这点成果离实战还差得远。可能你会有一些不服气,我给你举个例子好了,就拿你改良过的缓存版 promise 封装吧:

let cached = null;
function getData(data1) {
  return Promise.resolve(
    cached || $.get('http://xxx.xxx.xxx.xxx?data1='+data1)
  )
}

我没有直接把写缓存的逻辑放入封装函数,因为也可能有时候我就是不想写缓存呢?反正返回 promise 的意义就在于我可以自由的选择 then 去做什么,我完全可以把写缓存的主动权交给调用者。

或者,我可以提供一个开关:

let cached = null;
function getData(data1, useCache = false) {
  return Promise.resolve(
    cached || $.get('http://xxx.xxx.xxx.xxx?data1='+data1)
      .then(data => {
        if (useCache) cached = data;
        return data;
      })
  )
}

你现在知道我为什么说你没有正确理解 promise 了吗?另外 promise 真正的闪光点在于处理异常,这还是你完全没有考虑的层面。

回复

@aco useCache 这个名字起的不好,应该叫 writeCache 更合理,超时不能修改了,单独说一声。

回复

aco 作者 · 1月16日

兄弟,首先呢,我不是没读懂你的评论,而是我写这个文章的目的并不是你评论的那样。

我写这个函数的目的并不是想更好的使用 promise 并且在当前的环境下是不允许我使用 ES6 中的 promise 的,毕竟安卓低版本是没有这个东西的并且 JQuery 中也没有对应的实现,也就是说你给代码我是不能这么写的,虽然实现起来简单。

当然为了兼容可以引一个 promise 官方的兼容代码或是用 babel 把代码变成 ES5 的结构,但感觉也没什么必要。(既然有了干嘛不拿来用呢?对吧)

在强调一遍,我写这文章的目的不是如何更好的去用 promise ,至于为啥你理解成了这个,我想是我给的例子的问题。

我的目的是:让异步操作有一个默认的 promise 实现,并且不去考虑当前环境下,我应该使用的哪一种的 promise 实现

那就用一个笼统的例子来说我的目的,对于这个我已经把原文中的例子也给修改了,在 promise 中套一个 promise 确实是代码的冗余,而且我的目的也变的不那么明确了。

var asyn = function (time){
    var defer = $.Deferred();
    // 代表一般的异步操作
    setTimeout(function(){
        defer.resolve(/* 需要处理的数据 */)
    },time)
    return defer.promise();
}

对应于我的实现

var asyn = q(time, resolve, reject){
    // 代表一般的异步操作
    setTimeout(function(){
        resolve(/* 需要处理的数据 */)
    },time)
}

第一段代码,其实是没用通用性可言的,因为它必须在有 JQuery 的条件下才能运行,不然只可能是报错。而对于我实现的呢,虽然多了一个函数,但至少被我包装过的函数是都不用重写的,我需要重写的仅仅只是在当前环境下重新实现我的 q 就可以了。
封装好的异步操作,既可以复用,而且还有了一个默认的 promise 实现,我并不觉的这是画蛇添足。

文章中我也给出了其他实现了 promise 的框架中我的封装函数该怎么写,而且也就几行的函数,大概的逻辑懂了,我相信大家自己实现起来也很简单。

还有,既然你提到了项目的复杂度,说这个对于大型项目这点代码根本不算什么,说这个完全只是西瓜旁边的一粒芝麻,那么我就说说我自认为这个东西有什么用吧:

  1. 这个函数让异步的操作有了一个默认的 promise 实现

  2. 这些封装好的代码完全是可以复用的

  3. 基于第一点和第二点,完全可以这么想:甚至可以后端直接生成出来一个支持 CMDAMD 的文件给前端调用,前端只要根据当前的环境去实现一下 q 就可以了(当然这仅仅是对 ajax 而言,就如同上面说的,我不能确保前端有一个让人极其满意的环境的情况下)

最后说一句,这篇文章仅仅是我的一个小总结,相当如日记一类,看了你的评论,我也确实有所收益,但我想表达的和你的评论的不在一个点上面,还有既然你说小兄弟,把人说小了谁不乐意呢~

不积跬步,无以至千里;不积小流,无以成江海。
做人如此,写代码如此。回到最初的:难不成非要减少个百行代码才去封装一个函数?一共最多就不到 10 行的代码,减了 2 行也是 20% 啊,兄弟。

回复

aco 作者 · 1月16日

其实你的实现也过度封装了啊,$.ajax 是有 promise 实现,直接把 $.ajax 返回出去就好了,就可以像一般的 promise 那样调用了。

回复

William007 · 1月16日

我试了下,jQ确实有,zepto的默认模块是没有的。学习了,感谢。

回复

载入中...
aco aco

107 声望

发布于专栏

acco

专注于web前端

1 人关注

SegmentFault

一起探索更多未知

下载 App