Giter VIP home page Giter VIP logo

article's Introduction

Node.js

📦 5 个有趣的 Node.js 库,带你走进 彩色 Node.js 世界 🎉

[Node.js 入门系列] 事件触发器 events 模块

[Node.js 入门系列] 本地路径 path 模块

[Node.js 入门系列] 文件操作系统 fs 模块

[Node.js 入门系列] 全局对象 process 进程

[Node.js 入门系列] http 模块

[Node.js 入门系列] 统一资源定位符 url 模块

[Node.js 入门系列] 压缩 zlib 模块

[Node.js 入门系列] 流 stream 模块

[Node.js 入门系列] 逐行读取 readline 模块

[Node.js 入门系列] 查询字符串 querystring 模块

[Node.js 入门系列] module 模块

[Node.js 入门系列] 缓冲器 Buffer 模块

[Node.js 入门系列] 域名服务器 dns 模块

[Node.js 入门系列] TodoList 实践

[Node.js 进阶系列] Koa 源码分析之 EventEmitter

[Node.js 进阶系列] Koa 源码分析之 Http 模块

[Node.js 进阶系列] Koa 源码分析之 Use 方法

[Node.js 进阶系列] Koa 源码分析之洋葱模型

[Node.js 进阶系列] Koa 源码分析之 Context 对象

[Node.js 进阶系列] Koa 源码精读一

[Node.js 进阶系列] Koa 源码精读二

实践系列

[实践系列]前端路由

[实践系列]Babel 原理

[实践系列]Promises/A+ 规范

[实践系列]浏览器缓存

[实践系列]call,apply,bind 走一个

[实践系列]模拟实现 new 操作符

[实践系列]不要肆无忌惮地在你的项目中使用 ES78910 了~

[实践系列]「nodejs + docker + github pages 」 定制自己的 「今日头条」

[实践系列] null-cli 来啦, 一行命令提高你的效率 !

前端工程化

webpack 打包原理 ? 看完这篇你就懂了 !

null 记

前端应该会的 23 个 linux 常用命令

从 8 道面试题看浏览器渲染过程与性能优化

0202 年了, Chrome DevTools 你还只会 console.log 吗 ?

「 Dart Js Ts 」给前端工程师的一张 Dart 语言入场券

Vue

撸一个简版 vuex

12 道 vue 高频原理面试题,你能答出几道?

网络篇

TCP 的流量控制和拥塞控制

TCP 和 UDP 有什么区别

302 和 307 有什么区别

中间人攻击是什么

HTTPS 连接到底发生了什么

HTTP/2 带来了什么

Github 项目解析

css-diff

ms

lazyload

article's People

Contributors

webfansplz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

article's Issues

「nodejs + docker + github pages 」 定制自己的 「今日头条」

前言

在闲暇之余,我们经常会逛各种社区,逛掘金看技术软文,逛虎扑看今日赛事,逛头条看热门时事,逛 91……

每个社区都有各种各样的资讯,但有时我们只想看某个社区的某些资讯。那我们能不能将这些社区里我们想要的信息做一下整合 定制成自己的“今日头条”呢?

思路

每天定时抓取 资讯的标题和链接 整合后发布到自己的网站 这样每天只要打开自己的网站就可以看到属于自己的今日头条啦~

  • 抓取资讯 puppeteer
  • 定时任务 node-schedule
  • 部署 docker + github pages

我的今日头条

  • 掘金社区 前端热门文章
  • 今日头条 热门时事
  • 虎扑社区 nba 赛事
  • QQ 音乐 热门音乐

ok,开撸...

项目初始化

npm init -y
today's hot
│   README.md
└───html
│   │   index.html  // 网站入口,用于部署github pages
└───resource
│   │   index.json  // 资讯数据,爬取存放文件
└───tasks           // 任务队列
│   │   index.js
│   │   juejin.js
│   │   top.js
│   │   nba.js
│   │   music.js
│   │   jianshu.js
└───tools          //  工具类
    │   index.js
│   index.js       //  工程入口
│   package.json

抓取资讯

抓取资讯 我使用的是 puppeteer,它是 Google Chrome 团队官方的一个工具,提供了一些 API 来控制 chrome!(一听就很刺激。)

npm i puppeteer --save

我们先写一个简单的 demo 来了解一些 puppeteer 的基本 api.

const puppeteer = require("puppeteer");

const task = async () => {
  // 打开chrome浏览器
  const browser = await puppeteer.launch({
    // 关闭无头模式,方便查看
    headless: false
  });
  // 新建页面
  const page = await browser.newPage();
  // 跳转到掘金
  await page.goto("https://juejin.im");
  // 截屏保存
  await page.screenshot({
    path: "./juejin.png"
  });
};
task();

juejin

ok~我们趁阴明站长不在的时候,来掘金"拿点"东西~

掘金的前端热门文章是我比较关注的模块,我们来"拿"这个模块的资讯.

const puppeteer = require("puppeteer");

const task = async () => {
  // 打开chrome浏览器
  const browser = await puppeteer.launch({
    headless: false
  });
  // 新建页面
  const page = await browser.newPage();
  // 跳转到掘金
  await page.goto("https://juejin.im");
  // 菜单导航对应的类名
  const navSelector = ".view-nav .nav-item";
  // 前端菜单
  const navType = "前端";
  // 等待菜单加载完成...
  await page.waitFor(navSelector);
  // 菜单导航名称
  const navList = await page.$$eval(navSelector, ele =>
    ele.map(el => el.innerText)
  ); // [ '推荐', '后端', '前端', 'Android', 'iOS', '人工智能', '开发工具', '代码人生', '阅读' ]
  // 找出菜单中前端模块对应的索引
  const webNavIndex = navList.findIndex(item => item === navType);
  // 点击前端模块并等待页面跳转完成
  await Promise.all([
    page.waitForNavigation(),
    page.click(`${navSelector}:nth-child(${webNavIndex + 1})`)
  ]);
  // 截屏保存
  await page.screenshot({
    path: "./juejin-web.png"
  });
};
task();

juejin

上图可以看到,我们已经跳转到了前端模块.

接下来,我们只要找出文章列表对应的类名就可以对它进行爬取.

const puppeteer = require("puppeteer");

const task = async () => {
  // 打开chrome浏览器
  const browser = await puppeteer.launch({
    headless: false
  });
  // 新建页面
  const page = await browser.newPage();
  // 跳转到掘金
  await page.goto("https://juejin.im");
  // 菜单导航选择器
  const navSelector = ".view-nav .nav-item";
  // 文章列表选择器
  const listSelector = ".entry-list .item a.title";
  // 菜单类别
  const navType = "前端";
  await page.waitFor(navSelector);
  // 导航列表
  const navList = await page.$$eval(navSelector, ele =>
    ele.map(el => el.innerText)
  );
  // 前端导航索引
  const webNavIndex = navList.findIndex(item => item === navType);
  await Promise.all([
    page.waitForNavigation(),
    page.click(`${navSelector}:nth-child(${webNavIndex + 1})`)
  ]);
  // 等待文章列表选择器加载完成
  await page.waitForSelector(listSelector, {
    timeout: 5000
  });
  // 通过选择器找到对应列表项的标题和链接
  const res = await page.$$eval(listSelector, ele =>
    ele.map(el => ({
      url: el.href,
      text: el.innerText
    }))
  );
  // [ { url: 'https://juejin.im/post/5dd55512f265da47a807cc06',
  //   text: 'if 我是前端Leader,怎么走出小微前端团队的围墙?' },
  // { url: 'https://juejin.im/post/5dd49a45e51d45400206a655',
  //   text: 'Koa还是那个Koa,但是Nodejs已经不再是那个Nodejs' },
  // { url: 'https://juejin.im/post/5dd4b991e51d450818244c30',
  //   text: 'WebSocket 原理浅析与实现简单聊天' },...
};
task();

ok,我们已经成功拿到了掘金前端热门文章的内容,趁站长还没来,赶紧溜~其他网站也是一样的方法,这里就不啰嗦了~

我们拿到了资讯,接下来对它进行保存。

保存资讯

因为只是玩具级别的 demo,这里就不用数据库了,简单的用 json 进行保存。

// resource/index.json
{
  "data": []
}

我们基于 nodejs fs 文件操作模块,简单封装读写方法。

// tools/index.js
const fs = require("fs");
const fileServer = {
  // 写文件
  write(path, text) {
    fs.writeFileSync(path, text);
  },
  // 读文件
  read(path) {
    return fs.readFileSync(path);
  }
};

接下来,我们只要在每次获取完资讯,将内容写进文件就好了

const { fileServer } = require("./tools");
const path = require("path");
const task = () => {
  // 获取资讯任务
  const getMsgTask = Promise.all(tasks());
  getMsgTask.then(res => {
    // 读取json
    const { data } = JSON.parse(
      fileServer.read(path.join(resourcePath, "./index.json")).toString()
    );
    // ... 此处省略对资讯 格式化内容
    const text = msgHandle(res);
    // 写入资讯
    fileServer.write(
      path.join(resourcePath, "./index.json"),
      JSON.stringify({
        data: [
          {
            date: now,
            text
          },
          ...data
        ]
      })
    );
  });
};

保存完资讯,我们只要请求这个文件,将它渲染出来就好了~

// html/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>今日资讯</title>
    <script src="https://cdn.bootcss.com/marked/0.7.0/marked.min.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
  </body>
  <script>
    (function() {
      $(document).ready(function() {
        $.ajax({
          url: "http://localhost:8888/index.json",
          dataType: "json",
          success(data) {
            const content = data.data.reduce((a, b) => a + b.text, "");
            // 资讯我使用的是markdown进行保存,所以用marked进行转换
            $("#content").html(marked(content));
          }
        });
      });
    })();
  </script>
</html>

定时任务

定时任务使用的是node-schedule,非常简单易用的一个 nodejs 库。

// 每日18时定时任务
function crontab() {
  schedule.scheduleJob(`00 00 18 * * *`, mainTask);
}
// 任务
function mainTask(){...}

部署

部署我采用的是 docker + github pages 。

docker 部署这里有两个要注意的地方

  1. 时区问题:docker 时区是 UTC,和北京时间差了 8 小时,会导致我们的定时任务时间失准.

  2. docker 和 puppeteer chorium 源问题 ...

# Dockerfile

FROM node:10-slim
# 创建项目代码的目录
RUN mkdir -p /workspace

# 指定RUN、CMD与ENTRYPOINT命令的工作目录
WORKDIR /workspace

# 复制宿主机当前路径下所有文件到docker的工作目录
COPY . /workspace
# 清除npm缓存文件
RUN npm cache clean --force && npm cache verify
# 如果设置为true,则当运行package scripts时禁止UID/GID互相切换
# RUN npm config set unsafe-perm true

RUN npm config set registry "https://registry.npm.taobao.org"

RUN npm install -g pm2@latest
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work. 此处有墙...
# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
  && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
  && apt-get update \
  && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf \
  --no-install-recommends \
  && rm -rf /var/lib/apt/lists/*

# 只安装package.json dependencies
RUN npm install --production

RUN npm i puppeteer
# 设置时区
RUN rm -rf /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

EXPOSE 8888

CMD [ "pm2-docker", "start", "pm2.json" ]

构建镜像 shell

# build.sh
docker build -t today-hot .

启动容器 shell

# run.sh
curPath=`cd $(dirname $0);pwd -P`
docker run --name todayHot -d -v $curPath:/workspace -p 8888:8888 today-hot

� 接下来只要把 html 文件部署到网站上即可,我们这里使用 github-pages ,免费的静态网站托管平台~

npm install gh-pages --save

在 package.json 定义 scripts

  "scripts": {
    "deploy": "gh-pages -d html"
  }

  npm run deploy 将前端资源推送到github上,然后通过 xxx.github.io/xxx  就可以访问了

结语

本文主要讲解的是思路,具体代码如下,爬虫 服务并没有部署到服务器,大家可以 download 代码自行尝试。

完整代码地址

效果

如果觉得有帮助到你,你懂的~

从 8 道面试题看浏览器渲染过程与性能优化

前言

移动互联网时代,用户对于网页的打开速度要求越来越高。百度用户体验部研究表明,页面放弃率和页面的打开时间关系如下图 所示。

chart

根据百度用户体验部的研究结果来看,普通用户期望且能够接受的页面加载时间在 3 秒以内。若页面的加载时间过慢,用户就会失去耐心而选择离开。

首屏作为直面用户的第一屏,其重要性不言而喻。优化用户体验更是我们前端开发非常需要 focus 的东西之一。

本文我们通过 8 道面试题来聊聊浏览器渲染过程与性能优化。

我们首先带着这 8 个问题,来了解浏览器渲染过程,后面会给出题解~

  1. 为什么 Javascript 要是单线程的 ?

  2. 为什么 JS 阻塞页面加载 ?

  3. css 加载会造成阻塞吗 ?

  4. DOMContentLoaded 与 load 的区别 ?

  5. 什么是 CRP,即关键渲染路径(Critical Rendering Path)? 如何优化 ?

  6. defer 和 async 的区别 ?

  7. 谈谈浏览器的回流与重绘 ?

  8. 什么是渲染层合并 (Composite) ?

进程 (process) 和线程 (thread)

进程(process)和线程(thread)是操作系统的基本概念。

进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。

线程是 CPU 调度的最小单位(是建立在进程基础上的一次程序运行单位)。

process_thread

现代操作系统都是可以同时运行多个任务的,比如:用浏览器上网的同时还可以听音乐。

对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动了一个浏览器进程,打开一个 Word 就启动了一个 Word 进程。

有些进程同时不止做一件事,比如 Word,它同时可以进行打字、拼写检查、打印等事情。在一个进程内部,要同时做多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程

由于每个进程至少要做一件事,所以一个进程至少有一个线程。系统会给每个进程分配独立的内存,因此进程有它独立的资源。同一进程内的各个线程之间共享该进程的内存空间(包括代码段,数据集,堆等)。

借用一个生动的比喻来说,进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情。

当我们启动一个应用,计算机会创建一个进程,操作系统会为进程分配一部分内存,应用的所有状态都会保存在这块内存中。

应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。

process_thread_example

浏览器的多进程架构

一个好的程序常常被划分为几个相互独立又彼此配合的模块,浏览器也是如此。

以 Chrome 为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能,

每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。

Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。

process

优点

由于默认 新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器。

同样,第三方插件崩溃也不会影响到整个浏览器。

多进程可以充分利用现代 CPU 多核的优势。

方便使用沙盒模型隔离插件等进程,提高浏览器的稳定性。

缺点

系统为浏览器新开的进程分配内存、CPU 等资源,所以内存和 CPU 的资源消耗也会更大。

不过 Chrome 在内存释放方面做的不错,基本内存都是能很快释放掉给其他程序运行的。

浏览器的主要进程和职责

process_list

主进程 Browser Process

负责浏览器界面的显示与交互。各个页面的管理,创建和销毁其他进程。网络的资源管理、下载等。

第三方插件进程 Plugin Process

每种类型的插件对应一个进程,仅当使用该插件时才创建。

GPU 进程 GPU Process

最多只有一个,用于 3D 绘制等

渲染进程 Renderer Process

称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。 (本文重点分析)

渲染进程 (浏览器内核)

浏览器的渲染进程是多线程的,我们来看看它有哪些主要线程 :

renderder_process

1. GUI 渲染线程

  • 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。

  • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。

  • 注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。

2. JS 引擎线程

  • Javascript 引擎,也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)

  • JS 引擎线程负责解析 Javascript 脚本,运行代码。

  • JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。

  • 注意,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

3. 事件触发线程

  • 归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助)

  • 当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中

  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理

  • 注意,由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)

4. 定时触发器线程

  • 传说中的 setInterval 与 setTimeout 所在线程

  • 浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)

  • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行)

  • 注意,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。

5. 异步 http 请求线程

  • 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求。

  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。

浏览器渲染流程

如果要讲从输入 url 到页面加载发生了什么,那怕是没完没了了...这里我们只谈谈浏览器渲染的流程。

workflow

  1. 解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件

  2. CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树

  3. 布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算

  4. 绘制 RenderObject 树 (paint),绘制页面的像素信息

  5. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面

题解

1. 为什么 Javascript 要是单线程的 ?

这是因为 Javascript 这门脚本语言诞生的使命所致!JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。

如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突。

如果 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,

假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。

当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。

2. 为什么 JS 阻塞页面加载 ?

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。

当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

从上面我们可以推理出,由于 GUI 渲染线程与 JavaScript 执行线程是互斥的关系,

当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。

因此如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

3. css 加载会造成阻塞吗 ?

由上面浏览器渲染流程我们可以看出 :

DOM 解析和 CSS 解析是两个并行的进程,所以 CSS 加载不会阻塞 DOM 的解析

然而,由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,

所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。

因此,CSS 加载会阻塞 Dom 的渲染

由于 JavaScript 是可操纵 DOM 和 css 样式 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。

因此,样式表会在后面的 js 执行前先加载执行完毕,所以css 会阻塞后面 js 的执行

4. DOMContentLoaded 与 load 的区别 ?

  • 当 DOMContentLoaded 事件触发时,仅当 DOM 解析完成后,不包括样式表,图片。我们前面提到 CSS 加载会阻塞 Dom 的渲染和后面 js 的执行,js 会阻塞 Dom 解析,所以我们可以得到结论:
    当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件。如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。
  • 当 onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片等资源已经加载完毕。

  • DOMContentLoaded -> load。

5. 什么是 CRP,即关键渲染路径(Critical Rendering Path)? 如何优化 ?

关键渲染路径是浏览器将 HTML CSS JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是我们上面说的浏览器渲染流程。

为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:

  • 关键资源的数量: 可能阻止网页首次渲染的资源。

  • 关键路径长度: 获取所有关键资源所需的往返次数或总时间。

  • 关键字节: 实现网页首次渲染所需的总字节数,等同于所有关键资源传送文件大小的总和。

1. 优化 DOM

  • 删除不必要的代码和注释包括空格,尽量做到最小化文件。

  • 可以利用 GZIP 压缩文件。

  • 结合 HTTP 缓存文件。

2. 优化 CSSOM

缩小、压缩以及缓存同样重要,对于 CSSOM 我们前面重点提过了它会阻止页面呈现,因此我们可以从这方面考虑去优化。

  • 减少关键 CSS 元素数量

  • 当我们声明样式表时,请密切关注媒体查询的类型,它们极大地影响了 CRP 的性能 。

3. 优化 JavaScript

当浏览器遇到 script 标记时,会阻止解析器继续操作,直到 CSSOM 构建完毕,JavaScript 才会运行并继续完成 DOM 构建过程。

  • async: 当我们在 script 标记添加 async 属性以后,浏览器遇到这个 script 标记时会继续解析 DOM,同时脚本也不会被 CSSOM 阻止,即不会阻止 CRP。

  • defer: 与 async 的区别在于,脚本需要等到文档解析后( DOMContentLoaded 事件前)执行,而 async 允许脚本在文档解析时位于后台运行(两者下载的过程不会阻塞 DOM,但执行会)。

  • 当我们的脚本不会修改 DOM 或 CSSOM 时,推荐使用 async 。

  • 预加载 —— preload & prefetch 。

  • DNS 预解析 —— dns-prefetch 。

总结

  • 分析并用 关键资源数 关键字节数 关键路径长度 来描述我们的 CRP 。

  • 最小化关键资源数: 消除它们(内联)、推迟它们的下载(defer)或者使它们异步解析(async)等等 。

  • 优化关键字节数(缩小、压缩)来减少下载时间 。

  • 优化加载剩余关键资源的顺序: 让关键资源(CSS)尽早下载以减少 CRP 长度 。

前端性能优化之关键路径渲染优化

6. defer 和 async 的区别 ?

当浏览器碰到 script 脚本的时候 :

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

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

2. <script async src="script.js"></script>

有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

3. <script defer src="myscript.js"></script>

有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

从实用角度来说,首先把所有脚本都丢到 </body> 之前是最佳实践,因为对于旧浏览器来说这是唯一的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。

接着,我们来看一张图:

defer_async

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的。绿色线代表 HTML 解析。

因此,我们可以得出结论:

  1. defer 和 async 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)

  2. 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的

  3. 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用

  4. async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行

  5. 仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的

来自 defer 和 async 的区别 -- nightire 回答

7. 谈谈浏览器的回流与重绘

回流必将引起重绘,重绘不一定会引起回流。

回流(Reflow)

当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

会导致回流的操作:

页面首次渲染

浏览器窗口大小发生改变

元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)

元素字体大小变化

添加或者删除可见的 DOM 元素

激活 CSS 伪类(例如::hover)

查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

clientWidth、clientHeight、clientTop、clientLeft

offsetWidth、offsetHeight、offsetTop、offsetLeft

scrollWidth、scrollHeight、scrollTop、scrollLeft

scrollIntoView()、scrollIntoViewIfNeeded()

getComputedStyle()

getBoundingClientRect()

scrollTo()

重绘(Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

性能影响

回流比重绘的代价要更高。

有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操作进行优化:浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

当你访问以下属性或方法时,浏览器会立刻清空队列:

clientWidth、clientHeight、clientTop、clientLeft


offsetWidth、offsetHeight、offsetTop、offsetLeft


scrollWidth、scrollHeight、scrollTop、scrollLeft


width、height


getComputedStyle()


getBoundingClientRect()

因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

如何避免

CSS
  • 避免使用 table 布局。

  • 尽可能在 DOM 树的最末端改变 class。

  • 避免设置多层内联样式。

  • 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。

  • 避免使用 CSS 表达式(例如:calc())。

Javascript
  • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。

  • 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。

  • 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。

  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

8. 什么是渲染层合并 (Composite) ?

渲染层合并,对于页面中 DOM 元素的绘制(Paint)是在多个层上进行的。

在每个层上完成绘制过程之后,浏览器会将绘制的位图发送给 GPU 绘制到屏幕上,将所有层按照合理的顺序合并成一个图层,然后在屏幕上呈现。

对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

composite

RenderLayers 渲染层,这是负责对应 DOM 子树。

GraphicsLayers 图形层,这是负责对应 RenderLayers 子树。

RenderObjects 保持了树结构,一个 RenderObjects 知道如何绘制一个 node 的内容, 他通过向一个绘图上下文(GraphicsContext)发出必要的绘制调用来绘制 nodes。

每个 GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后 draw 到屏幕上,此时,我们的页面也就展现到了屏幕上。

GraphicsContext 绘图上下文的责任就是向屏幕进行像素绘制(这个过程是先把像素级的数据写入位图中,然后再显示到显示器),在 chrome 里,绘图上下文是包裹了的 Skia(chrome 自己的 2d 图形绘制库)

某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。

合成层的优点

一旦 renderLayer 提升为了合成层就会有自己的绘图上下文,并且会开启硬件加速,有利于性能提升。

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快 (提升到合成层后合成层的位图会交 GPU 处理,但请注意,仅仅只是合成的处理(把绘图上下文的位图输出进行组合)需要用到 GPU,生成合成层的位图处理(绘图上下文的工作)是需要 CPU。)

  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 (当需要 repaint 的时候可以只 repaint 本身,不影响其他层,但是 paint 之前还有 style, layout,那就意味着即使合成层只是 repaint 了自己,但 style 和 layout 本身就很占用时间。)

  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint (仅仅是 transform 和 opacity 不会引发 layout 和 paint,其他的属性不确定。)

一般一个元素开启硬件加速后会变成合成层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能。

注意不能滥用 GPU 加速,一定要分析其实际性能表现。因为 GPU 加速创建渲染层是有代价的,每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。并且在移动端 GPU 和 CPU 的带宽有限制,创建的渲染层过多时,合成也会消耗跟多的时间,随之而来的就是耗电更多,内存占用更多。过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。

这里就不细说了,有兴趣的童鞋推荐以下三篇文章 ~

Accelerated Rendering in Chrome

CSS GPU Animation: Doing It Right

无线性能优化:Composite

参考

史上最全!图解浏览器的工作原理

从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理

[实践系列]前端路由

什么是路由?

路由这概念最开始是在后端出现的,在以前前后端不分离的时候,由后端来控制路由,服务器接收客户端的请求,解析对应的url路径,并返回对应的页面/资源。

简单的说 路由就是根据不同的url地址来展示不同的内容或页面.

前端路由的来源

在很久很久以前~ 用户的每次更新操作都需要重新刷新页面,非常的影响交互体验,后来,为了解决这个问题,便有了Ajax(异步加载方案),Ajax给体验带来了极大的提升。

虽然Ajax解决了用户交互时体验的痛点,但是多页面之间的跳转一样会有不好的体验,所以便有了spa(single-page application)使用的诞生。而spa应用便是基于前端路由实现的,所以便有了前端路由。

如今比较火的vue-router/react-router 也是基于前端路由的原理实现的~

前端路由的两种实现原理

1.Hash模式

window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。

window.onhashchange = function(){
    
    // hash 值改变 
    
    // do you want
}

2.History 模式

HTML5的History API 为浏览器的全局history对象增加的扩展方法。

简单来说,history其实就是浏览器历史栈的一个接口。这里不细说history的每个API啦。具体可查阅 传送门

window对象提供了onpopstate事件来监听历史栈的改变,一旦历史栈信息发生改变,便会触发该事件。

需要特别注意的是,调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件。

window.onpopstate = function(){
    // 历史栈 信息改变
    // do you want
}

history提供了两个操作历史栈的API:history.pushState 和 history.replaceState

history.pushState(data[,title][,url]);//向历史记录中追加一条记录
history.replaceState(data[,title][,url]);//替换当前页在历史记录中的信息。
// data: 一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。

//title: FireFox浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。

//url: 新的历史记录条目的地址。浏览器不会在调用pushState()方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。该参数是可选的;不指定的话则为文档当前URL。

两种模式优劣对比

对比 Hash History
观赏性
兼容性 >ie8 >ie10
实用性 直接使用 需后端配合
命名空间 同一document 同源

造(cao) 一个简单的前端路由

本demo只是想说帮助我们通过实践更进一步的理解前端路由这个概念,所以只做了简单的实现~

history模式404

当我们使用history模式时,如果没有进行配置,刷新页面会出现404。

原因是因为history模式的url是真实的url,服务器会对url的文件路径进行资源查找,找不到资源就会返回404。

这个问题的解决方案这里就不细说了,google一下,你就知道~ 我们在以下demo使用webpack-dev-server的里的historyApiFallback属性来支持HTML5 History Mode。

文件结构

|-- package.json
|-- webpack.config.js
|-- index.html
|-- src
    |-- index.js
    |-- routeList.js
    |-- base.js
    |-- hash.js
    |-- history.js

1.搭建环境

废话不多说,直接上代码~

package.json

{
  "name": "web_router",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --config ./webpack.config.js"
  },
  "author": "webfansplz",
  "license": "MIT",
  "devDependencies": {
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.28.1",
    "webpack-cli": "^3.2.1",
    "webpack-dev-server": "^3.1.14"
  }
}

webpack.config.js

'use strict';

const path = require('path');

const webpack = require('webpack');

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js'
  },
  devServer: {
    clientLogLevel: 'warning',
    hot: true,
    inline: true,
    open: true,
    //在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html (解决histroy mode 404)
    historyApiFallback: true,
    host: 'localhost',
    port: '6789',
    compress: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    })
  ]
};

2.开撸

首先我们先初始化定义我们需要实现的功能及配置参数。

前端路由 参数 方法
x 模式(mode) push(压入)
x 路由列表(routeList) replace(替换)
x x go(前进/后退)

src/index.js

const MODE='';

const ROUTELIST=[];

class WebRouter {
  constructor() {
    
  }
  push(path) {
  
   ...
  }
  replace(path) {
  
   ...
    
  }
  go(num) {
  
   ...
    
  }
}

new WebRouter({
  mode: MODE,
  routeList: ROUTELIST
});

前面我们说了前端路由有两种实现方式。

1.定义路由列表

2.我们分别为这两种方式创建对应的类,并根据不同的mode参数进行实例化,完成webRouter类的实现。

src/routeList.js

export const ROUTELIST = [
  {
    path: '/',
    name: 'index',
    component: 'This is index page'
  },
  {
    path: '/hash',
    name: 'hash',
    component: 'This is hash page'
  },
  {
    path: '/history',
    name: 'history',
    component: 'This is history page'
  },
  {
    path: '*',
    name: 'notFound',
    component: '404 NOT FOUND'
  }
];

src/hash.js

export class HashRouter{
    
}

src/history.js

export class HistoryRouter{
    
}

src/index.js

import { HashRouter } from './hash';
import { HistoryRouter } from './history';
import { ROUTELIST } from './routeList';
//路由模式
const MODE = 'hash';  

class WebRouter {
  constructor({ mode = 'hash', routeList }) {
    this.router = mode === 'hash' ? new HashRouter(routeList) : new HistoryRouter(routeList);
  }
  push(path) {
    this.router.push(path);
  }
  replace(path) {
    this.router.replace(path);
  }
  go(num) {
    this.router.go(num);
  }
}

const webRouter = new WebRouter({
  mode: MODE,
  routeList: ROUTELIST
});

前面我们已经实现了webRouter的功能,接下来我们来实现两种方式。

因为两种模式都需要调用一个方法来实现不同路由内容的刷新,so~

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>前端路由</title>
  </head>
  <body>
    <div id="page"></div>
  </body>
</html>

js/base.js

const ELEMENT = document.querySelector('#page');

export class BaseRouter {
 //list = 路由列表
  constructor(list) {
    this.list = list;
  }
  render(state) {
   //匹配当前的路由,匹配不到则使用404配置内容 并渲染~
    let ele = this.list.find(ele => ele.path === state);
    ele = ele ? ele : this.list.find(ele => ele.path === '*');
    ELEMENT.innerText = ele.component;
  }
}

ok,下面我们来实现两种模式。

Hash模式

src/hash.js

import { BaseRouter } from './base.js'; 

export class HashRouter extends BaseRouter {
  constructor(list) {
    super(list);
    this.handler();
    //监听hash变化事件,hash变化重新渲染  
    window.addEventListener('hashchange', e => {
      this.handler();
    });
  }
  //渲染
  handler() {
    this.render(this.getState());
  }
  //获取当前hash
  getState() {
    const hash = window.location.hash;
    return hash ? hash.slice(1) : '/';
  }
  //获取完整url
  getUrl(path) {
    const href = window.location.href;
    const i = href.indexOf('#');
    const base = i >= 0 ? href.slice(0, i) : href;
    return `${base}#${path}`;
  }
  //改变hash值 实现压入 功能
  push(path) {
    window.location.hash = path;
  }
  //使用location.replace实现替换 功能 
  replace(path) {
    window.location.replace(this.getUrl(path));
  }
  //这里使用history模式的go方法进行模拟 前进/后退 功能
  go(n) {
    window.history.go(n);
  }
}

History模式

src/history.js

import { BaseRouter } from './base.js';

export class HistoryRouter extends BaseRouter {
  constructor(list) {
    super(list);
    this.handler();
    //监听历史栈信息变化,变化时重新渲染
    window.addEventListener('popstate', e => {
      this.handler();
    });
  }
  //渲染
  handler() {
    this.render(this.getState());
  }
  //获取路由路径
  getState() {
    const path = window.location.pathname;
    return path ? path : '/';
  }
  //使用pushState方法实现压入功能
  //PushState不会触发popstate事件,所以需要手动调用渲染函数
  push(path) {
    history.pushState(null, null, path);
    this.handler();
  }
  //使用replaceState实现替换功能  
  //replaceState不会触发popstate事件,所以需要手动调用渲染函数
  replace(path) {
    history.replaceState(null, null, path);
    this.handler();
  }
  go(n) {
    window.history.go(n);
  }
}

3.小功告成

源码地址

ms

仓库:

ms -Tiny milisecond conversion utility

源码实现:

/**
 * Helpers.
 */

var s = 1000;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var w = d * 7;
var y = d * 365.25; // why ? 见以下解析第1点

/**
 * Parse or format the given `val`.
 *
 * Options:
 *
 *  - `long` verbose formatting [false]
 *
 * @param {String|Number} val
 * @param {Object} [options]
 * @throws {Error} throw an error if val is not a non-empty string or a number
 * @return {String|Number}
 * @api public
 */

module.exports = function (val, options) {
  options = options || {};
  var type = typeof val;
  // 字符串类型且不等于'',走parse方法
  if (type === "string" && val.length > 0) {
    return parse(val);
  }
  // number类型 且 为一个有限数值
  else if (type === "number" && isFinite(val)) {
    // long选项? fmtLong: fmtShort
    return options.long ? fmtLong(val) : fmtShort(val);
  }
  // 类型错误报错
  throw new Error(
    "val is not a non-empty string or a valid number. val=" +
      JSON.stringify(val)
  );
};

/**
 * Parse the given `str` and return milliseconds.
 *
 * @param {String} str
 * @return {Number}
 * @api private
 */

// 字符串解析成数字
function parse(str) {
  str = String(str);
  // why? why 100? 见以下解析2,3点
  if (str.length > 100) {
    return;
  }
  // 基操正则..
  var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
    str
  );
  if (!match) {
    return;
  }
  var n = parseFloat(match[1]);
  var type = (match[2] || "ms").toLowerCase();
  switch (type) {
    case "years":
    case "year":
    case "yrs":
    case "yr":
    case "y":
      return n * y;
    case "weeks":
    case "week":
    case "w":
      return n * w;
    case "days":
    case "day":
    case "d":
      return n * d;
    case "hours":
    case "hour":
    case "hrs":
    case "hr":
    case "h":
      return n * h;
    case "minutes":
    case "minute":
    case "mins":
    case "min":
    case "m":
      return n * m;
    case "seconds":
    case "second":
    case "secs":
    case "sec":
    case "s":
      return n * s;
    case "milliseconds":
    case "millisecond":
    case "msecs":
    case "msec":
    case "ms":
      return n;
    default:
      return undefined;
  }
}

/**
 * Short format for `ms`.
 *
 * @param {Number} ms
 * @return {String}
 * @api private
 */
// 直接相除四舍五入取整
function fmtShort(ms) {
  var msAbs = Math.abs(ms);
  if (msAbs >= d) {
    return Math.round(ms / d) + "d";
  }
  if (msAbs >= h) {
    return Math.round(ms / h) + "h";
  }
  if (msAbs >= m) {
    return Math.round(ms / m) + "m";
  }
  if (msAbs >= s) {
    return Math.round(ms / s) + "s";
  }
  return ms + "ms";
}

/**
 * Long format for `ms`.
 *
 * @param {Number} ms
 * @return {String}
 * @api private
 */
// long选项其实就是单位描述更加详细完整
function fmtLong(ms) {
  var msAbs = Math.abs(ms);
  if (msAbs >= d) {
    return plural(ms, msAbs, d, "day");
  }
  if (msAbs >= h) {
    return plural(ms, msAbs, h, "hour");
  }
  if (msAbs >= m) {
    return plural(ms, msAbs, m, "minute");
  }
  if (msAbs >= s) {
    return plural(ms, msAbs, s, "second");
  }
  return ms + " ms";
}

/**
 * Pluralization helper.
 */

// 复数判断
function plural(ms, msAbs, n, name) {
  // 复数判断,因为Math.round是四舍五入取整,所以此处判断使用n*1.5
  var isPlural = msAbs >= n * 1.5;
  // ms(89999, { long: true }) 1 minute
  // ms(90000, { long: true }) 2 minutes
  return Math.round(ms / n) + " " + name + (isPlural ? "s" : "");
}

解析:

  1. Why are there 365.25 days in a year?

  2. Limit str to 100 to avoid ReDoS of 0.3s

  3. By limiting the input length It prevents the regular expression that does the parsing from consuming too much cpu time blocking the event loop

收获:

其实 ms 的源码非常精简,也非常易读,但是我却从它的 History Commits 榨出了一些知识点:

第一版的 ms 代码其实是更简单的,当然了也存在一些问题。后面随着迭代和一些大佬(我在这里也看到了 tj 大佬,tj 大佬真是无处不在啊...)的 pr 贡献,发生了以下变化:

  • 更好的可读性 (函数/变量命名)
  • 更强的易用性 (unit 参数更多样化,暴露 long 参数)
  • 更严谨的边界判断及优化 (见上面 2,3 点解析)
  • 抛出异常 (哈哈,一开始的几个版本,没做参数的类型校验异常错误抛出)

看源码,真的也能从它的 History Commits 学到很多~

100 多行,2kb 多的库,却有 3.2k 的 star,足已见得它的实用与易用。

[Node.js 入门系列] module 模块

module 模块

Node.js 实现了一个简单的模块加载系统。在 Node.js 中,文件和模块是一一对应的关系,可以理解为一个文件就是一个模块。其模块系统的实现主要依赖于全局对象 module,其中实现了 exports(导出)、require()(加载)等机制。

1. 模块加载

Node.js 中一个文件就是一个模块。如,在 index.js 中加载同目录下的 circle.js:

// circle.js
const PI = Math.PI

exports.area = r => PI * r * r

exports.circumference = r => 2 * PI * r
// index.js
const circle = require('./circle.js')

console.log(`半径为 4 的圆面积为 ${circle.area(4)}`) // 半径为 4 的圆面积为 50.26548245743669

circle.js 中通过 exports 导出了 area()和 circumference 两个方法,这两个方法可以其它模块中调用。

exports 与 module.exports

exports 是对 module.exports 的一个简单引用。如果你需要将模块导出为一个函数(如:构造函数),或者想导出一个完整的出口对象而不是做为属性导出,这时应该使用 module.exports。

// square.js

module.exports = width => {
  return {
    area: () => width * width
  }
}
// index.js

const square = require('./square.js')
const mySquare = square(2)
console.log(`The area of my square is ${mySquare.area()}`) // The area of my square is 4

2. 访问主模块

当 Node.js 直接运行一个文件时,require.main 属性会被设置为 module 本身。这样,就可通过这个属性判断模块是否被直接运行:

require.main === module

比如,对于上面例子的 index.js 来说, node index.js 上面值就是 true, 而通过 require('./index')时, 值却是 false.

module 提供了一个 filename 属性,其值通常等于__filename。 所以,当前程序的入口点可以通过 require.main.filename 来获取。

console.log(require.main.filename === __filename) // true

3. 解析模块路径

使用 require.resolve()函数,可以获取 require 加载的模块的确切文件名,此操作只返回解析后的文件名,不会加载该模块。

console.log(require.resolve('./square.js')) // /Users/null/meet-nodejs/module/square.js

require.resolve 的工作过程:

require(X) from module at path Y
1. If X is a core module,
   a. return the core module
   b. STOP
2. If X begins with './' or '/' or '../'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text.  STOP
2. If X.js is a file, load X.js as JavaScript text.  STOP
3. If X.json is a file, parse X.json to a JavaScript Object.  STOP
4. If X.node is a file, load X.node as binary addon.  STOP

LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
   a. Parse X/package.json, and look for "main" field.
   b. let M = X + (json main field)
   c. LOAD_AS_FILE(M)
2. If X/index.js is a file, load X/index.js as JavaScript text.  STOP
3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
4. If X/index.node is a file, load X/index.node as binary addon.  STOP

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   c. DIR = path join(PARTS[0 .. I] + "node_modules")
   b. DIRS = DIRS + DIR
   c. let I = I - 1
5. return DIRS

4. 模块缓存

模块在第一次加载后会被缓存到 require.cache 对象中, 从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块。

多次调用 require('index'),未必会导致模块中代码的多次执行。这是一个重要的功能,借助这一功能,可以返回部分完成的对象;这样,传递依赖也能被加载,即使它们可能导致循环依赖。

如果你希望一个模块多次执行,那么就应该输出一个函数,然后调用这个函数。

模块缓存的注意事项

模块的基于其解析后的文件名进行缓存。由于调用的位置不同,可能会解析到不同的文件(如,需要从 node_modules 文件夹加载的情况)。所以,当解析到其它文件时,就不能保证 require('index')总是会返回确切的同一对象。

另外,在不区分大小写的文件系统或系统中,不同的文件名可能解析到相同的文件,但缓存仍会将它们视为不同的模块,会多次加载文件。如:require('./index')和 require('./INDEX')会返回两个不同的对象,无论'./index'和'./INDEX'是否是同一个文件。

5. 循环依赖

当 require()存在循环调用时,模块在返回时可能并不会被执行。

// a.js
console.log('a starting')
exports.done = false
const b = require('./b.js')
console.log('in a, b.done = %j', b.done)
exports.done = true
console.log('a done')
// b.js
console.log('b starting')
exports.done = false
const a = require('./a.js')
console.log('in b, a.done = %j', a.done)
exports.done = true
console.log('b done')
// main.js
console.log('main starting')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done=%j, b.done=%j', a.done, b.done)

首先 main.js 会加载 a.js,接着 a.js 又会加载 b.js。这时,b.js 又会尝试去加载 a.js。

为了防止无限的循环,a.js 会返回一个 unfinished copy 给 b.js。然后 b.js 就会停止加载,并将其 exports 对象返回给 a.js 模块。

这样 main.js 就完成了 a.js、b.js 两个文件的加载。输出如下:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true

6. 文件模块

当加载文件模块时,如果按文件名查找未找到。那么 Node.js 会尝试添加.js 和.json 的扩展名,并再次尝试查找。如果仍未找到,那么会添加.node 扩展名再次尝试查找。

对于.js 文件,会将其解析为 JavaScript 文本文件;而.json 会解析为 JOSN 文件文件;.node 会尝试解析为编译后的插件文件,并由 dlopen 进行加载。

路径解析

当加载的文件模块使用'/'前缀时,则表示绝对路径。如,require('/home/null/index.js')会加载/home/null/index.js 文件。

而使用'./'前缀时,表示相对路径。如,在 index.js 中 require('./circle')引用时,circle.js 必须在相同的目录下才能加载成功。

当没有'/'或'./'前缀时,所引用的模块必须是“核心模块”或是 node_modules 中的模块。

如果所加载的模块不存在,require()会抛出一个 code 属性为'MODULE_NOT_FOUND'的错误。

7. __dirname

当前模块的目录名。 与 __filename 的 path.dirname() 相同。

console.log(__dirname) // /Users/null/meet-nodejs/module

console.log(require('path').dirname(__filename)) // /Users/null/meet-nodejs/module

console.log(__dirname === require('path').dirname(__filename)) // true

8. module 对象

module 在每个模块中表示对当前模块的引用。 而 module.exports 又可以通过全局对象 exports 来引用。module 并不是一个全局对象,而更像一个模块内部对象。

module.children

这个模块引入的所有模块对象

module.exports

module.exports 通过模块系统创建。有时它的工作方式与我们所想的并不一致,有时我们希望模块是一些类的实例。因此,要将导出对象赋值给 module.exports,但是导出所需的对象将分配绑定本地导出变量,这可能不是我们想要的结果。

// a.js

const EventEmitter = require('events')

module.exports = new EventEmitter()

// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(() => {
  module.exports.emit('ready')
}, 1000)
const a = require('./a')
a.on('ready', () => {
  console.log('module a is ready')
})

需要注意,分配给 module.exports 的导出值必须能立刻获取到,当使用回调时其不能正常执行。

exports 别名

exports 可以做为 module.exports 的一个引用。和任何变量一样,如果为它分配新值,其旧值将会失效:

function require(...) {
  // ...
  ((module, exports) => {
    // Your module code here
    exports = some_func;        // re-assigns exports, exports is no longer
                                // a shortcut, and nothing is exported.
    module.exports = some_func; // makes your module export 0
  })(module, module.exports);
  return module;
}
  • module.filename - 模块解析后的完整文件名

  • module.id - 用于区别模块的标识符,通常是完全解析后的文件名。

  • module.loaded - 模块是否加载完毕

  • module.parent - 父模块,即:引入这个模块的模块

  • module.require(id)

  • module.require 提供了类似 require()的功能,可以从最初的模块加载一个模块

上一节: [Node.js 入门系列] 查询字符串 querystring 模块

下一节: [Node.js 入门系列] 缓冲器 Buffer 模块

「 Dart Js Ts 」给前端工程师的一张Dart语言入场券

前言

各位大佬好久不见啊~

啊~好久没写文章了,惭愧惭愧。🐶

小 null 最近跑去写 Flutter 了 ~

Flutter 使用 Dart 语言进行开发,小 null 在写 Flutter 的过程中发现 Dart 和 Javascript/Typescript 有些相似之处~

wordclouds

本文分享上图中这些相似之处,希望能帮助到打算上车的你~

You might already know Dart. - from 10 good reasons to learn Dart

是的,你还没开始学 Dart,可能就对它很熟悉了。

Dart 的「 前世今生 . 衰落与崛起 」

Dart 语言的诞生

2011 年 9 月,网络上出现了一封标题为"Future of JavaScript"的谷歌内部电子邮件,邮件中表明,由于 Javascript 语言发展缓慢,谷歌内部正在开发一门比 JavaScript 更好的 web 语言。这门新语言的目标是实现 JavaScript 所能实现的一切。它的主要目标是"保持 JavaScript 的动态特性,但要有更好的性能配置文件,并能适应大型项目的工具"。它还可以交叉编译成 JavaScript。这种语言作为技术预览版向更广泛的世界发布,并命名为 Dart。 - 引自 《Dart in Action》

2011 年 10 月 10 日的 GOTO 大会上,谷歌的两位工程师 Lars Bak (V8 JavaScript engine 项目组长..)和 Gilad Bracha (实现定制 Java/JVM 规范,JVM 规范主要贡献者..) 发布了"Dart",也验证了之前 email 传闻。Dart 是一种全新的编程语言,旨在帮助开发者构建 Web 应用程序。

2011goto

对 Dart 语言开发团队有兴趣的话~可戳 👉Dart 语言背后有哪些大牛?

Dart 1.0 《Dart 1.0: A stable SDK for structured web apps》

2013 年 11 月 14 日,谷歌发布 Dart 1.0 版。Dart 1.0 版本发布,不但推出了 Dart 语言 1.0 版本而且还推出了相关开源工具箱和配套的编辑器。为了推广 Dart,Google 在 Chrome 内置了 DartVM 引擎(已在 2015 年移除),彼时 JavaScript 因为 NodeJs 生态的崛起而焕发了第二春,而 Dart 却不温不火,且因为其运行效率饱受诟病。

就这样,Dart 还在 2018 年 "荣获 20 大糟糕语言榜首",总结 「 Javascript 很"忙",Dart 很"惨" 」。

2018-worst-lang

Dart 2.0 《Announcing Dart 2 Stable and the Dart Web Platform》

2018 年 8 月 8 日,谷歌发布 Dart2.0 版本。谷歌对 Dart 进行全新改版,从底层重构了 Dart 语言,加入了很多面向未来的新特性,语言性能大幅提供。Dart 开发团队总结了 Dart1.0 版本的优缺点,决定打造一个运行更快、更加安全的强类型语言 Dart2.0(在 Dart2.0 之前,Dart 是一门弱类型语言。此次发布谷歌不仅发布了 Dart 2.0 稳定版,而且还重写了 Dart web platform。新版的 web platform 提供了一套高性能、可扩展的生产力工具。

Flutter 发布 《Flutter 1.0: Google’s Portable UI Toolkit》

Google 内部用 Dart 编写孵化了一个移动开发框架 Sky,之后又被命名为 Flutter,进入了移动跨平台开发的领域。

2018 年 12 月 4 日,谷歌发布 Flutter 1.0 版本。

Flutter 是谷歌开源的移动应用开发 SDK,使用 Flutter 可以直接开发 Android 和 iOS 应用。其最大的特点就是一套代码多平台运行、高性能和 Hot Reload(热重载)。谷歌即将发布 Fuchsia 系统就以 Flutter 为主要开发框架。Flutter 采用 Dart 作为其底层语言。Dart 也由于 Flutter 美好未来而得到众多开发者的青睐。

Fuchsia 技术选型,Dart 笑到最后

Android 和 Chrome OS 可能是谷歌最知名的 OS 项目,但实际上这两年曝光量逐渐增大的是谷歌正在开发的第三个操作系统——Fuchsia。Fuchsia 是一个开源项目,类似于 AOSP(Android 开放源代码项目),但 Fuchsia 可以运行各种设备,从智能家居设备到笔记本电脑和手机等等。它也被认为是建立在一个谷歌构建的名为“zircon”的全新内核之上,而不是构成 Android 和 Chrome 操作系统基础的 Linux 内核。

近日谷歌 Fuchsia 网站上更新了一则“Fuchsia Programming Language Policy”的文档,详细解释了 Fuchsia 项目在编程语言方面的选型考虑。据官方文档披露,C/C++、Dart、Rust、Go 语言都是 Fuchsia 开发的候选语言,除了老牌编程语言 C 和 C++ 的江湖地位稳固得到了官方开发人员的认可以外,新兴编程语言中,Dart 击败了 Rust 和 Go 语言,成为用户 UI 界面的正式官方语言。

Javascript 🆚 Dart

变量声明

// javascript

var name = 'null仔'

// dart

var name = 'null仔'

与 Javascript 一样,在 Dart 中,我们可以使用 var 定义变量。

不一样的是,在 Dart 中,变量都是引用类型,也就是说所有的变量都是对象,所以 Dart 是一门完全面向对象的语言。

Dart 是类型安全的,所以当你使用 var 关键字定义变量时,本质其实就是具体类型的引用。

比如上文代码其实就是一个 String 类型对象的引用,这个对象的内容是 null 仔 。

在 Dart 中,声明一个未初始化的变量,变量的类型可以更改,它的初始值是 null。

variable

在 Dart 中,声明一个初始化的变量,变量类型不能再更改 。

variable

常量声明

// javascript

const name = 'null仔';

// dart

const name = 'null仔';

与 Javascript 一样,在 Dart 中,我们可以使用 const 定义常量。

Dart 中,还可以使用 final 定义常量,由于本文主要将与 Javascript 的相似点,这里就不细说了。

constant

模版字符串

// javascript

const name = 'null仔';

const word = `My name is ${name}`;

// dart

const name = 'null仔';

const word = 'My name is $name';

与 Javascript 一样,Dart 同样支持模板字符串,语法为:${expression},如果expression是一个变量,那么可以省略{},即为$varibale。

如果表达式的结果是一个对象,那么会调用对象的 toString()方法。

template-string

箭头函数

// javascript

  const getName = (name) => name;

  getName('null仔');

// dart

  String getName(name) => name;

  getName('null仔');

与 Javascript 一样,Dart 同样支持箭头函数,如果函数只包含一个表达式,可以使用箭头表达式方法进行简写。=> 后面的表达式将作为函数的返回结果。

扩展运算符 (Spread Operator)

// javascript

  const list=[1,2,3,4,5];

  [0,...list,6];

// dart

  const list=[1,2,3,4,5];

  [0,...list,6];

Dart v2.3 引入了 Spread Operator,我们在 Javascript 中很喜欢用的神器,在 Dart 中也可以用啦~嗯,真香~

spread

参数默认值与可选参数

// javascript

  function getInfo({name='null仔',age}){
    console.log(`大家好,我是${name},今年${age}岁`);
  }
  getInfo({age:18});

// dart

  void getInfo({name="null仔",age}){
    print('大家好,我是$name,今年$age岁');
  }
  getInfo(age:18);

与 Javascript 相似,Dart 支持函数参数默认值与可选参数,Get it ~

default

async/await 函数

// javascript

  async function getData(){

    const name= await new Promise((resolve)=>setTimeout(()=>resolve('null仔'),1000));

    console.log(name);  // null仔
  }
  getData();

// dart

  Future getData() async{

    String name =  await Future.delayed(Duration(seconds: 1),()=>'null仔');

    print(name);  // null仔
  }
  getData();

与 Javascript 相同,Dart 也提供了 async/await 语法糖,让我们更好的处理异步操作~

Javascript async 函数返回的是 Promise 对象,而 Dart async 函数返回的是 Future 对象~

async

级联函数(链式调用)

// javascript

new Promise((r) => {
  r(1)
})
  .then((res) => ++res)
  .then((res) => ++res)
  .then((res) => console.log(++res)) // 4

// dart

 List<int> list =

   []..addAll([1,2,3,4,5])
     ..replaceRange(0,1,[6])
     ..sort((a,b)=>a-b);

  print(list);  // [2, 3, 4, 5, 6]

在 Javascript 中 我们一般通过手动 "return this" 来实现链式调用,而 Dart 提供了 Cascade (级联运算符) .. 帮我们实现链式调用~ 真香!

Cascade

模块导入和导出 import

Javascript 和 Dart 都使用 import 来导入模块,不过不同的是,Dart 并不需要使用 export 来导出模块。

// 完全导入

// javascript

import abc from "abc";
import * as xx from "abc";

// dart

import 'package:abc/abc';

// 部分导入

// javascript

import { xx } from "abc";

// dart

import 'package:abc/abc' show xxx; // 只导出其中一个对象/方法 xxx
import 'package:abc/abc' hide xxx; // 导出模块时不导出xxx

类 class

//javascript

class Person{
  // 私有属性提案
  #age=0;
  // 构造函数及参数默认值
  constructor(name='null仔'){
    this.name=name;
  }
  // 实例方法
  getName(){
    console.log(this.name);
  }
  // 静态方法
  static say(){
    console.log(`hello world`);
  }
  // getter && setter
  get age(){
    return this.#age;
  }
  set age(value){
    this.#age=value;
  }
}

//dart

class Person{
  // 私有属性
  int _age;
  String name;
   // 构造函数及参数默认值
  Person({this.name='null仔'});
   // 实例方法
  void getName(){
    print(this.name);
  }
   // 静态方法
  static say(){
    print("hello world");
  }
  // getter && setter
  int get age =>this._age;
  set age(int value)=>this._age=value;
}

fx

Typescript 🆚 Dart

泛型

Typescript 与 Dart 中都存在泛型,下面我们以一个简单的泛型函数简单介绍下~

// typescript

  function identity<T>(arg: T): T {
    return arg;
  };

  identity<String>('null仔'); // null仔

  identity<Number>(18); // 18

// dart

  T identity<T>(T arg){
    return arg;
  }

  identity<String>('null仔'); // null仔

  identity<int>(18);  // 18

fx

Typescript Type Assertion 🆚 Dart as 运算符

类型断言(Type Assertion)可以用来手动指定一个值的类型。

 as 类型

as-dart

as

Typescript Optional Chaining 🆚 Dart ?. 运算符

TypeScript 3.7 实现了呼声最高的 ECMAScript 功能之一:可选链(Optional Chaining)!

终于不用再写 一坨长长臭臭的&& 运算符执行中间属性检查 和 null/undefined 判断了~

// before
if (foo && foo.bar && foo.bar.baz) {
  // ...
}
// after

if (foo?.bar?.baz) {
  // ...
}

Dart 提供了?.运算符,我们来瞧瞧~

// typescript

let foo;

console.log(foo?.bar?.baz);


// dart

var foo;

print(foo?.bar?.baz);

optional-chaining

Typescript Nullish Coalescing 🆚 Dart ?? 运算符

TypeScript 3.7 实现了另一个即将推出的 ECMAScript 功能是 空值合并运算符(nullish coalescing operator)!

?? 运算符可以在处理 null 或 undefined 时“回退”到一个默认值上 !

// typescript

let x = foo ?? bar()

// 等价于

let x = foo !== null && foo !== void 0 ? foo : bar()

Dart 提供了??运算符,我们来瞧瞧~

// typescript
  let age;

  function setAge() {
    age = 18;
  }

  age ?? setAge();

  console.log(age) // 18

// dart

  var age;

  void setAge() {
    age = 18;
  }

  age ?? setAge();

  print(age); // 18

nullish coalescing

参考

Dart 语言的前世今生

lazyload

仓库:

lazyload-Vanilla JavaScript plugin for lazyloading images

源码实现:

/*!
 * Lazy Load - JavaScript plugin for lazy loading images
 *
 * Copyright (c) 2007-2019 Mika Tuupola
 *
 * Licensed under the MIT license:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Project home:
 *   https://appelsiini.net/projects/lazyload
 *
 * Version: 2.0.0-rc.2
 *
 */

(function (root, factory) {
  // umd export
  if (typeof exports === "object") {
    module.exports = factory(root);
  } else if (typeof define === "function" && define.amd) {
    define([], factory);
  } else {
    root.LazyLoad = factory(root);
  }
})(
  typeof global !== "undefined" ? global : this.window || this.global,
  function (root) {
    "use strict";

    if (typeof define === "function" && define.amd) {
      root = window;
    }

    const defaults = {
      src: "data-src",
      srcset: "data-srcset",
      selector: ".lazyload",
      root: null,
      rootMargin: "0px",
      threshold: 0,
    };

    /**
     * Merge two or more objects. Returns a new object.
     * @private
     * @param {Boolean}  deep     If true, do a deep (or recursive) merge [optional]
     * @param {Object}   objects  The objects to merge together
     * @returns {Object}          Merged values of defaults and options
     */
    const extend = function () {
      let extended = {};
      let deep = false;
      let i = 0;
      let length = arguments.length;

      /* Check if a deep merge */
      if (Object.prototype.toString.call(arguments[0]) === "[object Boolean]") {
        deep = arguments[0];
        i++;
      }

      /* Merge the object into the extended object */
      let merge = function (obj) {
        for (let prop in obj) {
          if (Object.prototype.hasOwnProperty.call(obj, prop)) {
            /* If deep merge and property is an object, merge properties */
            if (
              deep &&
              Object.prototype.toString.call(obj[prop]) === "[object Object]"
            ) {
              extended[prop] = extend(true, extended[prop], obj[prop]);
            } else {
              extended[prop] = obj[prop];
            }
          }
        }
      };

      /* Loop through each object and conduct a merge */
      for (; i < length; i++) {
        let obj = arguments[i];
        merge(obj);
      }

      return extended;
    };

    function LazyLoad(images, options) {
      // merget default options and options
      this.settings = extend(defaults, options || {});
      this.images = images || document.querySelectorAll(this.settings.selector);
      this.observer = null;
      this.init();
    }

    LazyLoad.prototype = {
      init: function () {
        /* Without observers load everything and bail out early. */
        // 不支持IntersectionObserver API,直接加载图片
        if (!root.IntersectionObserver) {
          this.loadImages();
          return;
        }

        let self = this;
        let observerConfig = {
          root: this.settings.root, // 祖先元素,null或未设置,默认使用顶级文档元素
          rootMargin: this.settings.rootMargin, // 计算交叉时添加到根(root)边界盒bounding box的矩形偏移量
          threshold: [this.settings.threshold], // 阀值(0-1),监听对象的交叉区域与边界区域的比率
        };
        // 使用IntersectionObserver API观察目标元素与root元素的交叉状态
        this.observer = new IntersectionObserver(function (entries) {
          Array.prototype.forEach.call(entries, function (entry) {
            // 目标元素与root元素交叉区域超过threshold阀值
            if (entry.isIntersecting) {
              // 停止监听
              self.observer.unobserve(entry.target);
              // 目前元素 src等属性赋值
              let src = entry.target.getAttribute(self.settings.src);
              let srcset = entry.target.getAttribute(self.settings.srcset);
              // img元素对src赋值,否则对backgroundImage赋值
              if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                  entry.target.src = src;
                }
                if (srcset) {
                  entry.target.srcset = srcset;
                }
              } else {
                entry.target.style.backgroundImage = "url(" + src + ")";
              }
            }
          });
        }, observerConfig);
        Array.prototype.forEach.call(this.images, function (image) {
          // 对目标元素进行监听
          self.observer.observe(image);
        });
      },
      //  加载后销毁
      loadAndDestroy: function () {
        if (!this.settings) {
          return;
        }
        this.loadImages();
        this.destroy();
      },
      // 加载图片
      loadImages: function () {
        if (!this.settings) {
          return;
        }

        let self = this;
        Array.prototype.forEach.call(this.images, function (image) {
          let src = image.getAttribute(self.settings.src);
          let srcset = image.getAttribute(self.settings.srcset);
          if ("img" === image.tagName.toLowerCase()) {
            if (src) {
              image.src = src;
            }
            if (srcset) {
              image.srcset = srcset;
            }
          } else {
            image.style.backgroundImage = "url('" + src + "')";
          }
        });
      },
      // 销毁
      destroy: function () {
        if (!this.settings) {
          return;
        }
        // 解除监听
        this.observer.disconnect();
        this.settings = null;
      },
    };
    // window.lazyload register
    root.lazyload = function (images, options) {
      return new LazyLoad(images, options);
    };
    // jquery plugin register
    if (root.jQuery) {
      const $ = root.jQuery;
      $.fn.lazyload = function (options) {
        options = options || {};
        options.attribute = options.attribute || "data-src";
        new LazyLoad($.makeArray(this), options);
        return this;
      };
    }

    return LazyLoad;
  }
);

收获:

lazyload2.x 版本的核心实现主要是使用了 IntersectionObserver API。

IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。可以很简单优雅的判断目标元素是否出现在可视区域内,从而进行处理。

IntersectionObserver API 的兼容性在 IE 全军覆没,但是 w3c 实现了IntersectionObserver polyfill,使得 IE 可以兼容到 7+,真香!

  • 深入了解了 layload 2.x 实现

  • 对 IntersectionObserver API 使用有了更进一步的理解

[Node.js 入门系列] 压缩 zlib 模块

压缩 zlib 模块

在流传输过程中,为减少传输数据加快传输速度,往往会对流进行压缩。

HTTP 流就是如此,为提高网站响应速度,会在服务端进行压缩,客户端收到数据后再进行相应的解压。

Node.js 中的 Zlib 模块提供了流压缩与解压缩功能,Zlib 模块提供了对 Gzip/Gunzip、Deflate/Inflate、DeflateRaw/InflateRaw 类的绑定,这些类可以实现对可读流/可写流的压缩与解压。

关于 gzip 与 deflate

deflate(RFC1951)是一种压缩算法,使用 LZ77 和哈弗曼进行编码。gzip(RFC1952)一种压缩格式,是对 deflate 的简单封装,gzip = gzip 头(10 字节) + deflate 编码的实际内容 + gzip 尾(8 字节)。在 HTTP 传输中,gzip 是一种常用的压缩算法,使用 gzip 压缩的 HTTP 数据流,会在 HTTP 头中使用 Content-Encoding:gzip 进行标识。

HTTP Request Header 中 Accept-Encoding 是浏览器发给服务器,声明浏览器支持的解压类型

Accept-Encoding: gzip, deflate, br

HTTP Response Header 中 Content-Encoding 是服务器告诉浏览器 使用了哪种压缩类型

Content-Encoding: gzip

对 web 性能优化有所了解的同学,相信对 gzip 都不陌生,我们就通过 gzip 来了解 zlib 模块.

1. 文件压缩/解压

文件压缩

const zlib = require('zlib')
const fs = require('fs')
const gzip = zlib.createGzip()
const inp = fs.createReadStream('zlib.txt')
const out = fs.createWriteStream('zlib.txt.gz')
inp.pipe(gzip).pipe(out)

文件解压

const zlib = require('zlib')
const fs = require('fs')
const gunzip = zlib.createGunzip()
const inp = fs.createReadStream('./un-zlib.txt.gz')
const out = fs.createWriteStream('un-zlib.txt')
inp.pipe(gunzip).pipe(out)

2. 服务端 gzip 压缩

const fs = require('fs')
const http = require('http')
const zlib = require('zlib')
const filepath = './index.html'

const server = http.createServer((req, res) => {
  const acceptEncoding = req.headers['accept-encoding']
  if (acceptEncoding.includes('gzip')) {
    const gzip = zlib.createGzip()
    res.writeHead(200, {
      'Content-Encoding': 'gzip'
    })
    fs.createReadStream(filepath)
      .pipe(gzip)
      .pipe(res)
  } else {
    fs.createReadStream(filepath).pipe(res)
  }
})

server.listen(4396)

上一节: [Node.js 入门系列] 统一资源定位符 url 模块

下一节: [Node.js 入门系列] 流 stream 模块

[Node.js 入门系列] 域名服务器 dns 模块

域名服务器 dns 模块

DNS(Domain Name System,域名系统),DNS 协议运行在 UDP 协议之上,使用端口号 53。DNS 是因特网上作为域名和 IP 地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。简单的说,就是把域名(网址)解析成对应的 IP 地址。Node.js 的 dns 模块,提供了 DNS 解析功能。当使用 dns 模块中的 net.connect(80, 'github.com/webfansplz')方法 或 http 模块的 http.get({ host: 'github.com/webfansplz' })方法时,在其底层会使用 dns 模块中的 dns.lookup 方法进行域名解析。

dns 模块的两种域名解析方式

1.使用操作系统底层的 DNS 服务解析

使用操作系统底层的 DNS 服务进行域名解析时,不需要连接到网络仅使用系统自带 DNS 解析功能。这个功能由 dns.lookup()方法实现。

dns.lookup(hostname[, options], callback):将一个域名(如:'www.baidu.com')解析为第一个找到的 A 记录(IPv4)或 AAAA 记录(IPv6)

hostname 表示要解析的域名。

options 可以是一个对象或整数。如果没有提供 options 参数,则 IP v4 和 v6 地址都可以。如果 options 是整数,则必须是 4 或 6。如果 options 是对象时,会包含以下两个可选参数:

  • family:可选,IP 版本。如果提供,必须是 4 或 6。不提供则,IP v4 和 v6 地址都可以

  • hints:可选。如果提供,可以是一个或者多个 getaddrinfo 标志。若不提供,则没有标志会传给 getaddrinfo。

callback 回调函数,参数包含(err, address, family)。出错时,参数 err 是 Error 对象。address 参数表示 IP v4 或 v6 地址。family 参数是 4 或 6,表示 address 协议版本。

const dns = require('dns')

dns.lookup(`www.github.com`, (err, address, family) => {
  if (err) throw err
  console.log('地址: %j 地址族: IPv%s', address, family) // 地址: "13.229.188.59" 地址族: IPv4
})

2.连接到 DNS 服务器解析域名

在 dns 模块中,除 dns.lookup()方法外都是使用 DNS 服务器进行域名解析,解析时需要连接到网络。

dns.resolve(hostname[, rrtype], callback):将一个域名(如 'www.baidu.com')解析为一个 rrtype 指定类型的数组

hostname 表示要解析的域名。

rrtype 有以下可用值:

rrtype records 包含 结果的类型 快捷方法
'A' IPv4 地址 (默认) string dns.resolve4()
'AAAA' IPv6 地址 string dns.resolve6()
'ANY' 任何记录 Object dns.resolveAny()
'CNAME' 规范名称记录 string dns.resolveCname()
'MX' 邮件交换记录 Object dns.resolveMx()
'NAPTR' 名称权限指针记录 Object dns.resolveNaptr()
'NS' 名称服务器记录 string dns.resolveNs()
'PTR' 指针记录 string dns.resolvePtr()
'SOA' 开始授权记录 Object dns.resolveSoa()
'SRV' 服务记录 Object dns.resolveSrv()
'TXT' 文本记录 string[] dns.resolveTxt()

callback 回调函数,参数包含(err, addresses)。出错时,参数 err 是 Error 对象。addresses 根据记录类型的不同返回值也不同。

const dns = require('dns')

dns.resolve('www.baidu.com', 'A', (err, addresses) => {
  if (err) throw err
  console.log(`IP地址 : ${JSON.stringify(addresses)}`) // IP地址 : ["163.177.151.110","163.177.151.109"]
})

// or

dns.resolve4('www.baidu.com', (err, addresses) => {
  if (err) throw err
  console.log(`IP地址 : ${JSON.stringify(addresses)}`) // IP地址 : ["163.177.151.110","163.177.151.109"]
})

反向 DNS 查询

将 IPv4 或 IPv6 地址解析为主机名数组。

使用 getnameinfo 方法将传入的地址和端口解析为域名和服务

dns.reverse(ip, callback)

ip 表示要反向解析的 IP 地址。

callback 回调函数,参数包含(err, domains)。出错时,参数 err 是 Error 对象。domains 解析后的域名数组。

dns.reverse('8.8.8.8', (err, domains) => {
  if (err) throw err
  console.log(domains) // [ 'dns.google' ]
})

dns.lookupService(address, port, callback)

address 表示要解析的 IP 地址字符串。

port 表示要解析的端口号。

callback 回调函数,参数包含(err, hostname, service)。出错时,参数 err 是 Error 对象。

dns.lookupService('127.0.0.1', 80, function(err, hostname, service) {
  if (err) throw err
  console.log('主机名:%s,服务类型:%s', hostname, service) // 主机名:localhost,服务类型:http
})

上一节: [Node.js 入门系列] 缓冲器 Buffer 模块

12道vue高频原理面试题,你能答出几道?

1. Vue 响应式原理

vue-reactive

核心实现类:

Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新

Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。

Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

Watcher 和 Dep 的关系

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

依赖收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集

  2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集

  3. render()的过程,触发 render watcher 依赖收集

  4. re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。

派发更新

  1. 组件中对响应的数据进行了修改,触发 setter 的逻辑

  2. 调用 dep.notify()

  3. 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。

原理

当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。

一句话总结:

vue.js 采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调

2. computed 的实现原理

computed 本质是一个惰性求值的观察者。

computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。

其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,

computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

3. computed 和 watch 有什么区别及运用场景?

区别

computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

运用场景

运用场景:

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用  watch  选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

4. 为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组

push();
pop();
shift();
unshift();
splice();
sort();
reverse();

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

5. Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数  a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key;
  const map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
}

6. 谈一谈 nextTick 的原理

JS 运行机制

JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

event-loop

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

for (macroTask of macroTaskQueue) {
  // 1. Handle current MACRO-TASK
  handleMacroTask();

  // 2. Handle all MICRO-TASK
  for (microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}

在浏览器环境中 :

常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate

常见的 micro task 有 MutationObsever 和 Promise.then

异步更新队列

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 方法的实现原理:

  1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行

  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

  3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

7. vue 是如何对数组方法进行变异的 ?

我们先来看看源码

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});

/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

8. Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?

因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once

Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。

class Vue {
  constructor() {
    //  事件通道调度中心
    this._events = Object.create(null);
  }
  $on(event, fn) {
    if (Array.isArray(event)) {
      event.map(item => {
        this.$on(item, fn);
      });
    } else {
      (this._events[event] || (this._events[event] = [])).push(fn);
    }
    return this;
  }
  $once(event, fn) {
    function on() {
      this.$off(event, on);
      fn.apply(this, arguments);
    }
    on.fn = fn;
    this.$on(event, on);
    return this;
  }
  $off(event, fn) {
    if (!arguments.length) {
      this._events = Object.create(null);
      return this;
    }
    if (Array.isArray(event)) {
      event.map(item => {
        this.$off(item, fn);
      });
      return this;
    }
    const cbs = this._events[event];
    if (!cbs) {
      return this;
    }
    if (!fn) {
      this._events[event] = null;
      return this;
    }
    let cb;
    let i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }
  $emit(event) {
    let cbs = this._events[event];
    if (cbs) {
      const args = [].slice.call(arguments, 1);
      cbs.map(item => {
        args ? item.apply(this, args) : item.call(this);
      });
    }
    return this;
  }
}

10. 说说 Vue 的渲染过程

render

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 函数解析 template,生成 ast(抽象语法树)

  • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)

  • generate 函数生成 render 函数字符串

  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象

  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

11. 聊聊 keep-alive 的实现原理和缓存策略

export default {
  name: "keep-alive",
  abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
  props: {
    include: patternTypes, // 被缓存组件
    exclude: patternTypes, // 不被缓存组件
    max: [String, Number] // 指定缓存大小
  },

  created() {
    this.cache = Object.create(null); // 缓存
    this.keys = []; // 缓存的VNode的键
  },

  destroyed() {
    for (const key in this.cache) {
      // 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    // 监听缓存/不缓存组件
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },

  render() {
    // 获取第一个子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name不在inlcude中或者在exlude中 直接返回vnode
      // check pattern
      const name: ?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode;
      }

      const { cache, keys } = this;
      // 获取键,优先获取组件的name字段,否则是组件的tag
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }
      // 不命中缓存,把 vnode 设置进缓存
      else {
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // keepAlive标记位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};

原理

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名

  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

  3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

  4. 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)

  5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心**是“如果数据最近被访问过,那么将来被访问的几率也更高”。

LRU

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

12. vm.$set()实现原理是什么?

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

export function set(target: Array<any> | Object, key: any, val: any): any {
  // target 为数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
    target.length = Math.max(target.length, key);
    // 利用数组的splice变异方法触发响应式
    target.splice(key, 1, val);
    return val;
  }
  // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__;
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 进行响应式处理
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}
  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式

  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值

  3. 如果 target 本身就不是响应式,直接赋值

  4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

[Node.js 入门系列] 逐行读取 readline 模块

逐行读取 readline 模块

readline 模块是一个流内容的逐行读取模块,通过 require('readline')引用模块。你可以用 readline 模块来读取 stdin,可以用来逐行读取文件流,也可用它来在控制台和用户进行一些交互。

const readline = require("readline");

const rl = readline.createInterface({
  //  监听的可读流
  input: process.stdin,
  //  逐行读取(Readline)数据要写入的可写流
  output: process.stdout
});

rl.question("你如何看待 null-cli ?", answer => {
  console.log(`感谢您的宝贵意见:${answer}`);
  rl.close();
});

readline

很多有趣的 CLI 工具是基于 readline 造的哦,有兴趣的同学也可以尝试~

上一节: [Node.js 入门系列] 流 stream 模块

下一节: [Node.js 入门系列] 查询字符串 querystring 模块

[Node.js 入门系列] 文件操作系统 fs 模块

文件操作系统 fs 模块

在一些场景下,我们需要对文件进行 增删改查等操作, Nodejs 提供了 fs 模块,让我们对文件进行操作.

下面我们来介绍几个经常用的 API

1. 读取文件

const fs = require("fs");
const fs = require("fs");

// 异步读取
fs.readFile("./index.txt", "utf8", (err, data) => {
  console.log(data); //  Hello Nodejs
});

// 同步读取
const data = fs.readFileSync("./index.txt", "utf8");

console.log(data); //  Hello Nodejs

// 创建读取流
const stream = fs.createReadStream("./index.txt", "utf8");

// 这里可以看到fs.createReadStream用到了我们前面介绍的events eventEmitter.on() 方法来监听事件
stream.on("data", data => {
  console.log(data); // Hello Nodejs
});

2. 写入/修改文件

写入文件时,如果文件不存在,则会创建并写入,如果文件存在,会覆盖文件内容.

const fs = require("fs");
// 异步写入
fs.writeFile("./write.txt", "Hello Nodejs", "utf8", err => {
  if (err) throw err;
});
// 同步写入
fs.writeFileSync("./writeSync.txt", "Hello Nodejs");
// 文件流写入
const ws = fs.createWriteStream("./writeStream.txt", "utf8");
ws.write("Hello Nodejs");
ws.end();

3. 删除文件/文件夹

  • 删除文件
// 异步删除文件
fs.unlink("./delete.txt", err => {
  if (err) throw err;
});

// 同步删除文件
fs.unlinkSync("./deleteSync.txt");
  • 删除文件夹
// 异步删除文件夹
fs.rmdir("./rmdir", err => {
  if (err) throw err;
});

// 同步删除文件夹
fs.rmdirSync("./rmdirSync");

4. 创建文件夹

// 异步创建文件夹
fs.mkdir("./mkdir", err => {
  if (err) throw err;
});

// 同步创建文件夹
fs.mkdirSync("./mkdirSync");

5. 重命名文件/文件夹

const fs = require("fs");

// 异步重命名文件
fs.rename("./rename.txt", "./rename-r.txt", err => {
  if (err) throw err;
});

// 同步重命名文件夹
fs.renameSync("./renameSync", "./renameSync-r");

6. 复制文件/文件夹

const fs = require("fs");

// 异步复制文件
fs.copyFile("./copy.txt", "./copy-c.txt", (err, copyFiles) => {
  if (err) throw err;
});

// 同步复制文件夹
fs.copyFileSync("./null", "null-c");

7. 文件夹状态- 文件/文件夹

const fs = require("fs");

// 异步获取文件状态
fs.stat("./dir", (err, stats) => {
  if (err) throw err;
  // 是否是文件类型
  console.log(stats.isFile()); // false
  // 是否是文件夹类型
  console.log(stats.isDirectory()); // true
});

// 同步获取文件状态
const stats = fs.statSync("./stats.txt");

// 是否是文件类型
console.log(stats.isFile()); // true
// 是否是文件夹类型
console.log(stats.isDirectory()); // false

在一些复杂的操作场景下,fs 模块要做很多判断与处理 ,这里我推荐大家使用 fs-extra,它在 fs 的基础上扩展了一些方法,让一些复杂操作更简便!

上一节: [Node.js 入门系列] 本地路径 path 模块

下一节: [Node.js 入门系列] 全局对象 process 进程

📦 5 个有趣的 Node.js 库,带你走进 彩色 Node.js 世界 🎉

1.chalk

chalk

GitHub:https://github.com/chalk/chalk

GitHub Stars : ✨13.5k

这是一个能给你的 log 染色的库,让你的代码靓起来 !!

黑白的 console.log 令人绝望 ? 让我们给它点颜色看看 !!!

chalk

2.Inquirer.js

chalk

GitHub:https://github.com/SBoudrias/Inquirer.js

GitHub Stars : ✨11.4k

这是一个非常好看的交互式命令行用户界面,用它来定制你的 CLI 吧 !

chalk

3.ora

ora

GitHub:https://github.com/sindresorhus/ora

GitHub Stars : ✨5.4k

优雅的转圈圈,让你的等待不再煎熬~

ora

4.figlet.js

cliui

GitHub:https://github.com/patorjk/figlet.js

GitHub Stars : ✨1k

Creating FIGfont ~

5.boxen

boxen

GitHub:https://github.com/sindresorhus/boxen

GitHub Stars : ✨600+

给你的代码画上界限,守护自己的地盘~

[实践系列]模拟实现new操作符

new操作符做了什么?

image

  • 创建一个新对象
  • 新对象继承了构造函数的原型
  • 构造函数的this指向新对象,并执行构造函数。
  • 最后隐式的返回this,即新对象。

如果我们在构造函数中进行显式返回,会发生什么?

image

image

由此我们可以得出结论:构造函数如果返回基本类型,则会忽略,还是返回原来的this(新对象).如果返回的是引用类型,则会对该返回值进行处理,返回该返回值。

new操作符的模拟实现

知道new操作符做了什么,我们要实现就不难了,直接上代码!

image

[Node.js 入门系列] 全局对象 process 进程

全局对象 process 进程

process 对象是一个 Global 全局对象,你可以在任何地方使用它,而无需 require。process 是 EventEmitter 的一个实例,所以 process 中也有相关事件的监听。使用 process 对象,可以方便处理进程相关操作。

process 常用属性

进程命令行参数: process.argv

process.argv 是一个当前执行进程折参数组,第一个参数是 node,第二个参数是当前执行的.js 文件名,之后是执行时设置的参数列表。

node index.js --tips="hello nodejs"

/*
[ '/usr/local/bin/node',
  'xxx/process/index.js',
  '--tips=hello nodejs' ]
*/

Node 的命令行参数数组:process.execArgv

process.execArgv 属性会返回 Node 的命令行参数数组。

node --harmony index.js --version

console.log(process.execArgv);  // [ '--harmony' ]

console.log(process.argv);

/*
[ '/usr/local/bin/node',
  'xxx/process/index.js',
  '--version' ]
*/

Node 编译时的版本: process.version

process.version 属性会返回 Node 编译时的版本号,版本号保存于 Node 的内置变量 NODE_VERSION 中。

console.log(process.version); // v10.15.3

当前进程的 PID process.pid

process.pid 属性会返回当前进程的 PID。

console.log("process PID: %d", process.pid);

//process PID: 10086

process 常用方法

当前工作目录 process.cwd()

process.cwd()方法返回进程当前的工作目录

console.log(process.cwd()); // /Users/null/nodejs/process

终止当前进程:process.exit([code])

process.exit()方法终止当前进程,此方法可接收一个退出状态的可选参数 code,不传入时,会返回表示成功的状态码 0。

process.on("exit", function(code) {
  console.log("进程退出码是:%d", code); // 进程退出码是:886
});

process.exit(886);

nodejs 微任务: process.nextTick()

process.nextTick()方法用于延迟回调函数的执行, nextTick 方法会将 callback 中的回调函数延迟到事件循环的下一次循环中,与 setTimeout(fn, 0)相比 nextTick 方法效率高很多,该方法能在任何 I/O 之前调用我们的回调函数。

console.log("start");
process.nextTick(() => {
  console.log("nextTick cb");
});
console.log("end");

// start
// end
// nextTick cb

process 标准流对象

process 中有三个标准备流的操作,与 其他 streams 流操作不同的是,process 中流操作是同步写,阻塞的。

标准错误流: process.stderr

process.stderr 是一个指向标准错误流的可写流 Writable Stream。console.error 就是通过 process.stderr 实现的。

标准输入流:process.stdin

process.stdin 是一个指向标准输入流的可读流 Readable Stream。

process.stdin.setEncoding("utf8");

process.stdin.on("readable", () => {
  let chunk;
  // 使用循环确保我们读取所有的可用数据。
  while ((chunk = process.stdin.read()) !== null) {
    if (chunk === "\n") {
      process.stdin.emit("end");
      return;
    }
    process.stdout.write(`收到数据: ${chunk}`);
  }
});

process.stdin.on("end", () => {
  process.stdout.write("结束监听");
});

process-stdin

标准输出流:process.stdout

process.stdout 是一个指向标准输出流的可写流 Writable Stream。console.log 就是通过 process.stdout 实现的

console.log = function(d) {
  process.stdout.write(d + "\n");
};

console.log("Hello Nodejs"); // Hello Nodejs

上一节: [Node.js 入门系列] 文件操作系统 fs 模块

下一节: [Node.js 入门系列] http 模块

[Node.js 入门系列] http 模块

http 模块

http 模块是 Node.js 中非常重要的一个核心模块。通过 http 模块,你可以使用其 http.createServer 方法创建一个 http 服务器,也可以使用其 http.request 方法创建一个 http 客户端。(本文先不说),Node 对 HTTP 协议及相关 API 的封装比较底层,其仅能处理流和消息,对于消息的处理,也仅解析成报文头和报文体,但是不解析实际的报文头和报文体内容。这样不仅解决了 HTTP 原本比较难用的特性,也可以支持更多的 HTTP 应用.

http.IncomingMessage 对象

IncomingMessage 对象是由 http.Server 或 http.ClientRequest 创建的,并作为第一参数分别传递给 http.Server 的'request'事件和 http.ClientRequest 的'response'事件。

它也可以用来访问应答的状态、头文件和数据等。 IncomingMessage 对象实现了 Readable Stream 接口,对象中还有一些事件,方法和属性。

在 http.Server 或 http.ClientRequest 中略有不同。

http.createServer([requestListener])创建 HTTP 服务器

实现 HTTP 服务端功能,要通过 http.createServer 方法创建一个服务端对象 http.Server。

这个方法接收一个可选传入参数 requestListener,该参数是一个函数,传入后将做为 http.Server 的 request 事件监听。不传入时,则需要通过在 http.Server 对象的 request 事件中单独添加。

var http = require("http");

// 创建server对象,并添加request事件监听器
var server = http.createServer(function(req, res) {
  res.writeHeader(200, { "Content-Type": "text/plain" });
  res.end("Hello Nodejs");
});

// 创建server对象,通过server对象的request事件添加事件事件监听器
var server = new http.Server();
server.on("request", function(req, res) {
  res.writeHeader(200, { "Content-Type": "text/plain" });
  res.end("Hello Nodejs");
});

http.Server 服务器对象

http.Server 对象是一个事件发射器 EventEmitter,会发射:request、connection、close、checkContinue、connect、upgrade、clientError 事件。

其中 request 事件监听函数为 function (request, response) { },该方法有两个参数:request 是一个 http.IncomingMessage 实例,response 是一个 http.ServerResponse 实例。

http.Server 对象中还有一些方法,调用 server.listen 后 http.Server 就可以接收客户端传入连接。

http.ServerResponse

http.ServerResponse 对象用于响应处理客户端请求。

http.ServerResponse 是 HTTP 服务器(http.Server)内部创建的对象,作为第二个参数传递给 'request'事件的监听函数。

http.ServerResponse 实现了 Writable Stream 接口,其对于客户端的响应,本质上是对这个可写流的操作。它还是一个 EventEmitter,包含:close、finish 事件。

创建一个 http.Server

创建 http.Server 使用 http.createServer()方法,为了处理客户端请求,需要在服务端监听来自客户的'request'事件。

'request'事件的回调函数中,会返回一个 http.IncomingMessage 实例和一个 http.ServerResponse。

const http = require("http");
/**
 * @param {Object} req 是一个http.IncomingMessag实例
 * @param {Object} res 是一个http.ServerResponse实例
 */
const server = http.createServer((req, res) => {
  console.log(req.headers);
  res.end(`Hello Nodejs`);
});

server.listen(3000);

http.ServerResponse 实例是一个可写流,所以可以将一个文件流转接到 res 响应流中。下面示例就是将一张图片流传送到 HTTP 响应中:

const http = require("http");
/**
 * @param {Object} req 是一个http.IncomingMessag实例
 * @param {Object} res 是一个http.ServerResponse实例
 */
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "image/jpg" });
  const r = require("fs").createReadStream("./kobe.jpg");
  r.pipe(res);
});

server.listen(3000);

上一节: [Node.js 入门系列] 全局对象 process 进程

下一节: [Node.js 入门系列] 统一资源定位符 url 模块

[Node.js 入门系列] 流 stream 模块

流 stream 模块

流(stream)是 Node.js 中处理流式数据的抽象接口。 stream 模块用于构建实现了流接口的对象。

Node.js 提供了多种流对象。 例如,HTTP 服务器的请求和 process.stdout 都是流的实例。

流可以是可读的、可写的、或者可读可写的。 所有的流都是 EventEmitter 的实例。

尽管理解流的工作方式很重要,但是 stream 模块主要用于开发者创建新类型的流实例。 对于以消费流对象为主的开发者,极少需要直接使用 stream 模块。

stream 类型

Node.js 中有四种基本的流类型:

  • Writable - 可写入数据的流(例如 fs.createWriteStream())。

  • Readable - 可读取数据的流(例如 fs.createReadStream())。

  • Duplex - 可读又可写的流(例如 net.Socket)。

  • Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())。

用于消费流的 API

const http = require("http");

const server = http.createServer((req, res) => {
  // req 是一个 http.IncomingMessage 实例,它是可读流。
  // res 是一个 http.ServerResponse 实例,它是可写流。

  let body = "";
  // 接收数据为 utf8 字符串,
  // 如果没有设置字符编码,则会接收到 Buffer 对象。
  req.setEncoding("utf8");

  // 如果添加了监听器,则可读流会触发 'data' 事件。
  req.on("data", chunk => {
    body += chunk;
  });

  // 'end' 事件表明整个请求体已被接收。
  req.on("end", () => {
    try {
      const data = JSON.parse(body);
      // 响应信息给用户。
      res.write(typeof data);
      res.end();
    } catch (er) {
      // json 解析失败。
      res.statusCode = 400;
      return res.end(`错误: ${er.message}`);
    }
  });
});

server.listen(1337);

// curl localhost:1337 -d "{}"
// object
// curl localhost:1337 -d "\"foo\""
// string
// curl localhost:1337 -d "not json"
// 错误: Unexpected token o in JSON at position 1

当数据可以从流读取时,可读流会使用 EventEmitter API 来通知应用程序 (比如例子中的 req data 事件)。 从流读取数据的方式有很多种。

可写流(比如例子中的 res)会暴露了一些方法,比如 write() 和 end() 用于写入数据到流。

可写流和可读流都通过多种方式使用 EventEmitter API 来通讯流的当前状态。Duplex 流和 Transform 流都是可写又可读的。

对于只需写入数据到流或从流消费数据的应用程序,并不需要直接实现流的接口,通常也不需要调用 require('stream')。

对于大部分的 nodejs 开发者来说,平常并不会直接用到 stream 模块,但是理解 stream 流的运行机制却是尤其重要的.

上一节: [Node.js 入门系列] 压缩 zlib 模块

下一节: [Node.js 入门系列] 逐行读取 readline 模块

不要肆无忌惮地在你的项目中使用 ES78910 了~

不要肆无忌惮地在你的项目中使用 ES78910 了~

如果我有故事,你有 star 吗~

故事背景

在一次 code review 中,我在我们的项目(项目基于 vue-cli 3 创建)中找到了这句代码 MDN

[1, 2, [3, 4, [5, 6]]].flat(Infinity); // [1, 2, 3, 4, 5, 6]

嗯嗯~多维数组扁平化,很酷炫霸拽吊炸天~

我再一看兼容性..

图片

打扰了..

先脑补一波互怼的画面

我 : 老哥,你这个 API 是 ES2019 新特性啊,万万使不得啊~

图片

同事: 我有 vue-cli 3 啊~ 他封装好了 Babel 啊, 我大 vue-cli 3 天下无敌啊~

我 : 我...我真想跳起来打他的膝盖啊~ 凭一句话 好像是毫无说服力啊,是时候表演真正的技术了..

说(睡)服同事

core-js

Modular standard library for JavaScript. Includes polyfills for ECMAScript up to 2019: promises, symbols, collections, iterators, typed arrays, many other features, ECMAScript proposals, some cross-platform WHATWG / W3C features and proposals like URL. You can load only required features or use it without global namespace pollution.

core-js 是 babel 转码的核心包,它使用 es5 API实现了一些 ECMAScript 到 2019 年的 polyfills,并且提供按需加载,且使用它不污染全局名称空间。

@babel/preset-env

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). This both makes your life easier and JavaScript bundles smaller!

@babel/preset-env 是一个智能插件集合,允许您使用最新的 JavaScript ,而不需要对目标环境所需的API转换(以及可选的 browser polyfills)进行微管理。这不仅使您的生活更轻松,而且 JavaScript 包也更小!

下面我们简单了解一下它的其中两个核心配置项.

useBuiltIns

"usage" | "entry" | false, defaults to false.

(提供"usage" | "entry" | false 三个配置项,默认值为 false)

This option configures how @babel/preset-env handles polyfills.

(这个配置项用来决定@babel/preset-env 如何处理 polyfills)

When either the usage or entry options are used, @babel-preset-env will add direct references to core-js modules as bare imports (or requires). This means core-js will be resolved relative to the file itself and needs to be accessible.

(当使用 usage 或 entry 配置项时,@babel-preset-env 将直接(entry)引用(或按需(usage)引入)core-js 模块,这意味着 core-js 将对文件本身进行解析)

Since @babel/polyfill was deprecated in 7.4.0, we recommend directly adding core-js and setting the version via the corejs option.

(由于@babel/polyfill 在 7.4.0 中被弃用,我们建议直接添加 core-js 并通过 corejs 选项设置版本。)

corejs

2, 3 or { version: 2 | 3, proposals: boolean }, defaults to 2.

(指定 corejs 版本,2 或 3,默认值为 2)

This option only has an effect when used alongside useBuiltIns: usage or useBuiltIns: entry, and ensures @babel/preset-env injects the correct imports for your core-js version.

(此选项只有在 useBuiltIns 选项配置为 entry 或 usage 时才生效,并确保@babel/preset-env 为您的 core-js 版本注入正确的引入)

Ok,接下来我们来看一哈 Vue-cli 3 的 babel 配置~

// babel.config.js
module.exports = {
  presets: ['@vue/app']
};

可以看到 vue-cli 3 这边用的预设集合是自己封装的@vue/app,我们在 node_modules 找到@vue/app 的 package.json

图片

// package.json

"dependencies":{

  "@babel/preset-env": "^7.0.0 < 7.4.0",

  "core-js": "^2.6.5"
}

可以看到依赖里有 core-js 2.x 版本和@babel/preset-env ~

打开@vue/app 的 index.js

//index.js  部分代码

const envOptions = {
  spec,
  loose,
  debug,
  modules,
  targets,
  useBuiltIns, //  划重点,此处值 已定义为 'usage'
  ignoreBrowserslistConfig,
  configPath,
  include,
  exclude: polyfills.concat(exclude || []),
  shippedProposals,
  forceAllTransforms
};

presets.unshift([require('@babel/preset-env'), envOptions]);

由上,我们可以得出结论,vue-cli 使用的 vue-preset-app 封装了@babel/preset-env`,且配置

useBuiltIns: 'usage';

corejs 没做配置,所以为默认值 2

useBuiltIns: 'usage';
corejs: 2;

这么一看,结合我们上面所讲知识,flat 是应该会被转成 es5 咯 ? 啪啪啪,打脸?

倔强的我上 github 打开了 core-js

图片

奇怪的是,我在 core-js 2.65 版本里并没有找到 flat API的实现.

图片

求知欲爆炸的我,翻了 core-js@3 的文档,找到了以下这段话

图片

发现 Array.prototype.flat API是在 core-js@3 才加入的。

图片

结论

vue-cli 3 使用的是 core-js2.x 版本,所以并不能转义 Arrary.prototype.flat 这个API。

实践

得出理论 不实践一波 好像不符合我的风格啊~

npm init -y

npm i @babel/core @babel/preset-env -D
const babel = require('@babel/core');

const code = `[1, 2, 3, 4, [5, 6, [7, 8]]].flat(Infinity);`;
const ast = babel.transform(code, {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 2
      }
    ]
  ]
});
// 用core-js@2 来看看转码后的结果
console.log(ast.code);

// "use strict";

// [1, 2, 3, 4, [5, 6, [7, 8]]].flat(Infinity);
const babel = require('@babel/core');

const code = `[1, 2, 3, 4, [5, 6, [7, 8]]].flat(Infinity);`;
const ast = babel.transform(code, {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3
      }
    ]
  ]
});
// 用core-js@3 来看看转码后的结果
console.log(ast.code);

// "use strict";

// require("core-js/modules/es.array.flat");

// require("core-js/modules/es.array.unscopables.flat");

// [1, 2, 3, 4, [5, 6, [7, 8]]].flat(Infinity);

ok~ 完美验证结论! 代码地址

vue-cli将在version 4 支持core-js 3

图片

思考

不可否认 vue-cli 是一个非常优秀的脚手架,它提供了一个很 nice 的工程化解决方案。

webpack 构建

babel 编译

postcss 兼容

...

我在一些简历上 经常看到 熟练使用 xxx 脚手架,难道我们应该熟练的是使用脚手架吗 ?

我们在享受工具带给我们的便利跟快感时,是不是也应该想想自己对前端工程化了解多少呢 ?

[Node.js进阶系列]Koa源码精读一

下面我们来看 Koa 的部分源码~

'use strict';

/**
 * Module dependencies.
 */

const util = require('util');
const only = require('only');

/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */

class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */

  constructor() {
    super();

    this.proxy = false;
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }

  /**
   * Return JSON representation.
   * We only bother showing settings.
   *
   * @return {Object}
   * @api public
   */

  toJSON() {
    return only(this, ['subdomainOffset', 'proxy', 'env']);
  }

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    return this.toJSON();
  }
}

???,哈哈哈,看完是不是一脸黑人问号???

node-only 模块

我们先来了解一下 only 模块是用来做什么的?

都说年龄和体重是女人的秘密 ...

const only = require('only');
const girlInfo = {
  name: 'lily',
  age: 25,
  weight: 88
};
const lily = only(girlInfo, ['name']);
console.log(lily); //  lily
const letMeSee = only(girlInfo, ['age', 'weight']);
console.log(letMeSee); //  { age: 25, weight: 88 }

这下你能明白 only 模块的用途了吗? (你好*啊.png)

demo 代码

util.inspect

util 模块是 Node.js 的核心模块,提供常用的函数集合,这里我们就上面两个 API 做简单介绍.

util.inspect(object[, options])

作用:返回 object 的字符串表示,主要用于调试。 附加的 options 可用于改变格式化字符串的某些方面。

util.inspect.custom

作用:可被用于声明自定义的查看函数。

Talk is cheap ...停停停,我写我写..

const util = require('util');
function app() {
  if (util.inspect.custom) {
    this[util.inspect.custom] = () => {
      return 'hello,util.inspect';
    };
  }
  return this;
}
console.log(util.inspect(app())); //  hello,util.inspect

demo 代码

subdomainOffset

这是什么神仙属性呢?我们先来看看 Koa 源码 request.js,里面有这么一段。

  /**
   * Return subdomains as an array.
   *
   * Subdomains are the dot-separated parts of the host before the main domain
   * of the app. By default, the domain of the app is assumed to be the last two
   * parts of the host. This can be changed by setting `app.subdomainOffset`.
   *
   * For example, if the domain is "tobi.ferrets.example.com":
   * If `app.subdomainOffset` is not set, this.subdomains is
   * `["ferrets", "tobi"]`.
   * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
   *
   * @return {Array}
   * @api public
   */

  get subdomains() {
    const offset = this.app.subdomainOffset;
    const hostname = this.hostname;
    if (net.isIP(hostname)) return [];
    return hostname
      .split('.')
      .reverse()
      .slice(offset);
  },

又给我看天文数字???好好好,Show U YoudaoDict~

  /**
   *  以数组的形式返回子域
   *  子域是主机主域前的点分隔部分应用程序的域。默认情况下,应用程序的域被假定为最后两个域主机的部分。这可以通过设置“app.subdomainOffset”来更改。
   *  举个例子,如果域名是“tobi.ferrets.example.com”:如果未设置subdomainOffset。子域是["ferrets", "tobi"],如果设置subdomainOffset等于3,那么子域就是["tobi"]
   * @return {Array}
   * @api public
   */

  get subdomains() {
    const offset = this.app.subdomainOffset;
    const hostname = this.hostname;
    if (net.isIP(hostname)) return [];
    return hostname
      .split('.')
      .reverse()
      .slice(offset);
  },

所以 subdomainOffset 其实就是子域名的偏移量~

env

env 属性大家都很熟悉了,就是 node 的环境变量 NODE_ENV 值。

proxy

proxy 属性值可以设置为 true 或 false,它的作用在于是否获取真正的客户端 ip 地址~

这里我们引用某位大佬的一波解释~

要知道, 我们在实际运用中, 可能会使用很多的代理服务器, 包括我们常见的正向代理与反向代理, 虽然代理的用处很大, 但是无法避免地我们有时需要知晓真正的客户端的请求 ip。

而其实实际上, 服务器并不知道真正的客户端请求 ip, 即使你使用 socket.remoteAddrss 属性来查看,

因为这个请求是代理服务器转发给服务器的, 幸好代理服务器例如 nginx 提供了一个 HTTP 头部来记录每次代理服务器的源 IP 地址, 也就是 X-Forwarded-For 头部.形式如下:

X-Forwarded-For: 192.168.210.13, 210.112.40.13, 43.56.210.10

如果一个请求跳转了很多代理服务器, 那么 X-Forwarded-For 头部的 ip 地址就会越多, 第一个就是原始的客户端请求 ip, 第二个就是第一个代理服务器 ip, 以此类推.

当然, X-Forwarded-For 并不完全可信, 因为中间的代理服务器可能会”使坏”更改某些 IP.

而 koa 中 proxy 属性的设置就是如果使用 true, 那么就是使用 X-Forwarded-For 头部的第一个
ip 地址, 如果使用 false, 则使用 server 中的 socket.remoteAddress 属性值.

除了 X-Forwarded-For 之外, proxy 还会影响 X-Forwarded-proto 的使用, 和 X-Forwarded-For 一样, X-Forwarded-proto 记录最开始的请求连接使用的协议类型(HTTP/HTTPS), 因为客户端与
服务端之间可能会存在很多层代理服务器, 而代理服务器与服务端之间可能只是使用 HTTP 协议, 并没有使用 HTTPS,

所以 proxy 属性为 true 的话, koa 的 protocol 属性会去取 X-Forwarded-proto 头部的值(koa 中 protocol 属性会先使用 tlsSocket.encrypted 属性来判断是否是 https 协议, 如果是则直接返回 ‘https’).

浪子回头

现在,你在回头看上面的源码,是不是感觉 so easy 呢?

上一节:Koa 源码分析之 Context 对象
下一节:Koa 源码精读二

1

1

[Node.js 入门系列] 本地路径 path 模块

本地路径 path 模块

Node.js 提供了 path 模块,用于处理文件路径和目录路径 . 不同操作系统 表现有所差异 !

1. 获取路径的目录名

const path = require('path')

path.dirname('/path/example/index.js') // /path/example

2. 获取路径的扩展名

const path = require('path')

path.extname('/path/example/index.js') // .js

3. 是否是绝对路径

const path = require('path')

path.isAbsolute('/path/example/index.js') // true

path.isAbsolute('.') // false

4. 拼接路径片段

path.join('/path', 'example', './index.js') // /path/example/index.js

5. 将路径或路径片段的序列解析为绝对路径。

path.resolve('/foo/bar', './baz')
// 返回: '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/')
// 返回: '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 如果当前工作目录是 /home/myself/node,
// 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'

6. 规范化路径

path.normalize('/path///example/index.js') //  /path/example/index.js

7. 解析路径

path.parse('/path/example/index.js')

/*
 { root: '/',
  dir: '/path/example',
  base: 'index.js',
  ext: '.js',
  name: 'index' }
*/

8. 序列化路径

path.format({
  root: '/',
  dir: '/path/example',
  base: 'index.js',
  ext: '.js',
  name: 'index'
}) // /path/example/index.js

9. 获取 from 到 to 的相对路径

path.relative('/path/example/index.js', '/path') // ../..

上一节: [Node.js 入门系列] 事件触发器 events 模块

下一节: [Node.js 入门系列] 文件操作系统 fs 模块

[实践系列]浏览器缓存

目录

 1. DNS 缓存   // 虽说跟标题关系不大,了解一下也不错
 2. CDN 缓存   // 虽说跟标题关系不大,了解一下也不错
 3. 浏览器缓存 // 本文将重点介绍并实践  

DNS 缓存

什么是DNS

全称 Domain Name System ,即域名系统。

万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。DNS协议运行在UDP协议之上,使用端口号53。

DNS解析

简单的说,通过域名,最终得到该域名对应的IP地址的过程叫做域名解析(或主机名解析)。

www.dnscache.com (域名)  - DNS解析 -> 11.222.33.444 (IP地址)

DNS缓存

有dns的地方,就有缓存。浏览器、操作系统、Local DNS、根域名服务器,它们都会对DNS结果做一定程度的缓存。

DNS查询过程如下:

  1. 首先搜索浏览器自身的DNS缓存,如果存在,则域名解析到此完成。

  2. 如果浏览器自身的缓存里面没有找到对应的条目,那么会尝试读取操作系统的hosts文件看是否存在对应的映射关系,如果存在,则域名解析到此完成。

  3. 如果本地hosts文件不存在映射关系,则查找本地DNS服务器(ISP服务器,或者自己手动设置的DNS服务器),如果存在,域名到此解析完成。

  4. 如果本地DNS服务器还没找到的话,它就会向根服务器发出请求,进行递归查询。

戳此处详细了解DNS解析过程

CDN 缓存

什么是CDN

全称 Content Delivery Network,即内容分发网络。

摘录一个形象的比喻,来理解CDN是什么。

10年前,还没有火车票代售点一说,12306.cn更是无从说起。那时候火车票还只能在火车站的售票大厅购买,而我所在的小县城并不通火车,火车票都要去市里的火车站购买,而从我家到县城再到市里,来回就是4个小时车程,简直就是浪费生命。后来就好了,小县城里出现了火车票代售点,甚至乡镇上也有了代售点,可以直接在代售点购买火车票,方便了不少,全市人民再也不用在一个点苦逼的排队买票了。

简单的理解CDN就是这些代售点(缓存服务器)的承包商,他为买票者提供了便利,帮助他们在最近的地方(最近的CDN节点)用最短的时间(最短的请求时间)买到票(拿到资源),这样去火车站售票大厅排队的人也就少了。也就减轻了售票大厅的压力(起到分流作用,减轻服务器负载压力)。

用户在浏览网站的时候,CDN会选择一个离用户最近的CDN边缘节点来响应用户的请求,这样海南移动用户的请求就不会千里迢迢跑到北京电信机房的服务器(假设源站部署在北京电信机房)上了。

CDN缓存

关于CDN缓存,在浏览器本地缓存失效后,浏览器会向CDN边缘节点发起请求。类似浏览器缓存,CDN边缘节点也存在着一套缓存机制。CDN边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过http响应头中的

Cache-control: max-age   //后面会提到

的字段来设置CDN边缘节点数据缓存时间。

当浏览器向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点就会向服务器发出回源请求,从服务器拉取最新数据,更新本地缓存,并将最新数据返回给客户端。 CDN服务商一般会提供基于文件后缀、目录多个维度来指定CDN缓存时间,为用户提供更精细化的缓存管理。

CDN 优势

  1. CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低。
  2. 大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源服务器的负载。

戳此处详细了解CDN工作过程

浏览器缓存(http缓存)

对着这张图先发呆30秒~
image

什么是浏览器缓存

image

简单来说,浏览器缓存其实就是浏览器保存通过HTTP获取的所有资源,是浏览器将网络资源存储在本地的一种行为。

缓存的资源去哪里了?

你可能会有疑问,浏览器存储了资源,那它把资源存储在哪里呢?

memory cache

MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。
目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

disk cache

DiskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。

- memory cache disk cache
相同点 只能存储一些派生类资源文件 只能存储一些派生类资源文件
不同点 退出进程时数据会被清除 退出进程时数据不会被清除
存储资源 一般脚本、字体、图片会存在内存当中 一般非脚本会存在内存当中,如css等

因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。

三级缓存原理 (访问缓存优先级)

  1. 先在内存中查找,如果有,直接加载。
  2. 如果内存中不存在,则在硬盘中查找,如果有直接加载。
  3. 如果硬盘中也没有,那么就进行网络请求。
  4. 请求获取的资源缓存到硬盘和内存。

浏览器缓存的分类

  1. 强缓存

  2. 协商缓存

浏览器再向服务器请求资源时,首先判断是否命中强缓存,再判断是否命中协商缓存!

浏览器缓存的优点

1.减少了冗余的数据传输

2.减少了服务器的负担,大大提升了网站的性能

3.加快了客户端加载网页的速度

强缓存

浏览器在加载资源时,会先根据本地缓存资源的 header 中的信息判断是否命中强缓存,如果命中则直接使用缓存中的资源不会再向服务器发送请求。

这里的 header 中的信息指的是 expires 和 cahe-control.

Expires

该字段是 http1.0 时的规范,它的值为一个绝对时间的 GMT 格式的时间字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。

Cache-Control

Cache-Control 是 http1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对时间,例如 Cache-Control:max-age=3600,代表着资源的有效期是 3600 秒。cache-control 除了该字段外,还有下面几个比较常用的设置值:

no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。

no-store:禁止使用缓存,每一次都要重新请求数据。

public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。

private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。

Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。

协商缓存

当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据 header 中的部分信息来判断是否命中缓存。如果命中,则返回 304 ,告诉浏览器资源未更新,可使用本地的缓存。

这里的 header 中的信息指的是 Last-Modify/If-Modify-Since 和 ETag/If-None-Match.

Last-Modify/If-Modify-Since

浏览器第一次请求一个资源的时候,服务器返回的 header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间。

当浏览器再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存。

如果命中缓存,则返回 304,并且不会返回资源内容,并且不会返回 Last-Modify。

缺点:

短时间内资源发生了改变,Last-Modified 并不会发生变化。

周期性变化。如果这个资源在一个周期内修改回原来的样子了,我们认为是可以使用缓存的,但是 Last-Modified 可不这样认为,因此便有了 ETag。

ETag/If-None-Match

与 Last-Modify/If-Modify-Since 不同的是,Etag/If-None-Match 返回的是一个校验码。ETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据浏览器上送的 If-None-Match 值来判断是否命中缓存。

与 Last-Modified 不一样的是,当服务器返回 304 Not Modified 的响应时,由于 ETag 重新生成过,response header 中还会把这个 ETag 返回,即使这个 ETag 跟之前的没有变化。

Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304。

总结

当浏览器再次访问一个已经访问过的资源时,它会这样做:

1.看看是否命中强缓存,如果命中,就直接使用缓存了。

2.如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。

3.如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。

4.否则,返回最新的资源。

实践加深理解

talk is cheap , show me the code 。让我们通过实践得真知~

在实践时,注意浏览器控制台Network的image按钮不要打钩。

由于时间问题,以下我们只对强缓存的Cache-Control和协商缓存的ETag进行实践,其他小伙伴们可以自己实践~

package.json

{
 "name": "webcache",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
   "cache": "nodemon ./index.js"
 },
 "author": "webfansplz",
 "license": "MIT",
 "devDependencies": {
   "@babel/core": "^7.2.2",
   "@babel/preset-env": "^7.2.3",
   "@babel/register": "^7.0.0",
   "koa": "^2.6.2",
   "koa-static": "^5.0.0"
 },
 "dependencies": {
   "nodemon": "^1.18.9"
 }
}

.babelrc

{
 "presets": [
   [
     "@babel/preset-env",
     {
       "targets": {
         "node": "current"
       }
     }
   ]
 ]
}

index.js

require('@babel/register');
require('./webcache.js');

webcache.js

import Koa from 'koa';
import path from 'path';
//静态资源中间件
import resource from 'koa-static';
const app = new Koa();
const host = 'localhost';
const port = 4396;
app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
  console.log(`server is listen in ${host}:${port}`);
});

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>前端缓存</title>
    <style>
      .web-cache img {
        display: block;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div class="web-cache"><img src="./web.png" /></div>
  </body>
</html>

我们用koa先起个web服务器,然后用koa-static这个中间件做静态资源配置,并在static文件夹下放了index.html和web.png。

Ok,接下来我们来启动服务。

npm run cache

server is listen in localhost:4396。

接下来我们打开浏览器输入地址:

localhost:4396

image

完美~(哈哈,猪仔别喷我,纯属娱乐效果)

Ok!!!接下来我们来实践下强缓存。~

Cache-Control

webcache.js

import Koa from 'koa';
import path from 'path';
//静态资源中间件
import resource from 'koa-static';
const app = new Koa();
const host = 'localhost';
const port = 4396;

app.use(async (ctx, next) => {
 // 设置响应头Cache-Control 设置资源有效期为300秒
  ctx.set({
    'Cache-Control': 'max-age=300'  
  });
  await next();
});
app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
  console.log(`server is listen in ${host}:${port}`);
});

image
我们刷新页面可以看到响应头的Cache-Control变成了max-age=300。

我们顺便来验证下三级缓存原理

我们刚进行了网络请求,浏览器把web.png存进了磁盘和内存中。

根据三级缓存原理,我们会先在内存中找资源,我们来刷新页面。

image

我们在红线部分看到了, from memory cache。nice~

ok,接下来,我们关掉该页面,再重新打开。因为内存是存在进程中的,所以关闭该页面,内存中的资源也被释放掉了,磁盘中的资源是永久性的,所以还存在。

根据三级缓存原理,如果在内存中没找到资源,便会去磁盘中寻找!

image

from disk cache !!! ok,以上也就验证了三级缓存原理,相信你对缓存资源的存储也有了更深的理解了。

我们刚对资源设置的有效期是300秒,我们接下来来验证缓存是否失效。

300秒后。。。

image

我们通过返回值可以看到,缓存失效了。

通过以上实践,你是否对强缓存有了更深入的理解了呢?

Ok!!!接下来我们来实践下协商缓存。~

由于Cache-Control的默认值就是no-cache(需要进行协商缓存,发送请求到服务器确认是否使用缓存。),所以我们这里不用对Cache-Control进行设置!

ETag

//ETag support for Koa responses using etag.
npm install koa-tag -D
// etag works together with conditional-get
npm install koa-conditional-get -D

我们这里直接使用现成的插件帮我们计算文件的ETag值,站在巨人的肩膀上!

webcache.js

import Koa from 'koa';
import path from 'path';
//静态资源中间件
import resource from 'koa-static';
import conditional from 'koa-conditional-get';
import etag from 'koa-etag';
const app = new Koa();
const host = 'localhost';
const port = 4396;

// etag works together with conditional-get
app.use(conditional());
app.use(etag());
app.use(resource(path.join(__dirname, './static')));

app.listen(port, () => {
 console.log(`server is listen in ${host}:${port}`);
});

ok。第一次请求.
image
我们发现返回值里面已经有了Etag值。

接下来再请求的时候,浏览器将会带上If-None-Match请求头,并赋值为上一次返回头的Etag值,然后与 这次返回值的Etag值进行对比。如果一致则命中协商缓存。返回304 Not Modified。接下来我们来验证一下~

image
ok,如图所示,完美验证了上面的说法。

接下来我们修改web.png ,来验证是否资源改变时 协商缓存策略也就失效呢?

image

如图所示.协商缓存的实践也验证了原理。

大功告成

如果觉得有帮助到你,请给star/follow 支持下作者~

源码地址

参考文献

前端性能优化之缓存利用

[实践系列]Babel原理

Babel是什么?我为什么要了解它?

1. 什么是babel ?

Babel 是一个 JavaScript 编译器。他把最新版的javascript编译成当下可以执行的版本,简言之,利用babel就可以让我们在当前的项目中随意的使用这些新最新的es6,甚至es7的语法。

为了能用可爱的ES678910写代码,我必须了解它!

2. 可靠的工具来源于可怕的付出

August 27, 2018 by Henry Zhu

历经 2 年,4k 多次提交,50 多个预发布版本以及大量社区援助,我们很高兴地宣布发布 Babel 7。自 Babel 6 发布以来,已经过了将近三年的时间!发布期间有许多要进行的迁移工作,因此请在发布第一周与我们联系。Babel 7 是更新巨大的版本:我们使它编译更快,并创建了升级工具,支持 JS 配置,支持配置 "overrides",更多 size/minification 的选项,支持 JSX 片段,支持 TypeScript,支持新提案等等!

Babel开发团队这么辛苦的为开源做贡献,为我们开发者提供更完美的工具,我为什么不去了解它呢?

(OS:求求你别更啦.老子学不动啦~)

3. Babel担任的角色

August 27, 2018 by Henry Zhu

我想再次介绍下过去几年中 Babel 在 JavaScript 生态系统中所担任的角色,以此展开本文的叙述。

起初,JavaScript 与服务器语言不同,它没有办法保证对每个用户都有相同的支持,因为用户可能使用支持程度不同的浏览器(尤其是旧版本的 Internet Explorer)。如果开发人员想要使用新语法(例如 class A {}),旧浏览器上的用户只会因为 SyntaxError 的错误而出现屏幕空白的情况。

Babel 为开发人员提供了一种使用最新 JavaScript 语法的方式,同时使得他们不必担心如何进行向后兼容,如(class A {} 转译成 var A = function A() {})。

由于它能转译 JavaScript 代码,它还可用于实现新的功能:因此它已成为帮助 TC39(制订 JavaScript 语法的委员会)获得有关 JavaScript 提案意见反馈的桥梁,并让社区对语言的未来发展发表自己的见解。

Babel 如今已成为 JavaScript 开发的基础。GitHub 目前有超过 130 万个仓库依赖 Babel,每月 npm 下载量达 1700 万次,还拥有数百个用户,其中包括许多主要框架(React,Vue,Ember,Polymer)以及著名公司(Facebook,Netflix,Airbnb)等。它已成为 JavaScript 开发的基础,许多人甚至不知道它正在被使用。即使你自己没有使用它,但你的依赖很可能正在使用 Babel。

即使你自己没有使用它,但你的依赖很可能正在使用 Babel。怕不怕 ? 了解不了解 ?

Babel的运行原理

babel

1.解析

解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。

1.词法分析

词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。

你可以把令牌看作是一个扁平的语法片段数组:

 n * n;
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]

每一个 type 有一组属性来描述该令牌:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

和 AST 节点一样它们也有 start,end,loc 属性。

2.语法分析

语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

简单来说,解析阶段就是

code(字符串形式代码) -> tokens(令牌流) -> AST(抽象语法树)

Babel 使用 @babel/parser 解析代码,输入的 js 代码字符串根据 ESTree 规范生成 AST(抽象语法树)。Babel 使用的解析器是 babylon

什么是AST

2.转换

转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程。

Babel提供了@babel/traverse(遍历)方法维护这AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。

3.生成

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。

代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。

babel.png

实践前提

在这之前,你必须对Babel有了基本的了解,下面我们只简单的了解下babel的一些东西,以便于后面开发插件。

babel-core

babel-core是Babel的核心包,里面存放着诸多核心API,这里说下transform。

transform : 用于字符串转码得到AST 。

传送门

//安装
npm install  babel-core -D;

import babel from 'babel-core';
/*
 * @param {string} code 要转译的代码字符串
 * @param {object} options 可选,配置项
 * @return {object} 
*/
babel.transform(code:String,options?: Object)
//返回一个对象(主要包括三个部分):
{
    generated code, //生成码
    sources map, //源映射
    AST  //即abstract syntax tree,抽象语法树
}

babel-types

Babel Types模块是一个用于 AST 节点的 Lodash 式工具库(译注:Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程风格的众多工具函数), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。
传送门

npm install babel-types -D;  

import traverse from "babel-traverse";

import * as t from "babel-types";

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});

JS CODE -> AST

查看代码对应的AST树结构

Visitors (访问者)

当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。

访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// 你也可以先创建一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

注意: Identifier() { ... } 是 Identifier: { enter() { ... } } 的简写形式

这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。

Paths(路径)

AST 通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,或者也可以用Paths(路径)来简化这件事情。

Path 是表示两个节点之间连接的对象。

在某种意义上,路径是一个节点在树中的位置以及关于该节点各种信息的响应式 Reactive 表示。 当你调用一个修改树的方法后,路径信息也会被更新。 Babel 帮你管理这一切,从而使得节点操作简单,尽可能做到无状态。

Paths in Visitors(存在于访问者中的路径)

当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};

Babel插件规则

Babel的插件模块需要我们暴露一个function,function内返回visitor对象。

//函数参数接受整个Babel对象,这里将它进行解构获取babel-types模块,用来操作AST。

module.exports = function({types:t}){

    return {
        visitor:{
            
        }
    }
    
}

撸一个Babel ...插件 !!!

做一个简单的ES6转ES3插件:
1. let,const 声明 -> var 声明  
2. 箭头函数 -> 普通函数

文件结构

|-- index.js  程序入口
|-- plugin.js 插件实现
|-- before.js 转化前代码
|-- after.js  转化后代码
|-- package.json  

首先,我们先创建一个package.json。

npm init

package.json

{
  "name": "babelplugin",
  "version": "1.0.0",
  "description": "create babel plugin",
  "main": "index.js",
  "scripts": {
    "babel": "node ./index.js"
  },
  "author": "webfansplz",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.2.2"
  }
}

可以看到,我们首先下载了@babel/core作为我们的开发依赖,然后配置了npm run babel作为开发命令。

index.js

const { transform } = require('@babel/core');

const fs = require('fs');

//读取需要转换的js字符串
const before = fs.readFileSync('./before.js', 'utf8');

//使用babel-core的transform API 和插件进行字符串->AST转化。
const res = transform(`${before}`, {
  plugins: [require('./plugin')]
});

// 存在after.js删除
fs.existsSync('./after.js') && fs.unlinkSync('./after.js');
// 写入转化后的结果到after.js
fs.writeFileSync('./after.js', res.code, 'utf8');

我们首先来实现 功能 1. let,const 声明 -> var 声明

let code = 1;

我们通过传送门查看到上面代码对应的AST结构为

image

我们可以看到这句声明语句位于VariableDeclaration节点,我们接下来只要操作VariableDeclaration节点对应的kind属性就可以啦~

before.js

const a = 123;

let b = 456;

plugin.js

module.exports = function({ types: t }) {
  return {
   //访问者
    visitor: {
     //我们需要操作的访问者方法(节点)
      VariableDeclaration(path) {
        //该路径对应的节点
        const node = path.node;
        //判断节点kind属性是let或者const,转化为var
        ['let', 'const'].includes(node.kind) && (node.kind = 'var');
      }
    }
  };
};

ok~ 我们来看看效果!

npm run babel

after.js

var a = 123;

var b = 456;

没错,就是这么吊!!!功能1搞定,接下来实现功能2. 箭头函数 -> 普通函数 (this指向暂不做处理~)

我们先来看看箭头函数对应的节点是什么?

let add = (x, y) => {
  return x + y;
};

我们通过传送门查看到上面代码对应的AST结构为
image

我们可以看到箭头函数对应的节点是ArrowFunctionExpression。

接下来我们再来看看普通函数对应的节点是什么?

let add = function(x, y){
  return x + y;
};

我们通过传送门查看到上面代码对应的AST结构为
image

我们可以看到普通函数对应的节点是FunctionExpression。

所以我们的实现思路只要进行节点替换(ArrowFunctionExpression->FunctionExpression)就可以啦。

plugin.js

module.exports = function({ types: t }) {
  return {
    visitor: {
      VariableDeclaration(path) {
        const node = path.node;
        ['let', 'const'].includes(node.kind) && (node.kind = 'var');
      },
      //箭头函数对应的访问者方法(节点)
      ArrowFunctionExpression(path) {
       //该路径对应的节点信息  
        let { id, params, body, generator, async } = path.node;
        //进行节点替换 (arrowFunctionExpression->functionExpression)
        path.replaceWith(t.functionExpression(id, params, body, generator, async));
      }
    }
  };
};

满怀激动的

npm run babel

after.js

var add = function (x, y) {
  return x + y;
};

惊不惊喜 ? 意不意外 ? 你以为这样就结束了吗 ? 那你就太年轻啦。

我们经常会这样写箭头函数来省略return。

let add = (x,y) =>x + y;

我们来试试 这样能不能转义

npm run babel

GG.控制台飘红~

下面我直接贴下最后的实现,具体原因我觉得读者自己研究或许更有趣~

plugin.js

module.exports = function({ types: t }) {
  return {
    visitor: {
      VariableDeclaration(path) {
        const node = path.node;
        ['let', 'const'].includes(node.kind) && (node.kind = 'var');
      },
      ArrowFunctionExpression(path) {
        let { id, params, body, generator, async } = path.node;
        //箭头函数我们会简写{return a+b} 为 a+b    
        if (!t.isBlockStatement(body)) {    
          const node = t.returnStatement(body);
          body = t.blockStatement([node]);
        }
        path.replaceWith(t.functionExpression(id, params, body, generator, async));
      }
    }
  };
};

小功告成

源码地址

如果觉得有帮助到你,请给个star或者follow 支持下作者哈~接下来还会有很多干货哦!!!

参考文献

很棒的Babel手册

css-diff

Repository (仓库地址) : css-diff

Gain (收获) : css-diff 是一个对比 css 差异的库,使用它可以对比两个 css 文件的差异。收获见以下源码解析~

// a.css

.a {
  font-size: 16px;
  color: #fff;
}

// b.css

.a {
  font-size: 16px;
  color: #fff;
}

.b {
  font-weight: normal;
}
// index.js

require("css-diff")({
  files: ["./a.css", "./b.css"],
  omit: ["comment"],
  visual: true,
}).then(function (diff) {
  console.log(diff.visual); // 见下图
  console.log(diff.different); // ture
});

css-diff

源码解析

require("colors");

var cssParse = require("css-parse");
var Promise = require("bluebird");
var Diff = require("diff");
var Compiler = require("./lib/compiler.js");
var Path = require("path");

module.exports = function (options) {
  this.options = options;

  return (
    getContents
      .call(this, options.files)
      // 将两个css文件的cssom 传递给Diff.diffLines方法进行对比
      .spread(Diff.diffLines)
      // 生成差异
      .then(generateDiff)
      .catch(handleError)
  );
};

function handleError(e) {
  process.stderr.write(("Error: " + e.message + "\n").red.inverse);
  process.exit(1);
}
// 核心实现
function generateDiff(diff) {
  var different = false;
  var visual = diff.reduce(function (prev, part) {
    // 判断block是否有差异,很*的双三元判断
    /**
     * 新增区域使用绿色渲染差异
     * 移除区域使用红色渲染差异
     * 灰色代表相同部分
     * 总结: 灰色表示相同的部分,绿色表示多出来的部分,红色表示少了的部分.
     */
    var color = part.added ? "green" : part.removed ? "red" : "grey";
    // 灰色,表示两个文件不存在差异
    if (color !== "grey") {
      different = true;
    }

    return prev + part.value[color] + "\n";
  }, "");

  return {
    different: different,
    visual: visual,
  };
}

function getContents(files) {
  var _this = this;

  if (files.length < 2) {
    return new Error("you must pass 2 file paths in");
  }

  return Promise.all(
    files.map(function (path) {
      return Compiler(Path.resolve(path)).then(function (css) {
        // 获取css字符串,使用cssParse将css字符串转换为CSSOM
        // css string -> cssom
        var rules = cssParse(css).stylesheet.rules; // 输出结果可见下图
        // 返回 序列化后的 过滤omit参数选项的cssom规则类型数组
        // 这里有个小技巧,JSON.stringify使用了space参数,用于在输出JSON字符串中插入空格以提高可读性。
        return JSON.stringify(
          rules.filter(function (rule) {
            return !~_this.options.omit.indexOf(rule.type);
          }),
          null,
          4
        );
      });
    })
  );
}

a.css,b.css cssom rules output

rules

思考

// 以下代码,你觉得输出的会是什么呢?

require("css-diff")({
  files: ["./b.css", "./a.css"],
  omit: ["comment"],
  visual: true,
}).then(function (diff) {
  console.log(diff.visual);
  console.log(diff.different);
});

[实践系列]call,apply,bind走一个

三兄弟的作用.

apply.call.bind 都是为了改变函数运行时上下文(this指向)而存在的。

image

三兄弟的区别.

  • 三兄弟接收的第一个参数都是 要绑定的this指向.
  • apply的第二个参数是一个参数数组,call和bind的第二个及之后的参数作为函数实参按顺序传入。
  • bind不会立即调用,其他两个会立即调用。

image

接下来,我们来对三兄弟进行模拟实现

call的简易模拟实现(es6)

思路

  • 函数定义在哪里 ?

call是可以被所有方法调用的,所以毫无疑问的定义在 Function的原型上!

  • 函数接收参数 ?

绑定函数被调用时只传入第二个参数及之后的参数

  • 如何显式绑定this ?

如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。

ojbk..理清了思路.开撸

image

apply的简易模拟实现(es6)

apply实现的思路与call基本相同,我们只需要对参数进行不同处理即可

image

bind的简易模拟实现(es6)

这里只是做简易实现,不考虑new操作符的情况,之后会写个文章对这个知识点进行详解~

思路

  • 函数定义在哪里 ?

bind是可以被所有方法调用的,所以毫无疑问的定义在 Function的原型上!

  • 函数接收参数 ?

bind函数返回一个绑定函数,最终调用需要传入函数实参和绑定函数的实参!!

  • 如何显式绑定this ?

如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。

image

webpack打包原理 ? 看完这篇你就懂了 !

webpack

什么是 webpack ?

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -- 深入浅出 webpack 吴浩麟

webpack

webpack 核心概念

Entry

入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。

Output

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。

基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

Module

模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

Chunk

代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

Loader

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。

loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

Plugin

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。

插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

webpack 构建流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。

  3. 确定入口:根据配置中的 entry 找出所有的入口文件。

  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。

  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。

  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。

  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

实践加深理解,撸一个简易 webpack

1. 定义 Compiler 类

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {}
  // 重写 require函数,输出bundle
  generate() {}
}

2. 解析入口文件,获取 AST

我们这里使用@babel/parser,这是 babel7 的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树。

// webpack.config.js

const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'main.js'
  }
}
//
const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    const ast = Parser.getAst(this.entry)
  }
  // 重写 require函数,输出bundle
  generate() {}
}

new Compiler(options).run()

3. 找出所有依赖模块

Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    const { getAst, getDependecies } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
  }
  // 重写 require函数,输出bundle
  generate() {}
}

new Compiler(options).run()

4. AST 转换为 code

将 AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST转换为code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
    const code = getCode(ast)
  }
  // 重写 require函数,输出bundle
  generate() {}
}

new Compiler(options).run()

5. 递归解析所有依赖项,生成依赖关系图

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST转换为code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    // 解析入口文件
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判断有依赖对象,递归解析所有依赖项
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依赖关系图
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 文件路径,可以作为每个模块的唯一标识符
      filename,
      // 依赖对象,保存着依赖模块路径
      dependecies,
      // 文件内容
      code
    }
  }
  // 重写 require函数,输出bundle
  generate() {}
}

new Compiler(options).run()

6. 重写 require 函数,输出 bundle

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST转换为code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    // 解析入口文件
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判断有依赖对象,递归解析所有依赖项
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依赖关系图
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
    this.generate(dependencyGraph)
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 文件路径,可以作为每个模块的唯一标识符
      filename,
      // 依赖对象,保存着依赖模块路径
      dependecies,
      // 文件内容
      code
    }
  }
  // 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
  generate(code) {
    // 输出文件路径
    const filePath = path.join(this.output.path, this.output.filename)
    // 懵逼了吗? 没事,下一节我们捋一捋
    const bundle = `(function(graph){
      function require(module){
        function localRequire(relativePath){
          return require(graph[module].dependecies[relativePath])
        }
        var exports = {};
        (function(require,exports,code){
          eval(code)
        })(localRequire,exports,graph[module].code);
        return exports;
      }
      require('${this.entry}')
    })(${JSON.stringify(code)})`

    // 把文件内容写入到文件系统
    fs.writeFileSync(filePath, bundle, 'utf-8')
  }
}

new Compiler(options).run()

7. 看完这节,彻底搞懂 bundle 实现

我们通过下面的例子来进行讲解,先死亡凝视 30 秒

;(function(graph) {
  function require(moduleId) {
    function localRequire(relativePath) {
      return require(graph[moduleId].dependecies[relativePath])
    }
    var exports = {}
    ;(function(require, exports, code) {
      eval(code)
    })(localRequire, exports, graph[moduleId].code)
    return exports
  }
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

step 1 : 从入口文件开始执行

// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {
  // 重写require函数
  function require(moduleId) {
    console.log(moduleId) // ./src/index.js
  }
  // 从入口文件开始执行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

step 2 : 使用 eval 执行代码

// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {
  // 重写require函数
  function require(moduleId) {
    ;(function(code) {
      console.log(code) // "use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));
      eval(code) // Uncaught TypeError: Cannot read property 'code' of undefined
    })(graph[moduleId].code)
  }
  // 从入口文件开始执行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

可以看到,我们在执行"./src/index.js"文件代码的时候报错了,这是因为 index.js 里引用依赖 hello.js,而我们没有对依赖进行处理,接下来我们对依赖引用进行处理。

step 3 : 依赖对象寻址映射,获取 exports 对象

// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {
  // 重写require函数
  function require(moduleId) {
    // 找到对应moduleId的依赖对象,调用require函数,eval执行,拿到exports对象
    function localRequire(relativePath) {
      return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}
    }
    // 定义exports对象
    var exports = {}
    ;(function(require, exports, code) {
      // commonjs语法使用module.exports暴露实现,我们传入的exports对象会捕获依赖对象(hello.js)暴露的实现(exports.say = say)并写入
      eval(code)
    })(localRequire, exports, graph[moduleId].code)
    // 暴露exports对象,即暴露依赖对象对应的实现
    return exports
  }
  // 从入口文件开始执行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

这下应该明白了吧 ~ 可以直接复制上面代码到控制台输出哦~

完整代码地址戳我 👈

总结

Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

对 Webpack 的使用者来说,它是一个简单强大的工具; 对 Webpack 的开发者来说,它是一个扩展性的高系统。

Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。

参考

webpack 中文文档

深入浅出 webpack

前端应该会的23个linux常用命令

1. ls 命令 : 显示目录内容列表

Linux ls 命令用于显示指定工作目录下之内容(列出目前工作目录所含之文件及子目录)。

ls [-alrtAFR] [name...]

常用 options

  • -a 显示所有文件及目录 (ls 内定将文件名或目录名称开头为"."的视为隐藏档,不会列出)
  • -A 同 -a ,但不列出 "." (目前目录) 及 ".." (父目录)
  • -R 若目录下有文件,则以下之文件亦皆依序列出

ls

2. rm 命令 : 删除文件/目录

Linux rm 命令用于删除一个文件或者目录。

rm [options] [name...]

options:

  • -i 删除前逐一询问确认
  • -r 或-R:递归处理,将指定目录下的所有文件与子目录一并处理
  • -f:强制删除文件或目录

rm

rm

3. tail 命令 : 查看文件内容

tail 命令可用于查看文件的内容,有一个常用的参数 -f 常用于查阅正在改变的日志文件。

tail -f filename 会把 filename 文件里的最尾部的内容显示在屏幕上,并且不断刷新,只要 filename 更新就可以看到最新的文件内容。

tail [options][file]

常用 options:

  • -f 循环读取

tail

4. mv 命令 : 文件移动/改名

Linux mv 命令用来为文件或目录改名、或将文件或目录移入其它位置。

mv [options] source dest
# or
mv [options] source... directory

options:

  • -i: 若指定目录已有同名文件,则先询问是否覆盖旧文件
  • -f: 在 mv 操作要覆盖某已有的目标文件时不给任何指示
命令格式 运行结果
mv 文件名 文件名 将源文件名改为目标文件名
mv 文件名 目录名 将文件移动到目标目录
mv 目录名 目录名 目标目录已存在,将源目录移动到目标目录.目标目录不存在则改名
mv 目录名 文件名 出错

mv

5. touch 命令 : 新建文件

Linux touch 命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。

ls -l 可以显示档案的时间记录。

touch [file]

touch

6. which 命令 : 查找文件

which 指令会在环境变量$PATH 设置的目录里查找符合条件的文件。

which [file...]

which

7. cp 命令 : 复制文件/目录

Linux cp 命令主要用于复制文件或目录。

cp [options] source dest
# or
cp [options] source... directory

常用 options:

  • -f:覆盖已经存在的目标文件而不给出提示。
  • -r:若给出的源文件是一个目录文件,此时将复制该目录下所有的子目录和文件。

cp

8. cd 命令 : 切换工作目录

Linux cd 命令用于切换当前工作目录至 dirName(目录参数)。

其中 dirName 表示法可为绝对路径或相对路径。若目录名称省略,则变换至使用者的 home 目录,"~" 也表示为 home 目录 的意思

cd [dirName]

cd

9. pwd 命令 : 显示工作目录

Linux pwd 命令用于显示工作目录。

执行 pwd 指令可立刻得知您目前所在的工作目录的绝对路径名称。

pwd

pwd

10. mkdir 命令 : 创建目录

Linux mkdir 命令用于建立名称为 dirName 之子目录。

mkdir [-p] dirName

options

  • -p 确保目录名称存在,不存在的就建一个。

mkdir

11. rmdir 命令 : 删除空目录

Linux rmdir 命令删除空的目录。

rmdir [-p] dirName

options

  • -p 是当子目录被删除后使它也成为空目录的话,则顺便一并删除。

rmdir

12. cat 命令 : 查看文件内容

cat 命令用于连接文件并打印到标准输出设备上。

cat fileName

cat

13. ping 命令 : 检测主机

执行 ping 指令会使用 ICMP 传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。

ping  [主机名称或IP地址]

常用 options:

  • -c<完成次数> 设置完成要求回应的次数。

ping

14. telnet 命令 : 端口是否可访问

虽然 Linux telnet 命令主要用于远端登入。执行 telnet 指令开启终端机阶段作业,并登入远端主机,

但是我更经常用它来查看某个远端主机端口是否可访问。

telnet [主机名称或IP地址<通信端口>]

telnet

15. grep 命令 : 查找关键字

Linux grep 命令用于查找文件里符合条件的字符串。

grep [文件或目录...]

grep

16. ps 命令 : 显示当前进程状态

Linux ps 命令用于显示当前进程 (process) 的状态。

ps [options]

常用 options

  • -e 显示所有进程。
  • -f 全格式。
ps -ef # 显示所有命令,连带命令行

17. | 命令 : 管道命令

通常情况下,我们只执行一条命令,那么如何执行多条命令呢?

管道是一种通信机制,通常用于进程间的通信(也可通过 socket 进行网络通信),它表现出来的形式将前面每一个进程的输出(stdout)直接作为下一个进程的输入(stdin)。

  • 只能处理前一条指令的正确输出,不能处理错误输出
  • 管道命令必须要能够接受来自前一个命令的数据成为 standard input 继续处理才行。

pipe

18. kill 命令 : 杀死进程

Linux kill 命令用于删除执行中的程序或工作。

kill 可将指定的信息送至程序。预设的信息为 SIGTERM(15),可将指定程序终止。若仍无法终止该程序,可使用 SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用 ps 指令或 jobs 指令查看。

kill [-s <信息名称或编号>][程序] 或 kill [-l <信息编号>]

kill

19. top 命令 : 实时显示进程动态

Linux top 命令用于实时显示 process 的动态。

top

常用 options:

  • -pid 指定进程 id
top -pid 4712

top

20. clear 命令 : 清除屏幕

Linux clear 命令用于清除屏幕。

clear

clear

21. alias 命令 : 别名配置

Linux alias 命令用于设置指令的别名。

用户可利用 alias,自定指令的别名。若仅输入 alias,则可列出目前所有的别名设置。alias 的效力仅及于该次登入的操作。若要每次登入是即自动设好别名,可在.profile 或.cshrc 中设定指令的别名。

alias[别名]=[指令名称]

比如 git 原先就配置了一些别名,我们来看看

alias

22. find 命令 : 查找文件

Linux find 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。

如果使用该命令时,不设置任何参数,则 find 命令将在当前目录下查找子目录与文件。并且将查找到的子目录和文件全部进行显示。

find   path   -option   [   -print ]   [ -exec   -ok   command ]   {} \;

find

23. curl 命令 : 文件传输

linux curl 是通过 url 语法在命令行下上传或下载文件的工具软件,它支持 http,https,ftp,ftps,telnet 等多种协议,常被用来抓取网页和监控 Web 服务器状态。

curl [options] [url]

常用 options:

  • -o 把输出写到该文件中

  • -I 仅仅返回 header

curl 命令能做很多事,用过的人都说香,我说说我常用的场景吧:

  1. 调试请求

curl

  1. 查看头部信息

curl

  1. 抓取网页

curl

[Node.js 入门系列] 缓冲器 Buffer 模块

缓冲器 Buffer 模块

在引入 TypedArray 之前,JavaScript 语言没有用于读取或操作二进制数据流的机制。 Buffer 类是作为 Node.js API 的一部分引入的,用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。

创建缓冲区

console.log(Buffer.from([1, 2, 3, 4, 5])); // <Buffer 01 02 03 04 05>

console.log(Buffer.from(new ArrayBuffer(8))); // <Buffer 00 00 00 00 00 00 00 00>

console.log(Buffer.from("Hello world")); // <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64>

Buffer 与字符编码

当字符串数据被存储入 Buffer 实例或从 Buffer 实例中被提取时,可以指定一个字符编码。

// 缓冲区转换为 UTF-8 格式的字符串

const buffer = Buffer.from("Hello world");

console.log(buffer.toString()); // Hello world
// 缓冲区数据转换为base64格式字符串

const buffer = Buffer.from("Hello world");

console.log(buffer.toString("base64")); // SGVsbG8gd29ybGQ=
// 将base64编码的字符串,转换为UTF-8编码

const buffer = Buffer.from("Hello world");

const base64Str = buffer.toString("base64");

const buf = Buffer.from(base64Str, "base64");

console.log(buf.toString("utf8")); // Hello world

上一节: [Node.js 入门系列] module 模块

下一节: [Node.js 入门系列] 域名服务器 dns 模块

[Node.js 入门系列] 统一资源定位符 url 模块

统一资源定位符 url 模块

Node.js 提供了 url 模块,用于处理与解析 URL。

1. URL 对象都有哪些属性 ?

const { URL } = require("url");

const myURL = new URL("https://github.com/webfansplz#hello");
console.log(myURL);
{
  href: 'https://github.com/webfansplz#hello',  // 序列化的 URL
  origin: 'https://github.com', // 序列化的 URL 的 origin
  protocol: 'https:', // URL 的协议
  username: '', // URL 的用户名
  password: '', //  URL 的密码
  host: 'github.com', // URL 的主机
  hostname: 'github.com',   // URL 的主机名
  port: '',  // URL 的端口
  pathname: '/webfansplz',  // URL 的路径
  search: '', // URL 的序列化查询参数
  searchParams: URLSearchParams {}, //  URL 查询参数的 URLSearchParams 对象
  hash: '#hello'  // URL 的片段
}

URL 对象属性 除了 origin 和 searchParams 是只读的,其他都是可写的.

2. 序列化 URL

const { URL } = require("url");

const myURL = new URL("https://github.com/webfansplz#hello");

console.log(myURL.href); //  https://github.com/webfansplz#hello

console.log(myURL.toString()); // https://github.com/webfansplz#hello

console.log(myURL.toJSON()); //  https://github.com/webfansplz#hello

上一节: [Node.js 入门系列] http 模块

下一节: [Node.js 入门系列] 压缩 zlib 模块

[实践系列]Promises/A+规范

什么是Promise ?

Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一

Promises/A+ 规范

为实现者提供一个健全的、可互操作的 JavaScript promise 的开放标准。

术语

  • 解决 (fulfill) : 指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但在后世的 promise 实现多以 resolve 来指代之。

  • 拒绝(reject) : 指一个 promise 失败时进行的一系列操作。

  • 拒因 (reason) : 也就是拒绝原因,指在 promise 被拒绝时传递给拒绝回调的值。

  • 终值(eventual value) : 所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)。

  • Promise : promise 是一个拥有 then 方法的对象或函数,其行为符合本规范。

  • thenable : 是一个定义了 then 方法的对象或函数,文中译作“拥有 then 方法”。

  • 异常(exception) : 是使用 throw 语句抛出的一个值。

基本要求

下面我们先来讲述Promise/A+ 规范的几个基本要求。

1. Promise的状态

一个Promise的当前状态必须是以下三种状态中的一种: 等待状态(Pending) 执行状态(Fulfilled)拒绝状态(Rejected)。

const PENDING = 'pending';

const FULFILLED = 'fulfilled';

const REJECTED = 'rejected';

等待状态 (Pending)

处于等待态时,promise 需满足以下条件:

  • 可以迁移至执行态或拒绝态
 if (this.state === PENDING) {
     this.state = FULFILLED || REJECTED 
 }

执行状态 (Fulfilled)

处于执行态时,promise 需满足以下条件:

  • 不能迁移至其他任何状态

  • 必须拥有一个不可变的终值

 this.value = value;

拒绝状态 (Rejected)

处于拒绝态时,promise 需满足以下条件:

  • 不能迁移至其他任何状态

  • 必须拥有一个不可变的据因

 this.reason = reason;

这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(译者注:盖指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)

2. Then 方法

一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因。

promise 的 then 方法接受两个参数:

promise.then(onFulfilled, onRejected)

参数可选

onFulfilled 和 onRejected 都是可选参数。

  • 如果 onFulfilled 不是函数,其必须被忽略

  • 如果 onRejected 不是函数,其必须被忽略

onFulfilled 特性

如果 onFulfilled 是函数:

  • 当 promise 执行结束后其必须被调用,其第一个参数为 promise 的终值

  • 在 promise 执行结束前其不可被调用

  • 其调用次数不可超过一次

onRejected 特性

如果 onRejected 是函数:

  • 当 promise 被拒绝执行后其必须被调用,其第一个参数为 promise 的据因

  • 在 promise 被拒绝执行前其不可被调用

  • 其调用次数不可超过一次

调用时机

onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用 注1

注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

这个事件队列可以采用“宏任务(macro - task)”机制或者“微任务(micro - task)”机制来实现。

由于 promise 的实施代码本身就是平台代码(译者注:即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。

调用要求

onFulfilled 和 onRejected 必须被作为函数调用(即没有 this 值)

多次调用

then 方法可以被同一个 promise 调用多次

  • 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调

  • 当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调

简易版实践

我们先通过实践一个简易版的Promise来消化一下上面Promises/A+规范的基本要求。

首先

npm init 

// 测试实现是否符合 promises/A+ 规范

npm install promises-aplus-tests -D 

package.json

{
  "name": "ajpromise",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "promises-aplus-tests ./simple.js"
  },
  "author": "webfansplz",
  "license": "MIT",
  "devDependencies": {
    "promises-aplus-tests": "^2.1.2"
  }
}
    

simple.js

//Promise 的三种状态  (满足要求 -> Promise的状态)
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class AjPromise {
  constructor(fn) {
    //当前状态
    this.state = PENDING;
    //终值
    this.value = null;
    //拒因
    this.reason = null;
    //成功态回调队列
    this.onFulfilledCallbacks = [];
    //拒绝态回调队列
    this.onRejectedCallbacks = [];

    //成功态回调
    const resolve = value => {
      // 使用macro-task机制(setTimeout),确保onFulfilled异步执行,且在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
      setTimeout(() => {
        if (this.state === PENDING) {
          // pending(等待态)迁移至 fulfilled(执行态),保证调用次数不超过一次。
          this.state = FULFILLED;
          // 终值
          this.value = value;
          this.onFulfilledCallbacks.map(cb => {
            this.value = cb(this.value);
          });
        }
      });
    };
    //拒绝态回调
    const reject = reason => {
      // 使用macro-task机制(setTimeout),确保onRejected异步执行,且在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。 (满足要求 -> 调用时机)
      setTimeout(() => {
        if (this.state === PENDING) {
          // pending(等待态)迁移至 fulfilled(拒绝态),保证调用次数不超过一次。
          this.state = REJECTED;
          //拒因
          this.reason = reason;
          this.onRejectedCallbacks.map(cb => {
            this.reason = cb(this.reason);
          });
        }
      });
    };
    try {
      //执行promise
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    typeof onFulfilled === 'function' && this.onFulfilledCallbacks.push(onFulfilled);
    typeof onRejected === 'function' && this.onRejectedCallbacks.push(onRejected);
    // 返回this支持then 方法可以被同一个 promise 调用多次
    return this;
  }
}

就这样,一个简单的promise就完成了.

new AjPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 2000);
})
  .then(res => {
    console.log(res);
    return res + 1;
  })
  .then(res => {
    console.log(res);
  });

//output  

// delay 2s..
//  2 
//  3 

接下来,我们来看看我们的实现是否完全符合promises/A+规范~

npm run test

GG,测试用例只过了一小部分,大部分飘红~

OK,接下来,我们来继续了解promises/A+ 进一步的规范要求~

进一步要求

由于接下来的要求比较抽象和难理解,所以我们将一步一步实践来加深理解。

1. 返回

  • 1.then方法必须返回一个promise对象

  • 2.如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)

  • 3.如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e。

  • 4.如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值。

  • 5.如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的据因。

  • 6.不论 promise1 被 reject 还是被 resolve 时 promise2 都会被 resolve,只有出现异常时才会被 rejected。

我们通过以上要求来一步一步完善then方法
1.

// 1.首先,then方法必须返回一个promise对象
  then(onFulfilled, onRejected) {
    let newPromise;
    return (newPromise = new AjPromise((resolve, reject) => {}));
  }
  then(onFulfilled, onRejected) {
    let newPromise;
    return (newPromise = new AjPromise((resolve, reject) => {
      // 2.如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)
      this.onFulfilledCallbacks.push(value => {
        let x = onFulfilled(value);
        //解决过程 resolvePromise
        resolvePromise(newPromise, x);
      });
      this.onRejectedCallbacks.push(reason => {
        let x = onRejected(reason);
        //解决过程 resolvePromise
        resolvePromise(newPromise, x);
      });
    }));
  }
  // 解决过程
  function resolvePromise() {
  //...
  }
  then(onFulfilled, onRejected) {
    let newPromise;
    return (newPromise = new AjPromise((resolve, reject) => {
      //  3.如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e。
      this.onFulfilledCallbacks.push(value => {
        try {
          let x = onFulfilled(value);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
      this.onRejectedCallbacks.push(reason => {
        try {
          let x = onRejected(reason);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
    }));
  }

4,5.

  then(onFulfilled, onRejected) {  
    let newPromise;
    // 4.如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值。
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    // 5.如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的据因。
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : reason => {
            throw reason;
          };
    return (newPromise = new AjPromise((resolve, reject) => {
      this.onFulfilledCallbacks.push(value => {
        try {
          let x = onFulfilled(value);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
      this.onRejectedCallbacks.push(reason => {
        try {
          let x = onRejected(reason);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
    }));
  }
  then(onFulfilled, onRejected) {
    let newPromise;

    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : reason => {
            throw reason;
          };
    // 2.2.6规范 对于一个promise,它的then方法可以调用多次.
    // 当在其他程序中多次调用同一个promise的then时 由于之前状态已经为FULFILLED / REJECTED状态,则会走以下逻辑,
    // 所以要确保为FULFILLED / REJECTED状态后 也要异步执行onFulfilled / onRejected ,这里使用setTimeout

    // 6.不论 promise1 被 reject 还是被 resolve 时 promise2 都会被 resolve,只有出现异常时才会被 rejected。
    // 由于在接下来的解决过程中需要调用resolve,reject进行处理,处理我们在调用处理过程时,传入参数
    if (this.state == FULFILLED) {  
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state == REJECTED) {
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state === PENDING) {
      return (newPromise = new AjPromise((resolve, reject) => {
        this.onFulfilledCallbacks.push(value => {
          try {
            let x = onFulfilled(value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
        this.onRejectedCallbacks.push(reason => {
          try {
            let x = onRejected(reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
  }

ok,完整的then方法搞定了。相信通过以上实践,你对返回要求已经有了更深的理解。

2. Promise 解决过程

Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise 。

这种 thenable 的特性使得 Promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+ 协议的 then 方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。

运行 [[Resolve]](promise, x) 需遵循以下步骤:

1。x 与 promise 相等

如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise。

2。x 为 Promise

  • 如果 x 为 Promise ,则使 promise 接受 x 的状态。

  • 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝。

  • 如果 x 处于执行态,用相同的值执行 promise。

  • 如果 x 处于拒绝态,用相同的据因拒绝 promise。

3。x 为对象或函数

如果 x 为对象或者函数:

  • 把 x.then 赋值给 then。

  • 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise。

  • 如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise:

    • 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
    • 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
    • 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
    • 如果调用 then 方法抛出了异常 e:
      • 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
      • 否则以 e 为据因拒绝 promise
    • 如果 then 不是函数,以 x 为参数执行 promise
  • 如果 x 不为对象或者函数,以 x 为参数执行 promise

如果一个 promise 被一个循环的 thenable 链中的对象解决,而 [[Resolve]](promise, thenable) 的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的 TypeError 为据因来拒绝 promise 。

1.x 与 promise 相等

function resolvePromise(promise2, x, resolve, reject) {
  //x 与 promise 相等 
  //如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错
  
  //如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise
  if (x === promise2) {
    reject(new TypeError('循环引用'));
  }
}

2.x 为 Promise。

function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError('循环引用'));
  }
  // x 为 Promise
  else if (x instanceof AjPromise) {
    // 如果 x 为 Promise ,则使 promise 接受 x 的状态
    // 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝
    if (x.state === PENDING) {
      x.then(
        y => {
          resolvePromise(promise2, y, resolve, reject);
        },
        reason => {
          reject(reason);
        }
      );
    } else {
      // 如果 x 处于执行态,用相同的值执行 promise
      // 如果 x 处于拒绝态,用相同的据因拒绝 promise
      x.then(resolve, reject);
    }
  }
}

3.x 为对象或函数

function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError('循环引用'));
  }
  if (x instanceof AjPromise) {
    if (x.state === PENDING) {
      x.then(
        y => {
          resolvePromise(promise2, y, resolve, reject);
        },
        reason => {
          reject(reason);
        }
      );
    } else {
      x.then(resolve, reject);
    }
  } else if (x && (typeof x === 'function' || typeof x === 'object')) {
    // 避免多次调用
    let called = false;
    try {
      //把 x.then 赋值给 then
      let then = x.then;
      if (typeof then === 'function') {
        // 如果 then 是函数,将 x 作为函数的作用域 this 调用之。
        // 传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise
        // 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
        then.call(
          x,
          // 如果 resolvePromise 以值 y 为参数被调用,则运行[[Resolve]](promise, y)
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          // 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      }else {
        // 如果 then 不是函数,以 x 为参数执行 promise
        resolve(x);
      }  
    } catch (e) {
      // 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
      // 如果调用 then 方法抛出了异常 e:
      // 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
      // 否则以 e 为据因拒绝 promise
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 如果 x 不为对象或者函数,以 x 为参数执行 promise
    resolve(x);
  }
}

Ok~比较复杂的解决过程也让我们搞定了.接下来我们整合下代码

Promises/A+ 规范 实践

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class AjPromise {
  constructor(fn) {
    this.state = PENDING;
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    const resolve = value => {
      if (value instanceof Promise) {
        return value.then(resolve, reject);
      }
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = FULFILLED;
          this.value = value;
          this.onFulfilledCallbacks.map(cb => {
            cb = cb(this.value);
          });
        }
      });
    };
    const reject = reason => {
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = REJECTED;
          this.reason = reason;
          this.onRejectedCallbacks.map(cb => {
            cb = cb(this.reason);
          });
        }
      });
    };
    try {
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    let newPromise;

    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : reason => {
            throw reason;
          };
    if (this.state === FULFILLED) {
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state === REJECTED) {
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state === PENDING) {
      return (newPromise = new AjPromise((resolve, reject) => {
        this.onFulfilledCallbacks.push(value => {
          try {
            let x = onFulfilled(value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
        this.onRejectedCallbacks.push(reason => {
          try {
            let x = onRejected(reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
  }
}
function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError('循环引用'));
  }
  if (x instanceof AjPromise) {
    if (x.state === PENDING) {
      x.then(
        y => {
          resolvePromise(promise2, y, resolve, reject);
        },
        reason => {
          reject(reason);
        }
      );
    } else {
      x.then(resolve, reject);
    }
  } else if (x && (typeof x === 'function' || typeof x === 'object')) {
    let called = false;
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

AjPromise.deferred = function() {
  let defer = {};
  defer.promise = new AjPromise((resolve, reject) => {
    defer.resolve = resolve;
    defer.reject = reject;
  });
  return defer;
};

module.exports = AjPromise;

再来看看我们的实现是否符合Promises/A+规范

npm run test

nice,测试用例全部通过!

参考文献

Promises/A+规范译文

Promise详解与实现

[Node.js进阶系列]Koa源码分析之Http模块

Node.js 提供了 http 模块,其中封装了一个高效的 http 服务器和一个简易的 http 客户端,主要是用于创建一个能够处理和响应 http 响应的服务

Koa 使用 http 模块的 createServer 方法创建 HTTP 服务器,并将 createServer 方法提供的两个参数,请求对象(req),响应对象(res),用来封装自己的 request 对象和 response 对象。

const Emitter = require('events');
const http = require('http');

class Application extends Emitter {
  constructor() {
    super();
  }
  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */
  listen(...args) {
    //创建http服务,接收一个回调函数
    const server = http.createServer(this.callback);
    return server.listen(...args);
  }
  //接收两个参数request和response,分别表示请求和响应的信息。
  callback(request, response) {
    //设置响应信息
    response.end('hello koa');
  }
}

const app = new Application();
//创建http服务器,监听1234端口
app.listen(1234, () => {
  console.log('koa app listen on 1234');
});

//  打开浏览器,输入localhost:1234 or 127.0.0.1:1234 ,就可以看到hello koa

demo 代码

上一节:Koa 源码分析之 EventEmitter
下一节:Koa 源码分析之 Use 方法

[Node.js进阶系列]Koa源码分析之Context对象

Context 是什么?

Koa 提供一个 Context 对象,表示一次对话的上下文.

Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。

context 对象主要是将上下文的访问器和方法直接委托给它们的 request 对象和 response 对象。其他部分代码做了一些异常错误处理,断言等,这里就不拿出来讲了。

那么,context 对象是怎么进行委托的呢? 解锁新姿势;

解锁了新姿势我们再来看以下代码,就能很清楚的了解 koa 是如何 把 ctx 对象接收到的一部分操作委托给了 request 和 response 对象

// koa context.js

/**
 * Response delegation. (响应委托)
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.  (请求委托)
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

上一节:Koa 源码分析之洋葱模型
下一节:Koa 源码精读一

[Node.js 入门系列] 查询字符串 querystring 模块

查询字符串 querystring 模块

querystring 模块是 Node.js 中的工具模块之一,用于处理 URL 中的查询字符串,即:querystring 部分。查询字符串指:URL 字符串中,从问号"?"(不包括?)开始到锚点"#"或者到 URL 字符串的结束(存在#,则到#结束,不存在则到 URL 字符串结束)的部分叫做查询字符串。querystring 模块可将 URL 查询字符串解析为对象,或将对象序列化为查询字符串。

1. 对象序列化为查询字符串

querystring.stringify(obj[, sep][, eq][, options])

const querystring = require('querystring')

const obj = {
  url: 'github.com/webfansplz',
  name: 'null'
}

console.log(querystring.stringify(obj)) // url=github.com%2Fwebfansplz&name=null

2. 查询字符串解析为对象

const querystring = require('querystring')

const o = querystring.parse(`url=github.com%2Fwebfansplz&name=null`)

console.log(o.url) // github.com/webfansplz

3. 编码查询字符串中的参数

querystring.escape 方法会对查询字符串进行编码,在使用 querystring.stringify 方法时可能会用到.

const str = querystring.escape(`url=github.com%2Fwebfansplz&name=null`)

console.log(str) // url%3Dgithub.com%252Fwebfansplz%26name%3Dnull

4. 解码查询字符串中的参数

querystring.unescape 方法是和 querystring.escape 相逆的方法,在使用 querystring.parse 方法时可能会用到。

const str = querystring.escape(`url=github.com%2Fwebfansplz&name=null`)

console.log(querystring.parse(str)) // { 'url=github.com%2Fwebfansplz&name=null': '' } ✖️

console.log(querystring.parse(querystring.unescape(str))) // { url: 'github.com/webfansplz', name: 'null' }

上一节: [Node.js 入门系列] 逐行读取 readline 模块

下一节: [Node.js 入门系列] module 模块

撸一个简版 vuex

撸一个简版 vuex

Vuex 是什么 ?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

vuex

Vuex 核心概念

  • State (单一状态树,数据共享数据存储)

  • Getter (Vuex 的计算属性,从 state 派生状态)

  • Mutation (更改 Vuex state 的唯一方法,同步操作)

  • Action (异步操作处理方法,提交 mutation 来更改 state,而不是直接变更)

vllx 做了哪些基本实现?

vllx 对 vuex 源码进行了拆分简化,让源码读起来更简单易懂,也让你通过这个简版的 vllx 理解 vuex 的核心实现,vllx 实现了以下功能:

  • Vuex.Store 构造器选项

    ✅ state

    ✅ mutations

    ✅ actions

    ✅ getters

  • Vuex.Store 实例方法

    ✅ commit

    ✅ dispatch

vllx 实践

1. Vue.use(vuex) 做了什么?

2. Vue computed 和 Vuex state 如何实现响应 ?

3. Vuex.Store 核心方法 commit,dispatch 的实现 !

4. 为什么说 Vuex getter 相当于 Vue 的 computed ?

demo 使用了 JavaScript modules,需要启动 web 服务器来调试 !

❤️ 结语

如果你和我一样对前端感兴趣,也喜欢"动手",欢迎关注我的博客一起玩耍啊~

0202年了, Chrome DevTools 你还只会console.log吗 ?

前言

Chrome 开发者工具(简称 DevTools)是一套 Web 开发调试工具,内嵌于 Google Chrome 浏览器中。DevTools 使开发者更加深入的了解浏览器内部以及他们编写的应用。通过使用 DevTools,可以更加高效的定位页面布局问题,设置 JavaScript 断点并且更好的理解代码优化。

本文分享 24 个 Chrome 调试技巧和一些快捷键,希望能帮你进一步了解 Chrome DevTools ~

调试技巧

1. 控制台中直接访问页面元素

在元素面板选择一个元素,然后在控制台输入$0,就会在控制台中得到刚才选中的元素。如果页面中已经包含了 jQuery,你也可以使用$($0)来进行选择。

你也可以反过来,在控制台输出的 DOM 元素上右键选择 Reveal in Elements Panel 来直接在 DOM 树种查看。

0

2. 访问最近的控制台结果

在控制台输入$_可以获控制台最近一次的输出结果。

_

3. 访问最近选择的元素和对象

控制台会存储最近 5 个被选择的元素和对象。当你在元素面板选择一个元素或在分析器面板选择一个对象,记录都会存储在栈中。 可以使用$x来操作历史栈,x 是从 0 开始计数的,所以$0 表示最近选择的元素,$4 表示最后选择的元素。

4

4. 选择元素

  • $() - 返回满足指定 CSS 规则的第一个元素,此方法为 document.querySelector()的简化。

  • $$() - 返回满足指定 CSS 规则的所有元素,此方法为 querySelectorAll()的简化。

  • $x() - 返回满足指定 XPath 的所有元素。

select

5. 使用 console.table

该命令支持以表格的形式输出日志信息。打印复杂信息时尝试使用 console.table 来替代 console.log 会更加清晰。

table

6. 使用 console.dir,可简写为 dir

console.dir(object)/dir(object) 命令可以列出参数 object 的所有对象属性。

dir

7. 复制 copy

你可以通过 copy 方法在控制台里复制你想要的东西。

copy

8. 获取对象键值 keys(object)/values(object)

keys_values

9. 函数监听器 monitor(function)/unmonitor(function)

monitor(function),当调用指定的函数时,会将一条消息记录到控制台,该消息指示调用时传递给该函数的函数名和参数。

使用 unmonitor(函数)停止对指定函数的监视。

monitor

10. 事件监听器 monitorEvents(object[, events])/unmonitorEvents(object[, events])

monitorEvents(object[, events]),当指定的对象上发生指定的事件之一时,事件对象将被记录到控制台。事件类型可以指定为单个事件或事件数组。

unmonitorevent (object[, events])停止监视指定对象和事件的事件。

monitorevents

11. 耗时监控

通过调用 time()可以开启计时器。你必须传入一个字符串参数来唯一标记这个计时器的 ID。当你要结束计时的时候可以调用 timeEnd(),并且传入指定的名字。计时结束后控制台会打印计时器的名字和具体的时间。

time

12. 分析程序性能

在 DevTools 窗口控制台中,调用 console.profile()开启一个 JavaScript CPU 分析器.结束分析器直接调用 console.profileEnd().

profile

具体的性能分析会在分析器面板中

profile_1

13. 统计表达式执行次数

count()方法用于统计表达式被执行的次数,它接受一个字符串参数用于标记不同的记号。如果两次传入相同的字符串,该方法就会累积计数。

count

14. 清空控制台历史记录

可以通过下面的方式清空控制台历史:

  • 在控制台右键,或者按下 Ctrl 并单击鼠标,选择 Clear Console。
  • 在脚本窗口输入 clear()执行。
  • 在 JavaScript 脚本中调用 console.clear()。
  • 使用快捷键 Cmd + K (Mac) Ctrl + L (Windows and Linux)。

clear

15. 异步操作

async/await 使得异步操作变得更加容易和可读。唯一的问题在于 await 需要在 async 函数中使用。Chrome DevTools 支持直接使用 await。

await

16. debugger 断点

有时候我们需要打断点进行单步调试,一般会选择在浏览器控制台直接打断点,但这样还需要先去 Sources 里面找到源码,然后再找到需要打断点的那行代码,比较麻烦。

使用 debugger 关键词,我们可以直接在源码中定义断点,方便很多。

debugger

17. 截图

我们经常需要截图,Chrome DevTools 提供了 4 种截图方式,基本覆盖了我们的需求场景,快捷键 ctrl+shift+p ,打开 Command Menu,输入 screenshot,可以看到以下 4 个选项:

screenshot

去试试吧,很香!

18. 切换主题

Chrome 提供了 亮&暗 两种主题,当你视觉疲劳的时候,可以 switch 哦, 快捷键 ctrl+shift+p ,打开 Command Menu,输入 theme ,即可选择切换

theme

19. 复制 Fetch

在 Network 标签下的所有的请求,都可以复制为一个完整的 Fetch 请求的代码。

copy-fetch

20. 重写 Overrides

在 Chrome DevTools 上调试 css 或 JavaScript 时,修改的属性值在重新刷新页面时,所有的修改都会被重置。

如果你想把修改的值保存下来,刷新页面的时候不会被重置,那就看看下面这个特性(Overrides)吧。Overrides 默认是关闭的,需要手动开启,开启的步骤如下。

开启的操作:

打开 Chrome DevTools 的 Sources 标签页
选择 Overrides 子标签
选择 + Select folder for overrides,来为 Overrides 设置一个保存重写属性的目录

overrides

21. 实时表达式 Live Expression

从 chrome70 起,我们可以在控制台上方可以放一个动态表达式,用于实时监控它的值。Live Expression 的执行频率是 250 毫秒。

点击 "Create Live Expression" 眼睛图标,打开动态表达式界面,输入要监控的表达式

live_expression

22. 检查动画

Chrome DevTools 动画检查器有两个主要用途。

  • 检查动画。您希望慢速播放、重播或检查动画组的源代码。

  • 修改动画。您希望修改动画组的时间、延迟、持续时间或关键帧偏移。 当前不支持编辑贝塞尔曲线和关键帧。

动画检查器支持 CSS 动画、CSS 过渡和网络动画。当前不支持 requestAnimationFrame 动画。

快捷键 ctrl+shift+p ,打开 Command Menu,键入 Drawer: Show Animations。

animations

23. 滚动到视图区域 Scroll into view

scrollintoview

24. 工作区编辑文件 Edit Files With Workspaces

工作空间使您能够将在 Chrome Devtools 中进行的更改保存到计算机上相同文件的本地副本。

进入 Sources Menu, Filesystem 下 点击 Add folder to workspace 添加要同步的工作目录

workspaces

快捷键

访问 DevTools

访问 DevTools Windows Mac
打开 Developer Tools (上一次停靠菜单) F12、Ctrl + Shift + I Cmd + Opt + I
打开/切换检查元素模式和浏览器窗口 Ctrl + Shift + C Cmd + Shift + C
打开 Developer Tools 并聚焦到控制台 Ctrl + Shift + J Cmd + Opt + J

全局键盘快捷键

下列键盘快捷键可以在所有 DevTools 面板中使用:

全局键盘快捷键 Windows Mac
下一个面板 Ctrl + ] Cmd + ]
上一个面板 Ctrl + [ Cmd + [
更改 DevTools 停靠位置 Ctrl + Shift + D Cmd + Shift + D
打开 Device Mode Ctrl + Shift + M Cmd + Shift + M
切换控制台 Esc Esc
刷新页面 F5、Ctrl + R Cmd + R
刷新忽略缓存内容的页面 Ctrl + F5、Ctrl + Shift + R Cmd + Shift + R
在当前文件或面板中搜索文本 Ctrl + F Cmd + F
在所有源中搜索文本 Ctrl + Shift + F Cmd + Opt + F
按文件名搜索(除了在 Timeline 上) Ctrl + O、Ctrl + P Cmd + O、Cmd + P
放大(焦点在 DevTools 中时) Ctrl + + Cmd + Shift + +
缩小 Ctrl + - Cmd + Shift + -
恢复默认文本大小 Ctrl + 0 Cmd + 0
打开 command 菜单 Ctrl + Shift + P Cmd + Shift + P

控制台

控制台快捷键 Windows Mac
上一个命令/行 向上键 向上键
下一个命令/行 向下键 向下键
聚焦到控制台 Ctrl + ` Ctrl + `
清除控制台 Ctrl + L Cmd + K
多行输入 Shift + Enter Shift + Enter
执行 Enter Return

[Node.js进阶系列]Koa源码分析之Use方法

Koa 中间件

Koa 生态提供了各种各样的中间件供我们使用,可插可拔,非常灵活,开发者也可以根据自己的需求开发中间件。

Koa 本身只是实现了中间件流程控制,就能让我们在 Koa 中简单的使用中间件,那么它是怎么实现的呢?

本节我们先说说 Koa 的 use 方法做了什么,下一节我们将对 Koa 精妙的中间件流程控制作重点分析~

const Emitter = require('events');
const http = require('http');
//  将Generator函数 转化为 使用co包装成的Promise对象
const convert = require('koa-convert');
//  控制输出弃用功能警告日志
const deprecate = require('depd')('koa');
//  是否是Generator函数
const isGeneratorFunction = require('is-generator-function');
class Application extends Emitter {
  constructor() {
    super();
    //装载中间件容器
    this.middleware = [];
  }
  listen(...args) {
    const server = http.createServer(this.callback);
    return server.listen(...args);
  }
  callback(request, response) {
    response.end('hello koa');
  }
  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use(fn) {
    //  参数类型非函数时,抛出类型错误异常.
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    //中间件如果是Generator函数
    if (isGeneratorFunction(fn)) {
      // 抛出弃用警告,对生成器的支持将在v3中移除。
      deprecate(
        'Support for generators will be removed in v3. ' +
          'See the documentation for examples of how to convert old middleware ' +
          'https://github.com/koajs/koa/blob/master/docs/migration.md'
      );
      // (因为koa1使用的是Generator函数, koa2使用的是async函数, 所以做了此处理)
      fn = convert(fn);
    }
    //往中间件容器添加中间件
    this.middleware.push(fn);
    //返回当前实例,支持链式调用中间件
    return this;
  }
}

const app = new Application();
app
  .use(() => {
    console.log('hi');
  })
  .use(() => {
    console.log('koa');
  })
  .use(() => {
    console.log('middleware');
  });
console.log(app.middleware);
app.listen(1234, () => {
  console.log('koa app listen on 1234');
});

demo 代码

上一节:Koa 源码分析之 Http 模块
下一节: Koa 源码分析之洋葱模型

[Node.js进阶系列]Koa源码分析之洋葱模型

koa-洋葱模型

koa 最大的特点就是独特的中间件流程控制,也就是大名鼎鼎的“洋葱模型”。没图说个???

洋葱模型

我们可以很清晰的看到 一个请求从外到里一层一层的经过中间件,响应时从里到外一层一层的经过中间件。

就像我们往洋葱插入一根牙签,牙签从外到里经过一层层洋葱皮,到达"葱心",我们拔出来时,牙签从里到外经过一层一层洋葱皮。(我怀疑你在开车,但是...)

Talk is cheap,Show me the code。简单说就是,没代码说个???

下面,我们来分析一波 koa-compose 源码

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose(middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!');
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!');
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function(context, next) {
    // last called middleware #
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

我们可以看到除去类型错误判断,代码只有短短的 10 多行...下面我们分析一哈核心代码~

function compose(middleware) {
  // 返回了一个函数,接受context和next参数,koa在调用koa-compose时只传入context,所以此处next即为undefined;
  return function(context, next) {
    // last called middleware #
    //  初始化index
    let index = -1;
    //  从第一个中间件开始执行~
    return dispatch(0);
    //  执行函数
    function dispatch(i) {
      //  在一个中间件执行两次next函数时,抛出异常.  注解: ⭕
      if (i <= index) return Promise.reject(new Error('next() called multiple times'));
      //  设置index,作用是判断在同一个中间件中是否调用多次next函数.
      index = i;
      //  当前中间件函数
      let fn = middleware[i];
      //  跑完所有中间件时,fn = next ,即fn = undefined,可以理解为终止条件;
      if (i === middleware.length) fn = next;
      //  返回一个空值的promise对象.  注解: ⭕⭕
      if (!fn) return Promise.resolve();
      try {
        //  返回一个定值的promise对象.值为下一个中间件的返回值。
        //  这里是最核心的逻辑,递归调用下一个中间件,并将返回值返回给上一个中间件。 注解:  ⭕⭕
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

如果看完上面的分析,你还觉得一脸萌比的话,没关系,下面我们来通过实践再来理解一波.

1.我们首先验证一哈我们上面说的高大上的洋葱模型.

const m1 = async (context, next) => {
  console.log('in-1');
  await next();
  console.log('out-1');
};
const m2 = async (context, next) => {
  console.log('in-2');
  await next();
  console.log('out-2');
};
const m3 = async (context, next) => {
  console.log('in-3');
  await next();
  console.log('out-3');
};
compose([m1, m2, m3])();

//output
// in-1
// in-2
// in-3
// out-3
// out-2
// out-1

没毛病,一层一层往里进,一层一层往里出. 如果以上代码的执行结果让你感到意外和困惑..你或许该补一下 call stack 的知识了,看看这个,你就豁然开朗了.

2.我们验证一哈注解 ⭕️.

const m1 = async (context, next) => {
  await next();
  await next();
};
compose([m1])();
// output
// UnhandledPromiseRejectionWarning: Error: next() called multiple times

☑️,验证通过

2.最后我们来验证一哈注解 ⭕️⭕️.

const m1 = async (context, next) => {
  const res = await next();
  console.log('m1收到的返回结果:', res);
};
const m2 = async (context, next) => {
  const res = await next();
  console.log('m2收到的返回结果:', res);
  return '我是m2,这是我返回的结果.';
};
const m3 = async (context, next) => {
  const res = await next();
  console.log('m3收到的返回结果:', res);
  return '我是m3,这是我返回的结果.';
};
compose([m1, m2, m3])();
//output
// m3收到的返回结果: undefined
// m2收到的返回结果: 我是m3,这是我返回的结果.
// m1收到的返回结果: 我是m2,这是我返回的结果.
☑️,验证通过

demo 代码

上一节:Koa 源码分析之 Use 方法
下一节:Koa 源码分析之 Context 对象

[Node.js进阶系列]Koa源码精读二

了解了 Koa 的一些核心实现**,我们最后来分析源码就会发现清晰很多,GO ~

'use strict';

/**
 * Module dependencies.
 */

const isGeneratorFunction = require('is-generator-function');
const response = require('./response');
const compose = require('koa-compose');
const isJSON = require('koa-is-json');
const context = require('./context');
const request = require('./request');
const statuses = require('statuses');
const Emitter = require('events');
const util = require('util');
const Stream = require('stream');
const http = require('http');
const convert = require('koa-convert');
const deprecate = require('depd')('koa');

module.exports = class Application extends Emitter {
  constructor() {
    super();
    this.proxy = false;
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    //  装载中间件容器
    this.middleware = [];
    //  创建context对象
    this.context = Object.create(context);
    //  创建request对象
    this.request = Object.create(request);
    //  创建response对象
    this.response = Object.create(response);

    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
  //  创建http服务
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  toJSON() {
    return only(this, ['subdomainOffset', 'proxy', 'env']);
  }
  inspect() {
    return this.toJSON();
  }
  // use中间件
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate(
        'Support for generators will be removed in v3. ' +
          'See the documentation for examples of how to convert old middleware ' +
          'https://github.com/koajs/koa/blob/master/docs/migration.md'
      );
      fn = convert(fn);
    }
    this.middleware.push(fn);
    return this;
  }

  callback() {
    //  前面我们提到的"洋葱模型",中间件流程控制
    const fn = compose(this.middleware);
    /*  
      events模块的listenerCount方法,
      判断error事件的监听数量为0时,监听error事件。
    */
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      //  创建上下文对象,传入http模块的req(请求对象),res(响应对象)
      const ctx = this.createContext(req, res);
      //  请求处理
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    /*  
      调用context.js的onerror函数
      context.js的onerror函数里,有这样一句代码 this.app.emit('error', err, this);
      其实就是我们之前提到的Eventemitter,所以这里会触发this.onerror方法.
      所以这里虽然调用的是context.js的onerror函数,但会触发两个error函数进行异常处理.
    */
    const onerror = err => ctx.onerror(err);
    //  对响应内容进行处理
    const handleResponse = () => respond(ctx);
    //  确保一个流在完成,关闭,报错时都会执行响应的回调函数
    onFinished(res, onerror);
    //  执行中间件,并处理对应响应
    return fnMiddleware(ctx)
      .then(handleResponse)
      .catch(onerror);
  }
  //  创建上下文对象, 看完如果觉得萌比的话,可看下方流程图
  createContext(req, res) {
    const context = Object.create(this.context);
    const request = (context.request = Object.create(this.request));
    const response = (context.response = Object.create(this.response));
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }
  //  错误处理
  onerror(err) {
    // 判断 err 是否是 Error 实例
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
    // 是否 404 错误
    if (404 == err.status || err.expose) return;
    // 是否有静默设置,
    if (this.silent) return;
    // 打印出出错堆栈,方便对问题进行定位
    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
};
//  响应内容处理
function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;
  //  是否为可写流
  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;
  //  如果响应的Status Code是body 为空的类型,将 body 置为 null并响应
  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }
  //  如果是HEAD方法
  if ('HEAD' == ctx.method) {
    //  http 响应头部是否已经被发送且body是否为json,未发送的话,添加 length 头部
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  // body值为空
  if (null == body) {
    // HTTP 版本>=2
    if (ctx.req.httpVersionMajor >= 2) {
      //  body值为code
      body = String(code);
    } else {
      //  body值为   context 中的 message 属性或 code
      body = ctx.message || String(code);
    }
    //  未发送响应头部,添加length和type
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  //  responses
  //  处理buffer类型的body
  if (Buffer.isBuffer(body)) return res.end(body);
  //  处理字符串类型的body
  if ('string' == typeof body) return res.end(body);
  //  body是流类型,合并处理
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  // body是json类型,序列化
  body = JSON.stringify(body);
  //  未发送响应头部,添加length
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

代码地址

下面,引用某位大佬整理的流程图,� 希望可以帮助你更清晰的理解 Koa 上下文对象~

创建上下文

结语

欢乐的时光总是这么短暂,到了跟大家说再见的时候啦,如果你觉得有帮助到你的话,请给我小星星~
上一节:Koa 源码精读一

✨ null-cli 来啦 🎉 一行命令提高你的效率 🚀

null-cli

null-cli 是什么 ?

在日常开发工作中,

我们需要用到各式各样的工具

用 有道翻译 来翻译单词

用 postman 来调试网络请求

用 express 来 启动 web 服务器

用 vue-cli , create-react-app 来搭建前端工程 ...

在不同的应用程序间来回切换难免觉得繁琐~

所以就有了 null-cli ! 一个命令行界面工具,集成了我们常用的一些功能~

null-cli 有哪些功能 ?

1. 压缩文件

压缩指定文件,file 文件类型可以是文件夹或者单个文件路径,文件夹路径会自动压缩该文件夹所有的 html/css/js 文件(支持文件类型 : html/css/js)。

null

2. 网络请求

发送 http 请求,目前只支持无头 get 请求 ~ (在命令行拼 header,body 感觉很繁琐,不如 postman 便捷)

null

3. 有道翻译

支持中英文翻译,根据传入的 word 自动识别中英文

null

这个 功能 借鉴了 https://github.com/kenshinji/yddict的实现。

4. 打开浏览器

通过你的默认浏览器打开指定的 url

$ null open <url>

5. 生成二维码

生成指定 url 的二维码

null

6. 正则表达式

常用正则表达式

null

7. 生成随机数

生成指定长度的随机数

null

8. 创建模版工程

null

v1 提供了 4 类基础模版 ,v2 将对模版进行细化。

  • vue

  • react

  • koa

  • active-page

9. 日期格式转换

日期格式转换,时间戳和字符串互相转换

null

10. 启动 web 服务器

指定工程启动 web 服务器

null

11. 字符串编解码/AES 加解密

null

null

如何使用 null-cli ?

安装

 npm install null-cli -g
 # OR
 yarn global add null-cli

项目地址

https://github.com/webfansplz/null-cli

文档地址

https://webfansplz.github.io/null-cli/

结语

如果有帮助到你,麻烦请给作者一个小星星支持作者,万分感谢 。🙏🙏🙏

[Node.js进阶系列]Koa源码分析之EventEmitter

事件驱动

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。

Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。

Allows you to build scalable network applications usingJavaScript on the server-side.

大多数 Node.js 核心 API 都采用惯用的事件驱动架构,其中某些类型的对象(触发器)会周期性地触发命名事件来调用函数对象(监听器),那么 Node.js 是如何实现事件驱动的呢?

events 模块是 Node.js 实现事件驱动的核心,在 node 中大部分的模块的实现都继承了 Events 类。比如 fs 的 readstream,net 的 server 模块。

events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装,EventEmitter 本质上是一个观察者模式的实现。

koa 实现就继承了 events 类,来监听 error 事件.

const Emitter = require('events');
/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */
class Application extends Emitter {
  constructor() {
    super();
  }
}
const app = new Application();
//  监听error事件
app.on('error', err => {
  console.log('触发了error事件:', err); //  hello koa
});
//  触发error事件
app.emit('error', 'hello koa');

demo 代码

这节我们简单了解了 koa 继承类 Emitter 的作用,后面我们会讲到 koa 具体在哪里使用了 events 模块。

下一节:Koa源码分析之Http模块

[Node.js 入门系列] 事件触发器 events 模块

事件触发器 events 模块

Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。

大多数 Node.js 核心 API 都采用惯用的事件驱动架构,其中某些类型的对象(触发器)会周期性地触发命名事件来调用函数对象(监听器),那么 Node.js 是如何实现事件驱动的呢?

events 模块是 Node.js 实现事件驱动的核心,在 node 中大部分的模块的实现都继承了 Events 类。比如 fs 的 readstream,net 的 server 模块。

events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装,EventEmitter 本质上是一个观察者模式的实现。

所有能触发事件的对象都是 EventEmitter 类的实例。 这些对象有一个 eventEmitter.on() 函数,用于将一个或多个函数绑定到命名事件上。 事件的命名通常是驼峰式的字符串,但也可以使用任何有效的 JavaScript 属性键。

EventEmitter 对象使用 eventEmitter.emit()触发事件,当 EventEmitter 对象触发一个事件时,所有绑定在该事件上的函数都会被同步地调用。 被调用的监听器返回的任何值都将会被忽略并丢弃。

下面我们通过几个简单的例子来学习 events 模块

1. 基础例子

注册 Application 实例,继承 EventEmitter 类,通过继承而来的 eventEmitter.on() 函数监听事件,eventEmitter.emit()触发事件

const EventEmitter = require("events");
/**
 * Expose `Application` class.
 * Inherits from `EventEmitter.prototype`.
 */
class Application extends EventEmitter {}
const app = new Application();
//  监听hello事件
app.on("hello", data => {
  console.log(data); // hello nodeJs
});
//  触发hello事件
app.emit("hello", "hello nodeJs");

2. 多个事件监听器及 this 指向

绑定多个事件监听器时,事件监听器按照注册的顺序执行。

当监听器函数被调用时, this 关键词会被指向监听器所绑定的 EventEmitter 实例。也可以使用 ES6 的箭头函数作为监听器,但 this 关键词不会指向 EventEmitter 实例。

const EventEmitter = require("events");

class Person extends EventEmitter {
  constructor() {
    super();
  }
}
const mrNull = new Person();
//  监听play事件
mrNull.on("play", function(data) {
  console.log(this);
  // Person {
  //   _events:
  //   [Object: null prototype] { play: [[Function], [Function]] },
  //   _eventsCount: 1,
  //     _maxListeners: undefined
  // }
  console.log(`play`);
});
//  监听play事件
mrNull.on("play", data => {
  console.log(this); // {}
  console.log(`play again`);
});
//  触发play事件
mrNull.emit("play", "hello nodeJs");

3. 同步 VS 异步

EventEmitter 以注册的顺序同步地调用所有监听器。

const EventEmitter = require("events");

class Person extends EventEmitter {
  constructor() {
    super();
  }
}
const mrNull = new Person();
mrNull.on("play", function(data) {
  console.log(data);
});

mrNull.emit("play", "hello nodeJs");

console.log(`hello MrNull`);

// hello nodeJs
// hello MrNull

监听器函数可以使用 setImmediate() 和 process.nextTick() 方法切换到异步的操作模式

const developer = new Person();
developer.on("dev", function(data) {
  setImmediate(() => {
    console.log(data);
  });
});
developer.on("dev", function(data) {
  process.nextTick(() => {
    console.log(data);
  });
});
developer.emit("dev", "hello nodeJs");

console.log(`hello developer`);

// hello developer
// hello nodeJs
// hello nodeJs

4. 只调用一次的事件监听器

使用 eventEmitter.once() 可以注册最多可调用一次的监听器。 当事件被触发时,监听器会被注销,然后再调用。

const EventEmitter = require("events");

class Person extends EventEmitter {
  constructor() {
    super();
  }
}
const mrNull = new Person();
mrNull.once("play", () => {
  console.log("play !");
});

mrNull.emit("play");
mrNull.emit("play");

// play ! 只输出一次

5. 事件触发顺序

在注册事件前,触发该事件,不会被触发 !!

const EventEmitter = require("events");

class Person extends EventEmitter {
  constructor() {
    super();
  }
}
const mrNull = new Person();

mrNull.emit("play");

mrNull.on("play", () => {
  console.log("play !");
});

// 无任何输出

6. 移除事件监听器

const EventEmitter = require("events");

class Person extends EventEmitter {
  constructor() {
    super();
  }
}
const mrNull = new Person();

function play() {
  console.log("play !");
}
mrNull.on("play", play);

mrNull.emit("play");

// mrNull.off("play", play); v10.0.0版本新增,emitter.removeListener() 的别名。
//  or
mrNull.removeListener("play", play);

mrNull.emit("play");

// play !  移除后不再触发

下一节: [Node.js 入门系列] 本地路径 path 模块

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.