JavaScript 笔记(十):网络编程

JavaScript 笔记(十):网络编程

SessionStorage / LocalStorage

简介

在 jQuery 笔记中,已经介绍了 Cookie,在 HTML5 中,新增了 SessionStorage 和 LocalStorage,都用于存储数据,在生存周期、容量以及网络请求等方面存在差异:

存储类型生存周期容量网络请求应用场景
Cookie默认在关闭浏览器后自动清除,可以设置过期时间20 ~ 50 个左右、大约 4KB在与服务器通信时,Cookie 将保存在 HTTP 请求头中(如果 Cookie 数量过多将存在性能问题)判断用户是否登录
SessionStorage在关闭窗口或浏览器后自动清除,不可以设置过期时间无个数限制,大约 5M不参与服务器通信表单数据
LocalStorage在浏览器中永久保存,除非手动清空无个数限制,大约 5M不参与服务器通信购物车

无论使用上述哪种方式存储数据,切记不能将敏感数据直接存储到本地

基本使用

SessionStorage 和 LocalStorage 的使用方式相同,即调用方法名称均相同,区别在于调用实例的不同,示例如下:

/* sessionStorage */
sessionStorage.setItem("key", "value");
sessionStorage.removeItem("key");
sessionStorage.getItem("key");
sessionStorage.setItem("key", "new value");
sessionStorage.clear();

/* localStorage */
localStorage.setItem("key", "value");
localStorage.removeItem("key");
localStorage.getItem("key");
localStorage.setItem("key", "new value");
localStorage.clear();

同源策略

同源策略(Same Origin Policy)是一种约定,是浏览器最基本也是最核心的安全功能,所谓同源指的是协议、域名、端口号都相同,否则即为跨域,在使用同源策略时,存在如下问题:浏览器只允许 Ajax 请求同源数据,不允许请求非同源的数据,在企业开发中通常将网页和数据分别存储在不同的服务器上,此时如果使用 Ajax 并不能获取数据,因此必须使用跨域的解决方案 —— JSONP

JSONP

JSONP 是用于在网页中访问跨域地址中的数据的技术,即跨域访问数据,原理基于 HTML 中 script 标签的特性:

  • 在一个 HTML 中可以定义若干 script 标签
  • 同一个 HTML 中的不同 script 标签中的数据可以相互访问
  • 使用 script 标签中的 src 属性可以将另外地址(同源/跨域)中的 js 代码拷贝到标签中
  • script 标签的 src 属性并没有同源限制,所以可以跨域访问资源

示例如下:

/* http://127.0.0.1:80/network/nosop.php */

<?php
echo "let num = 1024;";
?>
<!-- http://127.0.0.1:52330/network/nosop.html -->

<script src="http://127.0.0.1:80/network/nosop.php"></script>
<script>
    console.log(num);   // 1024
</script>

在上述示例中,由于 HTML 与 PHP 的端口号不同,所以是跨域的

在开发过程中,通常使用 JSONP 获取一个函数调用,而非一个变量,示例如下:

/* http://127.0.0.1:80/network/nosop.php */

<?php
echo "demo(1024);";
?>
<!-- http://127.0.0.1:52330/network/nosop.html -->

<script>
    function demo(num) {
        console.log(num);
    }
</script>
<script src="http://127.0.0.1:80/network/nosop.php"></script>

在上述示例中,将函数名称写死了,我们可以通过 GET 在地址之后传递参数,示例如下:

/* http://127.0.0.1:80/network/nosop.php */

<?php
$CB = $_GET["CB"];
echo $CB."(1024);";
?>
<!-- http://127.0.0.1:52330/network/nosop.html -->

<script>
    function reyn(num) {
        console.log(num);
    }
</script>
<script src="http://127.0.0.1:80/network/nosop.php?CB=reyn"></script>

由于 script 标签默认是同步的,即 script 是顺序执行的,如果将获取资源的 script 标签置于访问资源的 script 标签之前,那么将访问失败,因为在访问资源(调用函数)时函数并没有被定义,但我们可以以动态添加 script 标签的方式来获取资源,此时的 script 标签默认是异步的,即并不需要等到资源获取完成后才可以定义函数,示例如下:

<!-- http://127.0.0.1:52330/network/nosop.html -->

<script>
    let oScript = document.createElement("script");
    oScript.src = "http://127.0.0.1:80/network/nosop.php?CB=reyn";
    document.body.appendChild(oScript);

    function reyn(num) {
        console.log(num);
    }
</script>

在 jQuery 中,可以通过 ajax 使用 JSONP 来获取数据,示例如下:

/* http://127.0.0.1:80/network/jquery-ajax-jsonp.php */

$CB = $_GET("CB");  // $CB = fnc
echo $CB."(1024);"; // fnc(1024);
/* http://127.0.0.1:52330/network/nosop.html */

$.ajax({
    url: "http://127.0.0.1:80/network/jquery-ajax-jsonp.php",
    dataType: "jsonp",  // ajax 使用 JSONP
    // jsonp: "CB",    // 添加在 url 之后回调函数的 key
    // jsonpCallback: "fnc",   // 添加在 url 之后回调函数的 value -> http://...?CB=fnc
    success: function (msg) {
        /* 在服务器(PHP)使用回调函数传入的参数即为 msg */
        console.log(msg);   // 1024
    }
});

在 jQuery 中的 JSONP 也可以传入数据,示例如下:

/* http://127.0.0.1:80/network/jquery-ajax-jsonp.php */

<?php
// get data
$nam = $_GET['name'];
$age = $_GET['age'];

// create obj
$obj = array("name"=>$nam, "age"=>$age);

// obj -> json
$data = json_encode($obj);

$CB = $_GET["CB"];	// $CB = fnc
echo $CB."(".$data.");";	// fnc(json-data)
?>
/* http://127.0.0.1:52330/network/nosop.html */

$.ajax({
    url: "http://127.0.0.1:80/network/jquery-ajax-jsonp.php",
    data: { name: "reyn", age: 22 },
    dataType: "jsonp",
    jsonp: "CB",
    jsonpCallback: "fnc",
    success: function (msg) {
        console.log(msg);
    }
});

在 jQuery 中 JSONP 的原理如下所示:

function obj2str(obj) {
    obj._ = String(Math.random()).replace(".", ""); // 随机因子
    let arr = new Array();
    for (let key in obj) {
        arr.push(`${key}=${encodeURI(obj[key])}`);
    }
    return arr.join("&");
}

function myJSONP(options) {
    /* url => options.url */
    let requestURL = options.url;

    /* jsonp */
    if (options.jsonp) {
        requestURL += `?${options.jsonp}=`;
    } else {
        requestURL += "?callback=";
    }

    /* jsonpCallback */
    let callbackName = `myJSONP${String(Math.random()).replace(".", "")}`;
    if (options.jsonpCallback) {
        callbackName = options.jsonpCallback;
    }
    requestURL += callbackName;

    /* data */
    if (options.data) {
        let dataStr = obj2str(options.data);
        requestURL += `&${dataStr}`;
    }

    /* callback */
    let oScript = document.createElement("script");
    oScript.src = requestURL;
    document.body.appendChild(oScript);

    window[callbackName] = function (data) {
        document.body.removeChild(oScript);
        options.success(data);
    }
}

在上述示例中,随机因子的目的在于解决缓存问题,如下示例演示了如何使用此方法:

/* http://localhost:52330/code/myJSONP.html */

window.onload = function() {
    myJSONP({
        url: "http://127.0.0.1:80/network/jquery-ajax-jsonp.php",
        data: { name: "reyn", age: 22 },
        jsonp: "CB",
        jsonpCallback: "fnc",
        success: function (msg) {
            console.log(msg);
        }
    });
}

综上所述,JSONP 的本质是一次函数调用,在此函数调用中,函数体在本地的 HTML 的 script 标签中定义,函数的参数从服务器传入,函数调用则在将函数调用的代码从 script 标签的 src 属性指定的 url 中拷贝到本地后调用,在 JS6 以后,函数调用必须在函数定义之后,所以应当将函数调用的 script 标签置于函数定义之后,也可以使用 DOM 动态创建 script 标签,如此一来可以将 script 置于函数定义之前

Promise

在 JavaScript 中,介绍了静态数据和动态数据,静态数据是指存储在硬盘上的数据,动态数据是指存储在内存中的数据,二者之间可以相互转换,当从硬盘上读取数据到内存中时,即将静态数据转转换为动态数据,当将内存中的数据写入到磁盘时,即将动态数据转换为静态数据

程序通常是存储在磁盘上的二进制文件(静态数据),当执行程序时,系统将在内存中创建一个此程序对应的进程(动态数据),在创建进程时,系统将自动在进程中创建一个线程,此线程即为主线程,此外,在系统中可以同时执行一个程序,此时,在系统中将同时存在多个与此程序对应的进程,而在进程中除了主线程之外,也可以创建若干其它线程,总而言之,一个程序可以对应多个进程,而一个进程可以对应多个线程

如果此时有若干代码块,且在一段时间内以代码块的先后顺序执行,此时称之为串行,若在同一个时刻同时执行若干代码块,此时称之为并行,如果程序是串行的,那么此时在进程中只存在一个主线程,如果程序是并行的,那么在一个进程中同时存在多个线程执行若干不同的代码块

在执行 JavaScript 代码时,在进程中只存在一个主线程,所以是串行的,不过结果似乎并不是如此,示例如下:

console.log("1");
setTimeout(function () {
    console.log("2");
}, 1000);
console.log("3");

在上述示例中,输出的顺序是 1 -> 3 -> 2,尽管如此,但 JavaScript 程序的执行依然是串行的,原因如下:

在 JavaScript 代码中,除了事件绑定的函数回调函数之外的所有代码均为同步代码,即在 JavaScript 中,只有事件绑定的函数和回调函数是异步代码,之所以区分同步代码和异步代码(不同于同步和异步的概念),是由于 JavaScript 解释器之于不同的代码有不同的处理方式,在程序从上至下(串行)执行时,如果遇到同步代码,那么立即执行,如果遇到异步代码,那么解释器将异步代码添加到系统自动创建的数组中,当所有代码执行被执行完毕后,系统将在所有代码的末尾执行一个死循环,在此循环中不停遍历由系统创建的存储异步代码的数组,当事件触发或满足一定条件时,立即执行相应的事件函数或回调函数,此循环被称为事件循环,示例如下:

console.log("1");
setTimeout(function () {
    console.log("2");
}, 1000);
console.log("3");
alert("Hello World");

在上述示例中,从上至下串行执行代码时,如果并没有在一秒钟之内确认警告,那么程序将不会输出定时器中的内容,直到警告被确认,原因在于除了定时器中的回调函数之外,所有代码均为同步代码,系统在执行完毕所有代码之后才会执行事件循环,而由于警告一直没有被确认,那么 alert 相当于没有执行完毕,所以系统并不会执行事件循环,由此即可说明 JavaScript 代码是串行执行的

基本概念

现在我们实现使用异步代码顺序输出内容,示例如下:

function outputByOrder(callback) {
    setTimeout(() => {
        callback("Hello World");
    }, 1000);
}

outputByOrder(function (data) {
    console.log(data, 1);   // Hello World 1
    outputByOrder(function (data) {
        console.log(data, 2);   // Hello World 2
        outputByOrder(function (data) {
            console.log(data, 3);   // Hello World 3
        });
    });
});

在上述示例中,必须将函数调用以嵌套的方式组织,否则不能实现顺序输出,而是同时输出,如果试图理解上述代码,那么难度或许并不亚于理解一段递归代码

在 JS 中如果必须使用异步代码,且必须保证异步代码的顺序执行,那么往往使用嵌套的形式编写代码,但如此编写的代码可读性差,不容易维护,因此在 JS6 中新增了 Promise 异步编程解决方案,在程序中以 Promise 实例的形式体现,Promise 实例可以将异步代码以同步流程的形式编写,避免了回调函数层层嵌套的情形,示例如下:

function outputByOrder() {
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            resolve("Hello World");
        }, 1000);
    });
}

outputByOrder().then(function (data) {
    console.log(data, 1);
    return outputByOrder();
}).then(function (data) {
    console.log(data, 2);
    return outputByOrder();
}).then(function (data) {
    console.log(data, 3);
});

基本使用

在 JavaScript 中,Promise 以实例的形式体现,示例如下:

let promise = new Promise(function (resolve, reject) { });

在上述示例中,演示了如何创建一个 Promise 实例,必须明确的是 Promise 实例本身不是异步的,如果创建了 Promise 实例,那么将立即执行回调函数中的代码,示例如下:

console.log("Before Promise");
let promise = new Promise(function (resolve, reject) {
    console.log("Promise Instance");
});
console.log("After Promise");

JavaScript 中的 Promise 实例通过切换状态来实现同步流程表示异步代码,在 Promise 中存在 3 种状态,分别是 pending(默认)、resolved 和 rejected

在默认情况下,Promise 实例中的状态为 pending,即如果未告诉 Promise 实例任务成功或失败,那么状态即为 pending,反之,如果通过调用 resolve 函数告诉 Promise 实例任务成功,那么实例状态将被切换为 resolved,如果通过调用 reject 函数告诉 Promise 实例任务失败,那么实例状态将被切换为 rejected

Promise 的状态一旦被改变,那么将是不可逆的,即如果状态从 pending 到 resolved 或 rejected,那么状态不能从 resolved 或 rejected 到 pending,并且 resolved 和 rejected 状态也不能相互转换

在 Promise 实例中存在 2 个方法,分别是 then 和 catch,当 Promise 实例的状态被切换为 resolved 时,将执行 then 中的回调函数,当 Promise 实例的状态被切换为 rejected 时,将执行 catch 中的回调函数,示例如下:

let promise = new Promise(function (resolve, reject) {
    // resolve();   // Resolve Callback
    reject();   // Reject Callback
});

promise.then(function () {
    console.log("Resolve Callback");
});

promise.catch(function () {
    console.log("Reject Callback");
});
then 实例方法

在 Promise 实例的 then 方法中存在诸多特性,内容如下:

then 方法可以传入 2 个参数(回调函数),当 Promise 实例的状态切换为成功时,调用第一个回调函数,当 Promise 实例的状态切换为失败时,调用第二个回调函数,示例如下:

let promise = new Promise(function (resolve, reject) {
    // resolve();   // Success
    reject();   // Error
})

promise.then(function () {
    console.log("Success");
}, function () {
    console.log("Error");
});

在切换 Promise 实例状态时,可以通过 resolve 或 reject 传入参数到 then 方法的 2 个回调函数中,示例如下:

let promise = new Promise(function (resolve, reject) {
    // resolve("Resolve");  // Success Resolve
    reject("Reject");   // Error Reject
})

promise.then(function (info) {
    console.log("Success", info);
}, function (info) {
    console.log("Error", info);
});

同一个 Promise 实例可以多次调用 then 方法,当此实例的状态切换时,将调用此实例所调用的所有 then 方法中相应的回调函数,示例如下:

let firstPromise = new Promise(function (resolve, reject) {
    // resolve("Resolve"); // Success Resolve \ Victory Resolve
    reject("Reject");   // Error Reject \ Failed Reject
})

firstPromise.then(function (info) {
    console.log("Success", info);
}, function (info) {
    console.log("Error", info);
});

firstPromise.then(function (info) {
    console.log("Victory", info);
}, function (info) {
    console.log("Failed", info);
})

在一个 Promise 实例的 then 方法中的回调函数执行之后,将自动返回一个新的 Promise 实例,新实例的状态继承自调用 then 方法的实例,示例如下:

let firstPromise = new Promise(function (resolve, reject) {
    resolve("Resolve"); // Success Resolve
})

let secondPromise = firstPromise.then(function (data) {
    console.log("Success", data);
}, function (data) {
    console.log("Error", data);
});

console.log(firstPromise);  // fulfilled
console.log(secondPromise); // fulfilled
console.log(firstPromise === secondPromise);    // false

在上述示例中,实例 secondPromise 的 PromiseState 的值为 fulfilled,fulfilled 即为 resolved,此时实例 firstPromise 的状态亦为 fulfilled

如果在一个 Promise 实例的 then 方法中存在返回值,那么此返回值将作为新 Promise 实例的 then 方法的参数,示例如下:

let firstPromise = new Promise(function (resolve, reject) {
    resolve("Resolve"); // Success Resolve \ Victory Sub Success
})

let secondPromise = firstPromise.then(function (info) {
    console.log("Success", info);
    return "Sub Success";
}, function (info) {
    console.log("Error", info);
    return "Sub Error";
});

secondPromise.then(function (info) {
    console.log("Victory", info);
}, function (info) {
    console.log("Failed", info);
});

必须明确的是,不论上一个 Promise 实例是由于 resolve 或是 reject 调用 then 方法,方法的返回值都将传入到下一个 Promise 实例在成功时所执行的 then 方法中的回调函数,示例如下:

let firstPromise = new Promise(function (resolve, reject) {
    reject("Reject");   // Error Reject \ Victory Sub Error
})

let secondPromise = firstPromise.then(function (info) {
    console.log("Success", info);
    return "Sub Success";
}, function (info) {
    console.log("Error", info);
    return "Sub Error";
});

secondPromise.then(function (info) {
    console.log("Victory", info);   /* * */
}, function (info) {
    console.log("Failed", info);
});

如果在 then 方法中的返回值是一个 Promise 实例,那么下一个 Promise 实例中 then 方法的执行情况取决于被返回的 Promise 实例,包括状态和参数,示例如下:

let firstPromise = new Promise(function (resolve, reject) {
    resolve("Resolve");
})

let bridgePromise = new Promise(function (resolve, reject) {
    // resolve("Bridge Resolve"); // Success Resolve \ Victory Bridge Resolve
    reject("Bridge Reject");    // Success Resolve \ Failed Bridge Reject
})

let secondPromise = firstPromise.then(function (info) {
    console.log("Success", info);
    return bridgePromise;
}, function (info) {
    console.log("Error", info);
    return bridgePromise;
});

secondPromise.then(function (info) {
    console.log("Victory", info);
}, function (info) {
    console.log("Failed", info);
});
catch 实例方法

catch 方法是 then 方法的语法糖,即如下形式:

then(undefined, () => {})

目的在于表示 then 的 Promise 实例在失败状态且不必监听成功状态时的回调函数,示例如下:

let promise = new Promise(function (resolve, reject) {
    // resolve();  // Success
    reject();   // Error
})

promise.then(function () {
    console.log("Success");
}).catch(function () {
    console.log("Error");
});

在上述示例中,必须使用链式编程,否则在 Promise 实例是失败状态时将出现错误,示例如下:

let promise = new Promise(function (resolve, reject) {
    reject();   // Error \ Uncaught (in promise) undefine
})

promise.then(function () {
    console.log("Success");
});

promise.catch(function () {
    console.log("Error");
});

之所以出现上述示例中的情况,原因在于如果不存在回调函数监听 Promise 实例的失败状态,那么将出现错误,示例如下:

let promise = new Promise(function (resolve, reject) {
    reject();   // Uncaught (in promise) undefine
})

在上述示例中,若 Promise 实例处于默认状态或成功状态,那么将不会出现错误

根据此结论,再根据 then 方法的特性,由于在 then 方法的任意回调函数执行完毕,将返回一个新的 Promise 实例,此实例继承了调用 then 方法的 Promise 实例的状态,若 Promise 实例处于失败状态,即使存在监听 Promise 实例的回调函数,但新的 Promise 实例的失败状态并未被监听,如果我们可以监听新 Promise 实例的失败状态,那么可以避免错误,示例如下:

let firstPromise = new Promise(function (resolve, reject) {
    reject();   // Error \ Sub Error
});

let secondPromise = firstPromise.then(function () {
    console.log("Success");
});

firstPromise.catch(function () {
    console.log("Error");
});

secondPromise.catch(function () {
    console.log("Sub Error");
});

在上述示例中,也可以明白为什么使用 catch 方法时必须使用链式编程,即将原 Promise 实例的失败状态传递给由 then 方法返回的新 Promise 实例,并监听新 Promise 实例的失败状态

此时,如果我们将原 Promise 实例的 catch 方法删除,也不会出现错误,示例如下:

let firstPromise = new Promise(function (resolve, reject) {
    reject();   // Sub Error
});

let secondPromise = firstPromise.then(function () {
    console.log("Success");
});

secondPromise.catch(function () {
    console.log("Sub Error");
});

在上述示例中,我们可以得出一个结论,即任意 Promise 实例的失败状态可以不被自己监听,但必须存在 Promise 实例的 catch 方法监听,此 Promise 实例通常是由原 Promise 实例的 then 方法返回的新 Promise 实例,由此产生的新 Promise 实例将继承原 Promise 实例的状态从而及时响应,所以在开发中,如果使用 catch,那么推荐使用链式编程

Promise 实例的 catch 方法的绝大部分特性与 then 方法相同,区别在于,catch 方法可以捕获异常,示例如下:

let promise = new Promise((resolve, reject) => {
    resolve();  // Success \ Error ReferenceError: reyn is not defined
});

promise.then(function () {
    console.log("Success");
    reyn;
}).catch(function (err) {
    console.log("Error", err);
});

在上述示例中,ReferenceError 是参数 err 所捕获的异常信息

all 静态方法

在 Promise 类中存在一个称为 all 的静态方法,此方法传入以 Promise 实例为元素的数组,在所有 Promise 实例均处于成功状态时调用 then 成功的回调函数,如果有一个 Promise 实例处于失败状态,那么将调用 then 失败的回调函数,在调用成功的回调函数时,将所有实例的参数都保存到数组中并将数组返回,示例如下:

let firstPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("First");
    }, 1000);
});

let secondPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Second");
    }, 3000);
});

let thirdPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Third");
    }, 2000);
});

let promiseArray = [
    firstPromise,
    secondPromise,
    thirdPromise
];

Promise.all(promiseArray)
    .then(function (result) {
        console.log("Success", result);
    })
    .catch(function (err) {
        console.log("Error", err);
    });

如果传入数组中保存的不是 Promise 实例,那么将执行 then 成功的回调函数,Promise 类的 all 静态方法经常用于批量加载,不强调顺序,所有内容加载成功才加载,若有内容加载失败则不加载,此外,不使用 all 静态方法也可以实现无序加载,加载成功的内容加载,加载失败的内容不加载,示例如下:

let imagePaths = [
    "./images/HTML5.jpg",
    "./images/CSS3.jpg",
    "./images/JavaScript.jpg"
];

function loadImage(url) {
    return new Promise(function (resolve, reject) {
        let oImg = new Image();
        let loadTime = Math.random() * 1000;
        setTimeout(() => {
            oImg.src = url;
        }, loadTime);
        oImg.onload = function () {
            resolve(oImg);
        }
        oImg.onerror = function () {
            reject("Load Image Error");
        }
    });
}

for (let i = 0; i < imagePaths.length; i++) {
    loadImage(imagePaths[i]).then((result) => {
        document.body.appendChild(result);
    }).catch((err) => {
        console.log(err);
    });
}

如果传入数组中保存的不是 Promise 实例,那么将执行 then 成功的回调函数,Promise 类的 race 静态方法经常用于超时处理,示例如下:

let imagePath = "./images/HTML5.jpg";

function loadImage(url) {
    return new Promise((resolve, reject) => {
        let oImg = new Image();
        setTimeout(() => {
            oImg.src = url;
        }, 5000);
        oImg.onload = function () {
            resolve(oImg);
        };
        oImg.onerror = function () {
            reject("Load Error");
        };
    });
}

function timeout() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject("Timeout");
        }, 3000);
    });
}

Promise.race([loadImage(imagePath), timeout()])
    .then(function (result) {
        console.log(result);
    })
    .catch(function (err) {
        console.log(err);
    });
race 静态方法

在 Promise 类中存在一个称为 race 的静态方法,此方法传入以 Promise 实例为元素的数组,调用 then 成功或失败的回调函数取决于 Promise 实例数组中最快响应的 Promise 实例的结果,之后的实例将被抛弃,示例如下:

let firstPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("First");
    }, 1000);
});

let secondPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("Second");
    }, 3000);
});

let thirdPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("Third");
    }, 2000);
});

let promiseArray = [
    firstPromise,
    secondPromise,
    thirdPromise
];

Promise.race(promiseArray)
    .then(function (result) {
        console.log(result);
    })
    .catch(function (err) {
        console.log(err);
    });

实现原理

在 JavaScript 中 Promise 类的原理如下:

class MyPromise {
    constructor(handle) {
        if (!this._isFunction(handle)) {
            throw Error("Argument Is Not A Funtion");
        }

        this.promiseState = MyPromise.PENDING;

        this.result = undefined;
        this.err = undefined;

        this.onResolvedCallbacks = new Array();
        this.onRejectedCallbacks = new Array();

        handle(this._resolve.bind(this), this._reject.bind(this));
    }

    _resolve(result) {
        if (this.promiseState === MyPromise.PENDING) {
            this.promiseState = MyPromise.FULFILLED;
            this.result = result;
            this.onResolvedCallbacks.forEach(fn => fn(this.result));
        }
    }

    _reject(err) {
        if (this.promiseState === MyPromise.PENDING) {
            this.promiseState = MyPromise.REJECTED;
            this.err = err;
            this.onRejectedCallbacks.forEach(fn => fn(this.err));
        }
    }

    _isFunction(arg) {
        return typeof arg === "function";
    }

    then(onResolved, onRejected) {
        if (!this._isFunction(onResolved) || !this._isFunction(onRejected)) {
            throw Error("One Of Arguments Is Not A Function");
        }

        return new MyPromise((nextResolve, nextReject) => {
            if (this.promiseState === MyPromise.FULFILLED) {
                try {
                    let nextArg = onResolved(this.result);
                    if (nextArg instanceof MyPromise) {
                        nextArg.then(nextResolve, nextReject);
                    } else if (nextArg !== undefined) {
                        nextResolve(nextArg);
                    } else {
                        nextResolve();
                    }
                } catch (error) {
                    nextReject(error);
                }
            }

            if (this.promiseState === MyPromise.REJECTED) {
                try {
                    let nextArg = onRejected(this.err);
                    if (nextArg instanceof MyPromise) {
                        nextArg.then(nextResolve, nextReject);
                    } else if (nextArg !== undefined) {
                        nextResolve(nextArg);
                    } else {
                        nextReject();
                    }
                } catch (error) {
                    nextReject(error);
                }
            }

            if (this.promiseState === MyPromise.PENDING) {
                this.onResolvedCallbacks.push((result) => {
                    try {
                        let nextArg = onResolved(result);
                        if (nextArg instanceof MyPromise) {
                            nextArg.then(nextResolve, nextReject);
                        } else if (nextArg !== undefined) {
                            nextResolve(nextArg);
                        } else {
                            nextResolve();
                        }
                    } catch (error) {
                        nextReject(error);
                    }
                });
                this.onRejectedCallbacks.push((err) => {
                    try {
                        let nextArg = onRejected(err);
                        if (nextArg instanceof MyPromise) {
                            nextArg.then(nextResolve, nextReject);
                        } else if (nextArg !== undefined) {
                            nextResolve(nextArg);
                        } else {
                            nextReject();
                        }
                    } catch (error) {
                        nextReject(error);
                    }
                });
            }
        });
    }

    catch(onRejected) {
        return this.then(undefined, onRejected);
    }

    static all(myPromiseArray) {
        return new MyPromise((resolve, reject) => {
            let successResults = new Array();
            let count = 0;
            for (let promise of myPromiseArray) {
                promise.then(function (result) {
                    successResults.push(result);
                    count++;
                    if (count === myPromiseArray.length) {
                        resolve(successResults);
                    }
                }, function (err) {
                    reject(err);
                });
            }
        });
    }

    static race(myPromiseArray) {
        return new MyPromise((resolve, reject) => {
            for (let promise of myPromiseArray) {
                promise.then(function (result) {
                    resolve(result);
                }, function (err) {
                    reject(err);
                })
            }
        });
    }
}

MyPromise.PENDING   = "pending";
MyPromise.FULFILLED = "fulfilled";
MyPromise.REJECTED  = "rejected";

在上述示例中,由于 then 方法中参数检查过于严格,所以无法使用 catch 方法链式编程,此外,每一个执行相应回调函数的代码块均实现了异常处理,在 Promise 实例的相应状态函数中如果出现错误,那么新 Promise 实例中响应错误状态的回调函数将自动捕获此异常

在 Promise 类的实现中,相信最难理解的部分一定是 then 方法的实现,由于 then 方法可以返回新的 Promise 实例,且 Promise 构造方法中的回调函数是立即执行的,所以可以将 then 方法的实现均置于新 Promise 实例的回调函数中,此外,不同时刻的不同状态也有不同的执行方法,如果在响应状态之前状态已经确定,那么可以立即调用 then 方法中相应状态的回调函数,如果在响应状态之前 Promise 实例处于默认状态,那么在后续状态切换时,也必须立即响应,所以将相应状态的回调函数保存在不同的数组中,当状态切换时,在状态切换方法中遍历相应状态的数组,从而实现以顺序调用保存的回调函数

异常

由于 JavaScript 是串行的,如果在执行 JS 代码时出现错误,那么系统将终止程序执行,如果导致程序运行出错的原因来自于用户,为了避免程序终止,可以使用 JavaScript 提供的异常捕获机制将错误捕获并处理,从而使程序继续运行,在 JavaScript 中使用 try catch 代码块,示例如下:

console.log("Reyn");    // Reyn
try {
    Exception
} catch (e) {
    console.log(e); // ReferenceError: Exception is not defined
}
console.log("Morales"); // Morales

在上述示例中,由于 Exception 并未被定义,因此将执行 catch 代码块中的语句,参数 e 的内容为错误信息,如果 try 代码块中的语句正常执行,那么将自动忽略 catch 代码块中的语句,示例如下:

let Exception = "BUG";
console.log("Reyn");    // Reyn
try {
    Exception
} catch (e) {
    console.log(e);
}
console.log("Morales"); // Morales

fetch

在 JavaScript 中,除了可以使用 Ajax 同源请求之外,也可以使用 fetch 请求资源,fetch 是基于 Promise 的一种资源请求方式,格式如下:

fetch(url, {
    // options
}).then(function (result) {

}).catch(function (err) {

})

get

示例如下:

fetch("http://127.0.0.1/network/fetch.php", {
    method: "get"
}).then(function (data) {
    return data.text(); // Promise
}).then(function (result) {
    console.log(result);    // Reyn Morales
}).catch(function (err) {
    console.log(err);
});

在上述示例中,参数 data 是一个 Response 实例,text 方法获取 Promise 实例,如果在 then 成功的回调函数中将 Promise 实例返回,那么下一个 then 成功的回调函数的参数即为资源

传入参数的 get 请求示例如下:

fetch("http://127.0.0.1/network/fetch.php?name=reyn&age=21", {
    method: "get"
}).then(function (data) {
    return data.json();
}).then(function (result) {
    console.log(result);    // {name: 'reyn', age: '21'}
}).catch(function (err) {
    console.log(err);
});

在上述示例中,在 url 的末尾传入参数,如果后端将数据以 json 格式返回,那么可以使用 Response 实例的 json 方法将数据以 object 格式返回,在下一个 then 成功的回调函数的参数即为 object 形式的资源,此外,text 方法和 json 方法仅可以使用一次,即使用 text 方法后,不能再使用 text 方法和 json 方法,json 方法亦然

post

示例如下:

fetch("http://127.0.0.1/network/fetch.php", {
    method: "post"
}).then(function (data) {
    return data.text(); // Promise
}).then(function (result) {
    console.log(result);    // Reyn Morales
}).catch(function (err) {
    console.log(err);
});

传入参数的 post 请求示例如下:

fetch("http://127.0.0.1/network/fetch.php", {
    method: "post",
    body: JSON.stringify({ name: "reyn", age: 21 })
}).then(function (data) {
    return data.json();
}).then(function (result) {
    console.log(result);    // {name: 'reyn', age: '21'}
}).catch(function (err) {
    console.log(err);
})

Axios

Axios 插件是一个基于 Promise 的 HTTP 请求库,可以在 Node.js 和浏览器中使用,将库导入至项目,示例如下:

<script src="./axios.js"></script>

get

示例如下:

axios.get("http://127.0.0.1/network/fetch.php")
    .then((result) => {
        console.log(result.data);   // Reyn Morales
        console.log(result.status); // 200
        console.log(result.statusText); // OK
    }).catch((err) => {
        console.log(err);
    });

传入参数的 get 请求示例如下:

axios.get("http://127.0.0.1/network/fetch.php?name=reyn&age=21")
    .then((result) => {
        console.log(result.data);   // {name: 'reyn', age: '21'}
        console.log(result.status); // 200
        console.log(result.statusText); // OK
    }).catch((err) => {
        console.log(err);
    });

post

示例如下:

axios.post("http://127.0.0.1/network/fetch.php")
    .then((result) => {
        console.log(result.data);   // Reyn Morales
        console.log(result.status); // 200
        console.log(result.statusText); // OK
    }).catch((err) => {
        console.log(err);
    });

传入参数的 post 请求示例如下:

axios.post("http://127.0.0.1/network/fetch.php?name=reyn&age=21", {
    name: "reyn",
    age: 21
}).then((result) => {
    console.log(result.data);   // {name: 'reyn', age: '21'}
    console.log(result.status); // 200
    console.log(result.statusText); // OK
}).catch((err) => {
    console.log(err);
});

默认值

Axios 的优势在于可以配置一系列默认值,从而提高开发效率,此处介绍 timeout 和 baseURL

timeout

示例如下:

axios.defaults.timeout = 1000;
axios.get("http://127.0.0.1/network/fetch.php")
    .then((result) => {
        console.log(result.data);
    }).catch((err) => {
        console.log(err);   // AxiosError Instance
    });

在上述示例中,将 axios 请求的超时时限设置为 1000 毫秒,如果在超时时限内请求资源失败,axios 将返回一个 AxiosError 实例,在此实例中详述了错误信息

baseURL

示例如下:

axios.defaults.baseURL = "http://127.0.0.1";
axios.get("/network/fetch.php")
    .then((result) => {
        console.log(result.data);   // Reyn Morales
    }).catch((err) => {
        console.log(err);
    });

在上述示例中,将 axios 请求的 IP 地址设置为 http://127.0.0.1,如此一来,在每一次请求时可以省略 IP 地址,此外,如果之后网络资源在另一台主机上,可以通过修改 baseURL 从而提高编程效率

Symbol

在 ES6 中新增了 Symbol 基本数据类型,Symbol 类型可以生成一个独一无二的值,此类型可以避免在扩展第三方库时由于属性或方法重名而覆盖库中原有的属性和方法,示例如下:

let obj = {
    name: "Reyn Morales",
    age: 21,
    say: function () {
        console.log("Hello World");
    }
}

console.log(obj.name);  // Reyn Morales
obj.say();  // Hello World

obj.name = "Steven Jobs";
obj.say = function () {
    console.log("World Hello");
}

console.log(obj.name);  // Steven Jobs
obj.say();  // World Hello

在上述示例中,由于为 obj 实例添加了 name 属性和 say 方法而覆盖了原有实例中相应的属性和方法,使用 Symbol 数据类型可以避免此情况,示例如下:

let name = Symbol("name");
let say = Symbol("say");
let obj = {
    [name]: "Reyn Morales",
    [say]: function () {
        console.log("Hello World");
    }
}

obj.name = "Steven Jobs";
obj.say = function () {
    console.log("World Hello");
}

console.log(obj);   // {name: 'Steven Jobs', Symbol(name): 'Reyn Morales', say: ƒ, Symbol(say): ƒ}

在上述示例中,使用 Symbol 时,不使用 new 运算符,括号中的内容用作 Symbol 的标记,以便区分 Symbol,无任何实际意义,此外,Symbol 类型有如下注意点:

  • 使用 Symbol 类型时,必须是 Symbol(),不使用 new 运算符
  • 在括号中传入的字符串是一个标记,无任何实际意义
  • 不能将 Symbol 类型的数据转换为数值类型,也不能参与任何运算
  • 如果使用 Symbol 类型的值当作属性或方法的名称,必须保存,否则每次生成的值不同,将无法引用
  • 使用 for 循环无法遍历 Symbol 类型的属性和方法,可以使用 Object 类的 getOwnPropertySymbols 静态方法访问 Symbol 类型的属性和方法,示例如下:
let name = Symbol("name");
let say = Symbol("say");
let obj = {
    [name]: "Reyn Morales",
    [say]: function () {
        console.log("Hello World");
    },
    age: 21,
    leave: function () {
        console.log("Bye~");
    }
}

for (let key in obj) {
    console.log(key);   // age \ leave
}

let [objName, objSay] = Object.getOwnPropertySymbols(obj);
console.log(objName);   // Symbol(name);
console.log(objSay);    // Symbol(say);
console.log(obj[objName]);  // Reyn Morales
obj[objSay]();  // Hello World

在上述实例中,由于 Object 类的 getOwnPropertySymbols 静态方法将所有 Symbol 属性和方法以数组的形式返回,所以使用解构赋值

Iterator

Iterator 是一个被称为迭代器的接口,凡是实现了此接口的类可以使用 for…of、解构赋值和扩展运算符等功能,在 JavaScript 中诸如 Array、Map、Set、String、arguments、NodeList 等默认实现了 Iterator 接口,此外,我们也可以在自定义类中实现 Iterator 接口,示例如下:

class MyArray {
    constructor() {
        for (let i = 0; i < arguments.length; i++) {
            this[i] = arguments[i];
        }
        this.length = arguments.length;
    }

    [Symbol.iterator]() {
        let cur = 0;
        let ts = this;
        return {
            next: function () {
                if (cur < ts.length) {
                    return { value: ts[cur++], done: false };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
}

let arr = new MyArray(1, 3, 5);
console.log(arr);   // MyArray {0: 1, 1: 3, 2: 5, length: 3}
for (let value of arr) {
    console.log(value); // 1 \ 3 \ 5
}

在上述示例中,为自定义数组添加了名称为 [Symbol.iterator] 的方法,此方法返回一个匿名对象,此匿名对象有一个名称为 next 的方法,此方法也返回一个匿名对象,此匿名对象有 2 个属性,value 属性保存数值,done 属性表示此数组是否遍历完成,凡是实现了 iterator 接口的类可以使用 JavaScript 提供的某些功能,示例如下:

let arr = new MyArray(1, 3, 5);
console.log(arr);

/* for...of */
for (let value of arr) {
    console.log(value); // 1 \ 3 \ 5
}

/* 解构赋值 */
let [x, y, z] = arr;
console.log(x, y, z);   // 1 3 5

/* 扩展运算符 */
let firArray = [1, 3];
let secArray = [2, 4];
console.log([...firArray, ...secArray]);    // (4) [1, 3, 2, 4]

实际上,凡是实现了 iterator 接口的类都有一个名称为 [Symbol.iterator] 的方法,此方法返回一个迭代器,此迭代器有一个 next 方法可以遍历实例中的所有内容,此方法返回一个匿名对象,此对象有 value 和 done 属性,value 属性保存内容,done 属性表示是否遍历完成,示例如下:

let arr = new Array(1, 3, 5);
let it = arr[Symbol.iterator]();
console.log(it);    // Array Iterator {}
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 3, done: false}
console.log(it.next()); // {value: 5, done: false}
console.log(it.next()); // {value: undefined, done: true}

综上所示,为一个自定义类实现 iterator 接口的过程如下:

  1. 为类创建一个 [Symbol.iterator] 方法
  2. 在方法中返回迭代器实例
  3. 在迭代器实例中创建一个 next 方法
  4. next 方法中返回匿名对象
  5. 匿名对象必须有 value 和 done 属性

在类中实现 iterator 接口时,必须遵循以上述规范

Generator

在 JavaScript 中有一种被称为 Generator 的函数,是一种异步编程解决方案,在此函数内部可以封装若干状态,所以被称为状态机,此类函数有如下特点:

  • 在定义函数的关键字之后使用 * 符号标识此函数是一个 Generator 函数
  • 调用此函数时,不会立即执行此函数中的代码,取而代之的是返回一个可迭代对象
  • 在此函数中可以使用 yield 关键字将函数中的代码分为若干部分,亦即若干个状态
  • 在 yield 关键字之后可以使用字符串、数值或布尔标识此 yield
  • 通过此可迭代对象调用 next 方法时,执行函数中部分代码,此外,将返回一个匿名对象,此对象有 value 和 done 属性,value 属性保存函数中 yield 标识

示例如下:

function* gen() {
    console.log("A");
    yield "First";
    console.log("B");
    yield 2;
    console.log("C");
    yield true;
}

let it = gen();
console.log(it);    // gen {<suspended>}
console.log(it.next()); // A \ {value: 'First', done: false}
console.log(it.next()); // B \ {value: 2, done: false}
console.log(it.next()); // C \ {value: true, done: false}
console.log(it.next()); // {value: undefined, done: true}

yield 关键字只能在 Generator 函数中使用

当使用可迭代对象调用 next 方法时,可以向 next 传入参数,此参数将传入到上一个所执行的部分代码标识的 yield 中,示例如下:

function* gen() {
    console.log("A");
    let res = yield "First";
    console.log(res);
    console.log("B");
    yield 2;
    console.log("C");
    yield true;
}

let it = gen();
console.log(it.next()); // A \ {value: 'First', done: false}
console.log(it.next("arg")); // arg \ B \ {value: 2, done: false}

必须注意,当使用可迭代对象第一次调用 next 方法时,不能传入参数

在 JavaScript 中 Generator 函数的 yield 关键字可以让函数实现类似于解构赋值的功能,示例如下:

function* calc(x, y) {
    yield x + y;
    yield x - y;
}

let it = calc(5, 3);
console.log(it.next().value);   // 8
console.log(it.next().value);   // 2

此外,利用 Generator 函数的特性可以为任意对象快速部署 Iterator 接口,示例如下:

let obj = {
    name: "Reyn Morales",
    age: 21,
    gender: "Man",
    // [Symbol.iterator]() {
    //     let keys = Object.keys(this);
    //     let cur = 0;
    //     let ts = this;
    //     return {
    //         next() {
    //             if (cur < keys.length) {
    //                 return { value: ts[keys[cur++]], done: false };
    //             } else {
    //                 return { value: undefined, done: true };
    //             }
    //         }
    //     }
    // }
}

function* gen() {
    let keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
        yield obj[keys[i]];
    }
}

obj[Symbol.iterator] = gen;

for (let value of obj) {
    console.log(value); // Reyn Morales \ 21 \ Man
}

最后,利用 Generator 函数也可以实现以同步流程表示异步操作,示例如下:

function request() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Reyn Morales");
        }, 1000);
    });
}

request().then(function (result) {
    console.log(result, "Fir"); // Reyn Morales Fir
    return request();
}).then(function (result) {
    console.log(result, "Sec"); // Reyn Morales Sec
    return request();
}).then(function (result) {
    console.log(result, "Thi"); // Reyn Morales Thi
});

function* gen() {
    yield request();
    yield request();
    yield request();
}

let it = gen();
it.next().value.then((result) => {
    console.log(result, "Fir"); // Reyn Morales Fir
    return it.next().value;
}).then((result) => {
    console.log(result, "Sec"); // Reyn Morales Sec
    return it.next().value;
}).then((result) => {
    console.log(result, "Thi"); // Reyn Morales Thi
});

在上述示例中,看起来 Generator 函数似乎不如使用 Promise,实际上,在编写代码时的确不会使用此方式,而是使用 ES8 中的 async-await 实现类似的功能,底层原理依然是 Generator 函数,示例如下:

async function gen() {
    let firResult = await request();
    console.log(firResult, "Fir");  // Reyn Morales Fir
    let secResult = await request();
    console.log(secResult, "Sec");  // Reyn Morales Sec
    let thiResult = await request();
    console.log(thiResult, "Thi");  // Reyn Morales Thi
}

gen();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值