小白也要学 Promise

@CSS魔法

2016. 04. 22.

(入门篇)

  • Promise 是什么?
  • Promise 有什么用?
  • 如何用 Promise 改善既有代码?

(30~45 分钟)

Promise 是什么?

这是一个自取其辱的问题……

  • 懂的人本来就懂
  • 不懂的人还是不懂

Promise 就是 Promise 呀!!

Promise 就是对未来的 “承诺” 呀!

Promise 是一种异步编程方式。

所以……

  • 先别管它是什么了
  • 直接看它能做什么就好了

Promise 有什么用?

  • 获知未来的状态变化
  • 获取未来的值
  • 管理多个异步过程

优雅满足三种需求:

“通过什么来做到?”

先看最基本的 API。

// 有一个 Promise
var myPromise = (/* 通过某些黑魔法创建了一个 Promise */)

// 刚刚创建的 Promise 就像一个装了礼物的盒子,现在还不能拆
// 但在未来的某个时间点,盒子会自己打开
// 到时候你就知道里面装的是什么了
// 但你现在能做的,只是先想好到时候你要做什么
myPromise.then(function (content) {
    // 通过这个 “回调函数” 来约定你要对盒子里的内容做什么
    console.log(content)
})
// 有一个 Promise
var myPromise = {/* 通过某些黑魔法创建了一个 Promise */}

// 还有些时候,我们只关心盒子什么时候打开
// 并不关心盒子里有什么东西
myPromise.then(function () {
    // 通过这个 “回调函数” 来约定当盒子打开时你要做的事儿
    console.log('It is time.')
})

获知未来的状态变化

  • 以事件的方式
  • 以回调函数的方式
  • 以 Promise 的方式

在 JavaScript 的世界里,外部环境如果要通知你某个状态在未来发生了变化,有哪些做法?

以 DOM Ready 为例吧……

以事件的方式

// Whatwg API
document.addEventListener('DOMContentLoaded', function () {
    /* init widgets here */
});

以回调函数的方式

// 万能的 jQuery
var $ = require('jquery')

$(function () {
    /* init widgets here */
});

以 Promise 的方式

// 假设有一个类库,它返回一个 Promise
// 这个 Promise 在 DOM Ready 时打开……
var promiseDomReady = require('promise-dom-ready')

promiseDomReady.then(function () {
    /* init widgets here */
});

貌似并没有什么卵用?

获取未来的值

  • 以回调函数的方式
  • 以事件的方式
  • 以 Promise 的方式

有一个值,现在还不存在,在等到未来某个时间点才能拿到。在 JavaScript 世界中,如何实现?

比如,我们需要获取用户输入的某个值……

button.submit

请输入一个值:

#dialog

input.text

以回调函数的方式

// 先想好拿到值后要做什么
function handleUserInput(value) {
    console.log(value)
}

// 监听用户行为,等待用户提供值
$('#dialog button.submit').once('click', function () {
    $('#dialog').close()

    var value = $('#dialog input.text').val()
    // 当我们需要值出现时,让预先定义好的回调函数来处理它
    handleUserInput(value)
});

$('#dialog').popup()

“用值” 的代码混在 “等值” 的代码中了

以事件的方式

// 监听用户行为,等待用户提供值
$('#dialog button.submit').once('click', function () {
    $('#dialog').close()

    var value = $('#dialog input.text').val()
    // 通过自定义事件把值抛出
    $(window).trigger('user-input', {value: value})
});

$('#dialog').popup()

// 监听这个自定义事件,以便在未来获取抛出的值
$(window).on('user-input', function (event, param) {
    console.log(param.value)
})

“等值” 的代码和 “用值” 的代码分得比较清楚了。

以 Promise 的方式

// 把用户在未来提供的值包装成一个 Promise
var promiseUserInput = {/* 通过某些黑魔法创建了一个 Promise */}

// 等值可用时,我们来处理它
promiseUserInput.then(function (value) {
    console.log(value)
})

很清爽,有木有?

但是……“黑魔法” 部分属于作弊吧?

对应用层来说,连对话框的弹出和关闭都不需要操心了。

来看看 “黑魔法”

第二个重要的 API:

如何创建一个 Promise?

// Promise 构造器
var myPromise = new Promise(function (resolve, reject) {
    // 我们在这里决定这个 Promise 如何、何时得到未来的值
    // 再用 resolve() 函数来把值抛出来
    resolve(...)
})
// Promise 构造器
var myPromise = new Promise(function (resolve, reject) {
    // 我们在这里决定这个 Promise 如何、何时得到未来的值
    // 再用 resolve() 函数来把值抛出来
    
    // 不过一般来说,resolve() 都不会是同步调用
    // 它往往在一个异步过程中被调用,比如:
    setTimeout(function () {
        resolve(...)
    }, 1000)
})

我们来把那个 “等待用户输入”

的例子写完整。

// 把用户在未来提供的值包装成一个 Promise
var promiseUserInput = {/* 通过某些黑魔法创建了一个 Promise */}

// 等值可用时,我们来处理它
promiseUserInput.then(function (value) {
    console.log(value)
})

我们没写完的代码:

// 把用户在未来提供的值包装成一个 Promise
var promiseUserInput = new Promise(function (resolve, reject) {
    $('#dialog').popup()
    $('#dialog button.submit').once('click', function () {
        $('#dialog').close()
    
        var value = $('#dialog input.text').val()
        // 等值出现时,把它抛出
        resolve(value)
    });
})

目前为止我们感受到的 Promise 的好处:

  • 把异步逻辑包装在它内部
  • 对外暴露清爽的接口

我差不多懂了,我们找个案例来试试 Promise 吧!

我年轻的时候写过一个组件叫 XStorage:

// 以前设计的 API 一般采用回调函数方式
XStorage.getItem = function (key, callback) {
    /* ... */
}
XStorage.setItem = function (key, value, callback) {
    /* ... */
}

它用于跨域存取本地存储。

它通过 postMessage() 来实现跨域通讯,因此天生是异步的。

能用,但不好看,也不好用。

如果给我一个重新设计 API 的机会……

如果给我一个重新设计 API 的机会……

// 所有 API 返回 Promise
XStorage.getItem = function (key) {
    return new Promise(function (resolve, reject) {
        /* ... */
    })
}
XStorage.setItem = function (key, value) {
    return new Promise(function (resolve, reject) {
        /* ... */
    })
}

// 在调用时
XStorage.getItem('key').then(...)
XStorage.setItem('key', 1).then(...)

管理多个异步过程

这才是 Promise 真正的威力所在。

Task 1

先看多个 “串行” 的异步过程

Task 2

Task 3

Task 4

在旧社会,我们是这样写的:

在旧社会,我们是这样写的:

// 我们有一些异步任务,每个任务完成后会调用传给它的回调函数
function asyncTask1(callback) {...}
function asyncTask2(callback) {...}
function asyncTask3(callback) {...}
function asyncTask4(callback) {...}

// 通过回调函数来实现顺序依赖
asyncTask1(function () {
    asyncTask2(function () {
        asyncTask3(function () {
            asyncTask4(function () {
                console.log('All done!')
            })
        })
    })
})

传说中的 “回调金字塔”!

“Promise 对此有什么帮助?”

关于 .then() 这个 API 的真相……

真相一

每次调用 .then() 都返回一个新的 Promise

看代码:

// 有一个 Promise
var myPromise = new Promise(...)

myPromise
    .then(...)    // 返回一个新的 Promise
    .then(...)    // 又返回一个新的 Promise
    //...

可以一直 .then() 下去!

“链式调用”!

看代码:

(如果其返回值是 Promise,则 .then() 将其直接返回。)

真相二

每个 Promise 都可以包装一个值,

这个新的 Promise 包装的值从哪里来?

.then() 的回调函数的返回值

将被包装在这个新 Promise 中

myPromise.then(function () {
    /* 做了一些事 */
    return something
})

看个例子:

// 有一个 Promise
var myPromise = new Promise(...)

myPromise
    .then(function (value) {
        return handle(value)
    })    // 返回一个 Promise,它包装的值是上一个 Promise 包装的值处理后的结果

    .then(function (value) {
        var promiseAjax = new Promise (function (resolve) {
            $.post(url, {qty: value}, function (data) {
                resolve(data)
            })
        })
        return promiseAjax
    })    // 直接返回 promiseAjax

看个例子:

好,带着新技能来试试

如何组织多个串行的异步任务。

在 Promise 时代:

在 Promise 时代:

// 我们有一些异步任务,每个任务均返回一个 Promise
function asyncTask1() {...}
function asyncTask2() {...}
function asyncTask3() {...}
function asyncTask4() {...}

// 通过 .then() 来实现顺序依赖
asyncTask1().then(function () {
    return asyncTask2()
}).then(function () {
    return asyncTask3()
}).then(function () {
    return asyncTask4()
}).then(function () {
    console.log('All done.')
})

还可以简化!

在 Promise 时代:

// 我们有一些异步任务,每个任务均返回一个 Promise
function asyncTask1() {...}
function asyncTask2() {...}
function asyncTask3() {...}
function asyncTask4() {...}

// 通过 .then() 来实现顺序依赖
asyncTask1()
    .then(asyncTask2)
    .then(asyncTask3)
    .then(asyncTask4)
    .then(function () {
        console.log('All done.')
    })

好,接下来看看

多个 “并行” 任务是什么情况。

Task 1

获知多个 “并行” 异步过程全部结束的时间点

Task 2

Task 3

Task 4

又要 Get 新 API 了:

// 这个 API 可以监控多个 Promise 的状态
// 它接受多个 Promise 组成的数组,返回一个 Promise
// 当所有 Promise 都完成后,它自己也会完成。
Promise.all([
    promise1,
    promise2,
    ...
])

Promise.all()

来试试新技能:

来试试新技能:

// 我们有一些异步任务,每个任务均返回一个 Promise
function asyncTask1() {...}
function asyncTask2() {...}
function asyncTask3() {...}
function asyncTask4() {...}

// 通过 Promise.all() 来监控多个并行任务何时全部完成
Promise.all([
    asyncTask1(),
    asyncTask2(),
    asyncTask3(),
    asyncTask4(),
])
    .then(function () {
        console.log('All done.')
    })

Task 1  

获知多个 “并行” 异步过程中谁最先结束

Task 2

   Task 3

Task 4

还有一个新 API:

// 这个 API 可以监控多个 Promise 的状态
// 它接受多个 Promise 组成的数组,返回一个 Promise
// 当其中有一个 Promise 完成时,它自己也会完成。
Promise.race([
    promise1,
    promise2,
    ...
])

Promise.race()

来试试看,其实跟刚才的例子差不多:

来试试看,其实跟刚才的例子差不多:

// 我们有一些异步任务,每个任务均返回一个 Promise
function asyncTask1() {...}
function asyncTask2() {...}
function asyncTask3() {...}
function asyncTask4() {...}

// 通过 Promise.race() 来监控多个并行任务中谁最先完成
Promise.race([
    asyncTask1(),
    asyncTask2(),
    asyncTask3(),
    asyncTask4(),
])
    .then(function () {
        console.log('At less one task done.')
    })

用 Promise 改善既有代码

(Demo)

Q & A

Thank You!

小白也要学 Promise(入门篇)

By CSS魔法

小白也要学 Promise(入门篇)

初学者也能看懂的 Promise 入门教程,因为我也是一个初学者。

  • 1,157