xdm,异步编程

771 阅读10分钟

1. 起因

不想看废话的 hxd,可以直接跳到目录第四点知识储备,呜呜~

想着弄出一个用户登录功能,结果在用 Node.js 写后端服务的时候,遇到了困难,待我细细说来~

登录的思路,就是前端发起请求,携带用户填写好的用户名以及密码,后端收到请求,连接数据库,查询表里面是否有该用户,用则登录成功,返回给前端该用户的其他信息。

2. 问题代码

2.1 前端代码

JS 代码如下:

            const btn = document.getElementById('btn')
​
            btn.addEventListener('click',function() {//给按钮d绑定
                const user = document.getElementsByClassName('input')[0].value
                const password = document.getElementsByClassName('input')[1].value
​
                axios.get(`http://localhost:3001/home?user=${user}&password=${password}`)
                //测试用的 GET 请求,应该要用 Post 的
                .then(res =>console.log(res.data))
                .catch(err => console.log('我错了'+err))
        })

上边的代码不难理解,就获取用户输入的数据,作为请求的参数发送出去。

2.2 数据库截图

user 表如下:

image-20210723145257059.png

2.3 后端代码

2.3.1 代码

index.js 文件如下:

const mysql = require("mysql");
const cors = require("cors");
const express = require("express");
​
const app = express();
app.use(cors()); //express利用cors解决跨域let result = "2111"; //定义全局变量,给初始值
function connectMysql(user, password) {//自定义函数,连接数据库,获取数据
  let connection = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "******",//mysql数据库的密码
    database: "test", 
  });
  connection.connect();
​
  connection.query(
    "SELECT user_name,user_password FROM user",
    function (error, results, fields) {
        if (error) throw error;
        for (let i = 0; i < results.length; i++) {
            if (results[i].user_name === user &&results[i].user_password === password) {
                result = results[i].id;//匹配上了,就将用户名字赋值给全局变量
                break //不再查找,结束循环
            }else{
                console.log('匹配失败')
            }
        }
    }
  );
  connection.end();
}
​
app.get("/home", (req, res) => {
    const {user,password} = req.query;
    connectMysql(user, password)
    console.log('我可是有响应她的')
    res.send(result)
});
​
app.listen(3001);
console.log("服务启动成功了");

上边的代码有一些值得注意的地方:

  • 引入 mysql 模块用来连接数据库
  • 使用 express 框架来创建服务
  • 引入 expresscors 中间件来解决跨域问题,因为服务端是在 3001 端口运行着的,前端则默认在 5000 端口

2.3.2 跨域拓展

说到跨域问题,想起来之前看到过的一个有意思的事(代理的原理):

image-20210325133614108.png

如图:由 3000 端口直接向 5000 端口发送请求,会导致跨域问题,但是注意,这里的 3000 端口的请求是可以发送出去的,但是5000端口响应回来的数据,客户端的 ajax 引擎会阻挡(安全第一),设置一个 3000 端口的代理,这个中间人没有引擎,所以可以接收5000端口的数据,再由相同的3000端口把数据给客户端的3000端口,这样就能解决同源跨域问题。

去掉 cors 中间件,在终端 node index.js 前端,前端输入正确的用户名和密码(1,1),我们可以看到:

  • 终端:image-20210723145528360.png
  • 浏览器控制台:image-20210723145557211.png

结果一目了然,上述结论正确!

2.4 问题浮现

回归正题:我们把 cors 中间件加上去,重复上述操作,预计的结果如果用户信息正确,后端返回用户名给前端,这里返回的结果我用的是一个全局变量 result,初始值给了个 0 。但是幸福总是来得那么突然,输入正确的消息后,浏览器控制台打印出的是初始值,第二次点击按钮,才返回用户名,如图:

image-20210723154728394.png

聪明的小伙伴一眼就能发现问题,这不就是异步了嘛!也就是说连接数据库是异步操作?我也在群里问起了起来,老哥说:

截图.jpg

暂时就理解为 connection.connect(); 是多线程,同步的,connection.query() 是异步的

3.无脑改动

但是现在的问题不就是让异步操作变同步嘛!心想着这操作我懂, asyncawait 给我用起来。于是我改动了 index.js 的代码如下:

3.1 无脑行为1

主要做了如下改动:

  • 在声明 connectMysql 函数前加上 async,因为 await 要在 async 函数里面才能用
  • connection.connect,connection.query,connection.end() 加上 await 关键词

但是也不行,因为 connectMysql 还是异步的,并没有解决问题,为了解决问题我也是煞费苦心,各种变换 async,await 的位置,甚至搞出了如下的sao代码:

3.2 无脑行为2

app.get("/home", (req, res) => {
  const {user,password} = req.query;
  const myFun2 = async function aaa(){//await要放在函数里面
    await connectMysql(user, password);
  };
  myFun2();//这个函数还是异步的呀!这个函数没执行完,就已经send了(自己的脑回路就很离谱)
  res.send(result)
});

结果可想而知,浏览器控制台打印依旧是异步的。

3.3 无脑行为3

app.get("/home", (req, res) => {
  const {user,password} = req.query;
  const myFun2 = async function aaa(){//await要放在函数里面
    await connectMysql(user, password);
    res.send(result)
  };
  myFun2();
});

其实到了这一步,我觉得处理异步的逻辑,应该是没有问题了,但是还是没能解决问题,我意识到是我自己知识点出了问题,光靠 asyncawait 不能解决问题,于是我想到了 Promise,不是说 Promise 是异步编程的一种很好的解决方案吗,但是我不知道,Promiseasyncawait如何结合使用,于是我踏上了重新学习的道路,这一学才发现自己的基础知识点确实没掌握好。

4 知识储备

4.1 单线程与异步

JavaScript 是单线程(一个主线程)的,所以为了充分利用资源就用了异步的思想,对于一些比较耗费时间的,比如加载文件,发起网络请求,还有定时器等操作,遇到这些操作,就新开一个子线程,主线程依旧干着那些比较主要的事情,让这个子线程去干耗费时间资源的事,这就是异步。

4.2 回调

但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。

所以就会有了回调函数的概念,子线程执行完毕,就执行我指定的函数。同步的回调函数很好理解,

function f2(name) {
  alert('Hello ' + name);
}
​
function f1(callback) {
  var name = prompt('输入用户名');
  callback(name);
}
f1(f2);
log('我不是恁爹')

异步回调我见过的大多是封装好的,像setTimeOut(callback,time)fs.readFile(path,callback),对 Node.js 使用不多,但是作为写服务端的语言,肯定是可以创建子线程,并对此进行事件监听的。

但是回调函数有很多的问题,也就是常说的回调地狱,层级太多,不利于阅读维护

setTime(()=>{
    setTime(()=>{
        setTime(()=>{
            setTime(()=>{
​
            });
        });
    });
    console.log("小林别闹");
});

4.3 Promise

Promise 应运而生,它最早由社区提出并实现,ES6 将其写进了语言标准。像上述的回调地狱,使用 Promise 的链式能更加方便,直观的写

4.3.1 基础用法

function sleep(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(() => {
            console.log('小林别闹')
            resolve('1')
        }, ms);
    })
}
sleep(500).then( ()=> console.log("finished"));

观察上边的代码,我们一点一点的来说到说到,首先 new 了一个新的 Promise,传递的是一个函数参数,但是这个函数会在 new 的时候就马上执行,我们需要控制它的执行时机,所以我们在外面包装一个函数,当函数调用的时候再执行。

Promise 到底是如何方便的实现异步编程的呢,我们在参数函数里面放如入应该执行的异步操作,我们根据该操作执行的情况,执行来改变 Promise 的状态,Promise 有三种状态 Pending(默认),Fulfilled(成功),Rejected(失败)。通过执行 resolve()reject() 来实现 Pending -> Fulfilled 和 Pending -> Rejected 的状态变化,注意 Promise 的状态只会变化一次。

使用 resolve,reject 传递的参数,可以在 then 里面接收到:

sleep()
.then((result)=>{
  console.log(result);
}, (_err)=> {
  console.log('我出错啦,进到这里捕获错误,但是不经过catch了');
})

如果上一个 Promise 中的操作成功,使用resolve(),则then的第一个参数接收结果并执行,reject()则是第二个。

4.3.2 链式

注意:链式是靠往后面传递一个 Promise 来维护的,因为只有在 Promise 里面才存在,这样通过链式,我们就可以控制异步操作的执行顺序

sleep()
.then((result)=>{
  return new Promise(function(resolve, reject) {
  resolve(result+1)
  })
})
.then((res) => {
consolr.log(res)
})

当然,你不在 then 里面返回一个 Promise 后面的 then,也是会执行的,但是遇见异步的操作,顺序就完全不受控制,看哪个异步操作先完成了,失去了使用 Promise 解决异步编程的意义。

4.3.3 错误捕获

上边已经提到过,我们可以使用 then 的第二个参数来捕获错误,同时我们也可以使用 catch 来捕获

sleep()
.then((result)=>{
  console.log(result);
})
.catch(err => {
console.log('我错了'+err)
})
​

有一些需要注意的地方:

  • then 的第二个参数以及catch来捕获错误都不会 break链路,也就是说,捕获到错误后,后面的 then 语句是可以正常执行的,所以 catch 语句的位置也是有讲究的,重要且必须执行的语句一般放在 catch 语句后面,记得还出了个Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

  • catch 能够捕获到整条链路上的错误,一旦捕获到,错误发生地,到 catch 语句之间的 then 都不会执行了,而 then 的第二个参数是捕获上一个 promise 错误,两种捕获方式,谁捕获到就看谁在链路上的位置考前了。

promise 的内容远不止如此,蛋解决这次问题,这些就差不多了,等我过段时间看了存了老久了的廖雪峰老师的《ES6标准入门》后或许能够更加系统的学习到 Promise

4.4 aysnc and await

Promise 的使用也是存在一些问题的,大量复杂的 Promise 不易读,而且promise链式中断很不方便,就有了 asyncawait.

需要记住:

  • async, await缺少一个就没有意义了,await必须在async声明的函数内部使用
  • async 声明的函数的返回本质上是一个 Promise,获取返回值可以通过 then
  • await 后面必须是一个 Promise 才起作用,等待到有返回值了,代码往下继续执行 (我的无脑改动三就是这点没有意识到)
const demo = async ()=>{
    let result = await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('res')
      }, 1000)
    });
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    return result;
}
// demo的返回当做Promise
demo().then(result=>{
  console.log(result); // res
})

链式的 promisepromise1().then(() => {return promise2}).then(() => {return promise3})

使用 asyncawait 后可以这样写了:

(async ()=>{
  const result1 = await promise1();
  const result2 = await promise2(result1); 
  const result3 = await promise3(result2);
  console.log(result3);
})()

感觉直观了很多。

至此,我已经找到解决问题的方法了,服务端代码具体如下:

const mysql = require("mysql");
const cors = require("cors");
const express = require("express");
​
const app = express();
app.use(cors()); //express利用cors解决跨域let result = "2111"; //定义全局变量,给初始值
async function connectMysql(user, password) {//自定义函数,连接数据库,获取数据
  let connection = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "*******",
    database: "test", 
  });
   connection.connect();
​
  return new Promise((resolve,reject) => {
    connection.query(
      "SELECT user_name,user_password FROM user",
      function (error, results, fields) {
        if (error) throw error;
        for (let i = 0; i < results.length; i++) {
          if (
            results[i].user_name === user &&
            results[i].user_password === password
          ) {
            result = results[i].user_name;//匹配上了,就将用户名字赋值给全局变量
           // res = results
            console.log('匹配成功'+result)
            resolve('promise'+result)
          }else{
            console.log('匹配失败')
          }
        }
      }
    );
    connection.end();
    console.log(result+'connectMySql')
  }) 
}
​
app.get("/home", (req, res) => {
  const {user,password} = req.query;
  const myFun2 = async function aaa(){//await要放在函数里面
    await connectMysql(user, password)//await 后面必须是一个 promise
    res.send(result)
  };
  myFun2();
});
​
app.listen(3001);
console.log("服务启动成功了");
​

参考:

异步Promise及Async/Await可能最完整入门攻略