Tims

Tims 查看完整档案

广州编辑广州大学华软软件学院  |  移动互联网 编辑  |  填写所在公司/组织 timsonpig 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Tims 收藏了文章 · 2019-01-14

使用canvas绘制圆弧动画

效果预览

图片描述

canvas 绘制基本流程

图片描述

初始画布

对于canvas的绘制,首先需要在html内指定一块画布,即<canvas></canvas>, 可以看做是在PS中新建一个空白文档,之后所有的操作都将呈现在这个文档之上,与PS的区别是,canvas本身没有图层的特性,当需要展示不同维度的视图时,需要交由html的位置关系来解决。

canvas标签上,值得一提的就是width和height两个属性,这两个属性代表着画布的宽高,与canvas样式上的宽高有很大区别。在浏览器当中,看到的图形绘制大小,本身是由canvas.style.width/canvas.style.height决定的,他们决定了canvas这个dom元素的大小关系,而canvas.width和canvas.height决定的是canvas内部图形的大小关系。当这两个宽高比不同时,就会产生视觉上的形变。即,把canvas.style.height放大为2倍时,显示效果会被拉伸:
图片描述

当不设置样式宽高时,浏览器中canvas大小由画布大小决定(在实际开发中,碰到一个例外,是在使用mapbox时,绘制map的标签如果只设置canvas画布大小时,在ios移动端的浏览器上显示异常,PC正常)。

获取上下文

所谓上下文,代表的就是一个环境,在这个环境当中你可以获取到相关的方法,变量。程序中有上下文,html的媒体中也有上下文,比如音频上下文(AudioContext),只有拿到了上下文,才能进行相关的方法操作,canvas也是如此,canvas上的方法都是借由canvas上下文得到。

<canvas id="leftCanvas"></canvas>
const canvasL = document.getElementById("leftCanvas");
const cxtL = canvasL.getContext("2d");

配置线条

本次圆弧动画需要用到的上下文属性有:

  • lineCap 线段端点形状,本次设置为round
  • lineWidth 线宽
  • strokeStyle 线条填充颜色
  • clearRect 清除画布里面的内容
  • beginPath 在画布上开始一段新的路径
  • arc 圆弧绘制参数配置
  • stroke 绘制

角度计算

角度计算之前,先介绍一下绘制圆弧的基础api arc

ctx.arc(x, y, radius, startAngle, endAngle [, anticlockwise]);

这个函数可以接收6个参数,前五个为必填,分别为圆心x坐标,圆心y坐标,半径,起始角度,结束角度,方向(默认为false,顺时针)。

回到圆弧动画,当前动画有两段,以顺时针方向这段为例。

  • x, y:在canvas当中,坐标系默认以左上角为原点,如果想让圆弧动画以画布中心点旋转,可以将圆心点设置为画布中心点,即画布长宽的1/2,假设设置的画布长宽均为100,那么圆心点的坐标即为(50, 50),这个圆就绘制在了画布中间。
  • radius:为了不与画布产生切角,半径设置比画布一般略小,。
  • startAngle:起始角度为正北方向,而圆以x轴水平方向为0度,因此将起始点逆时针旋转90°,即:-1 / 2 * Math.PI。
  • endAngle:因为圆弧长度为30°,终点角度在起始角度的基础上增加 1 / 6 * Math.PI。

顺时针方向圆弧初始配置为:

 cxtL.arc(WidthL / 2, HeightL / 2, WidthL / 2 - 5, -1 / 2 * Math.PI, 1 / 6 * Math.PI, false);

开启动画

window.requestAnimationFrame()

借助requestAnimationFrame,来对canvas圆弧进行不断的重绘,每次重绘canvas之前清空画布,每轮动画方向角偏移2°,即2 / 180 * Math.PI,动画结束的标记为圆弧终点的角度,移动至3 / 2 * Math.PI,当满足条件时,调用window.cancelAnimationFrame(animationId)取消动画。

屏幕适配

通过进入html后,动态获取视口,来设置canvas宽高,比如希望画布大小为窗口的宽度的15%,可以通过

const clientWidth = document.documentElement.clientWidth;
const canvasWidth = Math.floor(clientWidth * 0.15);
const canvasL = document.getElementById("leftCanvas");
canvasL.setAttribute("width", canvasWidth + "px");

这样就可以使画布适应不同屏幕大小。

以下为未整理代码,较乱,仅供参考。

https://codepen.io/jbleach/pe...

查看原文

Tims 收藏了文章 · 2019-01-03

MySQL 索引及查询优化总结

本文由云+社区发表

文章《MySQL查询分析》讲述了使用MySQL慢查询和explain命令来定位mysql性能瓶颈的方法,定位出性能瓶颈的sql语句后,则需要对低效的sql语句进行优化。本文主要讨论MySQL索引原理及常用的sql查询优化。

一个简单的对比测试

前面的案例中,c2c_zwdb.t_file_count表只有一个自增id,FFileName字段未加索引的sql执行情况如下:

img

在上图中,type=all,key=null,rows=33777。该sql未使用索引,是一个效率非常低的全表扫描。如果加上联合查询和其他一些约束条件,数据库会疯狂的消耗内存,并且会影响前端程序的执行。

这时给FFileName字段添加一个索引:

alter table c2c_zwdb.t_file_count add index index_title(FFileName);

再次执行上述查询语句,其对比很明显:

img

在该图中,type=ref,key=索引名(index_title),rows=1。该sql使用了索引index_title,且是一个常数扫描,根据索引只扫描了一行。

比起未加索引的情况,加了索引后,查询效率对比非常明显。

MySQL索引

通过上面的对比测试可以看出,索引是快速搜索的关键。MySQL索引的建立对于MySQL的高效运行是很重要的。对于少量的数据,没有合适的索引影响不是很大,但是,当随着数据量的增加,性能会急剧下降。如果对多列进行索引(组合索引),列的顺序非常重要,MySQL仅能对索引最左边的前缀进行有效的查找。

下面介绍几种常见的MySQL索引类型。

索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。

1、MySQL索引类型

(1) 主键索引 PRIMARY KEY

它是一种特殊的唯一索引,不允许有空值。一般是在建表的时候同时创建主键索引。

img

当然也可以用 ALTER 命令。记住:一个表只能有一个主键。

(2) 唯一索引 UNIQUE

唯一索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。可以在创建表的时候指定,也可以修改表结构,如:

ALTER TABLE table_name ADD UNIQUE (column)

(3) 普通索引 INDEX

这是最基本的索引,它没有任何限制。可以在创建表的时候指定,也可以修改表结构,如:

ALTER TABLE table_name ADD INDEX index_name (column)

(4) 组合索引 INDEX

组合索引,即一个索引包含多个列。可以在创建表的时候指定,也可以修改表结构,如:

ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3)

(5) 全文索引 FULLTEXT

全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用分词技术等多种算法智能分析出文本文字中关键字词的频率及重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。

可以在创建表的时候指定,也可以修改表结构,如:

ALTER TABLE table_name ADD FULLTEXT (column)

2、索引结构及原理

mysql中普遍使用B+Tree做索引,但在实现上又根据聚簇索引和非聚簇索引而不同,本文暂不讨论这点。

b+树介绍

下面这张b+树的图片在很多地方可以看到,之所以在这里也选取这张,是因为觉得这张图片可以很好的诠释索引的查找过程。

img

如上图,是一颗b+树。浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。

真实的数据存在于叶子节点,即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。

查找过程

在上图中,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

性质

(1) 索引字段要尽量的小。

通过上面b+树的查找过程,或者通过真实的数据存在于叶子节点这个事实可知,IO次数取决于b+数的高度h。

假设当前数据表的数据量为N,每个磁盘块的数据项的数量是m,则树高h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;

而m = 磁盘块的大小/数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的;如果数据项占的空间越小,数据项的数量m越多,树的高度h越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。

(2) 索引的最左匹配特性。

当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

建索引的几大原则

(1) 最左前缀匹配原则

对于多列索引,总是从索引的最前面字段开始,接着往后,中间不能跳过。比如创建了多列索引(name,age,sex),会先匹配name字段,再匹配age字段,再匹配sex字段的,中间不能跳过。mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配。

一般,在创建多列索引时,where子句中使用最频繁的一列放在最左边。

看一个补符合最左前缀匹配原则和符合该原则的对比例子。

实例:表c2c_db.t_credit_detail建有索引(Flistid,Fbank_listid)

img

不符合最左前缀匹配原则的sql语句:

select * from t_credit_detail where Fbank_listid='201108010000199'G

该sql直接用了第二个索引字段Fbank_listid,跳过了第一个索引字段Flistid,不符合最左前缀匹配原则。用explain命令查看sql语句的执行计划,如下图:

img

从上图可以看出,该sql未使用索引,是一个低效的全表扫描。

符合最左前缀匹配原则的sql语句:

select * from t_credit_detail where Flistid='2000000608201108010831508721' and Fbank_listid='201108010000199'G

该sql先使用了索引的第一个字段Flistid,再使用索引的第二个字段Fbank_listid,中间没有跳过,符合最左前缀匹配原则。用explain命令查看sql语句的执行计划,如下图:

img

从上图可以看出,该sql使用了索引,仅扫描了一行。

对比可知,符合最左前缀匹配原则的sql语句比不符合该原则的sql语句效率有极大提高,从全表扫描上升到了常数扫描。

(2) 尽量选择区分度高的列作为索引。

比如,我们会选择学号做索引,而不会选择性别来做索引。

(3) =和in可以乱序

比如a = 1 and b = 2 and c = 3,建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

(4) 索引列不能参与计算,保持列“干净”

比如:Flistid+1>‘2000000608201108010831508721‘。原因很简单,假如索引列参与计算的话,那每次检索时,都会先将索引计算一次,再做比较,显然成本太大。

(5) 尽量的扩展索引,不要新建索引。

比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

索引的不足

虽然索引可以提高查询效率,但索引也有自己的不足之处。

索引的额外开销:

(1) 空间:索引需要占用空间;

(2) 时间:查询索引需要时间;

(3) 维护:索引须要维护(数据变更时);

不建议使用索引的情况:

(1) 数据量很小的表

(2) 空间紧张

常用优化总结

优化语句很多,需要注意的也很多,针对平时的情况总结一下几点:

1、有索引但未被用到的情况(不建议)

(1) Like的参数以通配符开头时

尽量避免Like的参数以通配符开头,否则数据库引擎会放弃使用索引而进行全表扫描。

以通配符开头的sql语句,例如:select * from t_credit_detail where Flistid like '%0'G

img

这是全表扫描,没有使用到索引,不建议使用。

不以通配符开头的sql语句,例如:select * from t_credit_detail where Flistid like '2%'G

img

很明显,这使用到了索引,是有范围的查找了,比以通配符开头的sql语句效率提高不少。

(2) where条件不符合最左前缀原则时

例子已在最左前缀匹配原则的内容中有举例。

(3) 使用!= 或 <> 操作符时

尽量避免使用!= 或 <>操作符,否则数据库引擎会放弃使用索引而进行全表扫描。使用>或<会比较高效。

select * from t_credit_detail where Flistid != '2000000608201108010831508721'G

img

(4) 索引列参与计算

应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。

select * from t_credit_detail where Flistid +1 > '2000000608201108010831508722'G

img

(5) 对字段进行null值判断

应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描,如: 低效:select * from t_credit_detail where Flistid is null ;

可以在Flistid上设置默认值0,确保表中Flistid列没有null值,然后这样查询: 高效:select * from t_credit_detail where Flistid =0;

(6) 使用or来连接条件

应尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如: 低效:select * from t_credit_detail where Flistid = '2000000608201108010831508721' or Flistid = '10000200001';

可以用下面这样的查询代替上面的 or 查询: 高效:select from t_credit_detail where Flistid = '2000000608201108010831508721' union all select from t_credit_detail where Flistid = '10000200001';

img

2、避免select *

在解析的过程中,会将'*' 依次转换成所有的列名,这个工作是通过查询数据字典完成的,这意味着将耗费更多的时间。

所以,应该养成一个需要什么就取什么的好习惯。

3、order by 语句优化

任何在Order by语句的非索引项或者有计算表达式都将降低查询速度。

方法:1.重写order by语句以使用索引;

  2.为所使用的列建立另外一个索引

  3.绝对避免在order by子句中使用表达式。

4、GROUP BY语句优化

提高GROUP BY 语句的效率, 可以通过将不需要的记录在GROUP BY 之前过滤掉

低效:

SELECT JOB , AVG(SAL)

FROM EMP

GROUP by JOB

HAVING JOB = ‘PRESIDENT'

OR JOB = ‘MANAGER'

高效:

SELECT JOB , AVG(SAL)

FROM EMP

WHERE JOB = ‘PRESIDENT'

OR JOB = ‘MANAGER'

GROUP by JOB

5、用 exists 代替 in

很多时候用 exists 代替 in 是一个好的选择: select num from a where num in(select num from b) 用下面的语句替换: select num from a where exists(select 1 from b where num=a.num)

6、使用 varchar/nvarchar 代替 char/nchar

尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

7、能用DISTINCT的就不用GROUP BY

SELECT OrderID FROM Details WHERE UnitPrice > 10 GROUP BY OrderID

可改为:

SELECT DISTINCT OrderID FROM Details WHERE UnitPrice > 10

8、能用UNION ALL就不要用UNION

UNION ALL不执行SELECT DISTINCT函数,这样就会减少很多不必要的资源。

9、在Join表的时候使用相当类型的例,并将其索引

如果应用程序有很多JOIN 查询,你应该确认两个表中Join的字段是被建过索引的。这样,MySQL内部会启动为你优化Join的SQL语句的机制。

而且,这些被用来Join的字段,应该是相同的类型的。例如:如果你要把 DECIMAL 字段和一个 INT 字段Join在一起,MySQL就无法使用它们的索引。对于那些STRING类型,还需要有相同的字符集才行。(两个表的字符集有可能不一样)

此文已由作者授权腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

查看原文

Tims 收藏了文章 · 2018-11-15

美团点评携手 PingCAP 开启新一代数据库深度实践之旅

一、背景和现状

在美团,基于 MySQL 构建的传统关系型数据库服务已经难于支撑公司业务的爆发式增长,促使我们去探索更合理的数据存储方案和实践新的运维方式。随着近一两年来分布式数据库大放异彩,美团 DBA 团队联合架构存储团队,于 2018 年初启动了分布式数据库项目。

图 1 美团点评产品展示图

图 1 美团点评产品展示图

立项之初,我们进行了大量解决方案的对比,深入了解了业界多种 scale-out、scale-up 方案,考虑到技术架构的前瞻性、发展潜力、社区活跃度、以及服务本身与 MySQL 的兼容性,最终敲定了基于 TiDB 数据库进行二次开发的整体方案,并与 PingCAP 官方和开源社区进行深入合作的开发模式。

美团业务线众多,我们根据业务特点及重要程度逐步推进上线,到截稿为止,已经上线 10 个集群,近 200 个物理节点,大部分是 OLTP 类型的应用,除了上线初期遇到了一些小问题,目前均已稳定运行。初期上线的集群,已经分别服务于配送、出行、闪付、酒旅等业务。

TiDB 架构分层清晰,服务平稳流畅,但在美团当前的数据量规模和已有稳定的存储体系的基础上,推广新的存储服务体系,需要对周边工具和系统进行一系列改造和适配,从初期探索到整合落地需要走很远的路。下面从几个方面分别介绍:

  • 一是从 0 到 1 的突破,重点考虑做哪些事情;
  • 二是如何规划实施不同业务场景的接入和已有业务的迁移;
  • 三是上线后遇到的一些典型问题介绍;
  • 四是后续规划和对未来的展望。

二、前期调研测试

2.1 对 TiDB 的定位

我们对于 TiDB 的定位,前期在于重点解决 MySQL 的单机性能和容量无法线性和灵活扩展的问题,与 MySQL 形成互补。业界分布式方案很多,我们为何选择了 TiDB 呢?考虑到公司业务规模的快速增长,以及公司内关系数据库以 MySQL 为主的现状,因此我们在调研阶段,对以下技术特性进行了重点考虑:

  • 协议兼容 MySQL:这个是必要项。
  • 可在线扩展:数据通常要有分片,分片要支持分裂和自动迁移,并且迁移过程要尽量对业务无感知。
  • 强一致的分布式事务:事务可以跨分片、跨节点执行,并且强一致。
  • 支持二级索引:为兼容 MySQL 的业务,这个是必须的。
  • 性能:MySQL 的业务特性,高并发的 OLTP 性能必须满足。
  • 跨机房服务:需要保证任何一个机房宕机,服务能自动切换。
  • 跨机房双写:支持跨机房双写是数据库领域一大难题,是我们对分布式数据库的一个重要期待,也是美团下一阶段重要的需求。

业界的一些传统方案虽然支持分片,但无法自动分裂、迁移,不支持分布式事务,还有一些在传统 MySQL 上开发一致性协议的方案,但它无法实现线性扩展,最终我们选择了与我们的需求最为接近的 TiDB。与 MySQL 语法和特性高度兼容,具有灵活的在线扩容缩容特性,支持 ACID 的强一致性事务,可以跨机房部署实现跨机房容灾,支持多节点写入,对业务又能像单机 MySQL 一样使用。

2.2 测试

针对官方声称的以上优点,我们进行了大量的研究、测试和验证。

首先,我们需要知道扩容、Region 分裂转移的细节、Schema 到 kv 的映射、分布式事务的实现原理。而 TiDB 的方案,参考了较多的 Google 论文,我们进行了阅读,这有助于我们理解 TiDB 的存储结构、事务算法、安全性等,包括:

  • Spanner: Google’s Globally-Distributed Database
  • Large-scale Incremental Processing Using Distributed Transactions and Notifications
  • In Search of an Understandable Consensus Algorithm
  • Online, Asynchronous Schema Change in F1

我们也进行了常规的性能和功能测试,用来与 MySQL 的指标进行对比,其中一个比较特别的测试,是证明 3 副本跨机房部署,确实能保证每个机房分布一个副本,从而保证任何一个机房宕机不会导致丢失超过半数副本。从以下几个点进行测试:

  • Raft 扩容时是否支持 learner 节点,从而保证单机房宕机不会丢失 2/3 的副本。
  • TiKV 上的标签优先级是否可靠,保证当机房的机器不平均时,能否保证每个机房的副本数依然是绝对平均的。
  • 实际测试,单机房宕机,TiDB 在高并发下,QPS、响应时间、报错数量,以及最终数据是否有丢失。
  • 手动 Balance 一个 Region 到其他机房,是否会自动回来。

从测试结果来看,一切都符合预期。

三、存储生态建设

美团的产品线丰富,业务体量大,业务对在线存储的服务质量要求也非常高。因此,从早期做好服务体系的规划非常重要。下面从业务接入层、监控报警、服务部署,来分别介绍一下我们所做的工作。

3.1 业务接入层

当前 MySQL 的业务接入方式主要有两种,DNS 接入和 Zebra 客户端接入。在前期调研阶段,我们选择了 DNS + 负载均衡组件的接入方式,TiDB-Server 节点宕机,15s 可以被负载均衡识别到,简单有效。业务架构如图 2。

图 2 业务架构图

图 2 业务架构图

后面我们会逐渐过渡到当前大量使用的 Zebra 接入方式来访问 TiDB,从而保持与访问 MySQL 的方式一致,一方面减少业务改造的成本,另一方面尽量实现从 MySQL 到 TiDB 的透明迁移。

3.2 监控报警

美团目前使用 Mt-Falcon 平台负责监控报警,通过在 Mt-Falcon 上配置不同的插件,可以实现对多种组件的自定义监控。另外也会结合 Puppet 识别不同用户的权限、文件的下发。这样,只要我们编写好插件脚本、需要的文件,装机和权限控制就可以完成了。监控架构如图 3。

图 3 监控架构图

图 3 监控架构图

而 TiDB 有丰富的监控指标,使用流行的 Prometheus + Grafana,一套集群有 700+ 的 Metric。从官方的架构图可以看出,每个组件会推送自己的 Metric 给 PushGateWay,Prometheus 会直接到 PushGateWay 去抓数据。

由于我们需要组件收敛,原生的 TiDB 每个集群一套 Prometheus 的方式不利于监控的汇总、分析、配置,而报警已经在 Mt-Falcon 上实现的比较好了,在 AlertManager 上再造一个也没有必要。因此我们需要想办法把监控和报警汇总到 Mt-Falcon 上面,有如下几种方式:

  • 方案一:修改源代码,将 Metric 直接推送到 Falcon,由于 Metric 散落在代码的不同位置,而且 TiDB 代码迭代太快,把精力消耗在不停调整监控埋点上不太合适。
  • 方案二:在 PushGateWay 是汇总后的,可以直接抓取,但 PushGateWay 是个单点,不好维护。
  • 方案三:通过各个组件(TiDB、PD、TiKV)的本地 API 直接抓取,优点是组件宕机不会影响其他组件,实现也比较简单。

我们最终选择了方案三。该方案的难点是需要把 Prometheus 的数据格式转化为 Mt-Falcon 可识别的格式,因为 Prometheus 支持 Counter、Gauge、Histogram、Summary 四种数据类型,而 Mt-Falcon 只支持基本的 Counter 和 Gauge,同时 Mt-Falcon 的计算表达式比较少,因此需要在监控脚本中进行转换和计算。

3.3 批量部署

TiDB 使用 Ansible 实现自动化部署。迭代快,是 TiDB 的一个特点,有问题快速解决,但也造成 Ansible 工程、TiDB 版本更新过快,我们对 Ansible 的改动,也只会增加新的代码,不会改动已有的代码。因此线上可能同时需要部署、维护多个版本的集群。如果每个集群一个 Ansible 目录,造成空间的浪费。我们采用的维护方式是,在中控机中,每个版本一个 Ansible 目录,每个版本中通过不同 inventory 文件来维护。这里需要跟 PingCAP 提出的是,Ansible 只考虑了单集群部署,大量部署会有些麻烦,像一些依赖的配置文件,都不能根据集群单独配置(咨询官方得知,PingCAP 目前正在基于 Cloud TiDB 打造一站式 HTAP 平台,会提供批量部署、多租户等功能,能比较好的解决这个问题)。

3.4 自动化运维平台

随着线上集群数量的增加,打造运维平台提上了日程,而美团对 TiDB 和 MySQL 的使用方式基本相同,因此 MySQL 平台上具有的大部分组件,TiDB 平台也需要建设。典型的底层组件和方案:SQL 审核模块、DTS、数据备份方案等。自动化运维平台展示如图 4。

图 4 自动化运维平台展示图

3.5 上下游异构数据同步

TiDB 是在线存储体系中的一环,它同时也需要融入到公司现有的数据流中,因此需要一些工具来做衔接。PingCAP 官方标配了相关的组件。

公司目前 MySQL 和 Hive 结合的比较重,而 TiDB 要代替 MySQL 的部分功能,需要解决 2 个问题:

  • MySQL to TiDB

    • MySQL 到 TiDB 的迁移,需要解决数据迁移以及增量的实时同步,也就是 DTS,Mydumper + Loader 解决存量数据的同步,官方提供了 DM 工具可以很好的解决增量同步问题。
    • MySQL 大量使用了自增 ID 作为主键。分库分表 MySQL 合并到 TiDB 时,需要解决自增 ID 冲突的问题。这个通过在 TiDB 端去掉自增 ID 建立自己的唯一主键来解决。新版 DM 也提供分表合并过程主键自动处理的功能。
  • Hive to TiDB & TiDB to Hive

    • Hive to TiDB 比较好解决,这体现了 TiDB 和 MySQL 高度兼容的好处,insert 语句可以不用调整,基于 Hive to MySQL 简单改造即可。
    • TiDB to Hive 则需要基于官方 Pump + Drainer 组件,Drainer 可以消费到 Kafka、MySQL、TiDB,我们初步考虑用下图 5 中的方案通过使用 Drainer 的 Kafka 输出模式同步到 Hive。

图 5 TiDB to Hive 方案图

图 5 TiDB to Hive 方案图

四、线上使用磨合

对于初期上线的业务,我们比较谨慎,基本的原则是:离线业务 -> 非核心业务 -> 核心业务。TiDB 已经发布两年多,且前期经历了大量的测试,我们也深入了解了其它公司的测试和使用情况,可以预期的是 TiDB 上线会比较稳定,但依然遇到了一些小问题。总体来看,在安全性、数据一致性等关键点上没有出现问题。其他一些性能抖动问题,参数调优的问题,也都得到了快速妥善的解决。这里给 PingCAP 的同学点个大大的赞,问题响应速度非常快,与我们内部研发的合作也非常融洽。

4.1 写入量大、读 QPS 高的离线业务

我们上线的最大的一个业务,每天有数百 G 的写入量,前期遇到了较多的问题,我们重点说说。

业务场景:

  • 稳定的写入,每个事务操作 100~200 行不等,每秒 6w 的数据写入。
  • 每天的写入量超过 500G,以后会逐步提量到每天 3T。
  • 每 15 分钟的定时读 job,5000 QPS(高频量小)。
  • 不定时的查询(低频量大)。

之前使用 MySQL 作为存储,但 MySQL 到达了容量和性能瓶颈,而业务的容量未来会 10 倍的增长。初期调研测试了 ClickHouse,满足了容量的需求,测试发现运行低频 SQL 没有问题,但高频 SQL 的大并发查询无法满足需求,只在 ClickHouse 跑全量的低频 SQL 又会 overkill,最终选择使用 TiDB。

测试期间模拟写入了一天的真实数据,非常稳定,高频低频两种查询也都满足需求,定向优化后 OLAP 的 SQL 比 MySQL 性能提高四倍。但上线后,陆续发现了一些问题,典型的如下:

4.1.1 TiKV 发生 Write Stall

TiKV 底层有 2 个 RocksDB 作为存储。新写的数据写入 L0 层,当 RocksDB 的 L0 层数量达到一定数量,就会发生减速,更高则发生 Stall,用来自我保护。TiKV 的默认配置:

  • level0-slowdown-writes-trigger = 20
  • level0-stop-writes-trigger = 36

遇到过的,发生 L0 文件过多可能的原因有 2 个:

  • 写入量大,Compact 完不成。
  • Snapshot 一直创建不完,导致堆积的副本一下释放,rocksdb-raft 创建大量的 L0 文件,监控展示如图 6。

图 6 TiKV 发生 Write Stall 监控展示图

图 6 TiKV 发生 Write Stall 监控展示图

我们通过以下措施,解决了 Write Stall 的问题:

  • 减缓 Raft Log Compact 频率(增大 raft-log-gc-size-limit、raft-log-gc-count-limit)
  • 加快 Snapshot 速度(整体性能、包括硬件性能)
  • max-sub-compactions 调整为 3
  • max-background-jobs 调整为 12
  • level 0 的 3 个 Trigger 调整为 16、32、64

4.1.2 Delete 大量数据,GC 跟不上

现在 TiDB 的 GC 对于每个 kv-instance 是单线程的,当业务删除数据的量非常大时,会导致 GC 速度较慢,很可能 GC 的速度跟不上写入。

目前可以通过增多 TiKV 个数来解决,长期需要靠 GC 改为多线程执行,官方对此已经实现,即将发布。

4.1.3 Insert 响应时间越来越慢

业务上线初期,insert 的响应时间 80 线(Duration 80 By Instance)在 20ms 左右,随着运行时间增加,发现响应时间逐步增加到 200ms+。期间排查了多种可能原因,定位在由于 Region 数量快速上涨,Raftstore 里面要做的事情变多了,而它又是单线程工作,每个 Region 定期都要 heartbeat,带来了性能消耗。tikv-raft propose wait duration 指标持续增长。

解决问题的办法:

  • 临时解决

    • 增加 Heartbeat 的周期,从 1s 改为 2s,效果比较明显,监控展示如图 7。

图 7 insert 响应时间优化前后对比图

图 7 insert 响应时间优化前后对比图

  • 彻底解决

    • 需要减少 Region 个数,Merge 掉空 Region,官方在 2.1 版本中已经实现了 Region Merge 功能,我们在升级到 2.1 后,得到了彻底解决。
    • 另外,等待 Raftstore 改为多线程,能进一步优化。(官方回复相关开发已基本接近尾声,将于 2.1 的下一个版本发布。)

4.1.4 Truncate Table 空间无法完全回收

DBA Truncate 一张大表后,发现 2 个现象,一是空间回收较慢,二是最终也没有完全回收。

  • 由于底层 RocksDB 的机制,很多数据落在 level 6 上,有可能清不掉。这个需要打开 cdynamic-level-bytes 会优化 Compaction 的策略,提高 Compact 回收空间的速度。
  • 由于 Truncate 使用 delete_files_in_range 接口,发给 TiKV 去删 SST 文件,这里只删除不相交的部分,而之前判断是否相交的粒度是 Region,因此导致了大量 SST 无法及时删除掉。

    • 考虑 Region 独立 SST 可以解决交叉问题,但是随之带来的是磁盘占用问题和 Split 延时问题。
    • 考虑使用 RocksDB 的 DeleteRange 接口,但需要等该接口稳定。
    • 目前最新的 2.1 版本优化为直接使用 DeleteFilesInRange 接口删除整个表占用的空间,然后清理少量残留数据,已经解决。

4.1.5 开启 Region Merge 功能

为了解决 region 过多的问题,我们在升级 2.1 版本后,开启了 region merge 功能,但是 TiDB 的响应时间 80 线(Duration 80 By Instance)依然没有恢复到当初,保持在 50ms 左右,排查发现 KV 层返回的响应时间还很快,和最初接近,那么就定位了问题出现在 TiDB 层。研发人员和 PingCAP 定位在产生执行计划时行为和 2.0 版本不一致了,目前已经优化。

4.2 在线 OLTP,对响应时间敏感的业务

除了分析查询量大的离线业务场景,美团还有很多分库分表的场景,虽然业界有很多分库分表的方案,解决了单机性能、存储瓶颈,但是对于业务还是有些不友好的地方:

  • 业务无法友好的执行分布式事务。
  • 跨库的查询,需要在中间层上组合,是比较重的方案。
  • 单库如果容量不足,需要再次拆分,无论怎样做,都很痛苦。
  • 业务需要关注数据分布的规则,即使用了中间层,业务心里还是没底。

因此很多分库分表的业务,以及即将无法在单机承载而正在设计分库分表方案的业务,主动找到了我们,这和我们对于 TiDB 的定位是相符的。这些业务的特点是 SQL 语句小而频繁,对一致性要求高,通常部分数据有时间属性。在测试及上线后也遇到了一些问题,不过目前基本都有了解决办法。

4.2.1 SQL 执行超时后,JDBC 报错

业务偶尔报出 privilege check fail。

是由于业务在 JDBC 设置了 QueryTimeout,SQL 运行超过这个时间,会发行一个 “kill query” 命令,而 TiDB 执行这个命令需要 Super 权限,业务是没有权限的。

其实 kill 自己的查询,并不需要额外的权限,目前已经解决了这个问题,不再需要 Super 权限,已在 2.0.5 上线。

4.2.2 执行计划偶尔不准

TiDB 的物理优化阶段需要依靠统计信息。在 2.0 版本统计信息的收集从手动执行,优化为在达到一定条件时可以自动触发:

  • 数据修改比例达到 tidb_auto_analyze_ratio
  • 表一分钟没有变更(目前版本已经去掉这个条件)

但是在没有达到这些条件之前统计信息是不准的,这样就会导致物理优化出现偏差,在测试阶段(2.0 版本)就出现了这样一个案例:业务数据是有时间属性的,业务的查询有 2 个条件,比如:时间+商家 ID,但每天上午统计信息可能不准,当天的数据已经有了,但统计信息认为没有。这时优化器就会建议使用时间列的索引,但实际上商家 ID 列的索引更优化。这个问题可以通过增加 Hint 解决。

在 2.1 版本对统计信息和执行计划的计算做了大量的优化,也稳定了基于 Query Feedback 更新统计信息,也用于更新直方图和 Count-Min Sketch,非常期待 2.1 的 GA。

五、总结展望

经过前期的测试、各方的沟通协调,以及近半年对 TiDB 的使用,我们看好 TiDB 的发展,也对未来基于 TiDB 的合作充满信心。

接下来,我们会加速推进 TiDB 在更多业务系统中的使用,同时也将 TiDB 纳入了美团新一代数据库的战略选型中。当前,我们已经全职投入了 3 位 DBA 同学和多位存储计算专家,从底层的存储,中间层的计算,业务层的接入,到存储方案的选型和布道,进行全方位和更深入的合作。

长期来看,结合美团不断增长的业务规模,我们将与 PingCAP 官方合作打造更强大的生态体系:

  • Titan:Titan 是 TiDB 下一步比较大的动作,也是我们非常期待的下一代存储引擎,它对大 Value 支持会更友好,将解决我们单行大小受限,单机 TiKV 最大支持存储容量的问题,大大提升大规模部署的性价比。
  • Cloud TiDB(based on Docker & K8s):云计算大势所趋,PingCAP 在这块也布局比较早,今年 8 月份开源了 TiDB Operator,Cloud TiDB 不仅实现了数据库的高度自动化运维,而且基于 Docker 硬件隔离,实现了数据库比较完美的多租户架构。和官方同学沟通,目前他们的私有云方案在国内也有重要体量的 POC,这也是美团看重的一个方向。
  • TiDB HTAP Platform:PingCAP 在原有 TiDB Server 计算引擎的基础上,还构建 TiSpark 计算引擎,和他们官方沟通,他们在研发了一个基于列的存储引擎,这样就形成了下层行、列两个存储引擎、上层两个计算引擎的完整混合数据库(HTAP),这个架构不仅大大的节省了核心业务数据在整个公司业务周期里的副本数量,还通过收敛技术栈,节省了大量的人力成本、技术成本、机器成本,同时还解决了困扰多年的 OLAP 的实效性。后面我们也会考虑将一些有实时、准实时的分析查询系统接入 TiDB。

图 8 TiDB HTAP Platform 整体架构图

图 8 TiDB HTAP Platform 整体架构图

后续的物理备份方案,跨机房多写等也是我们接下来逐步推进的场景,总之我们坚信未来 TiDB 在美团的使用场景会越来越多,发展也会越来越好。

TiDB 在业务层面、技术合作层面都已经在美团扬帆起航,美团点评将携手 PingCAP 开启新一代数据库深度实践、探索之旅。后续,还有美团点评架构存储团队针对 TiDB 源码研究和改进的系列文章,敬请期待!

作者介绍

赵应钢,美团点评研究员

李坤,美团点评数据库专家

朴昌俊,美团点评数据库专家

查看原文

Tims 收藏了文章 · 2018-10-16

曾经面试踩过的坑,都在这里了~

本文由@IT·平头哥联盟-首席填坑官∙苏南 分享,梅斌的专栏,首席填坑官∙苏南专栏,公众号:honeyBadger8

前言

  前段时间面试(包括阿里巴巴的电话面试),遇到过一些面试题,且面试中出现机率较高的提问/笔试,有些答的不是很好挂掉了,今天终于有时间整理出来分享给大家,希望对大家面试有所帮助,都能轻松拿offer。

主要分三部分htmlcssjs;react/vue等都归类于js,内容来源于面试过程中遇到的、在复习过程中看到认为值得加深巩固群友交流分享的;如有理解的错误或不足之处,欢迎留言纠错、斧正,这里是@IT·平头哥联盟,我是首席填坑官苏南(South·Su) ^_^~

本文由@IT·平头哥联盟-首席填坑官∙苏南 分享,君自故乡来,应知故乡事。来日绮窗前,寒梅着花未?——唐·王维

HTML

1、什么是盒子模型?

  有些面试官会问你对盒子模型的理解,在我们平时看到的网页中,内部的每一个标签元素它都是有几个部分构成的:内容(content)、外边距(margin)、内边距(padding)、边框(border),四个部分组成,当你说完这些面试官是不会满意这个答案的,因为还有一个重点(IE盒模型和标准盒模型的区别)———IE盒模型的content包括border、padding

本文由@IT·平头哥联盟-首席填坑官∙苏南分享

2、页面导入样式时有几种方法,它们之间有区别?
  • link标签引入,也是当下用的最多的一种方式,它属于XHTML标签,除了能加载css外,还能定义rel、type、media等属性;
  • @import引入,@import是CSS提供的,只能用于加载CSS;
  • style 嵌入方式引入,减少页面请求(优点),但只会对当前页面有效,无法复用、会导致代码冗余,不利于项目维护(缺点),此方式一般只会项目主站首页使用(腾讯、淘宝、网易、搜狐)等大型网站主页,之前有看到过都是这种方式,但后来有些也舍弃了 
小结link页面被加载的时,link会同时被加载,而@import引用的CSS会等到页面被加载完再加载,且link是XHTML标签,无兼容问题; link支持动态js去控制DOM节点去改变样式,而@import不支持,
3、简单讲述一下块元素、内联元素、空元素有哪些,它们之间的区别?
  • 行内元素有:a、b、span、img、input、select、textarea、em、img、strong(强调的语气);
  • 块级元素有:ul、ol、li、dl、dt、dd、h1、h2、h3、h4…p、section、div、form等;
  • 空元素: input type="hidden"/>、br>、hr>、link>、meta>; 
小结:块元素总是独占一行,margin对内联元素上下不起作用; 
4、说说 cookies,sessionStorage 、 localStorage 你对它们的理解?
  • cookie是网站为了标示用户身份而储存在用户本地终端上的数据(通常经过加密),cookie数据始终在同源的http请求中携带,记会在浏览器和服务器间来回传递。 
  • sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
  • 大小: cookie数据大小不能超过4k,sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
  • 时效:localStorage 存储持久数据,浏览器关闭后数据不丢失除非用户主动删除数据或清除浏览器/应用缓存;sessionStorage 数据在当前浏览器窗口关闭后自动删除。
  • cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭部分面试官可能还会再深入一些:
1)、如何让cookie浏览器关闭就失效?——不对cookie设置任何正、负或0时间的即可;
2)、sessionStorage在浏览器多窗口之间 (同域)数据是否互通共享? ——不会,都是独立的,localStorage会共享;
3)、能让localStorage也跟cookie一样设置过期时间?答案是可以的,在存储数据时,也存储一个时间戳,get数据之前,先拿当前时间跟你之前存储的时间戳做比较 详细可看我之前写的另一篇分享(小程序项目总结 )。
5、简述一下你对HTML语义化的理解 ?

  语义化是指根据内容的类型,选择合适的标签(代码语义化),即用正确的标签做正确的事情; html语义化让页面的内容结构化,结构更清晰,有助于浏览器、搜索引擎解析对内容的抓取; 语义化的HTML在没有CSS的情况下也能呈现较好的内容结构与代码结构; 搜索引擎的爬虫也依赖于HTML标记来确定上下文和各个关键字的权重,利于SEO;

CSS

1、position的static、relative、absolute、fixed它们的区别?
  • absolute:绝对定位,元素会相对于值不为 static 的第一个父元素进行定位(会一直往父级节点查找),且它是脱离正常文档流、不占位的;
  • fixed:同样是绝对定位,但元素会相对于浏览器窗口进行定位,而不是父节点的position (IE9以下不支持);
  • relative:相对定位,元素相对于自身正常位置进行定位,属于正常文档流;static: position的默认值,也就是没有定位,当元素设置该属性后( top、bottom、left、right、z-index )等属性将失效; 
  • inherit:貌似没用过,查了一下文档“规定从父元素继承 position 属性的值”;
2、如何让一个元素垂直/水平(垂直水平)都居中,请列出你能想到的几种方式?
  • 水平垂直居中 —— 方式一
<div class="div-demo"></div>
<style>
    .div-demo{
        width:100px;
        height:100px;
        background-color:#06c;
        margin: auto;
        position:absolute;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
    }
</style>
  • 水平垂直居中 —— 方式二
<style>
    .div-demo{
        width:100px;
        height:100px;
        background-color:#06c;
        margin: auto;
        position:absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%,-50%);
        -webkit-transform: translate(-50%,-50%);
    }
</style>
  • 水平垂直居中 —— 方式三,(新旧伸缩盒兼容)
<body class="container">
    <div class="div-demo"></div>
    <style>

        html,body{
            height:100%;
        }
        .container{
            display: box;
            display: -webkit-box;
            display: flex;
            display: -webkit-flex;
            -webkit-box-pack: center;
            -webkit-justify-content: center;
            justify-content: center;
            -webkit-box-align: center;
            -webkit-align-items: center;
            align-items: center;
        }
        .div-demo{
            width:100px;
            height:100px;
            background-color:#06c;
        }
    </style>

</body>
3、项目中有用纯CSS样式写过 三角形icon吗?
<body class="container">
    <div class="div-angles"></div>
    <div class="div-angles right"></div>
    <div class="div-angles bottom"></div>
    <div class="div-angles left"></div>
    <style>

        html,body{
            height:100%;
        }
        .container{
            display: box;
            display: -webkit-box;
            display: flex;
            display: -webkit-flex;
            -webkit-box-pack: center;
            -webkit-justify-content: center;
            justify-content: center;
            -webkit-box-align: center;
            -webkit-align-items: center;
            align-items: center;
        }
        .div-angles{
            width: 0;
            height: 0;
            border-style: solid;
            border-width:30px;
            width:0px;
            height:0px;
            border-color: transparent transparent #06c transparent;
        }
        .right{
            border-color: transparent transparent transparent #06c ;
        }
        .bottom{
            border-color: #06c transparent transparent ;
        }
        .left{
            border-color: transparent #06c transparent transparent;
        }
    </style>

</body>

本文由@IT·平头哥联盟-首席填坑官∙苏南 分享

4、什么是外边距合并,项目中是否有遇到过?
  • 有,外边距合并指的是,当两个垂直元素的都设置有margin外边距相遇时,它们将形成一个外边距。 合并后的外边距的高度等于两个发生合并的外边距的值中的较大那个。
5、:before 和 :after两伪元素,平时都是使用双冒号还是单冒号?有什么区别?以及它们的作用:
  • 单冒号(:)用于CSS3伪类,双冒号(::)用于CSS3伪元素。(伪元素由双冒号和伪元素名称组成) ;
  • 双冒号是在当前规范中引入的,用于区分伪类和伪元素。不过浏览器需要同时支持旧的已经存在的伪元素写法,比如:first-line、:first-letter、:before、:after等,

而新的在CSS3中引入的伪元素则不允许再支持旧的单冒号的写法;

  • 想让插入的内容出现在其它内容前,使用::before,之后则使用::after; 在代码顺序上,::after生成的内容也比::before生成的内容靠后。

如果按堆栈视角,::after生成的内容会在::before生成的内容之上; 

6、Chrome、Safari等浏览器,当表单提交用户选择记住密码后,下次自动填充表单的背景变成黄色,影响了视觉体验是否可以修改?
input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {
  background-color: #fff;//设置成元素原本的颜色
  background-image: none;
  color: rgb(0, 0, 0);
}
//方法2:由(licongwen )补充
input:-webkit-autofill {
    -webkit-box-shadow: 0px 0 3px 100px #ccc inset; //背景色
}
7、浏览器的最小字体为12px,如果还想再小,该怎么做?
  • 用图片:如果是展示的内容基本是固定不变的话,可以直接切图兼容性也完美(不到万不得已,不建议);
  • 找UI设计师沟通:为了兼容各大主流浏览器,避免后期设计师来找你撕逼,主动找TA沟通,讲明原因 ————注意语气,好好说话不要激动,更不能携刀相逼;
  • CSS3:css3的样式transform: scale(0.7),scale有缩放功能;
  • 又去找chrome复习了一下,说是 “display:table;display: table-cell;” 可以做到,没用过。
8、移动端的边框0.5px,你有几种方式实现?
  • devicePixelRatio:它是window对象中有一个devicePixelRatio属性,设备物理像素和设备独立像素的比例,也就是 devicePixelRatio = 物理像素 / 独立像素;这种方式好麻烦,js检测,再给元素添加类名控制,难维护;
  • 切图:直接.5px的切图,这种方式太low,建议还是别用了,特别难维护,高清屏就糊了,更重要的是被同行看到会觉得你们很渣渣~;
  • image背景:css3的background-image:linear-gradient,缺点就是:样式多,遇到圆角这个方案就杯剧了; box-shadow:网上看到说使用box-shadow模拟边框,box-shadow: inset 0px -1px 1px -1px #06c;没用过,不过多评论,建议自己百度;
  • 伪类缩放:现在用的比较多的方式之一 :after 1px然后transform: scale(0.5);基本能满足所有场景,对于圆角的处理也很方便;
贴上3、5两方案代码,也是目前公司一直在用的(预处理SCSS):
//3、css3的background-image 本文由@IT·平头哥联盟-首席填坑官∙苏南分享
@mixin border($top:1, $right:1, $bottom:1, $left:1, $color:#ebebf0) {
  background-image:linear-gradient(180deg, $color, $color 50%, transparent 50%), 
                  linear-gradient(90deg, $color, $color 50%, transparent 50%), 
                  linear-gradient(0deg, $color, $color 50%, transparent 50%),
                  linear-gradient(90deg, $color, $color 50%, transparent 50%);
  background-size: 100% $top + px, $right + px 100%, 100% $bottom + px, $left + px 100%;
  background-repeat: no-repeat;
  background-position: top, right top, bottom, left top ;
}

@mixin borderTop($top:1, $color:#ebebf0) {
  @include border($top, 0, 0, 0, $color);
}
@mixin borderRight($right:1, $color:#ebebf0) {
  @include border(0, $right, 0, 0, $color);
}
@mixin borderBottom($bottom:1, $color:#ebebf0) {
  @include border(0, 0, $bottom, 0, $color);
}
@mixin borderLeft($left:1, $color:#ebebf0) {
  @include border(0, 0, 0, $left, $color);
}
@mixin borderColor($color:#ebebf0) {
  @include border(1, 1, 1, 1, $color);
}

//5、css3的transform:scale  本文由平头哥联盟-首席填坑官∙苏南分享
@mixin borderRadius($width:1,$style:solid,$color:#ebebf0,$radius:2px) {
  position:relative;
    &:after{
       left:0px;
       top:0px;
       right:-100%;
       bottom:-100%;
       border-radius:$radius;
       border-style: $style;
       border-color: $color;
       border-width: $width+ px;
       position:absolute;
       display:block;
       transform:scale(0.5);
       -webkit-transform:scale(0.5);
       transform-origin:0 0;
       -webkit-transform-origin:0 0;
       content:'';
    }
}
display:none与visibility:hidden两者的区别?
  • display:none在页面中是不占位置的,而visibility:hidden保留原来的位置后;
  • display:none显示/隐藏 页面会产生回流和重绘的问题,visibility则不会 ——重绘/回流请看JS部分第七题;

Javascript

1、请将下列b函数进行修改,保证每次调用a都能+1(考闭包):
//本文由@IT·平头哥联盟-首席填坑官∙苏南分享,如有错误,欢迎留言
function b(){
    var a=1;
};

function b(){
    var a=1;
    return ()=>{
        a++;
        return a;
    }
};
let c = b();
c(); //2
c(); //3
c(); //4
2、js有哪些基本数据类型:   

ECMAScript 标准定义有7种数据类型:  

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol :(ECMAScript 6 新定义 ,Symbol 生成一个全局唯一、表示独一无二的值) 
  • Object :(Array、Function、Object)
3、用js将 386485473.88 转换为 386,485,473.88(千位分割符):
//方法1:
var separator=(num)=>{
    if(!num){
        return '0.00';
    };
    let str = parseFloat(num).toFixed(2);
    return str && str
        .toString()
        .replace(/(\d)(?=(\d{3})+\.)/g, function($0, $1) {
            return $1 + ",";
        });
}

separator(386485473.88) //"386,485,473.88"

//方法2:
(386485473.88).toLocaleString('en-US')  // "386,485,473.88" 由 (sRect)补充
4、js的 for 跟for in 循环它们之间的区别?
  • 遍历数组时的异同: for循环 数组下标的typeof类型:number, for in 循环数组下标的typeof类型:string;
var southSu = ['苏南','深圳','18','男'];
for(var i=0;i<southSu.length;i++){
    console.log(typeof i); //number
    console.log(southSu[i]);// 苏南 , 深圳 , 18 , 男
}
var arr = ['苏南','深圳','18','男','帅气',"@IT·平头哥联盟-首席填坑官"];
for(var k in arr){
    console.log(typeof k);//string
    console.log(arr[k]);// 苏南 , 深圳 , 18 , 男 , 帅气,平头哥联盟-首席填坑官
}
  • 遍历对象时的异同:for循环 无法用于循环对象,获取不到obj.length; for in 循环遍历对象的属性时,原型链上的所有属性都将被访问,解决方案:使用hasOwnProperty方法过滤或Object.keys会返回自身可枚举属性组成的数组
Object.prototype.test = '原型链上的属性,本文由平头哥联盟-首席填坑官∙苏南分享';
var southSu = {name:'苏南',address:'深圳',age:18,sex:'男',height:176};
for(var i=0;i<southSu.length;i++){
    console.log(typeof i); //空
    console.log(southSu[i]);//空
}


for(var k in southSu){
    console.log(typeof k);//string
    console.log(southSu[k]);// 苏南 , 深圳 , 18 , 男 , 176 ,本文由平头哥联盟-首席填坑官∙苏南分享
}
5、给table表格中的每个td绑定事件,td数量为1000+,写一下你的思路(事件委托题):
<body class="container">
    <table id="table">
        <tr><td>我们是@IT·平头哥联盟</td><td>,我是首席填坑官</td><td>苏南</td><td>前端开发</td><td>优秀</td></tr>
        <tr><td>我们是@IT·平头哥联盟</td><td>,我是首席填坑官</td><td>苏南</td><td>前端开发</td><td>优秀</td></tr>
        <tr><td>我们是@IT·平头哥联盟</td><td>,我是首席填坑官</td><td>苏南</td><td>前端开发</td><td>优秀</td></tr>
        …………
    </table>
<script>
    let table =document.querySelector("#table");
    table.addEventListener("click",(e)=>{
        let {nodeName} = e.target;
        if(nodeName.toUpperCase() === "TD"){
            console.log(e.target);//<td>N</td>
        }
    },false);

</script>
</body>
6、js把一串字符串去重(能统计出字符重复次数更佳),列出你的思路(两种以上):
<script>
    let str = "12qwe345671dsfa233dsf9876ds243dsaljhkjfzxcxzvdsf本文由平头哥联盟-首席填坑官∙苏南分享";
    let array = str.split("");

    //方案一:
    array = [...new Set(array)].join("");
    array = ((a)=>[...new Set(a)])(array).join("");
    console.log(array);//12qwe34567dsfa98ljhkzxcv本文由平头哥联盟-首席填坑官∙苏南分享  只能过滤,不会统计

    //方案二:
    function unique (arr) {
        const seen = new Map()
        return (arr.filter((a) => !seen.has(a) && seen.set(a, 1))).join("");
    }
    console.log(unique(array)) // 12qwe34567dsfa98ljhkzxcv本文由平头哥联盟-首席填坑官∙苏南分享

    //方案三:
    function unique (arr) {
        let arrs=[];
        var news_arr = arr.sort();//排序能减少一次循环
        for(var i=0;i<news_arr.length;i++){
                if(news_arr[i] == news_arr[i+1] && news_arr[i]!= news_arr[i-1] ){
                        arrs.push(arr[i]);
                };
 
        };
        return arrs.join("");
    }
    console.log(unique(array)) // 12qwe34567dsfa98ljhkzxcv本文由平头哥联盟-首席填坑官∙苏南分享

    //方案四:
    function unique (arr) {
        let obj={};
        for(var i=0;i<arr.length;i++){
            let key = arr[i];
            if(!obj[key] ){
                    obj[key]=1;
            }else{
                obj[key]+=1;
            }
 
        };
        return obj;
    }
    console.log(unique(array)) // object 对应每个key以及它重复的次数 

</script>
7、项目上线前,你们做过哪些性能优化:
  • 图片预加载,css样式表放在顶部且link链式引入,javascript放在底部body结束标签前;
  • 使用dns-prefetch对项目中用到的域名进行 DNS 预解析,减少 DNS 查询,如: <link rel="dns-prefetch" href="//github.com"/>; 
  • 减少http请求次数:图片静态资源使用CDN托管;
  • API接口数据设置缓存,CSS Sprites/SVG Sprites(如有疑惑:该如何以正确的姿势插入SVG Sprites? 这篇说的很详细), JS、CSS源码压缩、图片大小控制合适,使用iconfont(+ 字体图标)或SVG,它们比图片更小更清晰,网页Gzip压缩;
  • 减少DOM操作次数,优化javascript性能;
  • 减少 DOM 元素数量,合理利用:after、:before等伪类;
  • 避免重定向、图片懒加载;前后端分离开发,资源按需加载,最好能做到首屏直出(即服务端渲染); 
  • 避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性) ;
  • 多域名分发划分内容到不同域名,解决浏览器域名请求并发数问题,同时也解决了请求默认携带的cookie问题;
  • 尽量减少 iframe 使用,它会阻塞主页面的渲染; 对所有资源压缩 JavaScript 、 CSS 、字体、图片等,甚至html;
  • 只想到这些,欢迎补充……
8、你对重绘、重排的理解?
  • 首先网页数次渲染生成时,这个可称为重排; 
  • 修改DOM、样式表、用户事件或行为(鼠标悬停、页面滚动、输入框键入文字、改变窗口大小等等)这些都会导致页面重新渲染,那么重新渲染,就需要重新生成布局和重新绘制节点,前者叫做"重排",后者"重绘"; 
  • 减少或集中对页面的操作,即多次操作集中在一起执行; 
  • 总之可以简单总结为:重绘不一定会重排,但重排必然为会重绘。 
  • 更详细的可以看阮老师分析
8、有用过promise吗?请写出下列代码的执行结果,并写出你的理解思路:
setTimeout(()=>{
        console.log(1);
}, 0);

new Promise((resolve)=>{
        console.log(2);
        for(var i = 1; i < 200; i++){
                i = 198 && resolve();
        }
        console.log(3);
}).then(()=>{
        console.log(4);
});
console.log(5);

// 结果:2、3、5、4、1;
  • 首先要讲一下,js是单线程执行,那么代码的执行就有先后; 
  • 有先后,那就要有规则(排队),不然就乱套了,那么如何分先后呢?大体分两种:同步、异步; 
  • 同步很好理解,就不用多说了(我就是老大,你们都要给我让路); 
  • 异步(定时器[setTimeout ,setInterval]、事件、ajax、promise等),说到异步又要细分宏任务、微任务两种机制, 
  • 宏任务:js异步执行过程中遇到宏任务,就先执行宏任务,将宏任务加入执行的队列(event queue),然后再去执行微任务; 
  • 微任务:js异步执行过程中遇到微任务,也会将任务加入执行的队列(event queue),但是注意这两个queue身份是不一样的,不是你先进来,就你先出去的(就像宫里的皇上选妃侍寝一样,不是你先进宫(或先来排队)就先宠幸的 ),真执行的时候是先微任务里拿对应回调函数,然后才轮到宏任务的队列回调执行的; 
  • 理解了这个顺序,那上面的结果也就不难懂了。
说细步骤如下:
setTimeout 是异步,不会立即执行,加入执行队列;
new Promise 会立即执行 输出 2、3,而在2、3之间执行了resolve 也就是微任务;
再到console.log(5)了,输出5;
然后异步里的微任务先出,那就得到4;
最后执行宏任务 setTimeout 输出 1;

如有错误欢迎纠正!

9、new SouthSu() 在这个过程中都做了些什么?
function SouthSu(){
         this.name = "苏南";
         this.age = 18;
         this.address = "深圳";
         this.address = "首席填坑官";
};

 let South = new SouthSu();
 console.log(South,South.__proto__ === SouthSu.prototype) //true 

执行过程:
创建一个空的对象
 let p1 = new Object();


设置原型链
    p1.__proto__ = SouthSu.prototype;

让 构造函数 的this 指向 p1 这个空对象

    let funCall = SouthSu.call(p1);

处理 构造函数 的返回值:判断SouthSu的返回值类型,如果是值类型则返回obj,如果是引用类型,就返回这个引用类型的对象;
10、工作中如果让你使用js实现一个持续的动画,你会怎么做(比如转盘抽奖)??
  • js来实现动画,第一时间想到的就是定时器(setTimeout、setInterval); 
  • 后面想起来js有个 window.requestAnimationFrame ,当时只是说了记得有这么一个API,具体的细节没能答上,面试官直言想听的就是这个API的使用,好吧是我准备的不够充分,希望其他同学不再犯同样错误;
window.requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用,回调的次数通常是每秒60次,是大多数浏览器通常匹配 W3C 所建议的刷新频率。在大多数浏览器里,当运行在后台标签页或者隐藏的<iframe> 里时,requestAnimationFrame() 会暂停调用以提升性能和电池寿命。

小结:以往项目开发中大数人可能都是第一时间选择JS定时器setInterval 或者setTimeout 来控制的动画每隔一段时间刷新元素的状态,来达到自己所想要的动画效果,但是这种方式并不能准确地控制动画帧率,因为这是开发者主动要求浏览器去绘制,它这可能会因为动画控制的时间、绘制的频率、浏览器的特性等而导致丢帧的问题; requestAnimationFrame 是浏览器什么时候要开始绘制了浏览器它自己知道,通过requestAnimationFrame告诉我们,这样就不会出现重复绘制丢失的问题。

//一个持续旋转的正方形,
<div class="angle-div"></div>
<script>
    let timer = null;
    let Deg = 0;
    let distance = 360;
    var _requestAnimationFrame_ = window.requestAnimationFrame || window.webkitRequestAnimationFrame;//本文由平头哥联盟-首席填坑官∙苏南分享
    let angleDiv = document.querySelector(".angle-div");
    cancelAnimationFrame(timer);
    let fn = ()=>{
        if(Deg < distance){ 
            Deg++;
        }else{
            Deg=0;
        };
        angleDiv.style.transform = `rotateZ(${Deg}deg) translateZ(0)`; 
        angleDiv.style.WebkitTransform = `rotateZ(${Deg}deg) translateZ(0)`;
        timer = _requestAnimationFrame_(fn);
    }
    timer = _requestAnimationFrame_(fn);
</script>

本文由@IT·平头哥联盟-首席填坑官∙苏南分享

11、如何设置http缓存?

 1)、Expires

  • Expires的值为服务端返回的到期时间,响应时告诉浏览器可以直接从浏览器缓存中读取无需再次请求。

缺点:返回的是服务端的时间,比较的时间是客户端的时间,如果时间不一致有可能出现错误。

 2)、Cache-Control

  • Cache-Control可设置的字段有:
  • private:客户端可以缓存
  • public:客户端和代理服务器都可以缓存
  • max-age=xxx:缓存内容在xxx秒后失效
  • no-cache:需要用另一种缓存策略来验证缓存(ETag,Last-Modified)
  • no-store:不进行缓存
  • Last-Modified:浏览器请求获得文件后,服务器返回该文件的最后修改时间Last-Modified,下一次请求会带上If-Modified-Since标识,如果If-Modified-Since等于服务器的文件修改时间,则表示文件没有修改,返回304状态码,浏览器从浏览器缓存中读取文件。如果If-Modified-Since小于服务端的文件修改时间,则浏览器会重新发送请求获取文件,返回状态码200。
  • ETag:服务器文件的一个唯一标识,例如对文件内容取md5值作为ETag的字段返回给浏览器。当文件变化时ETag的值也会发生变化。下次请求会带上If-None-Match即浏览器保留的ETag值,如果发送了变化,则文件被修改,需要重新请求,返回200状态码。反之浏览器就从缓存中读取文件,返回304状态码。

总结:——几者之间的关系

  • Cache-Control设置为max-age=xx并且同事设置Expires时,Cache-Control的优先级更高
  • ETagLast-Modified同时存在时,服务器先会检查ETag,然后再检查Last-Modified,最终决定返回304还是200
  • 该题由 本文由平头哥联盟-成员(ZodiacSyndicate )补充
12、随机打乱一个数组
  • 思路:从数组的最后一项开始,随机选择前面的一个元素进行交换,然后一步步往前交换
//该题由 本文由平头哥联盟-成员(ZodiacSyndicate )补充
const shuffle = arr => {
  let end = arr.length - 1
  while(end) { // 当end为0时不需要交换
    const index = Math.floor(Math.random() * (end + 1))
    [arr[index], arr[end]] = [arr[end], arr[index]]
    end -= 1
  }
  return arr
}
13、用React实现一个显示鼠标位置的高阶组件
//该题由 本文由平头哥联盟-成员(ZodiacSyndicate )补充
const mousePosition = Component => class extends React.Component {
  state = {
    x: 0,
    y: 0,
  }

  handleMouseMove = e => {
    this.setState({
      x: e.clientX,
      y: e.clientY
    })
  }

  render() {
    const { x, y } = this.state
    return (
      <>
        <div onMouseMove={this.handleMouseMove}>
          <Component {...this.props} />
        </div>
        <span>x: {x}</span>
        <span>y: {y}</span>
      </>
    )
  }
}

文本将持续更新,整理收集自己/群友的面经分享给大家,如觉得不错,记得关注我们的公众号哦!!

宝剑锋从磨砺出,梅花香自苦寒来,做有温度的攻城狮,公众号:honeyBadger8,!本文由@IT·平头哥联盟-首席填坑官∙苏南分享,@IT·平头哥联盟 主要分享前端、测试 等领域的积累,文章来源于(自己/群友)工作中积累的经验、填过的坑,希望能尽绵薄之力 助其他同学少走一些弯路

更多文章:

做完小程序项目、老板给我加了6k薪资~
你应该做的前端性能优化之总结大全!
如何给localStorage设置一个过期时间?
手把手教你如何绘制一辆会跑车
如何用CSS3画出懂你的3D魔方?
SVG Sprites Icon的使用技巧
immutability因React官方出镜之使用总结分享!

作者:苏南 - 首席填坑官
交流群:912594095,公众号:honeyBadger8
本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。
查看原文

Tims 收藏了文章 · 2018-10-16

再来一波PHP程序员必看书籍

前言

https://segmentfault.com/a/11... 内列出的是已看过的。

本篇文章内列出的书籍是准备要看或者正在看的,与大家分享。

知识无价,还是建议各位童鞋把更多的资金投入到学习中。书名排名不分前后

《编码:隐匿在计算机软硬件背后的语言》

clipboard.png

https://item.jd.com/11116026....

《数学之美(第二版)》

clipboard.png

https://item.jd.com/11572052....

《计算机科学导论》

clipboard.png

https://item.jd.com/11758229....

《PHP Web安全开发实战》

clipboard.png

https://item.jd.com/335372257...

《深入浅出MySQL》

clipboard.png

https://item.jd.com/11381295....

这是一本比较基础的MySQL书籍

《现代操作系统》

clipboard.png

https://item.jd.com/12139635....

《高性能MySQL》

clipboard.png

https://item.jd.com/11220393....

《Head First》

clipboard.png

https://item.jd.com/10100236....

《领域驱动设计》

clipboard.png

https://item.jd.com/11961038....

《深入理解计算机系统》

clipboard.png

https://item.jd.com/257534918...

《代码大全》

clipboard.png

https://item.jd.com/292476885...

《Go语言从入门到进阶实战》

clipboard.png

https://item.jd.com/12380444....

最后就是Go语言,不得不说未来的PHP程序员的技术栈内一定有Go.

致谢

感谢你看到这里,希望本篇文章可以帮到你,谢谢。

查看原文

Tims 收藏了文章 · 2018-09-21

JavaScript数组去重(12种方法,史上最全)

数组去重,一般都是在面试的时候才会碰到,一般是要求手写数组去重方法的代码。如果是被提问到,数组去重的方法有哪些?你能答出其中的10种,面试官很有可能对你刮目相看。
在真实的项目中碰到的数组去重,一般都是后台去处理,很少让前端处理数组去重。虽然日常项目用到的概率比较低,但还是需要了解一下,以防面试的时候可能回被问到。

注:写的匆忙,加上这几天有点忙,还没有非常认真核对过,不过思路是没有问题,可能一些小细节出错而已。

数组去重的方法

一、利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。

二、利用for嵌套for,然后splice去重(ES5中最常用)

function unique(arr){            
        for(var i=0; i<arr.length; i++){
            for(var j=i+1; j<arr.length; j++){
                if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                }
            }
        }
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]     //NaN和{}没有去重,两个null直接消失了

双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
想快速学习更多常用的ES6语法,可以看我之前的文章《学习ES6笔记──工作中常用到的ES6语法》

三、利用indexOf去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array .indexOf(arr[i]) === -1) {
            array .push(arr[i])
        }
    }
    return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
   // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]  //NaN、{}没有去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。

四、利用sort()

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return;
    }
    arr = arr.sort()
    var arrry= [arr[0]];
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            arrry.push(arr[i]);
        }
    }
    return arrry;
}
     var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]      //NaN、{}没有去重

利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。

五、利用对象的属性不能相同的特点进行去重(这种数组去重的方法有问题,不建议用,有待改进)

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var arrry= [];
     var  obj = {};
    for (var i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            arrry.push(arr[i])
            obj[arr[i]] = 1
        } else {
            obj[arr[i]]++
        }
    }
    return arrry;
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}]    //两个true直接去掉了,NaN和{}去重

六、利用includes

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]     //{}没有去重

七、利用hasOwnProperty

function unique(arr) {
    var obj = {};
    return arr.filter(function(item, index, arr){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]   //所有的都去重了

利用hasOwnProperty 判断是否存在对象属性

八、利用filter

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]

九、利用递归去重

function unique(arr) {
        var array= arr;
        var len = array.length;

    array.sort(function(a,b){   //排序后更加方便去重
        return a - b;
    })

    function loop(index){
        if(index >= 1){
            if(array[index] === array[index-1]){
                array.splice(index,1);
            }
            loop(index - 1);    //递归loop,然后数组去重
        }
    }
    loop(len-1);
    return array;
}
 var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

十、利用Map数据结构去重

function arrayNonRepeatfy(arr) {
  let map = new Map();
  let array = new Array();  // 数组用于返回结果
  for (let i = 0; i < arr.length; i++) {
    if(map .has(arr[i])) {  // 如果有该key值
      map .set(arr[i], true); 
    } else { 
      map .set(arr[i], false);   // 如果没有该key值
      array .push(arr[i]);
    }
  } 
  return array ;
}
 var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果。

十一、利用reduce+includes

function unique(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

十二、[...new Set(arr)]

[...new Set(arr)] 
//代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)

PS:有些文章提到了foreach+indexOf数组去重的方法,个人觉得都是大同小异,所以没有写上去。

查看原文

Tims 关注了专栏 · 2018-09-11

前端小册子

冷哥哥的前端小册子。

关注 1440

Tims 赞了文章 · 2018-09-11

Javascript 模块化指北

前言

随着 Web 技术的蓬勃发展和依赖的基础设施日益完善,前端领域逐渐从浏览器扩展至服务端(Node.js),桌面端(PC、Android、iOS),乃至于物联网设备(IoT),其中 JavaScript 承载着这些应用程序的核心部分,随着其规模化和复杂度的成倍增长,其软件工程体系也随之建立起来(协同开发、单元测试、需求和缺陷管理等),模块化编程的需求日益迫切。

JavaScript 对模块化编程的支持尚未形成规范,难以堪此重任;一时间,江湖侠士挺身而出,一路披荆斩棘,从刀耕火种过渡到面向未来的模块化方案;

<!-- more -->

概念

模块化编程就是通过组合一些__相对独立可复用的模块__来进行功能的实现,其最核心的两部分是__定义模块__和__引入模块__;

  • 定义模块时,每个模块内部的执行逻辑是不被外部感知的,只是导出(暴露)出部分方法和数据;
  • 引入模块时,同步 / 异步去加载待引入的代码,执行并获取到其暴露的方法和数据;

刀耕火种

尽管 JavaScript 语言层面并未提供模块化的解决方案,但利用其可__面向对象__的语言特性,外加__设计模式__加持,能够实现一些简单的模块化的架构;经典的一个案例是利用单例模式模式去实现模块化,可以对模块进行较好的封装,只暴露部分信息给需要使用模块的地方;

// Define a module
var moduleA = (function ($, doc) {
  var methodA = function() {};
  var dataA = {};
  return {
    methodA: methodA,
    dataA: dataA
  };
})(jQuery, document);

// Use a module
var result = moduleA.mehodA();

直观来看,通过立即执行函数(IIFE)来声明依赖以及导出数据,这与当下的模块化方案并无巨大的差异,可本质上却有千差万别,无法满足的一些重要的特性;

  • 定义模块时,声明的依赖不是强制自动引入的,即在定义该模块之前,必须手动引入依赖的模块代码;
  • 定义模块时,其代码就已经完成执行过程,无法实现按需加载;
  • 跨文件使用模块时,需要将模块挂载到全局变量(window)上;

AMD & CMD 二分天下

题外话:由于年代久远,这两种模块化方案逐渐淡出历史舞台,具体特性不再细聊;

为了解决”刀耕火种”时代存留的需求,AMD 和 CMD 模块化规范问世,解决了在浏览器端的异步模块化编程的需求,__其最核心的原理是通过动态加载 script 和事件监听的方式来异步加载模块;__

AMD 和 CMD 最具代表的两个作品分别对应 require.js 和 sea.js;其主要区别在于依赖声明和依赖加载的时机,其中 require.js 默认在声明时执行, sea.js 推崇懒加载和按需使用;另外值得一提的是,CMD 规范的写法和 CommonJS 极为相近,只需稍作修改,就能在 CommonJS 中使用。参考下面的 Case 更有助于理解;

// AMD
define(['./a','./b'], function (moduleA, moduleB) {
  // 依赖前置
  moduleA.mehodA();
  console.log(moduleB.dataB);
  // 导出数据
  return {};
});
 
// CMD
define(function (requie, exports, module) {
  // 依赖就近
  var moduleA = require('./a');
  moduleA.mehodA();     

  // 按需加载
  if (needModuleB) {
    var moduleB = requie('./b');
    moduleB.methodB();
  }
  // 导出数据
  exports = {};
});

CommonJS

2009 年 ry 发布 Node.js 的第一个版本,CommonJS 作为其中最核心的特性之一,适用于服务端下的场景;历年来的考察和时间的洗礼,以及前端工程化对其的充分支持,CommonJS 被广泛运用于 Node.js 和浏览器;

// Core Module
const cp = require('child_process');
// Npm Module
const axios = require('axios');
// Custom Module
const foo = require('./foo');

module.exports = { axios };
exports.foo = foo;

规范

  • module (Object): 模块本身
  • exports (*): 模块的导出部分,即暴露出来的内容
  • require (Function): 加载模块的函数,获得目标模块的导出值(基础类型为复制,引用类型为浅拷贝),可以加载内置模块、npm 模块和自定义模块

实现

1、模块定义

默认任意 .node .js .json 文件都是符合规范的模块;

2、引入模块

首先从缓存(require.cache)优先读取模块,如果未命中缓存,则进行路径分析,然后按照不同类型的模块处理:

  • 内置模块,直接从内存加载;
  • 外部模块,首先进行文件寻址定位,然后进行编译和执行,最终得到对应的导出值;

其中在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装,结果如下:

(function (exports, require, module, __filename, __dirname) {
    var circle = require('./circle.js');
    console.log('The area of a circle of radius 4 is ' + circle.area(4));
});

特性总结

  • 同步执行模块声明和引入逻辑,分析一些复杂的依赖引用(如循环依赖)时需注意;
  • 缓存机制,性能更优,同时限制了内存占用;
  • Module 模块可供改造的灵活度高,可以实现一些定制需求(如热更新、任意文件类型模块支持);

ES Module(推荐使用)

ES Module 是语言层面的模块化方案,由 ES 2015 提出,其规范与 CommonJS 比之 ,导出的值<span data-type="color" style="color:rgb(26, 26, 26)"><span data-type="background" style="background-color:rgb(255, 255, 255)">都可以看成是一个具备多个属性或者方法的对象</span></span>,可以实现互相兼容;但写法上 ES Module 更简洁,与 Python 接近;

import fs from 'fs';
import color from 'color';
import service, { getArticles } from '../service'; 

export default service;
export const getArticles = getArticles;

主要差异在于:

  • ES Module 会对<span data-type="color" style="color:rgb(26, 26, 26)"><span data-type="background" style="background-color:rgb(255, 255, 255)">静态代码分析,即在代码编译时进行模块的加载,在运行时之前就已经确定了依赖关系(可解决循环引用的问题);</span></span>
  • ES Module 关键字:importexport 以及独有的 default 关键字,确定默认的导出值;
  • ES Module 中导出的值是一个 只读的值的引用 ,无论基础类型和复杂类型,而在 CommonJS 中 require 的是值的拷贝,其中复杂类型是值的浅拷贝;
// a.js
export let a = 1;
export function caculate() {
  a++;
};

// b.js
import { a, caculate } from 'a.js';

console.log(a); // 1
caculate();
console.log(a); // 2

a = 2; // Syntax Error: "a" is read-only

UMD

通过一层自执行函数来兼容各种模块化规范的写法,兼容 AMD / CMD / CommonJS 等模块化规范,贴上代码胜过千言万语,需要特别注意的是 ES Module 由于会对静态代码进行分析,故这种运行时的方案无法使用,此时通过 CommonJS 进行兼容;

(function (global, factory) {
  if (typeof exports === 'object') {   
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    define(factory);
  } else {
    this.eventUtil = factory();
  }
})(this, function (exports) {
 ​ // Define Module
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.default = 42;
});

构建工具中的实现

为了在浏览器环境中运行模块化的代码,需要借助一些模块化打包的工具进行打包( 以 webpack 为例),定义了项目入口之后,会先快速地进行依赖的分析,然后将所有依赖的模块转换成浏览器兼容的对应模块化规范的实现;

模块化的基础

从上面的介绍中,我们已经对其规范和实现有了一定的了解;在浏览器中,要实现 CommonJS 规范,只需要实现 module / exports / require / global 这几个属性,由于浏览器中是无法访问文件系统的,因此 require 过程中的文件定位需要改造为加载对应的 JS 片段(webpack 采用的方式为通过函数传参实现依赖的引入)。具体实现可以参考:tiny-browser-require

webpack 打包出来的代码快照如下,注意看注释中的时序;

(function (modules) {
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {}
  return __webpack_require__(0); // ---> 0
})
({
  0: function (module, exports, __webpack_require__) {
    // Define module A
    var moduleB = __webpack_require__(1); // ---> 1
  },
  1: function (module, exports, __webpack_require__) {
    // Define module B
    exports = {}; // ---> 2
  }
});

实际上,ES Module 的处理同 CommonJS 相差无几,只是在定义模块和引入模块时会去处理 __esModule 标识,从而兼容其在语法上的差异。

异步和扩展

1、浏览器环境下,网络资源受到较大的限制,因此打包出来的文件如果体积巨大,对页面性能的损耗极大,因此需要对构建的目标文件进行拆分,同时模块也需要支持动态加载;

webpack 提供了两个方法 require.ensure() 和 import() (推荐使用)进行模块的动态加载,至于其中的原理,跟上面提及的 AMD & CMD 所见略同,import() 执行后返回一个 Promise 对象,其中所做的工作无非也是动态新增 script 标签,然后通过 onload / onerror 事件进一步处理。

2、由于 require 函数是完全自定义的,我们可以在模块化中实现更多的特性,比如通过修改 require.resolve 或 Module._extensions 扩展支持的文件类型,使得 css / .jsx / .vue / 图片等文件也能为模块化所使用;

附录1:特性一览表

模块化规范加载方式加载时机运行环境备注
AMD异步运行时浏览器
CMD异步运行时浏览器依赖基于静态分析,require 时已经 module ready
CommonJS同步/异步运行时浏览器 / Node
ES Module同步/异步编译阶段浏览器 / Node通过 import() 实现异步加载

附录2:参考

查看原文

赞 103 收藏 84 评论 1

Tims 收藏了文章 · 2018-08-09

PHP 垃圾回收与内存管理指引

图片描述

本文首发于 PHP 垃圾回收与内存管理指引,转载请注明出处。

本文将要讲述 PHP 发展历程中的垃圾回收及内存管理相关内容,文末给出 PHP 发展在各个阶段有关内存管理及垃圾回收(内核)参考资料值得阅读。

引用计数

在 PHP 5.2 及以前的版本中,PHP 的垃圾回收采用的是 引用计数 算法。

引用计数基础知识

引用计数基础知识

php 的变量存储在「zval」变量容器(数据结构)中,「zval」属性包含如下信息:

  • 当前变量的数据类型;
  • 当前变量的值;
  • 用于标识变量是否为引用传递的 is_ref 布尔类型标识;
  • 指向该「zval」变量容器的变量个数的 refcount 标识符(即这个 zval 被引用的次数,注意这里的引用不是指引用传值,注意区分)。

当一个变量被赋值时,就会生成一个对应的「zavl」变量容器。

查看变量 zval 容器信息

要查看变量的「zval」容器信息(即查看变量的 is_ref 和 refcount),可以使用 XDebug 调试工具的 xdebug_debug_zval() 函数。

安装 XDebug 扩展插件的方法可以查看 这个教程,有关XDebug 使用方法请阅读 官方文档

假设,我们已经成功安装好 XDebug 工具,现在就可以来对变量进行调试了。

  • 查看普通变量的 zval 信息

如果我们的 PHP 语句只是对变量进行简单赋值时,is_ref 标识值为 0,refcount 值为 1;若将这个变量作为值赋值给另一个变量时,则增加 zval 变量容器的 refcount 计数;同理,销毁(unset)变量时,「refcount」相应的减去 1。

请看下面的示例:

<?php
// 变量赋值时,refcount 值等于 1
$name = 'liugongzi';
xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)

// $name 作为值赋值给另一个变量, refcount 值增加 1
$copy = $name;
xdebug_debug_zval('name'); // (refcount=2, is_ref=0)string 'liugongzi' (length=9)

// 销毁变量,refcount 值减掉 1
unset($copy);
xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
  • 写时复制
写时复制(Copy On Write:COW),简单描述为:如果通过赋值的方式赋值给变量时不会申请新内存来存放新变量所保存的值,而是简单的通过一个计数器来共用内存,只有在其中的一个引用指向变量的值发生变化时,才申请新空间来保存值内容以减少对内存的占用。 - TPIP 写时复制

通过前面的简单变量的 zval 信息我们知道 &dollar;copy&dollar;name 共用 zval 变量容器(内存),然后通过 refcount 来表示当前这个 zval 被多少个变量使用。

看个实例:

<?php
$name = 'liugongzi';
xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9)

$copy = $name;
xdebug_debug_zval('name'); // name: (refcount=2, is_ref=0)string 'liugongzi' (length=9)

// 将新的值赋值给变量 $copy
$copy = 'liugongzi handsome';
xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9)
xdebug_debug_zval('copy'); // copy: (refcount=1, is_ref=0)='liugongzi handsome'

注意到没有,当将值 liugongzi handsome 赋值给变量 &dollar;copy 时,name 和 copy 的 refcount 值都变成了 1,在这个过程中发生以下几个操作:

  • 将 &dollar;copy 从 &dollar;name 的 zval(内从)中分离出来(即复制);
  • 将 &dollar;name 的 refcount 减去 1;
  • 对 &dollar;copy 的 zval 进行修改(重新赋值和修改 refcount);

这里只是简单对「写时复制」进行介绍,感兴趣的朋友可以阅读文末给出的参考资料进行更加深入的研究。

  • 查看引用传递变量的 zval 信息

引用传值(&)的「引用计数」规则同普通赋值语句一样,只是 is_ref 标识的值为 1 表示该变量是引用传值类型。

我们现在来看看引用传值的示例:

<?php
$age = 'liugongzi';
xdebug_debug_zval('age'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)

$copy = &$age;
xdebug_debug_zval('age'); // (refcount=2, is_ref=1)string 'liugongzi' (length=9)

unset($copy);
xdebug_debug_zval('age'); // (refcount=1, is_ref=1)string 'liugongzi' (length=9)
  • 复合类型的引用计数

与标量类型(整型、浮点型、布尔型等)不同,数组(array)和对象(object)这种符合类型的引用计数规则会稍复杂一些。

为了更好的说明,还是先看看数组的引用计数示例:

$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );

// a:
// (refcount=1, is_ref=0)
// array (size=2)
//  'meaning' => (refcount=1, is_ref=0)string 'life' (length=4)
//  'number' => (refcount=1, is_ref=0)int 42

上面的引用计数示意图如下:

示意图

从图中我们发现复合类型的引用计数规则基本上同标量的计数规则一样,就给出的示例来说,PHP 会创建 3 个 zval 变量容器,一个用于存储数组本身,另外两个用于存储数组中的元素。

添加一个已经存在的元素到数组中时,它的引用计数器 refcount 会增加 1。

$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );

// a:
// (refcount=1, is_ref=0)
// array (size=3)
//  'meaning' => (refcount=2, is_ref=0)string 'life' (length=4)
//  'number' => (refcount=0, is_ref=0)int 42
//  'life' => (refcount=2, is_ref=0)string 'life' (length=4)

大致示意图如下:

示意图

  • 内存泄露

虽然,复合类型的引用计数规则同标量类型大致相同,但是如果引用的值为变量自身(即循环应用),在处理不当时,就有可能会造成内存泄露的问题。

让我们来看看下面这个对数组进行引用传值的示例:

<?php
// @link http://php.net/manual/zh/function.memory-get-usage.php#96280
function convert($size)
{
    $unit=array('b','kb','mb','gb','tb','pb');
    return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];
}

// 注意:有用的地方从这里开始
$memory = memory_get_usage();

$a = array( 'one' );

// 引用自身(循环引用)
$a[] =&$a;

xdebug_debug_zval( 'a' );

var_dump(convert(memory_get_usage() - $memory)); // 296 b

unset($a); // 删除变量 $a,由于 $a 中的元素引用了自身(循环引用)最终导致 $a 所使用的内存无法被回收

var_dump(convert(memory_get_usage() - $memory)); // 568 b

从内存占用结果上看,虽然我们执行了 unset(&dollar;a) 方法来销毁 &dollar;a 数组,但内存并没有被回收,整个处理过程的示意图如下:

示意图

可以看到对于这块内存,再也没有符合表(变量)指向了,所以 PHP 无法完成内存回收,官方给出的解释如下:

尽管不再有某个作用域中的任何符号指向这个结构 (就是变量容器),由于数组元素 “1” 仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,但是在 php 清除之前,将耗费不少内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。 - 摘自 官方文档 Cleanup Problems

简单来说就是「引用计数」算法无法检测并释放循环引用所使用的内存,最终导致内存泄露。

引用计数系统的同步周期回收

由于引用计数算法存在无法回收循环应用导致的内存泄露问题,在 PHP 5.3 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:

  • 引入了可能根(possible root)的概念:通过引用计数相关学习,我们知道如果一个变量(zval)被引用,要么是被全局符号表中的符号引用(即变量),要么被复杂类型(如数组)的 zval 中的符号(数组的元素)引用,那么这个 zval 变量容器就是「可能根」。
  • 引入根缓冲区(root buffer)的概念:根缓冲区用于存放所有「可能根」,它是固定大小的,默认可存 10000 个可能根,如需修改可以通过修改 PHP 源码文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新编译。
  • 回收周期:当缓冲区满时,对缓冲区中的所有可能根进行垃圾回收处理。

下图(来自 PHP 手册),展示了新的回收算法执行过程:

回收周期

引用计数系统的同步周期回收过程

  1. 缓冲区(紫色框部分,称为疑似垃圾),存储所有可能根(步骤 A);
  2. 采用深度优先算法遍历「根缓冲区」中所有的「可能根(即 zval 遍历容器)」,并对每个 zval 的 refcount 减 1,为了避免遍历时对同一个 zval 多次减 1(因为不同的根可能遍历到同一个 zval)将这个 zvel 标记为「已减」(步骤 B);
  3. 再次采用深度优先遍历算法遍历「可能根 zval」。当 zval 的 refcount 值不为 0 时,对其加 1,否则保持为 0。并请已遍历的 zval 变量容器标记为「已恢复」(即步骤 B 的逆运算)。那些 zval 的 refcount 值为 0 (蓝色框标记)的就是应该被回收的变量(步骤 C);
  4. 删除所有 refcount 为 0 的可能根(步骤 D)。

整个过程为:

采用深度优先算法执行:默认删除 > 模拟恢复 > 执行删除 达到内存回收的目的。

优化后的引用计数算法优势

  • 将内存泄露控制在阀值内,这个由缓存区实现,达到缓冲区大小执行新一轮垃圾回收;
  • 提升了垃圾回收性能,不是每次 refcount 减 1 都执行回收处理,而是等到根缓冲区满时才开始执行垃圾回收。

你可以从 PHP 手册 的回收周期 了解更多,也可以阅读文末给出的参考资料。

PHP 7 的内存管理

PHP 5 中 zval 实现上的主要问题:

  • zval 总是单独 从堆中分配内存;
  • zval 总是存储引用计数和循环回收 的信息,即使是整型(bool / null)这种可能并不需要此类信息的数据;
  • 在使用对象或者资源时,直接引用会导致两次计数;
  • 某些间接访问需要一个更好的处理方式。比如现在访问存储在变量中的对象间接使用了四个指针(指针链的长度为四);
  • 直接计数也就意味着数值只能在 zval 之间共享。如果想在 zval 和 hashtable key 之间共享一个字符串就不行(除非 hashtable key 也是 zval)。

PHP 7 中的 zval 数据结构实现的调整:

最基础的变化就是 zval 需要的内存 不再是单独从堆上分配,不再由 zval 存储引用计数。
复杂数据类型(比如字符串、数组和对象)的引用计数由其自身来存储。 - 摘自 Internal value representation in PHP 7 - Part 1

这种实现的优势:

  • 简单数据类型不需要单独分配内存,也不需要计数;
  • 不会再有两次计数的情况。在对象中,只有对象自身存储的计数是有效的;
  • 由于现在计数由数值自身存储(PHP 有 zval 变量容器存储),所以也就可以和非 zval 结构的数据共享,比如 zval 和 hashtable key 之间;
  • 间接访问需要的指针数减少了。

更具体的有关 PHP 7 zval 实现和内存优化细节可以阅读 深入理解 PHP7 内核之 zvalInternal value representation in PHP 7 - Part 1

参考资料

深入理解 PHP7 内核之 zval

Internal value representation in PHP 7 - Part 1

Internal value representation in PHP 7 - Part 2

TPIP:第六节 写时复制(Copy On Write)

TPIP:内存管理

PHP7 内核之 zval

浅谈 PHP5 中垃圾回收算法 (Garbage Collection) 的演化

Confusion about PHP 7 refcount

引用计数系统中的同步周期回收 (Concurrent Cycle Collection in Reference Counted Systems) 论文

PHP7 革新与性能优化

查看原文

Tims 收藏了文章 · 2018-08-06

转盘抽奖-- 自己手撸

自己很菜,不可否认。
所以上周日试试水,看看自己能否写个圆盘抽奖的demo。
// github L6zt
开发思路

  • 布局 css rotate 布局;
  • 抽奖过渡效果,采用css3 transition;
  • 动态计算抽奖结束时的角度,赋值给待选择的元素。

效果图
图片描述

代码

    <div class="rotate tn">
        <!-- 外部圆-->
        <div class="out-circle">
            <p class="p1">1</p>
            <p class="p2">2</p>
            <p class="p3">3</p>
        </div>
         <!--内部园-->
        <div class="inner-circle">
            <p class="p11">a</p>
            <p class="p12">b</p>
            <p class="p13">c</p>
            <p class="p14">d</p>
            <p class="p15">e</p>
            <p class="p16">f</p>
        </div>
    </div>
    <div class="start-game">
        <label for="num">
            <input 
                   type="text" 
                   id="num" name="num"
                   placeholder="请输入外数字(0-2))"
                   />
        </label>
        <a href="javascript:void(0);">开始</a>
    </div>
        * {
            margin: 0;
        }
        .rotate {
            position: relative;
            margin: 0 auto;
            width: 400px;
            height: 400px;
            text-align: center;
            color: #fff;
            font-size: 50px;
            border-radius: 50%;
            background: antiquewhite;
        }
        .tn {
            transition: all 3s cubic-bezier(.11,.77,.2,.94);
            transform-origin: 50% 50%;
        }
        .out-circle {
            position: absolute;
            width: 300px;
            height: 300px;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            margin: auto;
        }
        /*基础旋转的圆*/
        .out-circle  p {
            position: absolute;
            display: block;
            margin: 0 auto;
            left: 0;
            right: 0;
            width: 30px;
            height: 30px;
            line-height: 30px;
            background: red;
            /*以自己的宽度的一半 为 x,以父盒子的高度一半 为 y, 作为旋转点。*/
            transform-origin: 15px 150px;
            border-radius: 50%;
            font-size: 16px;
        }
        .inner-circle {
            position: absolute;
            width: 200px;
            height: 200px;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            margin: auto;
        }
        /*基础旋转的圆*/
        .inner-circle p {
            position: absolute;
            display: block;
            margin: 0 auto;
            left: 0;
            right: 0;
            width: 30px;
            height: 30px;
            line-height: 30px;
            background: #4eff00;
            transform-origin: 15px 100px;
            border-radius: 50%;
            font-size: 16px;
        }
        .p11 {
            transform: rotate(0deg);
        }
        .p12 {
            transform: rotate(60deg);
        }
        .p13 {
            transform: rotate(120deg);
        }
        .p14 {
            transform: rotate(180deg);
        }
        .p15 {
            transform: rotate(240deg);
        }
        .p16 {
            transform: rotate(300deg);
        }
        .p1 {
            transform: rotate(0deg);
        }
        .p2 {
            transform: rotate(120deg);
        }
        .p3 {

            transform: rotate(240deg);
        }
        a {
           padding: 2.5px 10px;
           background: #0ebeff;
           border-radius: 5px;
           color: #fff;
           text-decoration: none;
        }
     .start-game {
      position:absolute;
      top: 20px;
      left: 20px;
    }
        (function () {
            let deg = 0;
            // 基础角度
            let baseDeg = 120;
            let $input = $('#num');
            // 多少个旋转点
            let blocks = 360 / baseDeg;
            let k = null;
            let flag =  false;
            const $rotate = $('.rotate');
            // 0 1 2
            $('a').on('click', function () {
                var num = $input.val();
                // 当前旋转 位置
                var curLc = deg % 360 / 120;
                // 待旋转的角度
                deg = deg + 4 * 360 + (2*blocks  - num - curLc) * baseDeg;
                if (flag === true) {
                    return false;
                }
                flag = true;
                clearInterval(k);
                k = null;
                $rotate.addClass('tn');
                $rotate.css({
                    'transform': `rotate(${deg}deg)`
                });
                // 监听过渡结束效果!--没加入兼容性
                $rotate.on('transitionend', function () {
                    flag = false;
                    $(this).removeClass('tn');
                    let timeK = null;
                    // 抽奖后 圆盘动旋转
                    setTimeout(() => {
                        k = timeK = setInterval( () => {
                            var temDeg = deg.toString();
                            if (k !== timeK) {
                                clearInterval(timeK);
                                return false;
                            }
                            if ($rotate.hasClass('tn')) {
                                return false;
                            }
                            // 一下代码 正则是为了解决 js 小数点 计算 问题。
                            temDeg = (/\./).test(temDeg) ? temDeg.replace(/\.\d+/, function ($1) {
                                var result = $1.length === 2 ? `${$1.substr(1)}0`: `${$1.substr(1)}`;
                                return result
                            }) : `${temDeg}00`;
                            temDeg = parseInt(temDeg);
                            temDeg += 5;
                            temDeg = temDeg.toString().split('');
                            temDeg.splice(temDeg.length - 2, 0, '.');
                            temDeg = temDeg.join('');
                            deg = parseFloat(temDeg);
                            $(this).css({
                                'transform': `rotate(${deg}deg)`
                            });
                        }, 13)
                    }, 1000);

                });
            })
        })()

demo地址

查看原文

认证与成就

  • 获得 122 次点赞
  • 获得 9 枚徽章 获得 1 枚金徽章, 获得 1 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-03-14
个人主页被 1.2k 人浏览