Node基础知识学习记录

68 阅读7分钟
let likeArr = {0:1, length: 1};
let arr = [...likeArr]

上述代码会报错: Uncaught TypeError: likeArr is not iterable

js中一个对象有了Symbol(Symbol.iterator)这样一个属性,才可以通过...展开迭代

元编程指可以改变js本身功能的编程方式,例如

let obj = { // Symbol的用法
    get [Symbol.toStringTag](){
        return 'zf'
    }
};
console.log(Object.prototype.toString.call(obj))
// 结果为[object zf]

如果调用[...likeArray],则会调用likeArray的迭代器,想要让普通对象likeArray可以有迭代功能就要作如下处理:

let likeArray = { // 数组中有Symbol(Symbol.iterator) 这个方法就是告诉浏览器如何迭代此对象
    0:1,
    1:2,
    2:3,
    3:4,
    length:4,
    [Symbol.iterator](){ // 此方法要返回一个迭代器
        let i = 0;
        // this
        return {
             next:()=>{
                return {value:this[i],done:this.length === i++}
            }
        }
    }
}

完成这个功能还可以使用生成器

let likeArray = { // 数组中有Symbol(Symbol.iterator) 这个方法就是告诉浏览器如何迭代此对象
    0:1,
    1:2,
    2:3,
    3:4,
    length:4,
    [Symbol.iterator]:function *(){ // 迭代数组的时候 会自动调用next
        let i = 0;
        let len = this.length;
        while(len !== i){
            yield this[i++]
        }
    }
}

generator和Promise配合使用案例:

const fs = require("fs/promises");
const path = require('path')
function* readFile() {
  let data1 = yield fs.readFile(path.resolve(__dirname,"a.txt"), "utf8");
  let data2 = yield fs.readFile(path.resolve(__dirname,data1), "utf8");
  return data2; // 30
}

let it = readFile();
let {value,done} = it.next();
value.then(data1=>{
    let {value,done} = it.next(data1)
    value.then(data2=>{
        let {value,done} = it.next(data2);
        console.log(value)
    })
})

上面的then嵌套可以采用co库来简化:

const fs = require("fs/promises");
const path = require('path')
function* readFile() {
  let data1 = yield fs.readFile(path.resolve(__dirname,"a.txt"), "utf8");
  let data2 = yield fs.readFile(path.resolve(__dirname,data1), "utf8");
  return data2; // 30
}

co(readFile()).then(data=>{
    console.log(data);
}).catch(e=>{
    console.log(e);
})

co的实现:

function co(it){
    return new Promise((resolve,reject)=>{
        function step(data){
            let {value,done} = it.next(data);
            if(!done){
                Promise.resolve(value).then(data=>{ // 第一步完成
                    step(data); // 下一步
                }).catch(e=>{
                    reject(e);
                })
            }else{
                resolve(value)
            }
        }
        step();
    })
}

generator编译后的效果:

function _regeneratorRuntime(){
    function wrap(iteratorFn){
        // 基本稍后执行 所需要的信息
        const _context = {
            next:0, // 上下文信息
            done:false,
            stop(){
                _context.done = true;
            },
            sent:null
        }
        // wrap 函数执行后返回的是一个迭代器
        return { // it
            next(v){ // 每次调用next的时候 会给上一次的yield返回值赋值
                _context.sent = v;
                let value = iteratorFn(_context);
                return {value,done:_context.done}
            }
        }
    }
    return {
        wrap
    }
}
function foo(x) {
  var a, b;
  return _regeneratorRuntime().wrap(function foo$(_context) {
   // while (1) { // 表示一个状态机  状态的扭转, 表示这个逻辑会走多次 (根据状态执行对应的流程)
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return x + 1;

        case 2:
          a = _context.sent;
          console.log(a);
          _context.next = 6;
          return x + 2;

        case 6:
          b = _context.sent;
          console.log(b);
        case 8:
        case "end":
          return _context.stop();
      }
    //}
  });
}
let it = foo(1);
console.log(it.next())
console.log(it.next('ok'))
console.log(it.next('ok ~!'))

async和await会被编译成generator和cn

宏任务和微任务

浏览器是由多个进程组成的 (每个页签都是一个独立的进程) 浏览器中主要的进程有:主进程 网络进程、绘图的进程、 插件都是一个个的进程

一个页签都是一个进程 (渲染进程): 渲染的线程

渲染进程中 : 进程中包含的线程 ui线程 负责页面渲染、布局、绘制 js线程:js引擎线程, 主要负责执行js代码的 (ui线程 js线程互斥的,js也是单线程的) 为了保证渲的一致性要保证整个执行是单线程的 主线程:单线程的 (代码是从上大下执行) webworker(不能操作dom元素) 同步代码

定时器、请求、用户的事件操作 都是异步的 (每次开一个定时器 都会生成一个新的线程)

异步任务的划分 : 宏任务、微任务 微任务: Promise.then(ECMAScript里面提供的) mutationObserver(html5,这里的回调是一异步执行的) queueMircrotask 宏任务:默认的script脚本 ui渲染也是一个宏任务 setTimout, 请求,用户的事件、messageChannel, setImmediate

requestIDleCallback requestFrameAnimation (这个是放在渲染的) 只能算回调,你可以当成宏任务

宏任务队列(消息队列 底层是由多个队列组成 , 我们实际看的宏任务队列当成一个来理解) 时间到达后会将任务放到宏任务队列中 webApi 微任务队列 每次执行宏任务的时候 都会产生一个微任务队列 , 我们在看执行过程的时候当成只有一个来理解 微任务就是个回调 代码执行的过程中 宏任务和微任务 会将对应的结果放到不同的队列中 等待当前宏任务执行完毕后,会将微任务全部清空 (在微任务执行的过程中 如果在产生微任务 则会将当前产生的微任务放到队列尾部 ) 微任务都执行完毕后,会在宏任务队列中拿出来一个执行 (宏任务每次执行一个,微任务每次执行一堆)

js 是单线程的所以要有一个事件触发线程 来实现任务的调度 宏任务的执行顺序 是按照 调用的顺序 (时间一样的情况下), 如果时间不一样,则以放入的顺序为准 渲染是要在特定的时机才能渲染, 根据浏览器的刷新频率 16.6ms, 不是每一轮都要渲染的 (一定在微任务之后)

对于事件回调类宏任务,直接调用事件回调和通过用户触发效果不太一样,例如如下代码:

button.addEventListener('click',()=>{
  console.log('listener1');
  Promise.resolve().then(()=>console.log('micro task1'))
})
button.addEventListener('click',()=>{
  console.log('listener2');
  Promise.resolve().then(()=>console.log('micro task2'))
})
button.click();

控制台将输出:

listener1
listener2
micro task1
micro task2

从结果上来看,是把两个回调放到了同一个宏任务中执行 但如果点击按钮的话,控制台将输出:

listener1
micro task1
listener2
micro task2

从结果上来看,是分别执行了两个宏任务

循环引用

commonjs 的特点就是内部会维护一个属性 loaded 内部会用一个变量来表示这个模块是否加载完成 如果没有加载完成 只会运行到已经加载的部分

因此出现循环引用时,如果被循环引用的模块还没有执行module.exports = xxx,就会报错了,但依然可以正常执行 参考:/jiagouke07-3-node/5.module/c1.js

全局方法

process.cwd(), path.resolve()两种方式效果一致,而且这个路径是可以修改的 cwd: current working directory

process.chdir("../../")方法的执行会改变process.cwd(), path.resolve()的结果

事件循环

官方给的文档: nodejs.org/en/docs/gui…

对于微任务来说,nextTick比Promise.resolve()优先级要高,即使将Promise.resolve()放到process.nextTick前面,nextTick的回调也会先执行

Promise.resolve().then(()=>{
    console.log('promise')
})

process.nextTick(()=>{ // 此方法用的比较少
    console.log('nextTick')
})

EventEmit模块提供了一个固定的订阅名——newListener,如下面案例所示,girl对象通过once方法给"失恋"绑定了2个方法,每绑定一次,newListener就会触发一次

const EventEmitter = require("events"); //事件触发器 on emit off once

function Girl() {}

// class Girl extends EventEmitter{} ;// 会继承实例属性,也会继承原型属性

Object.setPrototypeOf(Girl.prototype, EventEmitter.prototype);

const girl = new Girl();

const sleep = function (type) {
  console.log("睡觉", type);
};
const drink = function (type) {
  console.log("喝", type);
};

let wating = false;
girl.on("newListener", function (type) {
  // 每次绑定事件的时候 就会执行此方法
  // newListener的回调的调用时机,实在绑定之前触发的
  girl.emit(type);
  if (!wating) {
    process.nextTick(() => {
      girl.emit(type);
      wating = false;
    });
    wating = true;
  }*/
});

girl.once("失恋", sleep); // on('wrapper')
girl.once("失恋", drink);

遍历删除文件夹下所有子文件和子文件夹

方法一:串行删除,即使树有很多个子节点,也是一个一个的删,和并行删除相比,效率低

function removeDir(filepath, callback) {
  fs.stat(filepath, function (err, statObj) {
    if (err) return callback(err);
    if (statObj.isDirectory()) {
      fs.readdir(filepath, function (err, dirs) {
        if (err) return callback(err);
        dirs = dirs.map((item) => path.join(filepath, item));
        let idx = 0; // 异步遍历 就是 递归迭代
        function next() {
          if (idx === dirs.length) return fs.rmdir(filepath, callback);
          let dir = dirs[idx++]; // 先删除第一个,第一个删除后,删除下一个
          removeDir(dir, next);
        }
        next();
      });
    } else {
      fs.unlink(filepath, callback); // 如果是文件直接删除即可
    }
  });
}

removeDir("a", function (err) {
  if (err) return console.log(err);
  console.log("删除成功");
});

方法二:并行删除,子树可以同时删除

function removeDir(filepath, callback) {
  fs.stat(filepath, function (err, statObj) {
    if (err) return callback(err);
    if (statObj.isDirectory()) {
      fs.readdir(filepath, function (err, dirs) {
        if (err) return callback(err);
        dirs = dirs.map((item) => path.join(filepath, item));
        if (dirs.length === 0) {
          // 如果没有孩子,则直接删除即可
          return fs.rmdir(filepath, callback);
        }
        let times = 0;
        function done() {
          if (++times === dirs.length) {
            return fs.rmdir(filepath, callback);
          }
        }
        for (let i = 0; i < dirs.length; i++) {
          // 同时删除两个节点
          removeDir(dirs[i], done);
        }
      });
    } else {
      fs.unlink(filepath, callback); // 如果是文件直接删除即可
    }
  });
}

removeDir("a", function (err) {
  if (err) return console.log(err);
  console.log("删除成功");
});

用Promise.all优化并行删的逻辑

function removeDir(filepath) {
  return new Promise((resolve, reject) => {
    fs.stat(filepath, function (err, statObj) {
      if (err) return reject(err);
      if (statObj.isDirectory()) {
        fs.readdir(filepath, function (err, dirs) {
          if (err) return reject(err);
          dirs = dirs.map((item) => removeDir(path.join(filepath, item)));

          Promise.all(dirs).then(() => fs.rmdir(filepath, resolve));
        });
      } else {
        fs.unlink(filepath, resolve); // 如果是文件直接删除即可
      }
    });
  });
}

用async await fsPromise继续优化上述代码:

const fsPromise = require("fs/promises");
async function removeDir(filepath) {
  let statObj = await fsPromise.stat(filepath);
  if (statObj.isDirectory()) {
    let dirs = await fsPromise.readdir(filepath);
    dirs = dirs.map((item) => removeDir(path.join(filepath, item)));
    await Promise.all(dirs);
    await fsPromise.rmdir(filepath);
  } else {
    return fsPromise.unlink(filepath); // 如果是文件直接删除即可
  }
}

方法三:层序遍历,即先广度优先遍历,从最底层的叶子结点开始逐级往上,将全路径存到一个stack中,再删除

function rmdirSync(filepath) {
  let stack = [filepath]; // 默认认为第一个是文件夹
  let idx = 0;
  let current;
  while ((current = stack[idx++])) {
    let statObj = fs.statSync(current);
    if (statObj.isDirectory()) {
      let dirs = fs.readdirSync(current);
      stack = [...stack, ...dirs.map((dir) => path.join(current, dir))];
    }
  }
  // [ 'a', 'a/b', 'a/b/c', 'a/b/c/d' ]
  for (let i = stack.length - 1; i >= 0; i--) {
    let statObj = fs.statSync(stack[i]);
    if (statObj.isDirectory()) {
      fs.rmdirSync(stack[i]);
    } else {
      fs.unlinkSync(stack[i]);
    }
  }
}