Node简介
Node可以解决什么问题?
- 可以创建一种高性能服务器的开发工具,所谓高性能仅指在某一方面性能高,在web开发中,解决一些并发服务器的问题,主要应用场景是web
- 对比
-
java和php 处理计算,压缩,解压,加密,解压运算;多线程
-
Node 非阻塞异步i/o 操作文件,网络操作,数据库操作,处理高并发
Node是什么
Node不是一门语言,只是运行环境(runtime),只是提供了基于V8引擎的运行时(运行的环境,如console),也不是js,不包含BOm,Dom,,为了能实现服务端的功能新增了许多模块,如https,fs,这种模块可以帮助我们系统级的操作API,,使用了事件驱动(回调),非阻塞i/o(异步)的模型,包管理器npm
进程和线程
-
进程是操作系统分配资源和调度任务的基本单位
-
线程是建立在进程上的一次运行单位,一个进程上可以有多个线程
-
我们经常用的浏览器就是多进程的
-
浏览器由用户界面 -> 浏览器引擎(在用户界面和呈现引擎之间传递信息,是浏览器的主进程) -> 渲染引擎
- (被称为浏览器的内核,浏览器渲染进程)
- (一个插件对应一个进程)
- (networking网络请求,javascript interpreter js解释器,ui backend ,ui线程,Data Presistence数据存储)
-
一般我们先渲染css js和UI是互斥的,执行js空闲下来了就会执行css
-
js是单线程的,不能同时操作dom
-
其他线程
- 浏览器事件触发线程(控制事件循环,存放setTimeout,浏览器事件,ajax回调)
- 定时触发器线程
- 异步HTTP请求线程
-
单线程特点是节约内存,并且不需要切换执行上下文,不需要管理锁的问题
-
浏览器中的 Event loop
-
webPack多线程 happyPack
webworker 可以配置子线程,,实现多线程
worker.html
console.log(1)
let worker = new Worker('./worker.js') ; //h5内置
worker.postMessage(1000);//发消息
worker.onmessage = function(e){//收消息
console.log(e.data);
};
console.log(2)
worker.js
onmessage = function (e) { //收消息
let sum = 200;
this.postMessage(sum)
}
//不能操作dom
关于浏览器event loop
我们知道js是单线程的,在stack栈里执行,浏览器可以调一些方法,,,如setTimeout属于另一个线程里的了,会先进行同步代码,将另一个进程放到一个callback que里面去
- 队列和栈
- 队列:先进先出
- 栈:先进后出
console.log()
setTimeout(function(){
console.log(1)
},1000)
setTimeout(function(){
console.log(1)
},5000)
如果调用异步代码,不会马上放到队列,栈中的代码执行完之后会去队列中(callback queue) 取 ,如果此时已经到1秒,就将函数去除,放到栈里去执行,比如ajax 在成功的时候会放到队列中,形成事件环循环,,event loop死循环 如果事件到达的时候栈中的代码没有执行完,就不会执行队列中的内容
node事件环
node 里面有个应用,他会请求V8引擎 请求后会调用node API 调用完之后会跑到 LIBUV 库, 阻塞 多线程 实现异步 当事情处理完之后通过i/o机制calback,,然后将事件放到队列里去,然后通过node返回给应用
宏任务 和 微任务 -- 异步 执行事件不一样,不确定谁先执行
- 浏览器 普通代码就是普通的调用栈
- 常见的宏任务 setTimeout setIMMediate(只兼容ie) messagechannel(兼容到IE11)
- 微任务 vue里的 promise.then MutationObserver是微任务
let channel = new MessageChannel();
let port1 = channel.port1;
let port2 = channel.port2;
port1.postMEessage('hello')
异步代码 vue 规定就是宏任务
port2.onMessage = function(e){
console.log(e.data)
}
浏览器事件环
setTimeout(()=>{
Promise.resolve('123').then(data =>{
console.log(3)
})
})
setTimeout(()=>{
console.log(3)
})
//先会执行栈中的内容,再去执行微任务,微任务清空后在执行宏任务,然后循环,宏任务会在栈中执行
vue 前身nextick怎么实现的 (兼容性有问题已经被废掉了)
let observe = new MutationOberver(function(){
console.log('dom全塞进去了')
})
//微任务
observe.observe(div,{child:true}
node 环
- 同步异步只带的是被调用方法
- 阻塞和非阻塞 是调用者
- 拿读取文件举例子
- 阻塞/非阻塞 同步/异步
- 调用者(请求等待)| 请求文件 | 被调用者(同意) => 同步阻塞
- 调用者(请求等待)| 请求文件 | 被调用者(异步回调) => 异步阻塞
- 调用者(请求其他)| 请求文件 | 被调用者(异步回调) => 异步非阻塞
- 调用者(请求其他)| 请求文件 | 被调用者(同意) => 同步非阻塞
- node 属于 异步非阻塞 适用场景大量兵法的输入输出,读取数据,并不复杂处理,聊天,电商网站做接口中间层接口
node运行
vscode 安装插件 code runner 或者爬虫,, 默认情况只认js
- 所谓的repl就是能和我们命令窗口交互的 read eval print loop
常用命令
.help.break Sometimes you get stuck, this gets you out.clear Alias for .break.editor Enter editor mode.exit Exit the repl.help Print this help message.load Load JS from a file into the REPL session.save Save all evaluated commands in this REPL session to a file
global(全局)
- process 进程
- argv 执行时的参数 后面写脚手架会用到 类似于 webpack --config --prot --line等 参数解析靠argv
console.log(process.argv)
[ '/usr/local/bin/node',//node的exe文件目录
'/Users/myloveyunyun/Desktop/node/node.js' ]//执行的文件
在cmd执行
取参数
我们想拿到参数,
console.log(process.argv)
let args ={};
process.argv.slice(2).forEach((item,index)=>{
if(item.includes('--')){
args[item] = process.argv.slice(2)[index+1]
}
})
console.log(args)
- env 环境变量,在开发的时候可能经常输出很多错误,开发可能8080,上线是域名,,上线是不会输出错误的,,我们一般用它判断是开发环境还是线上环境
let url;
//怎么配置process.env.NODE_ENV
//mac export 设置环境变量 windows set 可以根据环境变量打出对应的url
if(process.env.NODE_ENV== 'deveopment'){
url = "http://localhost:3000/api"
}else{
url = "http://baidu.com"
}
console.log(url)
- pid 一个进程对应的进程号, 手动找到pid 杀进程
- chdir change directory 改变文件夹 http-server 想读取当前目录内容,需要用chdir 和 cwd
- cwd current working derectory 读取当前文件夹中的内容
console.log(process.cwd())
let fs = require('fs')
console.log(fs.readFileSync('./1.txt','utf8'))
[Running] node "/Users/myloveyunyun/Desktop/node/2018/node.js"
/Users/myloveyunyun/Desktop/node`
`/Users/myloveyunyun/Desktop/node/2018
Error: ENOENT: no such file or directory, open './1.txt'
//这是因为当前读取的是根目录,我们需要对目录作出拼接
我们可以
process.chdir('./2018')
console.log(process.cwd())
let fs = require('fs')
console.log(fs.readFileSync('./1.txt','utf8'))
- hrtime 已经被取代
- stdout 标准输出 stderr 错误输出 监听stdin 标准输入 和console.log 一起的
process.stdin.on('data',function(data){
console.log(data)
})
process.stdin.on('data',function(data){
process.stdout.write(data)
})
111
<Buffer 31 31 31 0a>
- exit 退出进程 主动
- kill 杀进程 不动
- buffer 操作二进制 缓存,把数据读到内存中
- clearImmediate
- setImmediate 宏任务
这个方法是异步的
process.nextTick 微任务,比宏任务快 和promise.then 比也快 两个都是微任务,这里额nextTick和vue里面的nextTick不是一个东西,一个前端,一个服务端
浏览器不存在此方法
setTimeout 和 setImmediate 顺序是不固定的
setTimeout(function(){
console.log('setTimeout')
},0)
setImmediate(function(){
console.log('setImmediate')
},0)
[Done] exited with code=0 in 0.083 seconds
[Running] node "/Users/myloveyunyun/Desktop/node/201803/node.js"
setImmediate
setTimeout
[Done] exited with code=0 in 0.066 seconds
[Running] node "/Users/myloveyunyun/Desktop/node/201803/node.js"
setTimeout
setImmediate
Node事件环是怎么运行的
每个步骤里面都有一个队列
- timer(setTimeout,setInterval)
- I/0 (不是文件读取 是流,tcp 错误)
- idle,prepare(NODE 自带)
- poll(轮询, 读文件回调 i/0队列,,还有个功能看timer是否到时间)
- check setImemediate
- close 关闭 sockect.on('close')
上面的代码我们只看1/4/5 先走1,但是所有代码都是异步的,node执行的时候,有准备的时间
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve('p').then(()=>{console.log('p')})
},0)
setTimeout(()=>{
console.log('setTimeout2')
},0)
//在浏览器里
setTimeout1
p
setTimeout2
//node里
[Running] node "/Users/myloveyunyun/Desktop/node/201803/node.js"
setTimeout1
setTimeout2
p
setTimeout(()=>{
console.log('setTimeout1')
},0)
setTimeout(()=>{
console.log('setTimeout2')
},0)
//从当前栈切换到队列的瞬间执行微任务
Promise.resolve('p').then(()=>{console.log('p')})
[Running] node "/Users/myloveyunyun/Desktop/node/201803/node.js"
p
setTimeout1
setTimeout2
setImmediate(()=>{
console.log('setImmediate1');
setTimeout(()=>{
console.log('setTimeout1')
},0)
})
setTimeout(()=>{
console.log('setTimeout2');
setImmediate(()=>{
console.log('setImmediate2');
})
},0)
//三种情况
//setTimeout2,setImmediate1,setImmediate2,setTimeout1
//setImmediate1,setTimeout2,setTimeout1,setImmediate2
//setImmediate1,setTimeout2,setImmediate2,setTimeout1
//nextTick 插孔执行,在setTimeout,setImmediate切换的时候执行,不是按方法,而是按队列区分.且里面不能写递归,容易死循环,让特定值在下一个队列执行,好处是优先级高于setTimeout
fs.readFile('./1.txt','utf8',()=>{
setImmediate(()=>{
console.log('setImmediate1');
})
setTimeout(()=>{
console.log('setTimeout2');
},0)
})
//因为fs在轮询里,setImmediate1,setTimeout2
- setInterval
- setTimeout
- 新增
- console.log('log') //标准输出 process.stdout 默认1
- console.info('info')//标准输出 process.stdout 默认1
- console.error('error') //错误输出 默认2
- console.warn('warn') //错误输出 默认2
- console.time() - console.timeEnd // 算服务器启动时间
- console.assert() //测试用 node 自带一个assert 的模块
- onsole.dir(Array.prototype,{showHidden:true})
- stream 是一个模块
- args库
调试 inspect + n
- 命令行调试 node inspect n下一步, s进, o出, watch('a')监控,watchers
- node --inspect-brk 文件名
谷歌打开
chrome://inspect进入调试 - vscode
模块 seajs * requirejs
CMD 就近依赖, AMD依赖前置 eval 闭包 有关模块化的实现请参考js模块化
我们重点说CommonJS
- 每个文件都是一个模块
- 每个模块外面都套了个函数,实现模块化功能,闭包
(function(exports,require,moudle,_dirname,_filename){
})()
- 更方便的管理代码
node 模块分类
- 内置模块 (核心模块)(最快)
fs readFile path http - 文件模块 (自己写的)
- 第三方模块
package.json如果文件夹下没有index文件,就去这个文件查找- 安装包之前要先
npm init -y初始化 - 安装包
npm install name - 会去
node_modules文件夹查找 - 找不到会去查找上级目录
- fs模块
fs.accessSync('1.txt')//判断文件是否存在
- path模块 //resolve join basename extname
//resolve // 解析绝对路径,传多个参数可以拼接
//join // 解析相对路径,传多个参数可以拼接
let path = require('path');
console.log(path.resolve('./2.txt','a','b'))
console.log(path.join(__dirname,'./2.txt','a','b'))
console.log(__dirname)
console.log(__filename)
[Running] node "/Users/myloveyunyun/Desktop/node/201803/node.js"
/Users/myloveyunyun/Desktop/node/2.txt/a/b
/Users/myloveyunyun/Desktop/node/201803/2.txt/a/b
/Users/myloveyunyun/Desktop/node/201803
/Users/myloveyunyun/Desktop/node/201803/node.js
console.log(path.extname('1.a.d.f')) // 后最
console.log(path.basename('1.a.s.f.g','.g')) //基础名字
console.log(path.sep)//环境变量分隔符
console.log(path.posix.sep)//mac下分隔符 /windows \
console.log(path.delimiter)//mac下; /windows:
[Running] node "/Users/myloveyunyun/Desktop/node/201803/node.js"
.f
1.a.s.f
/
;
- vm模块 虚拟机
let vm = require('vm');
let a = 'sd'
vm.runInThisContext('console.log(a)') //沙箱
eval('console.log(a)'),会查找
ReferenceError: a is not defined
at evalmachine.<anonymous>:1:1
- vm模块 虚拟机
let vm = require('vm');
let a = 'sd'
vm.runInThisContext('console.log(a)') //沙箱
eval('console.log(a)')//会查找
ReferenceError: a is not defined
at evalmachine.<anonymous>:1:1
Conmon.js 实现
ConmonJS规范
- 定义了如何导入模块 require
- 还定义了如何导出模块 module.exports 导出xxx
- 还定义了一个js就是一个模块
- 如果第一次加载完成,则会存到缓存里,第二次从缓存中读取
下面我们根据以上三个特点,考虑各种情况,一步步实现一个简单的CommenJS引入功能
-
首先我们要引入所用到的模块
a. 读文件
b. 获取文件路径
c. 运行环境
d. 加载策略=>针对js/json/node文件
e. Module
f. 缓存
g. requier方法
let fs = require('fs');
let path = require('path');
let vm = require('vm');
function Module(p) {
this.id = p; // 当前模块的标识
this.exports = {}; // 每个模块都有一个exports属性
this.loaded = false; // 这个模块默认没有加载完
}
Module._extensions = {
//js优先级高于json,和node
'.js': function (Module) {},
'.json': function (Module) {},
'.node': 'xxx'
}
Module._cacheModule = {}// 根据的是绝对路径进行缓存的
function require(moduleId){//我们将加载模块以参数形式传递过来
}
以上是我们读取模块必备的条件,下面我们挨个增加内容,
2. 在require接收到路径的时候,我们首先要对此路径做解析,假设我们给个方法_resolveFileName(moduleId)对路径作出解析
// 解析绝对路径的方法 返回一个绝对路径
Module._resolveFileName = function (moduleId) {
let p = path.resolve(moduleId);
// 没有后缀在加上后缀 如果传过来的有后缀就不用加了
if (!path.extname(moduleId)) {//extname是path内部方法,在这里用到的类似用法,清自行查阅,笔者就先不多做解释了
//keys将一个对象转成数组
let arr = Object.keys(Module._extensions);
//如果没有后蕞名称,因为只有三种情况,我们挨个对比,在这里是有先后顺序的,假设传过来a,我们会现识别a.js
for (let i = 0; i < arr.length; i++) {
let file = p + arr[i];
try {
fs.accessSync(file);//accessSync 同步判定分拣是否存在
return file;
} catch (e) {
console.log(e)
}
}
} else {
return p;//如果有后坠就直接查找此文件,无需匹配
}
}
function req(moduleId) {
let p = Module._resolveFileName(moduleId);// p是一个绝对路径
}
- 当我们解析完绝对路径之后,就需要去查找要加载的文件了,我们之前说如果是第二次加载,就从缓存中查找,所以这里我们首先需要判断有没有缓存
Module._extensions = {
//js优先级高于json,和node
'.js': function (Module) {},//这里的function指的是加载该类型文件的方法
'.json': function (Module) {},
'.node': 'xxx'
}
function req(moduleId) {
let p = Module._resolveFileName(moduleId);// p是一个绝对路径
if (Module._cacheModule[p]) {
// 模块存在,如果有直接把对象返回即可稍后补充
}
// 表示没有缓存就生成一个模块
let module = new Module(p);
// 加载模块
let content = module.load(p); // 加载模块
Module._cacheModule[p] = module;
module.exports = content; //最终以module.export导出
return module.exports
}
在此过程中 ,我们生成一个模块,new了一个moudle, 将路径传过去,还记得上面的代码,在new的过程中,我们给模块加了id,exports,load,然后我加载此模块,并且将它添加到缓存中
load方法是在实例上调用的,我们将吧这个方法写在Module的原型上
//在new之后就有了这些标识
function Module(p) {
this.id = p; // 当前模块的标识
this.exports = {}; // 每个模块都有一个exports属性
this.loaded = false; // 这个模块默认没有加载完
}
Module.prototype.load = function (filepath) {
//判断加载的文件是json还是node,还是js
let ext = path.extname(filepath);
//根据文件类型添加方法
let content = Module._extensions[ext](this); //成功读取文件内容
//this指当前模块的实例 有id exports loaded
return content;
}
- 最后我们来补充下加载文件的方法,这里只介绍json和js的 如果是json我们直接parse,如果是js,我们说一个js是一个模块,那说明每读取一个js相当于读取一个闭包文件,我们会在js文件内容外包一个闭包,然后导出用moudul.export
//exports,require,module
Module.warpper = ['(function(exports,require,module){', '\n})'];
Module._extensions = {
//js优先级高于json,和node
'.js': function (Module) {
let script = fs.readFileSync(module.id, 'utf8');
let fn = Module.warpper[0] + script + Module.warpper[1];
//我们需要执行此文件,但是eval会在当前作用域向上查找,我们只想在require之后执行此文件,因此这里使用沙漏限制环境
vm.runInThisContext(fn).call(Module.exports, Module.exports, req, module)
return module.exports;
},
'.json': function (Module) {
return JSON.parse(fs.readFileSync(module.id, 'utf8')); // 读取那个文件
},
'.node': 'xxx'
}
这样我们的基本功能就实现了
// 什么是commonjs规范
// 定义了如何导入模块 require
// 还定义了如何导出模块 module.exports 导出xxx
// 还定义了一个js就是一个模块
let fs = require('fs');
let path = require('path');
let vm = require('vm');
//解析绝对路径方法,返回绝对路径
//由于es6不支持静态属性,我们暂时用es5实现
//所有加载策略
function Module(p) {
this.id = p; // 当前模块的标识
this.exports = {}; // 每个模块都有一个exports属性
this.loaded = false; // 这个模块默认没有加载完
}
// 所有的加载策略
Module.warpper = ['(function(exports,require,module){', '\n})'];
Module._extensions = {
//js优先级高于json,和node
'.js': function (Module) {
let script = fs.readFileSync(module.id, 'utf8');
let fn = Module.warpper[0] + script + Module.warpper[1];
vm.runInThisContext(fn).call(Module.exports, Module.exports, req, module)
return module.exports;
},
'.json': function (Module) {
return JSON.parse(fs.readFileSync(module.id, 'utf8')); // 读取那个文件
},
'.node': 'xxx'
}
//根据绝对路径进行缓存
Module._cacheModule = {};
Module.resolveFileName = function (ModuleId) {
let p = path.join(ModuleId);
//如果后最不存在则查找
if (!path.extname(ModuleId)) {
//keys将一个对象转成数组
let arr = Object.keys(Module._extensions);
for (let i = 0; i < arr.length; i++) {
let file = p + arr[i];
//模块加载的整个过程都是同步的
try {
fs.accessSync(file);
return file;
} catch (e) {
console.log(e)
}
}
} else {
return p;
}
}
Module.prototype.load = function (filepath) {
//判断加载的文件是json还是node,还是js
let ext = path.extname(filepath)
let content = Module._extensions[ext](this); //成功读取文件内容
//this指当前模块的实例 有id exports loaded
return content
}
function req(ModuleId) {
//解析绝对路径,
//且判断文件类型json,js,node
//得倒真实路径去查找缓存
let p = Module.resolveFileName(ModuleId); //p是一个绝对路径
if (Module._cacheModule[p]) {
// 模块不存在,如果有直接把exports对象返回即可
return Module._cacheModule[p].exports;
}
//没有缓存则创建一个模块
let moudule = new Module(p);
let content = module.load(p);
Module._cacheModule[p] = module;
module.exports = content;
return module.exports;
//加载模块
}
let a = req('./a.js');
req('./a.js');
console.log(a);
对于文件模块,如果js,json,node都不存在其实会找文件夹下的package.json,如果json文件没有,会找该文件夹下的index,在这里我们不做过多的阐述。 文件模块查找规则