面试题:手写ajax

7,396 阅读3分钟

前言

有时候会刷一些面试题

今天刷到一题,让手写ajax:

image.png

我当时的第一反应是:老子不会😂

...

回想一下,为什么会有这个反应?主要有两点

  1. 工作中都是直接使用jqueryaxios或者fetch等等

  2. 像这种默写API的题,觉得没什么意义

就像手写ajax,只要记清楚原生xhr的几个判断,send/open/abortreadyState就可以了

类似的题其实很多很多,很多面试都会考察API的使用,但我就是不喜欢

老问这样的问题,我就不相信,出这些题的公司的工程师都能把所有的原生 API 默写出来,而不上网查

非常赞同尤大之前说过一个观点:

技术面试如果足够有诚意就让人拿出笔记本当场写,随便查,你才能看到面试者在真实的写代码的时候是什么状态,什么思路。就看面试者去哪里查,怎么查,你就能得到比让人默写 API 要有价值得多的信息

个人也比较喜欢这种方式

...

但是到这,发现还不够500

那今天就来细说ajax吧,就当重新复习复习,当以后如果真被面,而且还记不太清楚了,那就直接翻开此文!

文中作者给了答案,Promise版本的,如下:

const getJSON = function (url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url, false);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    };
    xhr.send();
  });
};

那我们来实现一个更详细的、更原始的版本,一次性彻底搞清楚AJAX

手写实现

AJAXAsynchronous JavaScript And XML的简称,它允许我们在不刷新整个页面的情况下,就可以异步获取数据,并更新页面的部分内容。具体的实现步骤如下:

创建一个异步对象

var xmlhttp; 
if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari 
  xmlhttp=new XMLHttpRequest(); 
}else {// code for IE6, IE5 
  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); 
}

通过XMLHttpRequest构造函数创建一个异步对象xmlhttp, IE6, IE5 使用ActiveXObject创建,创建的这个异步对象上有很多属性和方法,常用的有:

  1. onreadystatechange

监听异步对象请求状态码readyState的改变,每当readyState改变时,就会触发onreadystatechange事件

  1. readyState:请求状态码

readyState表示异步对象目前的状态,状态码从0到4:

0: 表示请求未初始化,还没有调用 open()

1: 服务器连接已建立,但是还没有调用 send()

2: 请求已接收,正在处理中(通常现在可以从响应中获取内容头)

3: 请求处理中,通常响应中已有部分数据可用了,没有全部完成

4: 当readyState状态码为4时,表示请求已完成;此阶段确认全部数据都已经解析完毕,可以通过异步对象的属性获取对应数据

  1. status:http状态码

http状态码表示成功的http状态码有xmlhttp.status >= 200 && xmlhttp.status < 300 || xmlhttp.status == 304

  1. responseText:后台返回的字符串形式的响应数据

  2. responseXML:后台返回的XML形式的响应数据

设置请求方式和请求地址

创建异步对象之后,通过open()方法设置ajax请求方式和请求地址 格式:xmlhttp.open("GET/POST","ajax-get.txt",true);

第一个参数:请求的类型;GET 还是 POST

第二个参数:表示请求的文件的地址url

第三个参数:设置请求方法是不是异步asynctrue为异步, false为同步。AJAX存在的意义就是发异步请求,所以第三个参数永远传true

这里有个问题,就是IE中的缓存问题

IE浏览器中,如果通过Ajax发送GET请求,那么IE浏览器认为,同一个URL只有一个结果,如果地址没有发生变化,它就会把上一次返回的结果,直接返回。这样我们不能实时的拿到变化后的数据。如果要想我们拿到实时数据,必须保证每次的URL都是不一样的,有两种方式:

  1. Math.random()

  2. new Date().getTime()

即在请求地址后面拼接上?t=随机数或者1970.01.01至当前的毫秒数 所以在IE中通过ajax发送get请求时,可以设置请求地址为:

xmlhttp.open("GET","ajax-get.txt?t=" + (new Date().getTime()),true);
//或
xmlhttp.open("GET","ajax-get.txt?t=" + Math.random(),true);

发送请求

直接通过异步对象的send()发送请求

xmlhttp.send();

特别注意的是: 如果发送POST请求,请使用setRequestHeader()来添加 HTTP请求头,并在send()方法中传递要发送的数据:

xmlhttp.open("POST","ajax_test.html",true); 
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); 
xmlhttp.send("fname=Henry&lname=Ford");

通过onreadystatechange监听状态变化

每当异步对象的readyState发生改变,就会触发onreadystatechange函数,当readyState变成为4时,表示当前状态是请求完毕的状态,同时当http的响应码status200300之间(包括200300)或为304时,表示ajax请求成功;当http状态码不是200300之间的数也不是304时,表示请求不成功

//4.监听状态变化
xmlhttp.onreadystatechange = function(){
 // 判断当前状态改变是请求完毕的状态吗
 if (xmlhttp.readyState === 4) {
    if (xmlhttp.status >= 200 && xmlhttp.status < 300 || xmlhttp.status == 304) {
        console.log("成功的接收到服务器返回的数据");
    }else{
        console.log("不成功!");
    }   
 }
}  

处理返回的结果

如果成功,可通过异步对象的responseText属性来获取服务器返回的字符串

console.log(xmlhttp.responseText);

根据上述情况,如果每次请求都要手写以上五个步骤,那很麻烦,接下来,我们来封装一个方法ajax()用于发送请求

封装的时候,需要注意

  1. URL当中只能出现字母 数字 下划线和ASCII码,不能出现中文,可以使用encodeURIComponent()转码

  2. 当我们利用我们的ajax放的发送一个请求到远处服务器时,我们需要等待远程服务器去响应我们的请求,等待远程服务器将响应的结果返回给我们,但是这个响应的速度是不确定的,因为响应的速度是由本地网络和远程服务器的网速等共同决定的,所以我们不可能一直等待服务器的响应。那么这里我们需要新增另外一个功能:设置超时时间的功能,即告诉它我的请求会等待多长的时间,如果这么长的时间内都没有响应我们发送的请求,那我就在这里自动的终止这次请求;

function ajax(type, url, obj, timeout, success, error) {
    //  0.将对象转换成字符串
    var str = objToString(obj);
    //  1.创建一个异步对象xmlhttp;
    var xmlhttp, timer;
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest();
    } else {// code for IE6, IE5 
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    //  2.设置请求方式和请求地址; 
    // 判断请求的类型是POST还是GET
    if (type === 'GET') {
        xmlhttp.open(type, url + "?t=" + str, true);
        //  3.发送请求;
        xmlhttp.send();
    } else {
        //如果是post请求,需要设置请求头
        xmlhttp.open(type, url, true);
        // 注意:在post请求中,必须在open和send之间添加HTTP请求头:setRequestHeader(header,value);
        xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        //  3.发送请求;
        xmlhttp.send(str);
    }
    //  4.监听状态的变化;
    xmlhttp.onreadystatechange = function () {
        clearInterval(timer);
        if (xmlhttp.readyState === 4) {
            if (xmlhttp.status >= 200 && xmlhttp.status < 300 || xmlhttp.status == 304) {
                //  5.处理返回的结果;
                success(xmlhttp);//成功后回调;
            } else {
                error(xmlhttp);//失败后回调;
            }
        }
    }
    //处理obj 
    function objToString(obj) {
        obj.t = new Date().getTime();
        var res = [];
        for (var key in obj) {
            //需要将key和value转成非中文的形式,因为url不能有中文。使用encodeURIComponent();
            res.push(encodeURIComponent(key) + " = " + encodeURIComponent(obj[key]));
        }
        return res.join("&");
    }
    //判断外界是否传入了超时时间
    if (timeout) {
        timer = setInterval(function () {
            xmlhttp.abort();//中断请求
            clearInterval(timer);
        }, timeout);
    }
}

完善问题

在使用自己封装的代码的时候,感觉跟jQuery官方的ajax还是有一定的差异,所以还需要进一步完善

首先看第一个问题,传递多个参数,需要保持传递顺序。解决方案是可以改写成传递的是一个对象;因为对象里面的值,传递的是一个对象就不用考虑先后顺序,里面用的参数通过对象名.属性名的形式获取

第二,传递请求类型的区分大小写,jQuery官方的是大小写都可以;解决方案是可以使用toLowerCase()或者toUpperCase()将类型转成大写或小写再对比;

第三,我们传递的数据用的名字是objjQuery官方用的是data,语义更适合。所以将传递数据obj改成data

完善如下:

function ajax(option) {//type,url,obj,timeout,success,error将所有参数换成一个对象{}
    //  0.将对象转换成字符串
    var str = objToString(option.data);
    //  1.创建一个异步对象xmlhttp;
    var xmlhttp, timer;
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest();
    } else {// code for IE6, IE5 
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    //  2.设置请求方式和请求地址; 
    // 判断请求的类型是POST还是GET
    if (option.type.toLowerCase() === 'get') {
        xmlhttp.open(option.type, option.url + "?t=" + str, true);
        //  3.发送请求;
        xmlhttp.send();
    } else {
        xmlhttp.open(option.type, option.url, true);
        // 注意:在post请求中,必须在open和send之间添加HTTP请求头:setRequestHeader(header,value);
        xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        //  3.发送请求;
        xmlhttp.send(str);
    }
    //  4.监听状态的变化;
    xmlhttp.onreadystatechange = function () {
        clearInterval(timer);
        if (xmlhttp.readyState === 4) {
            if (xmlhttp.status >= 200 && xmlhttp.status < 300 || xmlhttp.status == 304) {
                //  5.处理返回的结果;
                option.success(xmlhttp);//成功后回调;
            } else {
                option.error(xmlhttp);//失败后回调;
            }
        }
    }
    //处理obj 
    function objToString(data) {
        data.t = new Date().getTime();
        var res = [];
        for (var key in data) {
            //需要将key和value转成非中文的形式,因为url不能有中文。使用encodeURIComponent();
            res.push(encodeURIComponent(key) + " = " + encodeURIComponent(data[key]));
        }
        return res.join("&");
    }
    //判断外界是否传入了超时时间
    if (option.timeout) {
        timer = setInterval(function () {
            xmlhttp.abort();//中断请求
            clearInterval(timer);
        }, timeout);
    }
}

以上就是ajax基本实现,到这面试应该问题不大了吧!实际工作中请用人家封装好的~

最后,希望:以后如果遇到需要默写API的,希望大家允许我上网,因为我记不住😂

好了,本次内容到这结束!

如果有问题或者你也有相同经历,欢迎留言分享~

END~

参考文献:www.w3school.com.cn/ajax/index.…