【JS】由ECMA规范学习ajax和promise

什么是异步操作?

Ajax

异步JavaScript与XML(AJAX)是一个专用术语,用于实现在客户端脚本与服务器之间的数据交互过程。这一技术的优点在于,它向开发者提供了一种从Web服务器检索数据而不必把用户当前正在观察的页面回馈给服务器。与现代浏览器的通过存取浏览器DOM结构的编程代码 (JavaScript) 动态地改变被显示内容的支持相配合,AJAX让开发者在浏览器端更新被显示的HTML内容而不必刷新页面。换句话说,AJAX可以不用刷新整个页面就更新网页内容,使基于浏览器的应用程序更具交互性而且更类似传统型桌面应用程序。
XMLHttpRequest工作原理

从这个描述里面,我们可以看出来,Ajax包含的内容应该有

  • 能够跟服务端发起网络请求(一般是http,但是也可以是ftp或者其他协议的请求),处理服务端的网络响应
  • 能够根据服务端返回的数据,调用JS来修改DOM结构和渲染CSS样式
  • 能够跟js事件循环交互处理异步的响应和回调

而Ajax实现核心功能其实就是通过XMLHttpRequest

XMLHttpRequest(XHR)

XHR在发起请求时,运行机制如图所示:


XMLHttpRequest.png

在XMLHttpRequest发起请求之后,渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,就会根据相关的状态来调用对应的回调函数

xmlhttprequest-运作机制
ajax底层实现

XMLHttpRequest关键方法和参数
  • XMLHttpRequest.open:表示开启一个请求
  • XMLHttpRequest.send:表示发送一个请求
  • XMLHttpRequest.readyState:表示当前所处的状态
状态 描述
0 UNSENT 代理被创建,但尚未调用 open() 方法。
1 OPENED open() 方法已经被调用。
2 HEADERS_RECEIVED send() 方法已经被调用,并且头部和状态已经可获得。可以通过 setRequestHeader() 方法来设置请求的头部, 可以调用 send() 方法来发起请求。
3 LOADING 下载中; responseText 属性已经包含部分数据。
4 DONE 下载操作已完成。
  • XMLHttpRequest.onreadystatechange:请求状态变化时的回调,用户定义的回调函数在此调用

此处只列出了关键参数,XMLHttpRequest详细的从参数和方法可以参考MDN
XMLHttpRequest/readyState

JS实现Ajax
  • 实现思路(以发起HTTP请求为例)
    1.ajax接收json格式的参数option,json里面的参数用于配置发起网络请求的具体参数
    2.参数option与HTTP请求相关配置参数处理,如果传入option里面有些必填的参数没有(比如:请求类型(get还是post),datatype等),那么ajax中可以设置默认值,或者判断之后抛出异常。
    3.参数option的数据具体参数data json的格式处理
    4.创建XMLHttpRequest实例对象,考虑兼容性(可选),因为在IE6以下版本是ActiveXobject
    5.XMLHttpRequest.open(method, url, async, user, password);初始化请求,根据请求的类型是get还是post,确定open的从参数是如何传输的
    6.XMLHttpRequest.send()发送请求,如果是POST,参数data数据是由send发送的,但是send不支持json格式参数,所以要对data数据进行编码
    7.当readyState状态变化,设置XMLHttpRequest.onreadystatechange()回调,如果返回的HTTP请求状态码是请求成功相关的状态码,执行,success的回调,否则执行error的回调
    8.请求超时未完成,XMLHttpRequest.abort()自动中断
<script>
    function ajax(options){
        options = options ||{};  //调用函数时如果options没有指定,就给它赋值{},一个空的Object
        options.type=(options.type || "GET").toUpperCase();/// 请求格式GET、POST,默认为GET
        options.dataType=options.dataType || "json";    //响应数据格式,默认json
       options.timeout=options.timeout|| 5000;    //响应数据格式,默认json
        var params=formatParams(options.data);//options.data请求的数据

        var xhr;

        //考虑兼容性
        if(window.XMLHttpRequest){
            xhr=new XMLHttpRequest();
        }else if(window.ActiveObject){//兼容IE6以下版本
            xhr=new ActiveXobject('Microsoft.XMLHTTP');
        }

        //启动并发送一个请求
        if(options.type=="GET"){
            xhr.open("GET",options.url+"?"+params,true);
            xhr.send(null);
        }else if(options.type=="POST"){
            xhr.open("post",options.url,true);

            //设置表单提交时的内容类型
            //Content-type数据请求的格式
            xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
            xhr.send(params);
        }

    //    设置有效时间
        setTimeout(function(){
            if(xhr.readySate!=4){
                xhr.abort();
            }
        },options.timeout)

    //    接收
    //     options.success成功之后的回调函数  options.error失败后的回调函数
    //xhr.responseText,xhr.responseXML  获得字符串形式的响应数据或者XML形式的响应数据
        xhr.onreadystatechange=function(){
            if(xhr.readyState==4){
                var status=xhr.status;
                if(status>=200&& status<300 || status==304){
                  options.success&&options.success(xhr.responseText,xhr.responseXML);
                }else{
                    options.error&&options.error(status);
                }
            }
        }
    }

    //格式化请求参数
    function formatParams(data){
        var arr=[];
        for(var name in data){
            arr.push(encodeURIComponent(name)+"="+encodeURIComponent(data[name]));
        }
        arr.push(("v="+Math.random()).replace(".",""));
        return arr.join("&");

    }
    //基本的使用实例
    ajax({
        url:"http://server-name/login",
        type:'post',
        data:{
            username:'username',
            password:'password'
        },
        dataType:'json',
        timeout:10000,
        contentType:"application/json",
        success:function(data){
      。。。。。。//服务器返回响应,根据响应结果,分析是否登录成功
        },
        //异常处理
        error:function(e){
            console.log(e);
        }
    })
</script>

js原生实现ajax

Promise


MDN中对于Promise 的定义是:表示一个异步操作最终完成 (或失败), 及其结果值.

了解过JS事件循环的机制的话,我们会发现,JS当中认为异步操作,在开始执行到获取最后结果之间,是允许其被挂起等待(无论它真实的执行事件多快),也就是说异步任务的代码执行了之后,可以先把这个任务的具体执行结果放一边,先执行其他同步代码,再来执行异步任务的回调。而且操作的结果可能是成功的也可能是失败的。因此在Promise内部有一个属性[[PromiseState]],保存了三种状态,表示异步行为的整个过程。

  • Pending:表示异步操作还没有结束
  • fulfilled:表示异步操作执行成功了
  • rejected:表示异步操作被拒绝了

对于fulfilledrejected状态来说,代表的都是promise已经结束了,所以这两个状态又统称为settle状态。
我们可以根据这三个状态的转换,来理解整个Promise的运行过程。

promise替代ajax为例,我们会看到形如这样的代码:


const promise1 = new Promise(function(resolve,reject){
    ajax(
        {
            url:'http://testUrl',
            success:function(data){
          resolve(data);
            },
            error:function(e){
                reject(e);
            }
        }
    )
});
promise1.then(function(data){
    //get data from resolve
},
function(e){
    //get e from reject 
});

从Promise的使用的代码中,我们可以把它大致分成三个部分:

  • Promise(function(resolve,[reject]))构造函数
  • Promise.prototype.then
  • onfulfilled和onReject回调

根据ecma的规范,我们可以看看每一个步骤中大概都发生了什么。

Promise(excutor)构造函数
  • excutor必须是个函数对象,含有两个函数参数resolvereject,用来表示异步操作成功或者失败的情况下,执行的操作。
    当这个构造函数调用的时候,有几个关键的步骤:
  • 创建一个Promise实例p
  • 把p的[[PromiseState]]状态设为pending
  • Promise resolve方法执行后,会把[[PromiseState]]状态设为fulfilled
    ecma262/#sec-promise-resolve-functions
  • 同理如果执行的是Promise reject方法,会把[[PromiseState]]状态设为reject
  • 执行Promise reject还是resolve会决定,then之后是执行fulfilled回调还是reject回调。
  • 最后返回这个Promise实例p
    ecma262/#sec-promise-executor

我们可以看出,Promise的状态是在excutor当中就完成pendingsettle的过程。

Promise.prototype.then
  • 首先,then中的this对象是当前调用它的promise实例
  • 会根据这个实例的具体状态(是fulfilled还是reject)来判断,将fulfilled回调还是reject回调任务加到微任务队列当中,等待事件队列中的任务执行完才执行。
  • 最后then返回一个新promise或者undefined

ecma262/#sec-performpromisethen

Promise.resolve(value)和Promise.reject(reason)可以理解为对excutor的封装
  • Promise.resolve(value)

返回一个状态由给定value决定的Promise对象。如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。

MDN中看到一个例子:

var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
  console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));

/*
*  打印顺序如下,这里有一个同步异步先后执行的区别
*  original === cast ? true
*  value: 33
*/

稍微有点疑惑为什么original和cast两个对象能够绝对相等?
查看了一下PromiseResolve源码规范

PromiseResolve.png

从上述代码中我们可以看到,如果resolve的是promise对象,那么会直接返回这个对象x,所以original和cast其实就是同一个对象。
所以我们可以看到,虽然Promise.resolve(33)都是33,但是因为33不是promise,所以resolve会返回一个新的promise对象,originalcast不相等。

var original = Promise.resolve(33);
var cast = Promise.resolve(33);
console.log('original === cast ? ' + (original === cast));//输出false
  • Promise.reject(reason)

返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法

  • promise.all(iterable)
    用于处理多个promise事件,传入一个promise数组,最终会返回一个新的promise,resolve value返回数据数组,顺序跟promise数组里面的顺序一样,如果是reject状态,那么返回第一个报错的error信息。所以如果不报错,all会等所有promise执行完毕再返回结果

  • promise.race(iterable)
    race在英文中表示竞赛的意思,所以我们可以理解为promise.race(iterable)就是传入的promise数组,会按照执行速度的快慢来返回结果(无论是resolve还是有异常发生)。
    MDN-Promise/race

Promise链式调用

因为Promise构造函数和then、catch最终都是返回一个新的Promise对象,所以Promise可以用链式调用来解决回调地狱的问题。

new Promise((resolve, reject) => {
    console.log('初始化');

    resolve();
})
.then(() => {
    throw new Error('有哪里不对了');
        
    console.log('执行「这个」”');
})
.catch(() => {
    console.log('执行「那个」');
})
.then(() => {
    console.log('执行「这个」,无论前面发生了什么');
});
//输出结果
初始化
执行“那个”
执行“这个”,无论前面发生了什么

MDN Promise
令人费解的 async/await 执行顺序

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容