依韵_宵音

依韵_宵音 查看完整档案

苏州编辑中国矿业大学  |  电子商务 编辑江苏国泰新点软件有限公司  |  前端工程师 编辑 blog.cdswyda.com/ 编辑
编辑

个人动态

依韵_宵音 赞了回答 · 2020-09-28

解决如何在electron中集成npm或者说node

这个很常见的问题,普通方式开发的软件也会有环境依赖的问题,一般这种是通过NSIS这类封装工具对我们的原始安装包进行二次包装、压缩,这个过程中就可以手动加入nodejs到软件目录下,然后安装时把安装目录下的nodejs指向系统的环境变量,这样就可以全局执行node的各种操作了。重要是的NSIS这类工具还可以对安装包做进一步压缩,减少体积,这对Electron不招人待见的体积控制来说,是个福利啊。

最近也是做了一个纯本地的应用,后端没有打包,只打了前端,在启动软件时用node启动后端服务,关闭软件时结束node进程。

关注 6 回答 4

依韵_宵音 赞了文章 · 2020-09-02

60亿次for循环,原来这么多东西

起因

  • 有人在思否论坛上向我付费提问

image.png

  • 当时觉得,这个人问的有问题吧。仔细一看,还是有点东西的

问题重现

  • 编写一段Node.js代码
var http = require('http');
  
http.createServer(function (request, response) {
    var num = 0
    for (var i = 1; i < 5900000000; i++) {
        num += i
    }
    response.end('Hello' + num);
}).listen(8888);
  • 使用nodemon启动服务,用time curl调用这个接口

image.png

  • 首次需要7.xxs耗时
  • 多次调用后,问题重现

image.png

  • 为什么这个耗时突然变高,由于我是调用的是本机服务,我看CPU使用当时很高,差不多打到100%了.但是我后面发现不是这个问题.

问题排查

  • 排除掉CPU问题,看内存消耗占用。
var http = require('http');

http
  .createServer(function(request, response) {
    console.log(request.url, 'url');
    let used = process.memoryUsage().heapUsed / 1024 / 1024;

    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'start',
    );
    console.time('测试');
    let num = 0;
    for (let i = 1; i < 5900000000; i++) {
      num += i;
    }
    console.timeEnd('测试');
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'end',
    );
    response.end('Hello' + num);
![](https://imgkr2.cn-bj.ufileos.com/13455121-9d87-42c3-a32e-ea999a2cd09b.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=E3cF2kymC92LifrIC5IOfIZQvnk%253D&Expires=1598883364)

![](https://imgkr2.cn-bj.ufileos.com/1e7b95df-2a48-41c3-827c-3c24b39f4b5b.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=%252FANTTuhgbpIsXslXMc1qCkj2TMU%253D&Expires=1598883362)

  })
  .listen(8888);
  • 测试结果:

image.png

  • 内存占用和CPU都正常
  • 跟字符串拼接有关,此刻关闭字符串拼接(此时为了快速测试,我把循环次数降到5.9亿次
  • 发现耗时稳定下来了

定位问题在字符串拼接,先看看字符串拼接的几种方式

  • 一、使用连接符 “+” 把要连接的字符串连起来
var a = 'java'
var b = a + 'script'

  * 只连接100个以下的字符串建议用这种方法最方便

  • 二、使用数组的 join 方法连接字符串
var arr = ['hello','java','script']
var str = arr.join("")
  • 比第一种消耗更少的资源,速度也更快
  • 三、使用模板字符串,以反引号( ` )标识
var a = 'java'
var b = `hello ${a}script`
  • 四、使用 JavaScript concat() 方法连接字符串
var a = 'java'
var b = 'script'

var str = a.concat(b)

五、使用对象属性来连接字符串

function StringConnect(){
    this.arr = new Array()
}

StringConnect.prototype.append = function(str) {
    this.arr.push(str)
}

StringConnect.prototype.toString = function() {
    return this.arr.join("")
}

var mystr = new StringConnect()

mystr.append("abc")
mystr.append("def")
mystr.append("g")

var str = mystr.toString()

更换字符串的拼接方式

  • 我把字符串拼接换成了数组的join方式(此时循环5.9亿次)
var http = require('http');

http
  .createServer(function(request, response) {
    console.log(request.url, 'url');
    let used = process.memoryUsage().heapUsed / 1024 / 1024;

    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'start',
    );
    console.time('测试');
    let num = 0;
    for (let i = 1; i < 590000000; i++) {
      num += i;
    }
    const arr = ['Hello'];
    arr.push(num);
    console.timeEnd('测试');
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'end',
    );
    response.end(arr.join(''));
  })
  .listen(8888);
  • 测试结果,发现接口调用的耗时稳定了(注意此时是5.9亿次循环)

image.png

  • 《javascript高级程序设计》中,有一段关于字符串特点的描述,原文大概如下:ECMAScript中的字符串是不可变的,也就是说,字符串一旦创建,他们的值就不能改变。要改变某个变量的保存的的字符串,首先要销毁原来的字符串,然后再用另外一个包含新值的字符串填充该变量

就完了?

  • +直接拼接字符串自然会对性能产生一些影响,因为字符串是不可变的,在操作的时候会产生临时字符串副本,+操作符需要消耗时间,重新赋值分配内存需要消耗时间。
  • 但是,我更换了代码后,发现,即使没有字符串拼接,也会耗时不稳定
var http = require('http');

http
  .createServer(function(request, response) {
    console.log(request.url, 'url');
    let used = process.memoryUsage().heapUsed / 1024 / 1024;

    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'start',
    );
    console.time('测试');
    let num = 0;
    for (let i = 1; i < 5900000000; i++) {
    //   num++;
    }
    const arr = ['Hello'];
    // arr[1] = num;
    console.timeEnd('测试');
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'end',
    );
    response.end('hello');
  })
  .listen(8888);
  • 测试结果:
  • 现在我怀疑,不仅仅是字符串拼接的效率问题,更重要的是for循环的耗时不一致
var http = require('http');

http
  .createServer(function(request, response) {
    console.log(request.url, 'url');
    let used = process.memoryUsage().heapUsed / 1024 / 1024;

    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'start',
    );
    let num = 0;
    console.time('测试');
    for (let i = 1; i < 5900000000; i++) {
    //   num++;
    }
    console.timeEnd('测试');
    const arr = ['Hello'];
    // arr[1] = num;
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'end',
    );
    response.end('hello');
  })
  .listen(8888);
  • 测试运行结果:

image.png

  • for循环内部的i++其实就是变量不断的重新赋值覆盖
  • 经过我的测试发现,40亿次50亿次的区别,差距很大,40亿次的for循环,都是稳定的,但是50亿次就不稳定了.
  • Node.jsEventLoop:
  • 我们目前被阻塞的状态:
  • 我电脑的CPU使用情况

优化方案

  • 遇到了60亿次的循环,像有使用多进程异步计算的,但是本质上没有解决这部分循环代码的调用耗时。
  • 改变策略,拆解单次次数过大的for循环:
var http = require('http');

http
  .createServer(function(request, response) {
    console.log(request.url, 'url');
    let used = process.memoryUsage().heapUsed / 1024 / 1024;

    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'start',
    );
    let num = 0;
    console.time('测试');
    for (let i = 1; i < 600000; i++) {
      num++;
      for (let j = 0; j < 10000; j++) {
        num++;
      }
    }
    console.timeEnd('测试');
    const arr = ['Hello'];
    console.log(num, 'num');
    arr[1] = num;
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'end',
    );
    response.end(arr.join(''));
  })
  .listen(8888);
  • 结果,耗时基本稳定,60亿次循环总共:

推翻字符串的拼接耗时说法

  • 修改代码回最原始的+方式拼接字符串
var http = require('http');

http
  .createServer(function(request, response) {
    console.log(request.url, 'url');
    let used = process.memoryUsage().heapUsed / 1024 / 1024;

    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'start',
    );
    let num = 0;
    console.time('测试');
    for (let i = 1; i < 600000; i++) {
      num++;
      for (let j = 0; j < 10000; j++) {
        num++;
      }
    }
    console.timeEnd('测试');
    // const arr = ['Hello'];
    console.log(num, 'num');
    // arr[1] = num;
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`,
      'end',
    );
    response.end(`Hello` + num);
  })
  .listen(8888);
  • 测试结果稳定,符合预期:

image.png

总结:

  • 对于单次循环超过一定阀值次数的,用拆解方式,Node.js的运行耗时是稳定,但是如果是循环次数过多,那么就会出现刚才那种情况,阻塞严重,耗时不一样。
  • 为什么?

深度分析问题

  • 遍历60亿次,这个数字是有一些大了,如果是40亿次,是稳定的
  • 这里应该还是跟CPU有一些关系,因为top查看一直是在升高
  • 此处虽然不是真正意义上的内存泄漏,但是我们如果在一个循环中不仅要不断更新i的值到60亿,还要不断更新num的值60亿,内存使用会不断上升,最终出现两份60亿的数据,然后再回收。(因为GC自动垃圾回收,一样会阻塞主线程,多次接口调用后,CPU占用也会升高)
  • 使用for循环拆解后:
 for (let i = 1; i < 60000; i++) {
      num++;
      for (let j = 0; j < 100000; j++) {
        num++;
      }
    }
  • 只要num60亿即可,解决了这个问题。

哪些场景会遇到这个类似的超大计算量问题:

  • 图片处理
  • 加解密
如果是异步的业务场景,也可以用多进程参与解决超大计算量问题,今天这里就不重复介绍了

最后

  • 如果感觉写得不错,可以点个在看/,转发一下,让更多人看到
  • 我是Peter谭老师,欢迎你关注公众号:前端巅峰,后台回复:加群即可加入大前端交流群
查看原文

赞 39 收藏 20 评论 16

依韵_宵音 赞了文章 · 2020-03-17

9个项目助你在2020年成为前端大师!

原文链接:https://dev.to/simonholdorf/9...

DEV的年度热文,读完觉得不错,所以翻译出来供大家参考,个人水平有限,文中可能会有一些翻译错误,可以在评论区指正。

本篇文章一共涉及了9个流行的框架/库,没有具体的介绍使用方法,而是给了一些非常棒的实战教程。

初学者(也许一些有经验的开发者也是一样)在读完官方文档,想写一个项目练手的时候不知道做什么项目好,或是有想法,但是无从下手。那么这篇文章将会给你带来很大的帮助。

更多文章可戳:https://github.com/YvetteLau/...

导读

无论你是编程新手还是经验丰富的开发人员。在这个行业中,我们不得不一直学习新概念和新语言或是框架,才能跟上快速变化。以React为例 —— FaceBook 四年前开源,现在它已经成为了全球JS开发者的首选。但是与此同时,Vue 和 Angular 也有自己的追求者。然后是 Svelte,Next 和 Nuxt.js,Gatsby,Gridsome,quasar 等等,如果你想成为专业的 JavaScript 开发人员,你在使用自己熟悉的框架进行开发的同时,还需要对不同的框架和库有一些了解。

为了帮助你在2020年成为一个前端大神,我收集了9个使用了不同JS框架/库的项目,你可以去构建或者将他们加入到自己未来的开发计划中。记住,没什么比实际开发一个项目更有帮助。所以,不要犹豫,试着去开发一下。

1. 使用React(with hooks)构建一个电影搜索应用

首先,你可以使用React构建一个电影搜索应用。展示如下:

k1.jpeg

你将学到什么?

构建这个项目,你可以使用较新的 Hook API 来提升你的 React 技能。示例项目使用了React组件,很多 hooks 以及一些外部的 API,当然还有一些CSS样式。

技术栈/点

  1. React(Hooks)
  2. create-react-app
  3. JSX
  4. CSS

你可以在这里看到这个示例项目:https://www.freecodecamp.org/...

2.使用Vue构建一个聊天应用

另外一个要介绍给你的很棒的项目是使用Vue构建的聊天应用程序。展示如下:

👀.png

你将学到什么?

您将学习到如何从头开始设置Vue应用,创建组件,处理状态,创建路由,连接到第三方服务,甚至是处理身份验证。

技术栈/点

  1. Vue
  2. Vuex
  3. Vue Router
  4. Vue CLI
  5. Pusher
  6. CSS

这真的是一个非常棒的项目,不管是用来学习Vue或者是提升现有的技能,以应对2020年的发展。你可以查看这个教程: https://www.sitepoint.com/pus...

3. 使用Augular8构建一款漂亮的天气应用

此示例将帮助你使用 Google 的 Angular 8 来构建一块漂亮的天气应用程序:

k3.png

你将学到什么?

该项目将教你一些宝贵的技能,例如从头开始创建应用,从设计到开发,一直到生产就绪部署。

技术栈/点

  1. Angular 8
  2. Firebase
  3. SSR
  4. 网络布局和Flexbox
  5. 移动端友好 && 响应式布局
  6. 深色模式
  7. 漂亮的用户界面

对于这个综合项目,我真正喜欢的是,不是孤立地学习东西,而是从设计到最终部署的整个开发过程。

https://medium.com/@hamedbaat...

4. 使用 Svelte 构建一个 To-Do 应用

与React,Vue和Angular相比,Svelte 还很新,但仍是热门之一。好的,To-Do应用不一定是那里最热门的项目,但这确实可以帮助你提高Svelte技能,如下:

k4.png

你将学到什么?

本教程将向你展示如何从头到尾使用Svelte3制作应用。 它利用了组件,样式和事件处理程序。

技术栈/点

  1. Svelte 3
  2. Components
  3. CSS
  4. ES6语法

Svelte 没有太多优秀的入门项目,这个是我觉得不错的一个上手项目:https://medium.com/codingthes...

5. 使用 Next.js 构建购物车

Next.js 是一个轻量级的 React 服务端渲染应用框架,该项目将向你展示如何构建一个如下所示的购物车:

k5.jpeg

你将学到什么?

在这个项目中,你将学习如何设置 Next.js 的开发环境,创建新页面和组件,获取数据,设置样式并部署一个 next 应用。

技术栈/点

  1. Next.js
  2. 组件和页面
  3. 数据获取
  4. 样式
  5. 部署
  6. SSR和SPA

你可以在此处找到该教程:https://snipcart.com/blog/nex...

6. 使用 Nuxt.js 构建一个多语言博客网站

Nuxt.js 是 Vue 服务端渲染应用框架。你可以创建一个如下所示的应用程序:

K6.jpg

你将学到什么?

这个示例项目从初始设置到最终部署一步一步教你如何使用 Nuxt.js 构建一个完整的网站。它使用了 Nuxt 提供的许多出色功能,如页面和组件以及SCSS样式。

技术栈/点

  • Nuxt.js
  • 组件和页面
  • Storyblok模块
  • Mixins
  • Vuex
  • SCSS
  • Nuxt中间件

这个项目包含了涵盖了 Nuxt.js 的许多出色功能。我个人很喜欢使用 Nuxt 进行开发,你应该尝试使用它,这将使你成为更好的 Vue 开发人员!https://www.storyblok.com/tp/...

除此之外,我还找到了一个B站的视频:https://www.bilibili.com/vide...

7. 使用 Gatsby 构建一个博客

Gatsby是一个出色的静态站点生成器,它允许使用React作为渲染引擎引擎来搭建一个静态站点,它真正具有现代web应用程序所期望的所有优点。该项目如下:

k7.png

你将学到什么?

在本教程中,你将学习如何利用 Gatsby 构建出色的博客。

技术栈/点

  1. Gatsby
  2. React
  3. GraphQL
  4. Plugins & Themes
  5. MDX / Markdown
  6. Bootstrap CSS
  7. Templates

如果你想创建博客,这个示例教你如何利用 React 和 GraphQL 来搭建。并不是说 Wordpress 是一个不好的选择,但是有了 Gatsby ,你可以在使用 React 的同时创建高性能站点!

https://blog.bitsrc.io/how-to...

8. 使用 Gridsome 构建一个博客

Gridsome 和 Vue的关系与 Gatsby 和 React 的关系一样。Gridsome 和 Gatsby 都使用 GraphQL 作为数据层,但是 Gridsome 使用的是 VueJS。这也是一个很棒的静态站点生成器,它将帮助您创建出色的博客:

k8.png

你将学到什么?

该项目将教你如何使用 Gridsome,GraphQL 和 Markdown 构建一个简单的博客,它还介绍了如何通过Netlify 部署应用程序。

技术栈/点

  1. Gridsome
  2. Vue
  3. GraphQL
  4. Markdown
  5. Netlify

当然,这不是最全面的教程,但涵盖了 Gridsome 和 Markdown 的基本概念,可能是一个很好的起点。

https://www.telerik.com/blogs...

9.使用 Quasar 构建一个类似 SoundCloud 的音频播放器

Quasar 是另一个 Vue 框架,也可以用于构建移动应用程序。 在这个项目中,你将创建一个音频播放器应用程序,如下所示:

k9.jpeg

你将学到什么?

不少项目主要关注Web应用程序,但这个项目展示了如何通过 Quasar 框架创建移动应用程序。你应该已经配置了可工作的 Cordova 设置,并配置了 android studio / xcode。 如果没有,在教程中有一个指向quasar 网站的链接,在那里你可以学习如何进行设置。

技术栈/点

  • Quasar
  • Vue
  • Cordova
  • Wavesurfer
  • UI Components

一个展示了Quasar在构建移动应用程序方面的强大功能的小项目:https://www.learningsomething...

总结

本文展示了你可以构建的9个项目,每个项目专注于一个JavaScript框架或库。现在,你可以自行决定:使用以前未使用的框架来尝试一些新的东西或是通过做一个项目来提升已有的技能,或者在2020年完成所有项目?

定稿.png

查看原文

赞 337 收藏 257 评论 20

依韵_宵音 赞了文章 · 2020-02-18

前端20个真正灵魂拷问,吃透这些你就是中级前端工程师 【上篇】

clipboard.png

网上参差不弃的面试题,本文由浅入深,让你在做面试官的时候,能够辨别出面试者是不是真的有点东西,也能让你去面试中级前端工程师更有底气。但是切记把背诵面试题当成了你的唯一求职方向

另外欢迎大家加入我们的前端交流二群~,里面很多小姐姐哦,下篇将是非常硬核的源码,原理,自己编写框架和库等,如果感觉写得不错,可以关注给个star

clipboard.png

越是开放性的题目,更能体现回答者的水平,一场好的面试,不仅能发现面试者的不足,也能找到他的闪光点,还能提升面试官自身的技术

1.CssHtml合并在第一个题目,请简述你让一个元素在窗口中消失以及垂直水平居中的方法,还有Flex布局的理解

标准答案:百度上当然很多,这里不做阐述,好的回答思路是:

  • 元素消失的方案先列出来, display:nonevisibility: hidden;的区别,拓展到vue框架的v-ifv-show的区别,可以搭配回流和重绘来讲解

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

回流(Reflow):

Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流
  • 下面内容会导致回流:

    • 页面首次渲染
    • 浏览器窗口大小发生改变
    • 元素尺寸或位置发生改变
    • 元素内容变化(文字数量或图片大小等等)
    • 元素字体大小变化
    • 添加或者删除可见的DOM元素
    • 激活CSS伪类(例如::hover)
    • 查询某些属性或调用某些方法
  • 一些常用且会导致回流的属性和方法:

    • clientWidth、clientHeight、clientTop、clientLeft
    • offsetWidth、offsetHeight、offsetTop、offsetLeft
    • scrollWidth、scrollHeight、scrollTop、scrollLeft
    • scrollIntoView()、scrollIntoViewIfNeeded()
    • getComputedStyle()
    • getBoundingClientRect()
    • scrollTo()

重绘

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

性能影响对比:

clipboard.png

原文出处,感谢作者

  • 列出元素垂直居中的方案,以及各种方案的缺陷

16种居中方案,感谢作者

  • 讲出flex常用的场景,以及flex 1做了什么

阮一峰老师的Flex布局

上面的问题如果答得非常好,在重绘和回流这块要下大功夫。这点是前端性能优化的基础,而性能优化是前端最重要的核心基础技能点,也是面试官最看中的基础之一

2.你对This了解吗,有自己实现过call,apply,bind吗?

50行javaScript代码实现call,apply,bind

这是一个很基础的技能点,考察你对闭包,函数调用的理解程度,我感觉我写得比较简单容易懂

3.如何减少重绘和回流的次数:

clipboard.png

4.你对前端的异步编程有哪些了解呢

这个题目如果回答非常完美,那么可以判断这个人已经脱离了初级前端工程师,前端的核心就是异步编程,这个题目也是体现前端工程师基础是否扎实的最重要依据。

还是老规矩,从易到难吧

传统的定时器,异步编程:

setTimeout(),setInterval()等。

缺点:当同步的代码比较多的时候,不确定异步定时器的任务时候能在指定的时间执行。

例如:

在第100行执行代码 setTimeout(()=>{console.log(1)},1000)//1s后执行里面函数

但是后面可能有10000行代码+很多计算的任务,例如循环遍历,那么1s后就无法输出console.log(1)

可能要到2s甚至更久

setInterval跟上面同理 当同步代码比较多时,不确保每次能在一样的间隔执行代码,

如果是动画,那么可能会掉帧

ES6的异步编程:

promise generator async
 new promise((resolve,reject)=>{ resolve() }).then()....
 缺点: 仍然没有摆脱回掉函数,虽然改善了回掉地狱
 
 generator函数 调用next()执行到下一个yeild的代码内容,如果传入参数则作为上一个

`yield`的
返回值
 缺点:不够自动化

 async await 
 只有async函数内部可以用await,将异步代码变成同步书写,但是由于async函数本身返回一个
promise,也很容易产生async嵌套地狱

requestAnimationFramerequestIdleCallback

传统的javascript 动画是通过定时器 setTimeout 或者 setInterval 实现的。但是定时器动画一直存在两个问题

第一个就是动画的循时间环间隔不好确定,设置长了动画显得不够平滑流畅,设置短了浏览器的重绘频率会达到瓶颈,推荐的最佳循环间隔是17ms(大多数电脑的显示器刷新频率是60Hz,1000ms/60);

第二个问题是定时器第二个时间参数只是指定了多久后将动画任务添加到浏览器的UI线程队列中,如果UI线程处于忙碌状态,那么动画不会立刻执行。为了解决这些问题,H5 中加入了 requestAnimationFrame以及requestIdleCallback

requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率

在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量

requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销

性能对比:

clipboard.png

requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。

我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了,那么在一帧里面浏览器都要做哪些事情呢,如下所示:

clipboard.png

图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。

假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调,如下图所示:

clipboard.png

5.简述浏览器的EventloopNode.jsEventloop

浏览器的EventLoop

clipboard.png

不想解释太多,看图

Node.jsEventLoop

clipboard.png

特别提示:网上大部分Node.jsEventLoop的面试题,都会有BUG,代码量和计算量太少,很可能还没有执行到微任务的代码,定时器就到时间被执行了

6.闭包与V8垃圾回收机制:

JS 的垃圾回收机制的基本原理是:

找出那些不再继续使用的变量,然后释放其占用的内存,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。

V8 的垃圾回收策略主要基于分代式垃圾回收机制,在 V8 中,将内存分为新生代和老生代,新生代的对象为存活时间较短的对象,老生代的对象为存活事件较长或常驻内存的对象。

clipboard.png

V8 堆的整体大小等于新生代所用内存空间加上老生代的内存空间,而只能在启动时指定,意味着运行时无法自动扩充,如果超过了极限值,就会引起进程出错。

Scavenge 算法

在分代的基础上,新生代的对象主要通过 Scavenge 算法进行垃圾回收,在 Scavenge 具体实现中,主要采用了一种复制的方式的方法—— Cheney 算法。

Cheney 算法将堆内存一分为二,一个处于使用状态的空间叫 From 空间,一个处于闲置状态的空间称为 To 空间。分配对象时,先是在 From 空间中进行分配。

当开始进行垃圾回收时,会检查 From 空间中的存活对象,将其复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空间和 To 空间的角色发生对换。

clipboard.png
当一个对象经过多次复制后依然存活,他将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用新的算法进行管理。

还有一种情况是,如果复制一个对象到 To 空间时,To 空间占用超过了 25%,则这个对象会被直接晋升到老生代空间中。

标记-清除和标记-整理算法

对于老生代中的对象,主要采用标记-清除和标记-整理算法。标记-清除 和前文提到的标记一样,与 Scavenge 算法相比,标记清除不会将内存空间划为两半,标记清除在标记阶段会标记活着的对象,而在内存回收阶段,它会清除没有被标记的对象。

而标记整理是为了解决标记清除后留下的内存碎片问题。

增量标记(Incremental Marking)算法

前面的三种算法,都需要将正在执行的 JavaScript 应用逻辑暂停下来,待垃圾回收完毕后再恢复。这种行为叫作“全停顿”(stop-the-world)。

在 V8 新生代的分代回收中,只收集新生代,而新生代通常配置较小,且存活对象较少,所以全停顿的影响不大,而老生代就相反了。

为了降低全部老生代全堆垃圾回收带来的停顿时间,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直到标记阶段完成。

clipboard.png

经过增量标记改进后,垃圾回收的最大停顿时间可以减少到原来的 1/6 左右。

内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄漏的常见场景:

  • 缓存:存在内存中数据一只没有被清掉
  • 作用域未释放(闭包)
  • 无效的 DOM 引用
  • 没必要的全局变量
  • 定时器未清除(React中的合成事件,还有原生事件的绑定区别)
  • 事件监听为清空
  • 内存泄漏优化

7.你熟悉哪些通信协议,它们的优缺点?

通信协议全解

我的这篇文章非常详细介绍了 http1.0 http1.1 http2.0 https websocket等协议

8.从输入url地址栏,发生了什么?由此来介绍如何性能优化:

性能优化不完全手册

如何优化你的超大型React应用

我的这两篇文章基本上涵盖了前端基础的性能优化,后期我会再出专栏。

9.浏览器的缓存实现,请您介绍:

1.preload,prefetch,dns-prefetch

什么是preload

使用 preload 指令的好处包括:

允许浏览器来设定资源加载的优先级因此可以允许前端开发者来优化指定资源的加载。

赋予浏览器决定资源类型的能力,因此它能分辨这个资源在以后是否可以重复利用。

浏览器可以通过指定 as 属性来决定这个请求是否符合 content security policy。

浏览器可以基于资源的类型(比如 image/webp)来发送适当的 accept 头。

Prefetch

Prefetch 是一个低优先级的资源提示,允许浏览器在后台(空闲时)获取将来可能用得到的资源,并且将他们存储在浏览器的缓存中。一旦一个页面加载完毕就会开始下载其他的资源,然后当用户点击了一个带有 prefetched 的连接,它将可以立刻从缓存中加载内容。

DNS Prefetching

DNS prefetching 允许浏览器在用户浏览页面时在后台运行 DNS 的解析。如此一来,DNS 的解析在用户点击一个链接时已经完成,所以可以减少延迟。可以在一个 link 标签的属性中添加 rel="dns-prefetch' 来对指定的 URL 进行 DNS prefetching,我们建议对 Google fonts,Google Analytics CDN 进行处理。

2.servece-worker,PWA渐进式web应用

PWA文档

clipboard.png

3.localstorage,sessionstorage,cookie,session等。
浏览器的会话存储和持久性存储
4.浏览器缓存的实现机制的实现

clipboard.png

10.同源策略是什么,跨域解决办法,cookie可以跨域吗?

跨域解决的办法

Q:为什么会出现跨域问题?

A:出于浏览器的同源策略限制,浏览器会拒绝跨域请求。

  • 注:严格的说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。浏览器的同源限制策略是这样执行的:

通常浏览器允许进行跨域写操作(Cross-origin writes),如链接,重定向;

通常浏览器允许跨域资源嵌入(Cross-origin embedding),如 img、script 标签;

通常浏览器不允许跨域读操作(Cross-origin reads)。

Q:什么情况才算作跨域?

A:非同源请求,均为跨域。名词解释:同源 —— 如果两个页面拥有相同的协议(protocol),端口(port)和主机(host),那么这两个页面就属于同一个源(origin)。

Q:为什么有跨域需求?

A:场景 —— 工程服务化后,不同职责的服务分散在不同的工程中,往往这些工程的域名是不同的,但一个需求可能需要对应到多个服务,这时便需要调用不同服务的接口,因此会出现跨域。

方法:JSONP,CORS,postmessage,webscoket,反向代理服务器等。

上篇已经结束,欢迎你关注,等待下篇非常硬核的文章出炉~

期待你加入我们哦~

现在一群满了,所以新开了一个二群

clipboard.png

查看原文

赞 301 收藏 249 评论 8

依韵_宵音 关注了用户 · 2020-02-18

Peter谭老师 @jerrytanjinjie

前端架构师

微信公众号:前端巅峰

欢迎技术探讨~

个人微信:CALASFxiaotan

关注 4297

依韵_宵音 赞了文章 · 2020-02-18

原创精读:带你从零看清Node源码createServer和负载均衡整个过程

写在开头:

作为一名曾经重度使用Node.js作为即时通讯客户端接入层的开发人员,无法避免调试V8,配合开发addon。于是对Node.js源码产生了很大的兴趣~ 

顺便吐槽一句,Node的内存控制,由于是自动回收,我之前做的产品是20万人超级群的IM产品,像一秒钟1000条消息,持续时间长了内存和CPU占用还是会有一些问题

之前写过cluster模块源码分析、PM2原理等,感觉兴趣的可以去公众号翻一翻

Node.js的js部分源码基本看得差不多了,今天写一个createServer过程给大家,对于不怎么熟悉Node.js源码的朋友来说,可能是一个不错的开始,源码在gitHub上有,直接克隆即可,最近一直比较忙,公司和业余的工作也是,所以原创比较少。


原生Node.js创建一个基本的服务:


var http = require('http');
http.createServer(function (request, response) {
// 发送 HTTP 头部 
// HTTP 状态值: 200 : OK
// 内容类型: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应数据 "Hello World"
response.end('Hello World\n');
}).listen(8888);
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');

我们目前只分析Node.js源码的js部分的

首先找到Node.js源码的lib文件夹

然后找到http.js文件

发现createServer真正返回的是new Server,而Server来自_http_server

于是找到同目录下的_http_server.js文件,发现整个文件有800行的样子,全局搜索Server找到函数


function Server(options, requestListener) {
if (!(this instanceof Server)) return new Server(options, requestListener);
if (typeof options === 'function') {
    requestListener = options;
    options = {};
  } else if (options == null || typeof options === 'object') {
    options = { ...options };
  } else {
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
  }
this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
this[kServerResponse] = options.ServerResponse || ServerResponse;
  net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.on('request', requestListener);
  }

createServer函数解析:

  • 参数控制有点像redux源码里的initState和reducer,根据传入类型不同,做响应的处理

    this.on('request', requestListener);}
  •  每次有请求,就会调用requestListener这个回调函数
  • 至于IncomingMessage和ServerResponse,请求是流,响应也是流,请求是可读流,响应是可写流,当时写那个静态资源服务器时候有提到过
  • 那么怎么可以链式调用?有人可能会有疑惑。Node.js源码遵循commonJs规范,大都挂载在prototype上,所以函数开头有,就是确保可以链式调用
   if (!(this instanceof Server)) 
    return new Server(options, requestListener);

上面已经将onrequest事件触发回调函数讲清楚了,那么链式调用listen方法,监听端口是怎么回事呢?

传统的链式调用,像JQ源码是return this, 手动实现A+规范的Promise则是返回一个全新的Promise,然后Promise原型上有then方法,于是可以链式调用


怎么实现.listen链式调用,重点在这行代码:

 net.Server.call(this, { allowHalfOpen: true });

 allowHalfOpen实验结论: 这里TCP的知识不再做过度的讲解


(1)allowHalfOpen为true,一端发送FIN报文:
进程结束了,那么肯定会发送FIN报文;
进程未结束,不会发送FIN报文
(2)allowHalfOpen为false,一端发送FIN报文:
进程结束了,肯定发送FIN报文;
进程未结束,也会发送FIN报文;

于是找到net.js文件模块中的Server函数

function Server(options, connectionListener) {
  if (!(this instanceof Server))
    return new Server(options, connectionListener);

  EventEmitter.call(this);

  if (typeof options === 'function') {
    connectionListener = options;
    options = {};
    this.on('connection', connectionListener);
  } else if (options == null || typeof options === 'object') {
    options = { ...options };

    if (typeof connectionListener === 'function') {
      this.on('connection', connectionListener);
    }
  } else {
    throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
  }

  this._connections = 0;

  Object.defineProperty(this, 'connections', {
    get: deprecate(() => {

      if (this._usingWorkers) {
        return null;
      }
      return this._connections;
    }, 'Server.connections property is deprecated. ' +
       'Use Server.getConnections method instead.', 'DEP0020'),
    set: deprecate((val) => (this._connections = val),
                   'Server.connections property is deprecated.',
                   'DEP0020'),
    configurable: true, enumerable: false
  });

  this[async_id_symbol] = -1;
  this._handle = null;
  this._usingWorkers = false;
  this._workers = [];
  this._unref = false;

  this.allowHalfOpen = options.allowHalfOpen || false;
  this.pauseOnConnect = !!options.pauseOnConnect;
}

这里巧妙的通过.call调用net模块Server函数,保证了this指向一致

this.\_handle = null 这里是因为Node.js考虑到多进程问题,所以会hack掉这个属性,因为.listen方法最终会调用_handle中的方法,多个进程只会启动一个真正进程监听端口,然后负责分发给不同进程,这个后面会讲

Node.js源码的几个特色:

  1. 遵循conmonjs规范,很多方法挂载到prototype上了
  2. 很多object.definepropoty数据劫持
  3. this指向的修改,配合第一个进行链式调用
  4. 自带自定义事件模块,很多内置的函数都继承或通过Object.setPrototypeOf去封装了一些自定义事件
  5. 代码模块互相依赖比较多,一个.listen过程就很麻烦,初学代码者很容易睡着
  6. 源码学习,本就枯燥。没什么好说的了
    • *

我在net.js文件模块中发现了一个原型上.listen的方法:

Server.prototype.listen = function(...args) {
  const normalized = normalizeArgs(args);
  var options = normalized[0];
  const cb = normalized[1];

  if (this._handle) {
    throw new ERR_SERVER_ALREADY_LISTEN();
  }

  if (cb !== null) {
    this.once('listening', cb);
  }
  const backlogFromArgs =
    // (handle, backlog) or (path, backlog) or (port, backlog)
    toNumber(args.length > 1 && args[1]) ||
    toNumber(args.length > 2 && args[2]);  // (port, host, backlog)

  options = options._handle || options.handle || options;
  const flags = getFlags(options.ipv6Only);
  // (handle[, backlog][, cb]) where handle is an object with a handle
  if (options instanceof TCP) {
    this._handle = options;
    this[async_id_symbol] = this._handle.getAsyncId();
    listenInCluster(this, null, -1, -1, backlogFromArgs);
    return this;
  }
  // (handle[, backlog][, cb]) where handle is an object with a fd
  if (typeof options.fd === 'number' && options.fd >= 0) {
    listenInCluster(this, null, null, null, backlogFromArgs, options.fd);
    return this;
  }

  // ([port][, host][, backlog][, cb]) where port is omitted,
  // that is, listen(), listen(null), listen(cb), or listen(null, cb)
  // or (options[, cb]) where options.port is explicitly set as undefined or
  // null, bind to an arbitrary unused port
  if (args.length === 0 || typeof args[0] === 'function' ||
      (typeof options.port === 'undefined' && 'port' in options) ||
      options.port === null) {
    options.port = 0;
  }
  // ([port][, host][, backlog][, cb]) where port is specified
  // or (options[, cb]) where options.port is specified
  // or if options.port is normalized as 0 before
  var backlog;
  if (typeof options.port === 'number' || typeof options.port === 'string') {
    if (!isLegalPort(options.port)) {
      throw new ERR_SOCKET_BAD_PORT(options.port);
    }
    backlog = options.backlog || backlogFromArgs;
    // start TCP server listening on host:port
    if (options.host) {
      lookupAndListen(this, options.port | 0, options.host, backlog,
                      options.exclusive, flags);
    } else { // Undefined host, listens on unspecified address
      // Default addressType 4 will be used to search for master server
      listenInCluster(this, null, options.port | 0, 4,
                      backlog, undefined, options.exclusive);
    }
    return this;
  }

  // (path[, backlog][, cb]) or (options[, cb])
  // where path or options.path is a UNIX domain socket or Windows pipe
  if (options.path && isPipeName(options.path)) {
    var pipeName = this._pipeName = options.path;
    backlog = options.backlog || backlogFromArgs;
    listenInCluster(this, pipeName, -1, -1,
                    backlog, undefined, options.exclusive);

    if (!this._handle) {
      // Failed and an error shall be emitted in the next tick.
      // Therefore, we directly return.
      return this;
    }

    let mode = 0;
    if (options.readableAll === true)
      mode |= PipeConstants.UV_READABLE;
    if (options.writableAll === true)
      mode |= PipeConstants.UV_WRITABLE;
    if (mode !== 0) {
      const err = this._handle.fchmod(mode);
      if (err) {
        this._handle.close();
        this._handle = null;
        throw errnoException(err, 'uv_pipe_chmod');
      }
    }
    return this;
  }

  if (!(('port' in options) || ('path' in options))) {
    throw new ERR_INVALID_ARG_VALUE('options', options,
                                    'must have the property "port" or "path"');
  }

  throw new ERR_INVALID_OPT_VALUE('options', inspect(options));
};

这个就是我们要找的listen方法,可是里面很多ipv4和ipv6的处理,最重要的方法是listenInCluster


这个函数需要好好看一下,只有几十行

function listenInCluster(server, address, port, addressType,
                         backlog, fd, exclusive, flags) {
  exclusive = !!exclusive;
  if (cluster === undefined) cluster = require('cluster');

  if (cluster.isMaster || exclusive) {
    // Will create a new handle
    // _listen2 sets up the listened handle, it is still named like this
    // to avoid breaking code that wraps this method
    server._listen2(address, port, addressType, backlog, fd, flags);
    return;
  }

  const serverQuery = {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags,
  };

  // Get the master's server handle, and listen on it
  cluster._getServer(server, serverQuery, listenOnMasterHandle);

  function listenOnMasterHandle(err, handle) {
    err = checkBindError(err, port, handle);

    if (err) {
      var ex = exceptionWithHostPort(err, 'bind', address, port);
      return server.emit('error', ex);
    }

    // Reuse master's server handle
    server._handle = handle;
    // _listen2 sets up the listened handle, it is still named like this
    // to avoid breaking code that wraps this method
    server._listen2(address, port, addressType, backlog, fd, flags);
  }
}

如果是主进程,那么就直接调用_.listen2方法了

Server.prototype._listen2 = setupListenHandle;

找到setupListenHandle函数

function setupListenHandle(address, port, addressType, backlog, fd, flags) {

里面的createServerHandle是重点


function setupListenHandle(address, port, addressType, backlog, fd, flags) {
  debug('setupListenHandle', address, port, addressType, backlog, fd);

  // If there is not yet a handle, we need to create one and bind.
  // In the case of a server sent via IPC, we don't need to do this.
  if (this._handle) {
    debug('setupListenHandle: have a handle already');
  } else {
    debug('setupListenHandle: create a handle');

    var rval = null;

    // Try to bind to the unspecified IPv6 address, see if IPv6 is available
    if (!address && typeof fd !== 'number') {
      rval = createServerHandle(DEFAULT_IPV6_ADDR, port, 6, fd, flags);

      if (typeof rval === 'number') {
        rval = null;
        address = DEFAULT_IPV4_ADDR;
        addressType = 4;
      } else {
        address = DEFAULT_IPV6_ADDR;
        addressType = 6;
      }
    }

    if (rval === null)
      rval = createServerHandle(address, port, addressType, fd, flags);

    if (typeof rval === 'number') {
      var error = uvExceptionWithHostPort(rval, 'listen', address, port);
      process.nextTick(emitErrorNT, this, error);
      return;
    }
    this._handle = rval;
  }

  this[async_id_symbol] = getNewAsyncId(this._handle);
  this._handle.onconnection = onconnection;
  this._handle[owner_symbol] = this;

  // Use a backlog of 512 entries. We pass 511 to the listen() call because
  // the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1);
  // which will thus give us a backlog of 512 entries.
  const err = this._handle.listen(backlog || 511);

  if (err) {
    var ex = uvExceptionWithHostPort(err, 'listen', address, port);
    this._handle.close();
    this._handle = null;
    defaultTriggerAsyncIdScope(this[async_id_symbol],
                               process.nextTick,
                               emitErrorNT,
                               this,
                               ex);
    return;
  }

  // Generate connection key, this should be unique to the connection
  this._connectionKey = addressType + ':' + address + ':' + port;

  // Unref the handle if the server was unref'ed prior to listening
  if (this._unref)
    this.unref();

  defaultTriggerAsyncIdScope(this[async_id_symbol],
                             process.nextTick,
                             emitListeningNT,
                             this);
}

已经可以看到TCP了,离真正的绑定监听端口,更近了一步

最终通过下面的方法绑定监听端口

 handle.bind6(address, port, flags);
 或者
 handle.bind(address, port);

首选ipv6绑定,是因为ipv6可以接受到ipv4的套接字,而ipv4不可以接受ipv6的套接字,当然也有方法可以接收,就是麻烦了一点


上面的内容,请你认真看,因为下面会更复杂,设计到Node.js的多进程负载均衡原理

如果不是主进程,就调用cluster._getServer,找到cluster源码


'use strict';

const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);

找到_getServer函数源码


// `obj` is a net#Server or a dgram#Socket object.
cluster._getServer = function(obj, options, cb) {
  let address = options.address;

  // Resolve unix socket paths to absolute paths
  if (options.port < 0 && typeof address === 'string' &&
      process.platform !== 'win32')
    address = path.resolve(address);

  const indexesKey = [address,
                      options.port,
                      options.addressType,
                      options.fd ].join(':');

  let index = indexes.get(indexesKey);

  if (index === undefined)
    index = 0;
  else
    index++;

  indexes.set(indexesKey, index);

  const message = {
    act: 'queryServer',
    index,
    data: null,
    ...options
  };

  message.address = address;

  // Set custom data on handle (i.e. tls tickets key)
  if (obj._getServerData)
    message.data = obj._getServerData();

  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  });

  obj.once('listening', () => {
    cluster.worker.state = 'listening';
    const address = obj.address();
    message.act = 'listening';
    message.port = (address && address.port) || options.port;
    send(message);
  });
};

我们之前传入了三个参数给它,分别是

server,serverQuery,listenOnMasterHandle


这里是比较复杂的,曾经我也在这里迷茫过一段时间,但是想着还是看下去吧。坚持下,大家如果看到这里看不下去了,先休息下,保存着。后面等心情平复了再静下来接下去看


首先我们传入了Server、serverQuery和cb(回调函数listenOnMasterHandle),整个cluster模块的_getServer中最重要的就是:


if (obj._getServerData)
    message.data = obj._getServerData();

  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  });

首先我们会先获取server上的data数据,然后调用send函数


function send(message, cb) {
  return sendHelper(process, message, null, cb);
}

send函数调用的是cluster模块的utills文件内的函数,传入了一个默认值process



function sendHelper(proc, message, handle, cb) {
  if (!proc.connected)
    return false;

  // Mark message as internal. See INTERNAL_PREFIX in lib/child_process.js
  message = { cmd: 'NODE_CLUSTER', ...message, seq };

  if (typeof cb === 'function')
    callbacks.set(seq, cb);

  seq += 1;
  return proc.send(message, handle);
}

这里要看清楚,我们调用sendHelper传入的第三个参数是null  !!!

那么主进程返回也是null



send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  });

所以我们会进入rr函数调用的这个判断,这里调用rr传入的cb就是在net.js模块定义的listenOnMasterHandle函数


Node.js的负载均衡算法是轮询,官方给出的解释是简单粗暴效率高

上面的sendHelper函数就是做到了这点,每次+1


 if (typeof cb === 'function')
    callbacks.set(seq, cb);

  seq += 1;
function rr(message, indexesKey, cb) {
  if (message.errno)
    return cb(message.errno, null);

  var key = message.key;

  function listen(backlog) {
    // TODO(bnoordhuis) Send a message to the master that tells it to
    // update the backlog size. The actual backlog should probably be
    // the largest requested size by any worker.
    return 0;
  }

  function close() {
    // lib/net.js treats server._handle.close() as effectively synchronous.
    // That means there is a time window between the call to close() and
    // the ack by the master process in which we can still receive handles.
    // onconnection() below handles that by sending those handles back to
    // the master.
    if (key === undefined)
      return;

    send({ act: 'close', key });
    handles.delete(key);
    indexes.delete(indexesKey);
    key = undefined;
  }

  function getsockname(out) {
    if (key)
      Object.assign(out, message.sockname);

    return 0;
  }

  // Faux handle. Mimics a TCPWrap with just enough fidelity to get away
  // with it. Fools net.Server into thinking that it's backed by a real
  // handle. Use a noop function for ref() and unref() because the control
  // channel is going to keep the worker alive anyway.
  const handle = { close, listen, ref: noop, unref: noop };

  if (message.sockname) {
    handle.getsockname = getsockname;  // TCP handles only.
  }

  assert(handles.has(key) === false);
  handles.set(key, handle);
  cb(0, handle);
}

此时的handle已经被重写,listen方法调用会返回0,不会再占用端口了。所以这样Node.js多个进程也只是一个进程监听端口而已

此时的cb还是net.js模块的setupListenHandle即 - _listen2方法。

官方的注释:

Faux handle. Mimics a TCPWrap with just enough fidelity to get away

花了一晚上整理,之前还有一些像cluster模块源码、pm2负载均衡原理等,有兴趣的可以翻一翻。觉得写得不错的可以点个在看,谢谢。时间匆忙,如果有写得不对的地方可以指出。

可以关注下公众号:前端巅峰 把文章推荐给需要的人

查看原文

赞 14 收藏 12 评论 2

依韵_宵音 赞了文章 · 2020-01-16

用 JavaScript 实现基于类的枚举模式

作者:Dr. Axel Rauschmayer

翻译:疯狂的技术宅

原文:https://2ality.com/2020/01/en...

未经允许严禁转载

在本文中,我们将会研究在 JavaScript 中实现基于类的枚举模式。还会研究一下 Enumify 这个能够帮助我们使用枚举模式的库


实现枚举:第一次尝试

枚举是由一组值组成的类型。例如 TypeScript 中有内置的枚举,我们可以通过它们来定义自己的布尔类型:

enum MyBoolean {
  false,
  true,
}

或者可以定义自己的颜色类型:

enum Color {
  red,
  orange,
  yellow,
  green,
  blue,
  purple,
}

这段 TypeScript 代码会被编译为以下 JavaScript 代码(省略了一些详细信息,以便于理解):

const Color = {
  red: 0,
  orange: 1,
  yellow: 2,
  green: 3,
  blue: 4,
  purple: 5,
};

这种实现有几个问题:

  1. 日志输出:如果你输出一个枚举值,例如 Color.red,是看不到它的名称的。
  2. 类型安全:枚举值不是唯一的,它们会其他数字所干扰。例如,数字 1 可能会误认为 Color.green,反之亦然。
  3. 成员资格检查:你无法轻松检查给定的值是否为 Color 的元素。

用普通 JavaScript,我们可以通过使用字符串而不是数字作为枚举值来解决问题 1:

const Color = {
  red: 'red',
  orange: 'orange',
  yellow: 'yellow',
  green: 'green',
  blue: 'blue',
  purple: 'purple',
}

如果我们用符号作为枚举值,还能够获得类型安全性:

const Color = {
  red: Symbol('red'),
  orange: Symbol('orange'),
  yellow: Symbol('yellow'),
  green: Symbol('green'),
  blue: Symbol('blue'),
  purple: Symbol('purple'),
}
assert.equal(
  String(Color.red), 'Symbol(red)');

符号存在的一个问题是需要将它们明确转换为字符串,而不能强制转换(例如,通过 + 或内部模板文字):

assert.throws(
  () => console.log('Color: '+Color.red),
  /^TypeError: Cannot convert a Symbol value to a string$/
);

尽管可以测试成员资格,但这并不简单:

function isMember(theEnum, value) {
  return Object.values(theEnum).includes(value);
}
assert.equal(isMember(Color, Color.blue), true);
assert.equal(isMember(Color, 'blue'), false);

枚举模式

通过对枚举使用自定义类可以使我们进行成员资格测试,并在枚举值方面具有更大的灵活性:

class Color {
  static red = new Color('red');
  static orange = new Color('orange');
  static yellow = new Color('yellow');
  static green = new Color('green');
  static blue = new Color('blue');
  static purple = new Color('purple');

  constructor(name) {
    this.name = name;
  }
  toString() {
    return `Color.${this.name}`;
  }
}

我把这种用类作为枚举的方式称为“枚举模式”。它受到 Java 中对枚举实现的启发。

输出:

console.log('Color: '+Color.red);

// Output:
// 'Color: Color.red'

成员资格测试:

assert.equal(
  Color.green instanceof Color, true);

枚举:枚举模式的辅助库

Enumify 是一个能够帮助我们使用枚举模式的库。它的用法如下:

class Color extends Enumify {
  static red = new Color();
  static orange = new Color();
  static yellow = new Color();
  static green = new Color();
  static blue = new Color();
  static purple = new Color();
  static _ = this.closeEnum();
}

实例属性

Enumify 能够把多个实例属性添加到枚举值中:

assert.equal(
  Color.red.enumKey, 'red');
assert.equal(
  Color.red.enumOrdinal, 0);

原型方法

用 Enumify 实现 .toStrin()

assert.equal(
  'Color: ' + Color.red, // .toString()
  'Color: Color.red');

静态功能

Enumify 设置了两个静态属性– .enumKeys.enumValues

assert.deepEqual(
  Color.enumKeys,
  ['red', 'orange', 'yellow', 'green', 'blue', 'purple']);
assert.deepEqual(
  Color.enumValues,
  [ Color.red, Color.orange, Color.yellow,
    Color.green, Color.blue, Color.purple]);

它提供了可继承的静态方法 .enumValueOf()

assert.equal(
  Color.enumValueOf('yellow'),
  Color.yellow);

它实现了可继承的可迭代性:

for (const c of Color) {
  console.log('Color: ' + c);
}
// Output:
// 'Color: Color.red'
// 'Color: Color.orange'
// 'Color: Color.yellow'
// 'Color: Color.green'
// 'Color: Color.blue'
// 'Color: Color.purple'

使用枚举的例子

具有实例属性的枚举值

class Weekday extends Enumify {
  static monday = new Weekday(true);
  static tuesday = new Weekday(true);
  static wednesday = new Weekday(true);
  static thursday = new Weekday(true);
  static friday = new Weekday(true);
  static saturday = new Weekday(false);
  static sunday = new Weekday(false);
  static _ = this.closeEnum();
  constructor(isWorkDay) {
    super();
    this.isWorkDay = isWorkDay;
  }
}
assert.equal(Weekday.sunday.isWorkDay, false);
assert.equal(Weekday.wednesday.isWorkDay, true);

通过 switch 使用枚举值

枚举模式也有其缺点:通常在创建枚举时不能引用其他的枚举(因为这些枚举可能还不存在)。解决方法是,可以通过以下函数在外部实现辅助函数:

class Weekday extends Enumify {
  static monday = new Weekday();
  static tuesday = new Weekday();
  static wednesday = new Weekday();
  static thursday = new Weekday();
  static friday = new Weekday();
  static saturday = new Weekday();
  static sunday = new Weekday();
  static _ = this.closeEnum();
}
function nextDay(weekday) {
  switch (weekday) {
    case Weekday.monday:
      return Weekday.tuesday;
    case Weekday.tuesday:
      return Weekday.wednesday;
    case Weekday.wednesday:
      return Weekday.thursday;
    case Weekday.thursday:
      return Weekday.friday;
    case Weekday.friday:
      return Weekday.saturday;
    case Weekday.saturday:
      return Weekday.sunday;
    case Weekday.sunday:
      return Weekday.monday;
    default:
      throw new Error();
  }
}

能够通过 getter 获取实例的枚举值

另一个解决在声明枚举时无法使用其他枚举的方法是通过 getter 延迟访问同级的值:

class Weekday extends Enumify {
  static monday = new Weekday({
    get nextDay() { return Weekday.tuesday }
  });
  static tuesday = new Weekday({
    get nextDay() { return Weekday.wednesday }
  });
  static wednesday = new Weekday({
    get nextDay() { return Weekday.thursday }
  });
  static thursday = new Weekday({
    get nextDay() { return Weekday.friday }
  });
  static friday = new Weekday({
    get nextDay() { return Weekday.saturday }
  });
  static saturday = new Weekday({
    get nextDay() { return Weekday.sunday }
  });
  static sunday = new Weekday({
    get nextDay() { return Weekday.monday }
  });
  static _ = this.closeEnum();
  constructor(props) {
    super();
    Object.defineProperties(
      this, Object.getOwnPropertyDescriptors(props));
  }
}
assert.equal(
  Weekday.friday.nextDay, Weekday.saturday);
assert.equal(
  Weekday.sunday.nextDay, Weekday.monday);

getter 传递给对象内部的构造函数。构造函数通过 Object.defineProperties() 和 Object.getOwnPropertyDescriptors()将它们复制到当前实例。但是我们不能在这里使用 Object.assign(),因为它无法复制 getter 和其他方法。

通过实例方法实现状态机

在下面的例子中实现了一个状态机。我们将属性(包括方法)传递给构造函数,构造函数再将其复制到当前实例中。

class State extends Enumify {
  static start = new State({
    done: false,
    accept(x) {
      if (x === '1') {
        return State.one;
      } else {
        return State.start;
      }
    },
  });
  static one = new State({
    done: false,
    accept(x) {
      if (x === '1') {
        return State.two;
      } else {
        return State.start;
      }
    },
  });
  static two = new State({
    done: false,
    accept(x) {
      if (x === '1') {
        return State.three;
      } else {
        return State.start;
      }
    },
  });
  static three = new State({
    done: true,
  });
  static _ = this.closeEnum();
  constructor(props) {
    super();
    Object.defineProperties(
      this, Object.getOwnPropertyDescriptors(props));
  }
}
function run(state, inputString) {
  for (const ch of inputString) {
    if (state.done) {
      break;
    }
    state = state.accept(ch);
    console.log(`${ch} --> ${state}`);
  }
}

状态机检测字符串中是否存在连续的三个 1 的序列:

run(State.start, '01011100');

// Output:
// '0 --> State.start'
// '1 --> State.one'
// '0 --> State.start'
// '1 --> State.one'
// '1 --> State.two'
// '1 --> State.three'

任意枚举值

有时我们需要枚举值是数字(例如,用于表示标志)或字符串(用于与 HTTP 头中的值进行比较)。可以通过枚举来实现。例如:

class Mode extends Enumify {
  static user_r = new Mode(0b100000000);
  static user_w = new Mode(0b010000000);
  static user_x = new Mode(0b001000000);
  static group_r = new Mode(0b000100000);
  static group_w = new Mode(0b000010000);
  static group_x = new Mode(0b000001000);
  static all_r = new Mode(0b000000100);
  static all_w = new Mode(0b000000010);
  static all_x = new Mode(0b000000001);
  static _ = this.closeEnum();
  constructor(n) {
    super();
    this.n = n;
  }
}
assert.equal(
  Mode.user_r.n | Mode.user_w.n | Mode.user_x.n |
  Mode.group_r.n | Mode.group_x.n |
  Mode.all_r.n | Mode.all_x.n,
  0o755);
assert.equal(
  Mode.user_r.n | Mode.user_w.n | Mode.user_x.n |
  Mode.group_r.n,
  0o740);

本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 12 收藏 11 评论 0

依韵_宵音 赞了文章 · 2020-01-08

vscode 自定义代码片段

实现效果

起因

最近在写一个全新的项目,在项目中频繁创建各种类,这就导致很多重复的东西需要频繁的写,例如类名,命名空间,继承关系...那么有没有一种 办法能解决这个问题呢?

提出设想

我想起了,最初用 sublime text 的时候,可以利用代码片段功能大段的生成html代码,当时就觉得十分的方便,那么 vscode 有没有这个功能呢?经过 google 之后我知道 vscode 是有代码片段功能的。既然有了想法,也具备了基础实施条件,那么接下来开始尝试实现之前的想法。

资料查询

经过一番 google 后发现对于 vscode snippet 介绍都在相对基础的简单应用(只是一些插入固定代码和光标介绍),者显然无法实现我们生成类名和明明空间的想法,google 无果,那么只能看看 vscode 官方文档果然有意想不到的收获,看完官网介绍后,基本就确定此路是可行的。

snippet 示例

File > Preferences (Code > Preferences on macOS) 中选择 User Snippets 在弹出框里选择对应的代码片段语言,我这里使用的是php

 "Print to console": {
         "prefix": "log",
         "body": [
             "console.log('$1');",
             "$2"
         ],
         "description": "Log output to console"
    }

在打开的 php.json 中有示例代码:

  • Print to console 代码片段名称
  • prefix 插件前缀
  • body 插件内容可以是字符串,也可以为数组,若为数组每个元素都做为单独的一行插入。
  • description 插件描述

Snippet 语法

制表位(Tabstops)

使用制表位(Tabstops)可是在代码片段中移动光标位置,使用$1,$2来指定光标的位置,数字代表光标的移动的顺序,值得注意的时$0代表光标的最后位置。如果有多个相同的制表位(Tabstops)会在编译器里同时出现多个光标(类似编译器的块编辑模式)。

占位符(Placeholders)

占位符(Placeholders) 是带默认值的制表位(Tabstops),占位符(Placeholders)的文本会被插入到制表位(Tabstops)所在位置并且全选以方便修改,占位符(Placeholders)可以嵌套使用,比如${1:another ${2:placeholder}}

选择项(Choice)

占位符(Placeholders)可以有多选值,每个选项的值用 , 分隔,选项的开始和结束用管道符号(|)将选项包含,例如: ${1|one,two,three|},当插入代码片段,选择制制表位(Tabstops)的时候,会列出选项供用户选择。

变量(Variables)

使用 $name 或者 ${name|default} 可以插入变量的值,如果变量未被赋值则插入 default 的值或者空值 。当变量未被定义,则将变量名插入,变量(Variables)将被转换为占位符(Placeholders)
系统变量如下

  • TM_SELECTED_TEXT 当前选定的文本或空字符串
  • TM_CURRENT_LINE 当前行的内容
  • TM_CURRENT_WORD 光标下的单词的内容或空字符串
  • TM_LINE_INDEX 基于零索引的行号
  • TM_LINE_NUMBER 基于一索引的行号
  • TM_FILENAME 当前文档的文件名
  • TM_FILENAME_BASE 当前文档的文件名(不含后缀名)
  • TM_DIRECTORY 当前文档的目录
  • TM_FILEPATH 当前文档的完整文件路径
  • CLIPBOARD 剪切板里的内容

插入当前日期或时间:

  • CURRENT_YEAR 当前年(四位数)
  • CURRENT_YEAR_SHORT 当前年(两位数)
  • CURRENT_MONTH 当前月
  • CURRENT_MONTH_NAME 本月的全名(’七月’)
  • CURRENT_MONTH_NAME_SHORT 月份的简称(’Jul’)
  • CURRENT_DATE 当前日
  • CURRENT_DAY_NAME 当天的名称(’星期一’)
  • CURRENT_DAY_NAME_SHORT 当天的短名称(’Mon’)
  • CURRENT_HOUR 当前小时
  • CURRENT_MINUTE 当前分钟
  • CURRENT_SECOND 当前秒

当前语言的行注释或块注释:

  • BLOCK_COMMENT_START 块注释开始标识,如 PHP /* 或 HTML <!--
  • BLOCK_COMMENT_END 块注释结束标识,如 PHP */ 或 HTML -->
  • LINE_COMMENT 行注释,如: PHP // 或 HTML <!-- -->

下面片则会生成 PHP / Hello World /

{
    "hello": {
        "scope": "php",
        "prefix": "hello",
        "body": "$BLOCK_COMMENT_START Hello World $BLOCK_COMMENT_END"
    }
}

变量转换(Variable transforms)

变量转换(Variable transforms) 允许变量在插入前改变变量的值,变量转换(Variable transforms)由三部分组成

  1. 正则匹配:使用正则表达式匹配变量值,若变量无法解析则值为空。
  2. 格式串:允许引用正则表达式匹配组。格式串允许条件插入和做简单的修改。
  3. 正则表达式匹配选项

下面例子是使用变量转换(Variable transforms)将带后缀的文件名转换为不带后缀的文件名

${TM_FILENAME/(.*)\\..+$/$1/}
  |           |         |  |
  |           |         |  |-> 无选项设置
  |           |         |
  |           |         |-> 引用捕获组的第一个分组内容
  |           |             
  |           |
  |           |-> 匹配后缀前的所有字符串
  |               
  |
  |-> 文件名(带后缀)

需求实现

要解决的问题

生成Class 的命名空间、类名、选择继承关系

问题分析

项目目录结构如下所示

peoject
|
|----application
|--------admin
|------------services
|----------------TestServices.php

类名可以直接使用 TM_FILENAME_BASE 变量的值即可,命名空间则需要使用 TM_DIRECTORY 变量,以 TestServices.php 为例,TM_DIRECTORY 得到的目录为 peoject\application\admin\services我们只需要将peoject\application\ 替换为 app 得到 app\admin\services 就是我们的明明空间了,至于继承就是一个选择项就可以了。既然已经全部知道该如何实现,接下来就是代码实现的过程了。

代码实现

"service-construct" :{
        "prefix": "gen",
        "body": [
            "namespace ${TM_DIRECTORY/.*application/app/};\n",
            "class $TM_FILENAME_BASE extends ${1|BaseService,BaseController,BaseModel|}",
            "{",
            "\tpublic function __construct() \r\n    {\n\t\t\\$this->model = new \r\n\t}",
            "}"
        ],
        "description": "generate service class"

    },

一些思考

上述代码基本上完成了我要实现的功能,但是也存在了一些问题,例如:我现在是用 windows 操作系统,因而TM_DIRECTORY 得到的目录为 peoject\application\admin\services 若是 linux 系统,此代码片段是无法正常的生成命名空间的,我做了一些资料的搜索,代码片段并没有自定义变量的功能(也许可以,只是我么有找到方法,如果有知道的大牛,请留言赐教。
随着对 vscode snippet 的深入了解,我之前所设想的方案要用代码片段的方式实现是有些困难的,vscode 将其作为一个快速生成代码的解决方案,我们所写的代码片段相当于一个带有填空模板,而对代码片段的应用就是生成带有制表符的代码模板,然后根据制表符顺序补全代码。
至于这个不完美的方案,我打算再研究一下代码片段是否能完全实现,如果依旧解决不了,就选用其它方案进行尝试。

参考链接

Creating your own snippets

查看原文

赞 19 收藏 12 评论 5

依韵_宵音 赞了文章 · 2019-12-10

前端也要懂一点 MongoDB Schema 设计

image

翻译自 MongoDB 官方博客:

时间仓促,水平有限,难免有遗漏和不足,还请不吝指正。

“我有很多 SQL 的开发经验,但是对于 MongoDB 来说,我只是个初学者。 我该如何在数据库里实现 One-to-N 的关系? ” 这是我从参加 MongoDB office hours 的用户那里得到的最常见的问题之一。

对于这个问题,我无法提供一个简单明了的答案,因为在 MongoDB 里,有很多种方法可以实现 One-to-N 关系。 Mongodb 拥有丰富且细致入微的词汇表,用来表达在 SQL 中精简的术语 One-to-N 所包含的内容。接下来让我带你遍历一下 使用 Mongodb 实现 One-to-N 关系的各种方式。

这一块涉及到的内容很多,因此我把它分成三部分。

  • 在第一部分中,我将讨论建立 One-to-N 关系模型的三种基本方法。
  • 在第二部分中,我将介绍更复杂的模式设计(schema designs),包括 反规范化(denormalization)双向引用(two-way referencing)
  • 在最后一部分,我将回顾一系列的选型,并给出一些建议和原则,保证你在创建 One-to-N 关系时,从成千上万的选择中做出正确的选择。

Part 1

许多初学者认为,在 MongoDB 中建立 One-to-N 模型的唯一方法是在父文档中嵌入一组 子文档(sub-documents),但事实并非如此。 你可以嵌入一个文档,并不意味着你应该嵌入一个文档。(PS:这也是我们写代码的原则之一:You can doesn’t mean you should )

在设计 MongoDB 模式时,您需要先考虑在使用 SQL 时从未考虑过的问题:关系(relationship)的基数(cardinality)是什么? 简而言之: 你需要用更细微的差别来描述你的 One-to-N 关系: 是 one-to-fewone-to-many 还是 one-to-squillions ? 根据它是哪一个,你可以使用不同的方式来进行关系建模

Basics: Modeling One-to-Few

one-to-few 的一个例子通常是一个人的地址。 这是一个典型的使用 嵌入(embedding)的例子 -- 你可以把地址放在 Person 对象的数组中:

> db.person.findOne()
{
  name: 'Kate Monster',
  ssn: '123-456-7890',
  addresses : [
     { street: '123 Sesame St', city: 'Anytown', cc: 'USA' },
     { street: '123 Avenue Q', city: 'New York', cc: 'USA' }
  ]
}

这种设计具有嵌入的所有优点和缺点:主要优点是不必执行单独的查询来获取嵌入的详细信息;主要缺点是无法将嵌入的详细信息作为 独立实体(stand-alone entities)来访问

例如,如果您为一个任务跟踪系统建模,每个 Person 都会有一些分配给他们的任务。 在 Person 文档中嵌入任务会使 “显示明天到期的所有任务” 这样的查询比实际需要的要困难得多。

Basics: One-to-Many

“one-to-many” 的一个例子可能是替换零件(parts)订购系统中的产品(products)的零部件。 每个产品可能有多达几百个替换零件,但从来没有超过几千(所有不同尺寸的螺栓、垫圈和垫圈加起来)。 这是一个很好的使用 引用(referencing)的例子 —— 您可以将零件的 ObjectIDs 放在产品文档的数组中。 (示例使用 2 字节的 ObjectIDs,以方便阅读)

每个零件都有自己的 document:

> db.parts.findOne()
{
    _id : ObjectID('AAAA'),
    partno : '123-aff-456',
    name : '#4 grommet',
    qty: 94,
    cost: 0.94,
    price: 3.99
}

每个产品也有自己的 document,其中包含对组成产品的各个零件的一系列 ObjectID 引用:

> db.products.findOne()
{
    name : 'left-handed smoke shifter',
    manufacturer : 'Acme Corp',
    catalog_number: 1234,
    parts : [     // array of references to Part documents
        ObjectID('AAAA'),    // reference to the #4 grommet above
        ObjectID('F17C'),    // reference to a different Part
        ObjectID('D2AA'),
        // etc
    ]
    ...

然后,您可以使用 应用程序级别的联接(application-level join)来检索特定产品的零件:

 // Fetch the Product document identified by this catalog number
> product = db.products.findOne({catalog_number: 1234});
   // Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;

为了高效运行,您需要在 products.catalog_number 上添加索引。 注意,零件上总是有一个索引 parts._id,这样查询通常效率很高。

这种类型的 引用(referencing)嵌入(embedding)相比有一系列的优点和缺点:每个零件都是一个独立的文档,因此很容易对它们进行搜索和单独更新。 使用这个模式的一个弊端是必须执行两次查询来获取有关产品零件的详细信息。 (但是在我们进入第二部分的 反规范化(denormalizing)之前,请保持这种想法。)

作为一个额外的好处,这个模式允许一个单独的零件被多个产品 使用,因此您的 One-to-N 模式就变成了 N-to-N 模式,而不需要任何 联接表(join table)

Basics: One-to-Squillions

one-to-squillions 的一个典型例子是为不同机器收集日志消息的事件日志系统。 任何给定的 主机(hosts)都可以生成足够的日志信息(logmsg),从而超过溢出 document 16 MB 的限制,即使数组中存储的所有内容都是 ObjectID。这就是 父引用(parent-referencing) 的经典案例 —— 你有一个 host document,然后将主机的 ObjectID 存储在日志信息的 document 中。

> db.hosts.findOne()
{
    _id : ObjectID('AAAB'),
    name : 'goofy.example.com',
    ipaddr : '127.66.66.66'
}

>db.logmsg.findOne()
{
    time : ISODate("2014-03-28T09:42:41.382Z"),
    message : 'cpu is on fire!',
    host: ObjectID('AAAB')       // Reference to the Host document
}

您可以使用(略有不同的) 应用程序级别的联接(application-level join)来查找主机最近的 5,000 条消息:

  // find the parent ‘host’ document
> host = db.hosts.findOne({ipaddr : '127.66.66.66'});  // assumes unique index
   // find the most recent 5000 log message documents linked to that host
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()

回顾

因此,即使在这个基本层次上,在设计 MongoDB Schema 时也需要比在设计类似的 关系模式( Relational Schema)时考虑更多的问题。 你需要考虑两个因素:

  • One-to-N 中 N-side 的实体是否需要独立存在?
  • 这种关系的基数性是什么:one-to-fewone-to-many、 还是 one-to-squillions

基于这些因素,您可以从三种基本的 One-to-N 模式设计中选择一种:

  • 如果基数是 one-to-few,并且不需要访问父对象上下文之外的 嵌入对象(embedded object),则将 N-side 嵌入父对象
  • 如果基数是 one-to-many ,或者如果 N-side 对象因为任何原因应该单独存在,则使用 N-side 对象的引用数组
  • 如果基数是 one-to-squillions,则使用 N-side 对象中对 One-side 的引用

Part 2

这是我们在 MongoDB 中构建 One-to-N 关系的第二站。 上次我介绍了三种基本的模式设计: 嵌入(embedding)、子引用(child-referencing)和父引用(parent-referencing)。 我还提到了在选择这些设计时要考虑的两个因素:

  • One-to-N 中 N-side 的实体是否需要独立存在?
  • 这种关系的基数性是什么: 是 one-to-fewone-to-many 还是 one-to-squillions

有了这些基本技术,我可以继续讨论更复杂的模式设计,包括 双向引用(two-way referencing)反规范化(denormalization)

Intermediate: Two-Way Referencing

如果您希望获得一些更好的引用,那么可以结合两种技术,并在 Schema 中包含两种引用样式,既有从 “one” side 到 “one” side 的引用,也有从 “many”side 到 “one” side 的引用。

例如,让我们回到任务跟踪系统。 有一个 “people” 的 collection 用于保存 Person documents,一个 “tasks” collection 用于保存 Task documents,以及来自 Person -> Task 的 One-to-N 关系。 应用程序需要跟踪 Person 拥有的所有任务,因此我们需要引用 Person -> Task。

使用对 Task documents 的引用数组,单个 Person document 可能看起来像这样:

db.person.findOne()
{
    _id: ObjectID("AAF1"),
    name: "Kate Monster",
    tasks [     // array of references to Task documents
        ObjectID("ADF9"), 
        ObjectID("AE02"),
        ObjectID("AE73") 
        // etc
    ]
}

另一方面,在其他一些上下文中,这个应用程序将显示一个 Tasks 列表(例如,一个多人项目中的所有 Tasks) ,它将需要快速查找哪个人负责哪个任务。 您可以通过在 Task document 中添加对 Person 的附加引用来优化此操作。

db.tasks.findOne()
{
    _id: ObjectID("ADF9"), 
    description: "Write lesson plan",
    due_date:  ISODate("2014-04-01"),
    owner: ObjectID("AAF1")     // Reference to Person document
}

这种设计具有 One-to-Many 模式的所有优点和缺点,但添加了一些内容。 在 Task document 中添加额外的 owner 引用意味着可以快速简单地找到任务的所有者,但是这也意味着如果你需要将任务重新分配给其他人,你需要执行两个更新而不是一个。

具体来说,您必须同时更新从 Person 到 Task 文档的引用,以及从 Task 到 Person 的引用。 (对于正在阅读这篇文章的关系专家来说,您是对的: 使用这种模式设计意味着不再可能通过单个 原子更新(atomic update)将一个任务重新分配给一个新的 Person。 这对于我们的任务跟踪系统来说是可行的: 您需要考虑这是否适用于您的特定场景。)

Intermediate: Denormalizing With “One-To-Many” Relationships

除了对关系的各种类型进行建模之外,您还可以在模式中添加 反规范化(denormalization)。 这可以消除在某些情况下执行 应用程序级联接(application-level join)的需要,但代价是在执行更新时会增加一些复杂性。 举个例子就可以说明这一点。

Denormalizing from Many -> One

对于产品-零件示例,您可以将零件的名称非规范化为“parts[]”数组。 作为比较,下面是未采用 反规范化(denormalization)的 Product document 版本。

> db.products.findOne()
{
    name : 'left-handed smoke shifter',
    manufacturer : 'Acme Corp',
    catalog_number: 1234,
    parts : [     // array of references to Part documents
        ObjectID('AAAA'),    // reference to the #4 grommet above
        ObjectID('F17C'),    // reference to a different Part
        ObjectID('D2AA'),
        // etc
    ]
}

而 反规范化(Denormalizing)意味着在显示 Product 的所有 Part 名称时不必执行应用程序级联接(application-level join),但是如果需要关于某个部件的任何其他信息,则必须执行该联接。

> db.products.findOne()
{
    name : 'left-handed smoke shifter',
    manufacturer : 'Acme Corp',
    catalog_number: 1234,
    parts : [
        { id : ObjectID('AAAA'), name : '#4 grommet' },         // Part name is denormalized
        { id: ObjectID('F17C'), name : 'fan blade assembly' },
        { id: ObjectID('D2AA'), name : 'power switch' },
        // etc
    ]
}

虽然这样可以更容易地获得零件名称,但只需要在 应用程序级别的联接(application-level join)中增加一点 客户端(client-side)工作:

// Fetch the product document
> product = db.products.findOne({catalog_number: 1234});  
  // Create an array of ObjectID()s containing *just* the part numbers
> part_ids = product.parts.map( function(doc) { return doc.id } );
  // Fetch all the Parts that are linked to this Product
> product_parts = db.parts.find({_id: { $in : part_ids } } ).toArray() ;

只有当读取和更新的比例很高时,反规范化(Denormalizing)才有意义。 如果你经常阅读非标准化(denormalized)的数据,但是很少更新,那么为了得到更有效的查询,付出更慢的更新和更复杂的更新的代价是有意义的。 随着相对于查询的更新变得越来越频繁,非规范化节省的开销会越来越少

例如: 假设零件名称不经常更改,但手头的数量经常更改。 这意味着,尽管在 Product document 中对零件名称进行 反规范化(Denormalizing)是有意义的,但是对数量进行 反规范化(Denormalizing) 是没有意义的。

还要注意,如果对 字段(field)进行 反规范化(Denormalizing),将失去对该 字段(field)执行原子(atomic)更新和 独立(isolated)更新的能力。 就像上面的 双向引用(two-way referencing)示例一样,如果你先在 Part document 中更新零件名称,然后在 Product 文档中更新零件名称,那么将会有一个 sub-second 的时间间隔,在这个间隔中,Product document 中 反规范化(Denormalizing)的 “name”将不会是 Part document 中新的更新值。

Denormalizing from One -> Many

你还可以将字段从 “One” 到 “Many” 进行 反规范化(denormalize):

> db.parts.findOne()
{
    _id : ObjectID('AAAA'),
    partno : '123-aff-456',
    name : '#4 grommet',
    product_name : 'left-handed smoke shifter',   // Denormalized from the ‘Product’ document
    product_catalog_number: 1234,                     // Ditto
    qty: 94,
    cost: 0.94,
    price: 3.99
}

但是,如果您已经将 Product 名称 反规范化(denormalize)到 Part document 中,那么在更新 Product 名称时,您还必须更新 ‘parts' collection 中出现的所有位置。 这可能是一个更昂贵的更新,因为您正在更新多个零件,而不是单个产品。 因此,在这种方式去规范化时,考虑 读写比( read-to-write ratio ) 显得更为重要。

Intermediate: Denormalizing With “One-To-Squillions” Relationships

你还可以对“one-to-squillions”示例进行 反规范化(denormalize)。 这可以通过两种方式之一来实现: 您可以将关于 “one” side 的信息('hosts’ document)放入“squillions” side(log entries) ,或者将来自 “squillions” side 的摘要信息放入 “one” side。

下面是一个将 反规范化(denormalize)转化为“squillions”的例子。 我将把主机的 IP 地址(from the ‘one’ side)添加到单独的日志消息中:

> db.logmsg.findOne()
{
    time : ISODate("2014-03-28T09:42:41.382Z"),
    message : 'cpu is on fire!',
    ipaddr : '127.66.66.66',
    host: ObjectID('AAAB')
}

你现在查询来自某个特定 IP 地址的最新消息变得更容易了: 现在只有一个查询,而不是两个。

> last_5k_msg = db.logmsg.find({ipaddr : '127.66.66.66'}).sort({time : -1}).limit(5000).toArray()

事实上,如果你只想在 “one” side 存储有限数量的信息,你可以把它们全部 反规范化(denormalize)为 “squillions” side ,从而完全摆脱 “one” collection:

> db.logmsg.findOne()
{
    time : ISODate("2014-03-28T09:42:41.382Z"),
    message : 'cpu is on fire!',
    ipaddr : '127.66.66.66',
    hostname : 'goofy.example.com',
}

另一方面,你也可以 反规范化(denormalize)到 “one” side。 让我们假设你希望在 'hosts’ document 中保留来自主机的最后 1000 条消息。 你可以使用 MongoDB 2.4中引入的 $each / $slice 功能来保持列表排序,并且只保留最后的1000条消息:

日志消息保存在 'logmsg’ collection 中以及 'hosts’ document 中的反规范化列表中: 这样,当消息超出 ‘hosts.logmsgs' 数组时,它就不会丢失。

 //  Get log message from monitoring system
logmsg = get_log_msg();
log_message_here = logmsg.msg;
log_ip = logmsg.ipaddr;
  // Get current timestamp
now = new Date()
  // Find the _id for the host I’m updating
host_doc = db.hosts.findOne({ipaddr : log_ip },{_id:1});  // Don’t return the whole document
host_id = host_doc._id;
  // Insert the log message, the parent reference, and the denormalized data into the ‘many’ side
db.logmsg.save({time : now, message : log_message_here, ipaddr : log_ip, host : host_id ) });
  // Push the denormalized log message onto the ‘one’ side
db.hosts.update( {_id: host_id }, 
        {$push : {logmsgs : { $each:  [ { time : now, message : log_message_here } ],
                           $sort:  { time : 1 },  // Only keep the latest ones 
                           $slice: -1000 }        // Only keep the latest 1000
         }} );

请注意,使用 projection specification({ _id: 1}) 可以防止 MongoDB 通过网络发布整个 ‘hosts’ document。 通过告诉 MongoDB 只返回 _id 字段,我将网络开销减少到仅存储该字段所需的几个字节(再加上一点 wire protocol 开销)。

正如在 “One-to-Many” 的情况下的反规范化一样,你需要考虑读取与更新的比率。 只有当日志消息的频率与应用程序查看单个主机的所有消息的次数相关时,将日志消息反规范化到 Host 文档才有意义。 如果您希望查看数据的频率低于更新数据的频率,那么这种特殊的反规范化是一个坏主意。

回顾

在这篇文章中,我已经介绍了嵌入(embed)子引用(child-reference)父引用( parent-reference)的基础知识之外的其他选择。

  • 如果使用双向引用优化了 Schema,并且愿意为不进行 原子更新(atomic updates)付出代价,那么可以使用双向引用
  • 如果正在引用,可以将数据从 “One” side 到 “N” side,或者从 “N” side 到 “One” side 进行反规范化(denormalize)

在决定是否否否定标准时,应考虑以下因素:

  • 无法对 反规范化(denormalization)的数据执行原子更新(atomic update)
  • 只有当读写比例很高时,反规范化(denormalization)才有意义

下一次,我会给你一些指导方针,让你在所有这些选项中做出选择。

Part 3

这是我们在 MongoDB 中建模 One-to-N 关系的最后一站。 在第一篇文章中,我介绍了建立 One-to-N 关系模型的三种基本方法。 上篇文章中,我介绍了这些基础知识的一些扩展: 双向引用(two-way referencing)反规范化(denormalization)

反规范化(denormalization)允许你避免某些 应用程序级别的连接( application-level joins),但代价是要进行更复杂和昂贵的更新。 如果这些字段的读取频率远高于更新频率,则对一个或多个字段进行 反规范化(denormalization)是有意义的。

那么,我们来回顾一下:

  • 你可以嵌入(embed)、引用(reference)“one” side,或 “N” side,或混合使用这些技术
  • 你可以将任意多的字段反规范化(denormalize)到 “one” side 或 “N” side

特别是反规范化,给了你很多选择: 如果一段关系中有 8 个 反规范化(denormalization)的候选字段,那么有 2 的 8 次方(1024)种不同的方法去反规范化(包括根本不去进行反规范化)。 再乘以三种不同的引用方式,你就有了 3000 多种不同的方式来建立关系模型。

你猜怎么着? 你现在陷入了 “选择悖论” —— 因为你有很多潜在的方法来建立 one-to-N 的关系模型,你选择如何建立模型只是变得更难了。。。

Rules of Thumb: Your Guide Through the Rainbow

这里有一些“经验法则”来指导你进行选择:

  • One:首选嵌入(embedding),除非有足够的的理由不这样做
  • Two:需要独立访问对象是不嵌入对象的一个令人信服的理由
  • Three:数组不应该无限制地增长。 如果在 “many” side 有几百个以上的 documents,不要嵌入它们; 如果在 “many” side 有几千个以上的文档,不要使用一个 ObjectID 引用数组。 高基数数组是不嵌入的一个令人信服的理由
  • Four:不要害怕 应用程序级别的连接(application-level joins): 如果正确地使用索引并使用 projection specifier(如第2部分所示) ,那么 应用程序级别的连接(application-level joins)几乎不会比关系数据库 的 服务器端连接(server-side joins )更昂贵
  • Five:考虑反规范化时的 读/写比率。 一个大多数时候会被读取但很少更新的字段是反规范化的好候选者: 如果你对一个频繁更新的字段进行反规范化,那么查找和更新所有实例的额外工作很可能会超过你从非规范化中节省的开销
  • Six:如何对数据建模完全取决于特定应用程序的数据访问模式。 您希望根据应用程序查询和更新数据的方式对数据进行结构化

Your Guide To The Rainbow

在 MongoDB 中建模 “One-to-N” 关系时,你有各种各样的选择,因此必须仔细考虑数据的结构。 你需要考虑的主要标准是:

  • 这种关系的基数是什么: 是 one-to-few, one-to-many 还是 one-to-squillions
  • 你需要单独访问 “N” side 的对象,还是仅在父对象的上下文中访问?
  • 特定字段的更新与读取的比率是多少?

你的数据结构的主要选择是:

  • 对于 one-to-few,可以使用嵌入文档的数组
  • 对于 one-to-many ,或者在 “N” side 必须单独存在的情况下,应该使用一个引用数组。 如果优化了数据访问模式,还可以在 “N” side 使用 父引用(parent-reference)
  • 对于 one-to-squillions,你应该在存储 “N” side 的文档中使用 父引用(parent-reference)

一旦你确定了数据的总体结构,那么你可以通过将数据从 “One” side 反规范化到 “N” side,或者从 “N” side 反规范化到 “One” side 来反规范化跨多个文档的数据。 只有那些经常被阅读、被阅读的频率远高于被更新的频率的字段,以及那些不需要 强一致性(strong consistency)的字段,才需要这样做,因为更新非标准化的值更慢、更昂贵,而且不是原子的。

Productivity and Flexibility

因此,MongoDB 使你能设计满足应用程序的需求的数据库 Schema。 你可以在 MongoDB 中构造你的数据,让它就可以很容易地适应更改,并支持你需要的查询和更新,以便最大限度地方便你的开发应用程序。

更多资料

欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

image

查看原文

赞 18 收藏 10 评论 3

认证与成就

  • 获得 114 次点赞
  • 获得 14 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-03-31
个人主页被 1.7k 人浏览