106-《前后端交互》-异步编程

169 阅读9分钟

目录

  • 异步编程

  • 同步和异步的概念

  • 回调函数

  • ajax

  • 回调地狱

  • Promise

  • async&await

  • 前提

    • 异步编程:利用异步代码完成相关业务的编程过程,就叫异步编程。即怎么写、什么时候写异步代码。比如ajax完成数据交互业务、动画业务(定时器)

一、同步和异步

  • 前提

    • 对于js代码,大多数代码是从上往下依次执行,但有的代码执行顺序跟书写顺序不一致???、
    • 用户体验:有的页面必须要加载完才能访问,有的页面仅仅加载了首屏的部分内容。
    • 学习异步编程能够更加深入理解js代码的执行过程以及书写用户体验更好的页面

1.1 同步和异步的概念

  • 在生活中,去下馆子,有的店家需要用户排队点餐取餐,是按照排队的顺序依次让顾客点餐。所以我们可以称需要按照顺序依次执行的情况都可以叫同步

    img

  • 异步是指,同一时间可以处理多个事情。比如去吃肯打鸡,扫码点餐之后,就去隔壁茶百道买了杯奶茶,回来的时候就直接取餐,无需排队。所以生活的异步是指同一时间可以执行多件事情,事情之间没有强制的先后之分。

    img

  • 程序中的同步和异步

    • 同步:是指代码需要

      按照书写顺序依次执行

      的情况,这种情况就叫同步,而同步情况里的代码也可以称为同步代码。

      • 异步:是指执行顺序跟书写顺序不一致的情况,这种就称为异步情况,异步中的代码就可以成为异步代码。比如定时器、jquery动画(底层用的定时器)、ajax
  • 为什么要有同步和异步代码,却不是纯同步或纯异步

1.2 事件循环

  • 目的:同步和异步代码的实际的执行顺序是什么样子的

  • 执行栈

    • 是js具体执行代码的地方,是js引擎的底层代码。js执行栈规定了代码是如何执行。

    • 大概执行过程

      • 先经过JavaScript的预编译过程将变量以及函数进行声明提升,然后将所有的代码推送到执行栈执行,那么在执行同步代码时,直接按照同步的代码书写顺序依次执行。当遇到异步代码时,会将异步代码推送到任务队列末尾进行排队(本质上是一个数组,由js引擎维护,里面包含了待执行的所有异步代码,先进先出)。当执行栈执行完所有同步代码后,那么就会进入事件循环阶段
    • 事件循环(执行栈执行代码的一部分过程:可重复触发)

      • 当执行栈执行完所有同步代码后,就会进入该事件循环。具体是执行栈会从任务队列中抽取第一个异步代码并放入到执行栈中执行。当执行完后又会继续抽取任务队列中的第一个放入执行栈中执行,像这种执行栈不断抽取队列第一个并执行的过程就是事件循环。
    • 最后阶段(等待阶段)

      • 当同步和任务队列中的代码就执行完之后处于等待状态,当用户触发交互动作时,就会继续执行事件处理程序里的代码(依然一样的处理规则)

1.3 宏任务和微任务

二、回调函数

  • 概念:回调函数指一个特殊的函数A,当函数A作为其他函数的参数使用,那么函数A就可以称为回调函数。一般是指当一个业务代码执行后需要执行另外的代码,而另外的代码我们就可以放在回调函数里面。

  • 分类

    • 根据业务代码是同步和异步的不同,回调函数就分为两类

      • 同步回调:同步情况下的回调函数

        • filter、map
      • 异步回调:异步情况下的回调函数

2.1 同步回调

  • 概念:同步情况下的回调函数,就将该回调函数称为同步回调函数

  • 作用:完成业务处理。比如数组数据的筛选(怎么筛选)、map(完成新数据的生成)、find(筛选的条件)

  • 例子

    let arr =[1,2,3,4,5];
    //筛选> 3 
    arr.filter(num=>num >3);
    
  • 自定义同步回调

    function A(callback){
        console.log('A函数的业务代码处理....此处省略100行代码');
        callback();
    }
    A(function(){
        console.log('另外的业务代码')
    })
    

2.2 异步回调

  • 概念:异步代码中的回调函数

  • 作用:起到通知的作用。比如定时器

  • 例子:定时器

    setTimeout(() => {
        console.log(6);
    }, 1000);
    
  • 例子:取快递

    function 送快递(callback){
        //送快递中
        if(快递到达){
            //通知
            callback();
        }
    }
    
  • 例子:服务器返回数据

    • 浏览器发送数据到服务器以及服务器返回响应这个过程是异步的(受网络影响),所以浏览器会有异步回调,会在接受数据后才执行这个异步回调。

三、Ajax

  • 目的:就是使用JavaScript完成跟服务器的数据交互

3.1 ajax简介

  • 概念:ajax(异步JavaScript和xml),是一种基于xmlHttprequest的集成技术,主要用于提供了一系列api实现跟服务器的数据交互,并可以使用dom技术将数据渲染在页面上

  • 目的:能够在不刷新页面情况下,利用ajax实现JavaScript跟服务器的数据交互(发送请求和接受响应)

  • 特点

    • 使用了JavaScript内置的对象xmlHttpRequest以及其提供的api实现了请求的发送和接受响应

3.2 jQuery ajax

  • 概念:原生的ajax api内容多,且实现步骤比较繁琐,jquery基于原生ajax封装成了几个快捷的api,能够让开发者更快捷 使用ajax技术,完成跟服务器的数据交互。

  • 相关api

    • $.ajax:可以用于发送任何请求类型的通用api
    • $.get():可以用于发送get请求的api
    • $.post():可以用于发送post的api
  • $.ajax

    $.ajax({
        url:"服务器的访问地址",
        type:"请求类型",//默认是get,如果是get可以不写
        data:{},//要发送的数据
        success:function(res){
            console.log('当接受到响应之后就会执行该异步回调,并且响应会放在res中')
            //利用dom技术将res渲染在页面上
        }
    })
    
  • $.get:针对get请求更快捷的api

    $.get(url,function(res){
        console.log("res就是返回的响应")
    })
    
  • $.post()::针对post请求的快捷api

    $.post(url,data,function(res){
        console.log("res就是返回的响应")
    })
    
    • url为请求地址,data就是要发送给服务器的数据

3.3 原生 ajax

  • 概念:JavaScript内置了xmlHttprequest对象用于完成js和服务器之间的数据交互。也是原生的ajax实现方案。
  • xmlHTTprequest本身是JavaScript内置的一个数据结构,其中包含了一系列的api以及事件来实现JavaScript和服务器之间进行数据交互
  • xmlHttpRequest对象常见api、属性和事件
名称说明
open()打开一个新的请求,需要声明请求的url和请求类型
send()用于发送通过open生成的请求,可以接受一个参数作为发送给服务器的数据,该数据会利用请求体进行发送,即适用于非get请求
readystate属性用于表示该请求的实时状态,4表示请求已经处理完毕,每次状态改变会触发该请求的onreadystatechange事件,可以配合来实现用于获取服务器返回的响应。默认为1,表示新建请求对象,还没发送,为2表示已经发送请求,但服务器还没返回响应,为3表示正在接受响应,为4表示服务器数据已经接受完毕
onreadystatechange事件当readystate改变时触发,可以在触发时判断readystate是否为4,为4就意味着响应数据接受完毕,可以获取数据进行处理
response属性用于保存服务器返回的数据
  • 例子:原生ajax体验

    function ajax({ url, type, data,success }) {
        //新建一个请求对象,一个对象对应一个请求
        let xml = new XMLHttpRequest();
        //1. 打开请求,设置url和请求类型
        // xml.open(type,"http://localhost:3000/movies/find");
        xml.open(type, url);
        //2. 设置onReadystatechange事件,监听readystate的变化
        xml.onreadystatechange = function () {
            // console.log(xml.readyState);
            if (xml.readyState == 4) {
                // console.log(JSON.parse(xml.response));
                success(JSON.parse(xml.response));
            }
        };
        //3. 发送请求
        xml.send(data);
    }
    ajax({
        url: "http://localhost:3000/movies/find",
        type: "get",
        data: null,
        success:function(res){
            console.log(res);
        }
    });
    

3.4 项目中使用ajax

3.4.1使用流程

  • 1.确保apifox上的接口测试成功,(保证后端服务器是正常运行状态)
  • 2.在需要发送请求页面引入jquery,书写$.ajax函数
  • 3.在浏览器中以服务器的地址访问页面,执行ajax请求
  • 4.在第二步ajax的success函数中获取服务器返回的数据并利用dom进行展示或根据响应实现跳转。

3.4.2例子:用户登录

  • 点击登录按钮进行发送请求

    $("#loginBtn").click(function(){
        //发送请求
        let username = $("#username").val();
        let userpass = $("#password").val();
        //发送请求
        // $.ajax({
        //     url:"http://localhost:3000/users/login",
        //     type:'post',
        //     data:{
        //         username,
        //         userpass
        //     },
        //     success:function(res){
        //         console.log(res);
        //         if(res.code==200){
        //             //相对地址
        //             location.href="index.html"
        //         }else{
        //             //用户反馈
        //             alert(res.message);
        //         }
        //     }
        // })
        //$.post 
        $.post("http://localhost:3000/users/login",{username,userpass},function(res){
            if(res.code==200){
                //相对地址
                location.href="index.html"
            }else{
                //用户反馈
                alert(res.message);
            }
        })
    })
    

四、Promise

  • 前提:为了解决实现多个异步操作顺序执行的技术方案(ES6)

4.1 Promise简介

  • 概念:是ES6的一个新特性,是作为编写异步代码的新一代技术方案,可以更好的编写异步代码

  • 作用

    • 相比传统的异步方案(比如回调地狱),Promise能够更优雅编写异步代码
    • 能够以链式调用的方式实现跟回调地狱一样的需求(多个异步操作顺序执行),比回调地狱更好理解和维护。
  • 特点

    • Promise本身是JavaScriptES6新增的一种数据结构-新的对象。里面提供了一系列api来更好的处理异步代码。
  • 常用Promise对象的api

名称说明
then当异步操作结束后并状态为成功时会执行的异步回调
catch当异步操作结束后并状态为失败时会执行的异步回调
new Promise(回调函数)新建Promise对象时传入的回调函数用于包裹Promise要管理的异步代码,其回调函数内部提供了resolvereject两个内置形参,都是函数,开发者主动调用,用于记录该回调函数内部的异步代码的状态,resolve会表示该异步操作状态为成功,reject就是指失败

4.2 Promise对象的基本使用

  • 使用流程

    • 1.新建一个Promise对象,并传入一个回调函数,函数中带有两个形式参数resolvereject
    • 2.将要执行的异步代码放入第一步的回调函数中,并主动调用resolvereject来记录该异步代码的结果(成功或者失败)
    • 3.根据异步操作的状态,编写then和catch api,作为该异步操作成功和失败要调用的异步回调。
  • 语法

    1. 新建Promise对象let p1 = new Promise(function(resolve,reject){    //2. 要执行的异步代码})
    
  • 例子:使用定时器来作为示例代码

    let p1 = new Promise(function(resolve,reject){
        setTimeout(function(){
            let random = Math.random();
            if(random >0.5 ){
                //模拟认为异步操作成功
                resolve();//表示该次异步操作是成功的状态并保存
            }else{
                //失败
                reject();
            }
        })
    })
    //手动编写then和catch作为异步操作结束时调用的异步回调
    p1.then(function(){
        //状态为成功时执行的函数
        console.log('异步操作成功,可以执行其他业务代码,比如ajax渲染数据')
    })
    p1.catch(function(){
        console.log('异步操作失败,')
    })
    
  • 例子2:Promise作用于ajax

    //promise包裹
    let p1 = new Promise(function (resolve, reject) {
        //异步操作
        $.ajax({
            url: "https://www.fastmock.site/mock/bb4157f45a0b5ffdcb3f6d984517a6c0/woniuMovie/getAllMovies",
            //拿到数据后自动触发
            success: function (res) {
                //暂存数据,调用then的时候可以获取
                resolve(res);
            },
        });
    });
    function demo(){
        p1.then(function(data){
            console.log('拿到数据',data);
        })
    }
    

4.3 Promise对象的状态

  • 每个Promise对象都有三个状态,初始状态都是pending(默认状态-待处理),其他有resolve以及reject状态
名称说明
pending(待处理)默认状态,表示异步操作还没结束。可以转为成功或失败,并只能转一次,转了之后状态就不会再改变。
fulfilled:成功只能由penging状态转变而来。表示Promise对象包裹的异步操作状态为成功,当开发者编写then代码之后,就可以执行then里 的函数。
reject(失败)只能由penging状态转变而来。表示Promise对象包裹的异步操作状态为失败,当开发者编写catch代码之后,就可以执行catch里 的函数。

4.4 Promise链式调用

  • promise中的链式调用是指then里的回调函数可以返回一个新的Promise对象并被then接受同时也会作为then函数本身的返回值。可以利用该特性实现多个Promise对象的链式调用

    Promise对象.then(返回下一个Promise对象).then(下一个Promise对象).then(...)
    
  • 例子:以多个div依次淡入进行举例

    let p1  = getPromise(1);
    p1.then(()=>getPromise(2))
        .then(()=>getPromise(3))
        .then(()=>getPromise(4))
        .then(()=>getPromise(5))
        .then(()=>getPromise(6))
        .then(()=>getPromise(7));
    function getPromise(num) {
        return new Promise(function (resolve, reject) {
            $(`div:nth-child(${num})`).fadeIn(1000, resolve);
        });
    }
    
  • 问题:

    • Promise的链式调用相较于回调地狱,书写格式更加友好和阅读,但本身链式调用是一句代码,并没有解决依次执行的异步操作数量不定的问题。最终方案-async&await

4.5 Promise其他api

  • 概念:promise对象提供了两个api用于监听多个异步操作的状态,方便进行后续的业务。

  • Promise.allSettled

    • 概念:allSettled需求一个由Promise对象构成的数组作为参数,目的是获取数组中所有Promise对象异步操作的结束状态,并将所有的结束状态形成一个新数组返回给调用者

    • 目的:能够获取多个异步操作的结束状态,即成功还是失败,可以观察到每个异步操作的状态。

    • 语法

      let all = Promise.allSettled([Promise对象数组]);//新Promise对象
      all.then(function(data){
          console.log(all)
      })
      
      • 返回的是一个新的Promise对象,并拿到了一个数组作为结果,包含了每个异步操作的状态。
  • Promise.all

    • 概念:需要一个Promise对象数组作为参数,用于监听所有异步操作是否都成功执行,但是只要有一个失败,那么该api也会认为失败

    • 目的:主要用于保持异步操作状态的一致性判断。是否都成功

    • 语法

      let all = Promise.all([Promise对象数组]);//新Promise对象
      all.then(function(data){
          console.log(all)
      })
      
      • Promise.all也是返回一个新Promise对象,如果该Promise对象状态为成功,那么意味着数组里面的所有异步操作都是成功的,或者返回第一个失败的Promise对象的状态。
    • 应用

      • 适合于支付类的强数据一致性的业务需求:因为银行汇款、商家接受款项、库存数量要改变、需要清理购物车之类的ajax操作都需要成功,才能认为是一次成功的支付操作。只要其中一个出了问题都可以认为该次支付是失败的。
  • 代码示例

    let p1 = new Promise(function (resolve, reject) {
            setTimeout(() => {
              let random = Math.random();
              if (random > 0.5) {
                resolve("p1成功");
              } else {
                reject("p1失败");
              }
            }, 1000);
          });
          let p2 = new Promise(function (resolve, reject) {
            setTimeout(() => {
              let random = Math.random();
              if (random > 0.5) {
                resolve("p2成功");
              } else {
                reject("p2失败");
              }
            }, 1000);
          });
          //Promise.all
        //   let all = Promise.all([p1,p2]); //新Promise对象
        //   all.then(function (data) {
        //     console.log(data);
        //   });
        async function demo(){
            let all =await Promise.all([p1,p2]);
            console.log(all);
        }
        demo();
          //Promise.allSettled
          //   let res =   Promise.allSettled([p1,p2]);
          //   res.then(function(data){
          //     console.log(data);
          //   })
    

五、async&await

  • 前提:这个是异步代码编写的最终技术方案(特别是浏览器和服务器交互部分)

5.1 async

  • 概念:async是ES7的函数修饰符,作用于函数(包含箭头函数和普通function函数)。可以将函数变为异步函数。

  • 作用:将某个被async修改的函数变成异步函数

  • 特点

    • 异步函数中仍可以包含所有代码(异步和同步)
    • 异步函数的返回值是一个Promise对象,并且状态就是成功以及将return后的数据作为Promise对象 成功状态 的数据。
    • 内部可以使用额外的await关键字
    • 异步函数的使用跟普通函数一样,需要手动调用
  • 语法

    async function 函数名(){
    //... 代码return 数据}
    函数名();
    
    • return后的数据会作为Promise对象调用then时传递的数据。

5.2 await

  • 概念:await是ES7新增的关键字。只能在异步函数中使用。

  • 作用:可以实现等待Promise对象异步操作结束并拿到Promise对象成功状态下的数据,即拿到resolve()里保存的数据

  • 语法

    async function demo(){
       let res =  await Promise对象
       console.log('await后Promise对象异步操作执行完才会输出这句话')
    }
    
    • 代码中的res会在await等待Promise对象中的异步操作结束并拿到结束后的数据(即传给resolve或reject里的数据),可以实现利用多个await实现多个异步操作顺序执行。
    • 只能等待await关键字后跟着的Promise对象异步代码执行结束,才会去执行await下一行的代码,仅限于异步函数里的代码
    • 异步函数在执行第一个await之前就会返回。之后就把该函数中await以及之后未执行的代码放在异步函数返回的Promise对象中去执行。简单来讲,异步函数会在await执行时就已经返回了。

5.3 项目中的运用

  • 例子:多个div依次淡入

    async function demo(){
        // let res = await getPromise(1);
        // console.log('第二个div淡入',res);
        //借助await实现多个异步操作顺序执行,书写格式类似于同步代码
        // await getPromise(1);
        // await getPromise(2);
        // await getPromise(3);
        // await getPromise(4);
        // await getPromise(5);
        //for 优化
        for(let i =1;i<=5;i++){
            await getPromise(i);
        }
    }
    demo();
    function getPromise(num) {
        return new Promise(function (resolve, reject) {
            $(`div:nth-child(${num})`).fadeIn(1000, function(){
                resolve('321')
            });
        });
    }
    
  • 例子:ajax配合async使用

    • 大概思路:需要先新建一个函数,返回一个Promise对象,Promise对象就包裹ajax代码,在使用时就await后跟要执行的ajax函数

    • 代码

      axios.js:
      function axios ({url,type = "get",data={}}){
          //option就是调用请求需要的参数,使用了函数的默认参数,type和data带有默认值,也就是说为默认值可以不传这两个参数
          return new Promise(function(resolve,reject){
              $.ajax({
                  url,type,data,
                  success:function(res){
                      resolve(res);//用await接受
                  }
              })
          })
      }
      
      • 需要引入到index.html中
    • 分页查询

      async function paginDataAwait(pageNum) {
        let res = await axios({
          url: "http://localhost:3000/movies/findByPage",
          data: {
            pageNum,
            pageSize: 3,
          }
        });
        //拿到res之后就可以使用
        renderMovies(res.data);
        totalPage = Math.ceil(res.total / curMoviePageSize);
        $("#curPageNum").text(curMoviePage);
        $("#totalPage").text(totalPage);
      }
      

六、请求封装

6.1 概念

  • 是将请求相关代码进行统一管理的方式。能够更加方便管理请求代码,避免过于分散,统一规范请求代码编写。

  • 一般项目的所有请求代码会根据数据不同进行模块的划分,一般一个模块是一个js文件。该js文件就保存了该模块的所有请求代码。

  • 例子:以电影后台为例

    • http/

      • 电影模块:movieHttp.js
      • 演员模块:actorHttp.js
      • 电影类型:movieTypeHttp.js
      • 导演:directorHttp.js
      • 封装的请求函数:axios.js

6.2 具体实现

  • 大概流程

    • 在项目的根目录下新建http文件夹
    • 在http文件夹下新建ajax的Promise封装函数:axios.js
    • 根据页面所需数据新建对应数据的请求js文件:比如movieHttp.js
    • 页面上引入请求js文件并使用
  • 详细流程

    • 1.新建http文件夹

    • 2.新建axios.js

    • 3.根据页面所需处理数据书写对应的请求js文件以及代码

      //包含处理电影数据的所有请求代码
      //开发环境
      let baseURL = "http://localhost:3000/movies";
      //部署环境
      // let baseURL= "124.24.50.74:3006"
      //书写请求函数并作为对象的属性
      function findByPage(data){
          return axios({
              url:baseURL+"/findByPage",
              type:'post',
              data
          })
      }
      
    • 4.页面上使用

      async function paginDataAwait(pageNum) {
        let res = await movieHttp.findByPage({
          pageNum,
          pageSize: 3,
        });
        //拿到res之后就可以使用
        //...
      }
      

补充

关于JSON的两个api

  • JSON.stringify(对象):是将某个对象或数组转为字符串(JSON格式)

    let str = JSON.stringify(对象或数组);
    console.log(str);
    
  • JSON.parse:将json格式的字符串转为原先的数据格式(对象或数组)

    let obj = JSON.parse(字符串);
    console.log(obj);//对象或数组