依韵_宵音

依韵_宵音 查看完整档案

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

个人动态

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

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

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

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

关注 6 回答 4

依韵_宵音 报名了系列讲座 · 2020-09-23

从0构建私有前端监控系统

> 新增Chrome 85版本开始支持的LCP和CLS指标采集原理和采集方法课时 **为什么要做前端监控** 前端监控是业务生产必不可少的重要技术设施,通过线上数据采集反馈形成业务闭环,能够帮助业务及时发现问题、定位问题、处理问题。出现问题时及时发现,减少业务损失。也可以帮助业务采集用户数据、分析用户画像,更精确了解业务诉求,解决客户问题。 前端监控是重要的业务数据来源,通过数据的角度分析问题,不仅能作为问题分析依据,也可以从统计学角度、行为学角度分析业务,创造更大的业务价值。本课程介绍的是一种简单高效、自主灵活的快速搭建可生产使用的前端监控体系。 **自主监控数据采集** 市面上大部分是结合开源监控做的监控系统,性能和自由度受限。本课程讲解前端监控设计架构,各个监控项采集数据的原理,手把手和你一起编写监控采集脚本,具有自主性和灵活的业务扩展能力。 **打造可视化数据分析大盘搭建** 介绍数据分析思维,讲解如何从数据角度发现、分析、定位问题。结合阿里云SLS日志服务分析监控数据,构建可视化数据分析大盘,分析业务数据。 **实时监控警告系统搭建** 从业务角度出发,基于稳定性和性能体验,构建业务大盘,根据数据情况构建稳定性和体验相关数据监控告警,基于邮件、钉钉、电话等准实时告警到业务,及时发现问题。 **必备技能,面试必问,必要知道的那些事** 前端开发和其他职业一样,总是要分出369等。技术深度和广度,从一些细节就能看出。前端监控也是从高级工程师进阶到专家级别工程师必备的技能。软实力是了解业务场景、技术价值,了解采集各个指标的方法和用处,怎么为业务服务,怎么转化成有用的生产数据。硬技能是能够从0到1,独立自主研发、搭建这么一套监控系统,能够满足业务个性化诉求,解决业务问题。 ### 讲师介绍 **扫地僧 - 高级技术专家** 前阿里、网易前端技术专家,负责流程驱动引擎、可视化搭建、前端发布、构建平台等技术能力建设,带领业务团队,负责整体业务线。 ### 课程大纲 **前端监控架构设计** - 课时1:为什么要做前端监控? - 课时2:前端监控什么数据? - 课时3:前端监控架构设计 **编写监控采集脚本** - 课时4:设计监控上报数据模型 - 课时5:开通 SLS 日志服务 - 课时6:如何通过 webTrack 上报数据 - 课时7:JS 错误和资源采集 - 课时8:接口错误采集 - 课时9:白屏错误统计方法和代码 - 课时10:页面加载时间计算方法和代码实现 - 课时11:FP/FCP/FMP/FID 时间原理介绍 - 课时12:FP/FCP/FMP/FID 时间代码实现 - 课时13:卡顿原理和采集代码实现 - 课时14:TBT 体验指标计算原理 - 课时15:PV 和自定义指标采集 - 课时16:TTI(首屏可交互时间)指标采集 - 课时17:LCP(最大区块渲染时间)计算原理和指标采集 - 课时18:CLS(页面累计位移)计算原理和指标才采集 - 课时19:TBT(首屏总阻塞时间)计算原理和指标才采集 **数据分析大盘** - 课时20:如何做数据查询和可视化 - 课时21:搭建可视化数据大盘 - 课时22:参数化数据查询与大盘构建 **数据监控告警** - 课时23:监控和告警的区别 - 课时24:设置 JS 同步增长告警 - 课时25:告警准确性问题

依韵_宵音 赞了文章 · 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谭老师,欢迎你关注公众号:前端巅峰,后台回复:加群即可加入大前端交流群
查看原文

赞 40 收藏 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

查看原文

赞 304 收藏 250 评论 8

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

Peter谭老师 @jerrytanjinjie

前端架构师

微信公众号:前端巅峰

欢迎技术探讨~

个人微信:CALASFxiaotan

关注 4325

依韵_宵音 赞了文章 · 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

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

基于 HTML5 + WebGL 的太阳系 3D 展示系统

前言

近年来随着引力波的发现、黑洞照片的拍摄、火星上存在水的证据发现等科学上的突破,以及文学影视作品中诸如《三体》、《流浪地球》、《星际穿越》等的传播普及,宇宙空间中那些原本遥不可及的事物离我们越来越近,人们对未知文明的关注和对宇宙空间的好奇达到了前所未有的高度。站在更高的立足点上,作为人类这个物种中的一员,我们理所应当对我们生活的星球、所在的太阳系有一定的认识,对 8 大行星各自的运行轨道、质量、资源存储量甚至是地形有一定的了解。

本系统采用 Hightopo 的 HT for Web 产品来构造轻量化的 3D 可视化场景。

Solar System 这套系统主要用于两种场景:

  1.作为科研成果、新发现的载体,做 3D 太空数据可视化呈现,用于向普通民众科普太阳系的构成、各行星组织结构等知识,可置于博物馆大屏、学校大屏,也可用于互联网产品,作为航空航天类网站的门户页、展示页。

  2.作为宇航局、航空航天相关研究机构的驾驶舱,在 3D 可视化界面中对行星相对位置、星体状态、星体气象、星体地形有一个直观快速的了解,在宇宙空间探索越来越成功的当下,在数据传输技术得到速度和质量上的突破后,甚至可以通过该系统对行星状态做实时监控呈现,对宇航员的作业点、作业情况做在线监控。在配置上人造卫星轨道、监控区域的数据后,本系统可用作卫星系统,描述覆盖范围和呈现观测数据。

预览地址: https://www.hightopo.com/demo/solar-system/

界面简介及效果预览 

主题一:太阳系检测系统

 本系统主要展示8大行星绕太阳公转轨道、相对位置、星体质量、资源含量等信息。

右上角行星按钮会触发视角切换,切换至相对应的行星观测点

this.g3d.flyTo(data, {
    animation: {
        duration: 1000,
        easing: function (t) {
            return (2-t) * t;
        }
    },
    distance: 2000
});

效果:

该主题提供两种视角,鸟瞰和斜视,其它视角可以通过鼠标自行旋转

两种视角的切换由右上角第二、三个圆形按钮触发。

调用 moveCamera 方法重新设置相机位置:

/\*\*
 \* 切换镜头
 \* @param {Number} num - 主题编号
 */
triggerThemeCamera(num) {
    //...
    this.g3d.moveCamera(
        \[ 6742.5, 4625.6, -836.7\],
        \[0, 0, 0\],
        {
            duration: 500,
            easing: function (t) {
                return (2-t) * t;
            }
        }
    );
}

 效果:

信息框默认采用跟随星体一起旋转,这可以达到俯视视角不出现信息框,看起来更清爽。

如果需要查看星体详情,可以通过点击右上角播放按钮,该按钮会触发所有信息框朝向屏幕方向。

通过改变消息面板 shape3d.autorotate 来实现:

setBillboardToCamera(flag) {
    const list = this.dm3d.getDatas();
    list.each( item => {
        if (item instanceof ht.Node) {
            if (/_board$/.test(item.getTag())) {
                if (flag) {
                    item.s('shape3d.autorotate', true);
                }
                else {
                    item.s('shape3d.autorotate', false);
                }
            }
        }
    });
}

效果:

主题二:戴森球星体 3D 拓扑结构

本系统主要展示用户所点选的行星与其它星际物质的相互作用,也可用于展示行星周围卫星的分布情况,以及展示星体间引力、辐射范围等的拓扑结构。

鼠标悬停在一个星体上会触发选中状态,右侧会监控该星体的相关数据。

通过监听 mousemove 后调用 resetPinkOutside 方法,将粉色边框重新设置到悬停的 node 位置:

/\*\*
 \* 重新设置边框
 \* @param node
 */
resetPinkOutside(node) {
    const pinkOutside = this.dm3d.getDataByTag('billboard4');
    pinkOutside.setPosition3d(node.getPosition3d()\[0\],node.getPosition3d()\[1\],node.getPosition3d()\[2\]);
}

效果:

主题三:星体气象、地形检测系统

该主题主要用于呈现在场景二中点选的星体上具体的检测点位,点位周边的等高线在左侧自动生成一个 3D 的地形和闪烁的点位示意,并与右侧的检测点位一一对应。

该功能可用于地形的呈现,也可以用于星体大气层的气象状态展示。

左下角实时监控点位的地质热量、气象流动数据。

点选右侧对应检测点,会触发右侧点的缩放动画,同时左侧对应的 3D 点位也会同步变化,其它的点则调用 setAnimation(null)

setTwinkleToPoints(flag) {
    //...
    if (flag) {
        if (point1_3D && point1) {
            if (this.animationFlags.twinklePointNum === 1) {
                point1_3D.setAnimation({
                    change: {},
                    start: \["change"\]
                });
                point1.setAnimation({
                    width: {},
                    height: {},
                    start: \["width", "height"\]
                });
            } else {
                SolarSystem.disableTwinkle(point1_3D, point1);
            }
        } else {
            SolarSystem.disableTwinkle(point1_3D, point1);
            //...
        }
    }
}

效果:

关联:三个主题(系统)的联动

三个系统是互相关联的,相互切换的方式有三种。

  1.点选左上角的切换按钮:

  左上角部分均为导航栏的响应范围,鼠标悬停时会改变动画控制器 animationFlags 的对应值,触发导航栏落下来,悬停和点选按钮会通过 setImage 方法设置不同的背景

this.g2d.getView().addEventListener('mousemove', event => {
    const node = this.g2d.getDataAt(event);
    let tag = '';
    if (node) {
        tag = node.getTag();
    }
    if('navigator' === tag){
        if(!this.animationFlags.navigatorRotate && this.animationFlags.navAnimationDone){
            this.animationFlags.navAnimationDone = false;
            this.animationControl(0, true);
        }
        this.resetButtonStyle();
    }
    else if (/^navButton/.test(tag)) {
        this.animationFlags.navButtonOnHover = true; // 防止动画过快导致无法点选按钮
        this.resetButtonStyle();
        if (!node.a('buttonOnClick')) {
            node.setImage('buttonOnHover');
        }
    }
    else {
        this.resetButtonStyle();
        this.animationFlags.navButtonOnHover = false;
        if(this.animationFlags.navigatorRotate && this.animationFlags.navAnimationDone){
            setTimeout(() => {
                if(!this.animationFlags.navButtonOnHover){
                    this.animationFlags.navButtonOnHover = true;
                    this.animationFlags.navAnimationDone = false;
                    this.animationControl(0, false);
                }
            }, 500);
        }
    }
}, false);

效果:

  2.点击最下方的标尺栏,分别对应 3 个模块:

   3.点选主题一中的行星跳转到的主题二的拓扑结构,点选主题二的星体跳转主题三的地形,主题三无法向前关联,只能通过前两种方式进行跳转:

总结:

该系统使用轻量高效的 ht 库,矢量平面信息与 3D 对象进行关联,并采用 3D 拓扑可视化呈现,相对位置清晰直观,3D 地形与等高线图对应,海拔高度和相互遮挡关系都可以准确把握。

该系统满足了最基本的太空场景和数据呈现的框架,更为详尽的数据呈现和业务功能有待相关的工作人员根据具体的业务场景提出更详尽的需求。

查看原文

赞 27 收藏 13 评论 7

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

GitHub 吸星大法 - 一年收获 2000+ Star 的心得

1. 前言

笔者做前端开发这些年,几乎每天都会刷 GitHub,也时不时在上面分享博客和做一些开源项目,也算是 GitHub 的重度使用者了,其中也掌握了一定的技巧,并在一年内收获了 2000+ Star。

因为有读者问过我,想知道我在 GitHub 上做开源项目并获得 2000+ Star 的心得,所以笔者在此分享一下这过程的一些经验与心得,算是给那些关注了我的读者的福利。

2. 为什么要经营好你的 GitHub ?

GitHub 可以说是你的技术名片,你在 GitHub 的贡献可以作为简历的加分项。

据我所知,对于技术岗位,猎头在找候选人的诸多方法中,有一条就是通过 GitHub 来找技术比较好的候选人的,如果你的 GitHub 经营得很好,开源项目收获的 Star 比较多,一般都会为你提供一些好的机会。

为什么笔者知道 ?因为 ta 们找过笔者,所以我知道,哈哈哈。

而且如果某个公司的团队负责人看到你的 GitHub,觉得你的技术不错,也会给你抛来招揽的橄榄枝。这种情况,笔者也遇到过,哈哈哈。

笔者也是最近裸辞并换了工作,最近在找工作过程中,笔者知道了:想通过社招获得好工作或者进大厂,一般都要有如下 4 点中的 1 - 2 个亮点才行。

  • 高学历,名校毕业
  • 工作年限足,经验丰富(但不是 1 年经验当 5 年用那种)
  • 有开源与影响力,GitHub 的贡献或者经常写优质博客
  • 本身就有大厂的工作经历

大多数人都是普通人,平时所做工作几乎都是写业务而已,那么只有你具备 1 - 2 个亮点,HR 或者面试官 在筛选简历时,才会选中你,或者好机会才会自动找上你。

找工作时,我简历中的亮点就是 GitHub 的贡献,在开源与影响力的一栏中,我是这样写的:

开源与影响力

  • GitHub: https://github.com/biaochenxuying
  • 本人有 写技术博客和做开源项目 的习惯,乐于分享,坚持写博客和做开源项目的时间长达 一年半
  • 利用业余时间开源和维护了 10 个个人项目,有 博客文章、Vue 源码的思维导图、Vue 版的博客网站前台、React 管理后台、Express 后台、还有一些 js 轮子。
  • GitHub 上总共收获 2000+ Star,500+ Fork ,570+ Followers;超过 100 star 的项目有 6 个,超过 500 star 的项目有 1 个。

如果没有这个亮点,估计在这互联网寒冬期间,笔者也很难有好公司的面试机会或者找到工作啊。

3. 如何经营好你的 GitHub ?

你能为他人提供什么样的价值。

想收获到很多小星星,那你首先要想的是:你能为他人提供什么样的价值

就笔者来说,笔者在 GitHub 上为他人提供的价值有:

  • 写的博客文章,他人可以从中吸取到 经验、知识点,或者思维得到提升;
  • 把相关知识总结成思维导图,分享出来,他人可以直接学习;
  • 把根据自己的兴趣,做了个博客网站,并把源码分享出来,并做了开源,别人可以直接用;
  • 自己工作中造的一些轮子,也分享出来,他人可以直接用。

总之,原则就是:你能提供的价值越大越多,收获到的小星星就会越多

3.1 写博客文章

至于为什么要写博客,我就不说了,很多大神已经写过了,可以参考一下几个大佬们写的 我为什么要写博客 ?

笔者只想说,只要你开始了写博客之路,那基本就是一条一去不回头的路了。因为笔者就是这样,而且我看到很多写博客的人也是这样。

还有就是最好用 markdown 语法来写作,也可以参考阮一峰写的 中文技术文档的写作规范,这样可以更加关注内容本身,而不是样式,多个平台也可以发布。

而且写作这是非常重要的一环,因为后面介绍的方法,多多少少都依赖于写作。

笔者专门在 GitHub 上创建了一个 blog 仓库来写文章的,也是目前笔者收获最多 Star 的开源项目,而且布局和风格什么的,都是比较正规的。如果你也想创建个仓库专门来写文章的,可以参考我这个 blog 项目。

3.2 做开源项目

可能你觉得自己的代码写的不好,没有什么流弊的功能,不敢开源代码之类的,这想法也没错,但你要知道,大神都是从小白过来的,每个人都有是小白的时候

而且后来者从来都不缺,很多时候,你的分享主要是对那些后来者有用而已;更何况,比你厉害的人可能会指出你分享中的错误或者改进的地方,也是能促进你的进步的。

这个开源项目类型可以是很多种的,有造轮子的、写插件的、高仿某个 app 或者网站的、用某些技术写个通用模版的、总结知识做成思维导图的、提供某个功能的 等等。

虽然类型那么多,最主要的是:要根据自身的兴趣和平时日常工作来选择要做哪种类型的开源项目

笔者因为平时有写博客,所以想做个自己的个人网站,专门来展示自己的文章的,而且当时想学习 react 和 node ,所以做了个网站的项目并开源了,包含 前台展示管理后台后台

还有一些开源项目是笔者在工作中造的轮子或者插件(ps:如果是公司的机密项目的轮子、插件之类,又或者公司声明了不能把代码外传的,不要随意开源哦)。

我是这样想的:既然自己有这样的需求(比如:做个自己的个人网站需求),那么同理,其他人可能也有这个需求的,所以我做好功能并开源,对他人就可能有帮助。

我开源了之后,也的确给不少人提供了经验或者帮助,因为这个项目,笔者收获了很多的小星星。而且很多人是伸手党来的,你做好了,别人可以直接用,多方便啊。

还有一个项目就是 vue + typescript 版的博客前台展示,当时我已经写了一版 react 版的前台展示了,为什么还写一版 vue 版的呢 ?因为我想学习 typescirpt,所以想在结合 vue 来实践一下,而工作中还没用得上,所以又把我的网站前台展示用 vue + typescript 用了一版。

而且当时 typescript 加 vue 的开源项目还很少的,连相关的博客都少,我想参考一下别人的项目,但是没有啊,所以当时也踩了很多坑。所以我想:我如果开源了的话,肯定很多人会参考我这个项目的,也会带来一定的流量,所以能收获不少的 star 。也的确是这样,这个项目也是我目前的完整项目中最多 star 的一个。

有一点要注意的是:一个人的精力与业余时间是非常有限的。如果是一个人的话,做的开源项目不要太多吧,维护好一个开源项目是很需要时间的,维护多个项目所需要的时间就更多了

你以为开源了就行了吗 ?太天真了。

那要写 README.md 来介绍你开源的项目的,比如一般要有如下内容:

  • 简介:简单说明一下这个项目是干嘛的
  • 结果:这个项目的代码达到了什么效果
  • 步骤:怎么运行你这个项目,或者怎么使用你写的插件。
  • 文章:详细讲解这个项目(可无,最好有)

有了这个 README.md 之后,别人一看到你的项目的 github 就知道这个项目的情况了。

3.3 硬核为王

以做好一个伟大的产品的心态来做开源项目。

做开源项目说白了就是做一个产品,我们要以做好一个产品的心态来做开源项目,这样你的产品质量才会更优,才会够硬核,也就是有料。

我做这个博客网站的时候是有这个意识的,做完第一版之后,也在不断的迭代和完善。

就我做成的成果来看,其实还不够硬核,因为还有一些优化的点和实用的功能的,只是我还没做。

目前,笔者比较遗憾的是:还没有一个达到 1000+ Star、甚至 10000+ Star 的硬核开源项目。以后技术更精进了,或者有好想法了,再开源一个好的开源项目吧。

我知道的一个比较硬核的开源项目是这个:支持自定义样式的 Markdown 编辑器,这个项目就是以一个产品的理念来做的,作者也在不断的迭代和完善。而且更新的速度很快,也很规范。

当然你也可以参考那些做得很出名的开源项目,毕竟做得那么成功,肯定有其原因。

3.4 时间与坚持

做开源项目是很需要时间的。

比如笔者做的博客网站项目就用了 2 个多月的业余时间来做,还好公司的正常的上班时间是 965 的,平时上班只需要 7 个钟,加班的情况比较少,所以业余时间比较多。

但利用业余时间做开源项目时,我的每天真实工作时间可以说是 9117 或者 907,因为晚上下班了,我都会用 2 - 3 个钟来做开源项目,周末的两天也是这样,而且周一到周五的中午吃完饭时,我也会挤出大概 30 - 40 分钟的时候来学习相关的技术,或者做开源项目。

这样习惯了大概两个月之后,终于把网站的第一版撸了出来。

所以时间很重要,没有时间你就做不出好的开源项目。

而且这是一直坚持的结果,如果中途觉得累了,可能就放弃了。

如果你问我难道不觉得累吗,其实我很少觉得累,因为是做自己喜欢的事,兴致比较高,再加上平时有锻炼身体,所以不累。

当然,如果你的工作时间是 996 的,可能没那么多时间了,最好是开源一些工作中开发好的插件或者特定功能的轮子之类的。

3.5 推广自己的项目

有才华很重要,让别人知道你的才华更重要。

酒好也怕巷子深。

当你做好你的开源项目之后,你以为就会有人给你小星星了,那你就太天真了。

想收获小星星,还要自己去技术社区推广的,不然没人知道你的项目,现在这个时代,流量为王,这一点对于开源项目也是一样的,人来了,了解到你的项目,才有可能给你小星星。

而且要推广就要脸皮厚,这叫做自我营销。

所以要写文章介绍你的开源项目,文章的要点主要是突出 效果与功能

然后就是 宣传 了,到各大技术社区(比如:思否、掘金 等)去发布你的文章,达到引流的目的。

如果想知道怎么写推广的文章,可以参考我写的这两篇文章: react + node + express + ant + mongodb 的简洁兼时尚的博客网站Vue + TypeScript + Element 项目实践(简洁时尚博客网站)及踩坑记

4. 总结

笔者觉得想做好开源项目,最重要因素是兴趣,不然你可能中途就放弃了,很难坚持到把项目做完和做好。

有时候,有很强的功利心(比如 为了钱、为了名)也是好事,这可是你的一大助力,是可以推动你做完你想做的事的。

最后,要掌握 GitHub 吸星大法,先从写作开始,从现在开始。

推荐阅读

GitHub 上能挖矿的神仙技巧 - 如何发现优秀开源项目,估计很多人都不知道的技巧,甚至很多高级工程师都不知道。

查看原文

赞 19 收藏 8 评论 4

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

如何编写高质量的 JS 函数(3) --函数式编程[理论篇]

本文首发于 vivo互联网技术 微信公众号 
链接:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ
作者:杨昆

 【编写高质量函数系列】中,

《如何编写高质量的 JS 函数(1) -- 敲山震虎篇》介绍了函数的执行机制,此篇将会从函数的命名、注释和鲁棒性方面,阐述如何通过 JavaScript 编写高质量的函数。

 《如何编写高质量的 JS 函数(2)-- 命名/注释/鲁棒篇》从函数的命名、注释和鲁棒性方面,阐述如何通过 JavaScript编写高质量的函数。

【 前 言 】

这是编写高质量函数系列文章的函数式编程篇。我们来说一说,如何运用函数式编程来提高你的函数质量。

函数式编程篇分为两篇,分别是理论篇和实战篇。此篇文章属于理论篇,在本文中,我将通过背景加提问的方式,对函数式编程的本质、目的、来龙去脉等方面进行一次清晰的阐述。

写作逻辑

通过对计算机和编程语言发展史的阐述,找到函数式编程的时代背景。通过对与函数式编程强相关的人物介绍,来探寻和感受函数式编程的那些不为人知的本质。

下面列一个简要目录:

一、背景介绍

  1. 计算机和编程语言的发展史

二、函数式编程的 10 问

  1. 为什么会有函数式语言?函数式语言是如何产生的?它存在的意义是什么?
  2. lambda 演算系统是啥?lambda 具体说的是啥内容?lambda 和函数有啥联系?为啥会有 lambda 演算系统?
  3. 函数式编程为什么要用函数去实现?
  4. 函数式语言中,或者在函数式编程中,函数二字的含义是什么?它具备什么能力?
  5. 函数式编程的特性关键词有哪些?
  6. 命令式和函数式编程是对立的吗?
  7. 按照 FP 思想,不能使用循环,那我们该如何去解决?
  8. 抛出异常会产生副作用,但如果不抛出异常,又该用什么替代呢?
  9. 函数式编程不允许使用可变状态的吗?如何没有副作用的表达我们的程序?
  10. 为什么函数式编程建议消灭掉语句?

三、JavaScript 函数式编程的 5 问

  1. 为什么函数式编程要避免使用 this
  2. JavaScript 中函数是一等公民, 就可以得出 JavaScript 是函数式语言吗?为什么说 JS 是多态语言?
  3. 为什么 JS 函数内部可以使用 for 循环吗?
  4. JS 函数是一等公民是啥意识?这样做的目的是啥?
  5. 用 JS 进行函数式编程的缺点是什么?

四、总结

  1. 函数式编程的未来。
简要目录介绍完啦,大家请和我一起往下看。

PS:我好像是一个在海边玩耍的孩子,不时为拾到比通常更光滑的石子,或更美丽的贝壳而欢欣鼓舞,而展现在我面前的是完全未探明的的真理之海。

【 正 文 】

计算机和编程语言的发展史

计算机和编程语言的发展史是由人类主导的,去了解在这个过程中起到关键作用的人物是非常重要的。

下面我们一起来认识几位起关键作用的超巨。

一、戴维·希尔伯特

点击图片介绍: 戴维·希尔伯特

希尔伯特被称为数学界的无冕之王 ,他是天才中的天才。

在我看来,希尔伯特最厉害的一点就是:他鼓舞大家去将证明过程纯机械化,因为这样,机器就可以通过形式语言推理出大量定理。

也正是他的坚持推动,形式语言才逐渐走向历史的舞台中央。

二、艾伦·麦席森·图灵

点击图片介绍: 艾伦·麦席森·图灵

艾伦·麦席森·图灵被称为计算机科学之父。

我认为,他最伟大的成就,就是发明了图灵机:

上图所示,就是图灵机的模型图。

这里我们注意一点:从图中,我们会发现,每个小方格可存储一个数字或者字母。这个信息非常重要,大家可以思考一下。

PS: 等我介绍 冯·诺依曼 的时候,就会明白它们之间的联系。

三、阿隆佐·邱奇

点击图片介绍: 阿隆佐·邱奇

阿隆佐·邱奇,艾伦·麦席森·图灵的博导。

他最伟大的成就,就是:发明了 λ(lambda) 演算。

如上图,就是 λ(lambda) 演算的基本形式。

阿隆佐·邱奇发明的 λ演算和图灵发明的图灵机,一起改写了当今世界,形式语言的历史。

思考: 邱奇的 λ演算 和图灵的图灵机,这两者有什么区别和联系?

四、冯·诺依曼

点击图片介绍: 冯·诺依曼

冯·诺依曼被称为计算机之父。

他提出了冯·诺依曼体系结构:

从上图,我们可以看出:冯·诺依曼体系结构由运算器、控制器、存储器、输入设备、输出设备五个部分组分组成。采用二进制逻辑,程序存储、执行作为计算机制造的三个原则。

注意一个信息:我们知道,计算机底层指令都是由 0 和 1 组成,通过对 0 和 1 的 CRUD ,来完成各种计算操作。我们再看图灵机,会发现其每个小方格可存储一个数字或者字母。

看到这,是不是发现冯·诺依曼体系结构和图灵机有一些联系。

是的,现冯·诺依曼体系结构就是按照图灵机的模型来实现的计算机结构。计算机的 0 和 1 ,就是图灵机的小方格中的数字或者字母的特例。

五、为什么要提这些人

因为如果想彻底解开函数式编程的困惑,那就必须要去了解这时代背景和关键人物的一些事迹。

六、说一说 邱奇-图灵论题

邱奇是图灵的博士生导师,他们之间有一个著名的论题,那就是 邱奇-图灵论题 。

论题大致的内容是:图灵和 lambda 这两种模型,有没有一个模型能表示的计算,另一个模型表示不了呢?

到目前为止,这个论题还没有答案。也正因为如此,让很多人对 lambda 模型充满了信心。后面的岁月中,lambda 模型一直在被很多人研究、论证、实践。

七、第一台可编程计算机的诞生

它叫 ENAIC

1946 年,世界上第一台电子计算机—— ENIAC 问世,它可以改变计算方式,即可以更改程序。

也就是说:它是一台可编程计算机。

八、为什么要可编程

perl 语言的设计者 Larry Wall 说过:优秀的程序员具有三大美德:懒惰、急躁、傲慢。

可编程完美诠释了懒惰的美德。在 ENAIC 诞生后,出现了各种各样的 程序设计语言。三大美德也提现的淋漓尽致。

九、计算机语言的分类

上图可以获得以下信息:

  1. 程序设计语言只是计算机语言的一个分类。
  2. HTML 、XML 是数据设计语言。
  3. 在程序设计语言中,分为说明式和声明式。
  4. 在说明式中,又包含函数式、逻辑式等。其实 MySQL,就是逻辑式语言,它通过提问的方式来完成操作。
  5. 冯诺依曼体系更符合面向过程的语言。
这个分类可以好好看看,会有一些感受的。

十、简单的程序设计语言发展史

上图非常简单明了,直到 1995 年。

时间线大概是这样的:xxx ---> xxx ---> .... ---> JavaScript ...

时间来到了 1996 年,JavaScript 诞生了!

十一、JavaScript 诞生了!

1、JavaScript 之父——布兰登·艾奇

图中这位老哥叫布兰登·艾奇 。那一年,他34岁。

2、看一看阮一峰写的一段话

从上图中你会有如下几点感受:

  1. 第一个感受:阿布对 Java 一点兴趣也没有。
  2. 第二个感受:由于讨厌 Java ,阿布不想用 Java 的对象表示形式,于是就借鉴了 Self 语言,使用基于原型的继承机制。埋下了前几年前端界用原型进行面对对象编程的种子。
  3. 第三个感受:阿布借鉴了 Scheme 语言,将函数提升到一等公民的地位,让 JS 拥有了函数式编程的能力。埋下了 JS 可以进行函数式编程的种子。
  4. 第四个感受:JS 是既可以函数式编程,也可以面对对象编程。

3、我个人的感受

我在回顾程序设计语言的发展史和一些故事后,我并不认为 JavaScript 是一个烂语言,相反正是这种中庸之道,才使得 JavaScript 能够流行到现在。

十二、总结

通过对计算机语言的发展史和关键人物的简洁介绍,我们可以从高层面去体会到函数式编程在计算机语言发展史中的潜力和影响力。

不过,通过背景和人物的介绍,对函数式编程的理解还是有限的。下面我将通过提问的方式来阐述函数式编程的来龙去脉。

函数式编程的 10 问

下面将通过 10 个问题的解答,来阐述函数式编程的理论支撑、函数式编程的诞生背景、函数式编程的核心理论以及推导等知识。

一、为什么会有函数式语言?函数式语言是如何产生的?它存在的意义是什么?

函数式语言的存在,是为了实现运算系统的本质——运算。

1、形式化运算系统的研究

计算机未问世之前,四位大佬 阿兰·图灵、约翰 ·冯·诺依曼 、库尔特 ·哥德尔 和阿隆左 ·丘奇。展开了对形式化的运算系统的研究。

通过形式系统来证明一个命题:可以用简单的数学法则表达现实系统。

2、图灵机和冯·诺依曼结构体系的缺陷

从上文的图片和分析可知,图灵机和冯诺依曼体系的计算机系统都依赖存储(内存)进行运算。

换句话说就是:通过修改内存来反映运算的结果。并不是真正意义上的运算。

修改内存并不是我们想要的,我们想要的仅仅是运算。从目的性的角度看,修改内存可以说是运算系统中的副作用。或者说,是表现运算结果的一种手段。

这一切,图灵的博导邱奇看在眼里,他看到了问题的本质。为了实现运算系统的本质——运算,即不修改内存,直接通过运算拿到结果。

他提出了 lambda 演算的形式系统,一种更接近于运算才是本质的理论。

3、函数式语言和命令式语言的隔阂

从语言学分类来说:是两种不同类型的计算范型。

从硬件系统来说:它们依赖于各自不同的计算机系统(也就是硬件)。为什么依赖不同的硬件,是因为如果用冯诺依曼结构的计算机,就意味着要靠修改内存来实现运算。但是,这和 lambda 演算系统是相矛盾的。

因为基于 lambda 演算系统实现的函数式语言,是不需要寄存器的,也不存在需要使用寄存器去存储变量的状态。它只注重运算,运算结束,结果就会出来。

最大的隔阂就是依赖各自不同的计算机系统 。

4、计算机硬件的限制

目前为止,在技术上做不到基于 A 范型的计算机系统,同时支持 B 范型。也就是说,不能指望在 X86 指令集中出现适用于 lambda 演算 的指令、逻辑或者物理设计。

你可能会疑问,既然硬件不支持,那我们为什么还能进行函数式编程?

其实现实中,大多数人都是用的冯诺依曼体系的命令式语言。所以为了获得特别的计算能力和编程特性。语言就在逻辑层虚拟一个环境,也因为这样,诞生了 JS 这样的多范型语言,以及 PY 这种脚本语言。

究其根源,是因为,冯·诺依曼体系的计算机系统是基于存储与指令系统的,并不是基于运算的。

5、黑暗中的曙光

在当时硬件设备条件的限制下,邱奇提出的 lambda 演算,在很长时间内,都没有被程序设计语言所实现。

直到冯诺依曼等人完成了 EDVAC 的十年之后。一位 MIT 的教授 John McCarthy 对邱奇的工作产生了兴趣。在 1958 年,他公开了表处理语言 LISP 。这个 LISP 语言就是对邱奇的 lambda 演算的实现。

自此,世界上第一个函数式语言诞生了。

LISP 就是函数式语言的鼻祖,完成了 lamda 演算的实现,实现了 运算才是本质的运算系统

上图是 Lisp 的图片,感受一下图片符号的魅力。

为什么我说是曙光?

是因为,并没有真正的胜利。此时的 LISP 依旧是工作在冯·诺依曼计算机上,因为当时只有这样的计算机系统。

所以从 LISP 开始,函数式语言就是运行在解释环境而非编译环境中的。也就是传说中的脚本语言,解释器语言。

6、真正的胜利

直到 1973 年,MIT 人工智能实验室的一组程序员开发了,被称为 LISP 机器的硬件。自此,阿隆左·丘奇的 lambda 演算终于得到了 硬件实现。终于有一个计算机(硬件)系统可以宣称在机器指令级别上支持了函数式语言。

7、总结

关于这问,我阐述了很多,从函数式语言诞生的目的、到函数式语言诞生的艰难过程、再到计算机硬件的限制。最后在不断的努力下,做到了既可以通过解释器,完成基于冯·诺依曼体系下,计算机系统的函数式编程。也可以在机器指令级别上支持了函数式语言的计算机上进行纯正的函数式编程。

思考题:想一想,在如今,函数式编程为什么越来越被人所了解和掌握。

二、lambda 演算系统是啥?lambda 具体说的是啥内容?lambda 和函数有啥联系?为啥会有 lambda 演算系统

1、lamda 诞生的目的

lambda 是一种解决数学中的函数语义不清晰,很难表达清楚函数的结构层次的问题的运算方案。

也就是在运算过程中,不使用函数中的函数运算形式,而使用 lambda 的运算形式来进行运算。

2、lamda 简单介绍

(1)一套用于研究函数定义、函数应用和递归的系统。

(2)函数式语言就是基于 lambda 运算而产生的运算范型。

3、函数式编程的理论基石

lambda 演算系统是学习函数式编程的一个非常重要的知识点。它是整个函数式编程的理论基石。

4、数学中的函数

如下图所示:

从上面的数学函数中,我们可以发现以下几点:

  1. 没有显示给出函数的自变量
  2. 对定义和调用区分不严格。x2-2*x+1 既可以看成是函数 f(x) 的定义,又可以看成是函数 g(x) 对变量 x-1 的调用。

体会上面几点,我们会发现:数学中的函数语义并不清晰,它很难表达清楚函数的结构层次。对此,邱奇给出了解决方法,他提出了 lambda(λ) 演算。

5、lambda(λ) 演算

基本定义形式:λ<变量>.<表达式>

通过这种方法定义的函数就叫 λ(lambda) 表达式。

我们可以把 lambda 翻译成函数,即可以把 lambda 表达式念成函数表达式。

PS: 这里说一下,函数式语言中的函数,是指 lambda(函数),它和我们现在的通用语言中,比如 C 中 的 function 是不同的两个东西。

6、单个变量的栗子

λx.x2-2*x+1 就是一个 λ 表达式,其中显式地指出了 x 是变量。将这个 λ 表达式定义应用于具体的变量值时,需要用一对括号把表达式括起来,当 x 是 1 时,如下所示:

(λx.x2-2*x+1)1

应用(也就是调用)过程,就是把变量值赋值给表达式中的 x ,并去掉 λ <变量>,过程如下:

(λx.x2-2*x+1)1=1-2*1+1=0

7、多个变量的栗子

表达式 λx.λy.x+y 中,有两个变量 分别为 x 和 y。

当 x=1, y=2 表达式调用过程如下:

((λx.λy.2*x+y)1)2 = (λy.2+y) 2 = 4

从上面,我们可以看到,lambda 表达式的调用中,参数是有执行顺序的,能感受到柯里化和组合的味道。

也就是说,由于函数就是表达式,表达式就是值。所以函数的返回值可以是一个函数,然后继续进行调用执行,循环往复。

这样,不同函数的层次问题也解决了,这里用到了高阶函数。在函数式编程语言中,当函数是一等公民时,这个规律是生效的。

8、总结

说到这,大家从根本上对函数式编程有了一个清晰的认知。比如它的数学基础,为什么存在、以及它和命令式语言的本质不同点。

lambda 演算系统 证明了:任何一个可计算函数都能用这种形式来表达和求值,它等价于图灵机。

至此,我阐述了函数式语言出现的原因。以及支持函数式语言的重要理论支撑 —— lambda 演算系统的由来和基本内容。

函数式编程为什么要用函数去实现

上文提到过,运算系统的本质是运算。

函数只是封装运算的一种手段,函数并不是真正的精髓,真正的精髓在于运算。

一、总结

说到这,大家从根本上对函数式编程有了一个清晰的认知。比如它的数学基础,为什么存在、以及它和命令式语言的本质不同点。

二、函数式语言中,或者在函数式编程中,函数二字的含义是什么?它具备什么能力?

1、函数二字的含义

这个函数是特指 满足 lambda 演算的 lambda 表达式。函数式编程中的函数表达式,又称为 lambda 表达式。

该函数具有四个能力:

  1. 可以调用
  2. 是运算元
  3. 可以在函数内保存数据
  4. 函数内的运算对函数外无副作用

2、运算元

在 JS 中,函数也是运算元,但它的运算只有调用。

3、函数内部保存数据

闭包的存在使得函数内保存数据得到了实现。函数执行,数据存在不同的闭包中,不会产生相互影响,就像面对对象中不同的实例拥有各自的自私有数据。多个实例之间不存在可共享的类成员。

4、总结

从这问可以知道,并不是一个语言支持函数,这个语言就可以叫做函数式语言,或者说就具有函数式编程能力。

三、函数式编程的特性关键词有哪些?

大致列一下:

引用透明性、纯洁性、无副作用、幂等性、惰性求值/非惰性求值、组合、柯里化、管道、高阶性、闭包、不可变性、递归、partial monad 、 monadic 、 functor 、 applicative 、尾递归、严格求值/非严格求值、无限流和共递归、状态转移、 pointfree 、一等公民、隐式编程/显式编程等。

1、引用透明性

定义:任何程序中符合引用透明的表达式都可以由它的结果所取代,而不改变该程序的含义。

意义:让代码具有得到更好的推导性、可以直接转成结果。

举个例子:比如将 TS 转换成 JS 的过程中,如果表达式具备引用透明性。那么在编译的时候,就可以提前把表达式的结果算出来,然后直接变成值,在 JS 运行的时候,执行的时间就会降低。

2、纯洁性

定义:对于相同的输入都将返回相同的输出。

优点:

  1. 可测试
  2. 无副作用
  3. 可以并行代码
  4. 可以缓存

3、惰性求值与非惰性求值

定义:如果一个参数是需要用到时,才会完成求值(或取值) ,那么它就是惰性求值的。反之,就是非惰性求值。

(1)惰性求值:

true || console.log('源码终结者')

特点:当不再需要后续表达式的结果的时候,就终止后续的表达式执行,提高了速度,节约了资源。

(2)非惰性求值:

let i = 200
console.log(i+=20, i*=2, 'value: ' + i)
console.log(i)

特点:浪费 cpu 资源,会存在不确定性。

4、pointfree——隐式编程

函数无需要提及将要操作的数据是什么。也就是说,函数不用指明操作的参数,而是让组合它的函数来处理参数。

通常使用柯里和组合来实现 pointfree。

5、组合

(1)没有组合的情况:

(2)组合后的情况:

具体的看我后面的实战篇,我会通过例子来介绍组合的作用。

6、柯里化

7、函数式编程的高级知识点——Functor 、Applicative 、Monad

点击图片:Typescript版图解Functor , Applicative 和 Monad

这些高级知识点,随便一个都够解释很长的,这里我就不做解释了。我推荐一篇文章,阐述的非常透彻。

对于这三个高级知识点,我有些个人的看法。

  1. 第一个:不要被名词吓到,通过敲代码去感受其差异性。
  2. 第二个:既然要去理解函数式语言的高级知识,那就要尽可能的摆脱命令式语言的固有思想,然后再去理解这些高级知识点。
  3. 第三个:为什么函数式编程中,会有这些高级知识点?

关于第三个看法,我个人的感受就是:函数式编程,需要你将隐式编程风格改成显式风格。这也就意味着,你要花很多时间在函数的输入和输出上。

如何解决这个问题?

可以通过上述的高级知识点来完成,在特定的场景下,比如在 IO 中,不需要列出所有的可能性,只需要通过一个抽象过程来完成所有情况的处理,并保证不会抛出异常。

它们都是为了一个目的,减少重复代码量,提高代码复用性。

8、总结

此问,我没有详细回答。我想说的是:

这些特性关键词,都值得认真研究,这里我只介绍了我认为该注意的点,具体的知识点,大家自行去了解和研究。

四、命令式编程和函数式编程是对立的吗?

从前面提到的一些阐述来看,命令式编程和函数式编程不是对立的。它们既可以独立存在,又可以共生。并且在共生的情况下,会发挥出更大的影响力。

我个人认为,在编程领域中,多范式语言才是王道,单纯只支持某一种范式的编程语言是无法适应多场景的。

五、按照 FP 思想,不能使用循环,那我们该如何去解决?

对于纯函数式语言,无法使用循环。我们能想到的,就是使用递归来实现循环,回顾一下前面提到的 lamda 演算系统,它是一套用于研究函数定义、函数应用和递归的系统。所以作为函数式语言,它已经做好了使用递归去完成一切循环操作的准备了。

六、抛出异常会产生副作用,但如果不抛出异常,又该用什么替代呢?

说到这,我们需要转变一下观念:比如在命令式语言中,我们通常都是使用 try catch 这种来捕获抛出的异常。但是在纯函数式语言中,是没有 try catch 的,通常使用函子来代替 try catch 。

看到上面这些话,你可能会感到不能理解,为什么要用函子来代替 try catch 。

其实有困惑是很正常的,主要原因就是:我们站在了命令式语言的理论基石上去理解函数式语言。

如果我们站在函数式语言的理论基石上去理解函数式语言,就不会感觉到困惑了。你会发现只能用递归实现循环、没有 try catch 等要求,是合理且合适的。

PS: 这就好像是一直使用函数式语言的人突然接触命令式语言,也会满头雾水的。

七、函数式编程不允许使用可变状态的吗?如何没有副作用的表达我们的程序?

可以使用局部的可变状态,只要该局部变量不会影响外部,那就可以说改函数整体是没有副作用的。

八、为什么函数式编程建议消灭掉语句?

因为语句的本质是:在于描述表达式求值的逻辑,或者辅助表达式求值。

JavaScript 函数式编程的 5 问

一、为什么函数式编程要避免使用this

主要有以下两点原因:

  1. JS 的 this 有多种含义,使用场景复杂。
  2. this 不取决于函数体内的代码。

    所有的数据都应以参数的形式提供给函数,而 this 不遵守这种规则。

二、为什么JS函数内部可以使用for循环吗?

很多人可能没有想过这个问题

其实在纯函数式语言中,是不存在循环语句的。循环语句需要使用递归实现,但是 JS 的递归性能并不好,比如没有尾递归优化,那怎么办呢?

为了能支持函数式编程,又要避免 JS 的递归性能问题。最后允许了函数内部可以使用 for 循环,你会看到 forEach 、 map 、 filter 、 reduce 的实现,都是对 for 循环进行了封装。内部还是使用了 for 循环。

PS: 在 JS 中,只要函数内的 for 循环不影响外部,那就可以看成是体现了纯洁性。

三、JS函数是一等公民是啥意识?这样做的目的是啥?

我总结了一下,大概有以下意识:

  1. 能够表达为匿名的直接量
  2. 能被变量存储
  3. 能被其它数据结构存储
  4. 有独立而确定的名称(如语法关键字)
  5. 可比较的
  6. 可作为参数传递
  7. 可作为函数结果值返回
  8. 在运行期可创建
  9. 能够以序列化的形式表达
  10. 可(以自然语言的形式)读的
  11. 可(以自然语言能在分布的或运行中的进程中传递与存储形式)读的

1、序列化的形式表达

这个是什么意识呢?

在 js 中,我们会发现有 eval 这个 api 。正是因为能够支持以序列化的形式表达,才能做到通过 eval 来执行字符串形式的函数。

2、总结

JS 之父设计函数为一等公民的初衷就是想让 JS 语言可以支持函数式编程。

函数是一等公民,就意味着函数能做值可以做的任何事情。

四、在JS中,如何做到函数式编程?

核心思想:通过表达式消灭掉语句。

有以下几个路径:

  1. 通过表达式消灭分支语句 举例:单个 if 语句,可以通过布尔表达式消灭掉
  2. 通过函数递归消灭循环语句
  3. 用函数去代替值(函数只有返回的值在影响系统的运算,一个函数调用过程其实只相当于表达式运算中的一个求值)

五、用 JS 进行函数式编程的缺点是什么?

  • 缺少不可变数据结构( JS 除了原始类型,其他都是可变的)
  • 没有提供一个原生的利于组合函数而产生新函数的方式,需要第三方支持
  • 不支持惰性序列
  • 缺少尾递归优化
  • JS 的函数不是真正纯种函数式语言中的函数形式(比如 JS 函数中可以写循环语句)
  • 表达式支持赋值

1、缺少尾递归优化

对于函数式编程来说,缺少尾递归优化,是非常致命的。就目前而言,浏览器对尾递归优化的支持还不是很好。

什么是尾递归?

如下图所示: 

我们来看下面两张图:

第一张图,没有使用尾递归,因为 n * fatorial(n - 1) 是最后一个表达式,而 fatorial(n - 1) 不是最后一个表达式。第二张图,使用了尾递归,最后一个表达式就是递归函数本身。

问题来了,为什么说 JS 对尾递归支持的不好呢?

这里我想强调的一点是,所有的解释器语言,如果没有解释环境,也就是没有 runtime ,那么它就是一堆文本而已。JS 主要跑在浏览器中,需要浏览器提供解释环境。如果浏览器的解释环境对 JS 的尾递归优化的不好,那就说明,JS 的尾递归优化很差。由于浏览器有很多,可见 JS 要实现全面的尾递归优化,还有很长的路要走。

PS: 任何需求都是有优先级的,对浏览器来说,像这种尾递归优化的优先级,明显不高。我个人认为,优先级不高,是到现在极少有浏览器支持尾递归优化的原因。

参考

一、参考链接

  • 符号: 抽象、语义
  • Typescript版图解Functor , Applicative 和 Monad
  • 邱奇-图灵论题与lambda演算
  • 为什么需要Monad?
  • 为什么是Y?

二、参考书籍

  • JavaScript 函数式编程指南
  • Scala 函数式编程
  • Haskell 趣学指南
  • 其他电子书

未来,可期

本文通过阐述加提问的方式,对函数式编程的一些理论知识进行了一次较为清晰的阐述。限于篇幅,一些细节无法展开,如有疑问,可以与我联系,一起交流一下,共同进步。

现在的前端,依旧在快速的发展中。从最近的 react Hooks 到 Vue 3.0 的 Function API 。我们能感受到,函数式编程的影响力在慢慢变大。

在可见的未来,函数式编程方面的知识,在脑海里,是要有一个清晰的认知框架。

最后,发表下我个人的看法:

JavaScript 最终会回到以函数的形式去处理绝大多数事情的模式上。

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:labs2020 联系。

查看原文

赞 46 收藏 32 评论 2

依韵_宵音 关注了问题 · 2019-07-30

import store from './store/' 表示的含义是什么?

Vue-cli生成的项目文件中,有以下导入文件的代码

import store from './store/'

它是引入src/store/index.js文件,还是引入src/store/中的所有文件呢?

关注 1 回答 1

依韵_宵音 回答了问题 · 2019-07-30

import store from './store/' 表示的含义是什么?

默认情况下是自动补全 index.js

补充

默认情况:

import store from './store' => import store from './store.js'

import store from './store/' => import store from './store/index' => import store from './store/index.js'

/结束表示是个目录,通常会找主文件。

最后不带扩展会自动补全扩展名,默认是 .js 。但不仅限于补全js,具体能补全那些,取决于配置,如 vue 项目中通常可以自动补全 .vue react 项目中可以自动补全 .jsx

关注 1 回答 1

依韵_宵音 赞了文章 · 2019-07-03

「译」编写更好的 JavaScript 条件式和匹配条件的技巧

介绍

如果你像我一样乐于见到整洁的代码,那么你会尽可能地减少代码中的条件语句。通常情况下,面向对象编程让我们得以避免条件式,并代之以继承和多态。我认为我们应当尽可能地遵循这些原则。

正如我在另一篇文章 JavaScript 整洁代码的最佳实践里提到的,你写的代码不单单是给机器看的,还是给“未来的自己”以及“其他人”看的。

从另一方面来说,由于各式各样的原因,可能我们的代码最终还是会有条件式。也许是修复 bug 的时间很紧,也许是不使用条件语句会对我们的代码库造成大的改动,等等。本文将会解决这些问题,同时帮助你组织所用的条件语句。

技巧

以下是关于如何构造 if...else 语句以及如何用更少的代码实现更多功能的技巧。阅读愉快!

1. 要事第一。小细节,但很重要

不要使用否定条件式(这可能会让人感到疑惑)。同时,使用条件式简写来表示 boolean 值。这个无须再强调了,尤其是否定条件式,这不符合正常的思维方式。

不好的:

const isEmailNotVerified = (email) => {
    // 实现
}

if (!isEmailNotVerified(email)) {
    // 做一些事...
}

if (isVerified === true) {
    // 做一些事...
}

好的:

const isEmailVerified = (email) => {
    // 实现
}

if (isEmailVerified(email)) {
    // 做一些事...
}

if (isVerified) {
    // 做一些事...
}

现在,理清了上面的事情后,我们就可以开始了。

2. 对于多个条件,使用 Array.includes

假设我们想要在函数中检查汽车模型是 renault 还是 peugeot。那么代码可能是这样的:

const checkCarModel = (model) => {
    if(model === 'renault' || model === 'peugeot') { 
    console.log('model valid');
    }
}

checkCarModel('renault'); // 输出 'model valid'

考虑到我们只有两个模型,这么做似乎也还能接受,但如果我们还想要检查另一个或者是几个模型呢?如果我们增加更多 or 语句,那么代码将变得难以维护,且不够整洁。为了让它更加简洁,我们可以像这样重写函数:

const checkCarModel = (model) => {
    if(['peugeot', 'renault'].includes(model)) { 
    console.log('model valid');
    }
}

checkCarModel('renault'); // 输出 'model valid'

上面的代码看起来已经很漂亮了。为了更进一步改善它,我们可以创建一个变量来存放汽车模型:

const checkCarModel = (model) => {
    const models = ['peugeot', 'renault'];

    if(models.includes(model)) { 
    console.log('model valid');
    }
}

checkCarModel('renault'); // 输出 'model valid'

现在,如果我们想要检查更多模型,只需要添加一个新的数组元素即可。此外,如果它很重要的话,我们还可以将 models 变量定义在函数作用域外,并在需要的地方重用。这种方式可以让我们集中管理,并使维护变得轻而易举,因为我们只需在代码中更改一个位置。

3. 匹配所有条件,使用 Array.every 或者 Array.find

在本例中,我们想要检查每个汽车模型是否都是传入函数的那一个。为了以更加命令式的方式实现,我们会这么做:

const cars = [
  { model: 'renault', year: 1956 },
  { model: 'peugeot', year: 1968 },
  { model: 'ford', year: 1977 }
];

const checkEveryModel = (model) => {
  let isValid = true;

  for (let car of cars) {
    if (!isValid) {
      break;
    }
    isValid = car.model === model;
  }

  return isValid;
}

console.log(checkEveryModel('renault')); // 输出 false

如果你更喜欢以命令式的风格行事,上面的代码或许还不错。另一方面,如果你不关心其背后发生了什么,那么你可以重写上面的函数并使用 Array.every 或者 Array.find 来达到相同的结果。

const checkEveryModel = (model) => {
  return cars.every(car => car.model === model);
}

console.log(checkEveryModel('renault')); // 输出 false

通过使用 Array.find 并做轻微的调整,我们可以达到相同的结果。两者的表现是一致的,因为两个函数都为数组中的每一个元素执行了回调,并且在找到一个 falsy 项时立即返回 false

const checkEveryModel = (model) => {
  return cars.find(car => car.model !== model) === undefined;
}

console.log(checkEveryModel('renault')); // 输出 false

4. 匹配部分条件,使用 Array.some

Array.every 匹配所有条件,这个方法则可以轻松地检查我们的数组是否包含某一个或某几个元素。为此,我们需要提供一个回调并基于条件返回一个布尔值。

我们可以通过编写一个类似的 for...loop 语句来实现相同的结果,就像之前写的一样。但幸运的是,有很酷的 JavaScript 函数可以来帮助我们完成这件事。

const cars = [
  { model: 'renault', year: 1956 },
  { model: 'peugeot', year: 1968 },
  { model: 'ford', year: 1977 }
];

const checkForAnyModel = (model) => {
  return cars.some(car => car.model === model);
}

console.log(checkForAnyModel('renault')); // 输出 true

5. 提前返回而不是使用 if...else 分支

当我还是学生的时候,就有人教过我:一个函数应该只有一个返回语句,并且只从一个地方返回。如果细心处理,这个方法倒也还好。我这么说也就意味着,我们应该意识到它在某些情况下可能会引起条件式嵌套地狱。如果不受控制,多个分支和 if...else 嵌套将会让我们感到很痛苦。

另一方面,如果代码库很大且包含很多行代码,位于深层的一个返回语句可能会带来问题。现在我们都实行关注点分离和 SOLID 原则,因此,代码行过多这种情况挺罕见的。

举例来解释这个问题。假设我们想要显示所给车辆的模型和生产年份:

const checkModel = (car) => {
  let result; // 首先,定义一个 result 变量
  
  // 检查是否有车
  if(car) {

    // 检查是否有车的模型
    if (car.model) {

      // 检查是否有车的年份
      if(car.year) {
        result = `Car model: ${car.model}; Manufacturing year: ${car.year};`;
      } else {
        result = 'No car year';
      }
    
    } else {
      result = 'No car model'
    }   

  } else {
    result = 'No car';
  }

  return result; // 我们的单独的返回语句
}

console.log(checkModel()); // 输出 'No car'
console.log(checkModel({ year: 1988 })); // 输出 'No car model'
console.log(checkModel({ model: 'ford' })); // 输出 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // 输出 'Car model: ford; Manufacturing year: 1988;'

正如你所看到的,即使本例的问题很简单,上面的代码也实在太长了。可以想象一下,如果我们有更加复杂的逻辑会发生什么事。大量的 if...else 语句。

我们可以重构上面的函数,分解成多个步骤并稍做改善。例如,使用三元操作符,包括 && 条件式等。不过,这里我直接跳到最后,向你展示借助现代 JavaScript 特性和多个返回语句,代码可以有多简洁。

const checkModel = ({model, year} = {}) => {
  if(!model && !year) return 'No car';
  if(!model) return 'No car model';
  if(!year) return 'No car year';

  // 这里可以任意操作模型或年份
  // 确保它们存在
  // 无需更多检查

  // doSomething(model);
  // doSomethingElse(year);
  
  return `Car model: ${model}; Manufacturing year: ${year};`;
}

console.log(checkModel()); // 输出 'No car'
console.log(checkModel({ year: 1988 })); // 输出 'No car model'
console.log(checkModel({ model: 'ford' })); // 输出 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // 输出 'Car model: ford; Manufacturing year: 1988;'

在重构版本中,我们包含了解构和默认参数。默认参数确保我们在传入 undefined 时有可用于解构的值。注意,如果传入 null ,函数将会抛出错误。这也是之前那个方法的优点所在,因为那个方法在传入 null 的时候会输出 'No car'

对象解构确保函数只取所需。例如,如果我们在给定车辆对象中包含额外属性,则该属性在我们的函数中是无法获取的。

根据偏好,开发者会选择其中一种方式。实践中,编写的代码通常介于两者之间。很多人觉得 if...else 语句更容易理解,并且有助于他们更为轻松地遵循程序流程。

6. 使用索引或者映射,而不是 switch 语句

假设我们想要基于给定的国家获取汽车模型。

const getCarsByState = (state) => {
  switch (state) {
    case 'usa':
      return ['Ford', 'Dodge'];
    case 'france':
      return ['Renault', 'Peugeot'];
    case 'italy':
      return ['Fiat'];
    default:
      return [];
  }
}

console.log(getCarsByState()); // 输出 []
console.log(getCarsByState('usa')); // 输出 ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // 输出 ['Fiat']

上诉代码可以重构,完全去除 switch 语句。

const cars = new Map()
  .set('usa', ['Ford', 'Dodge'])
  .set('france', ['Renault', 'Peugeot'])
  .set('italy', ['Fiat']);

const getCarsByState = (state) => {
  return cars.get(state) || [];
}

console.log(getCarsByState()); // 输出 []
console.log(getCarsByState('usa')); //输出 ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // 输出 ['Fiat']

或者,我们还可以为包含可用汽车列表的每个国家创建一个类,并在需要的时候使用。不过这个就是题外话了,本文的主题是关于条件句的。更恰当的修改是使用对象字面量。

const carState = {
  usa: ['Ford', 'Dodge'],
  france: ['Renault', 'Peugeot'],
  italy: ['Fiat']
};

const getCarsByState = (state) => {
  return carState[state] || [];
}

console.log(getCarsByState()); // 输出 []
console.log(getCarsByState('usa')); // 输出 ['Ford', 'Dodge']
console.log(getCarsByState('france')); // 输出 ['Renault', 'Peugeot']

7. 使用自判断链接和空合并

到了这一小节,我终于可以说“最后”了。在我看来,这两个功能对于 JavaScript 语言来说是非常有用的。作为一个来自 C# 世界的人,可以说我经常使用它们。

在写这篇文章的时候,这些还没有得到完全的支持。因此,对于以这种方式编写的代码,你需要使用 Babel 进行编译。你可以在自判断链接这里以及在空合并这里查阅。

自判断链接允许我们在没有显式检查中间节点是否存在的时候处理树形结构,空合并可以确保节点不存在时会有一个默认值,配合自判断链接使用会有不错的效果。

让我们用一些例子来支撑上面的结论。一开始,我们还是用以前的老方法:

const car = {
  model: 'Fiesta',
  manufacturer: {
    name: 'Ford',
    address: {
      street: 'Some Street Name',
      number: '5555',
      state: 'USA'
    }
  }
}

// 获取汽车模型
const model = car && car.model || 'default model';
// 获取厂商地址
const street = car && car.manufacturer && car.manufacturer.address && car.manufacturer.address.street || 'default street';
// 请求一个不存在的属性
const phoneNumber = car && car.manufacturer && car.manufacturer.address && car.manufacturer.phoneNumber;

console.log(model) // 输出 'Fiesta'
console.log(street) // 输出 'Some Street Name'
console.log(phoneNumber) // 输出 undefined

因此,如果我们想要知道厂商是否来自 USA 并将结果打印,那么代码是这样的:

const checkCarManufacturerState = () => {
  if(car && car.manufacturer && car.manufacturer.address && car.manufacturer.address.state === 'USA') {
    console.log('Is from USA');
  }
}

checkCarManufacturerState() // 输出 'Is from USA'

我无需再赘述如果对象结构更加复杂的话,代码会多么混乱了。许多库,例如 lodash,有自己的函数作为替代方案。不过这不是我们想要的,我们想要的是在原生 js 中也能做同样的事。我们来看一下新的方法:

    // 获取汽车模型
    const model = car?.model ?? 'default model';
    // 获取厂商地址
    const street = car?.manufacturer?.address?.street ?? 'default street';
    
    // 检查汽车厂商是否来自 USA
    const checkCarManufacturerState = () => {
      if(car?.manufacturer?.address?.state === 'USA') {
        console.log('Is from USA');
      }
    }

这看起来更加漂亮和简洁,对我来说,非常符合逻辑。如果你想知道为什么应该使用 ?? 而不是 || ,只需想一想什么值可以当做 true 或者 false,你将可能有意想不到的输出。

顺便说句题外话。自判断链接同样支持 DOM API,这非常酷,意味着你可以这么做:

const value = document.querySelector('input#user-name')?.value;

结论

好了,这就是全部内容了。如果你喜欢这篇文章的话,可以送一杯咖啡给我,让我提提神,还可以订阅文章或者在 twitter 上关注我。

感谢阅读,下篇文章见。


译者注:
关于最后一个例子的空合并为什么使用 ?? 而不是 ||,作者可能解释得不是很清楚,这里摘抄一下 tc39:proposal-nullish-coalescing 的例子:

const headerText = response.settings?.headerText || 'Hello, world!'; // '' 会被当作 false,输出: 'Hello, world!'
const animationDuration = response.settings?.animationDuration || 300; // 0 会被当作 false,输出: 300
const showSplashScreen = response.settings?.showSplashScreen || true; // False 会被当作 false,输出: true

照理来说,使用 || 是可以的,但是在上面代码中会有点小问题。比如我们想要获取的 animationDuration 的值为 0,那么由于 0 被当作 false,导致我们最后得到的是默认值 300,这显然不是我们想要的结果。而 ?? 就是用来解决这个问题的。
目前 optional-chaining 和 nullish-coalescing 还在 ecma 标准草案的 stage2 阶段,不过 babel 针对前者已有相关插件实现,更多相关文章可以看:
https://segmentfault.com/a/11...
https://zhuanlan.zhihu.com/p/...
https://www.npmjs.com/package...

查看原文

赞 112 收藏 76 评论 14

依韵_宵音 赞了回答 · 2019-04-18

解决后端用的java,前端通过下载接口显示的图片如何做缓存?

题主的意思应该是想让浏览器缓存图片避免多次请求对吧?
浏览器是否缓存与否取决于该URL返回的HTTP Header里的缓存协议,与什么样的URL格式无关。
在网上关于HTTP协议缓存机制的文章已有不少,题主可以搜索一下。
解决方法就是调用此URL返回的时候,增加一些缓存Header相关的参数,使得浏览器主动缓存图片。


我对HTTP缓存理解得不够到位,如果描述有误麻烦路过大神指正

期望缓存响应Header示例:

Cache-Control:public
Cache-Control:max-age=86400
Connection:keep-alive
Date:Fri, 05 Jan 2018 03:49:59 GMT
Expires:Sat, 06 Jan 2018 03:49:59 GMT
Last-Modified:Thu, 04 Jan 2018 09:23:31 GMT
Server:openresty/1.11.2.4

缓存Cache详解

关注 4 回答 3

依韵_宵音 赞了文章 · 2019-03-04

[译] 网速敏感的视频延迟加载方案

一个大视频的背景,如果做的好,会是一个绝佳的体验!但是,在首页添加一个视频并不仅仅是随便找个人,然后加个 25mb 的视频,那会让你的所有的性能优化都付之一炬。

Lazy pandas love lazy loading. (Photo by Elena Loshina)

我参加过一些团队,他们希望给首页加上类似的全屏视频背景。我通常不愿意那么做,因为这种做法通常会导致性能上的噩梦。老实说,我曾给一个页面加上一个 40mb 大的视频。 ?

上次有人让我这么做的时候,我很好奇应如何将背景视频的加载作为渐进增强(Progressive Enhancement),来提升网络连接状况比较好的用户的体验。除了和我的同事们强调视频体积小和压缩视频的重要性以外,也希望在代码上有一些奇迹发生。

下面是最终的解决方案:

  1. 尝试使用 JavaScript 加载 <source>
  2. 监听 canplaythrough 事件
  3. 如果 canplaythrough 事件没有在 2 秒内触发,那么使用 Promise.race() 将视频加载超时
  4. 如果没有监听到 canplaythrough 事件,那么移除 <source>,并且取消视频加载
  5. 如果监测到 canplaythrough 事件,那么使用淡入效果显示这个视频

标记

这里要注意的问题是,即使我正在 <video> 标签中使用 <source>,但我还没为这些 <source> 设置 src 属性。如果设置了 src 属性,那么浏览器会自动地找到它可以播放的第一个 <source>,并立即开始下载它。

因为在这个例子中,视频是作为渐进增强的对象,默认情况下我们不用真的加载视频。事实上唯一需要加载的,是我们为这个页面设置的预览图片。

  <video class="js-video-loader" poster="<?= $poster; ?>" muted="true" loop="true">
    <source data-data-original="path/to/video.webm" type="video/webm">
    <source data-data-original="path/to/video.mp4" type="video/mp4">
  </video>

JavaScript

我编写了一个简单的 JavaScript 类,用于查找带有 .js-video-loader 这个 class 的 video 元素,让我们以后可以在其他视频中复用这个逻辑。完整的源码可以从 Github 上看到

构造函数是这样的:

  constructor () {
    this.videos = Array.from(document.querySelectorAll('video.js-video-loader'));
    // 将在下面情况下返回
    // - 浏览器不支持 Promise
    // - 没有 video 元素
    // - 如果用户设置了减少动态偏好(prefers reduced motion) 
    // - 在移动设备上
    if (typeof Promise === 'undefined'
      || !this.videos
      || window.matchMedia('(prefers-reduced-motion)').matches
      || window.innerWidth < 992
    ) {
      return;
    }
    this.videos.forEach(this.loadVideo.bind(this));
  }

这里我们所做的就是找到这个页面上所有我们希望延迟加载的视频。如果没有,我们可以返回。当用户开启了减少动态偏好(preference for reduced motion)设置时,我们同样不会加载这样的视频。为了不让某些低网速或低图形处理能力的手机用户担心,在小屏幕手机上也会直接返回。(我在考虑是否可以通过 <source> 元素的媒体查询来做这些,但也不确定。)

然后给每个视频运行这个视频加载逻辑。

loadVideo

loadVideo() 是一个调用其他函数的简单的函数:

  loadVideo(video) {
    this.setSource(video);
    // 加上了视频链接后重新加载视频
    video.load();
    this.checkLoadTime(video);
  }

setSource

setSource() 中,我们找到那些作为数据属性(Data Attributes)插入的视频链接,并且将它们设置为真正的 src 属性。

  /**
    * 找 video 子元素中是 <source> 的,
    * 基于 data-src 属性,
    * 给每个 <source> 设置 src 属性
    *
    * @param {DOM Object} video
    */
    setSource (video) {
      let children = Array.from(video.children);
      children.forEach(child => {
        if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') {
          child.setAttribute('src', child.dataset.src);
        }
      });
    }

基本上,我所做的就是遍历每一个 <video> 元素的子元素,找一个定义了 data-src 属性(child.dataset.src)的 <source> 子元素。如果找到了,那就用 setAttribute 将它的 src 属性设置为视频链接。

现在视频链接已经被设置给 <video> 元素了,下面需要让浏览器再次加载视频。我们通过在 loadVideo() 中的 video.load() 来完成这个工作。load() 方法是 HTMLMediaElement API 的一部分,它可以重置媒体元素并且重启加载过程。

checkLoadTime

接下来是见证奇迹的时刻。在 checkLoadTime() 方法中我们创建了两个 Promise。第一个 Promise 将在 <video> 元素的 canplaythrough 事件触发时被 resolve。这个 canplaythrough 事件是浏览器认为这个视频可以在不停下来缓冲的情况下持续播放的时候被触发。我们在这个 Promise 中添加一个这个事件的监听回调,当这个事件触发的时候执行 resolve()

  // 创建一个 Promise,将在
  // video.canplaythrough 事件发生时被 resolve
  let videoLoad = new Promise((resolve) => {
    video.addEventListener('canplaythrough', () => {
      resolve('can play');
    });
  });

我们同时创建另一个 Promise 作为计时器。在这个 Promise 中,当经过一个设定好的时间后,我们使用 setTimeout 来将这个 Promise 给 resolve 掉,我这设置了一个 2 秒的时延(2000毫秒)。

  // 创建一个 Promise 将在
  // 特定时间(2s)后被 resolve
  let videoTimeout = new Promise((resolve) => {
    setTimeout(() => {
      resolve('The video timed out.');
    }, 2000);
  });

现在我们有了两个 Promise,我们可以通过 Promise.race() 看他们谁先完成。

  // 将 promises 进行 Race 看看哪个先被 resolves
  Promise.race([videoLoad, videoTimeout]).  then(data => {
    if (data === 'can play') {
      video.play();
      setTimeout(() => {
        video.classList.add('video-loaded');
      }, 3000);
    } else {
      this.cancelLoad(video);
    }
  });

在这个 .then() 的回调中我们等着拿到最先被 resolve 的那个 Promise 传回来的信息。如果这个视频可以播放,那么我就会拿到之前传的 can play,然后试一下是否可以播放这个视频。video.play() 是使用 HTMLMediaElement 提供的 play() 方法来触发视频播放。

3 秒后,setTimeout() 将会给这个标签加上 .video-loaded 类,这将有助于视频文件更巧妙的淡入自动循环播放。

如果我们没接收到 can play 字符串,那么我们将取消这个视频的加载。

cancelLoad

cancelLoad() 方法做的基本上跟 loadVideo() 方法相反。它从每个 source 标签移除 src 属性,并且触发 video.load() 来重置视频元素。

如果我们不这么做,这个视频元素将会在后台保持加载状态,即使我们都没将它显示出来。

  /**
    * 通过移除所有的 <source> 来取消视频加载
    * 然后触发 video.load().
    *
    * @param {DOM object} video
    */
    cancelLoad (video) {
      let children = Array.from(video.children);
      children.forEach(child => {
        if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') {
          child.parentNode.removeChild(child);
        }
      });
      // 重新加载没有 <source> 标签的 video
      // 这样它会停止下载
      video.load();
    }

总结

这个方法的缺点是,我们仍然试图通过一个不一定靠谱的链接来下载一个可能比较大的文件,但是通过提供一个超时时间,我们希望能够给某些网速慢的用户节约一些流量并且获得更好的性能。根据我在 Chrome Dev Tools 里将网速节流到慢 3G 条件下的测试,这个方法将在超时之前加载了 512kb 的视频。即使是一个 3-5mb 的视频,对于一些网速慢的用户来说,这也带来了显著的流量节省。

你觉得怎么样?如果有改进的建议,欢迎在评论里分享!


Originally published atbenrobertson.io.

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

PS:欢迎大家关注我的公众号【前端下午茶】,一起加油吧~

另外可以加入「前端下午茶交流群」微信群,长按识别下面二维码即可加我好友,备注加群,我拉你入群~

查看原文

赞 12 收藏 9 评论 0

依韵_宵音 赞了文章 · 2019-03-04

github 授权登录教程与如何设计第三方授权登录的用户表

效果图

需求:在网站上想评论一篇文章,而评论文章是要用户注册与登录的,那么怎么免去这麻烦的步骤呢?答案是通过第三方授权登录。本文讲解的就是 github 授权登录的教程。

效果体验地址: http://biaochenxuying.cn

1. github 第三方授权登录教程

先来看下 github 授权的完整流程图 1:

github 1

或者看下 github 授权的完整流程图 2:

github 2

1.1 申请一个 OAuth App

首先我们必须登录上 github 申请一个 OAuth App,步骤如下:

  1. 登录 github
  2. 点击头像下的 Settings -> Developer settings 右侧 New OAuth App
  3. 填写申请 app 的相关配置,重点配置项有2个
  4. Homepage URL 这是后续需要使用授权的 URL ,你可以理解为就是你的项目根目录地址
  5. Authorization callback URL 授权成功后的回调地址,这个至关重要,这是拿到授权 code 时给你的回调地址。

具体实践如下:

    1. 首先登录你的 GitHub 账号,然后点击进入Settings。

    1. 点击 OAuth Apps , Register a new application 或者 New OAuth App 。

    1. 输入信息。

image.png

    1. 应用信息说明。

流程也可看 GitHub 设置的官方文档-Registering OAuth Apps

1.2 授权登录

github 文档:building-oauth-apps/authorizing-oauth-apps

授权登录的主要 3 个步骤:

笔者这次实践中,项目是采用前后端分离的,所以第 1 步在前端实现,而第 2 步和第 3 步是在后端实现的,因为第 2 个接口里面需要Client_secret 这个参数,而且第 3 步获取的用户信息在后端保存到数据库。

1.3. 代码实现

1.3.1 前端

笔者项目的技术是 react。

// config.js

// ***** 处请填写你申请的 OAuth App 的真实内容
 const config = {
  'oauth_uri': 'https://github.com/login/oauth/authorize',
  'redirect_uri': 'http://biaochenxuying.cn/',
  'client_id': '*****',
  'client_secret': '*******',
};

// 本地开发环境下
if (process.env.NODE_ENV === 'development') {
  config.redirect_uri = "http://localhost:3001/"
  config.client_id = "******"
  config.client_secret = "*****"
}
export default config; 

代码参考 config.js

redirect_uri 回调地址是分环境的,所以我是新建了两个 OAuth App 的,一个用于线上生产环境,一个用于本地开发环境。

一般来说,登录的页面应该是独立的,对应相应的路由 /login , 但是本项目的登录 login 组件是 nav 组件的子组件,nav 是个全局用的组件, 所以回调地址就写了 http://biaochenxuying.cn/

  • 所以点击跳转是写在 login.js 里面;
  • 授权完拿到 code 后,是写在 nav.js 里面
  • nav.js 拿到 code 值后去请求后端接口,后端接口返回用户信息。
  • 其中后端拿到 code 还要去 github 取 access_token ,再根据 access_token 去取 github 取用户的信息。
// login.js

// html
<Button
    style={{ width: '100%' }}
    onClick={this.handleOAuth} >
      github 授权登录
</Button>

// js
handleOAuth(){
    // 保存授权前的页面链接
    window.localStorage.preventHref = window.location.href
    // window.location.href = 'https://github.com/login/oauth/authorize?client_id=***&redirect_uri=http://biaochenxuying.cn/'
    window.location.href = `${config.oauth_uri}?client_id=${config.client_id}&redirect_uri=${config.redirect_uri}`
}

代码参考 login.js

// nav.js

componentDidMount() {
    // console.log('code :', getQueryStringByName('code'));
    const code = getQueryStringByName('code')
    if (code) {
      this.setState(
        {
          code
        },
        () => {
          if (!this.state.code) {
            return;
          }
          this.getUser(this.state.code);
        },
      );
    }
  }

componentWillReceiveProps(nextProps) {
    const code = getQueryStringByName('code')
    if (code) {
      this.setState(
        {
          code
        },
        () => {
          if (!this.state.code) {
            return;
          }
          this.getUser(this.state.code);
        },
      );
    }
  }
  getUser(code) {
    https
      .post(
        urls.getUser,
        {
          code,
        },
        { withCredentials: true },
      )
      .then(res => {
        // console.log('res :', res.data);
        if (res.status === 200 && res.data.code === 0) {
          this.props.loginSuccess(res.data);
          let userInfo = {
            _id: res.data.data._id,
            name: res.data.data.name,
          };
          window.sessionStorage.userInfo = JSON.stringify(userInfo);
          message.success(res.data.message, 1);
          this.handleLoginCancel();
          // 跳转到之前授权前的页面
          const href = window.localStorage.preventHref
          if(href){
            window.location.href = href 
          }
        } else {
          this.props.loginFailure(res.data.message);
          message.error(res.data.message, 1);
        }
      })
      .catch(err => {
        console.log(err);
      });
  }

参考 nav.js

1.3.2 后端

笔者项目的后端采用的技术是 node.js 和 express。

  • 后端拿到前端传来的 code 后,还要去 github 取 access_token ,再根据 access_token 去取 github 取用户的信息。
  • 然后把要用到的用户信息通过 注册 的方式保存到数据库,然后返回用户信息给前端。
// app.config.js

exports.GITHUB = {
    oauth_uri: 'https://github.com/login/oauth/authorize',
    access_token_url: 'https://github.com/login/oauth/access_token',
    // 获取 github 用户信息 url // eg: https://api.github.com/user?access_token=******&scope=&token_type=bearer
    user_url: 'https://api.github.com/user',

    // 生产环境
    redirect_uri: 'http://biaochenxuying.cn/',
    client_id: '*****',
    client_secret: '*****',

    // // 开发环境
    // redirect_uri: "http://localhost:3001/",
    // client_id: "*****",
    // client_secret: "*****",
};

代码参考 app.config.js

// 路由文件  user.js

const fetch = require('node-fetch');
const CONFIG = require('../app.config.js');
const User = require('../models/user');

// 第三方授权登录的用户信息
exports.getUser = (req, res) => {
  let { code } = req.body;
  if (!code) {
    responseClient(res, 400, 2, 'code 缺失');
    return;
  }
  let path = CONFIG.GITHUB.access_token_url;
  const params = {
    client_id: CONFIG.GITHUB.client_id,
    client_secret: CONFIG.GITHUB.client_secret,
    code: code,
  };
  // console.log(code);
  fetch(path, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json', 
    },
    body: JSON.stringify(params),
  })
    .then(res1 => {
      return res1.text();
    })
    .then(body => {
      const args = body.split('&');
      let arg = args[0].split('=');
      const access_token = arg[1];
      // console.log("body:",body);
      console.log('access_token:', access_token);
      return access_token;
    })
    .then(async token => {
      const url = CONFIG.GITHUB.user_url + '?access_token=' + token;
      console.log('url:', url);
      await fetch(url)
        .then(res2 => {
          console.log('res2 :', res2);
          return res2.json();
        })
        .then(response => {
          console.log('response ', response);
          if (response.id) {
            //验证用户是否已经在数据库中
            User.findOne({ github_id: response.id })
              .then(userInfo => {
                // console.log('userInfo :', userInfo);
                if (userInfo) {
                  //登录成功后设置session
                  req.session.userInfo = userInfo;
                  responseClient(res, 200, 0, '授权登录成功', userInfo);
                } else {
                  let obj = {
                    github_id: response.id,
                    email: response.email,
                    password: response.login,
                    type: 2,
                    avatar: response.avatar_url,
                    name: response.login,
                    location: response.location,
                  };
                  //注册到数据库
                  let user = new User(obj);
                  user.save().then(data => {
                    // console.log('data :', data);
                    req.session.userInfo = data;
                    responseClient(res, 200, 0, '授权登录成功', data);
                  });
                }
              })
              .catch(err => {
                responseClient(res);
                return;
              });
          } else {
            responseClient(res, 400, 1, '授权登录失败', response);
          }
        });
    })
    .catch(e => {
      console.log('e:', e);
    });
};

代码参考 user.js

至于拿到 github 的用户信息后,是注册到 user 表,还是保存到另外一张 oauth 映射表,这个得看自己项目的情况。

从 github 拿到的用户信息如下图:

github-login.png

最终效果:

github-logining.gif

参与文章:

  1. https://www.jianshu.com/p/a9c...
  2. https://blog.csdn.net/zhuming...

2. 如何设计第三方授权登录的用户表

第三方授权登录的时候,第三方的用户信息是存数据库原有的 user 表还是新建一张表呢 ?

答案:这得看具体项目了,做法多种,请看下文。

第三方授权登录之后,第三方用户信息一般都会返回用户唯一的标志 openid 或者 unionid 或者 id,具体是什么得看第三方,比如 github 的是 id

  • 1. 直接通过 注册 的方式保存到数据库

第一种:如果网站 没有 注册功能的,直接通过第三方授权登录,授权成功之后,可以直接把第三的用户信息 注册 保存到自己数据库的 user 表里面。典型的例子就是 微信公众号的授权登录。

第二种:如果网站 注册功能的,也可以通过第三方授权登录,授权成功之后,也可以直接把第三的用户信息 注册 保存到自己数据库的 user 表里面(但是密码是后端自动生成的,用户也不知道,只能用第三方授权登录),这样子的第三方的用户和原生注册的用户信息都在同一张表了,这种情况得看自己项目的具体情况。笔者的博客网站暂时就采用了这种方式。

  • 2. 增加映射表

现实中很多网站都有多种账户登录方式,比如可以用网站的注册 id 登录,还可以用手机号登录,可以用 QQ 登录等等。数据库中都是有映射关系,QQ、手机号等都是映射在网站的注册 id 上。保证不管用什么方式登录,只要去查映射关系,发现是映射在网站注册的哪个 id 上,就让哪个 id 登录成功。

  • 3. 建立一个 oauth 表,一个 id 列,记录对应的用户注册表的 id

建立一个 oauth 表,一个 id 列,记录对应的用户注册表的 id,然后你有多少个第三方登陆功能,你就建立多少列,记录第三方登陆接口返回的 openid;第三方登陆的时候,通过这个表的记录的 openid 获取 id 信息,如果存在通过 id 读取注册表然后用 session 记录相关信息。不存在就转向用户登陆/注册界面要用户输入本站注册的账户进行 openid 绑定或者新注册账户信息进行绑定。

具体代码实践请参考文章:

1. 第三方登录用户信息表设计

2. 浅谈数据库用户表结构设计,第三方登录

4. 最后

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

如果您觉得这篇文章不错或者对你有所帮助,请给个赞或者星呗,你的点赞就是我继续创作的最大动力。

全栈修炼 有兴趣的朋友可以扫下方二维码关注我的公众号

我会不定期更新有价值的内容,长期运营。

关注公众号并回复 福利 可领取免费学习资料,福利详情请猛戳: Python、Java、Linux、Go、node、vue、react、javaScript

全栈修炼

查看原文

赞 38 收藏 22 评论 4

依韵_宵音 关注了用户 · 2019-02-25

littleLyon @littlelyon

华丽的跌倒胜过无谓的徘徊

关注 649