阿平

阿平 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 ignorantfool.github.io/ 编辑
编辑

在这片天空下,如果还有一双眼睛与你一同哭泣,生活就值得你为之奋斗。

个人动态

阿平 收藏了文章 · 2020-11-01

中了源码的毒,给你一副良药

近期阿宝哥在团队内搞了一个 如何读源码 的专题,主要目的是让团队的小伙伴们了解读源码的思路与技巧。在此期间,阿宝哥也写了 77.9K 的 Axios 项目有哪些值得借鉴的地方从 12.9K 的前端开源项目我学到了啥如何让你的 Express 飞起来 三篇源码解析的文章。其中前两篇在 掘金社区 获得不错的评价,平均 705+ 个 👍,所以阿宝哥就想写一篇文章来分享一下本人读源码的思路、技巧与工具。

好的,让我们开始出发吧!在进入正题之前,我们先来个读源码前的 灵魂四连问 热热身。

一、灵魂四连问

1.1 为什么要读源代码

1.2 如何选择项目

1.3 如何阅读源码

1.4 有实际的案例么

既然前两篇文章比较受大家喜欢,接下来阿宝哥就以最受欢迎的 Axios 为例,来分享一下读源码的思路与技巧。

二、如何品读 Axios?

2.1 走进 Axios

Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被广泛地应用在大量的 Web 项目中。

由上图可知,Axios 项目的 Star 数为 78.1K,Fork 数也高达 7.3K,是一个很优秀的开源项目,所以值得大家细细品读。

2.2 发现 Axios 的美

在确认 Axios 为 “追求目标” 之后,下一步我们就需要来发现它身上的优点(特性):

每个人对 “美” 都有不同的看法,对于阿宝哥来说,我看中了图中已选中的三点。因此,它们也很光荣地成为读源码的三个切入点。当然切入点也不是越多越好,可以先找自己最感兴趣的地方作为切入点。需要注意的是,如果切入点之间有关联关系的话,建议做个简单的排序。

2.3 感受 Axios 的美

选择切入点之后,我们就可以开始逐一感受 Axios 的设计之美。以 能够拦截请求与响应 这个切入点为例,首先我们就会接触到 拦截器 的概念。所以我们需要先了解拦截器是什么、拦截器有什么作用以及如何使用拦截器,这里我们可以从项目的 官方文档 或者项目中的 README.md 文档入手。

2.3.1 拦截器的作用

Axios 提供了请求拦截器和响应拦截器来分别处理请求和响应,它们的作用如下:

  • 请求拦截器:该类拦截器的作用是在请求发送前统一执行某些操作,比如在请求头中添加 token 字段。
  • 响应拦截器:该类拦截器的作用是在接收到服务器响应后统一执行某些操作,比如发现响应状态码为 401 时,自动跳转到登录页。
2.3.2 拦截器的使用
// 添加请求拦截器 —— 处理请求配置对象
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器 —— 处理响应对象
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

axios({
  url: '/hello',
  method: 'get',
}).then(res =>{
  console.log('axios res.data: ', res.data)
});

在了解完拦截器的作用和用法之后,我们就会把焦点聚焦到 axios 对象,因为注册拦截器和发送请求都与它有紧密的联系。不过在看具体源码之前,阿宝哥建议先对功能点做一下梳理。以下是阿宝哥的分析思路:

Axios 的作用是用于发送 HTTP 请求,请求拦截器和响应拦截器分别对应于 HTTP 请求的不同阶段,它们的本质是一个实现特定功能的函数。这时我们就可以按照功能把发送 HTTP 请求拆解成不同类型的子任务,比如有 用于处理请求配置对象的子任务用于发送 HTTP 请求的子任务用于处理响应对象的子任务。当我们按照指定的顺序来执行这些子任务时,就可以完成一次完整的 HTTP 请求。

既然已经提到了任务,我们就会联想到任务管理系统的基本功能:任务注册、任务编排(优先级排序)和任务调度等。因此我们就可以考虑从 任务注册、任务编排和任务调度 三个方面来分析 Axios 拦截器的实现。

2.3.3 任务注册
// 添加请求拦截器 —— 处理请求配置对象
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器 —— 处理响应对象
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

lib/axios.js 路径下,我们可以找到 axios 对象的定义。为了能直观地了解对象之间的关系,阿宝哥建议大家在读源码的过程中,多动手画画图。比如阿宝哥使用下图来总结一下 Axios 对象与 InterceptorManager 对象的内部结构与关系:

2.3.4 任务编排

现在我们已经知道如何注册拦截器任务,但仅仅注册任务是不够,我们还需要对已注册的任务进行编排,这样才能确保任务的执行顺序。

同样对于任务编排,也可以使用图的形式来展现任务编排后的结果。 这里有一个小技巧,就是可以采用对比的形式来展示任务编排后的结果,这样子会更加清楚任务编排的处理逻辑。

2.3.5 任务调度

任务编排完成后,要发起 HTTP 请求,我们还需要按编排后的顺序执行任务调度。

需要注意的是:在阅读源码过程中,不要太在意细节。比如在研究 Axios 拦截器原理时,不需要再深入了解 dispatchRequest 背后的具体实现,只需知道该方法用于实现发送 HTTP 请求即可,这样才不会把整个线路拉得太长。

在分析完特定的功能点之后,也许你已经读懂的具体的源代码。但阿宝哥觉得这并不是最重要的,更重要的是思考它的设计思想,这样设计有什么好处,对于我们有没有什么值得借鉴和学习的地方。比如参考 Axios 拦截器的设计模型,我们就可以抽出以下通用的任务处理模型:

上面阿宝哥以 Axios 的拦截器为例,分享了读 Axios 源码的思路与技巧。接下来阿宝哥来分享一些读源码的建议和辅助工具。

三、读源码的建议

四、读源码辅助工具

如果你对下列辅助工具感兴趣的话,可以通过以下图片来源的链接,来直接打开每个工具的在线地址。

(图片来源:https://www.processon.com/vie...

五、总结

其实除了上面的内容之外,读优秀开源项目还有挺多值得关注的地方。阿宝哥在学习 BetterScroll 项目源码时,总结了一张思维导图

(图片来源:https://www.processon.com/vie...

下面阿宝哥用一张图来总结一下 axiosbetter-scroll 这两个开源项目的学习路线:

1、Axios 项目的切入点是从 Github 中的功能特性中筛选出来的;

2、BetterScroll 的切入点是从掘金上 BetterScroll 2.0 发布:精益求精,与你同行 这篇文章中介绍的功能亮点中找到的。

除此之外,阿宝哥也来简单总结一下本文介绍的读源码的思路与技巧:

  • 站在巨人的肩膀,提前阅读一些项目相关的优质文章;
  • 汇总学习或工作中遇到的问题,带着问题进行源码学习;
  • 明确阅读源码的主线或切入点;
  • 尽可能从简单的示例出发来分析每个功能点;
  • 先梳理清楚主要流程,不要太在意细节,避免把整个线路拉得太长;
  • 在阅读源码过程中,要多多画图,这样理解起来会更加直观。

本文阿宝哥分享了个人读源码的思路、技巧与工具,希望阅读完本文能对你有所启发或帮助。如果你有读源码更好的思路与技巧,欢迎随时跟阿宝哥交流哈。有写得不好的地方,也请各位见谅哈。

六、参考资源

查看原文

阿平 关注了专栏 · 2020-10-30

SegmentFault 行业快讯

第一时间为开发者提供行业相关的实时热点资讯

关注 58853

阿平 收藏了文章 · 2020-10-09

Chrome DevTools中的这些骚操作,你都知道吗?

image

引言 🏂

作为开发人员,平时用的最多的就是Chrome devtools了,但是可能很多同学都像我一样平时用的最多也就只是ConsoleElements面板了。

我整理了一些我平时用的比较多的一些调试小技巧,相信对提高你的工作效率能起到不小的帮助!

命令(Command) 菜单 🏈

“命令”菜单是最最常用的,本文也会多次用到,所以这里先说一下打开方式:

Cmd + Shift + P(如果使用Windows,则按Ctrl + Shift + P)打开“命令”菜单。

截图DOM元素 🏉

当你只想对一个特别的 DOM 节点进行截图时,你可能需要使用其他工具弄半天,但现在你直接选中那个节点,打开 命令(Command) 菜单并且使用 节点截图 就可以了。

截取特定节点对应上图命令是Screenshot Capture node screenshot

截取特定DOM元素示例:

不只是这样,你同样可以用这种方式 实现全屏截图 :通过 Screenshot Capture full size screenshot 命令。

请注意,这里说的是全屏,并不只是页面可视区域,而是包含滚动条在内的所有页面内容。

对应截取全屏示例:

在控制台中使用上次操作的值 🎃

我是最近才发现这个技巧。使用$_可以引用在控制台执行的前一步操作的返回值。如果您正在控制台调试一些JavaScript代码,并且需要引用先前的返回值,那么这可能非常方便。

重新发起xhr请求 🚀

在平时和后端联调时,我们用的最多的可能就是Network面板了。但是每次想重新查看一个请求,我们往往都是通过刷新页面、点击按钮等方式去触发xhr请求,这种方式有时显得会比较麻烦,我们可以通过google提供的Replay XHR的方式去发起一条新的请求,这样对于我们开发效率的提升是有所帮助的。
chrome-调试-重新发起xhr请求

编辑页面上的任何文本 ✍

在控制台输入document.body.contentEditable="true"或者document.designMode = 'on'就可以实现对网页的编辑了。
chrome-调试-网页编辑

其实这个还是比较实用的,比如你要测试一个DOM节点文字太长时,样式是否会混乱,或者要去直接修改页面元素去满足一些业务需求时。(我之前是在Elements面板一个一个去修改的,,,)

网络面板(Network)的幻灯片模式 🌇

启动Network 面板下的Capture screenshots就可以在页面加载时捕捉屏幕截图。有点幻灯片的感觉。

单击每一帧截图,显示的就是对应时刻发生的网络请求。这种可视化的展现形式会让你更加清楚每一时刻发生的网络请求情况。

动画检查 🎏

DevTools 中有一个动画面板,默认情况下它是关闭的,很多人可能不太清楚这个功能。它可以让你控制和操纵 CSS 动画,并且可视化这些动画是如何工作的。

要打开该面板,可以在 DevTools 右上角菜单 → More tools 中打开 Animations

默认情况下,DevTools 会“监听”动画。一旦触发,它们将被添加到列表中。你能看到这些动画块如何显示。在动画本身上,DevTools 会向我们展示哪些属性正在更改,例如 background-colortransform

然后,我们可以通过使用鼠标拖动或调整时间轴来修改该动画。

递增/递减 CSS 属性值 🃏

作为前端开发,平时少不了通过Elements面板去查找元素以及它的css样式。有时调整像素px会比较麻烦一点,这时就可以使用快捷键去帮你完成:

* 增量0.1
  * Mac: Option +向上和Option +向下
  * Windows: Alt +向上和Alt +向下
* 增量1
  * Mac:向上+向下
  * Windows:向上+向下
* 增量10
  * Mac:⇧+向上和⇧+向下
  * Windows:⇧+向上和⇧+向下
* 递增100
  * Mac: ⌘+向上和⌘+向下
  * Windows: Ctrl +向上和Ctrl +向下

在低端设备和弱网情况下进行测试 📱

我们平时开发一般都是在办公室(wifi 网速加快),而且设备一般都是市面上较新的。但是产品的研发和推广,一定要考虑低设备人群和弱网的情况。

Chrome DevTools中可以轻松调节CPU功能和网络速度。这样,我们就可以测试 Web 应用程序性能并进行相应优化。

具体打开方式是:在Chrome DevTools中通过CMD/Ctrl + Shift + p打开命令菜单。然后输入Show Performance打开性能面板。

copying & saving 📜

在调试的过程中,我们总会有对 Dev Tools 里面的数据进行 复制 或者 保存 的操作,其实他们也是有一些小技巧的!

copy()

可以通过全局的方法 copy()consolecopy 任何你能拿到的资源
chrome-调试-copy

Store as global variable

如果在console中打印了一堆数据,想对这堆数据做额外的操作,可以将它存储为一个全局变量。只需要右击它,并选择 “Store as global variable”选项。

第一次使用的话,它会创建一个名为 temp1 的变量,第二次创建 temp2,第三次 ... 。通过使用这些变量来操作对应的数据,不用再担心影响到他们原来的值。

自定义 devtools 🌈

大家平时用的最多的Chrome 主题可能就是白色/黑色这两种了,但用的久了,难免想尝试像IDE一样切换主题。

打开方式

  • 首先需要启用实验模式中的Allow custom UI themes

    • 地址栏输入如下url
    chrome://flags/#enable-devtools-experiments # 启用实验功能
    • 启用实验功能,并重启浏览器

  • 控制台中使用快捷键F1打开设置,切换到Experiments 选项
  • 启用Allow custom UI themes

  • Chrome商店安装Material DevTools Theme Collection扩展程序

  • 选择你喜欢的主题即可

CSS/JS 覆盖率 ✅

Chrome DevTools 中的Coverage功能可以帮助我们查看代码的覆盖率。

打开方式

  • 打开调试面板,用快捷键 shift+command+P (mac)输入 Show Coverage调出相应面板

  • 点击reload 按钮开始检测

  • 点击相应文件即可查看具体的覆盖情况(绿色的为用到的代码,红色表示没有用到的代码)

自定义代码片段 Snippets 🌰

在平常开发过程中,我们经常有些 JavaScript 的代码想在 Chrome Devtools中调试,直接在 console 下 写比较麻烦,或者我们经常有些代码片段(防抖、节流、获取地址栏参数等)想保存起来,每次打开 Devtools 都能获取到这些代码片段,而不用再去google,正好Chrome Devtool 就提供了这种功能。

如图所示,在 Sources 这个tab栏下,有个 Snippets 标签,在里面可以添加一些常用的代码片段。

将图片复制为数据 URI 🦊

打开方式

  • 选择Network面板
  • 在资源面板中选择Img
  • 右键单击将其复制为数据URI(已编码为base 64

媒体查询 🔭

媒体查询是自适应网页设计的基本部分。在Chrome Devtools中的设备模式下,在三圆点菜单中点击 Show Media queries即可启用:

Devtools会在样式表中检测媒体查询,并在顶端标尺中将它们显示为彩色条形:

那怎么使用呢?其实也很简单:

  • 点击媒体查询条形,调整视口大小和预览适合目标屏幕大小的样式
  • 右键点击某个条形,查看媒体查询在 CSS 中何处定义并跳到源代码中的定义

keys/values 🎯

这个是Devtools提供的快速查看一个对象的keyvaluesAPI。用起来也很简单:

你可能会说Object.keys()Object.values()也可以实现啊,但这个不是更简单点吗 🤠

table 🦐

Devtools提供的用于将对象数组记录为表格的API:

❤️ 爱心三连击

1.如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~

2.关注公众号前端森林,定期为你推送新鲜干货好文。

3.特殊阶段,带好口罩,做好个人防护。

4.添加微信fs1263215592,拉你进技术交流群一起学习 🍻
前端森林公众号二维码2

查看原文

阿平 收藏了文章 · 2020-05-26

惊艳!可视化的 js:动态图演示 Promises & Async/Await 的过程!

原由

你是否运行过不按你预期运行的 js 代码 ?

比如:某个函数被随机的、不可预测时间的执行了,或者被延迟执行了。

这时,你需要从 ES6 中引入的一个非常酷的新特性: Promise 来处理你的问题。

为了深入理解 Promise ,我在某个不眠之夜,做了一些动画来演示 Promise 的运行,我多年来的好奇心终于得到实现。

对于 Promise ,您为什么要使用它,它在底层是如何工作的,以及我们如何以最现代的方式编写它呢?

介绍

在书写 JavaScript 的时候,我们经常不得不去处理一些依赖于其它任务的任务!

比如:我们想要得到一个图片,对其进行压缩,应用一个滤镜,然后保存它 。

首先,先用 getImage 函数要得到我们想要编辑的图片。

一旦图片被成功加载,把这个图片值传到一个 ocmpressImage 函数中。

当图片已经被成功地重新调整大小后,在 applyFilter 函数中为图片应用一个滤镜。

在图片被压缩和添加滤镜后,保存图片并且打印成功的日志!

最后,代码很简单如图:

注意到了吗?尽管以上代码也能得到我们想要的结果,但是完成的过程并不是友好。

使用了大量嵌套的回调函数,这使我们的代码阅读起来特别困难。

因为写了许多嵌套的回调函数,这些回调函数又依赖于前一个回调函数,这通常被称为 回调地狱

幸运的,ES6 中的 Promise 的能很好的处理这种情况!

让我们看看 promise 是什么,以及它是如何在类似于上述的情况下帮助我们的。

Promise语法

ES6引入了Promise。在许多教程中,你可能会读到这样的内容:

Promise 是一个值的占位符,这个值在未来的某个时间要么 resolve 要么 reject 。

对于我来说,这样的解释从没有让事情变得更清楚。

事实上,它只是让我感觉 Promise 是一个奇怪的、模糊的、不可预测的一段魔法。

接下来让我们看看 promise 真正是什么?

我们可以使用一个接收一个回调函数的 Promise 构造器创建一个 promise。

好酷,让我们尝试一下!

等等,刚刚得到的返回值是什么?

Promise 是一个对象,它包含一个状态 PromiseStatus 和一个值 PromiseValue

在上面的例子中,你可以看到 PromiseStatus 的值是 pending, PromiseValue 的值是 undefined。

不过 - 你将永远不会与这个对象进行交互,你甚至不能访问 PromiseStatusPromiseValue 这两个属性!

然而,在使用 Promise 的时候,这俩个属性的值是非常重要的。


PromiseStatus 的值,也就是 Promise 的状态,可以是以下三个值之一:

  • fulfilled: promise 已经被 resolved。一切都很好,在 promise 内部没有错误发生。
  • rejected: promise 已经被 rejected。哎呦,某些事情出错了。
  • pending: promise 暂时还没有被解决也没有被拒绝,仍然处于 pending 状态

好吧,这一切听起来很棒,但是什么时候 promise 的状态是 pendingfulfilledrejected 呢? 为什么这个状态很重要呢?

在上面的例子中,我们只是为 Promise 构造器传递了一个简单的回调函数 () => {}

然而,这个回调函数实际上接受两个参数。

  • 第一个参数的值经常被叫做 resolveres,它是一个函数,在 Promise 应该解决 resolve 的时候会被调用。
  • 第二个参数的值经常被叫做 reject rej,它也是一个函数,在 Promise 出现一些错误应该被拒绝 reject 的时候被调用。

让我们尝试看看当我们调用 resolvereject 方法时得到的日志。

在我的例子中,把 resolve 方法叫做 res,把 reject 方法叫做 rej

太好了!我们终于知道如何摆脱 pending 状态和 undefined 值了!

  • 当我们调用 resolve 方法时,promise 的状态是 fulfilled
  • 当我们调用 reject 方法时,promise 的状态是 rejected
有趣的是,我让(Jake Archibald)校对了这篇文章,他实际上指出 Chrome 中存在一个错误,该错误当前将状态显示为 “ fulfilled” 而不是 “ resolved”。感谢 Mathias Bynens,它现已在Canary 中修复! 🥳🕺🏼

好了,现在我们知道如何更好控制那个模糊的 Promise 对象。但是他被用来做什么呢?

在前面的介绍章节,我展示了一个获得图片、压缩图片、为图片应用过滤器并保存它的例子!最终,这变成了一个混乱的嵌套回调。

幸运的,Promise 可以帮助我们解决这个问题!

首先,让我们重写整个代码块,以便每个函数返回一个 Promise 来代替之前的函数。

如果图片被加载完成并且一切正常,让我们用加载完的图片解决 (resolve)promise

否则,如果在加载文件时某个地方有一个错误,我们将会用发生的错误拒绝 (reject)promise

让我们看下当我们在终端运行这段代码时会发生什么?

非常酷!就像我们所期望的一样,promise 得到了解析数据后的值。

但是现在呢?我们不关心整个 promise 对象,我们只关心数据的值!幸运的,有内置的方法来得到 promise 的值。

对于一个 promise,我们可以使用它上面的 3 个方法:

  • .then(): 在一个 promise 被 resolved 后调用
  • .catch(): 在一个 promise 被 rejected 后被调用
  • .finally(): 不论 promise 是被 resolved 还是 reject 总是调用

.then 方法接收传递给 resolve 方法的值。

.catch 方法接收传递给 rejected 方法的值。

最终,我们拥有了 promise 被解决后 (resolved) 的值,并不需要整个 promise 对象!

现在我们可以用这个值做任何我们想做的事。


顺便提醒一下,当你知道一个 promise 总是 resolve 或者总是 reject 的时候,你可以写 Promise.resolvePromise.reject,传入你想要 rejectresolvepromise 的值。

在下边的例子中你将会经常看到这个语法。

在 getImage 的例子中,为了运行它们,我们最终不得不嵌套多个回调。幸运的,.then 处理器可以帮助我们完成这件事!

.then 它自己的执行结果是一个 promise。这意味着我们可以链接任意数量的 .then:前一个 then 回调的结果将会作为参数传递给下一个 then 回调!

在 getImage 示例中,为了传递被处理的图片到下一个函数,我们可以链接多个 then 回调。

相比于之前最终得到许多嵌套回调,现在我们得到了整洁的 then 链。

完美!这个语法看起来已经比之前的嵌套回调好多了。

宏任务和微任务(macrotask and microtask)

我们知道了一些如何创建 promise 以及如何提取出 promise 的值的方法。

让我们为脚本添加一些更多的代码并且再次运行它:

等下,发生了什么?!

首先,Start! 被输出。

好的,我们已经看到了那一个即将到来的消息:console.log('Start!') 在最前一行输出!

然而,第二个被打印的值是 End!,并不是 promise 被解决的值!只有在 End! 被打印之后,promise 的值才会被打印。

这里发生了什么?

我们最终看到了 promise 真正的力量! 尽管 JavaScript 是单线程的,我们可以使用 Promise 添加异步任务!

等等,我们之前没见过这种情况吗?

JavaScript Event Loop 中,我们不是也可以使用浏览器原生的方法如 setTimeout 创建某类异步行为吗?

是的!然而,在事件循环内部,实际上有 2 种类型的队列:宏任务(macro)队列 (或者只是叫做 任务队列 )和 微任务队列

(宏)任务队列用于 宏任务,微任务队列用于 微任务

那么什么是宏任务,什么是微任务呢?

尽管他们比我在这里介绍的要多一些,但是最常用的已经被展示在下面的表格中!

| | | | |
| :------: | :------: | :------: | :------: |
| (Macro)task: | setTimeout | setInterval | setImmediate |
| Microtask: | process.nextTick | Promise callback | queueMicrotask |

我们看到 Promise 在微任务列表中! 当一个 Promise 解决 (resolve) 并且调用它的 then()catch()finally() 方法的时候,这些方法里的回调函数被添加到微任务队列!

这意味着 then(),chatch() 或 finally() 方法内的回调函数不是立即被执行,本质上是为我们的 JavaScript 代码添加了一些异步行为!

那么什么时候执行 then(),catch(),或 finally() 内的回调呢?

事件循环给与任务不同的优先级:

  1. 当前在调用栈 (call stack) 内的所有函数会被执行。当它们返回值的时候,会被从栈内弹出。
  2. 当调用栈是空的时,所有排队的微任务会一个接一个从微任务任务队列中弹出进入调用栈中,然后在调用栈中被执行!(微任务自己也能返回一个新的微任务,有效地创建无限的微任务循环 )
  3. 如果调用栈和微任务队列都是空的,事件循环会检查宏任务队列里是否还有任务。如果宏任务中还有任务,会从宏任务队列中弹出进入调用栈,被执行后会从调用栈中弹出!

让我们快速地看一个简单的例子:

  • Task1: 立即被添加到调用栈中的函数,比如在我们的代码中立即调用它。
  • Task2,Task3,Task4: 微任务,比如 promisethen 方法里的回调,或者用 queueMicrotask 添加的一个任务。
  • Task5,Task6: 宏任务,比如 setTimeout 或者 setImmediate 里的回调

首先,Task1 返回一个值并且从调用栈中弹出。然后,JavaScript 引擎检查微任务队列中排队的任务。一旦微任务中所有的任务被放入调用栈并且最终被弹出,JavaScript 引擎会检查宏任务队列中的任务,将他们弹入调用栈中并且在它们返回值的时候把它们弹出调用栈。

图中足够粉色的盒子是不同的任务,让我们用一些真实的代码来使用它!

在这段代码中,我们有宏任务 setTimeout 和 微任务 promise 的 then 回调。

一旦 JavaScript 引擎到达 setTimeout 函数所在的那行就会涉及到事件循环。

让我们一步一步地运行这段代码,看看会得到什么样的日志!

快速提一下:在下边的例子中,我正在展示的像 console.logsetTimeoutPromise.resolve 等方法正在被添加到调用栈中。它们是内部的方法实际上没有出现在堆栈痕迹中,因此如果你正在使用调试器,不用担心,你不会在任何地方见到它们。它只是在没有添加一堆样本文件代码的情况下使这个概念解释起来更加简单。

在第一行,JavaScript 引擎遇到了 console.log() 方法,它被添加到调用栈,之后它在控制台输出值 Start!。console.log 函数从调用栈内弹出,之后 JavaScript 引擎继续执行代码。

JavaScript 引擎遇到了 setTimeout 方法,他被弹入调用栈中。setTimeout 是浏览器的原生方法:它的回调函数 (() => console.log('In timeout')) 将会被添加到 Web API,直到计时器完成计时。尽管我们为计时器提供的值是 0,在它被添加到宏任务队列 (setTimeout 是一个宏任务) 之后回调还是会被首先推入 Web API

JavaScript 引擎遇到了 Promise.resolve 方法。Promise.resolve 被添加到调用栈。在 Promise 解决 (resolve) 值之后,它的 then 中的回调函数被添加到微任务队列。

JavaScript 引擎看到调用栈现在是空的。由于调用栈是空的,它将会去检查在微任务队列中是否有在排队的任务!是的,有任务在排队,promisethen 中的回调函数正在等待轮到它!它被弹入调用栈,之后它输出了 promise 被解决后( resolved )的值: 在这个例子中的字符串 Promise!

JavaScript 引擎看到调用栈是空的,因此,如果任务在排队的话,它将会再次去检查微任务队列。此时,微任务队列完全是空的。

到了去检查宏任务队列的时候了:setTimeout 回调仍然在那里等待!setTimeout 被弹入调用栈。回调函数返回 console.log 方法,输出了字符串 In timeout!setTimeout 回调从调用栈中弹出。

终于,所有的事情完成了! 看起来我们之前看到的输出最终并不是那么出乎意料。

Async/Await

ES7 引入了一个新的在 JavaScript 中添加异步行为的方式并且使 promise 用起来更加简单!随着 asyncawait 关键字的引入,我们能够创建一个隐式的返回一个 promiseasync 函数。但是,我们该怎么做呢?

之前,我们看到不管是通过输入 new Promise(() => {})Promise.resolvePromise.reject,我们都可以显式的使用 Promise 对象创建 promise

我们现在能够创建隐式地返回一个对象的异步函数,而不是显式地使用 Promise 对象!这意味着我们不再需要写任何 Promise 对象了。

尽管 async 函数隐式的返回 promise 是一个非常棒的事实,但是在使用 await 关键字的时候才能看到 async 函数的真正力量。当我们等待 await 后的值返回一个 resolvedpromise 时,通过 await 关键字,我们可以暂停异步函数。如果我们想要得到这个 resolvedpromise 的值,就像我们之前用 then 回调那样,我们可以为被 awaitpromise 的值赋值为变量!

这样,我们就可以暂停一个异步函数吗?很好,但这到底是什么意思?

当我们运行下面的代码块时让我们看下发生了什么:

额,这里发生了什么呢?

首先,JavaScript 引擎遇到了 console.log。它被弹入到调用栈中,这之后 Before function! 被输出。

然后,我们调用了异步函数myFunc(),这之后myFunc函数体运行。函数主体内的最开始一行,我们调用了另一个console.log,这次传入的是字符串In function!console.log被添加到调用栈中,输出值,然后从栈内弹出。

函数体继续执行,将我们带到第二行。最终,我们看到一个await关键字!

最先发生的事是被等待的值执行:在这个例子中是函数one。它被弹入调用栈,并且最终返回一个解决状态的promise。一旦Promise被解决并且one返回一个值,JavaScript遇到了await关键字。

当遇到await关键字的时候,异步函数被暂停。函数体的执行被暂停,async函数中剩余的代码会在微任务中运行而不是一个常规任务!

现在,因为遇到了await关键字,异步函数myFunc被暂停,JavaScript引擎跳出异步函数,并且在异步函数被调用的执行上下文中继续执行代码:在这个例子中是全局执行上下文! ‍♀️

最终,没有更多的任务在全局执行上下文中运行!事件循环检查看看是否有任何的微任务在排队:是的,有!在解决了one的值以后,异步函数myFunc开始排队。myFunc被弹入调用栈中,在它之前中断的地方继续运行。

变量res最终获得了它的值,也就是one返回的promise被解决的值!我们用res的值(在这个例子中是字符串One!)调用console.logOne!被打印到控制台并且console.log从调用栈弹出。

最终,所有的事情都完成了!你注意到async函数相比于promisethen有什么不同吗?await关键字暂停了async函数,然而如果我们使用then的话,Promise的主体将会继续被执行!

嗯,这是相当多的信息! 当使用Promise的时候,如果你仍然感觉有一点不知所措,完全不用担心。我个人认为,当使用异步JavaScript的时候,只是需要经验去注意模式之后便会感到自信。

当使用异步JavaScript的时候,我希望你可能遇到的“无法预料的”或“不可预测的”行为现在变得更有意义!

最后

外国友人技术博客的语言表达的方式和风格、与国人的还是有很大差别的啊。

每每看到有很长或者很拗口的句子的时候,我就想按自己的语言来写一篇了 🤩

可能自己写一篇都比翻译的快 🤩

推荐阅读:

  1. 通过10个实例小练习,快速入门熟练 Vue3 核心新特性

2. 重磅:GitHub 上 100K+ Star 的前端面试开源项目汇总(进大厂必备)

  1. 强烈推荐: GitHub 上 170K+ Star 的前端学习的数据结构与算法项目
  2. 一张图理清Vue 3.0的响应式系统,实现精简版的响应式系统
  3. 黑学习方式:GitHub 上能挖矿的神仙技巧 - 如何发现优秀开源项目

支持一下下👇

查看原文

阿平 提出了问题 · 2019-08-13

解决js数组push会自动去除精度?

问题如下:

clipboard.png

有没有大佬给解惑一下,我现在想要插入什么就是什么,可以实现吗?谢谢!

关注 4 回答 3

阿平 收藏了文章 · 2019-07-26

前端离线化探索

原文:前端离线化探索
作者:flyfu wang

飞机上的梗

某天,小明同学突然反馈 :“昨晚发现根本无法使用你们的应用... ....怎么回事呢”。我和我的小伙伴们立马惊呆了,心想:“老司机多年的经验有一种预感,那就是同学你使用姿势不正确...(此处省略300字)”。 然后默默排查了许久,答案竟然是:

“小明昨晚在飞机上”。

为了以后能够在飞机上愉快的玩耍,这里的离线体验我们有必要再着重优化下。自此,一个不分昼夜的需求快马飞鞭地提上了日程。

离线化带来的价值

在这个流量日益白菜价,不断提及云端计算、5g网络的时代,有人觉得,离线已经完全没有必要。谈及离线,仿佛想到的是深海老林,荒无人烟之处。事实上,离线离我们的生活很近,也非常频繁。高速公路、地铁隧道、楼道角落,以及诸多日常信号不稳定区域,这些场景每天都有大量用户经过,每天有成千万用户频繁因为网络问题,心底里吐槽抱怨过我们的应用,断网离线并非我们的错,但我们是否能够从用户体验的角度,尝试改善他们在进入弱网或无网络状态时的焦虑情绪呢,从而给产品带来更正向的体验收益,提升用户留存与口碑。

乐观UI

谈及改善用户焦虑情绪,很有必要介绍下乐观 UI[Optimistic User Interfaces]。乐观 UI 是一种界面的响应模式,它推荐前端在服务端接收响应之前,先更新 UI,一旦服务器返回,再变更为实际结果。

比如,用户点击按钮,前端更新数据状态为成功,请求到达后台,服务器响应,更新前端数据。因为99%的响应都是成功的,所以只有少部分用户需要退回到失败状态。

乐观 UI 不是一种先进的技术和新东西,而是一种“离线优先”思维模式下,改善用户体验情绪的设计。

前端离线化几种常用的方案

Application Cache

HTML5 最早提供一种了一种缓存机制,可以使web的应用程序离线运行。我们使用 Application Cache 接口设置浏览器应该缓存的资源,即配置manifest文件, 在用户处于离线状态时,点击刷新按钮,应用也能正常加载与工作。

不过该接口很快被标准废弃了,原因之一是这是个设计很不合理的接口,比如更新不及时,无法做到用 javascript 精细化控制,可用性很差,如果你不严格的遵循其规则,会遇到很多坑。取而代之的是更强大的service-worker。

service-worker

正因为Application Cache一直无法有效的解决离线资源精细化控制,service-worker (以下简称sw)接口被设计出来了,比起Application Cache,它提供独立的后台JS线程,是一种特殊的worker上下文访问环境。在渐进式web应用PWA中,sw为Network independent特性提供了最核心的支持。

借助CacheStorage,我们可以在 sw 安装激活的生命周期中,按需填充缓存资源,然后在fetch 事件中,拦截 http 请求,将缓存资源或者自定义消息返回给页面。

service-worker 实现了真正的可用性及安全性。首先,相对于原有web 应用逻辑是不可见,它类似于一个中间拦截服务,中间发生任何错误,都会退回到请求线上逻辑。其次,它只能在 https 下运行保证了安全性。

sw对于我们的离线化方案而言,有一个致命的问题,就是ios webview 兼容性问题。ios 11.3以上自带的Safari是支持 ws,然而, 苹果一贯的特性, 默认UIWebView 不支持service-worker。

离线数据

事实上,我们的大部分离线场景将是会在本地独立 app 之中,借助客户端能力,我们可以把 web 代码包提前内置到客户端之中,然后使用一套代码更新机制,前端代码缓存问题可以得到解决。离线代码加载和更新逻辑本身不复杂,下面是一个简化图,具体特定业务场景下还需要考虑比如是否灰度用户,代码版本和数据是否同步等问题。

离线化方案的复杂度之一在于离线数据的处理,及如何对设计之初就没有考虑过“Offline First”的旧代码进行最小改造处理,既优先考虑在离线状态的基本功能,在线时再进一步增强。基于离线和在线逻辑解耦的考虑,我们应该本着最大限度减少对原有在线逻辑侵入的原则去思考离线化方案。我们看下常见的离线数据前端方案。

PouchDB

PouchDB 是一个跨平台javascript 数据库,内部封装了IndexDB、WebSql兼容前端处理.

一般而言前端pouchDB进行离线处理,搭配后台CouchDB,可以更方便双向数据同步。

Sync 接口专门用不同步前后的数据:

在中小型项目,特别是那种后台可以由前端接手的全栈式开发,pouchDB是一种不错的离线数据处理方案。此方案问题是压缩后任然有130多kb,并且依赖于特定后台方案,不够通用。

Redux-Offline

对于项目使用了 redux 数据管理的项目而言,最快捷的办法,就是使用 redux-offline,其基本思路是通过redux middleware 监听每次 acton 数据变化,然后将需要离线的数据序列化到本地(对于 web 浏览而言存储兼容顺序是indexdb—websql—localstorage),等下一次刷新页面时,优先从本地还原数据还原到 store 中。这种方案的好处是快速配置需要缓存的API接口到中间件即可,充分结合了 redux 特性,对于想要达到简单优先展示离线数据的应用而言,是非常不错的。

但这种思路带来的问题是操作数据不够灵活,本地储存数据无法方便的和其他非 redux逻辑共享。在离线数据量较大的情况,一次性读写,并同时序列化大量本地数据也会带来性能问题,对于频繁有数据变更的场景也不合适。

Redux与IndexDB结合

如果想要达到对数据精细化控制,并且同时不对原有在线逻辑有过多的侵入,我们可以在数据储存上用 IndexDB 替换后台返回数据,前端数据处理仍然复用原有redux。

业务数据的本地储存需要注意的就是合理抽象业务使用的数据,然后按照数据库设计的基本原则本地建表,这里也可以和后台同学聊聊,避免有遗漏的设计问题。

由于IndexDB 原生操作api比较粗糙,我们分装了一套通用DB底层操作库,同时将api接口抽象出来,以 git 子仓库的形式在各业务放公用。这里首先简化了前端业务层DB本地读写、排序等逻辑, 便于相互关联项目的共用,其次将 DB 抽象出来也是为了更好的方便业务本身可以不依赖 IndexDB本身,可以结合客户端特性,给底层数据库替换及进行优化提供了便捷,或者对于纯web 端,为向下兼容可以使用WebSql、LocalStorage等兼容提供了拓展。

对于前端代码架构上,如何借助 redux将原有的在线请求后台接口,快速优雅的转换到对本地的读写呢?

比较合适的做法是,单独抽出一层redux 中间件,通过配置文件的形式,将需要离线的 API初始化时传进去,然后在middleware中,完成对 DB 的读写操作,将数据组装好给下一个 reducer,我们可以叫offline中间件。为了更进一步合理的对 api 参数分解出来,我们也需要在offline 中间件前将接口请求层再抽象一个中间件,我们叫 API middleware ,这样经过离线中间件的 api 参数已经被分解,可以直接作为查询 db 使用,同时也能服务于后台请求。

离线优先与数据同步

我们已经可以通过配置将需要离线的接口通过offline中间件进行离线化,那么这里面临着两种数据更新方式,第一种是在线时走正常逻辑等待后台数据返回,异步同步到本地数据库,再进行渲染;当判断离线时,从本地读取。还有一种是具备乐观UI的思维,配置了离线的接口,优先从本地进行数据操作,渲染UI,然后再将服务端数据与本地数据进行同步。显然,后者离线优先的方案显得更为明智。

数据同步分为本地向后台同步,和后台向本地同步。后者需要增加增量变更的逻辑,用于解决离线下用户数据由于其他原因发生的变更,比如当用户登录多台设备数据移动、删除等场景(前端离线增量变更涉及很多细节及业务相关考虑点,这里暂不细述)。

如何记录本地的数据变更然后同步到后台呢?这里我们需要定义一个数据变更的抽象,比如Change

里面功能主要是定义变更类型,字段等。每次抵达offline中间件的数据通过一个数据同步管理器对变更进行注册,待合适的时机再去同步。数据同步管理器主要接受change,进行diff管理,判断数据是否有变化,及去重管理,最后再触发异步同步任务。同步可能会失败,这里的超时,重试,失败退回处理都需要加以注意,保证同步的事务性。

储存安全

储存安全包括数据加密安全和储存大小问题。对于对称加密,前端查看,客户端必须要知道密匙,密匙本身绕不开加密的问题,理论上,不和服务端通信的离线状态,任何能够在前端能够离线下查看的数据,不管采用什么加密手段,数据都能被还原。纯前端数据加密并无可靠性, 但是访问权限可以依赖于IndexDB浏览器同源策略进行数据安全隔离。避免明文储存和加大数据直接还原的难度才是思考的方向。

有一个容易忽视的安全问题是 iframe, 它可以访问它所嵌入的源的 IndexedDB 库,所以我们需要保证页面全部资源可信任。

采用IndexDB 的储存方案涉及到一个储存大小问题,浏览器的最大存储空间是动态的,总共为可用磁盘空间的50%,每个站点为所用空间的20%,超出限制的写入将导致数据被删除,并且导严重在的数据丢失。因为从浏览器本身无法直接获取到 IndexDB 储存空间(以字符串方式计算性能不可靠,也极不准确),从产品统计角度,限制储存条数是一直思路之一,当然更好的方案是采用端上储存比如larveldb,杜绝此类数据丢失现象。对于纯 web 端,采用浏览器插件拓展的形式也值得尝试(比如 Google doc),更合理的保证数据安全。

离线化是很多前端项目不会设计进去的特性,因为对于大部分纯展示型 web 项目而言,它的收益性价比低。但作为工具型,创造型应用而言,离线会是一个具有长期受益的特性,想象一个艺术家,在飞机上看着风景,突然灵光一现,打开我们的产品进行创作,提示它无法使用,可是不小的损失....


AlloyTeam 欢迎优秀的小伙伴加入。
简历投递: alloyteam@qq.com
详情可点击 腾讯AlloyTeam招募Web前端工程师(社招)

clipboard.png

查看原文

阿平 收藏了文章 · 2019-06-28

强烈推荐 GitHub 上值得前端学习的开源实战项目

clipboard.png

强烈推荐 GitHub 上值得前端学习的开源实战项目。

Vue.js

React.js

Angular

Node.js

最后

笔者博客首更地址 :https://github.com/biaochenxuying/blog

欢迎关注公众号: 全栈修炼,每周至少更新两篇高质量的文章,为你保驾护航 !

clipboard.png

查看原文

阿平 收藏了文章 · 2019-06-05

[译] 在你学习 React 之前必备的 JavaScript 基础

写在前面

为了不浪费大家的宝贵时间,在开头申明一下,这篇文章针对的阅读对象是:没有写过 React 或者刚刚才接触 React 并且对于 ES6 的语法不太了解的同学,这是一篇基础入门的文章,在一开始我并没有准备翻译一篇这样的基础文章,但是在阅读完全文之后,我想起自己刚开始学习 React 时的迷茫,ES6 有那么多,我需要掌握多少呢?对于一个急于上手 React 写代码的人来说,这篇文章告诉你最基本要掌握的知识,让你快速的写起来。但是后期的提高,仍旧需要去夯实 Javascript 的基础。

前言

在理想的状态下,你可以在深入了解React之前了解 JavaScriptWeb 开发的所有知识。 不幸的是,我们生活在一个不完美的世界,所以在 React 之前把所有的JavaScript 都咀嚼一遍只会让你举步维艰。 如果你已经拥有一些 JavaScript 经验,那么在 React 之前你需要学习的只是实际用于开发 React 应用程序的 JavaScript 功能。 在学习 React之前你应该学会的JavaScript 的知识点:

  • ES6
  • 使用 let / const 声明变量
  • 箭头函数
  • 解构赋值
  • Mapfilter
  • ES6 模块系统

这是你将在 80% 的时间内使用的20%JavaScript 新特性,因此在本文中,我将帮助你学习所有这些特性。

创建 React 应用程序的探索

开始学习 React 的常见情况是运行 create-react-app 包,它会设置运行 React 所需的一切。 该过程完成之后,打开 src / app.js 这里给我们展示了整个应用程序中唯一的 React 类:


import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img data-original={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  }
}

export default App;

如果之前你从未学习过 ES6 ,那么你可能认为这个 class 语句是 React 的一个特性。 实际上这是 ES6 的一个新特性,这就是为什么正确学习 ES6 可以让你更好地理解 React 代码。 我们将从 ES6 的类开始。

ES6 的类

ES6 引入了 class 语法,类似于 JavaPythonOO(面向对象) 语言。 ES6 中的基本类如下所示:

class Developer {
  constructor(name){
    this.name = name;
  }

  hello(){
    return 'Hello World! I am ' + this.name + ' and I am a web developer';
  }
}

class 语法后跟一个可用于创建新对象的标识符(或一个名称)。 始终在对象初始化中调用构造函数方法。 传递给这个对象的任何参数都将传递给新对象。 例如:

var nathan = new Developer('Nathan');
nathan.hello(); // Hello World! I am Nathan and I am a web developer

类可以定义任意它所需的方法,在这种情况下,我们定义了一个返回字符串的 hello 方法。

类继承

类可以扩展另一个类的定义,从该类初始化的新对象将具有这两个类的所有方法。

class ReactDeveloper extends Developer {
  installReact(){
    return 'installing React .. Done.';
  }
}

var nathan = new ReactDeveloper('Nathan');
nathan.hello(); // Hello World! I am Nathan and I am a web developer
nathan.installReact(); // installing React .. Done.

继承另一个类的类,通常称为 child 类或 sub 类,而正在扩展的类称为 parent 类或 super 类。 子类也可以覆盖父类中定义的方法,这意味着它将使用自己定义的新方法来替换父类方法的定义。 例如,让我们覆盖 hello 函数:

class ReactDeveloper extends Developer {
  installReact(){
    return 'installing React .. Done.';
  }

  hello(){
    return 'Hello World! I am ' + this.name + ' and I am a REACT developer';
  }
}

var nathan = new ReactDeveloper('Nathan');
nathan.hello(); // Hello World! I am Nathan and I am a REACT developer

就这样,我们重写了 Developer 类中的 hello 方法。

在React 中使用

现在我们了解了 ES6 的类和继承,我们可以理解 src / app.js 中定义的 React 类。 这是一个 React 组件,但它实际上只是一个普通的 ES6 类,它继承了从 React 包导入的 React Component 类的定义。

import React, { Component } from 'react';

class App extends Component {
  // class content
  render(){
    return (
      <h1>Hello React!</h1>
    )
  }
}

这使得我们能够使用 render() 方法,JSXthis.state 和其他方法。 所有这些定义都在Component 类中。 但正如我们稍后将看到的,class 不是定义 React Component 的唯一方法。 如果你不需要状态和其他生命周期方法,则可以使用函数。

使用 ES6 中的 letconst 来声明变量

因为 JavaScriptvar 关键字是声明全局的变量,所以在 ES6 中引入了两个新的变量声明来解决这个问题,即 letconst 。 它们都用于声明变量。 区别在于 const 在声明后不能改变它的值,而 let 则可以。 这两个声明都是本地的,这意味着如果在函数作用域内声明 let ,则不能在函数外部调用它。

const name = "David";
let age = 28;
var occupation = "Software Engineer";

用哪个呢?

按以往经验来说,默认使用 const 声明变量。 稍后当您编写应用程序时,当你意识到 const 的值需要更改,才是你应该将const 重构为 let 时。 希望它会让你习惯新的关键字,并且你将开始认识到应用程序中需要使用 constlet 的模式。

我们什么时候在 React 中使用呢?

在我们需要变量的时候:

import React, { Component } from 'react';

class App extends Component {
  // class content
  render(){
    const greeting = 'Welcome to React';
    return (
      <h1>{greeting}</h1>
    )
  }
}

在整个应用的生命周期中 greeting 并不会发生改变,所以我们在这里使用 const

箭头函数

箭头函数是 ES6 的一种新特性,在现代代码库中几乎被广泛使用,因为它使代码简洁易读。 它允许我们使用更短的语法编写函数。

// regular function
const testFunction = function() {
  // content..
}

// arrow function
const testFunction = () => {
  // content..
}

如果您是一位经验丰富的 JS 开发人员,那么从常规函数语法转换为箭头语法可能会让您感到不舒服。 当我学习箭头函数时,我用这两个简单的步骤来重写我的函数:

  1. 移除 function 关键字
  2. () 后面加上 =>

括号仍然用于传递参数,如果只有一个参数,则可以省略括号。

const testFunction = (firstName, lastName) => {
  return firstName+' '+lastName;
}

const singleParam = firstName => {
  return firstName;
}

隐藏的 return

如果箭头函数只有一行,则可以返回值而无需使用 return 关键字以及大括号。

const testFunction = () => 'hello there.';
testFunction(); 

React 中的使用

const HelloWorld = (props) => {
  return <h1>{props.hello}</h1>;
}

等同于 ES6 的类组件

class HelloWorld extends Component {
  render() {
    return (
      <h1>{props.hello}</h1>;
    );
  }
}

React 应用程序中使用箭头功能可使代码更简洁。 但它也会从组件中删除状态的使用。 这种类型的组件称为无状态功能组件。 你会在许多 React 教程中看到这个名字。

解析数组和对象的赋值

ES6 中引入的最有用的新语法之一,解构赋值只是复制对象或数组的一部分并将它们放入命名变量中。 一个简单的例子:

const developer = {
  firstName: 'Nathan',
  lastName: 'Sebhastian',
  developer: true,
  age: 25,
}

//destructure developer object
const { firstName, lastName } = developer;
console.log(firstName); // returns 'Nathan'
console.log(lastName); // returns 'Sebhastian'
console.log(developer); // returns the object

如您所见,我们将开发人员对象中的 firstNamelastName 分配给新变量 firstNamelastName 。 现在,如果要将 firstName 放入名为 name 的新变量中,该怎么办?

const { firstName:name } = developer;
console.log(name); // returns 'Nathan'

解构也适用于数组,使用索引而不是对象键:

const numbers = [1,2,3,4,5];
const [one, two] = numbers; // one = 1, two = 2

你可以通过传入 , 来在解构的过程中跳过一些下标:

const [one, two, , four] = numbers; // one = 1, two = 2, four = 4

React 中的使用

最常见是在方法中解构 state:

reactFunction = () => {
  const { name, email } = this.state;
};

或者是在无状态的函数组件中,结合之前提到的例子:

const HelloWorld = (props) => {
  return <h1>{props.hello}</h1>;
}

我们可以立即简单地解构参数:

const HelloWorld = ({ hello }) => {
  return <h1>{hello}</h1>;
}

Mapfilter

虽然本文侧重于 ES6 ,但需要提及 JavaScript 数组 Mapfilter 方法,因为它们可能是构建 React 应用程序时最常用的 ES5 功能之一。 特别是在处理数据上。

这两种方法在处理数据时使用得更多。 例如,假设从 API 结果中获取返回 JSON 数据的数组:

const users = [
  { name: 'Nathan', age: 25 },
  { name: 'Jack', age: 30 },
  { name: 'Joe', age: 28 },
];

然后我们可以在 React 中呈现项目列表,如下所示:

import React, { Component } from 'react';

class App extends Component {
  // class content
  render(){
    const users = [
      { name: 'Nathan', age: 25 },
      { name: 'Jack', age: 30 },
      { name: 'Joe', age: 28 },
    ];

    return (
      <ul>
        {users
          .map(user => <li>{user.name}</li>)
        }
      </ul>
    )
  }
}

我们同样可以在 render 中筛选数据

<ul>
  {users
    .filter(user => user.age > 26)
    .map(user => <li>{user.name}</li>)
  }
</ul>

ES6 模块系统

ES6 模块系统使 JavaScript 能够导入和导出文件。 让我们再看一下 src / app.js 代码来解释这一点。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img data-original={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  }
}

export default App;

在第一行代码中我们看到 import 语句:

import React, { Component } from 'react';

在第一行代码中我们看到 export default 语句:

export default App;

要理解这些语句,我们先讨论模块语法。

模块只是一个 JavaScript 文件,它使用 export 关键字导出一个或多个值(可以是对象,函数或变量)。 首先,在 src 目录中创建一个名为 util.js 的新文件

touch util.js

然后我们在这里面写一个函数,使用一个默认导出

export default function times(x) {
  return x * x;
}

或多个命名的导出

export function times(x) {
  return x * x;
}

export function plusTwo(number) {
  return number + 2;
}

然后我们可以在 src/App.js 中引入它。

import { times, plusTwo } from './util.js';

console.log(times(2));
console.log(plusTwo(3));

每个模块可以有多个命名导出但只有一个默认导出。 可以导入默认导出,而无需使用花括号和相应的导出函数名称:

// in util.js
export default function times(x) {
  return x * x;
}

// in app.js
export k from './util.js';

console.log(k(4)); // returns 16

但是对于命名导出,必须使用花括号和确切名称导入。 或者,import可以使用别名来避免两个不同的导入具有相同的名称:

// in util.js
export function times(x) {
  return x * x;
}

export function plusTwo(number) {
  return number + 2;
}

// in app.js
import { times as multiplication, plusTwo as plus2 } from './util.js';

直接这样引入名称:

import React from 'react';

将使 JavaScript 检查node_modules 以获取相应的包名称。 因此,如果您要导入本地文件,请不要忘记使用正确的路径。

在 React 中使用

显然我们已经在 src / App.js 文件中看到了这个,然后在 index.js 文件中看到了导出的 App 组件的呈现方式。 我们暂时忽略 serviceWorker 部分。

//index.js file

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

请注意如何从 ./App 目录导入 App ,并省略了 .js 扩展名。 我们只能在导入 JavaScript 文件时省略文件扩展名,但在其他文件中我们必须包含扩展名,例如 .css 。 我们还导入另一个 node 模块 react-dom ,这使我们能够将 React 组件呈现为 HTML元素。

至于 PWA ,它是使 React 应用程序脱机工作的一项功能,但由于默认情况下它已被禁用,因此无需在开始时学习它。 在你有足够的信心构建 React 用户界面之后,最好学习 PWA

总结

React 的优点在于它不会像其他 Web 框架一样在 JavaScript 之上添加任何外部抽象层。 这就是为什么 React 变得非常受 JS 开发人员欢迎的原因。 它只是使用最好的 JavaScript 来使构建用户界面更容易和可维护。 在 React 应用程序中,确实有比 React specix 语法更多的 JavaScript ,所以一旦你更好地理解了 JavaScript - 特别是 ES6 - 你就可以自信地编写 React 应用程序了。 但这并不意味着您必须掌握 JavaScript 的所有内容才能开始编写 React 应用程序。 现在去写一个,随着机会的到来,你将成为一个更好的开发者。
感谢阅读,我希望你学到一些新东西:)

最后

小册 你不知道的 Chrome 调试技巧 已经开始预售啦。

欢迎关注公众号 「前端恶霸」,扫码关注,会有很多好东西等着你~

查看原文

阿平 收藏了文章 · 2019-06-04

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

图片描述

目前Electrongithub上面的star量已经快要跟React-native一样多了

这里吐槽下,webpack感觉每周都在偷偷更新,很糟心啊,还有Angular更新到了8,Vue马上又要出正式新版本了,5G今年就要商用,华为的系统也要出来了,RN还没有更新到正式的1版本,还有号称让前端开发者失业的技术flutter也在疯狂更新,前端真的是学不完的

图片描述
图片描述

回到正题,不能否认,现在的大前端,真的太牛了,PC端可以跨三种平台开发,移动端可以一次编写,生成各种小程序以及React-native应用,然后跑在ios和安卓以及网页中 , 这里不得不说-------京东的Taro框架 这些人 已经把Node.jswebpack用上了天

webpack不熟悉的,看我之前的文章 ,今天不把重点放在webpack

欢迎关注我的专栏 《前端进阶》 都是百星高赞文章

先说说Electron官网介绍:

使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用 ,如果你可以建一个网站,你就可以建一个桌面应用程序。 Electron 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可。

  • 什么意思呢?
  • Electron = Node.js + 谷歌浏览器 + 平常的JS代码生成的应用,最终打包成安装包,就是一个完整的应用
  • Electron分两个进程,主进程负责比较难搞的那部分,渲染进程(平常的JS代码)部分,负责UI界面展示
  • 两个进程之间可以通过remote模块,以及IPCRenderIPCMain之间通信,前者类似于挂载在全局的属性上进行通信(很像最早的命名空间模块化方案),后者是基于发布订阅机制,自定义事件的监听和触发实现两个进程的通信。
  • Electron相当于给React生成的单页面应用套了一层壳,如果涉及到文件操作这类的复杂功能,那么就要依靠Electron的主进程,因为主进程可以直接调用Node.jsAPI,还可以使用C++插件,这里Node.js的牛逼程度就凸显出来了,既可以写后台的CRUD,又可以做中间件,现在又可以写前端。

谈谈技术选型

  • 使用React去做底层的UI绘制,大项目首选React+TS
  • 状态管理的最佳实践肯定不是Redux,目前首选dva,或者redux-saga
  • 构建工具选择webpack,如果不会webpack真的很吃亏,会严重限制你的前端发展,所以建议好好学习Node.jswebpack
  • 选择了普通的Restful架构,而不是GraphQL,可能我对GraphQL理解不深,没有领悟到精髓
  • 在通信协议这块,选择了websoket和普通的http通信方式
  • 因为是demo,很多地方并没有细化,后期会针对electron出一个网易云音乐的开源项目,这是一定要做到的

先开始正式的环境搭建

clipboard.png

  • config文件放置webpack配置文件
  • server文件夹放置Node.js的后端服务器代码
  • src下放置源码
  • main.jsElectron的入口文件
  • json文件是脚本入口文件,也是包管理的文件~

开发模式项目启动思路:

  • 先启动webpack将代码打包到内存中,实现热更新
  • 再启动Electron读取对应的url地址的文件内容,也实现热更新

设置webpack入口

        app: ['babel-polyfill', './src/index.js', './index.html'],
        vendor: ['react']
        }
    

忽略Electron中的代码,不用webpack打包(因为Electron中有后台模块代码,打包就会报错)


externals: [
        (function () {
            var IGNORES = [
                'electron'
            ];
            return function (context, request, callback) {
                if (IGNORES.indexOf(request) >= 0) {
                    return callback(null, "require('" + request + "')");
                }
                return callback();
            };
        })()
    ]
    

加入代码分割

optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all'
        }
    },

设置热更新等

 
    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html'
        }),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NamedModulesPlugin(),
    ],
    mode: 'development',
    devServer: {
        contentBase: '../build',
        open: true,
        port: 5000,
        hot: true
    },

#### 加入babel

{
    loader: 'babel-loader',
    options: {   //jsx语法
        presets: ["@babel/preset-react",
            //tree shaking 按需加载babel-polifill  presets从后到前执行 
            ["@babel/preset-env", {
                "modules": false,
                "useBuiltIns": "false", "corejs": 2,
            }],
        ],

    plugins: [
        //支持import 懒加载    plugin从前到后
        "@babel/plugin-syntax-dynamic-import",
        //andt-mobile按需加载  true是less,如果不用less style的值可以写'css' 
        ["import", { libraryName: "antd-mobile", style: true }],
        //识别class组件
        ["@babel/plugin-proposal-class-properties", { "loose": true }],
        //
    ],
    cacheDirectory: true
},
}

看看主进程的配置文件main.js

// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain, Tray, Menu } = require('electron')
const path = require('path')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
app.disableHardwareAcceleration()
// ipcMain.on('sync-message', (event, arg) => {
//   console.log("sync - message")
//   // event.returnValue('message', 'tanjinjie hello')
// })
function createWindow() {
  // Create the browser window.
  tray = new Tray(path.join(__dirname, './src/assets/bg.jpg'));
  tray.setToolTip('wechart');
  tray.on('click', () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
  });
  const contextMenu = Menu.buildFromTemplate([
    { label: '退出', click: () => mainWindow.quit() },
  ]);
  tray.setContextMenu(contextMenu);
  mainWindow = new BrowserWindow({
    width: 805,
    height: 500,
    webPreferences: {
      nodeIntegration: true
    },
    // titleBarStyle: 'hidden'
    frame: false
  })

  //自定义放大缩小托盘功能
  ipcMain.on('changeWindow', (event, arg) => {
    if (arg === 'min') {
      console.log('min')
      mainWindow.minimize()
    } else if (arg === 'max') {
      console.log('max')
      if (mainWindow.isMaximized()) {
        mainWindow.unmaximize()
      } else {
        mainWindow.maximize()
      }
    } else if (arg === "hide") {
      console.log('hide')
      mainWindow.hide()
    }
  })
  // and load the index.html of the app.
  // mainWindow.loadFile('index.html')
  mainWindow.loadURL('http://localhost:5000');
  BrowserWindow.addDevToolsExtension(
    path.join(__dirname, './src/extensions/react-dev-tool'),
  );


  // Open the DevTools.
  // mainWindow.webContents.openDevTools()

  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null
    BrowserWindow.removeDevToolsExtension(
      path.join(__dirname, './src/extensions/react-dev-tool'),
    );
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') app.quit()
})

app.on('activate', function () {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) createWindow()
})


// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.


今天只讲开发模式下的配置,因为实在太多,得分两篇文章写了~ 剩下的配置去git仓库看

在开发模式下启动项目:

  • 使用 "dev": "webpack-dev-server --config ./config/webpack.dev.js", 将代码打包到内存中
  • 使用 "start": "electron ." 开启electron,读取对应的内存地址中的资源,实现热更新

项目起来后,在入口处index.js文件中,注入dva

import React from 'react'
import App from './App'
import dva from 'dva'
import Homes from './model/Homes'
import main from './model/main'
const app = dva()
app.router(({ history, app: store }) => (
  <App
    history={history}
    getState={store._store.getState}
    dispatch={store._store.dispatch}
  />
));
app.model(Homes)
app.model(main)
app.start('#root')

这里不得不说redux,redux-sage,dva的区别 直接看图

首先是Redux

  • React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store,状态及页面逻辑从 <App/>里面抽取出来, 成为独立的 store,
  • 页面逻辑就是 reducer,<TodoList/> 及<AddTodoBtn/>都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatchstore 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新,使用 dispatchstore 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, eg: logging这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好

这个是Redux

然后是注入Redux-sage

  • 上面说了, 可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:
  • 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action
  • saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc action, 提示创建成功, 反之则发送 type == addTodoFailaction 即可

最后是: Dva

  • 有了前面的三步铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:
  • store saga 统一为一个 model 的概念, 写在一个 js 文件里面
  • 增加了一个 Subscriptions, 用于收集其他来源的 action, eg: 键盘操作
  • model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️
  • 约定优于配置, 总是好的😆

clipboard.png

clipboard.png

在入口APP组件中,注入props,实现状态树的管理

import React from 'react'
import { HashRouter, Route, Redirect, Switch } from 'dva/router';
import Home from './pages/home'
const Router = (props) => {
    return (
        <HashRouter>
            <Switch>
                <Route path="/home" component={Home}></Route>
                <Redirect to="/home"></Redirect>
            </Switch>
        </HashRouter>
    )
}
export default Router

在组件中connect连接状态树即可

import React from 'react'
import { ipcRenderer } from 'electron'
import { NavLink, Switch, Route, Redirect } from 'dva/router'
import Title from '../../components/title'
import Main from '../main'
import Friend from '../firend'
import More from '../more'
import { connect } from 'dva'
import './index.less'
class App extends React.Component {
    componentDidMount() {
        ipcRenderer.send('message', 'hello electron')
        ipcRenderer.on('message', (event, arg) => {
            console.log(arg, new Date(Date.now()))
        })
        const ws = new WebSocket('ws://localhost:8080');
        ws.onopen = function () {
            ws.send('123')
            console.log('open')
        }
        ws.onmessage = function () {
            console.log('onmessage')
        }
        ws.onerror = function () {
            console.log('onerror')
        }
        ws.onclose = function () {
            console.log('onclose')
        }
    }
    componentWillUnmount() {
        ipcRenderer.removeAllListeners()
    }
    render() {
        console.log(this.props)
        return (
            <div className="wrap">
                <div className="nav">
                    <NavLink to="/home/main">Home</NavLink>
                    <NavLink to="/home/firend">Friend</NavLink>
                    <NavLink to="/home/more">More</NavLink>
                </div>
                <div className="content">
                    <Title></Title>
                    <Switch>
                        <Route path="/home/main" component={Main}></Route>
                        <Route path="/home/firend" component={Friend}></Route>
                        <Route path="/home/more" component={More}></Route>
                        <Redirect to="/home/main"></Redirect>
                    </Switch>
                </div>
            </div>
        )
    }
}
export default connect(
    ({ main }) => ({
        test: main.main
    })
)(App)
// ipcRenderer.sendSync('sync-message','sync-message')

捋一捋上面的组件做了什么

  • 上来在组件挂载的生命周期函数中,启动了websocket连接,并且挂载了响应的事件监听,对主线程发送了消息,并且触发了主线程的message事件。
  • 在组件即将卸载的时候,移除了所有的跨进程通信的事件监听
  • 使用了dva进行路由跳转
  • 连接了状态树,读取了状态树main模块的main状态数据

进入上一个组件的子组件

import React from 'react'
import { connect } from 'dva'
class App extends React.Component {
    handleAdd = () => {
        this.props.dispatch({
            type: 'home/add',
            val: 5,
            res: 1
        })
    }
    handleDel = () => {
    }
    render() {
        const { homes } = this.props
        console.log(this.props)
        return (
            <div>
                <button onClick={this.handleAdd}>add</button>
                <button onClick={this.handleDel}>{homes}</button>
            </div>
        )
    }
}
export default connect(
    ({ home, main }) => ({
        homes: home.num,
        mains: main.main
    })
)(App)

同样看看,这个组件做了什么

  • 连接状态树,读取了 home,main模块的状态数据,并且转换成了props
  • 绑定了事件,如果点击按钮,dispatch给对应的effects,更新状态树的数据,进而更新页面

最后我们看下如何通过渲染进程控制主进程的窗口显示

import React from 'react'
import { ipcRenderer } from 'electron'
import './index.less'
export default class App extends React.Component {
    handle = (type) => {
        return () => {
            if (type === 'min') {
                console.log('min')
                ipcRenderer.send('changeWindow', 'min')
            } else if (type === 'max') {
                console.log('max')
                ipcRenderer.send('changeWindow', 'max')
            } else {
                console.log('hide')
                ipcRenderer.send('changeWindow', 'hide')
            }
        }
    }
    render() {
        return (
            <div className="title-container">
                <div className="title" style={{ "WebkitAppRegion": "drag" }}>可以拖拽的区域</div>
                <button onClick={this.handle('min')}>最小化</button>
                <button onClick={this.handle('max')}>最大化</button>
                <button onClick={this.handle('hide')}>托盘</button>
            </div>
        )
    }
}

  • 通过IPCRender与主进程通信,控制窗口的显示和隐藏

我们一起去dva中管理的model看看

  • home模块
export default {
    namespace: 'home',
    state: {
        homes: [1, 2, 3],
        num: 0
    },
    reducers: {
        adds(state, { newNum }) {
            return {
                ...state,
                num: newNum
            }
        }
    },
    effects: {
        * add({ res, val }, { put, select, call }) {
            const { home } = yield select()
            console.log(home.num)
            yield console.log(res, val)
            const newNum = home.num + 1
            yield put({ type: 'adds', newNum })
        }
    },
}

dva真的可以给我们省掉很多很多代码,而且更好维护,也更容易阅读

  • 它的大概流程

clipboard.png

  • 如果不会的话建议去官网看例子,一般来说不会像RXJS学习路线那么陡峭

Node.js中代码

const express = require('express')
const { Server } = require("ws");
const app = express()
const wsServer = new Server({ port: 8080 })
wsServer.on('connection', (ws) => {
    ws.onopen = function () {
        console.log('open')
    }
    ws.onmessage = function (data) {
        console.log(data)
        ws.send('234')
        console.log('onmessage' + data)
    }
    ws.onerror = function () {
        console.log('onerror')
    }
    ws.onclose = function () {
        console.log('onclose')
    }
});

app.listen(8000, (err) => {
    if (!err) { console.log('监听OK') } else {
        console.log('监听失败')
    }
})

上来先给一个websocket 8080端口监听,绑定事件,并且使用express监听原生端口8000

  • 这样好处,一个应用并不一定全部需要实时通讯,根据需求来决定什么时候进行实时通讯
  • Restful架构依然存在,Node.js作为中间件或者IO输出比较多的底层服务器进行CRUD都可以

今天先写到这里,套路都一样,基本的架子已经已经搭好,可以把代码clone下去慢慢玩,加功能。 可以的话给个star和赞,谢谢

本文git仓库源码地址,欢迎star

查看原文

阿平 收藏了文章 · 2019-06-03

当我们在谈论高并发的时候究竟在谈什么?

什么是高并发?

高并发是互联网分布式系统架构的性能指标之一,它通常是指单位时间内系统能够同时处理的请求数,
简单点说,就是QPS(Queries per second)。

那么我们在谈论高并发的时候,究竟在谈些什么东西呢?

高并发究竟是什么?

这里先给出结论:
高并发的基本表现为单位时间内系统能够同时处理的请求数,
高并发的核心是对CPU资源的有效压榨

举个例子,如果我们开发了一个叫做MD5穷举的应用,每个请求都会携带一个md5加密字符串,最终系统穷举出所有的结果,并返回原始字符串。这个时候我们的应用场景或者说应用业务是属于CPU密集型而不是IO密集型。这个时候CPU一直在做有效计算,甚至可以把CPU利用率跑满,这时我们谈论高并发并没有任何意义。(当然,我们可以通过加机器也就是加CPU来提高并发能力,这个是一个正常猿都知道废话方案,谈论加机器没有什么意义,没有任何高并发是加机器解决不了,如果有,那说明你加的机器还不够多!🐶)

对于大多数互联网应用来说,CPU不是也不应该是系统的瓶颈,系统的大部分时间的状况都是CPU在等I/O (硬盘/内存/网络) 的读/写操作完成。

这个时候就可能有人会说,我看系统监控的时候,内存和网络都很正常,但是CPU利用率却跑满了这是为什么?

这是一个好问题,后文我会给出实际的例子,再次强调上文说的 '有效压榨' 这4个字,这4个字会围绕本文的全部内容!

控制变量法

万事万物都是互相联系的,当我们在谈论高并发的时候,系统的每个环节应该都是需要与之相匹配的。我们先来回顾一下一个经典C/S的HTTP请求流程。

clipboard.png

如图中的序号所示:
1 我们会经过DNS服务器的解析,请求到达负载均衡集群
2 负载均衡服务器会根据配置的规则,想请求分摊到服务层。服务层也是我们的业务核心层,这里可能也会有一些PRC、MQ的一些调用等等
3 再经过缓存层
4 最后持久化数据
5 返回数据给客户端

要达到高并发,我们需要 负载均衡、服务层、缓存层、持久层 都是高可用、高性能的,甚至在第5步,我们也可以通过 压缩静态文件、HTTP2推送静态文件、CDN来做优化,这里的每一层我们都可以写几本书来谈优化。

本文主要讨论服务层这一块,即图红线圈出来的那部分。不再考虑讲述数据库、缓存相关的影响。
高中的知识告诉我们,这个叫 控制变量法

再谈并发

  • 网络编程模型的演变历史

clipboard.png

并发问题一直是服务端编程中的重点和难点问题,为了优系统的并发量,从最初的Fork进程开始,到进程池/线程池,再到epoll事件驱动(Nginx、node.js反人类回调),再到协程。
从上中可以很明显的看出,整个演变的过程,就是对CPU有效性能压榨的过程。
什么?不明显?

  • 那我们再谈谈上下文切换

在谈论上下文切换之前,我们再明确两个名词的概念。
并行:两个事件同一时刻完成。
并发:两个事件在同一时间段内交替发生,从宏观上看,两个事件都发生了

线程是操作系统调度的最小单位,进程是资源分配的最小单位。由于CPU是串行的,因此对于单核CPU来说,同一时刻一定是只有一个线程在占用CPU资源的。因此,Linux作为一个多任务(进程)系统,会频繁的发生进程/线程切换。

在每个任务运行前,CPU都需要知道从哪里加载,从哪里运行,这些信息保存在CPU寄存器和操作系统的程序计数器里面,这两样东西就叫做 CPU上下文
进程是由内核来管理和调度的,进程的切换只能发生在内核态,因此 虚拟内存、栈、全局变量等用户空间的资源,以及内核堆栈、寄存器等内核空间的状态,就叫做 进程上下文
前面说过,线程是操作系统调度的最小单位。同时线程会共享父进程的虚拟内存和全局变量等资源,因此 父进程的资源加上线上自己的私有数据就叫做线程的上下文

对于线程的上下文切换来说,如果是同一进程的线程,因为有资源共享,所以会比多进程间的切换消耗更少的资源。

现在就更容易解释了,进程和线程的切换,会产生CPU上下文切换和进程/线程上下文的切换。而这些上下文切换,都是会消耗额外的CPU的资源的。

  • 进一步谈谈协程的上下文切换

那么协程就不需要上下文切换了吗?需要,但是不会产生CPU上下文切换进程/线程上下文的切换,因为这些切换都是在同一个线程中,即用户态中的切换,你甚至可以简单的理解为协程上下文之间的切换,就是移动了一下你程序里面的指针,CPU资源依旧属于当前线程。
需要深刻理解的,可以再深入看看Go的GMP模型
最终的效果就是协程进一步压榨了CPU的有效利用率

回到开始的那个问题

这个时候就可能有人会说,我看系统监控的时候,内存和网络都很正常,但是CPU利用率却跑满了这是为什么?

注意本篇文章在谈到CPU利用率的时候,一定会加上有效两字作为定语,CPU利用率跑满,很多时候其实是做了很多低效的计算。
以"世界上最好的语言"为例,典型PHP-FPM的CGI模式,每一个HTTP请求:
都会读取框架的数百个php文件,
都会重新建立/释放一遍MYSQL/REIDS/MQ连接,
都会重新动态解释编译执行PHP文件,
都会在不同的php-fpm进程直接不停的切换切换再切换。

php的这种CGI运行模式,根本上就决定了它在高并发上的灾难性表现

找到问题,往往比解决问题更难。当我们理解了当我们在谈论高并发究竟在谈什么 之后,我们会发现高并发和高性能并不是编程语言限制了你,限制你的只是你的思想。

找到问题,解决问题!当我们能有效压榨CPU性能之后,能达到什么样的效果?

下面我们看看 php+swoole的HTTP服务 与 Java高性能的异步框架netty的HTTP服务之间的性能差异对比。

性能对比前的准备

Swoole是一个为PHP用C和C++编写的基于事件的高性能异步&协程并行网络通信引擎
Netty是由JBOSS提供的一个java开源框架。 Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
  • 单机能够达到的最大HTTP连接数是多少?

回忆一下计算机网络的相关知识,HTTP协议是应用层协议,在传输层,每个TCP连接建立之前都会进行三次握手。
每个TCP连接由 本地ip,本地端口,远端ip,远端端口,四个属性标识。
TCP协议报文头如下(图片来自维基百科):

clipboard.png

本地端口由16位组成,因此本地端口的最多数量为 2^16 = 65535个。
远端端口由16位组成,因此远端端口的最多数量为 2^16 = 65535个。
同时,在linux底层的网络编程模型中,每个TCP连接,操作系统都会维护一个File descriptor(fd)文件来与之对应,而fd的数量限制,可以由ulimit -n 命令查看和修改,测试之前我们可以执行命令: ulimit -n 65536修改这个限制为65535。

因此,在不考虑硬件资源限制的情况下,
本地的最大HTTP连接数为: 本地最大端口数65535 * 本地ip数1 = 65535 个。
远端的最大HTTP连接数为:远端最大端口数65535 * 远端(客户端)ip数+∞ = 无限制~~ 。
PS: 实际上操作系统会有一些保留端口占用,因此本地的连接数实际也是达不到理论值的。

性能对比

  • 测试资源

各一台docker容器,1G内存+2核CPU,如图所示:

clipboard.png

docker-compose编排如下:

# java8
version: "2.2"
services:
  java8:
    container_name: "java8"
    hostname: "java8"
    image: "java:8"
    volumes:
      - /home/cg/MyApp:/MyApp
    ports:
      - "5555:8080"
    environment:
      - TZ=Asia/Shanghai
    working_dir: /MyApp
    cpus: 2
    cpuset: 0,1

    mem_limit: 1024m
    memswap_limit: 1024m
    mem_reservation: 1024m
    tty: true
    
# php7-sw
version: "2.2"
services:
  php7-sw:
    container_name: "php7-sw"
    hostname: "php7-sw"
    image: "mileschou/swoole:7.1"
    volumes:
      - /home/cg/MyApp:/MyApp
    ports:
      - "5551:8080"
    environment:
      - TZ=Asia/Shanghai
    working_dir: /MyApp
    cpus: 2
    cpuset: 0,1

    mem_limit: 1024m
    memswap_limit: 1024m
    mem_reservation: 1024m
    tty: true    
  • php代码
<?php

use Swoole\Server;
use Swoole\Http\Response;

$http = new swoole_http_server("0.0.0.0", 8080);
$http->set([
    'worker_num' => 2
]);
$http->on("request", function ($request, Response $response) {
    //go(function () use ($response) {
        // Swoole\Coroutine::sleep(0.01);
        $response->end('Hello World');
    //});
});

$http->on("start", function (Server $server) {
    go(function () use ($server) {
        echo "server listen on 0.0.0.0:8080 \n";
    });
});
$http->start();
  • Java关键代码

源代码来自, https://github.com/netty/netty

    public static void main(String[] args) throws Exception {
        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(2);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.option(ChannelOption.SO_BACKLOG, 1024);
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new HttpHelloWorldServerInitializer(sslCtx));

            Channel ch = b.bind(PORT).sync().channel();

            System.err.println("Open your web browser and navigate to " +
                    (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');

            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

因为我只给了两个核心的CPU资源,所以两个服务均只开启连个work进程即可。
5551端口表示PHP服务。
5555端口表示Java服务。

  • 压测工具结果对比:ApacheBench (ab)

ab命令: docker run --rm jordi/ab -k -c 1000 -n 1000000 http://10.234.3.32:5555/
在并发1000进行100万次Http请求的基准测试中,

Java + netty 压测结果:

clipboard.png

clipboard.png

PHP + swoole 压测结果:

clipboard.png

clipboard.png

服务QPS响应时间ms(max,min)内存(MB)
Java + netty84042.11(11,25)600+
php + swoole87222.98(9,25)30+

ps: 上图选择的是三次压测下的最佳结果。

总的来说,性能差异并不大,PHP+swoole的服务甚至比Java+netty的服务还要稍微好一点,特别是在内存占用方面,java用了600MB,php只用了30MB。
这能说明什么呢?
没有IO阻塞操作,不会发生协程切换。
这个仅仅只能说明 多线程+epoll的模式下,有效的压榨CPU性能,你甚至用PHP都能写出高并发和高性能的服务。

性能对比——见证奇迹的时刻

上面代码其实并没有展现出协程的优秀性能,因为整个请求没有阻塞操作,但往往我们的应用会伴随着例如 文档读取、DB连接/查询 等各种阻塞操作,下面我们看看加上阻塞操作后,压测结果如何。
Java和PHP代码中,我都分别加上 sleep(0.01) //秒的代码,模拟0.01秒的系统调用阻塞。
代码就不再重复贴上来了。

带IO阻塞操作的 Java + netty 压测结果:

clipboard.png

大概10分钟才能跑完所有压测。。。

带IO阻塞操作的 PHP + swoole 压测结果:

clipboard.png

服务QPS响应时间ms(max,min)内存(MB)
Java + netty1562.69(52,160)100+
php + swoole9745.20(9,25)30+

从结果中可以看出,基于协程的php+ swoole服务比 Java + netty服务的QPS高了6倍。

当然,这两个测试代码都是官方demo中的源代码,肯定还有很多可以优化的配置,优化之后,结果肯定也会好很多。

可以再思考下,为什么官方默认线程/进程数量不设置的更多一点呢?
进程/线程数量可不是越多越好哦,前面我们已经讨论过了,在进程/线程切换的时候,会产生额外的CPU资源花销,特别是在用户态和内核态之间切换的时候!

对于这些压测结果来说,我并不是针对Java,我是指 只要明白了高并发的核心是什么,找到这个目标,无论用什么编程语言,只要针对CPU利用率做有效的优化(连接池、守护进程、多线程、协程、select轮询、epoll事件驱动),你也能搭建出一个高并发和高性能的系统。

所以,你现在明白了,当我们在谈论高性能的时候,究竟在谈什么了吗?

思路永远比结果重要!

本文欢迎转载,转载请注明作者和出处即可,谢谢!

查看原文

认证与成就

  • 获得 33 次点赞
  • 获得 15 枚徽章 获得 1 枚金徽章, 获得 3 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-04-07
个人主页被 944 人浏览