Aima

Aima 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 msxiaoma.github.io/ 编辑
编辑

what you need is not strength, but passion.

个人动态

Aima 赞了文章 · 2020-09-24

前端经典面试题: 从输入URL到页面加载发生了什么?

从输入URL到页面加载发生了什么

最近在进行前端面试方面的一些准备,看了网上许多相关的文章,发现有一个问题始终绕不开: 在浏览器中输入URL到整个页面显示在用户面前时这个过程中到底发生了什么。仔细思考这个问题,发现确实很深,这个过程涉及到的东西很多。这个问题的回答真的能够很好的考验一个web工程师的水平,于是我自问自答一番。

总体来说分为以下几个过程:

  1. DNS解析

  2. TCP连接

  3. 发送HTTP请求

  4. 服务器处理请求并返回HTTP报文

  5. 浏览器解析渲染页面

  6. 连接结束

具体过程

DNS解析

DNS解析的过程就是寻找哪台机器上有你需要资源的过程。当你在浏览器中输入一个地址时,例如www.baidu.com,其实不是百度网站真正意义上的地址。互联网上每一台计算机的唯一标识是它的IP地址,但是IP地址并不方便记忆。用户更喜欢用方便记忆的网址去寻找互联网上的其它计算机,也就是上面提到的百度的网址。所以互联网设计者需要在用户的方便性与可用性方面做一个权衡,这个权衡就是一个网址到IP地址的转换,这个过程就是DNS解析。它实际上充当了一个翻译的角色,实现了网址到IP地址的转换。网址到IP地址转换的过程是如何进行的?

解析过程

DNS解析是一个递归查询的过程。

DNS解析过程

上述图片是查找www.google.com的IP地址过程。首先在本地域名服务器中查询IP地址,如果没有找到的情况下,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也不存在该域名时,本地域名会向com顶级域名服务器发送一个请求,依次类推下去。直到最后本地域名服务器得到google的IP地址并把它缓存到本地,供下次查询使用。从上述过程中,可以看出网址的解析是一个从右向左的过程: com -> google.com -> www.google.com。但是你是否发现少了点什么,根域名服务器的解析过程呢?事实上,真正的网址是www.google.com.,并不是我多打了一个.,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上,所有网址真正的解析过程为: . -> .com -> google.com. -> www.google.com.。

DNS优化

了解了DNS的过程,可以为我们带来哪些?上文中请求到google的IP地址时,经历了8个步骤,这个过程中存在多个请求(同时存在UDP和TCP请求,为什么有两种请求方式,请自行查找)。如果每次都经过这么多步骤,是否太耗时间?如何减少该过程的步骤呢?那就是DNS缓存。

DNS缓存

DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。

  • 在你的chrome浏览器中输入:chrome://dns/,你可以看到chrome浏览器的DNS缓存。

  • 系统缓存主要存在/etc/hosts(Linux系统)中:

DNS系统缓存

  • ...

DNS负载均衡

不知道大家有没有思考过一个问题: DNS返回的IP地址是否每次都一样?如果每次都一样是否说明你请求的资源都位于同一台机器上面,那么这台机器需要多高的性能和储存才能满足亿万请求呢?其实真实的互联网世界背后存在成千上百台服务器,大型的网站甚至更多。但是在用户的眼中,它需要的只是处理他的请求,哪台机器处理请求并不重要。DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡,又叫做DNS重定向。大家耳熟能详的CDN(Content Delivery Network)就是利用DNS的重定向技术,DNS服务器会返回一个跟用户最接近的点的IP地址给用户,CDN节点的服务器负责响应用户的请求,提供所需的内容。在这里打个免费的广告,我平时使用的比较多的是七牛云的CDN(免费)储存图片,作为我个人博客的图床使用。

TCP连接

HTTP协议是使用TCP作为其传输层协议的,当TCP出现瓶颈时,HTTP也会受到影响。但由于TCP优化这一块我平常接触的并不是很多,再加上大学时的计算机网络的基础基本上忘完,所以这一部分我也就不在这里分析了。

HTTPS协议

我不知道把HTTPS放在这个部分是否合适,但是放在这里好像又说的过去。HTTP报文是包裹在TCP报文中发送的,服务器端收到TCP报文时会解包提取出HTTP报文。但是这个过程中存在一定的风险,HTTP报文是明文,如果中间被截取的话会存在一些信息泄露的风险。那么在进入TCP报文之前对HTTP做一次加密就可以解决这个问题了。HTTPS协议的本质就是HTTP + SSL(or TLS)。在HTTP报文进入TCP报文之前,先使用SSL对HTTP报文进行加密。从网络的层级结构看它位于HTTP协议与TCP协议之间。

HTTPS

HTTPS过程

HTTPS在传输数据之前需要客户端与服务器进行一个握手(TLS/SSL握手),在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL使用了非对称加密,对称加密以及hash等。具体过程请参考经典的阮一峰先生的博客TLS/SSL握手过程
HTTPS相比于HTTP,虽然提供了安全保证,但是势必会带来一些时间上的损耗,如握手和加密等过程,是否使用HTTPS需要根据具体情况在安全和性能方面做出权衡。

HTTP请求

其实这部分又可以称为前端工程师眼中的HTTP,它主要发生在客户端。发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口(HTTP协议80/8080, HTTPS协议443)。HTTP请求报文是由三部分组成: 请求行, 请求报头请求正文

请求行

格式如下:
Method Request-URL HTTP-Version CRLF

eg: GET index.html HTTP/1.1

常用的方法有: GET, POST, PUT, DELETE, OPTIONS, HEAD。

TODO:

  • GET和POST有什么区别?

请求报头

请求报头允许客户端向服务器传递请求的附加信息和客户端自身的信息。
PS: 客户端不一定特指浏览器,有时候也可使用Linux下的CURL命令以及HTTP客户端测试工具等。
常见的请求报头有: Accept, Accept-Charset, Accept-Encoding, Accept-Language, Content-Type, Authorization, Cookie, User-Agent等。

HTTP分析

上图是使用Chrome开发者工具截取的对百度的HTTP请求以及响应报文,从图中可以看出,请求报头中使用了Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Cookie等字段。Accept用于指定客户端用于接受哪些类型的信息,Accept-Encoding与Accept类似,它用于指定接受的编码方式。Connection设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。

请求正文

当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求包头中有一些与请求正文相关的信息,例如: 现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json。

服务器处理请求并返回HTTP报文

自然而然这部分对应的就是后端工程师眼中的HTTP。后端从在固定的端口接收到TCP报文开始,这一部分对应于编程语言中的socket。它会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。这一部分工作一般是由Web服务器去进行,我使用过的Web服务器有Tomcat, Jetty和Netty等等。

HTTP响应报文也是由三部分组成: 状态码, 响应报头响应报文

状态码

状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:

  • 1xx:指示信息–表示请求已接收,继续处理。

  • 2xx:成功–表示请求已被成功接收、理解、接受。

  • 3xx:重定向–要完成请求必须进行更进一步的操作。

  • 4xx:客户端错误–请求有语法错误或请求无法实现。

  • 5xx:服务器端错误–服务器未能实现合法的请求。
    平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500(分别表示什么请自行查找)。

TODO:

  • 301和302有什么区别?

  • HTTP缓存

状态码

该图是本公司对状态码的一个总结,绘制而成的status code map,请大家参考。

响应报头

常见的响应报头字段有: Server, Connection...。

响应报文

服务器返回给浏览器的文本信息,通常HTML, CSS, JS, 图片等文件就放在这一部分。

浏览器解析渲染页面

浏览器在收到HTML,CSS,JS文件后,它是如何把页面呈现到屏幕上的?下图对应的就是WebKit渲染的过程。

WebKit渲染过程

浏览器是一个边解析边渲染的过程。首先浏览器解析HTML文件构建DOM树,然后解析CSS文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。

Event loop

JS的解析是由浏览器中的JS解析引擎完成的。JS是单线程运行,也就是说,在同一个时间内只能做一件事,所有的任务都需要排队,前一个任务结束,后一个任务才能开始。但是又存在某些任务比较耗时,如IO读写等,所以需要一种机制可以先执行排在后面的任务,这就是:同步任务(synchronous)和异步任务(asynchronous)。JS的执行机制就可以看做是一个主线程加上一个任务队列(task queue)。同步任务就是放在主线程上执行的任务,异步任务是放在任务队列中的任务。所有的同步任务在主线程上执行,形成一个执行栈;异步任务有了运行结果就会在任务队列中放置一个事件;脚本运行时先依次运行执行栈,然后会从任务队列里提取事件,运行任务队列中的任务,这个过程是不断重复的,所以又叫做事件循环(Event loop)。

浏览器在解析过程中,如果遇到请求外部资源时,如图像,iconfont,JS等。浏览器将重复1-6过程下载该资源。请求过程是异步的,并不会影响HTML文档进行加载,但是当文档加载过程中遇到JS文件,HTML文档会挂起渲染过程,不仅要等到文档中JS文件加载完毕还要等待解析执行完毕,才会继续HTML的渲染过程。原因是因为JS有可能修改DOM结构,这就意味着JS执行完成前,后续所有资源的下载是没有必要的,这就是JS阻塞后续资源下载的根本原因。CSS文件的加载不影响JS文件的加载,但是却影响JS文件的执行。JS代码执行前浏览器必须保证CSS文件已经下载并加载完毕。

Web优化

上面部分主要介绍了一次完整的请求对应的过程,了解该过程的目的无非就是为了Web优化。在谈到Web优化之前,我们回到一个更原始的问题,Web前端的本质是什么。我的理解是: 将信息快速并友好的展示给用户并能够与用户进行交互。快速的意思就是在尽可能短的时间内完成页面的加载,试想一下当你在淘宝购买东西的时候,淘宝页面加载了10几秒才显示出物品,这个时候你还有心情去购买吗?怎么快速的完成页面的加载呢?优雅的学院派雅虎给出了常用的一些手段,也就是我们熟悉的雅虎34条军规。这34军规实际上就是围绕请求过程进行的一些优化方式。

如何尽快的加载资源?答案就是能不从网络中加载的资源就不从网络中加载,当我们合理使用缓存,将资源放在浏览器端,这是最快的方式。如果资源必须从网络中加载,则要考虑缩短连接时间,即DNS优化部分;减少响应内容大小,即对内容进行压缩。另一方面,如果加载的资源数比较少的话,也可以快速的响应用户。当资源到达浏览器之后,浏览器开始进行解析渲染,浏览器中最耗时的部分就是reflow,所以围绕这一部分就是考虑如何减少reflow的次数。

总结

写这篇文章真的非常纠结,前前后后断断续续写了两个星期,因为涉及到的东西比较多,再加上有些东西记忆的没有那么清晰了,所以不好下笔。所涉及到的大部分内容,也基本上是一笔带过,只是给读者一个浅显的认知,当遇到相关的问题时,知道如何去查询。大家可以当成一篇Web开发的科普类文章去阅读。

另外在这里为公司的产品打个广告,在Chrome store中搜索DHC,这是一款超级好用的Web客户端工具,囊括了很多的功能: 报文分析,API测试等等,可谓说是WEB工程师必备工具。

查看原文

赞 393 收藏 507 评论 33

Aima 赞了文章 · 2020-09-24

前端工程师成长之多读好书

1 引言

乱七八糟的书看了很多,有一本讲JavaScript的印象特别深开篇说的是"JavaScript是Java的脚本语言",但还是看完了,最后忘了书名。

下面列的这些都是看过后至少记得起书名的,也有部分是经常看的书,一起列出来,推荐给爱学习的同学。

2 前端技术

2.1 综合

  • 《现代前端技术解析》
  • 《Web前端开发最佳实践》
  • 《Web前端工程师修炼之道》
  • 《编写高质量代码-Web前端开发修炼之道》
  • 《响应式Web设计 HTML5和CSS3实战》 第二版
  • 《响应式设计、改造与优化》

2.2 基础

2.2.1 HTML && HTML5

  • 《HTML与CSS基础教程》第八版
  • 《HTML与XHTML权威指南》第六版
  • 《HTML5与CSS3实战指南》
  • 《HTML5和CSS3权威指南》
  • 《HTML5与CSS3设计模式》

2.2.2 CSS && CSS3

  • 《CSS世界》
  • 《CSS核心技术详解》
  • 《CSS权威指南》 第三版
  • 《CSS设计指南》第三版
  • 《精通CSS-高级Web标准解决方案》第二版
  • 《图解CSS3-核心技术与案例实战》

2.2.3 JavaScript && ES6+

  • 《看透JavaScript:原理、方法与实践》
  • 《实战ES2015:深入现代JavaScript 应用开发》
  • 《学习JavaScript数据结构与算法》 第二版
  • 《ES6标准入门》第三版
  • 《JavaScript忍者秘籍》第二版
  • 《JavaScript学习指南》第三版
  • 《You Don't Know JS》《你不知道的JS》
  • 《JavaScript权威指南》第六版
  • 《JavaScript高级程序设计》 第三版
  • 《JavaScript核心概念及实践》
  • 《JavaScript面向对象编程指南》第二版
  • 《JavaScript DOM编程艺术》第二版
  • 《JavaScript语言精粹》
  • 《动态函数式编程语言精髓与编程实践》

2.3 性能优化

  • 《Web性能权威指南》
  • 《高性能JavaScript》
  • 《JavaScript性能优化:度量、监控与可视化》
  • 《高性能网站建设指南》
  • 《高性能网站建设进阶指南》
  • 《大型网站性能监测、分析与优化》
  • 《网站性能监测与优化》
  • 《高效前端-Web高效编程与优化实践》
  • 《速度与激情-以网站性能提升用户体验》

2.4 安全

  • 《Web前端黑客技术揭秘》
  • 《白帽子讲Web安全》
  • 《黑客攻防技术宝典 Web实战篇》第二版
  • 《Web应用安全威胁与防治 基于OWASP Top 10与ESAPI》
  • 《Web之困-现代Web应用安全指南》
  • 《Web安全开发指南》
  • 《Web应用安全权威指南》
  • 《黑客攻防技术宝典 浏览器实战篇》
  • 《XSS跨站脚本攻击剖析与防御》

2.5 工程化 && 自动化

  • 《深入浅出Webpack》
  • 《深入PostCSS Web设计》
  • 《前端工程化体系设计与实践》
  • 《Web前端测试与集成- Jasmine/Selenium/Protractor/Jenkins的最佳实践》
  • 《Web前端自动化构建-Gulp、Bower和Yeoman开发指南》

2.6 协议

  • 《Web性能权威指南》
  • 《图解HTTP》
  • 《HTTP权威指南》
  • 《HTTPS权威指南》
  • 《图解TCP-IP》

2.7 浏览器

  • 《浏览器工作原理》 文章
  • 《Webkit技术内幕》

2.8 架构

  • 《JavaScript框架设计》第二版
  • 《前端架构设计》
  • 《JavaScript开发框架权威指南》
  • 《大型JavaScript应用实践最佳指南》
  • 《JavaScript框架高级编程》
  • 《JavaScript设计模式与开发实践》
  • 《JavaScript设计模式》
  • 《JavaScript模式》

3 学点其他的

3.1 所谓的全栈

  • Web开发者技能路线图
  • 教你成为全栈工程师
  • 《全栈增长工程师指南》 《全栈应用开发-精益实践》
  • 《Web全栈工程师的自我修养》
  • 《Web开发权威指南》
  • 《JavaScript快速全栈开发》
  • 《单页Web应用-JavaScript从前端到后端》
  • 《全栈开发之道-MongoDB+Express+AngularJS+Node.js》
  • 《全端Web开发-使用JavaScript和Java》

3.2 程序设计

  • 《代码大全》第二版
  • 《修改代码的艺术》
  • 《重构-改善既有代码的设计》
  • 《代码整洁之道》

3.3 计算机基础

  • 《深入理解计算机系统》第三版
  • 《计算机是怎样跑起来的》
  • 《程序是怎样跑起来的》
  • 《网络是怎样连接的》
查看原文

赞 377 收藏 333 评论 11

Aima 赞了文章 · 2020-09-01

【Copy攻城狮日志】聊聊JavaScript heap out of memory

JavaScript heap out of memory
↑开局一张图,故事全靠编↑


从一次宕机说起

这是一个很狗血的故事,故事的开头是一个项目,这个项目十分草率,草率到什么程度?没有设计稿,没有文档,需求全靠口口相传,当然最草率的是交给了我,我简单列了下需求:

  • 官网的形式,主要介绍公司某些业务
  • 要能发文章

尽管很简单的需求,对于水得一匹的我来说,简直是“难于上青天”,三大件(html,css,javascript)我样样精通个P,网站部署我也只略知一二,代码编水平更是不学无术。作为Copy工程师,遇到需求我便开始了copy之路,先github溜达了一圈,找了几个满足需求的项目,最终对比了一下,选择了一个名叫iBlog2的项目--基于 Node.js 的个人开源博客系统。您没看错,就是一个博客系统!这跟官网有个毛关系?这个宕机又有个毛关系?我想说的是,经过copy然后小改之后,iBlog2摇身一变就成了能发布文章的官网项目,就是这么简单粗暴,就是这么不学无术(温馨提示:少壮不努力,老大偷代码)。

iBlog2.png

这个3年之前的项目,在现在看来的确是有些陈旧,但作者@eshengsky依旧坚持不懈的在更新维护,而对于我而言,只是为了完成能发文章的官网,所以只关注文章是如何发布和储存的,恰恰是因为我关注的面窄,忽略了部署和部署之后可能会遇到的各种问题,比如window下pm2可能出现问题、比如这次的JavaScript heap out of memory。当然并不是人家开源项目有问题,而是实际部署的时候压根没按照作者的文档来,如果按照文档,我应该用pm2部署,或者启用redis,或者使用Noginx,或者使用本机的MongoDB服务,然而,这一切,我只是在我们那个服务器新开了个端口,然后直接npm run dev就开始跑在线上了,所以呢,这么“锈”的操作,不宕机才是天理难容,印象中JavaScript heap out of memory遇到两次了,才两三个月啊!

检索JavaScript heap out of memory

通常遇到问题,我首选的解决流程是打开Chrome--输入关键词--搜索--浏览--copy--尝试,好像从来没有去思考过产生问题的根源,甚至都没有去记录这个问题以及解决的方案,导致再遇到同样的坑,又掉进去了,然后又是一通检索尝试等操作,这也是我从业这么多年来,一直没养成的习惯,也是这么多年一直没成长的某一个小的原因,“少抱怨,多思考,未来会更美好”,而我一直以反面教材在诠释这个金句。

JavaScript heap out of memory.png

通常来说,只要您的关键词够准确,您就能通过google搜索找到尽可能满意的解决方案,如果连关键词都没把握好,我想就算请教的大牛,也不一定能有效的回答,当然思否Stack Overflow都可能有填您那个坑的“铁楸”,还有一个阵地就是github

clipboard.png

通常来说,程序报错一般都有详细的报错说明,比如哪一行、出了什么错、出错明细等,就比如文章开头的那张报错图,我找到了其他用户遇到的一模一样的问题:

    <--- Last few GCs --->
    
    [8138:0x102801600]   145460 ms: Mark-sweep 1265.6 (1301.6) -> 1265.6 (1308.6) MB, 289.8 / 0.0 ms  allocation failure GC in old space requested
    [8138:0x102801600]   145740 ms: Mark-sweep 1265.6 (1308.6) -> 1265.6 (1277.6) MB, 280.6 / 0.0 ms  last resort gc 
    [8138:0x102801600]   146035 ms: Mark-sweep 1265.6 (1277.6) -> 1265.6 (1277.6) MB, 295.0 / 0.0 ms  last resort gc 
    
    
    <--- JS stacktrace --->
    
    ==== JS stack trace =========================================
    
    Security context: 0x39c891dc0d31 <JS Object>
        1: DoJoin(aka DoJoin) [native array.js:~97] [pc=0x5d1facabad4](this=0x39c891d04311 <undefined>,q=0x5a024bf3be1 <JS Array[2241635]>,r=2241635,F=0x39c891d043b1 <true>,B=0x39c891ddafe9 <String[1]\: \n>,A=0x39c891d04421 <false>)
        2: Join(aka Join) [native array.js:~122] [pc=0x5d1fb5cde96](this=0x39c891d04311 <undefined>,q=0x5a024bf3be1 <JS Array[2241635]>,r=2241635,B=0x39c891ddafe9 <String[1...
    
    FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
     1: node::Abort() [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     4: v8::internal::Factory::NewRawTwoByteString(int, v8::internal::PretenureFlag) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     5: v8::internal::Runtime_StringBuilderJoin(int, v8::internal::Object**, v8::internal::Isolate*) [/Users/erossignon/.nvm/versions/node/v7.2.0/bin/node]
     6: 0x5d1faa063a7
    Abort trap: 6

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory这个是报错的关键词,通常也是我们检索的关键词,至于为什么会导致这个错误,报错信息就显示JavaScript堆内存不足,信息中也显示了最近几次GC的详情,GC(Garbage collection
)是垃圾回收机制,具体可以阅读一下JavaScript 内存泄漏教程。经过初步了解,就是我们的应用内容泄露的,通常治标不治本的解决方案就是加大Node.js运行时内存中保留的“未使用”空间量:

node --max-old-space-size=4096 yourFile.js

JavaScript heap out of memory的原因及解决方案

Node运行时V8内存的限制

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.,一般情况下,Node在运行时只能使用到系统的一部分内存,64位系统下约为1.4GB,32位系统下约为0.7GB【有待考证,出处@JerryC】。当GC时,如果老生区大小超过设定的值时,就会报错。
一般解决方案:
在启动node程序的时候,可以传递两个参数来调整内存限制的大小,解除默认的限制

node --max-nex-space-size=1024 app.js // 单位为KB
node --max-old-space-size=2000 app.js // 单位为MB

实践中的解决可能会有以下操作:

代码问题

除了环境问题,最关键的问题就是代码本身存在问题,毕竟上面的方法治标不治本,要根治这个毛病,可能需要审视代码,先监测到内存泄漏的原因,把这部分代码找出优化。一般是无限制增长的数组、无限制设置属性和值、大循环等【出处:@林小新】。这部分由于Copy攻城狮并为深入,可以参考如何定位Node.js 的内存泄漏node内存泄漏以及定位

查看原文

赞 8 收藏 2 评论 3

Aima 关注了用户 · 2020-08-23

ConardLi @conardli

Reading makes a full man, conference a ready man, and writing an exact man.

关注 2324

Aima 赞了文章 · 2020-08-23

【React深入】深入分析虚拟DOM的渲染原理和特性

image

导读

React的虚拟DOMDiff算法是React的非常重要的核心特性,这部分源码也非常复杂,理解这部分知识的原理对更深入的掌握React是非常必要的。

本来想将虚拟DOMDiff算法放到一篇文章,写完虚拟DOM发现文章已经很长了,所以本篇只分析虚拟DOM

本篇文章从源码出发,分析虚拟DOM的核心渲染原理(首次渲染),以及React对它做的性能优化点。

说实话React源码真的很难读😅,如果本篇文章帮助到了你,那么请给个赞👍支持一下吧。

开发中的常见问题

  • 为何必须引用React
  • 自定义的React组件为何必须大写
  • React如何防止XSS
  • ReactDiff算法和其他的Diff算法有何区别
  • keyReact中的作用
  • 如何写出高性能的React组件

如果你对上面几个问题还存在疑问,说明你对React的虚拟DOM以及Diff算法实现原理还有所欠缺,那么请好好阅读本篇文章吧。

首先我们来看看到底什么是虚拟DOM:

虚拟DOM

image

在原生的JavaScript程序中,我们直接对DOM进行创建和更改,而DOM元素通过我们监听的事件和我们的应用程序进行通讯。

React会先将你的代码转换成一个JavaScript对象,然后这个JavaScript对象再转换成真实DOM。这个JavaScript对象就是所谓的虚拟DOM

比如下面一段html代码:

<div class="title">
      <span>Hello ConardLi</span>
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
</div>

React可能存储为这样的JS代码:


const VitrualDom = {
  type: 'div',
  props: { class: 'title' },
  children: [
    {
      type: 'span',
      children: 'Hello ConardLi'
    },
    {
      type: 'ul',
      children: [
        { type: 'li', children: '苹果' },
        { type: 'li', children: '橘子' }
      ]
    }
  ]
}

当我们需要创建或更新元素时,React首先会让这个VitrualDom对象进行创建和更改,然后再将VitrualDom对象渲染成真实DOM

当我们需要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。

为何使用虚拟DOM

React为何采用VitrualDom这种方案呢?

提高开发效率

使用JavaScript,我们在编写应用程序时的关注点在于如何更新DOM

使用React,你只需要告诉React你想让视图处于什么状态,React则通过VitrualDom确保DOM与该状态相匹配。你不必自己去完成属性操作、事件处理、DOM更新,React会替你完成这一切。

这让我们更关注我们的业务逻辑而非DOM操作,这一点即可大大提升我们的开发效率。

关于提升性能

很多文章说VitrualDom可以提升性能,这一说法实际上是很片面的。

直接操作DOM是非常耗费性能的,这一点毋庸置疑。但是React使用VitrualDom也是无法避免操作DOM的。

如果是首次渲染,VitrualDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。

VitrualDom的优势在于ReactDiff算法和批处理策略,React在页面更新之前,提前计算好了如何进行更新和渲染DOM。实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。所以,在这个过程中React帮助我们"提升了性能"。

所以,我更倾向于说,VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。

如果您对本部分的分析有什么不同见解,欢迎在评论区拍砖。

跨浏览器兼容

image

React基于VitrualDom自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题。

跨平台兼容

image

VitrualDomReact带来了跨平台渲染的能力。以React Native为例子。React根据VitrualDom画出相应平台的ui层,只不过不同平台画的姿势不同而已。

虚拟DOM实现原理

如果你不想看繁杂的源码,或者现在没有足够时间,可以跳过这一章,直接👇虚拟DOM原理总结

image

在上面的图上我们继续进行扩展,按照图中的流程,我们依次来分析虚拟DOM的实现原理。

JSX和createElement

我们在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写:

class Hello extends Component {
  render() {
    return <div>Hello ConardLi</div>;
  }
}

第二种是直接使用React.createElement编写:

class Hello extends Component {
  render() {
    return React.createElement('div', null, `Hello ConardLi`);
  }
}

实际上,上面两种写法是等价的,JSX只是为 React.createElement(component, props, ...children) 方法提供的语法糖。也就是说所有的JSX 代码最后都会转换成React.createElement(...) Babel帮助我们完成了这个转换的过程。

如下面的JSX

<div>
  <img data-original="avatar.png" className="profile" />
  <Hello />
</div>;

将会被Babel转换为

React.createElement("div", null, React.createElement("img", {
  src: "avatar.png",
  className: "profile"
}), React.createElement(Hello, null));

注意,babel在编译时会判断JSX中组件的首字母,当首字母为小写时,其被认定为原生DOM标签,createElement的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象;

另外,由于JSX提前要被Babel编译,所以JSX是不能在运行时动态选择类型的,比如下面的代码:

function Story(props) {
  // Wrong! JSX type can't be an expression.
  return <components[props.storyType] story={props.story} />;
}

需要变成下面的写法:

function Story(props) {
  // Correct! JSX type can be a capitalized variable.
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

所以,使用JSX你需要安装Babel插件babel-plugin-transform-react-jsx

{
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

创建虚拟DOM

下面我们来看看虚拟DOM的真实模样,将下面的JSX代码在控制台打印出来:

<div className="title">
      <span>Hello ConardLi</span>
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
</div>

image

这个结构和我们上面自己描绘的结构很像,那么React是如何将我们的代码转换成这个结构的呢,下面我们来看看createElement函数的具体实现(文中的源码经过精简)。

image

createElement函数内部做的操作很简单,将props和子元素进行处理后返回一个ReactElement对象,下面我们来逐一分析:

(1).处理props:

image

  • 1.将特殊属性refkeyconfig中取出并赋值
  • 2.将特殊属性selfsourceconfig中取出并赋值
  • 3.将除特殊属性的其他属性取出并赋值给props

后面的文章会详细介绍这些特殊属性的作用。

(2).获取子元素

image

  • 1.获取子元素的个数 —— 第二个参数后面的所有参数
  • 2.若只有一个子元素,赋值给props.children
  • 3.若有多个子元素,将子元素填充为一个数组赋值给props.children

(3).处理默认props

image

  • 将组件的静态属性defaultProps定义的默认props进行赋值

ReactElement

ReactElement将传入的几个属性进行组合,并返回。

  • type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class
  • key:组件的唯一标识,用于Diff算法,下面会详细介绍
  • ref:用于访问原生dom节点
  • props:传入组件的props
  • owner:当前正在构建的Component所属的Component

$$typeof:一个我们不常见到的属性,它被赋值为REACT_ELEMENT_TYPE

var REACT_ELEMENT_TYPE =
  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
  0xeac7;

可见,$$typeof是一个Symbol类型的变量,这个变量可以防止XSS

如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能会成为一个问题:

// JSON
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* put your exploit here */'
    },
  },
};
let message = { text: expectedTextButGotJSON };
<p>
  {message.text}
</p>

JSON中不能存储Symbol类型的变量。

ReactElement.isValidElement函数用来判断一个React组件是否是有效的,下面是它的具体实现。

ReactElement.isValidElement = function (object) {
  return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
};

可见React渲染时会把没有$$typeof标识,以及规则校验不通过的组件过滤掉。

当你的环境不支持Symbol时,$$typeof被赋值为0xeac7,至于为什么,React开发者给出了答案:

0xeac7看起来有点像React

selfsource只有在非生产环境才会被加入对象中。

  • self指定当前位于哪个组件实例。
  • _source指定调试代码来自的文件(fileName)和代码行数(lineNumber)。

虚拟DOM转换为真实DOM

上面我们分析了代码转换成了虚拟DOM的过程,下面来看一下React如何将虚拟DOM转换成真实DOM

本部分逻辑较复杂,我们先用流程图梳理一下整个过程,整个过程大概可分为四步:

image

过程1:初始参数处理

在编写好我们的React组件后,我们需要调用ReactDOM.render(element, container[, callback])将组件进行渲染。

render函数内部实际调用了_renderSubtreeIntoContainer,我们来看看它的具体实现:

  render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
  },

image

  • 1.将当前组件使用TopLevelWrapper进行包裹

TopLevelWrapper只一个空壳,它为你需要挂载的组件提供了一个rootID属性,并在render函数中返回该组件。

TopLevelWrapper.prototype.render = function () {
  return this.props.child;
};

ReactDOM.render函数的第一个参数可以是原生DOM也可以是React组件,包裹一层TopLevelWrapper可以在后面的渲染中将它们进行统一处理,而不用关心是否原生。

  • 2.判断根结点下是否已经渲染过元素,如果已经渲染过,判断执行更新或者卸载操作
  • 3.处理shouldReuseMarkup变量,该变量表示是否需要重新标记元素
  • 4.调用将上面处理好的参数传入_renderNewRootComponent,渲染完成后调用callback

_renderNewRootComponent中调用instantiateReactComponent对我们传入的组件进行分类包装:

image

根据组件的类型,React根据原组件创建了下面四大类组件,对组件进行分类渲染:

  • ReactDOMEmptyComponent:空组件
  • ReactDOMTextComponent:文本
  • ReactDOMComponent:原生DOM
  • ReactCompositeComponent:自定义React组件

他们都具备以下三个方法:

  • construct:用来接收ReactElement进行初始化。
  • mountComponent:用来生成ReactElement对应的真实DOMDOMLazyTree
  • unmountComponent:卸载DOM节点,解绑事件。

具体是如何渲染我们在过程3中进行分析。

过程2:批处理、事务调用

_renderNewRootComponent中使用ReactUpdates.batchedUpdates调用batchedMountComponentIntoNode进行批处理。

ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);

batchedMountComponentIntoNode中,使用transaction.perform调用mountComponentIntoNode让其基于事务机制进行调用。

 transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);

关于批处理事务,在我前面的分析setState执行机制中有更多介绍。

过程3:生成html

mountComponentIntoNode函数中调用ReactReconciler.mountComponent生成原生DOM节点。

mountComponent内部实际上是调用了过程1生成的四种对象的mountComponent方法。首先来看一下ReactDOMComponent

image

  • 1.对特殊DOM标签、props进行处理。
  • 2.根据标签类型创建DOM节点。
  • 3.调用_updateDOMPropertiesprops插入到DOM节点,_updateDOMProperties也可用于props Diff,第一个参数为上次渲染的props,第二个参数为当前props,若第一个参数为空,则为首次创建。
  • 4.生成一个DOMLazyTree对象并调用_createInitialChildren将孩子节点渲染到上面。

那么为什么不直接生成一个DOM节点而是要创建一个DOMLazyTree呢?我们先来看看_createInitialChildren做了什么:

image

判断当前节点的dangerouslySetInnerHTML属性、孩子节点是否为文本和其他节点分别调用DOMLazyTreequeueHTMLqueueTextqueueChild

image

可以发现:DOMLazyTree实际上是一个包裹对象,node属性中存储了真实的DOM节点,childrenhtmltext分别存储孩子、html节点和文本节点。

它提供了几个方法用于插入孩子、html以及文本节点,这些插入都是有条件限制的,当enableLazy属性为true时,这些孩子、html以及文本节点会被插入到DOMLazyTree对象中,当其为false时会插入到真实DOM节点中。

var enableLazy = typeof document !== 'undefined' &&
  typeof document.documentMode === 'number' ||
  typeof navigator !== 'undefined' &&
  typeof navigator.userAgent === 'string' &&
  /\bEdge\/\d/.test(navigator.userAgent);

可见:enableLazy是一个变量,当前浏览器是IEEdge时为true

IE(8-11)Edge浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树。

所以lazyTree主要解决的是在IE(8-11)Edge浏览器中插入节点的效率问题,在后面的过程4我们会分析到:若当前是IEEdge,则需要递归插入DOMLazyTree中缓存的子节点,其他浏览器只需要插入一次当前节点,因为他们的孩子已经被渲染好了,而不用担心效率问题。

下面来看一下ReactCompositeComponent,由于代码非常多这里就不再贴这个模块的代码,其内部主要做了以下几步:

  • 处理propscontex等变量,调用构造函数创建组件实例
  • 判断是否为无状态组件,处理state
  • 调用performInitialMount生命周期,处理子节点,获取markup
  • 调用componentDidMount生命周期

performInitialMount函数中,首先调用了componentWillMount生命周期,由于自定义的React组件并不是一个真实的DOM,所以在函数中又调用了孩子节点的mountComponent。这也是一个递归的过程,当所有孩子节点渲染完成后,返回markup并调用componentDidMount

过程4:渲染html

mountComponentIntoNode函数中调用将上一步生成的markup插入container容器。

在首次渲染时,_mountImageIntoNode会清空container的子节点后调用DOMLazyTree.insertTreeBefore

image

判断是否为fragment节点或者<object>插件:

  • 如果是以上两种,首先调用insertTreeChildren将此节点的孩子节点渲染到当前节点上,再将渲染完的节点插入到html
  • 如果是其他节点,先将节点插入到插入到html,再调用insertTreeChildren将孩子节点插入到html
  • 若当前不是IEEdge,则不需要再递归插入子节点,只需要插入一次当前节点。

image

  • 判断不是IEbEdgereturn
  • children不为空,递归insertTreeBefore进行插入
  • 渲染html节点
  • 渲染文本节点

原生DOM事件代理

有关虚拟DOM的事件机制,我曾专门写过一篇文章,有兴趣可以👇【React深入】React事件机制

虚拟DOM原理、特性总结

React组件的渲染流程

  • 使用React.createElementJSX编写React组件,实际上所有的JSX 代码最后都会转换成React.createElement(...) Babel帮助我们完成了这个转换的过程。
  • createElement函数对keyref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个ReactElement对象(所谓的虚拟DOM)。
  • ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM

虚拟DOM的组成

ReactElementelement对象,我们的组件最终会被渲染成下面的结构:

  • type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class
  • key:组件的唯一标识,用于Diff算法,下面会详细介绍
  • ref:用于访问原生dom节点
  • props:传入组件的propschidrenprops中的一个属性,它存储了当前组件的孩子节点,可以是数组(多个孩子节点)或对象(只有一个孩子节点)
  • owner:当前正在构建的Component所属的Component
  • self:(非生产环境)指定当前位于哪个组件实例
  • _source:(非生产环境)指定调试代码来自的文件(fileName)和代码行数(lineNumber)

防止XSS

ReactElement对象还有一个$$typeof`属性,它是一个`Symbol`类型的变量`Symbol.for('react.element')`,当环境不支持`Symbol`时,`$$typeof被赋值为0xeac7

这个变量可以防止XSS。如果你的服务器有一个漏洞,允许用户存储任意JSON对象, 而客户端代码需要一个字符串,这可能为你的应用程序带来风险。JSON中不能存储Symbol类型的变量,而React渲染时会把没有$$typeof标识的组件过滤掉。

批处理和事务

React在渲染虚拟DOM时应用了批处理以及事务机制,以提高渲染性能。

关于批处理以及事务机制,在我之前的文章【React深入】setState的执行机制中有详细介绍。

针对性的性能优化

IE(8-11)Edge浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树。

React通过lazyTree,在IE(8-11)Edge中进行单个节点依次渲染节点,而在其他浏览器中则首先将整个大的DOM结构构建好,然后再整体插入容器。

并且,在单独渲染节点时,React还考虑了fragment等特殊节点,这些节点则不会一个一个插入渲染。

虚拟DOM事件机制

React自己实现了一套事件机制,其将所有绑定在虚拟DOM上的事件映射到真正的DOM事件,并将所有的事件都代理到document上,自己模拟了事件冒泡和捕获的过程,并且进行统一的事件分发。

React自己构造了合成事件对象SyntheticEvent,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括stopPropagation() preventDefault() 等等,在所有浏览器中他们工作方式都相同。这抹平了各个浏览器的事件兼容性问题。

上面只分析虚拟DOM首次渲染的原理和过程,当然这并不包括虚拟 DOM进行 Diff的过程,下一篇文章我们再来详细探讨。

关于开篇提的几个问题,我们在下篇文章中进行统一回答。

推荐阅读

末尾

本文源码中的版本为React15版本,相对16版本会有一些出入,关于16版本的改动,后面的文章会单独分析。

文中如有错误,欢迎在评论区指正,或者您对文章的排版,阅读体验有什么好的建议,欢迎在评论区指出,谢谢阅读。

想阅读更多优质文章、下载文章中思维导图源文件、阅读文中demo源码、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!

推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。

图片描述

查看原文

赞 73 收藏 54 评论 4

Aima 赞了文章 · 2020-07-02

Observable详解

浏览新版,请访问 RxJS Observable

在介绍 Observable 之前,我们要先了解两个设计模式:

  • Observer Pattern - (观察者模式)
  • Iterator Pattern - (迭代器模式)

这两个模式是 Observable 的基础,下面我们先来介绍一下 Observer Pattern。

Observer Pattern

观察者模式定义

观察者模式软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。 — 维基百科

观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

我们可以使用日常生活中,期刊订阅的例子来形象地解释一下上面的概念。期刊订阅包含两个主要的角色:期刊出版方和订阅者,他们之间的关系如下:

  • 期刊出版方 - 负责期刊的出版和发行工作
  • 订阅者 - 只需执行订阅操作,新版的期刊发布后,就会主动收到通知,如果取消订阅,以后就不会再收到通知

在观察者模式中也有两个主要角色:Subject (主题) 和 Observer (观察者) 。它们分别对应例子中的期刊出版方和订阅者。接下来我们来看张图,从而加深对上面概念的理解。

图片描述

观察者模式优缺点

观察者模式的优点:

  • 支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用

观察者模式的缺点:

  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃

观察者模式的应用

在前端领域,观察者模式被广泛地使用。最常见的例子就是为 DOM 对象添加事件监听,具体示例如下:

<button id="btn">确认</button>

function clickHandler(event) {
    console.log('用户已点击确认按钮!');
}
document.getElementById("btn").addEventListener('click', clickHandler);

上面代码中,我们通过 addEventListener API 监听 button 对象上的点击事件,当用户点击按钮时,会自动执行我们的 clickHandler 函数。

观察者模式实战

Subject 类定义:

class Subject {
    
    constructor() {
        this.observerCollection = [];
    }
    
    registerObserver(observer) {
        this.observerCollection.push(observer);
    }
    
    unregisterObserver(observer) {
        let index = this.observerCollection.indexOf(observer);
        if(index >= 0) this.observerCollection.splice(index, 1);
    }
    
    notifyObservers() {
        this.observerCollection.forEach((observer)=>observer.notify());
    }
}

Observer 类定义:

class Observer {
    
    constructor(name) {
        this.name = name;
    }
    
    notify() {
        console.log(`${this.name} has been notified.`);
    }
}

使用示例:

let subject = new Subject(); // 创建主题对象

let observer1 = new Observer('semlinker'); // 创建观察者A - 'semlinker'
let observer2 = new Observer('lolo'); // 创建观察者B - 'lolo'

subject.registerObserver(observer1); // 注册观察者A
subject.registerObserver(observer2); // 注册观察者B
 
subject.notifyObservers(); // 通知观察者

subject.unregisterObserver(observer1); // 移除观察者A

subject.notifyObservers(); // 验证是否成功移除

以上代码成功运行后控制台的输出结果:

semlinker has been notified. # 输出一次
2(unknown) lolo has been notified. # 输出两次

需要注意的是,在观察者模式中,通常情况下调用注册观察者后,会返回一个函数,用于移除监听,有兴趣的读者,可以自己尝试一下。(备注:在 Angular 1.x 中调用 $scope.$on() 方法后,就会返回一个函数,用于移除监听)

Iterator Pattern

迭代器模式定义

迭代器(Iterator)模式,又叫做游标(Cursor)模式。它提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

迭代器模式的优缺点

迭代器模式的优点:

  • 简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标取得,但用户需要在对集合了解的前提下,自行遍历对象,但是对于 hash 表来说,用户遍历起来就比较麻烦。而引入迭代器方法后,用户用起来就简单的多了。
  • 封装性良好,用户只需要得到迭代器就可以遍历,而不用去关心遍历算法。

迭代器模式的缺点:

  • 遍历过程是一个单向且不可逆的遍历

ECMAScript 迭代器

在 ECMAScript 中 Iterator 最早其实是要采用类似 Python 的 Iterator 规范,就是 Iterator 在没有元素之后,执行 next 会直接抛出错误;但后来经过一段时间讨论后,决定采更 functional 的做法,改成在取得最后一个元素之后执行 next 永远都回传 { done: true, value: undefined }

一个迭代器对象 ,知道如何每次访问集合中的一项, 并记录它的当前在序列中所在的位置。在 JavaScript 中迭代器是一个对象,它提供了一个 next() 方法,返回序列中的下一项。这个方法返回包含 donevalue 两个属性的对象。对象的取值如下:

  • 在最后一个元素前:{ done: false, value: elementValue }
  • 在最后一个元素后:{ done: true, value: undefined }

详细信息可以参考 - 可迭代协议和迭代器协议

ES 5 迭代器

接下来我们来创建一个 makeIterator 函数,该函数的参数类型是数组,当调用该函数后,返回一个包含 next() 方法的 Iterator 对象, 其中 next() 方法是用来获取容器对象中下一个元素。具体示例如下:

function makeIterator(array){
    var nextIndex = 0;
    
    return {
       next: function(){
           return nextIndex < array.length ?
               {value: array[nextIndex++], done: false} :
               {done: true};
       }
    }
}

一旦初始化, next() 方法可以用来依次访问可迭代对象中的元素:

var it = makeIterator(['yo', 'ya']);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done);  // true

ES 6 迭代器

在 ES 6 中我们可以通过 Symbol.iterator 来创建可迭代对象的内部迭代器,具体示例如下:

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

调用 next() 方法来获取数组中的元素:

> iter.next()
{ value: 'a', done: false }
> iter.next()
{ value: 'b', done: false }
> iter.next()
{ value: 'c', done: false }
> iter.next()
{ value: undefined, done: true }

ES 6 中可迭代的对象:

  • Arrays
  • Strings
  • Maps
  • Sets
  • DOM data structures (work in progress)

Observable

RxJS 是基于观察者模式和迭代器模式以函数式编程思维来实现的。RxJS 中含有两个基本概念:Observables 与 Observer。Observables 作为被观察者,是一个值或事件的流集合;而 Observer 则作为观察者,根据 Observables 进行处理。

Observables 与 Observer 之间的订阅发布关系(观察者模式) 如下:

  • 订阅:Observer 通过 Observable 提供的 subscribe() 方法订阅 Observable。
  • 发布:Observable 通过回调 next 方法向 Observer 发布事件。

Proposal Observable

自定义 Observable

如果你想真正了解 Observable,最好的方式就是自己写一个。其实 Observable 就是一个函数,它接受一个 Observer 作为参数然后返回另一个函数。

它的基本特征:

  • 是一个函数
  • 接受一个 Observer 对象 (包含 next、error、complete 方法的对象) 作为参数
  • 返回一个 unsubscribe 函数,用于取消订阅

它的作用:

作为生产者与观察者之间的桥梁,并返回一种方法来解除生产者与观察者之间的联系,其中观察者用于处理时间序列上数据流。接下来我们来看一下 Observable 的基础实现:

DataSource - 数据源

class DataSource {
  constructor() {
    let i = 0;
    this._id = setInterval(() => this.emit(i++), 200); // 创建定时器
  }
  
  emit(n) {
    const limit = 10;  // 设置数据上限值
    if (this.ondata) {
      this.ondata(n);
    }
    if (n === limit) {
      if (this.oncomplete) {
        this.oncomplete();
      }
      this.destroy();
    }
  }
  
  destroy() { // 清除定时器
    clearInterval(this._id);
  }
}

myObservable

function myObservable(observer) {
    let datasource = new DataSource(); // 创建数据源
    datasource.ondata = (e) => observer.next(e); // 处理数据流
    datasource.onerror = (err) => observer.error(err); // 处理异常
    datasource.oncomplete = () => observer.complete(); // 处理数据流终止
    return () => { // 返回一个函数用于,销毁数据源
        datasource.destroy();
    };
}

使用示例:

const unsub = myObservable({
  next(x) { console.log(x); },
  error(err) { console.error(err); },
  complete() { console.log('done')}
});

/**
* 移除注释,可以测试取消订阅
*/
// setTimeout(unsub, 500); 

具体运行结果,可以查看线上示例

SafeObserver - 更好的 Observer

上面的示例中,我们使用一个包含了 next、error、complete 方法的普通 JavaScript 对象来定义观察者。一个普通的 JavaScript 对象只是一个开始,在 RxJS 5 里面,为开发者提供了一些保障机制,来保证一个更安全的观察者。以下是一些比较重要的原则:

  • 传入的 Observer 对象可以不实现所有规定的方法 (next、error、complete 方法)
  • complete 或者 error 触发之后再调用 next 方法是没用的
  • 调用 unsubscribe 方法后,任何方法都不能再被调用了
  • completeerror 触发后,unsubscribe 也会自动调用
  • nextcompleteerror 出现异常时,unsubscribe 也会自动调用以保证资源不会浪费
  • nextcompleteerror是可选的。按需处理即可,不必全部处理

为了完成上述目标,我们得把传入的匿名 Observer 对象封装在一个 SafeObserver 里以提供上述保障。SafeObserver 的具体实现如下:

class SafeObserver {
  constructor(destination) {
    this.destination = destination;
  }
  
  next(value) {
    // 尚未取消订阅,且包含next方法
    if (!this.isUnsubscribed && this.destination.next) {
      try {
        this.destination.next(value);
      } catch (err) {
        // 出现异常时,取消订阅释放资源,再抛出异常
        this.unsubscribe();
        throw err;
      }
    }
  }
  
  error(err) {
    // 尚未取消订阅,且包含error方法
    if (!this.isUnsubscribed && this.destination.error) {
      try {
        this.destination.error(err);
      } catch (e2) {
        // 出现异常时,取消订阅释放资源,再抛出异常
        this.unsubscribe();
        throw e2;
      }
      this.unsubscribe();
    }
  }

  complete() {
    // 尚未取消订阅,且包含complete方法
    if (!this.isUnsubscribed && this.destination.complete) {
      try {
        this.destination.complete();
      } catch (err) {
        // 出现异常时,取消订阅释放资源,再抛出异常
        this.unsubscribe();
        throw err;
      }
      this.unsubscribe();
    }
  }
  
  unsubscribe() { // 用于取消订阅
    this.isUnsubscribed = true;
    if (this.unsub) {
      this.unsub();
    }
  }
}

myObservable - 使用 SafeObserver

function myObservable(observer) {
  const safeObserver = new SafeObserver(observer); // 创建SafeObserver对象
  const datasource = new DataSource(); // 创建数据源
  datasource.ondata = (e) => safeObserver.next(e);
  datasource.onerror = (err) => safeObserver.error(err);
  datasource.oncomplete = () => safeObserver.complete();

  safeObserver.unsub = () => { // 为SafeObserver对象添加unsub方法
    datasource.destroy();
  };
  // 绑定this上下文,并返回unsubscribe方法
  return safeObserver.unsubscribe.bind(safeObserver); 
}

使用示例:

const unsub = myObservable({
  next(x) { console.log(x); },
  error(err) { console.error(err); },
  complete() { console.log('done')}
});

具体运行结果,可以查看线上示例

Operators - 也是函数

Operator 是一个函数,它接收一个 Observable 对象,然后返回一个新的 Observable 对象。当我们订阅新返回的 Observable 对象时,它内部会自动订阅前一个 Observable 对象。接下来我们来实现常用的 map 操作符:

Observable 实现:

class Observable {
  constructor(_subscribe) {
    this._subscribe = _subscribe;
  }
  
  subscribe(observer) {
    const safeObserver = new SafeObserver(observer);
    safeObserver.unsub = this._subscribe(safeObserver);
    return safeObserver.unsubscribe.bind(safeObserver);
  }
}

map 操作符实现:

function map(source, project) {
  return new Observable((observer) => {
    const mapObserver = {
      next: (x) => observer.next(project(x)),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    };
    return source.subscribe(mapObserver);
  });
}

具体运行结果,可以查看线上示例

改进 Observable - 支持 Operator 链式调用

如果把 Operator 都写成如上那种独立的函数,我们链式代码会逐渐变丑:

map(map(myObservable, (x) => x + 1), (x) => x + 2);

对于上面的代码,想象一下有 5、6 个嵌套着的 Operator,再加上更多、更复杂的参数,基本上就没法儿看了。

你也可以试下 Texas Toland 提议的简单版管道实现,合并压缩一个数组的Operator并生成一个最终的Observable,不过这意味着要写更复杂的 Operator,上代码:JSBin。其实写完后你会发现,代码也不怎么漂亮:

pipe(myObservable, map(x => x + 1), map(x => x + 2));

理想情况下,我们想将代码用更自然的方式链起来:

myObservable.map(x => x + 1).map(x => x + 2);

幸运的是,我们已经有了这样一个 Observable 类,我们可以基于 prototype 在不增加复杂度的情况下支持多 Operators 的链式结构,下面我们采用prototype方式再次实现一下 Observable

Observable.prototype.map = function (project) {
    return new Observable((observer) => {
        const mapObserver = {
            next: (x) => observer.next(project(x)),
            error: (err) => observer.error(err),
            complete: () => observer.complete()
        };
        return this.subscribe(mapObserver);
    });
};

现在我们终于有了一个还不错的实现。这样实现还有其他好处,例如:可以写子类继承 Observable 类,然后在子类中重写某些内容以优化程序。

接下来我们来总结一下该部分的内容:Observable 就是函数,它接受 Observer 作为参数,又返回一个函数。如果你也写了一个函数,接收一个 Observer 作为参数,又返回一个函数,那么,它是异步的、还是同步的 ?其实都不是,它就只是一个函数。任何函数的行为都依赖于它的具体实现,所以当你处理一个 Observable 时,就把它当成一个普通函数,里面没有什么黑魔法。当你要构建 Operator 链时,你需要做的其实就是生成一个函数将一堆 Observers 链接在一起,然后让真正的数据依次穿过它们。

Rx.Observable.create

var observable = Rx.Observable
    .create(function(observer) {
        observer.next('Semlinker'); // RxJS 4.x 以前的版本用 onNext
        observer.next('Lolo');
    });
    
// 订阅这个 Observable    
observable.subscribe(function(value) {
    console.log(value);
});

以上代码运行后,控制台会依次输出 'Semlinker' 和 'Lolo' 两个字符串。

需要注意的是,很多人认为 RxJS 中的所有操作都是异步的,但其实这个观念是错的。RxJS 的核心特性是它的异步处理能力,但它也是可以用来处理同步的行为。具体示例如下:

var observable = Rx.Observable
    .create(function(observer) {
        observer.next('Semlinker'); // RxJS 4.x 以前的版本用 onNext
        observer.next('Lolo');
    });
    
console.log('start');
observable.subscribe(function(value) {
    console.log(value);
});
console.log('end');

以上代码运行后,控制台的输出结果:

start
Semlinker
Lolo
end

当然我们也可以用它处理异步行为:

var observable = Rx.Observable
    .create(function(observer) {
        observer.next('Semlinker'); // RxJS 4.x 以前的版本用 onNext
        observer.next('Lolo');
        
        setTimeout(() => {
            observer.next('RxJS Observable');
        }, 300);
    })
    
console.log('start');
observable.subscribe(function(value) {
    console.log(value);
});
console.log('end');

以上代码运行后,控制台的输出结果:

start
Semlinker
Lolo
end
RxJS Observable

从以上例子中,我们可以得出一个结论 - Observable 可以应用于同步和异步的场合。

Observable - Creation Operator

RxJS 中提供了很多操作符,用于创建 Observable 对象,常用的操作符如下:

  • create
  • of
  • from
  • fromEvent
  • fromPromise
  • empty
  • never
  • throw
  • interval
  • timer

上面的例子中,我们已经使用过了 create 操作符,接下来我们来看一下其它的操作符:

of

var source = Rx.Observable.of('Semlinker', 'Lolo');

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log(error);
    }
});

以上代码运行后,控制台的输出结果:

Semlinker
Lolo
complete!

from

var arr = [1, 2, 3];
var source = Rx.Observable.from(arr); // 也支持字符串,如 "Angular 2 修仙之路"

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log(error);
    }
});

以上代码运行后,控制台的输出结果:

1
2
3
complete!

fromEvent

Rx.Observable.fromEvent(document.querySelector('button'), 'click');

fromPromise

var source = Rx.Observable
  .fromPromise(new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Hello RxJS!');
    },3000)
}));
  
source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log(error);
    }
});

以上代码运行后,控制台的输出结果:

Hello RxJS!
complete!

empty

var source = Rx.Observable.empty();

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log(error);
    }
});

以上代码运行后,控制台的输出结果:

complete!

empty 操作符返回一个空的 Observable 对象,如果我们订阅该对象,它会立即返回 complete 信息。

never

var source = Rx.Observable.never();

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log(error);
    }
});

never 操作符会返回一个无穷的 Observable,当我们订阅它后,什么事情都不会发生,它是一个一直存在却什么都不做的 Observable 对象。

throw

var source = Rx.Observable.throw('Oop!');

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log('Throw Error: ' + error);
    }
});

以上代码运行后,控制台的输出结果:

Throw Error: Oop!

throw 操作如,只做一件事就是抛出异常。

interval

var source = Rx.Observable.interval(1000);

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log('Throw Error: ' + error);
    }
});

以上代码运行后,控制台的输出结果:

0
1
2
...

interval 操作符支持一个数值类型的参数,用于表示定时的间隔。上面代码表示每隔 1s,会输出一个递增的值,初始值从 0 开始。

timer

var source = Rx.Observable.timer(1000, 5000);

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log('Throw Error: ' + error);
    }
});

以上代码运行后,控制台的输出结果:

0 # 1s后
1 # 5s后
2 # 5s后
...

timer 操作符支持两个参数,第一个参数用于设定发送第一个值需等待的时间,第二个参数表示第一次发送后,发送其它值的间隔时间。此外,timer 操作符也可以只传递一个参数,具体如下:

var source = Rx.Observable.timer(1000);

source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log('Throw Error: ' + error);
    }
});

以上代码运行后,控制台的输出结果:

0
complete!

Subscription

有些时候对于一些 Observable 对象 (如通过 interval、timer 操作符创建的对象),当我们不需要的时候,要释放相关的资源,以避免资源浪费。针对这种情况,我们可以调用 Subscription 对象的 unsubscribe 方法来释放资源。具体示例如下:

var source = Rx.Observable.timer(1000, 1000);

// 取得subscription对象
var subscription = source.subscribe({
    next: function(value) {
        console.log(value);
    },
    complete: function() {
        console.log('complete!');
    },
    error: function(error) {
        console.log('Throw Error: ' + error);
    }
});

setTimeout(() => {
    subscription.unsubscribe();
}, 5000);

RxJS - Observer

Observer (观察者) 是一个包含三个方法的对象,每当 Observable 触发事件时,便会自动调用观察者的对应方法。

Observer 接口定义

interface Observer<T> {
  closed?: boolean; // 标识是否已经取消对Observable对象的订阅
  next: (value: T) => void;
  error: (err: any) => void;
  complete: () => void;
}

Observer 中的三个方法的作用:

  • next - 每当 Observable 发送新值的时候,next 方法会被调用
  • error - 当 Observable 内发生错误时,error 方法就会被调用
  • complete - 当 Observable 数据终止后,complete 方法会被调用。在调用 complete 方法之后,next 方法就不会再次被调用

接下来我们来看个具体示例:

var observable = Rx.Observable
    .create(function(observer) {
            observer.next('Semlinker');
            observer.next('Lolo');
            observer.complete();
            observer.next('not work');
    });
    
// 创建一个观察者
var observer = {
    next: function(value) {
        console.log(value);
    },
    error: function(error) {
        console.log(error);
    },
    complete: function() {
        console.log('complete');
    }
}

// 订阅已创建的observable对象
observable.subscribe(observer);

以上代码运行后,控制台的输出结果:

Semlinker
Lolo
complete

上面的例子中,我们可以看出,complete 方法执行后,next 就会失效,所以不会输出 not work

另外观察者可以不用同时包含 next、complete、error 三种方法,它可以只包含一个 next 方法,具体如下:

var observer = {
    next: function(value) {
        console.log(value);
    }
};

有时候 Observable 可能是一个无限的序列,例如 click 事件,对于这种场景,complete 方法就永远不会被调用。

我们也可以在调用 Observable 对象的 subscribe 方法时,依次传入 next、error、complete 三个函数,来创建观察者:

observable.subscribe(
    value => { console.log(value); },
    error => { console.log('Error: ', error); },
    () => { console.log('complete'); }
);

Pull vs Push

Pull 和 Push 是数据生产者和数据的消费者两种不同的交流方式。

什么是Pull?

在 "拉" 体系中,数据的消费者决定何时从数据生产者那里获取数据,而生产者自身并不会意识到什么时候数据将会被发送给消费者。

每一个 JavaScript 函数都是一个 "拉" 体系,函数是数据的生产者,调用函数的代码通过 ''拉出" 一个单一的返回值来消费该数据。

const add = (a, b) => a + b;
let sum = add(3, 4);

ES6介绍了 iterator迭代器Generator生成器 — 另一种 "拉" 体系,调用 iterator.next() 的代码是消费者,可从中拉取多个值

什么是Push?

在 "推" 体系中,数据的生产者决定何时发送数据给消费者,消费者不会在接收数据之前意识到它将要接收这个数据。

Promise(承诺) 是当今 JS 中最常见的 "推" 体系,一个Promise (数据的生产者)发送一个 resolved value (成功状态的值)来执行一个回调(数据消费者),但是不同于函数的地方的是:Promise 决定着何时数据才被推送至这个回调函数。

RxJS 引入了 Observables (可观察对象),一个全新的 "推" 体系。一个可观察对象是一个产生多值的生产者,当产生新数据的时候,会主动 "推送给" Observer (观察者)。

生产者消费者
pull拉被请求的时候产生数据决定何时请求数据
push推按自己的节奏生产数据对接收的数据进行处理

接下来我们来看张图,从而加深对上面概念的理解:

图片描述

Observable vs Promise

Observable(可观察对象)是基于推送(Push)运行时执行(lazy)的多值集合。

MagicQ单值多值
拉取(Pull)函数遍历器
推送(Push)PromiseObservable
  • Promise

    • 返回单个值
    • 不可取消的
  • Observable

    • 随着时间的推移发出多个值
    • 可以取消的
    • 支持 map、filter、reduce 等操作符
    • 延迟执行,当订阅的时候才会开始执行

延迟计算 & 渐进式取值

延迟计算

所有的 Observable 对象一定会等到订阅后,才开始执行,如果没有订阅就不会执行。

var source = Rx.Observable.from([1,2,3,4,5]);
var example = source.map(x => x + 1);

上面的示例中,因为 example 对象还未被订阅,所以不会进行运算。这跟数组不一样,具体如下:

var source = [1,2,3,4,5];
var example = source.map(x => x + 1); 

以上代码运行后,example 中就包含已运算后的值。

渐进式取值

数组中的操作符如:filter、map 每次都会完整执行并返回一个新的数组,才会继续下一步运算。具体示例如下:

var source = [1,2,3,4,5];
var example = source
                .filter(x => x % 2 === 0) // [2, 4]
                  .map(x => x + 1) // [3, 5]

关于数组中的 mapfilter 的详细信息,可以参考 - RxJS Functional Programming

为了更好地理解数组操作符的运算过程,我们可以参考下图:

图片描述
查看原图

虽然 Observable 运算符每次都会返回一个新的 Observable 对象,但每个元素都是渐进式获取的,且每个元素都会经过操作符链的运算后才输出,而不会像数组那样,每个阶段都得完整运算。具体示例如下:

var source = Rx.Observable.from([1,2,3,4,5]);
var example = source
              .filter(x => x % 2 === 0)
              .map(x => x + 1)

example.subscribe(console.log);

以上代码的执行过程如下:

  • source 发出 1,执行 filter 过滤操作,返回 false,该值被过滤掉
  • source 发出 2,执行 filter 过滤操作,返回 true,该值被保留,接着执行 map 操作,值被处理成 3,最后通过 console.log 输出
  • source 发出 3,执行 filter 过滤操作,返回 false,该值被过滤掉
  • source 发出 4,执行 filter 过滤操作,返回 true,该值被保留,接着执行 map 操作,值被处理成 5,最后通过 console.log 输出
  • source 发出 5,执行 filter 过滤操作,返回 false,该值被过滤掉

为了更好地理解 Observable 操作符的运算过程,我们可以参考下图:

图片描述
查看原图

学习资源

参考资源


查看原文

赞 96 收藏 138 评论 16

Aima 赞了文章 · 2020-05-08

Sketch 插件开发实践

sketch-plugin-boilerplate -- 快速创建侧边栏 sketch plugins 开发模版样例, 点赞 star 。

sketch

Sketch 是非常流行的 UI 设计工具,2014年随着 Sketch V43 版本增加 Symbols 功能、开放开发者权限,吸引了大批开发者的关注。

目前 Sketch 开发有两大热门课题:① React 组件渲染成 sketch 由 airbnb 团队发起,② 使用 skpm 构建开发 Sketch 插件。

Sketch 插件开发相关资料较少且不太完善,我们开发插件过程中可以重点参考官方文档,只是有些陈旧。官方有提供 JavaScript API 借助 CocoaScript bridge 访问内部 Sketch API 和 macOS 框架进行开发插件(Sketch 53~56 版 JS API 在 native MacOS 和 Sketch API 暴露的特殊环境中运行),提供的底层 API 功能有些薄弱,更深入的就需要了解掌握 Objective-CCocoaScriptAppKitSketch-Headers

Sketch 插件结构

Sketch Plugin 是一个或多个 scripts 的集合,每个 script 定义一个或多个 commands。Sketch Plugin 是以 .sketchplugin 扩展名的文件夹,包含文件和子文件夹。严格来说,Plugin 实际上是 OS X package,用作为 OS X bundle

Bundle 具有标准化分层结构的目录,其保存可执行代码和该代码使用的资源。

Plugin Bundle 文件夹结构

Bundles 包含一个 manifest.json 文件,一个或多个 scripts 文件(包含用 CocoaScript 或 JavaScript 编写的脚本),它实现了 Plugins 菜单中显示的命令,以及任意数量的共享库脚本和资源文件。

mrwalker.sketchplugin
  Contents/
    Sketch/
      manifest.json
      shared.js
      Select Circles.cocoascript
      Select Rectangles.cocoascript
    Resources/
      Screenshot.png
      Icon.png

最关键的文件是 manifest.json 文件,提供有关插件的信息。

小贴士:

Sketch 插件包可以使用 skpm 在构建过程中生成,skpm 提供 Sketch 官方插件模版:

💁 Tip: Any Github repo with a 'template' folder can be used as a custom template:

skpm create <project-name> --template=<username>/<repository>

Manifest

manifest.json 文件提供有关插件的信息,例如作者,描述,图标、从何处获取最新更新、定义的命令 (commands) 、调用菜单项 (menu) 以及资源的元数据。

{
  "name": "Select Shapes",
  "description": "Plugins to select and deselect shapes",
  "author": "Joe Bloggs",
  "homepage": "https://github.com/example/sketchplugins",
  "version": "1.0",
  "identifier": "com.example.sketch.shape-plugins",
  "appcast": "https://excellent.sketchplugin.com/excellent-plugin-appcast.xml",
  "compatibleVersion": "3",
  "bundleVersion": 1,
  "commands": [
    {
      "name": "All",
      "identifier": "all",
      "shortcut": "ctrl shift a",
      "script": "shared.js",
      "handler": "selectAll"
    },
    {
      "name": "Circles",
      "identifier": "circles",
      "script": "Select Circles.cocoascript"
    },
    {
      "name": "Rectangles",
      "identifier": "rectangles",
      "script": "Select Rectangles.cocoascript"
    }
  ],
  "menu": {
    "items": ["all", "circles", "rectangles"]
  }
}

Commands

声明一组 command 的信息,每个 command 以 Dictionary 数据结构形式存在。

  • script : 实现命令功能的函数所在的脚本
  • handler : 函数名,该函数实现命令的功能。Sketch 在调用该函数时,会传入 context 上下文参数。若未指定 handler,Sketch 会默认调用对应 script 中 onRun 函数
  • shortcut:命令的快捷键
  • name:显示在 Sketch Plugin 菜单中
  • identifier : 唯一标识,建议用 com.xxxx.xxx 格式,不要过长

Menu

Sketch 加载插件会根据指定的信息,在菜单栏中有序显示命令名。

在了解了 Sketch 插件结构之后,我们再来了解一下,sketch提供的官方 API: Actions API, Javascript API。

Sketch Actions API

Sketch Actions API 用于监听用户操作行为而触发事件,例如 OpenDocumen(打开文档)、CloseDocument(关闭文档)、Shutdown(关闭插件)、TextChanged(文本变化)等,具体详见官网:https://developer.sketch.com/...

  • register Actions

manifest.json 文件,配置相应 handlers。

示例:当 OpenDocument 事件被触发时调用 onOpenDocument handler 。

"commands" : [
  ...
  {
    "script" : "my-action-listener.js",
    "name" : "My Action Listener",
    "handlers" : {
      "actions": {
        "OpenDocument": "onOpenDocument"
      }
    },
    "identifier" : "my-action-listener-identifier"
  }
  ...
],

my-action-listener.js

export function onOpenDocument(context) {                
  context.actionContext.document.showMessage('Document Opened')
}
  • Action Context

Action 事件触发时会将 context.actionContext 传递给相应 handler。注意有些 Action 包含两个状态beginfinish,例如 SelectionChanged,需分别订阅 SelectionChanged.beginSelectionChanged.finish,否则会触发两次事件。

Sketch JS API

Sketch 插件开发大概有如下三种方式:① 纯使用 CocoaScript 脚本进行开发,② 通过 Javascript + CocoaScript 的混合开发模式, ③ 通过 AppKit + Objective-C 进行开发。Sketch 官方建议使用 JavaScript API 编写 Sketch 插件,且官方针对 Sketch Native API 封装了一套 JS API,目前还未涵盖所有场景, 若需要更丰富的底层 API 需结合 CocoaScript 进行实现。通过 JS API 可以很方便的对 Sketch 中 DocumentArtboardGroupLayer 进行相关操作以及导入导出等,可能需要考虑兼容性, JS API 原理图如下:

api-reference

CocoaScript

CocoaScript 实现 JavaScript 运行环境到 Objective-C 运行时的桥接功能,可通过桥接器编写 JavaScript 外部脚本访问内部 Sketch API 和 macOS 框架底层丰富的 API 功能。

小贴士:

Mocha 实现提供 JavaScript 运行环境到 Objective-C 运行时的桥接功能已包含在CocoaScript中。

CocoaScript 建立在 Apple 的 JavaScriptCore 之上,而 JavaScriptCore 是为 Safari 提供支持的 JavaScript 引擎,使用 CocoaScript 编写代码实际上就是在编写 JavaScript。CocoaScript 包括桥接器,可以从 JavaScript 访问 Apple 的 Cocoa 框架。

借助 CocoaScript 使用 JavaScript 调 Objective-C 语法:

  • 方法调用用 ‘.’ 语法
  • Objective-C 属性设置

    • Getter: object.name()
    • Setter: object.setName('Sketch')object.name='sketch'
  • 参数都放在 ‘ ( ) ’ 里
  • Objective-C 中 ' : '(参数与函数名分割符) 转换为 ' _ ',最后一个下划线是可选的
  • 返回值,JavaScript 统一用 var/const/let 设置类型
注意:详细 Objective-C to JavaScript 请参考 Mocha 文档

示例:

// oc: MSPlugin 的接口 valueForKey:onLayer:
NSString * value = [command valueForKey:kAutoresizingMask onLayer:currentLayer];

// cocoascript:
const value = command.valueForKey_onLayer(kAutoresizingMask, currentLayer);

// oc:
const app = [NSApplication sharedApplication];
[app displayDialog:msg withTitle:title];

// cocoascript:
const app = NSApplication.sharedApplication();
app.displayDialog_withTitle(msg, title)

// oc:
const openPanel = [NSOpenPanel openPanel]
[openPanel setTitle: "Choose a location…"]
[openPanel setPrompt: "Export"];

// cocoascript:
const openPanel = NSOpenPanel.openPanel
openPanel.setTitle("Choose a location…")
openPanel.setPrompt("Export")

Objective-C Classes

Sketch 插件系统可以完全访问应用程序的内部结构和 macOS 中的核心框架。Sketch 是用 Objective-C 构建的,其 Objective-C 类通过 Bridge (CocoaScript/mocha) 提供 Javascript API 调用,简单的了解 Sketch 暴露的相关类以及类方法,对我们开发插件非常有帮助。

使用 Bridge 定义的一些内省方法来访问以下信息:

String(context.document.class()) // MSDocument

const mocha = context.document.class().mocha()

mocha.properties() // array of MSDocument specific properties defined on a MSDocument instance
mocha.propertiesWithAncestors() // array of all the properties defined on a MSDocument instance

mocha.instanceMethods() // array of methods defined on a MSDocument instance
mocha.instanceMethodsWithAncestors()

mocha.classMethods() // array of methods defined on the MSDocument class
mocha.classMethodsWithAncestors()

mocha.protocols() // array of protocols the MSDocument class inherits from
mocha.protocolsWithAncestors()

Context

当输入插件定制的命令时,Sketch 会去寻找改命令对应的实现函数, 并传入 context 变量。context 包含以下变量:

  • command: MSPluginCommand 对象,当前执行命令
  • document: MSDocument 对象 ,当前文档
  • plugin: MSPluginBundle 对象,当前的插件 bundle,包含当前运行的脚本
  • scriptPath: NSString 当前执行脚本的绝对路径
  • scriptURL: 当前执行脚本的绝对路径,跟 scriptPath 不同的是它是个 NSURL 对象
  • selection: 一个 NSArray 对象,包含了当前选择的所有图层。数组中的每一个元素都是 MSLayer 对象
小贴士:MS 打头类名为 Sketch 封装类如图层基类 MSLayer)、文本层基类 MSTextLayer) 、位图层基类 MSBitmapLayer,NS 打头为 AppKit 中含有的类
const app = NSApplication.sharedApplication()

function initContext(context) {
        context.document.showMessage('初始执行脚本')
        
    const doc = context.document
    const page = doc.currentPage()
    const artboards = page.artboards()
    const selectedArtboard = page.currentArtboard() // 当前被选择的画板
    
    const plugin = context.plugin
    const command = context.command
    const scriptPath = context.scriptPath
    const scriptURL = context.scriptURL
    const selection = context.selection // 被选择的图层
}

Sketch 插件开发上手

前面我们了解了许多 Sketch 插件开发知识,那接下来实际上手两个小例子: ① 创建辅助内容面板窗口, ② 侧边栏导航。为了方便开发,我们在开发前需先进行如下操作:

崩溃保护

当 Sketch 运行发生崩溃,它会停用所有插件以避免循环崩溃。对于使用者,每次崩溃重启后手动在菜单栏启用所需插件非常繁琐。因此可以通过如下命令禁用该特性。

defaults write com.bohemiancoding.sketch3 disableAutomaticSafeMode true

插件缓存

通过配置启用或禁用缓存机制:

defaults write com.bohemiancoding.sketch3 AlwaysReloadScript -bool YES

该方法对于某些场景并不适用,如设置 COScript.currentCOScript().setShouldKeepAround(true) 区块会保持常驻在内存,那么则需要通过 coscript.setShouldKeepAround(false) 进行释放。

WebView 调试

如果插件实现方案使用 WebView 做界面,可通过以下配置开启调试功能。

defaults write com.bohemiancoding.sketch3 WebKitDeveloperExtras -bool YES

创建辅助内容面板窗口

首先我们先熟悉一下 macOS 下的辅助内容面板, 如下图最左侧 NSPanel 样例, 它是有展示区域,可设置样式效果,左上角有可操作按钮的辅助窗口。

Sketch 中要创建如下内容面板,需要使用 macOS 下 AppKit 框架中 NSPanel 类,它是 NSWindow 的子类,用于创建辅助窗口。内容面板外观样式设置,可通过 NSPanel 类相关属性进行设置, 也可通过 AppKitNSVisualEffectView 类添加模糊的背景效果。内容区域则可通过 AppKitWKWebView 类,单开 webview 渲染网页内容展示。

console

  • 创建 Panel
const panelWidth = 80;
const panelHeight = 240;

// Create the panel and set its appearance
const panel = NSPanel.alloc().init();
panel.setFrame_display(NSMakeRect(0, 0, panelWidth, panelHeight), true);
panel.setStyleMask(NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask);
panel.setBackgroundColor(NSColor.whiteColor());

// Set the panel's title and title bar appearance
panel.title = "";
panel.titlebarAppearsTransparent = true;

// Center and focus the panel
panel.center();
panel.makeKeyAndOrderFront(null);
panel.setLevel(NSFloatingWindowLevel);

// Make the plugin's code stick around (since it's a floating panel)
COScript.currentCOScript().setShouldKeepAround(true);

// Hide the Minimize and Zoom button
panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true);
panel.standardWindowButton(NSWindowZoomButton).setHidden(true);
  • Panel 添加模糊的背景
// Create the blurred background
const vibrancy = NSVisualEffectView.alloc().initWithFrame(NSMakeRect(0, 0, panelWidth, panelHeight));
vibrancy.setAppearance(NSAppearance.appearanceNamed(NSAppearanceNameVibrantLight));
vibrancy.setBlendingMode(NSVisualEffectBlendingModeBehindWindow);

// Add it to the panel
panel.contentView().addSubview(vibrancy);
  • Panel 插入 webview 渲染
  const wkwebviewConfig = WKWebViewConfiguration.alloc().init()
  const webView = WKWebView.alloc().initWithFrame_configuration(
    CGRectMake(0, 0, panelWidth, panelWidth),
    wkwebviewConfig
  )
  
  // Add it to the panel
  panel.contentView().addSubview(webView);
  
  // load file URL
  webview.loadFileURL_allowingReadAccessToURL(
    NSURL.URLWithString(url),
    NSURL.URLWithString('file:///')
  )

侧边栏导航开发

我们开发复杂的 Sketch 插件,一般都要开发侧边栏导航展示插件功能按钮,点击触发相关操作。那开发侧边栏导航,我们主要使用 AppKit 中的那些类呢,有 NSStackViewNSBoxNSImageNSImageViewNSButton 等,大致核心代码如下:

  // create toolbar
  const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0, 0, 40, 400))
  threadDictionary[SidePanelIdentifier] = toolbar
  toolbar.identifier = SidePanelIdentifier
  toolbar.setSpacing(8)
  toolbar.setFlipped(true)
  toolbar.setBackgroundColor(NSColor.windowBackgroundColor())
  toolbar.orientation = 1
    
  // add element
  toolbar.addView_inGravity(createImageView(NSMakeRect(0, 0, 40, 22), 'transparent', NSMakeSize(40, 22)), 1)
  const Logo = createImageView(NSMakeRect(0, 0, 40, 30), 'logo', NSMakeSize(40, 28))
  toolbar.addSubview(Logo)

  const contentView = context.document.documentWindow().contentView()
  const stageView = contentView.subviews().objectAtIndex(0)

  const views = stageView.subviews()
  const existId = views.find(d => ''.concat(d.identifier()) === identifier)

  const finalViews = []

  for (let i = 0; i < views.count(); i++) {
    const view = views[i]
    if (existId) {
      if (''.concat(view.identifier()) !== identifier) finalViews.push(view)
    } else {
      finalViews.push(view)
      if (''.concat(view.identifier()) === 'view_canvas') {
        finalViews.push(toolbar)
      }
    }
  }

    // add to main Window
  stageView.subviews = finalViews
  stageView.adjustSubviews()

详细见开源代码: https://github.com/o2team/ske... (欢迎 star 交流)

调试

当插件运行时,Sketch 将会创建一个与其关联的 JavaScript 上下文,可以使用 Safari 来调试该上下文。

在 Safari 中, 打开 Developer > 你的机器名称 > Automatically Show Web Inspector for JSContexts,同时启用选项 Automatically Pause Connecting to JSContext,否则检查器将在可以交互之前关闭(当脚本运行完时上下文会被销毁)。

现在就可以在代码中使用断点了,也可以在运行时检查变量的值等等。

日志

JavaScriptCore 运行 Sketch 插件的环境 也有提供类似调试 JavaScript 代码打 log 的方式,我们可以在关键步骤处放入一堆 console.log/console.error 等进行落点日志查看。

有以下几种选择可以查看日志:

  • 打开 Console.app 并查找 Sketch 日志
  • 查看 ~/Library/Logs/com.bohemiancoding.sketch3/Plugin Output.log 文件
  • 运行 skpm log 命令,该命令可以输出上面的文件(执行 skpm log -f 可以流式地输出日志)
  • 使用 skpm 开发的插件,安装 sketch-dev-tools,使用 console.log 打日志查看。

    console

SketchTool

SketchTool 包含在 Sketch 中的 CLI 工具,通过 SketchTool 可对 Sketch 文档执行相关操作:

sketchtool 二进制文件位于 Sketch 应用程序包中:

Sketch.app/Contents/Resources/sketchtool/bin/sketchtool

设置 alias

alias sketchtool="/Applications/Sketch.app/Contents/Resources/sketchtool/bin/sketchtool"

使用:

sketchtool -h  # 查看帮助
sketchtool export artboards path/to/document.sketch  # 导出画板
sketchtool dump path/to/document.sketch # 导出 Sketch 文档 JSON data
sketchtool metadata path/to/document.sketch # 查看 Sketch 文档元数据
sketchtool run [Plugin path] # 运行插件

注意:SketchTool 需要 OSX 10.11或更高版本。

Other Resources

sketch reference api

Github SketchAPI

查看原文

赞 2 收藏 1 评论 0

Aima 关注了标签 · 2020-03-11

safari

Safari 是苹果公司所开发的网页浏览器,并自带于 Mac OS X。Safari 从2003年开始成为 Mac OS X v10.3 与之后的默认浏览器,也是 iPhone、iPod touch、iPad 和 iPad 2 的指定浏览器。

关注 137

Aima 赞了文章 · 2019-11-22

webpack使用-详解DllPlugin

前言

(时光飞逝,转眼又偷懒了一个多月)

什么是DLL

DLL(Dynamic Link Library)文件为动态链接库文件,在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。

举个例子:很多产品都用到螺丝,但是工厂在生产不同产品时,不需要每次连带着把螺丝也生产出来,因为螺丝可以单独生产,并给多种产品使用。在这里螺丝的作用就可以理解为是dll。

为什么要使用Dll

通常来说,我们的代码都可以至少简单区分成业务代码第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码

还是上面的例子:把每次构建,当做是生产产品的过程,我们把生产螺丝的过程先提取出来,之后我们不管调整产品的功能或者设计(对应于业务代码变更),都不必重复生产螺丝(第三方模块不需要重复打包);除非是产品要使用新型号的螺丝(第三方模块需要升级),才需要去重新生产新的螺丝,然后接下来又可以专注于调整产品本身。

基本用法

使用dll时,可以把构建过程分成dll构建过程和主构建过程(实质也就是如此),所以需要两个构建配置文件,例如叫做webpack.config.jswebpack.dll.config.js

1. 使用DLLPlugin打包需要分离到动态库的模块

DllPluginwebpack内置的插件,不需要额外安装,直接配置webpack.dll.config.js文件:

module.exports = {=
  entry: {
    // 第三方库
    react: ['react', 'react-dom', 'react-redux']
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    filename: '[name].dll.js',
    path: resolve('dist/dll'),
    // library必须和后面dllplugin中的name一致 后面会说明
    library: '[name]_dll_[hash]'
  },
  plugins: [
  // 接入 DllPlugin
    new webpack.DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      name: '[name]_dll_[hash]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dist/dll', '[name].manifest.json')
    }),
  ]
}

我们先来看看,这一步到底做了什么。执行:webpack --config webpack.dll.config,然后到指定的输出文件夹查看输出:

  1. react.dll文件里是使用数组保存的模块,索引值就作为id;
  2. react.manifest.json文件里,是用来描述对应的dll文件里保存的模块

里暴露出刚刚构建的所有模块,如下:

{
  "name":"react_dll_553e24e2c44987d2578f",
  "content":{
    "./node_modules/webpack/node_modules/process/browser.js":{"id":0,"meta":{}},"./node_modules/react/node_modules/fbjs/lib/invariant.js":{"id":1,"meta":{}},"./node_modules/react/lib/Object.assign.js":{"id":2,"meta":{}},"./node_modules/react/node_modules/fbjs/lib/warning.js":{"id":3,"meta":{}}
    //省略相似代码
  }
}

2. 在主构建配置文件使用动态库文件

webpack.config中使用dll要用到DllReferencePlugin,这个插件通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 webpack_require 函数来 require 他们.

  new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require('./dist/dll/react.manifest.json')
  }),

第一步产出的manifest文件就用在这里,给主构建流程作为查找dll的依据:DllReferencePlugin去 manifest.json 文件读取 name 字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名,因此:在 webpack_dll.config.js 文件中,DllPlugin 中的 name 参数必须和 output.library 中保持一致。

3. 在入口文件引入dll文件。

生成的dll暴露出的是全局函数,因此还需要在入口文件里面引入对应的dll文件。

<body>
  <div id="app"></div>
  <!--引用dll文件-->
  <script data-original="../../dist/dll/react.dll.js" ></script>
</body>

作用

首先从前面的介绍,至少可以看出dll的两个作用

  1. 分离代码,业务代码和第三方模块可以被打包到不同的文件里,这个有几个好处:

    • 避免打包出单个文件的大小太大,不利于调试
    • 将单个大文件拆成多个小文件之后,一定情况下有利于加载(不超出浏览器一次性请求的文件数情况下,并行下载肯定比串行快)
  2. 提升构建速度。第三方库没有变更时,由于我们只构建业务相关代码,相比全部重新构建自然要快的多。

注意事项

从前面可以看到dll带来的优点,但并不意味着我们就应该把除业务代码外的所有代码全部都丢到dll中,举一个例子:
1.对于lodash这种第三方库,正确的用法是只去import所需的函数(用什么引什么),例如:

// 正确用法
import isPlainObject from 'lodash/isPlainObject'

//错误用法
import { isPlainObject } from 'lodash'

这两种写法的差别在于,打包时webpack会根据引用去打包依赖的内容,所以第一种写法,webpack只会打包lodash的isPlainObject库,第二种写法却会打包整个lodash。现在假设在项目中只是用到不同模块对lodash里的某几个函数并且没有对于某个函数重复使用非常多次,那么这时候把lodash添加到dll中,带来的收益就并不明显,反而导致2个问题:

  1. 由于打包了整个lodash,而导致打包后的文件总大小(注意是总大小)比原先还要大
  2. 在dll打包太多内容也需要耗费时间,虽然我们一般只在第三方模块更新之后才进行重新预编译(就是dll打包的过程),但是如果这个时间太长的话体验也不好、

实践与反思

放一张自己在一个比较大的项目中单纯使用dll之后的收益,提取的内容是 react相关的第三方库,和fish组件,构建时间从120s降低到80s左右(当然这个时间还是有点恐怖),构建前appjs的大小是680kb,拆分业务代码和第三方代码分别是400kb和380kb(这就是拆分后大小大于拆分前大小的例子),从这一点来看,对于常见第三方库是否要放进dll可能比较明确(比如react系列打包一般肯定不亏),但是还有一些就要结合具体的项目内容来进行判断和取舍。(强烈推荐使用webpack-bundle-analyzer插件进行性能分析)
图片描述
图片描述
图片描述

总结

本文介绍了Dllplugin的思想,基本用法和应用场景(关于使用的部分更详细的内容可以看官方文档),结合个人的一些实践经验,对于常见第三方库是否要放进dll可能比较明确(比如react系列打包一般肯定不亏),但是还有一些就要结合具体的项目内容来判断,例如我上面的实践的例子就说明目前的拆分还不够好。这一块也欢迎大家一起探讨。如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果对你有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流,主页有邮箱地址

查看原文

赞 36 收藏 19 评论 1

Aima 赞了文章 · 2019-07-09

Moment.js常见用法总结

Moment.js常见用法总结

Moment.js是一个轻量级的JavaScript时间库,它方便了日常开发中对时间的操作,提高了开发效率。

​ 日常开发中,通常会对时间进行下面这几个操作:比如获取时间,设置时间,格式化时间,比较时间等等。接下来,我将按照这些操作对Moment.js中的Doc进行整理分类,方便学习和日后的查阅。

获取时间

  • Start of Time

    moment().startOf(String)
    • 获取今天0时0分0秒
      moment().startOf('day')
    • 获取本周第一天(周日)0时0分0秒
      moment().startOf('week')
    • 获取本周周一0时0分0秒
      moment().startOf('isoWeek')
    • 获取当前月第一天0时0分0秒
      moment().startOf('month')
  • End of Time

    moment().endOf(String)
    • 获取今天23时59分59秒
      moment().endOf('day')
    • 获取本周最后一天(周六)23时59分59秒
      moment().endOf('week')
    • 获取本周周日23时59分59秒
      moment().endOf('isoWeek')
    • 获取当前月最后一天23时59分59秒
      moment().endOf('month')
  • Days in Month

    moment().daysInMonth()
    • 获取当前月的总天数
      moment().daysInMonth() 
  • Timestamp

    • 获取时间戳(以秒为单位)
      moment().format('X') // 返回值为字符串类型
      moment().unix() // 返回值为数值型
    • 获取时间戳(以毫秒为单位)
      moment().format('x') // 返回值为字符串类型
      moment().valueOf() // 返回值为数值型
  • Get Time

    • 获取年份
      moment().year()
      moment().get('year')
    • 获取月份
      moment().month() (0~11, 0: January, 11: December)
      moment().get('month')
    • 获取一个月中的某一天
      moment().date()
      moment().get('date')
    • 获取一个星期中的某一天
      moment().day() (0~6, 0: Sunday, 6: Saturday)
      moment().weekday() (0~6, 0: Sunday, 6: Saturday)
      moment().isoWeekday() (1~7, 1: Monday, 7: Sunday)
      moment().get('day')
      mment().get('weekday')
      moment().get('isoWeekday')
    • 获取小时
      moment().hours()
      moment().get('hours')
    • 获取分钟
      moment().minutes()
      moment().get('minutes')
    • 获取秒数
      moment().seconds()
      moment().get('seconds')
    • 获取当前的年月日时分秒
      moment().toArray() // [years, months, date, hours, minutes, seconds, milliseconds]
      moment().toObject() // {years: xxxx, months: x, date: xx ...}

设置时间

  • Set Time

    moment().year(Number), moment().month(Number)...
    moment().set(String, Int)
    moment().set(Object)
    • 设置年份
      moment().year(2019)
      moment().set('year', 2019)
      moment().set({year: 2019})
    • 设置月份
      moment().month(11) (0~11, 0: January, 11: December)
      moment().set('month', 11) 
    • 设置某个月中的某一天
      moment().date(15)
      moment().set('date', 15)
    • 设置某个星期中的某一天
      moment().weekday(0) // 设置日期为本周第一天(周日)
      moment().isoWeekday(1) // 设置日期为本周周一
      moment().set('weekday', 0)
      moment().set('isoWeekday', 1)
    • 设置小时
      moment().hours(12)
      moment().set('hours', 12)
    • 设置分钟
      moment().minutes(30)
      moment().set('minutes', 30)
    • 设置秒数
      moment().seconds(30)
      moment().set('seconds', 30)
  • Add Time

    moment().add(Number, String)
    moment().add(Object)
    • 设置年份
      moment().add(1, 'years')
      moment().add({years: 1})
    • 设置月份
      moment().add(1, 'months')
    • 设置日期
      moment().add(1, 'days')
    • 设置星期
      moment().add(1, 'weeks')
    • 设置小时
      moment().add(1, 'hours')
    • 设置分钟
      moment().add(1, 'minutes')
    • 设置秒数
      moment().add(1, 'seconds')
  • Subtract Time

    moment().subtract(Number, String)
    moment().subtract(Object)
    • 设置年份
      moment().subtract(1, 'years')
      moment().subtract({years: 1})
    • 设置月份
      moment().subtract(1, 'months')
    • 设置日期
      moment().subtract(1, 'days')
    • 设置星期
      moment().subtract(1, 'weeks')
    • 设置小时
      moment().subtract(1, 'hours')
    • 设置分钟
      moment().subtract(1, 'minutes')
    • 设置秒数
      moment().subtract(1, 'seconds')

格式化时间

  • Format Time

    moment().format()
    moment().format(String)
    • 格式化年月日: 'xxxx年xx月xx日'
      moment().format('YYYY年MM月DD日')
    • 格式化年月日: 'xxxx-xx-xx'
      moment().format('YYYY-MM-DD')
    • 格式化时分秒(24小时制): 'xx时xx分xx秒'
      moment().format('HH时mm分ss秒')
    • 格式化时分秒(12小时制):'xx:xx:xx am/pm'
      moment().format('hh:mm:ss a')
    • 格式化时间戳(以秒为单位)
      moment().format('X') // 返回值为字符串类型
    • 格式化时间戳(以毫秒为单位)
      moment().format('x') // 返回值为字符串类型

比较时间

  • Difference

    moment().diff(Moment|String|Number|Date|Array)
    • 获取两个日期之间的时间差
      let start_date = moment().subtract(1, 'weeks')
      let end_date = moment()
      
      end_date.diff(start_date) // 返回毫秒数
      
      end_date.diff(start_date, 'months') // 0
      end_date.diff(start_date, 'weeks') // 1
      end_date.diff(start_date, 'days') // 7
      start_date.diff(end_date, 'days') // -7

转化为JavaScript原生Date对象

moment().toDate()
new Date(moment())
  • 将Moment时间转换为JavaScript原生Date对象

    let m = moment()
    let nativeDate1 = m.toDate()
    let nativeDate2 = new Date(m)
    
    String(nativeDate1) === String(nativeDate2) // true

实战

  • 获取昨日0时0分0秒到昨日23时59分59秒, 格式:[milliseconds, milliseconds]
  • 获取上周一到上周日时间范围,格式: [seconds, seconds]
  • 获取上个月第一天和最后一天时间范围, 格式:[YYYY-MM-DD, YYYY-MM-DD]
查看原文

赞 50 收藏 28 评论 1

认证与成就

  • 获得 398 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-12-28
个人主页被 2.5k 人浏览