Javascript 同步/异步请求发展史

今天来整理下之前在公司分享过的关于Javascript 同步/异步请求、响应的4个发展阶段;

Javascript 同步/异步请求发展史

如上图所示,主要分为4个阶段:

阶段一:同步时代->同步请求响应阶段,用户和网页交互性差、体验不好,页面执行一个耗时的网络操作就会卡住,用户必须等待操作结束才能继续和网页进行交互。如下代码所示:


    
    
    
    
    

这段代码的执行效果如下:

Javascript 同步/异步请求发展史

界面上用户可以交互的内容包括了输入框和按钮,Generate primes这个按钮会触发一个耗时的计算操作,一旦点击后,就会进入计算,而这时候会发现界面的其他按钮和输入框无法进行交互了,页面瞬间卡住了,用户体验差。

阶段二:AJAX时代 -> 异步请求响应阶段,可以做到局部刷新,用户不必等待请求返回,可以继续与页面上的其他组件进行交互。如下代码所示:






这段代码的执行效果如下:

Javascript 同步/异步请求发展史

点击Click to start request按钮后,就会发出请求,而在发出请求后,用户可以继续在右边的输入框内输入操作,不必等待请求执行完成,体验良好;
在等待一段时间后,请求返回响应如下:

Javascript 同步/异步请求发展史

这种异步的效果会让用户感觉界面比较流畅,没有卡顿;

阶段三:Promise时代 -> 更加优雅的异步编程方式;
上面阶段二中用AJAX技术解决了用户与网页交互卡顿、不流畅等体验差的问题,但是后来使用的过程中发现,因为AJAX技术的回调机制会产生callback hell问题(回调函数的套壳),进而导致代码可读性低,不易维护等诸多问题,如下有一段callback hell的代码:

 function xmlOpe2(){
        const xhr = new XMLHttpRequest();
        xhr.addEventListener('loadend', () => {
            log.textContent = `${log.textContent}Finished with status: ${xhr.status}`;
        });
        xhr.open('GET', 'https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json');
        xhr.send();
    }
    function xmlOpe1(){
        const xhr = new XMLHttpRequest();
        xhr.addEventListener('loadend', () => {
            xmlOpe2();
            log.textContent = `${log.textContent}Finished with status: ${xhr.status}`;
        });
        xhr.open('GET', 'https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json');
        xhr.send();
    }
    //AJAX: Asynchronized JAVAScript and XML
    function xmlOpe(){
        const xhr = new XMLHttpRequest();
        xhr.addEventListener('loadend', () => {
            xmlOpe1();
            log.textContent = `${log.textContent}Finished with status: ${xhr.status}`;
        });
        xhr.open('GET', 'https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json');
        xhr.send();
    }
    document.querySelector('#xhr').addEventListener('click', () => {
        log.textContent = '';
        xmlOpe();
        log.textContent = `${log.textContent}Started XHR request
`;
    });
    document.querySelector('#reload').addEventListener('click', () => {
        log.textContent = '';
        document.location.reload();
    });

这段代码,就是在一段套壳代码的实现,xmlOpe->xmlOpe1->xmlOpe2... ,代码看上去比较复杂,如果一旦网页中出现较多的请求的异步加载操作,代码的结构将会极其复杂;所以后来有了Promise这种相对优雅的改进方式:
单个请求:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
console.log(fetchPromise);
fetchPromise.then( response => {
    console.log(`Received response: ${response.status}`);
});
console.log("Started request...");

请求的链式调用:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
console.log(fetchPromise);
fetchPromise
.then( response => {
    // throw new Error(`HTTP error: ${response.status}`);
    console.log(`Received response: ${response.status}`);
    return response.json();
})
.then( json => {
    // throw new Error(`json error parsed`);
    console.log(json[0].name);
})
.catch( error => {
    console.error(`Could not get products: ${error}`);
});
console.log("Started request...");

多个请求合并:

const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('invalide-schema://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise3 = fetch('invalide-schema://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then( responses => {
for (const response of responses) {
      console.log(`${response.url}: ${response.status}`);
}
})
.catch( error => {
console.error(`Failed to fetch: ${error}`)
});

会发现相比之前AJAX的代码,看起来要舒服很多,易于理解。

阶段四:Async/Await时代 -> 更加人性化的、符合直觉的异步编程方式,化异步为同步写法;
经过阶段三的演化,会发现Javascript中的异步操作其实已经变得优雅、可维护,但是这不是尽头,出现了一种更加符合直觉的写法,为什么说符合直觉呢?程序员编程往往大多数都会习惯代码一行行执行,而异步执行的代码往往是违反这种直觉的,就好比下面这份代码:

1: const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
2: func1(fetchPromise1);
3: func2();
4: console.log("end");

这份代码,第一感觉看起来,就是一句句执行,可是结果不是如此,在执行完第1fetch操作后,fetch的操作是一个网络请求,这个网络请求其实并没有完成,如果这时候当你把参数fetchPromise1传入第2句func1中时,这是好的fetchPromise1是一个半完成状态的返回值,而不是最终结果,所以这就是为什么fetch操作一般都会配上then方法去处理请求结果,如下所示:

fetchPromise
.then( response => {
    console.log(`Received response: ${response.status}`);
    return response.json();
})

这里的then方法其实就是对请求完成后一个完整结果的处理,那么为了避免这种异步编程方式给程序员带来的困扰以及debug时候的疑惑等问题,就产生了下面的编程方式:

async function fetchProducts() {
  try {
    // after this line, our function will wait for the `fetch()` call to be settled
    // the `fetch()` call will either return a Response or throw an error
    const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    // after this line, our function will wait for the `response.json()` call to be settled
    // the `response.json()` call will either return the JSON object or throw an error
    const json = await response.json();
    console.log(json[0].name);
  }
  catch(error) {
    console.error(`Could not get products: ${error}`);
  }
}
fetchProducts();
console.log("request start");

async/await这种编程模式就是化异步编程方式为同步编程方式,请求还是异步执行,而只是利于开发者理解,给开发者一种仿佛在写同步代码的感觉,实质上JS还是异步执行,对于程序员开发和实际程序运行可谓双赢。
async/await这对关键字是成对出现的,这也是Javascript对底层做了处理,以语法糖技术包装了一层,开发者才能有此收益。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章