Node.js是什么?
Node. js
是一个基于 Chrome v8
引擎的服务器端 JavaScript
运行环境;Node. js
是一个事件驱动、非阻塞式I/O
的模型,轻量而又高效;Node. js
的包管理器npm
是全球最大的开源库生态系统。
Node. js的使用场景是什么?
高并发、实时聊天、实时消息推送、客户端逻辑强大的SPA
(单页面应用程序)。
为什么要用 Node. js?
(1)简单, Node. js
用 JavaScript
、JSON
进行编码,简单好学。
(2)功能强大,非阻塞式I/O
,在较慢的网络环境中,可以分块传输数据,事件驱动,擅长高并发访问。
(3)轻量级, Node. js
本身既是代码又是服务器,前后端使用同一语言。
(4)可扩展,可以轻松应对多实例、多服务器架构,同时有海量的第三方应用组件。
Node的基本用法
方法一:在终端输入 node “js文件所在路径地址”,点击Enter运行
例如要运行的service.js文件,就输入:node ./service.js
点击运行即可在终端运行node文件并输出文件内容
方法二:直接在终端输入 node,进入node环境,然后输入node指令即可
如果要退出node环境,按Ctrl + C
即可退出
相关拓展
单线程和同步异步
JS语言的一大特色就是单线程,所谓单线程就是,同一时间只能做一件事。为什么JS不能有多个线程呢?因为JS主要用途是与用户进行交互以及操作DOM。这就决定了它只能是单线程,否则假设有多个线程,一个线程在某个DOM节点上添加内容,同时另一个线程又要删除这个节点,这时浏览器该以谁为准?
JS是一门单线程语言,它有一个主线程(main thread)和调用栈(也叫执行栈call-stack),所有的任务都会被放到调用栈等待主线程执行。单线程就意味着所有任务需要排队,前一个任务结束,才执行下一个任务。但是如果前一个任务耗时很长,后一个任务就需要一直等着了。如果其中一个任务很慢,占用很多时间,此时网页就会卡住。而有些I/O
(输入输出)操作是很慢的,比如AJAX
操作。
所以JS语言的设计者意识到,主线程可以先不管这些I/O
操作,把等待中的任务先挂起,先运行排在后面的任务。等到I/O
操作返回结果后,再去执行挂起的任务。
因此任务可以分为两种,一种是同步任务,一种是异步任务。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才执行下一个任务。
异步任务指的是,不进入主线程,而是进入任务队列,通过Event Loop
机制等待合适的时间调用。
有了Event Loop
的加持,JS才能非阻塞地运行。
同步任务与异步任务的流程图:
宏任务和微任务
宏任务:
宏任务Macro task,也叫tasks。一些异步任务的回调会依次进入Marco task queue,等待后续被调用。
宏任务包括:整体代码script
,ajax
网络请求,setTimeout
,setInterval
,node中的setImmediate
、node的I/O
操作,requestAnimationFrame
(浏览器独有)、UI rendering
(浏览器独有)等。
微任务:
微任务Micro task,也叫jobs。另一些异步任务的回调会依次进入Micro task queue,等待被调用。
微任务包括:Promise.then()
,async
,await
,node中的process.nextTick()
等。
事件循环,宏任务,微任务的关系如图所示:
事件循环Event Loop步骤流程
1.一开始执行全局的Script代码,这些代码中有同步语句或者异步语句,遇到同步语句直接执行,异步语句放入事件队列中。异步语句中不同类型的任务会进入对应的Event Queue,宏任务中的事件放在callback queue中,由事件触发主线程维护;微任务的事件放在微任务队列中,由js引擎线程维护。
2.全局的Script代码执行完毕之后,调用栈Stack会清空,然后先检查事件队列中的微任务队列中是否有任务。
3.如果有就从微任务队列中取出位于队首的回调,放入调用栈Stack中执行,执行完成之后,微任务队列长度相应减一。
4.如果微任务队列中还有任务,继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到把微任务队列中所有的任务都执行完毕。注意,如果在执行微任务过程中又产生了新的微任务,那么会加入到微任务队列的队尾,也会在这个周期内被执行。
5.当微任务队列中所有任务都执行完毕之后,此时微任务队列为空,调用栈Stack清空,检查事件队列中的宏任务队列中是否有任务。
6.如果有,依次取出宏任务队列中位于队首的任务放入调用栈Stack中执行。
7.主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个循环往复的过程就被称为“Event Loop事件循环”。
小测试:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
复制代码
结果是:1 => 'new Promise' => 3 => 'then' => 2
流程如下
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,new Promise是在解析生成promise对象,里面的操作是同步的,直接执行,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
复制代码
promise
Promise是一个对象,代表一个异步操作的最终完成或者失败。由于它的then
、catch
、finally
方法会返回一个新的promise
对象,所以可以允许我们链式调用,解决了由于传统的函数嵌套函数所导致的回调地狱问题。
promise的三种状态:
pending
、resolved
、rejected
Promise的状态一经改变就不能再改变
1.pending
状态,不会触发then
和catch
2.resolved
状态,会触发后续的then
回调函数
3.rejected
状态,会触发后续的catch
回调函数
4.then
正常返回的状态是resolved
,里面有报错则返回rejected
5.catch
正常返回的状态是resolved
,里面有报错则返回rejected
promise状态转变的两个过程:
1.pending
-> resolve
2.pending
-> reject
promise的.then和.catch方法:
1、Promise
的状态一经改变就不能再改变
2、.then
和.catch
方法都会返回一个新的Promise
对象
3、.catch
方法不管被连接到哪里,都能捕获上层未被捕捉过的错误
4、在Promise
中,返回任意一个非promise
的值都会被包裹成promise
对象,例如return 2会被包装为return Promise.resolve(2)
5、Promise
的.then
和.catch
可以被多次调用,但是如果Promise
内部的状态一经改变,并且有了一个值,那么后续每次调用.then
或者.catch
的时候都会直接拿到该值
6、.then
或者.catch
中return一个error对象并不会抛出错误,所以不会被后续的.catch
捕捉
7、.then
或者.catch
返回的值不能是该promise
本身,否则会造成死循环
8、.then
或者.catch
的参数期望是函数,传入非函数则会发生值透传
9、.then
方法是能接收两个参数的,一个是处理成功的函数resolve
,一个是处理失败的函数reject
,在某些时候可以认为.catch
是.then
的第二个参数的简便写法
promise的.finally方法:
1、.finally
方法也是返回一个Promise
对象,它在Promise
结束的时候,无论结果为resolved
还是rejected
,都会执行里面的回调函数
2、.finally
方法的回调函数不接收任何的参数,也就是说我们在.finally
函数里面是没法知道Promise
最终的状态是resolved
还是rejected
的
3、.finally
最终返回的默认会是一个“上一次的Promise
对象值”,不过如果抛出的是一个异常则返回异常的Promise
对象
promise的.all和.race方法:
1、Promise.all()
方法是接收一组异步任务(数组形式的参数),然后并行执行异步任务,只有在所有异步操作都成功执行(resolve)之后才执行回调函数
2、.race()
方法也是接收一组异步任务,然后并行执行异步任务,只保留第一个执行完成的异步操作的结果,其他方法仍然在执行,不过执行结果会被抛弃
3、Promise.all().then()
结果中数组的顺序和Promise.all()
接收到的数组顺序一致
4、.all()
和.race()
传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被.then()
的第二个参数reject
或者后面的.catch()
捕获,但是并不会影响数组中后续其他的异步任务的继续执行
Promise.all中如果有一个抛出异常了会如何处理:
.all
和.race
传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被.then
的第二个参数或者后面的.catch
捕获;但并不会影响数组中其它的异步任务的执行。
Promise可以链式调用的原因:
由于它的.then
、.catch
、.finally
方法都会返回一个全新的Promise对象,所以可以允许我们链式调用
async与await
async
是异步的意思,await
则可以理解为等待
放到一起可以理解async
就是用来声明一个异步方法,而 await
是用来等待异步方法执行
async
async
函数返回一个promise
对象,下面两种方法是等效的
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
复制代码
await
正常情况下,await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回对应的值
async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
复制代码
不管await
后面跟着的是什么,await
都会阻塞后面的代码
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
复制代码
上面的例子中,await
会阻塞下面的代码(即加入微任务队列),先执行 async
外面的同步代码,同步代码执行完,再回到 async
函数中,再执行之前阻塞的代码
所以上述输出结果的顺序为:1 fn 3 2
综合练习:
//练习1
setTimeout(()=>{ // 宏任务
console.log("a")
},0)
new Promise((resolve)=>{
resolve() // 同步的
console.log("b") // 同步的
}).then(()=>{ // 从这里开始,到下面
console.log("c") // 上一次resolve执行触发then里面的操作
Promise.resolve().then(()=>{ // 这里的已经是resolve状态,then操作立马触发
console.log("e") // e在c之后立马执行
}).then(()=>{
console.log("f") // f在e之后立马执行
})
}) // 接上面,到这里都是微任务,注意这里的整个then操作中包裹的都是微任务
console.log("d")
复制代码
上面执行后返回的结果顺序是:b d c e f a
//练习2
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
复制代码
分析过程:
- 执行整段代码,遇到
console.log('script start')
直接打印结果,输出script start
- 遇到定时器了,它是宏任务,先放着不执行
- 遇到
async1()
,执行async1
函数,先打印async1 start
,下面遇到await
怎么办?先执行async2
,打印async2
,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码 - 跳到
new Promise
这里,直接执行,打印promise1
,下面遇到.then()
,它是微任务,放到微任务列表等待执行 - 最后一行直接打印
script end
,现在同步代码执行完了,开始执行微任务,即await
下面的代码,打印async1 end
- 继续执行下一个微任务,即执行
then
的回调,打印promise2
- 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印
settimeout
所以最后的结果是:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout
Node的核心概念
非阻塞I/O:
访问磁盘和网络这样的I/O 请求会比较慢,所以我们希望,在读取文件或通过网络发送消息时,运行平台不会阻塞业务逻辑的执行。
Node 用三种技术来解决这个问题:事件、异步API、非阻塞I/O
。
非阻塞I/O
是个底层术语。它的意思是说,你的程序可以在做其他事情时发起一个请求来获取网络资源,然后当网络操作完成时,将会运行一个回调函数来处理这个操作的结果。
同步式I/O
是指: 线程在执行中如果遇到磁盘读写或网络通信(统称为 I/O 操作),通常要耗费较长的时间,这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为阻塞 。
当 I/O 操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。
这种 I/O 模式就是通常的同步式 I/O(Synchronous I/O)
或阻塞式 I/O (Blocking I/O)
。
多线程同步式I/O
如下图:
异步式 I/O (Asynchronous I/O)
或非阻塞式 I/O (Non-blocking I/O)
则针对所有 I/O 操作不采用阻塞的策略。
当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。
当操作系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个事件。
为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。
单线程异步式I/O
如下图:
同步式I/O与异步式I/O比较
简而言之,异步式 I/O
就是少了多线程的开销。
对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。
npm的基本用法
npm
就是一个网站,里面存放着各种node需要用到的插件,可以使用npm
的指令进行下载。
npm的基本指令集:
npm init
:初始化项目,下图
package.json
中的内容:下图
npm install packagename
:安装模块不指定版本号 默认会安装最新的版本,其中packagename
是你需要安装的包的对应名称,下图
npm install packagename@0.0.1
:安装指定版本的模块,如npm install express@4.17.0
,就是安装4.17.0版本的express,下图
安装之后,如何使用?使用require
引入express
:下图
npm install packagename -g
或 --global
:安装全局的模块(不加参数的时候默认安装本地模块)
npm install packagename --save
或 -S:--save、-S
参数意思是把模块的版本信息保存到dependencies(生产环境依赖)中,即你的package.json文件的dependencies字段中
npm install packagename --save-dev
或 -D:--save-dev
、 -D
参数意思是吧模块版本信息保存到devDependencies(开发环境依赖)中,即你的package.json文件的devDependencies字段中,下图
npm uninstall packagename [options]
:卸载已经安装的模块,后面的options参数意思与安装时候的意思一样,与这个命令相同的还有npm remove
、npm rm
、npm r
、 npm un
、 npm unlink
这几个命令功能和npm uninstall
基本一样
npm outdated
:这个命令会列出所有已经过时了的模块
npm update [-g]
:对于已经过时了的模块可以使用上面的命令去更新
npm update [包名称]
:更新指定名称的包