JavaScript 知识:this、apply/call/bind、Promise、async/await、HTTP 库 Axios

news2024/11/5 9:30:41

1、变量、声明、传递 (值、引用)

javascript:void(0) 含义

javascript:void(0) 中最关键的是 void 关键字, void 是 JavaScript 中非常重要的关键字,该操作符指定要计算一个表达式但是不返回值。void() 仅仅是代表不返回任何值但是括号内的表达式还是要运行,如:void(alert("Warnning!"))

href="#"与href="javascript:void(0)"的区别

  • #包含了一个位置信息,默认的锚是#top也就是网页的上端。在页面很长的时候会使用#来定位页面的具体位置,格式为:# + id。
  • 而 javascript:void(0), 仅仅表示一个死链接。

JavaScript 变量

JavaScript 变量均为 对象。当声明一个变量时,就创建了一个新的对象。 JavaScript  一切皆对象。typeof 操作符可以检测变量的类型。

  • 值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。
  • 引用数据类型:对象(Object)、数组(Array)、函数(Function)、Date 等有多个值构成的可变长度的复杂类型。
console.log(typeof "John")                // 返回 string
console.log(typeof 3.14)                  // 返回 number
console.log(typeof false)                 // 返回 boolean
console.log(typeof [1,2,3,4])             // 返回 object
console.log(typeof {name:'John', age:34}) // 返回 object

null 和 undefined 的值相等,但类型不等

  • null 是一个只有一个值的特殊类型。表示一个空对象引用。用 typeof 检测 null 返回是object。可以设置为 null 来清空对象。var person = null;  // 值为 null(空), 但类型为对象
  • undefined 是一个没有设置值的变量。typeof 一个没有值的变量会返回 undefined。任何变量都可以通过设置值为 undefined 来清空。 类型为 undefined
    var person;            // 值为 undefined(空), 类型是undefined
    person = undefined;    // 值为 undefined, 类型是undefined

undefined:是所有没有赋值变量的默认值,自动赋值。
null:主动释放一个变量引用的对象,表示一个变量不再指向任何对象地址。当使用完一个比较大的对象时,需要对其进行释放内存时,设置为 null。

声明 (创建):var、let、const

  • 使用 var 声明的全局作用域变量属于window 对象。
  • 使用 let  声明的全局作用域变量不属于 window 对象。
  • 使用 var 声明的变量在任何地方都可以修改。使用 var 可以重新声明已经存在的变量
  • 在相同的作用域或块级作用域中,不能使用 let 重新声明已经存在的变量,
  • let 声明的变量只在 let 命令所在的代码块内有效。
  • 在相同的作用域或块级作用域中,不能使用const关键字来重置var和let关键字声明的变量。
  • 在相同的作用域或块级作用域中,不能使用const关键字来重置const关键字声明的变量
  • const 关键字在不同作用域,或不同块级作用域中是可以重新声明赋值的:
  • var 关键字定义的变量可以先使用后声明。
  • let 关键字定义的变量需要先声明再使用。
  • const 用来声明一个只读的常量,声明时必须进行初始化,且初始化后不可再修改。
    使用const 声明的数组,不能重新赋值给 变量arr,但可以修改数组中的元素:
    const arr = []
    arr.push('one');
    arr.push('two');
    arr.push('three');
    console.log(arr)

在 JavaScript 中创建变量通常称为"声明"变量。使用 var 关键词来声明变量:var carname;
变量声明之后,该变量是空的(它没有值)。如需向变量赋值,使用等号:carname="Volvo";
也可以在声明变量时对其赋值:var carname="Volvo"; 示例:创建名为 carname 的变量,并向其赋值 "Volvo",然后把它放入 id="demo" 的 HTML 段落中:

var carname="Volvo";
document.getElementById("demo").innerHTML=carname;

let 变量 = ""; //当前代码块生效
const 常量 = "123456"; //被const创建出来的东西不可以被修改

var 和 let 区别。在JavaScript中,var 和 let 都用于声明变量,但它们之间存在几个关键的区别。

作用域

  • var 声明的变量 作用域是分情况的:
            如果在一个函数内部声明,则具有函数作用域,它仅在该函数整个内部可见。
            如果在函数外部声明,它就在全局作用域内可见。
  • let 声明的变量:
        具有块级作用域,即它们只在包含它们的代码块(例如:if语句、循环体)内有效。

提升 (Hoisting)

  • var声明的变量会被提升到它们所在作用域的顶部,也就是说在声明之前就可以访问这些变量,但是访问的结果是undefined
  • let声明的变量同样会在它们的作用域内被提升,但是不允许在声明之前访问它们(这会导致一个ReferenceError),这种行为被称为暂时性死区(Temporal Dead Zone,TDZ)。

重复声明

  • 在相同的作用域内,用var声明的变量可以被重新声明。
  • let声明的变量则不允许在相同的作用域内重复声明,尝试这样做会引发错误。

全局对象属性

  • 在全局作用域中,用var声明的变量会成为全局对象的属性(在浏览器中是window对象,在Node.js中是global对象)。
  • let声明的变量不会成为全局对象的属性。

考虑到var的一些特性(如变量提升和较宽松的作用域)可能引发意外的行为,现代JavaScript开发中优先使用let(和const,对于不需要重新赋值的变量)来声明变量,以促进更加可靠和可预测的代码编写风格。为什么建议使用let?

  • 减少错误let的块级作用域和暂时性死区特性有助于减少因变量提升或不小心的变量重复声明导致的错误。
  • 提高代码可读性let的块级作用域使得代码结构更加清晰,易于理解,特别是在循环和条件语句中。
  • 更安全的作用域管理:使用let可以避免在不应该访问某个变量的作用域中误访问到它,从而提高代码的健壮性和安全性。
  • 避免全局污染:由于let声明的变量不会成为全局对象的属性,这有助于避免不必要的全局命名空间污染。

一条语句,多个变量

可以在一条语句中声明很多变量。该语句以 var 开头,并使用逗号分隔变量即可:var lastname="Doe", age=30, job="carpenter";

声明也可横跨多行:

var lastname="Doe",
age=30,
job="carpenter";

一条语句中声明的多个变量不可以同时赋同一个值:

var x,y,z=1;  // x,y 为 undefined, z 为 1。

Value = undefined

在计算机程序中,经常会声明无值的变量。未使用值来声明的变量,其值实际上是 undefined。

在执行过以下语句后,变量 carname 的值将是 undefined:var carname;

 var 可以重新声明 变量

var 可以重新声明 JavaScript 变量,该变量的值不会丢失。

在以下两条语句执行后,变量 carname 的值依然是 "Volvo":

var carname="Volvo";
var carname;

变量提升

示例:

function fn(){
    console.log(name);  // name 先使用,后声明并初始化
    var name = '变量提升';
}
fn()
  • 在其他编程语言里,变量必须是先声明然后才能使用,不允许先使用后声明。
  • 但是在 js 里 "允许先使用后声明"。 因为在js执行的时候。它会首先检测你的代码,发现在代码中会有 name 使用,那么运行时就会变成如下的逻辑:
function fn(){
    var name;  // 直接把 name 的声明提到作用域的最前面,
    console.log(name);  // 这里使用name, 但是 name 只是声明,没有初始化,所以打印 undefined
    name = '变量提升';  // 这里进行初始化
}
fn()

这种把变量声明,提前到代码块作用域最前面运行的逻辑,被称为变量提升。

  • 在JavaScript中,变量提升(Hoisting)是指 "变量、函数" 的声明,在编译阶段被移动到它们所在作用域的顶部的行为。变量提升 只适用于通过var关键字声明的变量和函数声明,不适用于通过letconst声明的变量,以及函数表达式。JavaScript 严格模式 (strict mode) 不允许使用未声明的变量。
  • 变量提升 在其他语言里是绝对没有的,并且也不是什么好事情。在ES6中就明确了这样使用变量是不完善的,es6 提出使用 let 来声明变量,就不会出现该问题了。

JavaScript 只有声明的变量会提升,初始化的不会。示例:

function test_1() {
    /*只有声明的变量会提升*/
    var x1 = 10; // 初始化 x
    y1 = 20;
    console.log(x1 + "" + y1); // y 是先使用,后面声明
    var y1;          // 声明 变量 y
    console.log(x1 + "" + y1);
}

function test_2() {
    /*初始化的变量不会提升*/
    var x2 = 5; // 初始化 x
    console.log(x2 + "" + y2); // y 是先使用,后面声明和初始化
    var y2 = 7; // 初始化 y
}

test_1();
test_2();

函数声明的提升:

sayHello();

function sayHello() {
  console.log("Hello!");
}

例子中的函数调用可以正常工作,并打印出"Hello!",因为函数声明被提升到了作用域的顶部。

总结:理解变量提升对于编写可预测和易于维护的JavaScript代码至关重要。推荐的做法是总是在作用域的顶部声明变量,以避免由于变量提升导致的潜在错误。此外,鉴于letconst提供的额外安全性及其遵循块级作用域的行为,推荐使用这两者而非var进行变量声明。

值传递,引用传递,共享传递

  • 基本类型不可变类型,传递时是传递的值。传值的意思就是:传内存拷贝。值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。基本类型参数的传递与基本类型的复制一样,传递的是变量值。场景:JavaScript 中如果传递给函数的形参是不可变类型,则在函数中改变形参的值不会影响外部实参的值。示例:JS的基本类型,是按值传递的。
    var a = 1;
    function foo(x) {
        x = 2;
    }
    foo(a);
    console.log(a); // 仍为1, 未受x = 2赋值所影响
  • 对象类型可变类型,引用类型参数的传递与引用类型的复制一样,传递时是传递的内存地址。传引用的意思就是:传内存指针。引用数据类型:对象(Object)、数组(Array)、函数(Function)、Date 等有多个值构成的可变长度的复杂类型。场景:JavaScript 中如果传递给形参是对象类型,由于对象是可变(mutable),当修改形参对象的属性值,也会影响到实参的属性值。这意味着在函数内部对对象或数组的修改会反映到外部,因为它们都指向同一个内存地址。示例:
    var obj = {x : 1};
    function foo(o) {
        o.x = 3;
    }
    foo(obj);
    console.log(obj.x); // 3, 被修改了!

    说明 o 和 obj 是同一个对象,o 不是 obj 的副本。所以不是按值传递。 但这样是否说明 JS 的对象是按引用传递的呢?我们再看下面的例子:

    var obj = {x : 1};
    function foo(o) {
        o = 100;
    }
    foo(obj);
    console.log(obj.x); // 仍然是1, obj并未被修改为100.

    如果是按引用传递,修改形参o的值,应该影响到实参才对。但这里修改o的值并未影响obj。 因此JS中的对象并不是按引用传递。那么究竟对象的值在JS中如何传递的呢?

  • 共享传递:该求值策略被用于Python、Java、Ruby、JS等多种语言中。该策略中,基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。共享传递和引用传递的区别在于:在共享传递中对函数形参的赋值,不会影响实参的值。如下面例子中,不可以通过修改形参o的值,来修改obj的值。

    var obj = {x : 1};
    function foo(o) {
        o = 100;
    }
    foo(obj);
    console.log(obj.x); // 仍然是1, obj并未被修改为100.

"按值传递、按引用传递" 区别

  • 按值传递(call by value) 是最常用的求值策略:函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。按值传递由于每次都需要克隆副本,对一些复杂类型,性能较低。
  • 按引用传递(call by reference)时,函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。按引用传递会使函数调用的追踪更加困难,有时也会引起一些微妙的BUG。

2、this 关键字

this 关键字对象详解:http://blog.csdn.net/chenchunlin526/article/details/78889301

this 是面向对象语言中的一个重要概念,在 C++、JAVA,C#等大型语言中,this 指向运行时的当前对象。但是在 JavaScript中,由于 JavaScript 的动态性(解释执行,当然也有简单的预编译过程),this 的指向在运行时才确定。也就是 JavaScript 中 this 会随着执行环境的改变而改变。这个特性带来了编程上的自由和灵活,结合 apply、call、bind 方法,可以使 JS 变得异常强大。

this 的值通常是由所在函数的执行环境决定,也就是说要看函数是如何被调用的。同一个函数每一次调用,this 都可能指向不同的对象; 

  • 没有绑定到任何对象的 变量、函数、属性 等,都是 绑定到 全局对象
  • this 永远指向最后调用它的那个对象
  • 匿名函数的 this 永远指向 window
  • 使用 call() 或者 apply() 的函数会直接执行
  • bing() 是创建一个新的函数,需要手动调用才会执行
  • 如果 call、appy、bind 接收到的第一个参数是空或者 null,undefine 的话,则会忽略这个参数
  • forEach、map、filter 函数的第二个参数也是能显式绑定 this 的

谁调用或者哪个对象调用this所在的函数,this 就指向谁。this 永远指向 最后调用它 的 那个对象。但是 箭头函数this 是由 外层作用域决定的

  • 如果单独使用则 this 表示全局(Global)对象。在浏览器中 window 就是该全局对象
  • 在对象的方法中 this 表示该方法所属的对象
    var person = {
        firstName: "John",
        lastName : "Doe",
        id       : 5566,
        fullName : function() {
            console.log(this.firstName + " " + this.lastName;);
            return this;
        }
    };
    
    console.log(person.fullName());
  • 在函数中 this 表示全局对象。但是在严格模式下 this 是未定义的(undefined)
  • 在事件中,this 表示接收事件的元素。在 HTML 事件句柄中,this 指向了接收事件的 HTML 元素。示例:事件中的 this
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>菜鸟教程(runoob.com)</title>
    </head>
    <body>
    
    <h2>JavaScript <b>this</b> 关键字</h2>
    
    <button onclick="this.style.display='none'">点我后我就消失了</button>
    
    </body>
    </html>
  • 改变 this 指向 (apply、call、bind)。在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。示例:使用 person2 作为参数来调用 person1.fullName 方法时, this 将指向 person2, 即便它是 person1 的方法:
    var person1 = {
        fullName: function() {
            return this.firstName + " " + this.lastName;
        }
    }
    var person2 = {
        firstName:"John",
        lastName: "Doe",
    }
    person1.fullName.call(person2);  // 返回 "John Doe"

new 绑定

对象构造函数中的 this,使用 new 创建 实例this 会绑定到这个新实例对象。

<script type="text/javascript">
    var name = "chunlynn";
    //构造函数
    function Foo(){
        this.name = 'linda';
        this.foo = function(){
            console.log(this.name);
        }
    }
    var obj = new Foo(); // 实例化对象
    obj.foo(); // 输出 linda
</script>

构造函数 和 普通函数 的区别:

  • 构造的属性用 this.name="chunlynn",然后使用 new 创建 实例。
  • 普通函数用 var name = 'linda'。

普通函数中的 this

在普通函数直接调用 this 时,则指向 window对象

<script type="text/javascript">
    var name = "chunlynn";
    //普通函数
    function foo(){
        var name = 'linda';
        console.log(this.name);
    }
    foo(); //输出 chunlynn
</script>

下面再来看一个有些难度的:

<script type="text/javascript">
    var name = "chunlynn";
    //构造函数
    function Foo (){
        this.name = 'linda';
        this.foo = function(){
            var name = 'doudou';
            return function(){
                console.log(this.name);
            };
        }
    }
    
    var obj = new Foo();  // 实例化对象
    obj.foo()(); //输出 chunlynn

    // 拆开分步执行如下:
    // var temp = obj.foo();       // obj.foo() 执行完成后返回一个函数
    // temp(); ---> window.temp(); 函数没有显示的赋值给一个变量,所以window对象接收这个函数
    // 所以在 window 对象的方法中,this.name 指的就是 window.name,所以输出 chunlynn
</script>

onclick(this) 中的 this

对于一个onclick属性,因为它是所属的HTML元素所拥有,this 指向该HTML元素。

<a href="javascript:void(0);" title="a标签标题" onclick="test(this);">A标签测试</a>
<script type="text/javascript">
    function test(obj){
        var value = obj.title;
        console.log(value);  //这是一个标题!!
    }
</script>

onclick 为它所属的 <a> 元素所有,this 指向 <a> 元素。

定时器 setTimeout 中 this

定时器中的this指向window对象。因为定时器就是全局函数,由window调用

示例:

<script type="text/javascript">
    var name = "chunlynn";
    //普通函数
    function foo(){
        var name = 'linda';
        console.log(this.name);
    }
    setTimeout(foo, 5000); //output:chunlynn
    // this指向:谁调用或者哪个对象调用this所在的函数,this就指向谁。
    // 定时器为全局函数,由window调用,因此定时器中的this指向window对象。
</script> 

函数嵌套中  this

箭头函数内部的 this 由 箭头函数所在的上下文确定。

<script type="text/javascript">
    var name = "chunlynn";
    var obj = {
        name:'linda',
        fn: function(){
            console.log(this.name);
        },
        foo:function(){
            setTimeout(this.fn, 5000);
        }
    }
    obj.foo(); //output:chunlynn
    // this指向:谁调用或者哪个对象调用this所在的函数,this就指向谁。
    // 嵌套带入进去最终执行的就是
    // window.setTimeout(function (){console.log(this.name)}, 5000); this指向的就是window对象
</script>

换个思路,上面最终执行的代码为:

<script type="text/javascript">
    window.setTimeout(
        function (){
        // this指向的就是window对象,直接包含this.name的为setTimeout函数
        console.log(this.name);
        }
    , 5000);
</script>

这样答案一目了然了吧,this 绑定到了 window 对象。

箭头函数内部的 this 由 箭头函数所在的上下文确定。所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:

var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2015); // 25

改变 this 指向:apply、call、bind

使用 apply 、call 或 bind方法时都可以改变 this 指向。

使用 call() 或者 apply()  的函数是会直接执行的。

bind() 是创建一个新的函数,需要手动调用才会执行

<script type="text/javascript">
    var name = "chunlynn";
    var obj = {
        name:'linda',
        foo:function(){
            console.log(this.name);
        }
    }
    var f = {};
    f.name = "doudou";
    f.fun = obj.foo; //将obj对象的foo函数赋值给f对象的fun属性。本质为复制。
    f.fun(); //output:doudou
    f.fun.apply(obj); //output:linda
    f.fun.apply(window); //output:doudou
    f.fun.apply(); //output:chunlynn
    // this指向:谁调用或者哪个对象调用this所在的函数,this就指向谁。
</script> 

call 、bind 、 apply 这三个函数的参数:

  • 第一个参数:在函数内部,this 指向第一个参数,
  • 第二个参数差别就来了:
        call 的参数是直接按原函数传递,参数之间全都用逗号分隔
        apply 的所有参数都必须放在一个数组里面。 
        bind 除了返回是函数以外,它的参数和 call 一样。
var name = '小王', age = 17;
var obj = {
    name: '小张',
    objAge:this.age,
    myFun:function () {
        console.log(this.name + "年龄" + this.age);
    }
}
var db = {
    name: '德玛',
    age: 99
}

obj.myFun.call(db,'成都','上海');      // 德玛 年龄 99  来自 成都去往上海
obj.myFun.apply(db,['成都','上海']);      // 德玛 年龄 99  来自 成都去往上海
obj.myFun.bind(db,'成都','上海')();       // 德玛 年龄 99  来自 成都去往上海
obj.myFun.bind(db,['成都','上海'])();   // 德玛 年龄 99  来自 成都, 上海去往 undefined

​call 函数有(call的调用者表示函数的参数个数+1)个参数

  • 参数1:会被赋值给 call 的调用者,在函数内部 this 指向第一个参数
  • 参数 2,3,4 ... 

a.call(n, i) 等同于:a(i), 同时 a函数的内部 this 指向 n

// JavaScript 中一切皆对象,所以函数也是一个对象
// 所以每个函数内部都会有一个this指针,该this就好比是python中的self
function sign(n) {
    //this = 123
    console.log(`n ---> ${n}`)
    console.log(`this ---> ${this}`)
}

//基于call机制的函数调用
//参数123会被赋值给sign函数内部的this,456会被赋值给sing的参数n。
sign.call(123, 456)

/*
n ---> 456
this ---> 123
*/

apply() 的参数为空时,默认调用全局对象 window。

查看 chrome 浏览器控制台:

"匿名函数" 中的 this

匿名函数的 this 永远指向 全局对象

"箭头函数" 中的 this

  • 当创建一个函数,并将其赋值给一个变量时,这便是函数表达式。该函数是匿名的,这意味着它没有名字。
  • 箭头函数在ES6被引入。是函数表达式的紧凑替代品,并且总是匿名的。语法:(params) => { <function body> }

ES6中的 "箭头函数" 相当于 "匿名函数" 所以没有自己的 this。但是和匿名函数不同的是 this 指向

  • 匿名函数的 this 永远指向 全局对象
  • 箭头函数中的 this 继承自外层代码块的 this。所以箭头函数定义在哪,this 就指向那。即箭头函数中:this的指向由外层作用域决定,并且指向函数定义时的this,而不是执行时的this。

示例:箭头函数定义在一个对象里,那箭头函数里的 this 就指向该对象。

//es6中的双箭头操作符,类似于java中的lamada表达式 
<script type="text/javascript">
    var author = "chunlynn";
    var book = {
        name: 'linda',
        init: function(){
            setTimeout(ev=>{
                console.log(this.name);
            },3000);
        }
    }
    book.init(); //output:linda
    // this指向:谁调用或者哪个对象调用this所在的函数,this就指向谁。
</script>

总结

  • ① 谁调用 或者 哪个对象调用 this 所在的函数,this 就指向谁。 如果有嵌套调用,则其值会被绑定到调用 this 所在函数的最近的父对象 ,不论这个 this 出现在什么样的函数中,层次有多深,结构多复杂,只要看直接包含它的函数即可
  • “this” always refers to the “owner” of the function we're executing。
  •  this 通常指向的是我们正在执行的函数本身,或者是,指向该函数所属的对象。
  • ④ this 是 Javascript 语言的一个关键字,它代表函数运行时自动生成的一个内部对象,只能在函数内部使用。

3、JavaScript 异步编程

异步的概念

异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。

  • 传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。
  • 而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。

同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。如图通俗地解释一下异步:异步就是从主线程发射一个子线程来完成任务。

什么时候用异步编程

在前端编程中(甚至后端有时也是这样),在处理一些简短快速的操作时,例如计算 1 + 1 的结果,往往在主线程中就可以完成。主线程作为一个线程,不能够同时接受多方面的请求。所以,当一个事件没有结束时,界面将无法处理其他请求。

现在有一个按钮,如果我们设置它的 onclick 事件为一个死循环,那么当这个按钮按下,整个网页将失去响应。为了避免这种情况的发生,我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情,比如读取一个大文件或者发出一个网络请求。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但是子线程有一个局限:一旦创建并执行了子线程后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。为了解决这个问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。

回调函数

回调函数就是一个函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。

示例:

function print() {
    console.log('test callback')
}

// setTimeout 会在子线程中等待 3 秒,在 setTimeout 函数执行之后主线程并没有停止
setTimeout(print, 3000);
console.log('main thread continue')

这段程序中的 setTimeout 就是一个消耗时间较长(3 秒)的过程,它的第一个参数是个回调函数,第二个参数是毫秒数,这个函数执行之后会产生一个子线程,子线程会等待 3 秒,然后执行回调函数 "print",在命令行输出 "Time out"。当然,JavaScript 语法十分友好,不必单独定义一个函数 print ,通常将上面的程序写成:

function print() {
    
}
setTimeout(print, 3000);
console.log('main thread continue')

setTimeout(function () {
    console.log('test callback');
}, 3000);

异步 AJAX

除了 setTimeout 函数以外,异步回调广泛应用于 AJAX 编程。见:AJAX 教程 | 菜鸟教程

XMLHttpRequest 常常用于请求来自远程服务器上的 XML 或 JSON 数据。一个标准的 XMLHttpRequest 对象往往包含多个回调:

示例:( 尝试一下 )

<!DOCTYPE html>
<html>
<head> 
<meta charset="utf-8"> 
<title>菜鸟教程(runoob.com)</title> 
</head>
<body>

<p><strong>以下内容是通过异步请求获取的:</strong></p>
<p id="demo"></p>
<script>
var xhr = new XMLHttpRequest();
 
xhr.onload = function () {
    // 输出接收到的文字数据
    document.getElementById("demo").innerHTML=xhr.responseText;
}
 
xhr.onerror = function () {
    document.getElementById("demo").innerHTML="请求出错";
}
 
// 发送异步 GET 请求
xhr.open("GET", "https://www.runoob.com/try/ajax/ajax_info.txt", true);
xhr.send();
</script>

</body>
</html>

XMLHttpRequest 的 onload 和 onerror 属性都是函数,分别在它请求成功和请求失败时被调用。

如果你使用完整的 jQuery 库,也可以更加优雅的使用异步 AJAX:

示例:( 尝试一下 )

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
<script>
$(document).ready(function(){
	$("button").click(function(){
		$.get("/try/ajax/demo_test.php",function(data,status){
			alert("数据: " + data + "\n状态: " + status);
		});
	});
});
</script>
</head>
<body>

<button>发送一个 HTTP GET 请求并获取返回结果</button>

</body>
</html>

AJAX原理(含常见面试题)

:https://juejin.cn/post/6844904114896240647

AJAX 即 Asynchronous Javascript And XML(异步JavaScript和XML)是指一种创建交互式网页应用的网页开发技术,用于创建快速动态网页。它可以令开发者只向服务器获取数据(而不是图片,HTML文档等资源),互联网资源的传输变得前所未有的轻量级和纯粹,这激发了广大开发者的创造力,使各式各样功能强大的网络站点,和互联网应用如雨后春笋一般冒出,不断带给人惊喜。

一、什么是 AJAX

Ajax 是一种异步请求数据的web开发技术,对于改善用户的体验和页面性能很有帮助。简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。常见运用场景有表单验证是否登入成功、百度搜索下拉框提示和快递单号查询等等。

Ajax的目的是提高用户体验,较少网络数据的传输量。同时,由于AJAX请求获取的是数据而不是HTML文档,因此它也节省了网络带宽,让互联网用户的网络冲浪体验变得更加顺畅。

二、AJAX原理是什么

Ajax相当于在用户和服务器之间加了一个中间层,使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像一些数据验证和数据处理等都交给Ajax引擎自己来做,只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。

Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据。要清楚这个过程和原理,我们必须对 XMLHttpRequest有所了解。

XMLHttpRequest 是 ajax 的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说就是JavaScript可以向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。

发送 ajax 请求

from flask import Flask, render_template, request  # pip install Flask


app = Flask(__name__)


@app.route("/")
def index():
    # 跳转到首页
    print("你曾经来过服务器")
    name = "alex"
    # 数据是在这里渲染后, 返回个客户端的html
    return render_template("index.html", name=name)  


if __name__ == '__main__':
    app.run()

首先,用 Flask 创建一个后台服务器​

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="/static/js/jquery.js"></script>
</head>
<body>
    我就是一个传统的html页面, 我的名字是{{name}}
</body>

使用 jquery 来发送 ajax 的 get 请求

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="/static/js/jquery.js"></script>
    <script>
        function setCookie(name, value) {
            let Days = 30;
            let exp = new Date();
            exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
            document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
        }
        $(function(){
            // 可以在js任意位置设置cookie信息
            setCookie("name", "i-have-a-dream")
            $(".get_btn").click(function(){
                $.ajax({
                    url:"/ajax_get", // 服务器地址: 域名+url
                    method:'get',  // 发送get请求
                    headers:{  // 添加请求头信息
                        "token":"mememmememe",
                    },
                    data:{   // 传递参数
                        name:'alex',
                        _: new Date().getTime()
                    },
                    contentType:'application/json;charset=utf8',  
                    beforeSend: function(req){  // 也可以这样添加请求头信息
                        req.setRequestHeader("tokken", "i-can-do-it-haha");
                    },
                    success: function(back){  // 请求成功后. 返回数据了. 要做什么?
                        console.log(back);
                    }
                });
            })
        })
    </script>
</head>
<body>
    我就是一个传统的html页面, 我的名字是{{name}}
    <br/>
    <input type="button" class="get_btn" value="点我发送get_请求">
    <hr/>
    <a href="javascript:void(0);" class="post_btn" >点我发送post_请求</a>
</body>
</html>

服务器处理ajax_get请求

@app.route("/ajax_get")
def ajax_get_req():
    # 接收cookie中的信息
    n = request.cookies.get('name')
    if not n:
        return "没有cookie就不要来了."
    # 接收header中的信息
    token = request.headers.get('token')
    if not token:
        return "没token还想来?"

    # Flask接收get请求的参数
    name = request.args.get('name')
    _ = request.args.get('_')
    if name and _:
        # 返回json
        return {"name":'alex', "id": 10086, "isMen": True}
    else:
        return "回家去吧"

发送post请求(json)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="/static/js/jquery.js"></script>
    <style>
        #mask{
            position:fixed;
            top:0;
            left:0;
            right:0;
            bottom: 0;
            background-color: rgba(0,0,0, .3);
            color: #fff;
            font-size:30px;
            text-align: center;
            padding-top:300px;
            display:none;
        }

    </style>
    <script>
        $(function(){
            $(".post_btn").click(function(){
                $('#mask').css("display","block");
                $("#data_tbody").remove();
                $.ajax({
                    url:'/ajax_post',
                    method:'post',
                    data: JSON.stringify({
                        name:'alex',
                        id:'10086'
                    }),
                    headers: {  // 发送json数据. 要换这个头, 否则服务器收不到数据
                        "Content-Type": "application/json;charset=utf-8"
                    },
                    dataType:"text",
                    success:function(d){
                        $('#mask').css("display","none"); // 设置不遮罩
                        let data = JSON.parse(d);
                        let tbody = $("<tbody id='data_tbody'></tbody>")
                        data.forEach(function(item){
                            let tr = `<tr><td>${item.id}</td><td>${item.name}</td><td>${item.age}</td></tr>`;
                            tbody.append(tr);
                        });
                        $('table').append(tbody);
                    }
                })
            });
        })
    </script>
</head>
<body>
    我就是一个传统的html页面, 我的名字是{{name}}
    <br/>
    <input type="button" class="get_btn" value="点我发送get_请求">
    <hr/>
    <a href="javascript:void(0);" class="post_btn" >点我发送post_请求_加载一个表格试试</a>
    <hr/>
    <table width="80%" border="1">
        <thead>
        <tr>
            <td>id</td>
            <td>name</td>
            <td>age</td>
        </tr>
        </thead>

    </table>
    <div id="mask"><span>正在加载中......</span></div>
</body>
</html>

Python 服务器

from flask import Flask, render_template, request  # pip install Flask
import time
import json

app = Flask(__name__)

@app.route("/")
def index():
    # 跳转到首页
    print("你曾经来过服务器")
    name = "alex"
    return render_template("index.html", name=name)  # 数据是在这里渲染后, 返回个客户端的html


@app.route("/ajax_post", methods=['POST'])
def ajax_get_post():

    # time.sleep(3)
    # 接收JSON数据
    print(request.json)

    lst = [
        {"id": 1, "name": "张飞", "age": 16},
        {"id": 2, "name": "孙斌", "age": 16},
        {"id": 3, "name": "樵夫", "age": 16},
        {"id": 4, "name": "大佬", "age": 16},
    ]

    return json.dumps(lst)

if __name__ == '__main__':
    app.run()

JavaScript 的 Promise

3 种 异步 用法

什么是异步编程:https://www.runoob.com/js/javascript-async.html
JavaScript Promise 对象:https://www.runoob.com/w3cnote/javascript-promise-object.html
大白话透彻讲解 Promise 的使用:https://juejin.cn/post/7011755708496478215
JavaScript 高级深入浅出:Promise 详解:https://juejin.cn/post/7063377198014529572

js 执行的时候,一次只能执行一个任务,这就会导致其他任务阻塞而无法运行。由于这个缺陷导致 js 的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以使用回调函数执行。

常见的异步模式有以下几种:

  • 定时器。定时器是在固定时间触发某个回调函数。
    // setTimeout 示例
    function callBack(){
     console.log('执行完成')
    }
    console.log('before setTimeout')
    setTimeout(callBack,1000)// 1秒后调用callBack函数
    console.log('after setTimeout')
    但是,对于 ajax 网络请求就没有这么简单了,可能有多个网络请求是关联的,先执行某个请求返回结果后,第一个返回结果作为第二个请求的参数,调用第二个网络请求。如此,如果业务复杂,网络请求太多时,回调也很多,容易出现回调地狱。如下示例:回调地狱。这还只是 3 个回调,如果几十个回调,那看起来相当酸爽!!!
    setTimeout(function () {
        console.log("First");
        setTimeout(function () {
            console.log("Second");
            setTimeout(function () {
                console.log("Third");
            }, 3000);
        }, 4000);
    }, 1000);
    所以 Promise 出现了,专门解决异步回调地狱问题。JavaScript 的 Promise 是一个对象,代表异步操作是成功还是失败。Promise 允许对异步操作进行更优雅的管理,包括成功情况和错误处理。在过去,异步操作经常依赖回调函数,但是这会导致所谓的"回调地狱",特别是在处理多个异步操作时。Promise提供了一种更好的解决方案,允许链式调用和更简洁的错误处理。
  • 接口调用
  • 事件函数

Promise 基本概念

Promise 翻译成中文:承诺、保证。
resolve  解决
reject   拒绝

通俗地讲 Promise 就像一个容器,里面存放着未来才会结束,返回结果的容器(resolve、reject),返回的结果只需要在出口处接收就好了。从语法上讲,Promise 是一个对象,从它可以获取异步操作的消息。Promise 对象代表一个异步操作,有三种状态:

  • Pending(挂起、待定):初始状态,既不是成功,也不是失败。
  • Fulfilled(已实现):意味着操作成功完成。
  • Rejected(已拒绝):意味着操作失败。

Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise 构造函数

Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务。创建 Promise时,Promise 构造函数只有一个参数并且是一个函数,该函数是同步的并且会被立即执行,所以这个函数被称为起始函数。起始函数包含两个参数 resolve 和 reject。起始函数执行成功时可以调用resolve函数传递成功的结果。当起始函数执行失败时可以调用reject函数传递失败的原因。

起始函数的两个参数通常命名为 resolve(用于成功时调用)和 reject(用于失败时调用)。

  • resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;
  • resolve 和 reject 并不能够使起始函数停止运行,别忘了 return。

Promise 还提供了.then(), .catch(), 和 .finally()方法来处理成功、失败和无论成功还是失败都要执行的代码。如果不需要给 then 中的函数传递返回值,则参数 resolve 和 reject 都可以省略

  • then:用于处理 Promise 成功状态的回调函数。
  • catch:用于处理 Promise 失败状态的回调函数。
  • finally:无论 Promise 是成功还是失败,都会执行的回调函数。

Promise 使用示例:

function send(arg_test) {
    return  new Promise(function(resolve, reject) {
        resolve(`[成功 ---> ${arg_test}]`);
        reject(`[失败 ---> ${arg_test}]`);
    });
}
send("测试Promise").then(function (arg_success){
    console.log(`arg_success ---> ${arg_success}`);
}, (arg_fail)=>{
    console.log(`arg_fail ---> ${arg_fail}`);
}).catch(function (error) {})

Promise 使用示例:

// resolve 和 reject 都是函数。
new Promise(function (resolve, reject) {
    console.log("Run");
});
// 这段程序会直接输出 Run。

const p = new Promise((resolve, reject) => {
    setTimeout(() => {resolve('123')}, 1000)
}).then(res => {
    console.log(res) //1秒后打印123
})

Promise 使用示例:

let promise = new Promise(function (resolve, reject) {
    // 异步操作
    let isSuccess = true; // 假设这是根据异步操作结果得来的
    if (isSuccess) {
        resolve('操作成功');
    } else {
        reject('操作失败');
    }
});

promise.then((result) => {
    console.log(result); // "操作成功"
}).catch((error) => {
    console.error(error);
}).finally(() => {
    console.log("无论成功还是失败,都会执行");
});

示例:使用 Promise 构造函数创建了一个 Promise 对象,并使用 setTimeout 模拟了一个异步操作。如果异步操作成功,则调用 resolve 函数并传递成功的结果;如果异步操作失败,则调用 reject 函数并传递失败的原因。然后我们使用 then 方法处理 Promise 成功状态的回调函数,使用 catch 方法处理 Promise 失败状态的回调函数。这段程序会直接输出 error 或 success

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    if (Math.random() < 0.5) {
      resolve('success');
    } else {
      reject('error');
    }
  }, 1000);
});
 
promise.then(result => {
  console.log(result);
}).catch(error => {
  console.log(error);
});

resolve 和 reject 都是函数,其中调用 resolve 代表一切正常,reject 是出现异常时所调用的:

new Promise(function (resolve, reject) {
    var a = 0;
    var b = 1;
    if (b == 0) reject("Divide zero");
    else resolve(a / b);
}).then(function (value) {
    console.log("a / b = " + value);
}).catch(function (err) {
    console.log(err);
}).finally(function () {
    console.log("End");
});

Promise 类有 .then() .catch() 和 .finally() 三个方法,这三个方法的参数都是一个函数,.then() 可以将参数中的函数添加到当前 Promise 的正常执行序列,.catch() 则是设定 Promise 的异常处理序列,.finally() 是在 Promise 执行的最后一定会执行的序列。 .then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列:

new Promise(function (resolve, reject) {
    console.log(1111);
    resolve(2222);
}).then(function (value) {
    console.log(value);
    return 3333;
}).then(function (value) {
    console.log(value);
    throw "An error";
}).catch(function (err) {
    console.log(err);
});

Promise 静态方法

  • Promise.all():接收一个promise数组,只有当所有promise都完成时,才会通过resolve返回所有结果,如果有一个失败,则立即通过reject返回。
  • Promise.race():也是接收一个promise数组,但是只返回第一个完成(无论是成功还是失败)的promise的结果。
  • Promise.resolve()、Promise.reject():快速创建一个状态为fulfilled或rejected的promise。

Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

var p = Promise.all([p1,p2,p3]);

代码中,Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 对象的实例。(Promise.all 方法的参数不一定是数组,但是必须具有 iterator 接口,且返回的每个成员都是 Promise 实例。)

p 的状态由 p1、p2、p3 决定,分成两种情况。

  • (1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • (2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

下面是一个具体的例子。

// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function(id){
  return getJSON("/post/" + id + ".json");
});
 
Promise.all(promises).then(function(posts) {
  // ...  
}).catch(function(reason){
  // ...
});

Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

var p = Promise.race([p1,p2,p3]);

代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。

如果Promise.all方法和Promise.race方法的参数,不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

Promise.resolve 方法,Promise.reject 方法

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

代码将 jQuery 生成 deferred 对象,转为一个新的 ES6 的 Promise 对象。

如果 Promise.resolve 方法的参数,不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为fulfilled。

var p = Promise.resolve('Hello');
 
p.then(function (s){
  console.log(s)
});
// Hello

代码生成一个新的Promise对象的实例p,它的状态为fulfilled,所以回调函数会立即执行,Promise.resolve方法的参数就是回调函数的参数。

如果Promise.resolve方法的参数是一个Promise对象的实例,则会被原封不动地返回。

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

var p = Promise.reject('出错了');
 
p.then(null, function (s){
  console.log(s)
});
// 出错了

代码生成一个Promise对象的实例,状态为rejected,回调函数会立即执行。

链式调用 实现 回调地狱

Promise 的一个重要特性是可以链式调用,每个.then()或.catch()实际上也都会返回一个新的 Promise,让链式调用成为可能。链式调用可以直接执行一系列异步操作。

现在用 Promise 来实现上面 setTimeout 回调地狱的功能:

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve();
    }, 1000);
}).then(function () {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("Second");
            resolve();
        }, 4000);
    });
}).then(function () {
    setTimeout(function () {
        console.log("Third");
    }, 3000);
});

使用 Promise 将嵌套格式的代码变成了顺序格式的代码。代码看着更清晰。也可以将核心部分写成一个 Promise 函数。这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库。

function print(delay, message) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(message);
            resolve();
        }, delay);
    });
}

print(1000, "First").then(function () {
    return print(4000, "Second");
}).then(function () {
    print(3000, "Third");
});

Promise.prototype.then方法:链式操作

Promise.prototype.then 方法返回的是一个新的 Promise 对象,因此可以采用链式写法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // proceed
});

代码使用 then 方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // 对comments进行处理
});

种设计使得嵌套的异步操作,可以被很容易得改写,从回调函数的"横向发展"改为"向下发展"。

Promise.prototype.catch方法:捕捉错误

Promise.prototype.catch 方法是 Promise.prototype.then(null, rejection) 的别名,用于指定发生错误时的回调函数。

getJSON("/posts.json").then(function(posts) {
  // some code
}).catch(function(error) {
  // 处理前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

Promise 对象的错误具有"冒泡"性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获。

async / await

在JavaScript中,一些操作是异步的。这意味着它们产生的结果或者值不会立即奏效。

function fetchDataFromApi() {
  fetch('https://v2.jokeapi.dev/joke/Programming?type=single')
    .then(res => res.json())
    .then(json => console.log(json.joke));
}

fetchDataFromApi();
console.log('Finished fetching data');

JavaScript 解释器不会等待异步fetchDataFromApi函数完成后再解释下一条语句。因此,在打印API 返回的真实数据之前,它就会打印 Finished fetching data。大多数情况下,这并不是我们想要的行为。可以使用asyncawait关键字,使程序在继续前进之前等待异步操作的完成。

async/await 是在 JavaScript 中用于处理异步操作的一种语法结构,它基于 Promise 机制,使异步代码看起来更像同步代码,从而提高代码的可读性和可维护性。

  • async 关键字用于声明一个异步函数,该函数返回一个 Promise 对象。
  • await 关键字只能在 async 函数内部使用,用于暂停异步函数的执行,等待一个 Promise 对象的解决,并返回解决的值。

async 关键字

定义函数时,在 function 前面加上 async 关键字,函数就标记为异步函数。

async function fetchDataFromApi() {
  fetch('https://v2.jokeapi.dev/joke/Programming?type=single')
    .then(res => res.json())
    .then(json => console.log(json.joke));
}

异步函数总是返回一个promise

async function echo(arg) {
  return arg;
}

const res = echo(5);
console.log(res);

所以可以通过在函数调用上链接一个then()来获得正确的执行顺序:

fetchDataFromApi()
  .then(() => {
    console.log('Finished fetching data');
  });

但是这种写法还是不太优雅,所以就出现了 async/await,它能够用一种看起来更像同步代码的语法来编写异步代码,而且更容易阅读。

await 关键字

在任何异步操作前面加上 await 关键字。这将迫使 JavaScript 解释器"暂停"执行并等待结果。可以将这些操作的结果分配给变量:

async function fetchDataFromApi() {
  const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
  const json = await res.json();
  console.log(json.joke);
}

等待调用fetchDataFromApi函数的结果:

async function fetchDataFromApi() {
  const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
  const json = await res.json();
  console.log(json.joke);
}

async function init() {
  await fetchDataFromApi();
  console.log('Finished fetching data');
}

init();

现在这个代码比基于 promise 的版本更容易阅读。

"链式调用 实现 回调地狱" 代码可以改写如下:

async function asyncFunc() {
    await print(1000, "First");
    await print(4000, "Second");
    await print(3000, "Third");
}
asyncFunc();

可以看到通过 async / await 将异步操作变得像同步操作一样。异步函数 async function 中可以使用 await 指令,await 指令后必须跟着一个 Promise,异步函数会在这个 Promise 运行中暂停,直到其运行结束再继续运行。异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于程序员阅读。

处理异常的机制将用 try-catch 块实现:

async function asyncFunc() {
    try {
        await new Promise(function (resolve, reject) {
            throw "Some error"; // 或者 reject("Some error")
        });
    } catch (err) {
        console.log(err);
        // 会输出 Some error
    }
}
asyncFunc();

如果 Promise 有一个正常的返回值,await 语句也会返回它

async function asyncFunc() {
    let value = await new Promise(
        function (resolve, reject) {
            resolve("Return value");
        }
    );
    console.log(value);
}
asyncFunc();

声明 异步函数

上面例子中,使用了两个具名函数声明(function关键字后跟着函数名字),也可以把函数表达式、箭头函数和匿名函数标记为async

异步函数表达式

当创建一个函数,并将其赋值给一个变量时,这便是函数表达式。该函数是匿名的,这意味着它没有名字。比如:

const fetchDataFromApi = async function() {
  const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
  const json = await res.json();
  console.log(json.joke);
}

异步箭头函数

箭头函数在ES6被引入。它们是函数表达式的紧凑替代品,并且总是匿名的。

(async () => {
  async function fetchDataFromApi() {
    const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single');
    const json = await res.json();
    console.log(json.joke);
  }
  await fetchDataFromApi();
  console.log('Finished fetching data');
})();

使用函数表达式或函数声明并没有什么大的区别:大部分情况下,这只是一个使用偏好的问题。但有几件事情需要注意,比如变量提升,或者箭头函数无法绑定this的事实。

Await / Async 内部机制

async / await 在很大程度上是 promise 的语法糖。

promise可能会是三种状态之一:pendingfulfilled、或者rejected。一个promise开始时处于pending状态。如果与该promise有关的行为成功了,该promise就被称为fulfilled。如果行为不成功,该promise就被称为rejected。一旦promisefulfilled或者rejected,但不是pending,它也被认为是settled

当我们在async函数中使用 await 关键字来"暂停"函数执行时,真正发生的是我们在等待一个promise(无论是显式还是隐式)进入resolvedrejected状态。

async function echo(arg) {
  return arg;
}

async function getValue() {
  const res = await echo(5);
  console.log(res);
}

getValue();
// 5

因为echo函数返回一个promise,而getValue函数中的await关键字在继续程序之前等待这个promise完成,所以我们能够将所需的值打印到控制台。

promise是对JavaScript中流程控制的一大改进,并且被一些较新的浏览器API所使用。比如Battery status API、Clipboard API、Fetch API、MediaDevices API等等。

Node还在其内置的util模块中添加了一个promise函数,可以将使用回调函数的代码转换为返回promise。而从v10开始,Node的fs模块中的函数可以直接返回promise

promise 转成 async/await

 任何返回promise的函数都可以使用async/await。但并不是应该对所有的事情都使用async/await(该语法确实有其缺点,将在讨论错误处理时看到)。

看另一个例子。这里有一个小的实用函数,使用Node基于promise的API和它的readFile方法来获取一个文件的内容。

使用Promise.then():

const { promises: fs } = require('fs');

const getFileContents = function(fileName) {
  return fs.readFile(fileName, enc)
}

getFileContents('myFile.md', 'utf-8')
  .then((contents) => {
    console.log(contents);
  });

有了async/await就会变成:

import { readFile } from 'node:fs/promises';

const getFileContents = function(fileName, enc) {
  return readFile(fileName, enc)
}

const contents = await getFileContents('myFile.md', 'utf-8');
console.log(contents);

注意:这是在利用一个叫做top-level await的功能,它只在ES模块中可用。要运行这段代码,请将文件保存为index.mjs并使用Node>=14.8的版本。

虽然这些都是简单的例子,但可以发现async/await的语法更容易理解。当处理多个then()语句和错误处理时,这一点变得尤其真实。

错误处理

在处理异步函数时,有几种方法来处理错误。最常见的可能是使用try...catch块,可以把它包在异步操作中并捕捉任何发生的错误。

async function fetchDataFromApi() {
  try {
    const res = await fetch('https://non-existent-url.dev');
    const json = await res.json();
    console.log(json.joke);
  } catch (error) {
    // Handle the error here in whichever way you like
    console.log('Something went wrong!');
    console.warn(error)
  }
}

await fetchDataFromApi();
console.log('Finished fetching data');

fetch 返回一个promise。当fetch操作失败时,promisereject方法被调用,await关键字将这种reject转换为一个可捕捉的错误。然而,这种方法有几个问题。主要的问题是它很啰嗦,而且相当难看。假如正在构建一个CRUD应用程序,每个CRUD方法(创建、读取、更新、销毁)都有一个单独的函数。如果这些方法中的每一个都进行了异步API调用,就必须把每个调用包在自己的try...catch块中。这是相当多的额外代码。

另一个问题是,如果不使用await关键字,这将导致一个未处理的拒绝的promise

import { readFile } from 'node:fs/promises';

const getFileContents = function(fileName, enc) {
  try {
    return readFile(fileName, enc)
  } catch (error) {
    console.log('Something went wrong!');
    console.warn(error)
  }
}

const contents = await getFileContents('this-file-does-not-exist.md', 'utf-8');
console.log(contents);

await不同,return关键字不会将拒绝的promise转化为可捕捉的错误。

在函数调用中使用catch(),上例中的代码将优雅地处理错误:

const contents = await getFileContents('this-file-does-not-exist.md', 'utf-8')
  .catch((error) => {
    console.log('Something went wrong!');
    console.warn(error);
  });
console.log(contents);

至于使用哪种策略,可以使用try/catch来恢复async函数内部的预期错误,但通过在调用函数中添加catch()来处理意外错误。

并行运行异步命令

使用await关键字来等待一个异步操作完成时,JavaScript解释器会相应地暂停执行。虽然这很方便,但这可能并不总是我们想要的。考虑一下下面的代码:

(async () => {
  async function getStarCount(repo){
    const repoData = await fetch(repo);
    const repoJson = await repoData.json()
    return repoJson.stargazers_count;
  }

  const reactStars = await getStarCount('https://api.github.com/repos/facebook/react');
  const vueStars = await getStarCount('https://api.github.com/repos/vuejs/core');
  console.log(`React has ${reactStars} stars, whereas Vue has ${vueStars} stars`)
})();

这里我们正在进行两次API调用,分别获取React和Vue的GitHub star数。虽然这样可以正常运转,但我们没有理由在发出第二个fetch请求之前等待第一个promise完成。如果我们要发出很多请求,这将是一个相当大的瓶颈。

为了解决这个问题,我们可以使用Promise.all,它接收一个promise数组,并等待所有promise被解决或其中任何一个Promise 调用 reject:

(async () => {
  async function getStarCount(repo){
    // As before
  }

  const reactPromise = getStarCount('https://api.github.com/repos/facebook/react');
  const vuePromise = getStarCount('https://api.github.com/repos/vuejs/core');
  const [reactStars, vueStars] = await Promise.all([reactPromise, vuePromise]);

  console.log(`React has ${reactStars} stars, whereas Vue has ${vueStars} stars`);
})();

同步代码 调用 异步await

在一个同步循环中调用一个异步函数。比如说:

// Return promise which resolves after specified no. of milliseconds
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

async function process(array) {
  array.forEach(async (el) => {
    await sleep(el); // we cannot await promise here
    console.log(el);
  });
}

const arr = [3000, 1000, 2000];
process(arr);

这不会像预期的那样奏效,因为forEach只会调用函数而不等待它完成,以下内容将被打印到控制台:

1000
2000
3000

同样的事情也适用于其他许多数组方法,如mapfilterreduce

幸运的是,ES2018引入了异步迭代器,除了它们的next()方法会返回一个promise外,它们就像普通的迭代器。这意味着我们可以在其中使用 await。让我们使用for...of重写上面的代码:

async function process(array) {
  for (el of array) {
    await sleep(el);
    console.log(el);
  };
}

现在,process函数的输出就是正确的顺序:

3000
1000
2000

就像之前等待异步fetch请求的例子一样,这也会带来性能上的代价。for循环中的每个await都会阻塞事件循环,通常应该重构代码,一次性创建所有的promise,然后使用Promise.all()来获取结果。甚至有一条ESLint规则,如果它检测到这种行为就会警告。

顶层 await

顶层await 是ES2022中引入的语言,从14.8版开始在Node中可用。

当试图在一个async函数之外使用await时,就会发生这种情况。

Uncaught SyntaxError: await is only valid in async functions, async generators and modules

例如,在代码的顶层:

const ms = await Promise.resolve('Hello, World!');
console.log(msg);

顶层await解决了这个问题,使上述代码有效,但只在ES模块中奏效。如果我们在浏览器中工作,我们可以把这段代码添加到一个叫做index.js的文件中,然后像这样把它加载到我们的页面中:

<script src="index.js" type="module"></script>

同步代码 获取 异步返回值

当返回的是Promise时,可以通过在函数调用上链接一个then()来获得正确的执行顺序:

async function getData(url) {
  try {
    const res = await new Promise((resolve, reject) => {
      uni.request({
        url: url,
        success: (result) => {
          resolve(result);
        },
        fail: (error) => {
          reject(error);
        }
      });
    });
    return res;
  } catch (err) {
    console.error(err);
  }
}

// 使用该函数
getData('https://example.com/api')
 .then((res) => {
    console.log(res);
  })
 .catch((err) => {
    console.error(err);
  });

用 Promise 对象实现的 Ajax 操作的例子。

function ajax(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest(); 
        req.open('GET', URL, true);
        req.onload = function () {
        if (req.status === 200) { 
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            } 
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send(); 
    });
}
var URL = "/try/ajax/testpromise.php"; 
ajax(URL).then(function onFulfilled(value){
    document.write('内容是:' + value); 
}).catch(function onRejected(error){
    document.write('错误:' + error); 
});

常见的问题 (FAQ)

Q: then、catch 和 finally 序列能否顺序颠倒?

A: 可以,效果完全一样。但不建议这样做,最好按 then-catch-finally 的顺序编写程序。

Q: 除了 then 块以外,其它两种块能否多次使用?

A: 可以,finally 与 then 一样会按顺序执行,但是 catch 块只会执行第一个,除非 catch 块里有异常。所以最好只安排一个 catch 和 finally 块。

Q: then 块如何中断?

A: then 块默认会向下顺序执行,return 是不能中断的,可以通过 throw 来跳转至 catch 实现中断。

Q: 什么时候适合用 Promise 而不是传统回调函数?

A: 当需要多次顺序执行异步操作的时候,例如,如果想通过异步方法先后检测用户名和密码,需要先异步检测用户名,然后再异步检测密码的情况下就很适合 Promise。

Q: Promise 是一种将异步转换为同步的方法吗?

A: 完全不是。Promise 只不过是一种更良好的编程风格。

Q: 什么时候我们需要再写一个 then 而不是在当前的 then 接着编程?

A: 当你又需要调用一个异步任务的时候。

4、JavaScript HTTP 库 Axios 

官方文档:起步 | Axios中文文档 | Axios中文网

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。Axios 不是一种新的技术。axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是 Promise 的实现版本,符合最新的ES规范。特点

  • 从浏览器创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 超时处理
  • 查询参数序列化支持嵌套项处理
  • 自动将请求体序列化为:
    • JSON (application/json)
    • Multipart / FormData (multipart/form-data)
    • URL encoded form (application/x-www-form-urlencoded)
  • 将 HTML Form 转换成 JSON 进行请求
  • 自动转换JSON数据
  • 获取浏览器和 node.js 的请求进度,并提供额外的信息(速度、剩余时间)
  • 为 node.js 设置带宽限制
  • 兼容符合规范的 FormData 和 Blob(包括 node.js)
  • 客户端支持防御XSRF

axios优点:

  • 1.支持 node 端和浏览器端。同样的API,node和浏览器全支持,平台切换无压力
  • 2.支持 Promise。使用Promise管理异步,告别传统callback方式
  • 3.丰富的配置项。支持拦截器等高级配置
  • 4.社区支持。axios相关的npm包数量一直在增长

安装 axios

使用 npm:$ npm install axios
使用 bower:$ bower install axios
使用 yarn:$ yarn add axios
使用 jsDelivr CDN:
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
使用 unpkg CDN:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
使用 require 导入预构建的 CommonJS 模块:
const axios = require('axios/dist/browser/axios.cjs'); // browser
const axios = require('axios/dist/node/axios.cjs'); // node

axios 依赖原生的 ES6 Promise 实现而被支持,如果你的环境不支持 ES6 Promise,可以使用 polyfill。

axios 包括 TypeScript 定义:

import axios from 'axios';
axios.get('/user?ID=12345');

CommonJS 用法
为了在CommonJS中使用 require() 导入时获得TypeScript类型推断(智能感知/自动完成),可以使用以下方法:

const axios = require('axios').default;
// axios.<method> 能够提供自动完成和参数类型推断功能

案  例

<script src="/static/axios.min.js"></script>
<script>
    window.onload = function(){
        axios.post("/movies", {"page": 10086}).then(function(resp){
            console.log(resp.data);
        })
    }
</script>

axios 默认发送和接收的数据就是json. 所以在浏览器抓包时可以看到

执行 GET 请求

import axios from "axios";

// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345').then(function (response) {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});

// 上面的请求也可以这样做
axios.get('/user', {
    params: {
        ID: 12345
    }
}).then(function (response) {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});
const axios = require('axios');

// 向给定ID的用户发起请求
axios.get('/user?ID=12345')
    .then(function (response) {
        // 处理成功情况
        console.log(response);
    })
    .catch(function (error) {
        // 处理错误情况
        console.log(error);
    })
    .finally(function () {
        // 总是会执行
    });

// 上述请求也可以按以下方式完成(可选)
axios.get('/user', {
    params: {
        ID: 12345
    }
})
    .then(function (response) {
        console.log(response);
    })
    .catch(function (error) {
        console.log(error);
    })
    .finally(function () {
        // 总是会执行
    });

// 支持async/await用法
async function getUser() {
    try {
        const response = await axios.get('/user?ID=12345');
        console.log(response);
    } catch (error) {
        console.error(error);
    }
}

注意: 由于async/await 是ECMAScript 2017中的一部分,而且在IE和一些旧的浏览器中不支持,所以使用时务必要小心。

执行 POST 请求

import axios from "axios";

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
}).then(function (response) {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});

并发 请求

function getUserAccount() {
    return axios.get('/user/12345');
}

function getUserPermissions() {
    return axios.get('/user/12345/permissions');
}

const [acct, perm] = await Promise.all([getUserAccount(), getUserPermissions()]);

// OR

Promise.all([getUserAccount(), getUserPermissions()])
    .then(function ([acct, perm]) {
        // 两个请求都执行完成
    });

将 HTML Form 转换成 JSON 进行请求

const axios = require('axios');

const {data} = await axios.post('/user', document.querySelector('#my-form'), {
    headers: {
        'Content-Type': 'application/json'
    }
})

Forms

  • Multipart (multipart/form-data)
const {data} = await axios.post('https://httpbin.org/post', {
    firstName: 'Fred',
    lastName: 'Flintstone',
    orders: [1, 2, 3],
    photo: document.querySelector('#fileInput').files
  }, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  }
)
  • URL encoded form (application/x-www-form-urlencoded)
const {data} = await axios.post('https://httpbin.org/post', {
    firstName: 'Fred',
    lastName: 'Flintstone',
    orders: [1, 2, 3]
  }, {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
})

Axios API

可以通过向 axios 传递相关配置来创建请求:

axios(config)

import axios from "axios";

// 发送 POST 请求
axios({
    method: 'post',
    url: '/user/12345',
    data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
    }
})

// 获取远端图片
axios({
    method: 'get',
    url: 'http://bit.ly/2mTM3nY',
    responseType: 'stream'
}).then(function (response) {
    response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));
})

axios(url [, config])

// 发送 GET 请求(默认的方法)
axios('/user/12345');

请求方法的别名

为方便起见,为所有支持的请求方法提供了别名

  • axios.request(config)
  • axios.get(url [config])
  • axios.delete(url [config])
  • axios.head(url [config])
  • axios.options(url [config])
  • axios.post(url [ data[ config]])
  • axios.put(url [data[config]])
  • axios.patch(url [ data[ config]])

注意:在使用别名方法时, urlmethoddata 这些属性都不必在配置中指定。

axios基本使用及封装:https://juejin.cn/post/7084163923552780319

并 发

处理并发请求的助手函数:

  • axios.all(iterable)
  • axios.spread(callback)

创建 实例

可以使用自定义配置创建一个 axios 实例:axios.create([config])

import axios from "axios";

const instance = axios.create({
    baseURL: 'https://some-domain.com/api/',
    timeout: 1000,
    headers: {'X-Custom-Header': 'foobar'}
})

注意:使用创建的 axios 实例请求时,请求的配置项将与实例的配置合并。

请求 配置

这些是创建请求时可以用的配置选项。只有 url 是必需的,如果没有指定 method,请求将默认使用 get方法。

{
  // `url` 是用于请求的服务器 URL
  url: '/user',

  // `method` 是创建请求时使用的方法
  method: 'get', // 默认值

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
  // 你可以修改请求头。
  transformRequest: [function (data, headers) {
    // 对发送的 data 进行任意转换处理

    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对接收的 data 进行任意转换处理

    return data;
  }],

  // 自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` 是与请求一起发送的 URL 参数
  // 必须是一个简单对象或 URLSearchParams 对象
  params: {
    ID: 12345
  },

  // `paramsSerializer`是可选方法,主要用于序列化`params`
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function (params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },

  // `data` 是作为请求体被发送的数据
  // 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
  // 在没有设置 `transformRequest` 时,则必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属: FormData, File, Blob
  // - Node 专属: Stream, Buffer
  data: {
    firstName: 'Fred'
  },
  
  // 发送请求体数据的可选语法
  // 请求方式 post
  // 只有 value 会被发送,key 则不会
  data: 'Country=Brasil&City=Belo Horizonte',

  // `timeout` 指定请求超时的毫秒数。
  // 如果请求时间超过 `timeout` 的值,则请求会被中断
  timeout: 1000, // 默认值是 `0` (永不超时)

  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default

  // `adapter` 允许自定义处理请求,这使测试更加容易。
  // 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
  adapter: function (config) {
    /* ... */
  },

  // `auth` HTTP Basic Auth
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

  // `responseType` 表示浏览器将要响应的数据类型
  // 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
  // 浏览器专属:'blob'
  responseType: 'json', // 默认值

  // `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
  // 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // 默认值

  // `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
  xsrfCookieName: 'XSRF-TOKEN', // 默认值

  // `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
  xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值

  // `onUploadProgress` 允许为上传处理进度事件
  // 浏览器专属
  onUploadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  // 浏览器专属
  onDownloadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
  maxContentLength: 2000,

  // `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
  maxBodyLength: 2000,

  // `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
  // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
  // 则promise 将会 resolved,否则是 rejected。
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 默认值
  },

  // `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
  // 如果设置为0,则不会进行重定向
  maxRedirects: 5, // 默认值

  // `socketPath` 定义了在node.js中使用的UNIX套接字。
  // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
  // 只能指定 `socketPath` 或 `proxy` 。
  // 若都指定,这使用 `socketPath` 。
  socketPath: null, // default

  // `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
  // and https requests, respectively, in node.js. This allows options to be added like
  // `keepAlive` that are not enabled by default.
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // `proxy` 定义了代理服务器的主机名,端口和协议。
  // 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
  // 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
  // `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
  // 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
  // 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
  proxy: {
    protocol: 'https',
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // see https://axios-http.com/zh/docs/cancellation
  cancelToken: new CancelToken(function (cancel) {
  }),

  // `decompress` indicates whether or not the response body should be decompressed 
  // automatically. If set to `true` will also remove the 'content-encoding' header 
  // from the responses objects of all decompressed responses
  // - Node only (XHR cannot turn off decompression)
  decompress: true // 默认值

}

响应 结构

某个请求的响应包含以下信息

{
    // data 由服务器提供的响应
    data: {},
    
    // status 来自服务器响应的 HTTP 状态码
    status: 200,

    // statusText 来自服务器响应的 HTTP 状态信息
    statusText: 'OK',

    // headers 服务器响应的头
    headers: {},

    // config 是为请求提供的配置信息
    config: {},

    // request 是生成当前响应的请求
    // 在 node.js 中是最后一个 ClientRequest 实例 (在重定向中)
    // 在浏览器中是 XMLHttpRequest 实例
    request: {}
}

使用 then 时,你将接收下面这样的响应 :

axios.get('/user/12345')
    .then(function (response) {
        console.log(response.data);
        console.log(response.status);
        console.log(response.statusText);
        console.log(response.headers);
        console.log(response.config);
    })

在使用 catch 、或传递 rejection callback 作为 then 的第二个参数时,响应可以通过 error 对象被使用,可参考后面的篇章 —— 错误处理。

配置 默认值

全局的 axios 默认值

axios.defaults.baseURL = 'http://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

自定义实例默认值

// 创建实例时设置配置默认值
const instance = axios.create({
    baseURL: 'https://api.example.com'
});

// 实例创建之后可修改默认配置
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

配置的优先顺序

配置会以一个优先顺序进行合并。这个顺序是:在 lib/defaults.js 找到的库的默认值,然后是实例的 defaults 属性,最后是请求的 config 参数。后者将优先于前者。这里是一个例子:

// 使用由库提供的配置默认值来创建实例
// 此时超时配置的默认值是 0
const instance = axios.create();

// 覆写库的超时默认值
// 现在,在超时前,所有请求都会等待 2.5 秒
instance.defaults.timeout = 2500;

// 为已知需要花费很长时间的请求覆写超时设置
instance.get('/longRequest', {
    timeout: 5000
});

拦截器

场景:有些网站会对每次请求都添加加密信息. 或者每次返回数据的时候, 都有解密逻辑. 那此时. 你思考. 不可能每次请求都要程序员去手动写加密逻辑. 例如

window.onload = function(){
    // 加密数据
    axios.post("/movies", {"page": 10086}).then(function(resp){
        明文 = 解密(resp.data);
        console.log(明文);
    })

    // 加密数据
    axios.post("/movies", {"page": 10086}).then(function(resp){
        明文 = 解密(resp.data);
        console.log(明文);
    })
}

这样很麻烦. 也很蛋疼. axios想到过类似的问题. 它提供了拦截器. 一次性处理好这种问题

axios.interceptors.request.use(function(config){  // 拦截所有请求
    console.log("我是拦截器. 我可以对数据进行加密");
    console.log(config)
    return config;
}, function(error){
    return Promise.reject(error);
});

axios.interceptors.response.use(function(response){  // 拦截所有响应
    console.log("我是响应回来之后拦截器. 我可以对数据进行解密")
    return response.data;  
}, function(error){
    return Promise.reject(error);
});

这样. 对于业务层的代码而言就简单很多了

window.onload = function(){
    // 加密的逻辑拦截器帮我完成了
    axios.post("/movies", {"page": 10086}).then(function(data){
        // 解密的逻辑拦截器帮我完成了
        console.log(data);
    })
    // 加密的逻辑拦截器帮我完成了
    axios.post("/movies", {"page": 10086}).then(function(data){
        // 解密的逻辑拦截器帮我完成了
        console.log(data);
    })
}

拦截器作用就是:在请求或响应被 then 或 catch 处理前拦截它们。

// 添加请求拦截器
axios.interceptors.request.use(
    function (config) {
        // 在发送请求之前做些什么
        return config;
    },
    function (error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    }
);

// 添加响应拦截器
axios.interceptors.response.use(
    function (response) {
        // 对响应数据做点什么
        return response;
    },
    function (error) {
        // 对响应错误做点什么
        return Promise.reject(error);
    }
);

如果你想在稍后移除拦截器,可以这样:

const myInterceptor = axios.interceptors.request.use(function () { /* ... */ });
axios.interceptors.request.eject(myInterceptor);

可以为自定义 axios 实例添加拦截器:

const instance = axios.create();
instance.interceptors.request.use(function () { /* ... */ });

错误 处理

axios.get('/user/12345')
    .catch(function (error) {
        if (error.response) {
            // 请求已发出,且服务器的响应状态码超出了 2xx 范围
            console.log(error.response.data);
            console.log(error.response.status);
            console.log(error.response.headers);
        } else if (error.request) {
            // 请求已发出,但没有接收到任何响应
            // 在浏览器中,error.request 是 XMLHttpRequest 实例
            // 在 node.js 中,error.request 是 http.ClientRequest 实例
            console.log(error.request);
        } else {
            // 引发请求错误的错误信息
            console.log('Error', error.message);
        }
        console.log(error.config);
    });

你可以使用 validateStatus 配置选项定义一个自定义 HTTP 状态码的错误范围:

axios.get('/user/12345', {
    validateStatus: function (status) {
        // 当且仅当 status 大于等于 500 时 Promise 才被 reject
        return status < 500;
    }
});

取消 请求

AbortController

从 v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求:

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()

CancelToken deprecated

已经

请求体 编码

默认情况下,axios将 JavaScript 对象序列化为 JSON 。 要以application/x-www-form-urlencoded格式发送数据,您可以使用以下选项之一。

浏览器

在浏览器中,可以使用URLSearchParams API,如下所示:

const params = new URLSearchParams();
params.append('param1', 'value1');
params.append('param2', 'value2');
axios.post('/foo', params);

请注意,不是所有的浏览器(参见 caniuse.com)都支持 URLSearchParams ,但是可以使用polyfill (确保 polyfill 全局环境)

或者, 您可以使用qs 库编码数据:

const qs = require('qs');
axios.post('/foo', qs.stringify({ 'bar': 123 }));

或者用另一种方式 (ES6),

import qs from 'qs';
const data = { 'bar': 123 };
const options = {
  method: 'POST',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  data: qs.stringify(data),
  url,
};
axios(options);

Node.js

Query string

在 node.js 中, 可以使用 querystring 模块,如下所示:

const querystring = require('querystring');
axios.post('http://something.com/', querystring.stringify({ foo: 'bar' }));

或者从'url module'中使用'URLSearchParams',如下所示:

const url = require('url');
const params = new url.URLSearchParams({ foo: 'bar' });
axios.post('http://something.com/', params.toString());

您也可以使用 qs 库。

注意:如果需要对嵌套对象进行字符串化处理,则最好使用 qs 库,因为 querystring 方法在该用例中存在已知问题(querystring.stringify can't deal with nested objects · Issue #1665 · nodejs/node-v0.x-archive · GitHub)。

Form data

在 node.js, 您可以使用 form-data 库,如下所示:

const FormData = require('form-data');
 
const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));

axios.post('https://example.com', form, { headers: form.getHeaders() })

或者, 使用一个拦截器:

axios.interceptors.request.use(config => {
  if (config.data instanceof FormData) {
    Object.assign(config.headers, config.data.getHeaders());
  }
  return config;
});

自动序列化

当请求头中的 content-type 是 application/x-www-form-urlencoded 时,Axios 将自动地将普通对象序列化成 urlencoded 的格式。

在浏览器和 node.js 环境中都适用:

const data = {
  x: 1,
  arr: [1, 2, 3],
  arr2: [1, [2], 3],
  users: [{name: 'Peter', surname: 'Griffin'}, {name: 'Thomas', surname: 'Anderson'}],
};

await axios.post('https://postman-echo.com/post', data,
  {headers: {'content-type': 'application/x-www-form-urlencoded'}}
);

服务器接收到的数据就像是这样:

  {
    x: '1',
    'arr[]': [ '1', '2', '3' ],
    'arr2[0]': '1',
    'arr2[1][0]': '2',
    'arr2[2]': '3',
    'arr3[]': [ '1', '2', '3' ],
    'users[0][name]': 'Peter',
    'users[0][surname]': 'griffin',
    'users[1][name]': 'Thomas',
    'users[1][surname]': 'Anderson'
  }

如果您的服务器框架的请求体解析器(例如express.jsbody-parser)支持嵌套对象解码,则其接收到的数据将与您提交的数据一样。

以下是一个express.js的服务器示例,它将会把接收到的数据作为响应返回:

  var app = express();
  
  app.use(bodyParser.urlencoded({ extended: true })); // support url-encoded bodies
  
  app.post('/', function (req, res, next) {
     res.send(JSON.stringify(req.body));
  });

  server = app.listen(3000);

Multipart 实体 请求

使用 multipart/form-data 类型发起 POST 请求

使用 FormData API

浏览器

const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Blob([1,2,3]));
form.append('my_file', fileInput.files[0]);

axios.post('https://example.com', form)

Axios 会将传入数据序列化,因此使用 Axios 提供的 API 可以无需手动处理 FormData 的数据并实现一样的效果:

axios.postForm('https://httpbin.org/post', {
  my_field: 'my value',
  my_buffer: new Blob([1,2,3]),
  my_file:  fileInput.files // FileList will be unwrapped as sepate fields
});

HTML 表单可以直接作为请求内容来进行传输。

Node.js

import axios from 'axios';

const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Blob(['some content']));

axios.post('https://example.com', form)

由于 node.js 当前不支持从文件创建 Blob,因此您可以使用第三方软件包来实现该目的。

import {fileFromPath} from 'formdata-node/file-from-path'

form.append('my_field', 'my value');
form.append('my_file', await fileFromPath('/foo/bar.jpg'));

axios.post('https://example.com', form)

当 Axios 版本小于 v1.3.0 时您必须引入 form-data 包。

const FormData = require('form-data');

const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));

axios.post('https://example.com', form)

自动序列化

从 v0.27.0 版本开始,当请求头中的 Content-Type 是 multipart/form-data 时,Axios 支持自动地将普通对象序列化成一个 FormData 对象。

这个示例请求演示了如何将一个数据通过 FormData 格式进行提交(浏览器与 Node.js 环境):

import axios from 'axios';

axios.post('https://httpbin.org/post', {
  user: {
    name: 'Dmitriy'
  },
  file: fs.createReadStream('/foo/bar.jpg')
}, {
  headers: {
    'Content-Type': 'multipart/form-data'
  }
}).then(({data})=> console.log(data));

Axios FormData 序列化器支持一些特殊的结尾,以执行以下操作:

  • {} - 通过 JSON.stringify 序列化数据
  • [] - 将 array-like 的对象使用相同的键值来展开为单独的字段

提示:默认情况下,展开、扩展操作将在数组和 FileList 对象上使用。

FormData 序列化器支持通过 config.formSerializer: object 这个参数来传递一些额外的选项,以支持一些特殊的情况:

  • visitor: Function - 用户定义的处理函数,将递归调用以按照自定义规则将数据对象序列化为FormData对象。
  • dots: boolean = false - 使用点符号而不是括号来序列化数组和对象;
  • metaTokens: boolean = true - 在 FormData 键值中添加特殊结尾(例如user{}: '{"name": "John"}')。后端的 body-parser 可能会使用此元信息自动将值解析为 JSON。
  • indexes: null|false|true = false - 控制如何添加索引到打平的 array-like 对象的展开键值中
    • null - 不添加中括号(arr: 1arr: 2arr: 3
    • false(默认值)- 添加空中括号(arr[]: 1arr[]: 2arr[]: 3
    • true - 添加带有索引的中括号(arr[0]: 1arr[1]: 2arr[2]: 3

假设说我们有一个这样的示例对象:

const obj = {
  x: 1,
  arr: [1, 2, 3],
  arr2: [1, [2], 3],
  users: [{name: 'Peter', surname: 'Griffin'}, {name: 'Thomas', surname: 'Anderson'}],
  'obj2{}': [{x:1}]
};

接下来这些序列化的步骤将会由 Axios 内置的序列化器自动执行:

const formData= new FormData();
formData.append('x', '1');
formData.append('arr[]', '1');
formData.append('arr[]', '2');
formData.append('arr[]', '3');
formData.append('arr2[0]', '1');
formData.append('arr2[1][0]', '2');
formData.append('arr2[2]', '3');
formData.append('users[0][name]', 'Peter');
formData.append('users[0][surname]', 'Griffin');
formData.append('users[1][name]', 'Thomas');
formData.append('users[1][surname]', 'Anderson');
formData.append('obj2{}', '[{"x":1}]');
import axios from 'axios';

axios.post('https://httpbin.org/post', {
  'myObj{}': {x: 1, s: "foo"},
  'files[]': document.querySelector('#fileInput').files 
}, {
  headers: {
    'Content-Type': 'multipart/form-data'
  }
}).then(({data})=> console.log(data));

Axios支持以下别名方法:postFormputFormpatchForm,这些方法只是对应的 HTTP 方法,其 content-type 头部默认设为multipart/form-data

FileList 对象可以被直接传递:

await axios.postForm('https://httpbin.org/post', document.querySelector('#fileInput').files)

所有文件将使用相同的字段名files[]发送。

框架 整合

  1. vue-axios

  2. react-axios

  3. nuxtjs-axios

插 件

  1. axios-retry

  2. vue-axios-plugin

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2107510.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++ 第二十章】智能指针

1.为什么需要智能指针&#xff1f; 下面我们先分析一下下面这段程序有没有什么内存方面的问题&#xff1f;提示一下&#xff1a;注意分析下面 Func 函数中的问题。 #include<exception> int div() {int a, b;cin >> a >> b;if (b 0)throw invalid_argume…

【Python基础】这篇文章带你了解Python的基本特点,让学习Python变得事半功倍!!!

一、Python的基本特点 简单易学&#xff1a;Python语法简洁清晰&#xff0c;拥有极其简单的说明文档&#xff0c;对于初学者来说非常友好。面向对象&#xff1a;Python既支持面向过程的编程也支持面向对象的编程&#xff0c;这使得Python能够灵活地应对各种编程需求。可移植性…

投放Facebook广告开户全流程解析:从开户到广告投放的实用指南

Facebook作为全球最大的社交平台之一&#xff0c;广告业务覆盖范围广泛&#xff0c;已成为各类企业推广产品和服务的重要渠道。要在Facebook上成功投放广告&#xff0c;首先需要完成广告账户的开户流程。本文将详细介绍投放Facebook广告开户的步骤和条件&#xff0c;并解释如何…

VBA Excel 出报表

源数据 目标 PS:调休 以高亮颜色区分 整理一下 CMDBUT命令 VBA代码 Private Sub CommandButton1_Click() Dim ps As Integer Dim pe As Integer Dim srcs As Integer Dim srce As Integer Dim i As Integer Dim j As Integer Dim m As Integer Dim pname As Variant Dim pn…

力扣刷题--442. 数组中重复的数据【中等】

题目描述 给你一个长度为 n 的整数数组 nums &#xff0c;其中 nums 的所有整数都在范围 [1, n] 内&#xff0c;且每个整数出现 一次 或 两次 。请你找出所有出现 两次 的整数&#xff0c;并以数组形式返回。 你必须设计并实现一个时间复杂度为 O(n) 且仅使用常量额外空间&am…

【深度学习】线性回归的从零开始实现与简洁实现

前言 我原本后面打算用李沐老师那本《动手学深度学习》继续“抄书”&#xff0c;他们团队也免费提供了电子版(https://zh-v2.d2l.ai/d2l-zh-pytorch.pdf)。但书里涉及到代码&#xff0c;一方面展示起来不太方便&#xff0c;另一方面我自己也有很多地方看不太懂。 这让我开始思…

栈和队列的习题详解(2):用队列实现栈

前言&#xff1a; 小编在上一篇博客写了栈和队列其中一个习题&#xff0c;为了体现出题目的重要性所以我把每个题目都分开写了&#xff0c;下面废话不多说&#xff0c;开启我们今天的做题之旅~ 目录 1.用队列实现栈 1.1.题目介绍 1.2.做题方法介绍 1.3.栈功能的实现 1.3.1.…

天聚数行®近期上线了六个实用的API接口

天聚数行近期上线了一系列实用的API接口服务&#xff0c;涵盖了多种场景下的数据处理和信息查询的需求&#xff0c;为企业和开发者带来了便捷高效的工具支持。这些服务包括工商信息查询、手机状态检测&#xff08;如在网状态和空号检测&#xff09;、坐标系转换等功能&#xff…

飞利浦的精益转型之路:从传统制造到智能制造的华丽蜕变

飞利浦作为一家拥有百年历史的全球知名品牌&#xff0c;其在精益转型方面的经验值得我们深入研究和借鉴。本文将从飞利浦的转型背景、转型过程、转型成效以及给我们的启示等方面&#xff0c;探讨飞利浦如何成功实现精益转型&#xff0c;从而在新的市场竞争中脱颖而出。 一、转型…

沐渥科技:两显氮气柜和三显氮气柜要怎么选择?

两显氮气柜通常指的是控制面板上有两个LED数码显示界面&#xff0c;用于显示温度和湿度&#xff1b;三显氮气柜则有三个LED数码显示界面&#xff0c;能够直观地显示出温度、湿度和含氧量。这样的设计便于用户快速全面地了解柜内环境状态&#xff0c;不需要额外的操作即可掌握所…

录屏神器!这四款免费版助你轻松成为剪辑大师

在数字化的时代&#xff0c;录屏软件已经成为了我们工作和学习中的得力助手。对于需要记录电脑屏幕操作&#xff0c;或是制作教学视频、游戏解说等内容的用户来说&#xff0c;一款好用的录屏软件是必不可少的&#xff1b;这篇文章将分享四款免费录屏软件&#xff1a; 第一款&a…

深度学习(九)-图像形态操作

仿射变换 仿射变换是指图像可以通过一系列的几何变换来实现平移、旋转等多种操作。该变换能够保持图像的平直性和平行性。平直性是指图像经过仿射变换后&#xff0c;直线仍然是直线&#xff1b;平行性是指图像在完成仿射变换后&#xff0c;平行线仍然是平行线。 平移 镜像 旋转…

spark读取csv文件

测试spark读取本地和hdfs文件 from pyspark.sql import SparkSessionspark SparkSession.builder \.appName("Example PySpark Script") \.getOrCreate()# 读取本地csv文件 df spark.read.csv("/Users/xiaokkk/Desktop/local_projects/spark/intents.csv&quo…

三诺动态血糖管理在医院中的应用

一、引言 随着糖尿病患病率的逐年上升&#xff0c;糖尿病管理已成为医疗体系中的重要挑战。特别是在医院环境中&#xff0c;针对重症及需要精细化治疗的患者&#xff0c;动态血糖管理显得尤为重要。传统血糖监测手段的局限性日益凸显&#xff0c;而三诺生物传感股份有限公司&am…

国产航顺HK32F030M:WS2812 炫彩LED灯驱动笔记(C51/STM32/HK32)

WS2812B参数 3528 幻彩雾状 贴片式发光二极管 XL-3528RGBW-WS2812B

批处理常用指令与脚本的例子

另&#xff1a;win7扩展名显示 一、常用指令 1、REM 和:: 2、ECHO和 3、PAUSE 4、ERRORLEVEL 5、TITLE 6、COLOR 7、mode 配置系统设备 8、GOTO和: 9、FIND 10、START 11、assoc和 ftype 12、pushd和 popd 13、CALL 14、shift 15、IF 16、setlocal 与变量延迟…

Ethernet 系列(2)-- 物理层测试::IOP Test::Signal Quality

车载以太网物理层IOP测试&#xff0c;即互操作性测试&#xff08;Interop- erability Tests&#xff09;&#xff0c;用于验证车载以太网PHY&#xff08;通常也称为收发器&#xff09;的可靠性和检查PHY能否在给定的有限时间内建立稳定的链路;还用于车载以太网PHY的诊断&#x…

利用SSH加密实现的HTTP隧道分析与检测

1.隧道介绍 Chisel是一个快速稳定的TCP/UDP隧道工具&#xff0c;该工具基于HTTP实现&#xff0c;并通过SSH加密保证通信安全。Chisel可以进行端口转发、反向端口转发以及SOCKS流量代理&#xff0c;使用GO语言编写&#xff0c;具备较好的跨平台特性。该工具的主要用于绕过防火墙…

已过售后期客服话术分享

在电商运营中&#xff0c;我们经常会遇到一些过了售后期还来找客服的买家。他们的问题五花八门&#xff0c;有的真的是质量问题&#xff0c;有的则可能是使用不当或者其他原因导致的。面对这样的情况&#xff0c;我们应该如何正确处理呢&#xff1f;下面给大家分享一些技巧和已…

构建高效医护人员排班系统:Spring Boot框架的优势

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理医护人员排班系统的相关信息成为必然。开发…