promise、generator、async的简单应用

2017-10-19  本文已影响0人  我是xy

javascript的运行机制是单线程处理,即只有上一个任务完成后,才会执行下一个任务,这种机制也被称为“同步”。

“同步”的最大缺点,就是如果某一任务运行时间较长,其后的任务就无法执行。这样会阻塞页面的渲染,导致页面加载错误或是浏览器不响应进入假死状态。

如果这段任务采用“异步”机制,那么它就会等到其他任务运行完后再执行,不会阻塞线程。

一、es6之前实现异步的方式:

最常用的方法是采用回调函数,但普通的回调函数并不能实现异步效果:

(1)同步回调
function testFn(data, callback){
  callback(data);
}
testFn('0', function(data2){
  for (var i=data2; i<300000000; i++);
  console.log(1);
});

console.log(2);

等一秒钟左右for循环结束,控制台输出1后,才会输出2。这是因为 testFn 的回调函数 callback 是同步执行,所以for循环会阻塞后面的任务。

(2)异步回调

想要实现回调的异步执行,必须要借助js的其它方法,例如将上面 testFn 的回调函数放到延时器中调用,延迟的时间设置为0:

function testFn(data, callback){
  setTimeout(function(){
    callback(data)
  },0);
}

这样控制台会先输出2,接着大约一秒钟后再输出1,可见回调函数并没有阻塞后面的任务,实现了异步效果。

除了使用定时器,实现异步执行的方法还有:事件监听(例如点击事件“click”)、requestAnimationFrame、XMLHttpRequest(jq中ajax方法的核心)、WebSocket、Worker以及Node.js 的 fs.readFIle等。

二、promise、generator和async/await:

es6 引入了 generator 和 promise,es7 又增加了async/await。这三种函数有一个重要的作用,就是解决回调函数的异步执行和嵌套问题。

(因为网络上介绍这三种方法的文章很多,所以这里只介绍个人觉得相对常用的知识点)
(一)普通的回调嵌套:

定义一个函数,要求只有在获取两个数据(例如"data1"和"data2")后,才会执行“console.log”任务,通过对回调函数进行嵌套就可以达到目的:

function Test1(data, callback){
    if(data){          // 本例是同步,要实现异步可以添加延时器
        callback(data);
    };
};

Test1("data1",function(){        
    Test1("data2",function(data2){
        console.log(data2);     // 如果不传数据就不会执行回调函数
    });
});

回调函数的执行需要依赖上一个函数,这样的缺点是如果有多个回调函数,就需要嵌套很多层。这会使代码的可读性较差,并增加调试和维护的难度。

(二)promise

Promise是一个构造函数,它接收一个匿名函数作为参数:

new Promise(function(resolve, reject){
  resolve(console.log(1));
  reject(console.log(2));
  console.log(3);
})

console.log(4);
// 输出的顺序为1、2、3、4 

1、因为 promise 是构造函数(带有属性或方法的函数就叫构造函数),所以必须使用 new 实例化才能调用。而作为参数的匿名函数不需要额外调用就能执行。

2、匿名函数只能有 resolve 和 reject 两个参数。结合if判断使用的话,resolve 是条件正确时执行的方法,reject 则是错误时执行的方法。

3、将某一任务直接放到 resolve、reject 方法里,或者放到匿名函数里并不能实现异步效果。

function Test2(){
  return new Promise(function(resolve, reject){
    resolve(1);                     
  });
};

Test2().then(function(num){
  console.log(num);
  return num;
}).then(function(num2){
  console.log(num2);
})
console.log(2);      
// 数字输出的顺序是2、1、1

4、为了防止匿名函数的自动执行,需要再定义一个函数,并将 promise 函数作为该函数的返回值。

5、将回调函数写在 then 或 catch 方法里才能实现异步,then 接受的是 resolve 方法传递的数据,catch 则对应的是 reject。

6、resolve 只能向第一个 then 里的函数传递数据,后面 then 里的函数只能通过前一个 then 里函数的返回值获取参数。

function pro1(){
  return new Promise(function(resolve, reject){
    resolve(1);
  });
};

function pro2(){
  return new Promise(function(resolve, reject){
  });
};

Promise.all([ pro1(), pro2() ]).then().catch();
Promise.race([ pro1(), pro2() ]).then().catch();

7、all方法的作用:只有 pro1 和 pro2 都执行 resolve,Promise 才会执行 then 方法。如果其中有一个函数执行 reject,那么Promise 就执行catch方法。

8、race方法的作用:pro1 和 pro2 中只要有一个状态发生改变,Promise的状态就跟着发生改变,不论是resolve还是reject。

promise最大的作用

(一)能把嵌套改成链式调用。对上面的普通嵌套进行改造:

function Test2(data){
  return new Promise(function(resolve, reject){
    if(data){
      resolve(data);
    }
  });
};

Test2(1).then(function( data1 ){
  console.log( data1 );
  return 2;     
}).then(function( data2 ){
  console.log( data2 );
})

console.log(3);
// 输出结果为3、1、2

实现效果:
(1)先输出3,表示 then 方法里的回调函数是异步执行。
(2)如果调用 Test2 时不传数字“1”,就不会执行 resolve 方法,这样即使第一个 then 方法里的函数有返回值“2”,也不会执行第二个 then 方法,实现了需求。

(二)解决 ajax 不能传值给外部变量的问题
ajax 在 success 获取的数值无法传递给外部变量,除非设置为同步模式,而 promise 的 resolve 方法可以解决这个问题。

设一个外部变量 outsideData:

var outsideData ;
function TestAjax(data){
  return new Promise(function(resolve, reject){
    $.ajax({
        url: 'xxx.php';
        type: 'GET',
        datatype: 'json',
        data: ' ',
        success: function(res){
          resolve(res.data)
        }
    });
  });
};

TestAjax().then(function( data ){
  outsideData = data;
})
(三)async/await

先说 async/await,是因为 async 函数是 Generator 函数的语法糖,容易理解,而且和 promise 函数也有很大的关联。

用 async function 定义一个函数:

async function Test(){      
  return 1;         
}
Test().then(function(num){
  console.log(num);      
});
console.log( Test() );

得到的结果是

1、async function 返回的是一个 promise 对象,所以该函数可以调用 then 方法,并因此实现异步执行效果。

async function Test(){      
  await console.log( 1 );
  console.log( 2 );
  await console.log( 3 );
}
Test();
console.log( 4 );
// 结果为1、4、2、3

2、await 可以代替 then 方法,以实现异步效果。

3、但第一个 await 是同步执行。不论跟的是 promise,还是其他的函数或方法。

4、第一个 await 后面的任务,不论有没有 await 都是异步执行。但是如果要在 async 函数里再放一个函数,那前面就必须添加 await,否则会报错。

function proFn(){
  return new Promise(function(resolve, reject){
    resolve(2);
  });
};

async function Test(){
  var data1 = await proFn();
  console.log(1);
  console.log(data1);
};
Test();
console.log(3);
// 结果为3、1、2

5、对于第一个 await,最好的方法是使其等于一个变量,然后对这个变量进行处理。

6、如果 await 后面跟的是 promise ,那么匿名函数必须执行 resolve 方法,否则 await 后面的任务就无法执行。

async/await 最大的作用就是替代 promise 的 then 方法:
function proFn(data){
  return new Promise(function(){
    if(data){
      resolve(data);
    };
  });
};

async function Test(){
  var data1 = await proFn(1);
  var data1 = await proFn(2);
  console.log(data1);
  console.log(data2);
};
Test();

console.log(3);
// 结果为3、1、2

(1)async/await 将链式调用变得更加简化。
(2)async/await 传递参数的方式也比 then 简单,不需要通过 return 来传递参数。

(四)generator

和 promise、async/await 不同,generator 本身并不具有异步执行的功能。它在异步中的主要应用,是管理异步回调的执行流程。

generator 函数的特征是使用 * 和关键词 yield:

function* Gen(){
  yield 1;
}
var runGen = Gen();
console.log(runGen.next());  
// 输出结果为 {value: 1, done: false};

1、函数名后面加小括号并不能调用 generator 函数,只是创建了一个指针对象,next 方法才会执行函数。

2、next 方法返回的对象带有两个属性,分别是“done”和“value”。done 的值表示 generator 函数是否运行完。value 对应的是 yield 后面的值。

function* Gen (num){
  var num2 = yield console.log(num);
  yield console.log(num2);
}
var runGen = Gen(1); 
runGen.next(2);
runGen.next(3);
// 结果输出1和3。                      

3、next 方法可以传递参数,该参数是上一个 yield 表达式的值。
之所以没有输出“2”,就是因为第一个 next 方法并没有与之对应的"上一个 yield",所以传参无效。

generator 函数管理流程的应用:

使用 generator 管理异步函数,需要用到三个知识点,thunk函数、next方法得到的done属性、递归。

1、thunk函数
普通的多参数函数在调用时,需要一次性传入多个数据。
thunk 函数则是把多个参数拆开,使得在调用时,数据可以分开传入。

实现方法是定义一个函数,并且该函数的返回值也是一个函数,这样就能将参数拆开放在两个函数里:

// 普通的多参数函数:
function Test(data, callback){};
// 调用普通函数:
Test(data, callback);


// thunk函数:
function Test(data){
 return function(callback){
   callback(); 
 }
}
// 调用thunk函数:
var runThunk = Test(data);
runThunk(callback);

将 callback 参数提取出来,放在作为返回值的匿名函数里,在调用该函数时 callback 和 data 所对应的数据就可以分两步传入。

2、通过 done 属性控制流程:

generator 函数代替“嵌套”去控制流程的思路,就是通过上一个 yield 的执行情况,来决定下一个 next 方法是否执行,这需要用到 done 属性:

function Test(){
  setTimeout(function(){
    console.log(1);
  },0);     
}
function* Gen(){
  yield Test();
  yield console.log(2);
}

var runGen = Gen();
var genObj = runGen.next();

if(!genObj.done){
  runGen.next();
}
// 结果为2、1

(1)直接给 next 方法外面添加 if 判断的缺点是,假如某个 yield 后面跟的是异步函数,那么其他 yield 所对应的非异步任务就会优先执行。

如果必须保证前一个任务运行完后,才会执行下一步,就需要把 next 方法放到 value 属性里:

function Test2(){
  return function(callback){
      console.log(3);
      callback();
  }
}
function* Gen2(){
  yield Test2();
  yield console.log(4);
}

var runGen2 = Gen2();
var genObj2 = runGen2.next();

genObj2.value( function(){
  runGen2.next();
} );
// 得到的结果是 3、4

(2)想在 value 里使用 next 方法,需要将 value 变成一个函数,这就要用到 thunk 函数。而 value 后面加一个小括号,就能调用作为返回值的函数了。

(3)如果把 next 方法直接放到 value 里,那么 next 方法得到的结果会被当成 value 的参数,先输出。所以需要给 next 方法外面再包一层函数。

下面的函数就是最终形态:

function Test3(){
  return function(callback){
    setTimeout(function(){
       console.log(5);
       callback();
    },0)  
  }
}
function* Gen3(){
  yield Test3();
  yield console.log(6);
}

var runGen3 = Gen3();
var genObj3 = runGen3.next();

genObj3.value( function(){
  if(genObj3.done) return;
  runGen3.next();
} );
// 结果为5、6
3、使用递归自动执行 generator 函数:

首先看看手动执行 generator 的例子:

// 为了简洁,本例并没有使用异步
function Thunk(num){
  return function(callback){
    console.log(num);
    callback();          
  }; 
};

function* Gen(){
  yield Thunk(1);
  yield Thunk(2);
}

var runGen = Gen();

var runNext = runGen.next();
if(runNext.done) return;
runNext.value(function(){
  
  var runNext2 = runGen.next();
  if(runNext.done) return;
  runNext2.value(function(){
   
  });
});
// 结果输出1、2

假如存在多个 yield,就需要写很多 next,这会令代码变得臃肿。通过观察可以看出使用 next 方法的部分存在很大的重复性,所以可以使用递归(也就是函数内部调用自身)对其进行改造。

var  runGen = Gen();

function next(err, data) {
  var runNext = runGen.next(data);
  if (runNext.done) return;
  runNext.value(next);          
}
next();

5、修改 promise 的链式调用

function Test(num){
  return new Promise( function(resolve, reject){
    resolve(num);
  } );
}

function* Gen(){
  yield Test(1);
  yield Test(2);
}

var runGen = Gen();
function next(){
  var genObj = runGen.next();
                
  if(genObj.done) return;
  genObj.value.then(function(num){
    console.log(num);
    next();
  });
}
next();

console.log(3);
// 得到的结果为 3、1、2

总结:

关于es6的研究到此就告一段落了。个人觉得“类”和“箭头函数”是一定要掌握的,因为这两点能简化代码结构。至于异步,从例子的长短也能看出,generator 没必要了解很深,还是交给 promise 和 ansyc/await 吧。

三、参考:

1、http://www.cnblogs.com/webeye/p/5383785.html (js同步的缺点)
2、http://blog.csdn.net/tywinstark/article/details/48447135 (15楼回复)
3、http://stackoverflow.com/questions/9516900/how-can-i-create-an-asynchronous-function-in-javascript (js实现异步的方法)
4、http://www.ruanyifeng.com/blog/2014/10/event-loop.html (js运行机制)
5、http://www.nowamagic.net/librarys/veda/detail/787 (浏览器假死原因)
6、https://segmentfault.com/a/1190000003096984 (异步回调的缺点)
7、https://www.oschina.net/translate/event-based-programming-what-async-has-over-sync (回调函数嵌套的缺点)
8、https://segmentfault.com/q/1010000002577322 (回调函数如何实现异步)
9、http://es6.ruanyifeng.com/#docs/promise (promise知识点)
10、http://www.cnblogs.com/lvdabao/p/es6-promise-1.html (promise需要放到另一个函数里)
11、https://segmentfault.com/a/1190000007535316(await知识点)
12、http://blog.rangle.io/javascript-asynchronous-options-2016/(generator不是异步)
13、http://es6.ruanyifeng.com/#docs/generator (generator知识点)
14、http://www.liaoxuefeng.com/wiki/ (generator函数的调用)
15、http://www.jianshu.com/p/87183851756f (promise使用例子)
16、https://segmentfault.com/q/1010000011014844 (使用return返回ajax获取的数值)

上一篇下一篇

猜你喜欢

热点阅读