注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

ios客服云集成常见报错

注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。   1、很多同学在首次“导入...
继续阅读 »
注意:向自己工程中添加环信SDK和UI文件的时候,不要直接向xcode中拖拽添加,先把SDK和UI文件粘贴到自己工程的finder目录中,再从finder中向xcode中拖拽添加,避免出现找不到SDK或者UI文件的情况。

 

1、很多同学在首次“导入SDK”或“更新SDK重新导入SDK”后,Xcode运行报以下的error:

dyld: Library not loaded: @rpath/Hyphenate.framework/Hyphenate

  Referenced from:
/Users/shenchong/Library/Developer/CoreSimulator/Devices/C768FE68-6E79-40C8-8AD1-FFFC434D51A9/data/Containers/Bundle/Application/41EA9A48-4DD5-4AA4-AB3F-139CFE036532/CallBackTest.app/CallBackTest

  Reason: image not found

       这个原因是工程未加载到 framework,正确的处理方式是在TARGETS → General → Embedded
Binaries 中添加HelpDesk.framework和Hyphenate.framework依赖库,且 Linked
Frameworks and Libraries中依赖库的Status必须是Required。

1访客端_image_not_found.png



 

2、运行之后,自变量为nil,这就有可能是因为上面所说的依赖库的status设置为了Optional,需要改成Required。

2访客端自变量为nil.png



 

3、打包后上传到appstore报错

(1)ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. The bundle at
'Payload/toy.app/HelpDeskUIResource.bundle' does not contain a bundle
executable. If this bundle intentionally does not contain an executable,
consider removing the CFBundleExecutable key from its Info.plist and
using a CFBundlePackageType of BNDL. If this bundle is part of a
third-party framework, consider contacting the developer of the
framework for an update to address this issue."

方法:把HelpDeskUIResource.bundle里的Info.plist删掉就即可。

3访客端打包90535.png



(2)This bundle is invalid. The value for key CFBundleShortVersionString
‘1.2.2.1’in the Info.plist must be a period-separated list of at most
three non-negative integers. 

4访客端打包90060.png



把sdk里的plist文件的版本号改成3位数即可

5访客端打包1.2_.2_.1位置_.png



(3)Invalid Mach-O Format.The Mach-O in bundle
“SMYG.app/Frameworks/Hyphenate.framework” isn’t consistent with the
Mach-O in the main bundle.The main bundle Mach-O contains armv7(bitcode)
and arm64(bitcode),while the nested bundle Mach-O contains
armv7(machine code) and arm64(machine code).Verify that all of the
targets for a platform have a consistent value for the ENABLE_BITCODE
build setting.”

6访客端打包90636.png



将TARGETS-Build Settings-Enable Bitcode改为NO

7访客端打包bitcode改为NO.png



(4)还有很多同学打包失败,看不出什么原因

8访客端打包需剔除.png



那么可以先看看有没有按照文档剔除x86_64 i386两个平台

文档链接:http://docs.easemob.com/cs/300visitoraccess/iossdk#%E4%B8%8A%E4%BC%A0appstore%E4%BB%A5%E5%8F%8A%E6%89%93%E5%8C%85ipa%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9

 

4、那么剔除x86_64 i386时会遇到can't open input file的错误,这是因为cd的路径错误,把“/HelpDesk.framework”删掉。是cd到framework所在的路径,不是cd到framework

9访客端剔除cd错误.png



 

5、下图中的报错,需要创建一个pch文件,并且在pch文件添加如下判断,将环信的和自己的头文件都引入到#ifdef内部,参考文档:iOS访客端sdk集成准备工作

   #ifdef __OBJC__

   #endif

(swift项目也需这样操作)

10pch加判断1.png



11pch加判断2.png



pch加判断3.png




6、集成环信HelpDeskUI的时候,由于HelpDeskUI内部使用了第三方库,如果与开发者第三方库产生冲突,可将HelpDeskUI中冲突的第三方库删除,如果第三方库中的接口有升级的部分,请酌情进行升级。

12第三方库冲突.png



 

7、集成1.2.2版本demo中的HelpDeskUI,Masonry报错:Passing ‘CGFloat’(aka ‘double’) to parameter of incompatible type ‘__strong id’

需要在pch中添加#define MAS_SHORTHAND_GLOBALS

注意:要在#import "Masonry.h"之前添加此宏定义

13访客端Masonry报错.png



 

8、Xcode11运行demo,PSTCollectionView第三方库会有如下报错

iOS13中PSTCollectionView报错.png



标明下类型就行了,如图

iOS13中PSTCollectionView报错1.png



 

9、Xcode12.3编译报错(Building for iOS, but the linked and embedded framework......)

xcode12.3报错1_.jpg
解决方案:
e1d64313718a467a6bc19b70fadd4543.png
 或者打开xcode,左上方点击File --- Workspace Settings,按照截图修改试下(不建议)

xcode12.3报错2_.jpg
收起阅读 »

ios客服云集成常见问题

1、UI上很多地方显示英文,比如聊天页面的工具栏 把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。   2、进入聊天页面没有加载聊天记录 这种情况一般出现在只使用了 HDMessageView...
继续阅读 »




1、UI上很多地方显示英文,比如聊天页面的工具栏

显示英文1.png



把客服demo中配置的国际化文件添加到您自己的工程中。拖之前要打开国际化文件,全部选中这三个,再进行拖入。

显示英文2.png



 

2、进入聊天页面没有加载聊天记录

这种情况一般出现在只使用了 HDMessageViewController 没有使用 HDChatViewController 的时候

在HDMessageViewController 的 viewDidLoad 方法中, 将 [self
tableViewDidTriggerHeaderRefresh]; 的注释打开,再在这句代码之前加上
self.showRefreshHeader = YES; 

 

3、发送表情却显示字符串

访客端表情符号.png



把下面这段代码添加到appdelegate中就可以了

[[HDEmotionEscape sharedInstance] setEaseEmotionEscapePattern:@"\\[[^\\[\\]]{1,3}\\]"];

[[HDEmotionEscape sharedInstance] setEaseEmotionEscapeDictionary:[HDConvertToCommonEmoticonsHelper emotionsDictionary]];

 

4、文本消息,收发双方的布局不一样,如图

文本消息布局错误1.png



参考一下截图修改即可

文本消息布局错误2.png




5、客服能收到访客的消息,访客收不到客服的消息

(1)客服和im同时使用的话,初始化sdk、登录、登出用的是im的api会出现这种情况。必须使用客服的api。

(2)IM sdk升级为客服sdk,不兼容导致的,这种情况可以线上发起会话咨询。

      
6、发送的消息,出现在聊天页面的左侧

一般是由于当前访客没有登录或者登录失败,断点仔细检查下。

7、修改聊天页面导航栏标题
修改_title的值

ff6ff7a40cfea125e0d59e70efb131b8.png









收起阅读 »

接手一个不合格的业务线代码,我是如何去维护以及重构的

iOS
项目背景IM聊天功能作为整个产品业务功能的补充和重要支撑,相信很多的App都会集成这么一个业务功能在,很多App的的IM功能相信都是集成的第三方提供的的SDK服务。相信作为产品业务的有力支撑,IM的消息对于各个公司来说都有不同的业务需求,也就是说普通的图片、文...
继续阅读 »

项目背景

IM聊天功能作为整个产品业务功能的补充和重要支撑,相信很多的App都会集成这么一个业务功能在,很多App的的IM功能相信都是集成的第三方提供的的SDK服务。

相信作为产品业务的有力支撑,IM的消息对于各个公司来说都有不同的业务需求,也就是说普通的图片、文字、红包甚至语音这种常用的消息类型并不能有力支撑起一个IM的业务,今天说的这个的IM业务功能正式在这种背景下。

曾经接手了一个IM模块业务功能,刚开始是起因于解决线上的一个bug,于是开始梳理了一下代码逻辑,于是。。。懵逼了好久好久好久。 虽然IM的代码已经在线上跑了很久了,在我开始解决bug之前貌似有大概有大半年将近一年的的时间少有人来维护,从架构设计上、业务代码实现等点来看,可维护性不高。

梳理代码

这个IM的架构设计大概是在几年以前,长连接协议使用的是WebSocket,业务逻辑有一些复杂,目测从数据逻辑到业务逻辑再到UI逻辑,代码量可能会接近5w这个量级,所以说一开始就一行一行的看代码逻辑显然是不太理智的。

梳理第一步

第一步的主要目的是熟悉代码的主要脉络,于是我开始有序的梳理沿着数据流向梳理主干,要点如下:

  • 分析各个数据模型(model)。
  • 整理各个HTTP请求的API。
  • 整理并备注各个Notification的Key,并标记使用场景。
  • 整理各个Delegate回调函数的使用场景。
  • 整理并备注各个枚举值的含义以及使用场景。

因为此部分项目代码开发周期很久了且开发维护人员换了好几茬之后,代码量大且逻辑比较混乱,在一开始梳理的时候大部分时间都花在了备注各种代码上。

梳理第二步

第一步之后,我其实已经对于IM的架构逻辑开始有了一个初步的比较宽泛的了解。第二步的的主要目的是整理在使用的主要的几个组件:

  • 数据库的初始化创建以及使用逻辑。
  • HTTP代码的初始化创建以及使用逻辑。
  • 长连接代码的初始化创建以及使用逻辑,特别是与服务端沟通和保活的部分。
  • 针对以上基础控件的二次封装控件的整理。

梳理了上面几个之后,我陆续整理了如下:

  • 数据库的表结构设计、初始化创建以及销毁等逻辑。
  • 了解HTTP的接口功能,进一步了解了基础的业务设计。
  • 通过和服务端的同事沟通,明确了在当前的长连接协议下,两端是如何保活、沟通数据以及沟通各种状态的
  • 各个API的使用场景以及使用逻辑。

梳理第三步

上面两步,基本上把最核心的工具整理完成,下面就开始将代码逻辑串起来,整理业务逻辑:

  • 群聊的收发消息逻辑。
  • 私聊的收发消息逻辑。
  • 登录、退出登录、更换账号登录以及绑定账号之后的业务逻辑。
  • 自后台唤醒之后重连以及获取最新消息等的业务逻辑。
  • 收到推送消息之后的业务逻辑。

整理好了以上之后,我利用流程图工具ProcessOn创建了大概有10张左右的流程表,数据流向和业务逻辑一清二楚。

特别是聊天的数据流转逻辑,从HTTP请求、长连接推送以及数据库操作,无所不包。但是图整理完之后,对于主要流程几乎是掌握的比较清楚了,即使回头有忘记的,回头查看表之后就会一清二楚,不仅便于我熟悉逻辑,对以后的维护也很有益处。

维护

遇到的困境

虽然代码质量较差,但是代码中的bug还算比较少,所以在解决线上bug这个问题上,没有遇到过多的麻烦。但是其中也有几个bug十分麻烦,找了好久才找到问题的原因。

其中有一个,找原因大概找了一周多的时间,最后定位问题在我们的账号系统上。因为历史原因,我们的IM业务和其他业务是两套账号体系,中间经历过一次账号变更,但是由于某些奇葩的原因(有部分业务经历了hack),导致部分用户的账号没有成功的过渡账号变更,导致了一些问题。

表象原因是代码逻辑混乱,bug原因复杂,无法复现并难以定位问题。 但更深的原因是时间久远,我们对当时的代码设计以及业务逻辑变更没有记录,且逻辑上存在缺陷,导致无法快速定位到问题的原因。

思考

对于一个逻辑复杂的业务,首先要考虑的是易拓展、易维护和模块组件间的高度解耦。

对于一个成熟的业务来说,我认为易于维护性更为重要,因为业务已经进入成熟阶段的话就意味着大块功能增加的几率比较低,那么对于线上bug修复、小修小补的功能上的优化就是主要工作。

当前的这个IM业务就是这样的,大家看梳理的代码的第一步应该能看出来,我耗费了大量的时间去做各种备注,然而,这些重要节点的备注应该在开发阶段就应该写好了。包括对于之前两套账号系统存在的原因,包括变更一次账号的原因,是否需要有一个详细的记录?

那么对于这种这种有助于后期维护的记录,我认为我们需要对重要的业务变更以及业务设计要有一个详细的记录,无论是自己作为记录还是为后面接手的同学做个参考,我认为都是极为重要的。

重构

思索需求

经过最开始的了解和一段时间维护,我发现遇到的最大麻烦是数据逻辑、业务逻辑、和UI操作逻辑混到了一起,简直可以说是牵一发而动全身。特别是数据逻辑十分混乱,因为数据逻辑的混乱,导致我对于后面业务逻辑的变更十分费力,测试成本也是指数级上涨,另外UI逻辑杂乱,适配iPhone X的时候也遇到了一些小麻烦。

现在的架构设计的原因是什么?这样设计业务需求是否合理?是否有优化的空间?

分析现状与判断未来

无论是客户端还是服务端,亦或是两端数据交互上,IM业务的架构设计本身就存在很多问题。因为时间短暂,不太可能一次性解决所有的问题,特别是对于服务端来说,一次性的大规模重构可能性极低。

对于现在来说:

  • UI重构是首当其冲的, 在保证现有逻辑的基础上,重新设计UI层的逻辑结构,保证代码的复用性和可扩展性,为了将来有可能的业务升级留足空间。
  • 梳理基础组件,比如HTTP、长连接协议和数据库,还有其他的一些工具类,通过封装成组件和组件引用来是他们能从业务逻辑中独立出来。便于独立维护、升级甚至完整替换
  • 将原有的数据操作逻辑从UI逻辑中完整抽离出来,需要达到向下对基础组件要有封装和控制,向上对业务逻辑要有承接,而且依然要做到耦合度尽量的低。
  • 因为IM业务在多个App中都存在,功能逻辑上也有非常多的代码重合,所有需要考虑多个项目通用兼容的问题。

架构设计

  • 工厂模式
  • 瘦Model
  • 去model化的Cell
  • 项目优先,分离核心业务模块组成pod
  • 考虑业务变更可能性,尽可能向上保持API稳定性
  • KVO进行反向传值

着手动工

UI层重构

UI层的重构是最先开始的,无论架构怎么变,UI层都是直接面向用户的,直接承载了产品的业务功能实现,所以为了灵活适应业务的升级或变化,给用户一个好用流畅的入口,UI层设计上要尽可能的灵活。耦合尽可能的小,流畅性上要有保证。

重构思路相对简单,重写View和Controller,去除冗余复杂的UI逻辑代码,规范并统一第三方框架使用,封装公用组件,隔离胶水代码,设计灵活的UI结构。

因为IM系统的最主要UI仍然是TableView,所以针对TableView的各种优化就是重中之重,我着重说一下我对于复杂Cell类型的设计方案。

1、共有的组件有很多,比如时间、头像、背景气泡等等,所以说子类继承父类是最基础的方案。
2、弃置Autolayout的UI书写方式,完全用Frame来写UI布局。Cell高度以及内部UI组件的布局和位置,通过异步计算并缓存为LayoutModel,通过这种方式降低计算的重复耗时操作。
3、有的消息类型只是负责展示,但是有的确有相对复杂的业务逻辑,但是为了防止Cell代码的膨胀,采用了瘦Cell的方式分离逻辑,力求使Cell尽量只负责UI的承载和展示,增加helper层处理相关逻辑。
4、因为虽然是相同的一个数据,但是呈现的方式会存才差异化,所以采用瘦Model的形式,通过创建Helper对取到的原始数据进行相对应的加工,直接提供给业务逻辑处理好的数据。在AFNetworking给的Demo中,是一个典型的胖Model的例子,倒不是说他的例子不好,只是随着业务逻辑的复杂以及生数据和熟数据的差异越来越大的时候,胖Mode的代码量会几何级数般的膨胀,所以还是要因地制宜,具体情况具体分析。
5、利用Factory模式分离出关于复杂Cell类型的判断,包括初始化、赋值等。
6、使用KVO取代delegate进行反向传值,用以减少代码耦合。

关于如何保证UI性能以及优化ViewController,可参考我的其他几篇Blog,iOS 性能优化的探索复杂业务下UIViewController的减负工作

最终结果,第一个完整case的UI层Controller代码,从3000行直接缩减到了1200行,Controller中没有复杂的多方数据处理逻辑,复杂的逻辑判断。只作为UI展示以及接口调用,完全剥离了数据逻辑的处理,所有的处理逻辑由下面的数据逻辑层处理。

数据逻辑层重构

我在分析了业务需求并设计了架构之后,决定重构以自下而上的顺序来进行,于是第一部分就是对于数据逻辑层的重构。步骤如下: 此部分,分为以下三层结构:

1、业务数据逻辑层
2、适配器层(adapter)
3、基础组件服务层(server)

1、业务数据逻辑层

这部分的主要作用是直接受到UI层的调用,负责长连接以及短连接的建立,数据库的初始化操作等。 向上直接承接UI逻辑和业务逻辑,是高度面向业务封装的接口。比如在发送照片消息的时候,只需要将调用API传入Image对象,其他的流程比如说是上传资源以及组成message对象等,则不需要上一层调用和考虑。

所以这一层尽可能的会很薄,不会有特别多的逻辑代码。

2、适配器层(adapter)

这部分的主要工作是承上启下,承接上一层的面向业务的封装,调用下一层基础组件服务层的接口,可以说绝大多数的接口封装都集中在这一层。因为我们有些业务的长连接和短连接的使用上不是很合理,所以我将长短连接都封装到了一个网络服务的类中,此后假如长短连接的业务产生了变化,但仍可以保持向上的接口稳定性。

举个例子说,当推送来一条消息之后,是通过长连接,但是需要收到数据之后在进行AFN的操作完成消息体完整数据的获取,之后要存入数据库并且将是否读取状态设置为NO,当用户读取当前消息之后,将这部分消息还要更新为已读状态。

这部分操作涉及到了所有的组件的操作,但是反馈到最上面一层的时候,大概只是新的消息,并且是完整的消息,然后再刷新UI。所以说,这一层的业务量比较大,几乎是要按照各种标准操作,完整的处理好所有组件的接口。

3、基础组件服务层(server)

这部分基本可以说是基于IM本身的业务特点,对于基础组件的调用封装。 包括:

1、数据库部分,对于IM消息的数据结构,封装的对于数据库的创建,以及增删改查等接口。以及基于业务的一些接口,例如一次性设置当前聊天的所有消息为已读状态等接口。
2、AFN部分,这部分相对来说就很简单了,基本上是依赖于AFN封装的接口,比如获取当前User的详细信息等。
3、长连接部分,包括对于长连接协议的创建连接、断连以及心跳超时上报等操作,也包括了发送消息和收到消息回调等底层操作。
复制代码

拆分之后的Manager层代码量所见到原来的40%左右,于是改名为Session层。

基础组件层的重构以及封装

这部分因为属于公用的基础组件,所以相对来说只是基础组件的比如说AFN以及数据库(FMDB)是整个App的组成,所以没什么其他的操作,只是单纯做了一层逻辑上分层。

但是对于长连接我们做了一些定制,比如:

1、增加了重连的逻辑机制。
2、增加创建连接以及断掉连接时候各种状态的判断等。
复制代码

主要任务还是集中在对于协议库本身的逻辑补充和健壮性优化等。

其他操作

1、创建枚举文件,扩展标准化的枚举变量。
2、合并以及分割Model,随着业务的扩展,原来的Model设计已经不符合当下的业务发展,根据现在固定的业务,重新设计了Model的集成关系,对于分化严重的也做了重新分割。
3、分离并封装了胶水代码到一个大的工具类,便于调用和调试。

走过的弯路

1、过度思考代码解耦合而忽略了业务逻辑复杂性,错将组件化各组件的解耦合的逻辑应用在了本来就是高耦合的MVC架构上。尝试使用去model化的Cell,但是实际操作环节发现增加了大量的逻辑判断,无形中将Model本该处理的业务逻辑转接到Controller和View上,表面上看上去API简洁到家,但是上手代码量并不算小,不利于维护。

2、在一开始采用了MVCS的设计重构UI层,简化Controller中对于Model的处理,在Store中进行了主动和被动网络逻辑、本地数据库调用等。但事实上最后通过封装统一入口的方式将数据处理的逻辑全部从UI逻辑层剥离开,下沉到了数据逻辑层,对于UI层来说只需要考虑的是进行了调用获取数据的API操作或者是被动受到了新的数据,不需要考虑数据来自于服务端、Cache还是本地数据库,也不需要考虑后面的逻辑,当然,从另一个角度说仍然是MVCS,只不过Store相对复杂且庞大。

3、对于UI层和下一层的数据沟通,虽然采用了KVO的方式回调,降低了耦合性,但是仍然存在参数复杂的情况下,传递过多的Key的情况,导致解析稍显困难和复杂。

4、Cell的继承,看上去是一个很直观的设计,但是随着重构代码量的增加以及业务变化发现继承过程中会存在很多问题,通过面向协议等方式或许可以解决继承中庞大Api的问题。

总结以及思考

架构设计的时候,一定要预判用户的使用习惯,判断未来的业务导向,尽可能的降低代码侵入性和耦合性。对于性能产生的影响的地方,通过以上几点来设计架构,模块健壮性以及可扩展性是设计之初就要优先考虑到的。

架构设计分层要清晰,API设计要尽可能简洁,避免暴露过多的接口和参数,避免模块之间的紧耦合,UI设计要尽可能灵活。 重构前,需要思考切入点,是从上值下、从下至上,还是模块化抽离。

已不再维护这部分业务,部分逻辑全凭记忆整理,如果有疏漏或错误,还请大家海涵。

Refrence


作者:derek
链接:https://juejin.cn/post/6844904054577954824

收起阅读 »

iOS Operation 自定义的注意点

iOS
问题 碰到一个问题,就是做一个点击后添加动画效果,连续点击则有多个动画效果按顺序执行,通过自定Operation,以队列实现,但是发现每次点击玩上次动画效果还没完全执行完点击之后的动画就出来,不符合需求。 后来查资料得知自定义Operation中有两个属性分...
继续阅读 »
问题


  • 碰到一个问题,就是做一个点击后添加动画效果,连续点击则有多个动画效果按顺序执行,通过自定Operation,以队列实现,但是发现每次点击玩上次动画效果还没完全执行完点击之后的动画就出来,不符合需求。

  • 后来查资料得知自定义Operation中有两个属性分别表示任务是否在执行以及是否执行完毕,如下


@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
复制代码


因此在自定义Operation时设置这两个属性,同时在完全当前队列中任务时给予标识表明任务完成,具体代码如下




  • CustomOperation 类


@interface CustomOperation : NSOperation

@end

#import "CustomOperation.h"

@interface CustomOperation ()

@property(nonatomic,readwrite,getter=isExecuting)BOOL executing; // 表示任务是否正在执行
@property(nonatomic,readwrite,getter=isFinished)BOOL finished; // 表示任务是否结束

@end

@implementation CustomOperation

@synthesize executing = _executing;
@synthesize finished = _finished;

- (void)start
{
    @autoreleasepool {
        self.executing = YES;
        if (self.cancelled) {
            [self done];
            return;
        }
        // 执行任务
        __weak typeof(self) weakSelf = self;
        dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
        dispatch_after(delayTime, dispatch_get_main_queue(), ^{
            NSLog(@"动画完毕");
            // 任务执行完毕手动关闭
            [weakSelf done];
        });
    }
}

-(void)done
{
    self.finished = YES;
    self.executing = NO;
}

#pragma mark - setter -- getter
// 监听并设置executing
- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isExecuting
{
    return _executing;
}

// 监听并设置finished
- (void)setFinished:(BOOL)finished
{
    if (_finished != finished) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = finished;
        [self didChangeValueForKey:@"isFinished"];
    }
}

- (BOOL)isFinished
{
    return _finished;
}

// 返回YES 标识并发Operation
- (BOOL)isAsynchronous
{
    return YES;
}

@end


  • swift版


class AnimationOperation: Operation {

    var animationView:AnimationView? // 动画view
    var superView:UIView? // 父视图
    var finishCallBack:(()->())? // 完成动画的回调

    override var isExecuting: Bool {
        return operationExecuting
    }

    override var isFinished: Bool {
        return operationFinished
    }

    override var isAsynchronous: Bool {
        return true
    }
// 监听
    private var operationFinished:Bool = false {
        willSet {
            willChangeValue(forKey: "isFinished")
        }
        didSet {
            didChangeValue(forKey: "isFinished")
        }
    }

    private var operationExecuting:Bool = false {
        willSet {
            willChangeValue(forKey: "isExecuting")
        }
        didSet {
            didChangeValue(forKey: "isExecuting")
        }
    }

// 每次点击添加动画队列
    class func addOperationShowAnimationView(animationView:AnimationView,superView:UIView) -> AnimationOperation {

        let operation = AnimationOperation()
        operation.animationView = animationView
        operation.superView = superView
        return operation
    }

    override func start() {
        self.operationExecuting = true
        if isCancelled == true {
            self.done()
            return
        }

        guard let superView = self.superView,
              let subView = self.animationView,
              let callback = self.finishCallBack else {
            print("superView == nil")
            return
        }

        OperationQueue.main.addOperation {[weak self] in
            superView.addSubview(subView)
            subView.finishCallBack = {
                self?.done()
                callback()
            }
            subView.showAnimation()
        }
    }

    

    private func done() {
        self.operationFinished = true
        self.operationExecuting = false
    }

}

作者:取个有意思的昵称
链接:https://juejin.cn/post/7034518171314815007

收起阅读 »

如何系统性治理 iOS 稳定性问题

iOS
字节跳动如何系统性治理 iOS 稳定性问题本文是丰亚东讲师在2021 ArchSummit 全球架构师峰会中「如何系统性治理 iOS 稳定性问题」的分享全文首先做一下自我介绍:我是丰亚东,2016 年 4 月加入字节跳动,先后负责今日头条 App 的工程架构、...
继续阅读 »

字节跳动如何系统性治理 iOS 稳定性问题

本文是丰亚东讲师在2021 ArchSummit 全球架构师峰会中「如何系统性治理 iOS 稳定性问题」的分享全文

首先做一下自我介绍:我是丰亚东,2016 年 4 月加入字节跳动,先后负责今日头条 App 的工程架构、基础库和体验优化等基础技术方向。2017 年 12 月至今专注在 APM 方向,从 0 到 1 参与了字节跳动 APM 中台的建设,服务于字节的全系产品,目前主要负责 iOS 端的性能稳定性监控和优化。 请添加图片描述 本次分享主要分为四大章节,分别是:1.稳定性问题分类;2.稳定性问题治理方法论;3.疑难问题归因;4.总结回顾。其中第三章节「疑难问题归因」是本次分享的重点,大概会占到60%的篇幅。

一、稳定性问题分类

在讲分类之前,我们先了解一下背景:大家都知道对于移动端应用而言,闪退是用户能遇到的最严重的 bug,因为在闪退之后用户无法继续使用产品,那么后续的用户留存以及产品本身的商业价值都无从谈起。 这里有一些数据想和大家分享:有 20% 的用户在使用移动端产品的时候,最无法忍受的问题就是闪退,这个比例仅次于不合时宜的广告;在因为体验问题流失的用户中,有 1/3 的用户会转而使用竞品,由此可见闪退问题是非常糟糕和严重的。 请添加图片描述 字节跳动作为拥有像抖音、头条等超大量级 App 的公司,对稳定性问题是非常重视的。过去几年,我们在这方面投入了非常多的人力和资源,同时也取得了不错的治理成果。过去两年抖音、头条、飞书等 App 的异常崩溃率都有 30% 以上的优化,个别产品的部分指标甚至有 80% 以上的优化。 通过上图中右侧的饼状图可以看出:我们以 iOS 平台为例,根据稳定性问题不同的原因,将已知稳定性问题分成了这五大类,通过占比从高到低排序:第一大类是 OOM ,就是内存占用过大导致的崩溃,这个比例能占到 50% 以上;其次是 Watchdog,也就是卡死,类比于安卓中的 ANR;再次是普通的 Crash;最后是磁盘 IO 异常和 CPU 异常。 看到这里大家心里可能会有一个疑问:字节跳动究竟做了什么,才取得了这样的成果?接下来我会将我们在稳定性治理方面沉淀的方法论分享给大家。

二、稳定性问题治理的方法论

在这里插入图片描述 首先我们认为在稳定性问题治理方面,从监控平台侧视角出发,最重要的就是要有完整的能力覆盖,比如针对上一章节中提到所有类型的稳定性问题,监控平台都应该能及时准确的发现。 另外是从业务研发同学的视角出发:稳定性问题治理这个课题,需要贯穿到软件研发的完整生命周期,包括需求研发、测试、集成、灰度、上线等,在上述每个阶段,研发同学都应该重视稳定性问题的发现和治理。

上图中右侧是我们总结的两条比较重要的治理原则: 第一条是控制新增,治理存量。一般来说新增的稳定性问题可能是一些容易爆发的问题,影响比较严重。存量问题相对来说疑难的问题居多,修复周期较长。 第二条比较容易理解:先急后缓,先易后难。我们应该优先修复那些爆发的问题以及相对容易解决的问题。 在这里插入图片描述 如果我们将软件研发周期聚焦在稳定性问题治理这个方向上,又可以抽象出以下几个环节: 首先第一个环节是问题发现:当用户在线上遇到任何类型的闪退,监控平台都应该能及时发现并上报。同时可以通过报警以及问题的自动分发,将这些问题第一时间通知给开发者,确保这些问题能够被及时的修复。 第二个阶段是归因:当开发者拿到一个稳定性问题之后,要做的第一件事情应该是排查这个问题的原因。根据一些不同的场景,我们又可以把归因分为单点归因、共性归因以及爆发问题归因。 当排查到问题的原因之后,下一步就是把这个问题修复掉,也就是问题的治理。在这里我们有一些问题治理的手段:如果是在线上阶段,我们首先可以做一些问题防护,比如网易几年前一篇文章提到的基于 OC Runtime 的线上 Crash 自动修复的方案大白,基于这种方案我们可以直接在线上做 Crash 防护;另外由于后端服务上线导致的稳定性问题爆发,我们可以通过服务的回滚来做到动态止损。除了这两种手段之外,更多的场景还是需要研发在线下修复 native 代码,再通过发版做彻底的修复。 最后一个阶段也是最近几年比较火的一个话题,就是问题的防劣化。指的是需求从研发到上线之间的阶段,可以通过机架的自动化单元测试/UI自动化测试,以及研发可以通过一些系统工具,比如说 Xcode 和 Instruments,包括一些第三方工具,比如微信开源的 MLeaksFinder 去提前发现和解决各类稳定性问题。

如果我们想把稳定性问题治理做好的话,需要所有研发同学关注上述每一个环节,才能达到最终的目标。 可是这么多环节我们的重点究竟在哪里呢?从字节跳动的问题治理经验来看,我们认为最重要的环节是第二个——线上的问题的归因。因为通过内部的统计数据发现:线上之所以存在长期没有结论,没有办法修复的问题,主要还是因为研发并没有定位到这些问题的根本原因。所以下一章节也是本次分享的重点:疑难问题归因。

三、疑难问题归因

我们根据开发者对这些问题的熟悉程度做了一下排序,分别是:Crash、Watchdog、OOM 和 CPU&Disk I/O。每一类疑难问题我都会分享这类问题的背景和对应的解决方案,并且会结合实战案例演示各种归因工具究竟是如何解决这些疑难问题的。

3.1 第一类疑难问题 —— Crash

在这里插入图片描述 上图中左侧这张饼状图是我们根据 Crash 不同的原因,把它细分成四大类:包括 Mach 异常、 Unix Signal 异常、OC 和 C++ 语言层面上的异常。其中比例最高的还是 Mach 异常,其次是 Signal 异常,OC 和 C++ 的异常相对比较少。 为什么是这个比例呢? 大家可以看到右上角有两个数据。第一个数据是微软发布的一篇文章,称其发布的 70% 以上的安全补丁都是内存相关的错误,对应到 iOS 平台上就是 Mach 异常中的非法地址访问,也就是 EXC_BAD_ACCESS。内部统计数据表明,字节跳动线上 Crash 有 80% 是长期没有结论的,在这部分 Crash 当中,90% 以上都是 Mach 异常或者 Signal 异常。 看到这里,大家肯定心里又有疑问了,为什么有这么多 Crash 解决不了?究竟难在哪里?我们总结了几点这些问题归因的难点:

  • 首先不同于 OC 和 C++ 的异常,可能开发者拿到的崩溃调用栈是一个纯系统调用栈,这类问题显然修复难度是非常大的;
  • 另外可能有一部分Crash是偶发而不是必现的问题,研发同学想在线下复现问题是非常困难的,因为无法复现,也就很难通过 IDE 调试去排查和定位这些问题;
  • 另外对于非法地址访问这类问题,崩溃的调用栈可能并不是第一现场。这里举一个很简单的例子:A业务的内存分配溢出,踩到了B业务的内存,这个时候我们认为 A 业务应该是导致这个问题的主要原因,但是有可能B业务在之后的某一个时机用到了这块内存,发生了崩溃。显然这种问题实际上是 A 业务导致的,最终却崩在了 B 业务的调用栈里,这就会给开发者排查和解决这个问题带来非常大的干扰。

看到这里大家可能心里又有问题:既然这类问题如此难解,是不是就完全没有办法了呢?其实也并不是,下面我会分享字节内部两个解决这类疑难问题非常好用的归因工具。 在这里插入图片描述

3.1.1 Zombie 检测

首先第一个是 Zombie 检测,大家如果用过 Xcode 的 Zombie 监控,应该对这个功能比较熟悉。如果我们在调试之前打开了 Zombie Objects 这个开关,在运行的时候如果遇到了 OC 对象野指针造成的崩溃,Xcode 控制台中会打印出一行日志,它会告诉开发者哪个对象在调用什么消息的时候崩溃了。

这里我们再解释一下 Zombie 的定义,其实非常简单,指的是已经释放的 OC 对象。 Zombie 监控的归因优势是什么呢?首先它可以直接定位到问题发生的类,而不是一些随机的崩溃调用栈;另外它可以提高偶现问题的复现概率,因为大部分偶现问题可能跟多线程的运行环境有关,如果我们能把一个偶现问题变成必现问题的话,那么开发者就可以借助 IDE 和调试器非常方便地排查问题。但是这个方案也有自己的适用范围,因为它的底层原理基于 OC 的 runtime 机制,所以它仅仅适用于 OC 对象野指针导致的内存问题。 在这里插入图片描述 这里再和大家一起回顾一下 Zombie 监控的原理:首先我们会 hook 基类 NSObject 的 dealloc 方法,当任意 OC 对象被释放的时候,hook 之后的那个 dealloc 方法并不会真正的释放这块内存,同时将这个对象的 ISA 指针指向一个特殊的僵尸类,因为这个特殊的僵尸类没有实现任何方法,所以这个僵尸对象在之后接收到任何消息都会 Crash,与此同时我们会将崩溃现场这个僵尸对象的类名以及当时调用的方法名上报到后台分析。 在这里插入图片描述 这里是字节的一个真实案例:这个问题是飞书在某个版本线上 Top 1 的 Crash,当时持续了两个月没有被解决。首先大家可以看到这个崩溃调用栈是一个纯系统调用栈,它的崩溃类型是非法地址访问,发生在视图导航控制器的一次转场动画,可能开发者一开始看到这个崩溃调用栈是毫无思路的。 在这里插入图片描述 那么我们再看 Zombie 功能开启之后的崩溃调用栈:这个时候报错信息会更加丰富,可以直接定位到野指针对象的类型,是 MainTabbarController 对象在调用 retain 方法的时候发生了 Crash。

看到这里大家肯定有疑问了,MainTabbarController 一般而言都是首页的根视图控制器,理论上在整个生命周期内不应该被释放。为什么它变成了一个野指针对象呢?可见这样一个简单的报错信息,有时候还并不足以让开发者定位到问题的根本原因。所以这里我们更进一步,扩展了一个功能:将 Zombie 对象释放时的调用栈信息同时上报上来。 在这里插入图片描述 大家看倒数第二行,实际上是一段飞书的业务代码,是视图导航控制器手势识别的代理方法,这个方法在调用的时候释放了 MainTabbarController。因为通过这个调用栈找到了业务代码的调用点,所以我们只需要对照源码去分析为什么会释放 TabbarController,就可以定位到这个问题的原因。 在这里插入图片描述 上图中右侧是简化之后的源码(因为涉及到代码隐私问题,所以通过一段注释代替)。历史上为了解决手势滑动返回的冲突问题,在飞书视图导航控制器的手势识别代理方法中写了一段 trick 代码,正是这个 trick 方案导致了首页视图导航控制器被意外释放。 排查到这里,我们就找到了问题的根本原因,修复的方案也就非常简单了:只要下掉这个 trick 方案,并且依赖导航控制器的原生实现来决定这个手势是否触发就解决了这个问题。

3.1.2 Coredump

刚才也提到:Zombie 监控方案是有一些局限的,它仅适用于 OC 对象的野指针问题。大家可能又会有疑问: C 和 C++ 代码同样可能会出现野指针问题,在 Mach 异常和 Signal 异常中,除了内存问题之外,还有很多其他类型的异常比如 EXC_BAD_INSTRUCTION和SIGABRT。那么其他的疑难问题我们又该怎么解决呢?这里我们给出了另外一个解决方案 —— Coredump。 在这里插入图片描述 这个先解释一下什么是 Coredump:Coredump 是由 lldb 定义的一种特殊的文件格式,Coredump 文件可以还原 App 在运行到某一时刻的完整运行状态(这里的运行状态主要指的是内存状态)。大家可以简单的理解为:Coredump文件相当于在崩溃的现场打了一个断点,并且获取到当时所有线程的寄存器信息,栈内存以及完整的堆内存。

Coredump 方案它的归因优势是什么呢?首先因为它是 lldb 定义的文件格式,所以它天然支持 lldb 的指令调试,也就是说开发者无需复现问题,就可以实现线上疑难问题的事后调试。另外因为它有崩溃时现场的所有内存信息,这就为开发者提供了海量的问题分析素材。

这个方案的适用范围比较广,可以适用于任意 Mach 异常或者 Signal 异常问题的分析。 在这里插入图片描述 下面也带来一个线上真实案例的分析:当时这个问题出现在字节的所有产品中,而且在很多产品中的量级非常大,排名Top 1 或者 Top 2,这个问题在之前两年的时间内都没有被解决。

大家可以看到这个崩溃调用栈也全是系统库方法,最终崩溃在 libdispatch 库中的一个方法,异常类型是命中系统库断言。 在这里插入图片描述 我们将这次崩溃的 Coredump 文件上报之后,用前面提到的 lldb 调试指令去分析,因为拥有崩溃时的完整内存状态,所以我们可以分析所有线程的寄存器和栈内存等信息。

这里最终我们分析出:崩溃线程的 0 号栈帧(第一行调用栈),它的 x0 寄程器实际上就是 libdispatch 中定义的队列结构体信息。在它起始地址偏移 0x48 字节的地方,也就是这个队列的 label 属性(可以简单理解为队列的名字)。这个队列的名字对我们来说是至关重要的,因为要修复这个问题,首先应该知道究竟是哪个队列出现了问题。通过 memory read 指令我们直接读取这块内存的信息,最终发现它是一个 C 的字符串,名字叫 com.apple.CFFileDescriptor,这个信息非常关键。我们在源码中全局搜索这个关键字,最终发现这个队列是在字节底层的网络库中创建的,这也就能解释为什么字节所有产品都有这个崩溃了。 在这里插入图片描述 最终我们和网络库的同学一起排查,同时结合 libdispatch 的源码,定位到这个问题的原因是 GCD 队列的外部引用计数小于0,存在过度释放的问题,最终命中系统库断言导致崩溃。 在这里插入图片描述 排查到问题之后,解决方案就比较简单了:我们只需要在这个队列创建的时候,使用 dispatch_source_create 的方式去增加队列的外部引用计数,就能解决这个问题。和维护网络库的同学沟通后,确认这个队列在整个 App 的生命周期内不应该被释放。这个问题最终解决的收益是直接让字节所有产品的 Crash 率降低了8%。

3.2 第二类疑难问题 —— Watchdog

我们进入疑难问题中的第二类问题 —— Watchdog 也就是卡死。 在这里插入图片描述 上图中左侧是我在微博上截的两张图,是用户在遇到卡死问题之后的抱怨。可见卡死问题对用户体验的伤害还是比较大的。那么卡死问题它的危害有哪些呢?

首先卡死问题通常发生于用户打开 App 的冷启动阶段,用户可能等待了10 秒什么都没有做,这个 App 就崩溃了,这对用户体验的伤害是非常大的。另外我们线上监控发现,如果没有对卡死问题做任何治理的话,它的量级可能是普通 Crash 的 2-3 倍。另外现在业界普遍监控 OOM 崩溃的做法是排除法,如果没有排除卡死崩溃的话,相应的就会增加 OOM 崩溃误判的概率。

卡死类问题的归因难点有哪些呢?首先基于传统的方案——卡顿监控:认为主线程无响应时间超过3秒~5秒之后就是一次卡死,这种传统的方案非常容易误报,至于为什么误报,我们下一页中会讲到。另外卡死的成因可能非常复杂,它不一定是单一的问题:主线程的死锁、锁等待、主线程 IO 等原因都有可能造成卡死。第三点是死锁问题是一类常见的导致卡死问题的原因。传统方案对于死锁问题的分析门槛是比较高的,因为它强依赖开发者的经验,开发者必须依靠人工的经验去分析主线程到底跟哪个或者哪些线程互相等待造成死锁,以及为什么发生死锁。 在这里插入图片描述 大家可以看到这是基于传统的卡顿方案来监控卡死,容易发生误报。为什么呢?图中绿色和红色的部分是主线程的不同耗时阶段。假如主线程现在卡顿的时间已经超过了卡死阈值,刚好发生在图中的第5个耗时阶段,我们在此时去抓取主线程调用栈,显然它并不是这次耗时的最主要的原因,问题其实主要发生在第4个耗时阶段,但是此时第4个耗时阶段已经过去了,所以会发生一次误报,这可能让开发者错过真正的问题。

针对以上提到的痛点,我们给出了两个解决方案:首先在卡死监控的时候可以多次抓取主线程调用栈,并且记录每次不同时刻主线程的线程状态,关于线程状态包括哪些信息,下一页中会提到。 另外我们可以自动识别出死锁导致的卡死问题,将这类问题标识出来,并且可以帮助开发者自动还原出各个线程之间的锁等待关系。 在这里插入图片描述 首先是第一个归因工具——线程状态,这张图是主线程在不同时刻调用栈的信息,在每个线程名字后面都有三个 tag ,分别指的是三种线程的状态,包括当时的线程 CPU 占用、线程运行状态和线程标志。

上图中右侧是线程的运行状态和线程标志的解释。当看到线程状态的时候,我们主要的分析思路有两种:第一种,如果看到主线程的 CPU 占用为 0,当前处于等待的状态,已经被换出,那我们就有理由怀疑当前这次卡死可能是因为死锁导致的;另外一种,特征有所区别,主线程的 CPU 占用一直很高 ,处于运行的状态,那么就应该怀疑主线程是否存在一些死循环等 CPU 密集型的任务。 在这里插入图片描述 第二个归因工具是死锁线程分析,这个功能比较新颖,所以首先带领大家了解一下它的原理。基于上一页提到的线程状态,我们可以在卡死时获取到所有线程的状态并且筛选出所有处于等待状态的线程,再获取每个线程当前的 PC 地址,也就是正在执行的方法,并通过符号化判断它是否是一个锁等待的方法。

上图中列举了目前我们覆盖到的一些锁等待方法,包括互斥锁、读写锁、自旋锁、 GCD 锁等等。每个锁等待的方法都会定义一个参数,传入当前锁等待的信息。我们可以从寄存器中读取到这些锁等待信息,强转为对应的结构体,每一个结构体中都会定义一个线程id的属性,表示当前这个线程正在等待哪个线程释放锁。对每一个处于等待状态的线程完成这样一系列操作之后,我们就能够完整获得所有线程的锁等待关系,并构建出锁等待关系图。 在这里插入图片描述 通过上述方案,我们可以自动识别出死锁线程。假如我们能判断 0 号线程在等待 3 号线程释放锁, 同时3 号线程在等待0号线程释放锁,那么显然就是两个互相等待最终造成死锁的线程。

大家可以看到这里主线程我们标记为死锁,它的 CPU 占用为 0,状态是等待状态,而且已经被换出了,和我们之前分析线程状态的方法论是吻合的。 在这里插入图片描述 通过这样的分析之后,我们就能够构建出一个完整的锁等待关系图,而且无论是两个线程还是更多线程互相等待造成的死锁问题,都可以自动识别和分析。 在这里插入图片描述 这是上图中死锁问题的一段示意的源码。它的问题就是主线程持有互斥锁,子线程持有 GCD 锁,两个线程之间互相等待造成了死锁。这里给出的解决方案是:如果子线程中可能存在耗时操作,尽量不要和主线程有锁竞争关系;另外如果在串行队列中同步执行 block 的话,一定要慎重。 在这里插入图片描述 上图是通过字节内部线上的监控和归因工具,总结出最常见触发卡死问题的原因,分别是死锁、锁竞争、主线程IO、跨进程通信。

3.3 第三类疑难问题 —— OOM

OOM 就是 Out Of Memory,指的是应用占用的内存过高,最终被系统强杀导致的崩溃。 在这里插入图片描述 OOM 崩溃的危害有哪些呢?首先我们认为用户使用 App 的时间越长,就越容易发生 OOM 崩溃,所以说 OOM 崩溃对重度用户的体验伤害是比较大的;统计数据显示,如果 OOM 问题没有经过系统性的治理,它的量级一般是普通 Crash 的 3-5 倍。最后是内存问题不同于 Crash 和卡死,相对隐蔽,在快速迭代的过程中非常容易劣化。

那么 OOM 问题的归因难点有哪些呢?首先是内存的构成是非常复杂的事情,并没有非常明确的异常调用栈信息。另外我们在线下有一些排查内存问题的工具,比如 Xcode MemoryGraph 和 Instruments Allocations,但是这些线下工具并不适用于线上场景。同样是因为这个原因,如果开发者想在线下模拟和复现线上 OOM 问题是非常困难的。 在这里插入图片描述 这里我们给出解决线上 OOM 疑难问题的归因工具是MemoryGraph。这里的 MemoryGraph 主要指的是在线上环境中可以使用的 MemoryGraph。跟 Xcode MemoryGraph 有一些类似,但是也有不小的区别。最大的区别当然是它能在线上环境中使用,其次它可以对分散的内存节点进行统计和聚合,方便开发者定位头部的内存占用。

这里带领大家再回顾一下线上 MemoryGraph 的基本原理:首先我们会定时的去检测 App 的物理内存占用,当它超过危险阈值的时候,就会触发内存 dump,此时 SDK 会记录每个内存节点符号化之后的信息,以及他们彼此之间的引用关系,如果能判定出是强引用还是弱引用,也会把这个强弱引用关系同时上报上来,最终这些信息整体上报到后台之后,就可以辅助开发者去分析当时的大内存占用和内存泄露等异常问题。

这里我们还是用一个实战案例带领大家看一下 MemoryGraph 到底是如何解决 OOM 问题的。 在这里插入图片描述 分析 MemoryGraph 文件的思路一般是抽丝剥茧,逐步找到根本原因。

上图是 MemoryGraph 文件分析的一个例子,这里的红框标注了不同的区域:左上角是类列表,会把同一类型对象的数量以及它们占用的内存大小做一个汇总;右侧是这个类所有实例的地址列表,右下角区域开发者可以手动回溯对象的引用关系(当前对象被哪些其他对象引用、它引用了哪些其他对象),中间比较宽的区域是引用关系图。

因为不方便播放视频,所以这边就跟大家分享一些比较关键的结论:首先看到类列表,我们不难发现 ImageIO 类型的对象有 47 个,但是这 47 个对象居然占了 500 多 MB 内存,显然这并不是一个合理的内存占用。我们点开 ImageIO 的类列表,以第一个对象为例,回溯它的引用关系。当时我们发现这个对象只有一个引用,就是 VM Stack: Rust Client Callback ,它实际上是飞书底层的 Rust 网络库线程。 排查到这里,大家肯定会好奇:这 47 个对象是不是都存在相同的引用关系呢?这里我们就可以用到右下角路径回溯当中的 add tag 功能,自动筛选这 47 个对象是否都存在相同的引用关系。大家可以看到上图中右上角区域,通过筛选之后,我们确认这 47 个对象 100% 都有相同的引用关系。

我们再去分析 VM Stack: Rust Client Callback这个对象。发现它引用的对象中有两个名字非常敏感,一个是 ImageRequest,另外一个是 ImageDecoder ,从这两个名字我们可以很容易地推断出:应该是图片请求和图片解码的对象。 在这里插入图片描述 我们再用这两个关键字到类列表中搜索,可以发现 ImageRequest 对象有 48 个,ImageDecoder 对象有 47 个。如果大家还有印象的话,上一页中占用内存最大的对象 ImageIO 也是 47 个。这显然并不是一个巧合,我们再去排查这两类对象的引用关系,发现这两类对象也同样是 100% 被 VM Stack: Rust Client Callback 对象所引用。

最终我们和飞书图片库的同学一起定位到这个问题的原因:在同一时刻并发请求 47 张图片并解码,这不是一个合理的设计。问题的根本原因是飞书图片库的下载器依赖了 NSOperationQueue 做任务管理和调度,但是却没有配置最大并发数,在极端场景下就有可能造成内存占用过高的问题。与之相对应的解决方案就是对图片下载器设置最大并发数,并且根据待加载图片是否在可视区域内调整优先级。 在这里插入图片描述 上图是通过字节内部的线上监控和归因工具,总结出来最常见的几类触发 OOM 问题的原因,分别是:内存泄露,这个较为常见;第二个是内存堆积,主要指的是 AutoreleasePool 没有及时清理;第三是资源异常,比如加载一张超大图或者一个超大的 PDF 文件;最后一个是内存使用不当,比如内存缓存没有设计淘汰清理的机制。

3.4 第四类疑难问题 —— CPU 异常和磁盘 I/O 异常

这里之所以把这两类问题合并在一起,是因为这两类问题是高度相似的:首先它们都属于资源的异常占用;另外它们也都不同于闪退,导致崩溃的原因并不是发生在一瞬间,而都是持续一段时间的资源异常占用。 在这里插入图片描述 异常 CPU 占用和磁盘 I/O 占用危害有哪些呢?首先我们认为,这两类问题即使最终没有导致 App 崩溃,也特别容易引发卡顿或者设备发烫等性能问题。其次这两类问题的量级也是不可以被忽视的。另外相比之前几类稳定性问题而言,开发者对这类问题比较陌生,重视程度不够,非常容易劣化。

这类问题的归因难点有哪些呢?首先是刚刚提到它的持续时间非常长,所以原因也可能并不是单一的;同样因为用户的使用环境和操作路径都比较复杂,开发者也很难在线下复现这类问题;另外如果 App 想在用户态去监控和归因这类问题的话,可能需要在一段时间内高频的采样调用栈信息,然而这种监控手段显然性能损耗是非常高的。 在这里插入图片描述 上图中左侧是我们从 iOS 设备中导出的一段 CPU 异常占用的崩溃日志,截取了关键部分。这部分信息的意思是:当前 App 在 3 分钟之内的 CPU 时间占用已经超过80%,也就是超过了 144 秒,最终触发了这次崩溃。

上图中右侧是我截取苹果 WWDC2020 一个 session 中的截图,苹果官方对于这类问题,给出了一些归因方案的建议:首先是 Xcode Organizer,它是苹果官方提供的问题监控后台。然后是建议开发者也可以接入 MetricKit ,新版本有关于 CPU 异常的诊断信息。 请添加图片描述 上图中左侧是磁盘异常写入的崩溃日志,也是从 iOS 设备中导出,依然只截取了关键部分:在 24 小时之内,App 的磁盘写入量已经超过了 1073 MB,最终触发了这次崩溃。

上图中右侧是苹果官方的文档,也给出了对于这类问题的归因建议。同样是两个建议:一个是依赖 Xcode Organizer,另一个是依赖 MetricKit。我们选型的时候最终确定采用 MetricKit 方案,主要考虑还是想把数据源掌握在自己手中。因为 Xcode Organizer 毕竟是一个苹果的黑盒后台,我们无法与集团内部的后台打通,更不方便建设报警、问题自动分配、issue状态管理等后续流程。 请添加图片描述 MetricKit 是苹果提供的官方性能分析以及稳定性问题诊断的框架,因为是系统库,所以它的性能损耗很小。在 iOS 14 系统以上,基于Metrickit,我们可以很方便地获取 CPU 和磁盘 I/O 异常的诊断信息。它的集成也非常方便。我们只需要导入系统库的头文件,设置一个监听者,在对应的回调中把 CPU 和磁盘写入异常的诊断信息上报到后台分析就好了。 请添加图片描述 其实这两类异常的诊断信息格式也是高度类似的,都是记录一段时间内所有方法的调用以及每个方法的耗时。上报到后台之后,我们可以把这些数据可视化为非常直观的火焰图。通过这样直观的形式,可以辅助开发者轻松地定位到问题。对于上图中右侧的火焰图,我们可以简单的理解为:矩形块越长,占用的 CPU 时间就越长。那么我们只需要找到矩形块最长的 App 调用栈,就能定位到问题。图中高亮的红框,其中有一个方法的关键字是 animateForNext,看这个名字大概能猜到这是动画在做调度。

最终我们和飞书的同学一起定位到这个问题的原因:飞书的小程序业务有一个动画在隐藏的时候并没有暂停播放,造成了 CPU 占用持续比较高。解决方案也非常简单,只要在动画隐藏的时候把它暂停掉就可以了。

四、总结回顾

请添加图片描述 在第二章节稳定性问题治理方法论中,我提到“如果想把稳定性问题治理好,就需要将这件事情贯穿到软件研发周期中的每一个环节,包括问题的发现、归因、治理以及防劣化”,同时我们认为线上问题特别是线上疑难问题的归因,是整个链路中的重中之重。针对每一类疑难问题,本次分享均给出了一些好用的归因工具:Crash 有 Zombie 监控和 Coredump;Watchdog 有线程状态和死锁线程分析;OOM 有 MemoryGraph;CPU 和磁盘 I/O 异常有 MetricKit。 请添加图片描述 本次分享提到的所有疑难问题的归因方案,除了MetricKit 之外,其余均为字节跳动自行研发,开源社区尚未有完整解决方案。这些工具和平台后续都将通过字节火山引擎应用开发套件 MARS 旗下的 APM Plus 平台提供一站式的企业解决方案。本次分享提到的所有能力均已在字节内部各大产品中验证和打磨多年,其自身的稳定性以及接入后所带来的业务效果都是有目共睹的,欢迎大家持续保持关注。

链接:https://juejin.cn/post/7034418275728097288

收起阅读 »

什么是元宇宙?Facebook 的战略以及微软、迪士尼和亚马逊如何取胜。

什么是元宇宙?简单地说,元界是一个数字空间,由人、地点和事物的数字表示形式表示。换句话说,这是一个“数字世界”,由数字对象代表真实的人。在很多方面,Microsoft Teams 或 Zoom 已经是 Metaverse 的一种形式。你在房间里“在那里”,但你...
继续阅读 »

什么是元宇宙?

简单地说,元界是一个数字空间,由人、地点和事物的数字表示形式表示。换句话说,这是一个“数字世界”,由数字对象代表真实的人。

在很多方面,Microsoft Teams 或 Zoom 已经是 Metaverse 的一种形式。你在房间里“在那里”,但你可能是一个静态图像、一个化身或一个直播视频。所以元界是一个更广泛的“将人们聚集在一起”的背景。

它可用于许多事情:会议、参观工厂车间、入职或培训。事实上,几乎所有与人力资源和人才相关的项目都可以为元界重新设计。如果您戴上 3D 眼镜,Metaverse 将完全身临其境。


为什么感觉这么奇怪?
许多供应商将在这个领域发挥作用。
当然,Facebook 选择重命名整个公司是为了说明这一点。微软(我将在下面解释)已经有一个主要的存在。但它会更大。事实上,每家科技公司、零售商和娱乐企业都想加入。

当你向专家询问元界这个词时,他们解释说它是多年前在一本科幻小说中创造的。但对于我们其他人来说,这听起来像是马克·扎克伯格想出的一些令人毛骨悚然的事情来捕捉我们所有的信息并向我们出售更多的广告。(并且可能会制造出更可怕的阴谋论、假新闻等等。)

不幸的是,由于 Facebook 似乎首先提出了这一点,因此有很多关于其价值的阴谋论。

好吧,让我向你保证这件事是真实的。对我们生活的价值可能很大。

首先让我建议没有一个“Metaverse”。将会有很多“元界”,一些用于商业,一些用于商业,一些用于教育,一些用于娱乐。首先让我主要谈谈业务应用程序,但我相信其他的很快就会出现。

商业元界可以做什么

首先让我提一下,今天的 Metaverse 是 AOL 期间互联网的所在地。换句话说,有很多新的东西要来。请记住,最初的网络都是基于文本的,速度很慢,甚至没有任何视频。

商业元界已经成型。在培训中,我们希望从“电子学习”到“我们学习”再到“数字学习”,再到“沉浸式学习”。这意味着大量的新应用程序——从入职和培训到领导力发展、会议、模拟体验、大型员工活动,当然还有娱乐。

大公司已经尝试了一段时间。埃森哲基于微软早期发布的 Mesh(其 Metaverse 和 Avatar 工具集)为顾问构建了一个完整的“Nth Floor”

我刚刚与微软的产品团队进行了交谈,他们的计划令人印象深刻Microsoft Mesh for Teams 将于明年年中推出,让您可以用头像替换您的视频状态,创建虚拟房间,并在 Teams 中实现 3D 空间。想象一下贸易展、学习会议或 3D 入职体验,所有这些都基于 Teams。我不得不相信,人们会对这项技术产生兴趣。

至于成为化身的价值?它实际上比你想象的更有价值。游戏玩家已经了解到,使用头像的人可以更有表现力、更诚实,并且在心理上更有安全感。快速发展的培训公司 Mursion了解到,基于虚拟形象的学习让人们在体验中更加诚实和真实,使他们能够比面对面的培训更好地学习和改变行为。(你必须经历它才能相信我。)

如果你只是害羞、内向、疲倦,或者可能是残疾人怎么办?化身是一种让您以一种新的有趣的方式“做自己”的方式,开启了一系列在传统环境中可能不舒服或不可能的对话和互动。

我还想象 Microsoft Metaverse(和其他供应商会这样做)将让您参观和了解制造您的产品的工厂。您的销售团队可以与您的客户进行真实的模拟。如果您使用像 STRIVR 这样专业开发平台,您将能够体验公司中每个运营、高风险或高价值培训场景的 3D 模拟。在 STRIVR 中的体验已经远低于现实世界的模拟,而且应用程序令人惊叹。

例如,想一想 Verizon 如何教其门店员工如何应对第一人称射击游戏?沃尔玛如何培训员工为黑色星期五做好准备。联邦快递如何教其包装工在卡车后部完美地包装和利用包裹?或者 JetBlue 如何教其飞行机械师在飞机下方行走时检查安全性。


埃森哲已经跃入这个大局。
除了试用 Microsoft Mesh 外,该公司还成立了一个完整的 AR 咨询团队,
帮助大公司构建基于元界的解决方案我们可以期待 Zoom、Webex(思科)、Nvidia、Netflix 和 Apple 等其他供应商加入。我见过的最有趣的 Metaverse 应用程序之一(来自 STRIVR)是一家公用事业公司,它教其运营人员如何爬下检修孔,识别需要调整的仪器和安全阀,并在不造成危险的情况下修复或诊断问题. 
如果没有 VR,这种类型的培训将是昂贵的、容易发生风险的并且是高度配给的。使用 STRIVR(Metaverse 应用程序)可以大规模体验。

为什么微软能大获全胜

与所有新技术一样,将会有很多“快速致富”的想法出现。虽然 Facebook 可能会经常谈论它,但他们不能“拥有”Metaverse。这就像说 Facebook“拥有互联网”一样。Metaverse 是许多提供商将构建的一组新技术,现在是一场竞赛,看谁能最快增加价值。

就我个人而言,我现在是微软战略的粉丝。该公司专注于构建 Mesh,这是一个支持 Teams 和其他应用程序的平台,以及Hololens,一种广泛用于制造、教育和军事的增强现实解决方案。微软希望为商业、教育、培训和娱乐启用 Metaverse 应用程序。这些都是现实世界的需求,每个需求都可以通过 Avatars、VR 和 AR 来增强和重塑。


微软的 AR 耳机和平台 Hololens 已经同样强大。
宝马、戴姆勒和福特等公司正在
为制造专业人士使用该技术,它可以帮助您快速学习、避免错误以及跟踪和改进流程质量。

我在听Jarod Lanier,VR 的先驱之一,描述 Facebook 的 Meta 公告他的分析很有趣:他对扎克伯格的评论:Facebook的战略是什么?

听完马克·扎克伯格的演讲后,听起来就像是某个狂妄自大的人拿走了我的东西,并通过某种奇怪的自我夸大过滤器进行了过滤。我的意思是,这只是最奇怪的事情。我一直认为你会出现,就像 1 亿微型企业家在这里和那里做他们的小事。而且不会有什么霸主……

他和我一样认为,Facebook 对苹果成为其广告业务的守门人感到不安。因此,通过专注于 Metaverse,公司可以尝试使 Oculus 成为未来的“平台”。然后 Meta 成为该域的下一个看门人。这是 Jarod 的看法:

我的意思是,我要大胆地猜测,如果蒂姆库克没有开始关闭 Facebook 对免费数据的访问,那 Meta 就永远不会发生。我认为 Meta 的意义在于,我们不拥有本轮获取数据的外围设备。所以在未来,我们需要赢得下一场设备大战,这样我们才能获得数据。而且,哦,如果我们拥有智能扬声器和门把手或其他任何东西,那就太好了,但亚马逊拥有这些。所以让我们去买耳机吧。我认为这就是它的最终目的。这是关于您必须拥有边缘设备才能拥有自己的力量来使您的云成为好或坏的事实。而且他显然想让它变得邪恶,所以他需要拥有一个设备,而他目前没有。

鉴于 Facebook 的商业模式,Meta 很可能会专注于广告。因此,您的 Facebook 体验可能涉及将您的大量个人数据提供给广告商(Facebook 的真实客户)。事实上,2018 年Oculus 执行官罗伯特·鲁宾曾表示“这是我们要失去的市场”。

收入也将来自广告,这是 Facebook 最了解的市场。鲁宾想象 可口可乐 为展馆的主要位置 付费, 福特为其虚拟汽车付费以使其可用,或者 宝洁公司 在数字广告牌上宣传其品牌。Gucci 可以开设一家虚拟商店, 康卡斯特 (CNBC 母公司 NBCUniversal 的所有者)将为“一个巨大的标语支付费用,上面写着‘康卡斯特:获得更好的 MetaSpeed!’”

是的,Facebook 也对企业市场产生了兴趣。Workplace by Facebook 拥有数百名客户,它似乎是一个精心设计的公司社交网络(尽管 Facebook 系统崩溃时它确实崩溃了)。Oculus Horizon Workrooms(可能会更名)正在尝试做 Microsoft Mesh 对 Teams 所做的事情。


迪士尼、Netflix、亚马逊和其他公司呢?
但 Facebook 也面临着挑战。
首先,他们不是一家值得信赖的公司(
只有 15% 的美国人信任 Facebook)。其次,他们的平台远远落后于微软。是的,他们很顽强,但在企业界,他们还有很长的路要走。

如果我猜想的会出现,我们将看到其他“元宇宙”出现。考虑娱乐业。本周迪士尼宣布将采取行动,将“连接数字世界和物理世界”用于讲故事和动画。谁不想走进 3D 或虚拟化身驱动的迪士尼电影并与角色交谈?迪士尼可能会成为“元界中最快乐的地方”。


这将随着时间的推移而发生
购物元界怎么样?
您不想去虚拟的亚马逊(或沃尔玛)商店购买产品、杂货、书籍和电影吗?消费者可以与作者交谈,拜访电影角色,并以 3D 形式观看食品。
亚马逊拥有抽搐,在世界上最大的游戏网络。认为他们没有在这方面工作?继续猜。得到我的漂移?

还有很多“边缘情况”尚未发生。NFT(Non-Fungible Tokens)将成为这个世界的一部分——使我们能够购买、拥有、许可和保护数字资产。由于此处捕获的 3D 数据比以往任何时候都多,因此将大量关注隐私、安全和数据保护。随着眼镜和耳机变得更便宜(在这个领域看 Apple),你可以打赌这些应用程序将影响我们的工作、家庭和周末生活。

并且还会有土地掠夺。品牌已经在收购有价值的房地产(视频游戏中的“虚拟空间”),因此您的公司可能想在 Farmville 购买一块土地。耐克只是为数字产品申请了专利,所以他们将来会销售数字产品。

在您认为这是一场反乌托邦的噩梦之前,请考虑一下这种转变的积极意义。我们多年来一直在构建的许多技术(区块链、VR、AR、传感器、相机、5G)现在正在融合在一起。我们今天所知的 Metaverse 一开始可能看起来很奇怪或不寻常,但很快你就会看到真正的应用程序出现。

我们将保持警惕,让我们看看会发生什么。

收起阅读 »

(转载)“元宇宙”究竟是什么

摘要:什么是“元宇宙”,1000个人眼里有1000个“元宇宙”。 本文分享自华为云社区《【云驻共创】年轻人如何入场元宇宙?未来已来!》,作者:启明。 近期,Facebook把自己公司更名为Meta(元),上了一波热搜;而前段时间国内的阿里腾讯,国外的谷歌等巨...
继续阅读 »

摘要:什么是“元宇宙”,1000个人眼里有1000个“元宇宙”。



本文分享自华为云社区《【云驻共创】年轻人如何入场元宇宙?未来已来!》,作者:启明。


近期,Facebook把自己公司更名为Meta(元),上了一波热搜;而前段时间国内的阿里腾讯,国外的谷歌等巨头纷纷宣布入局元宇宙;行业“冥”灯罗永浩也宣布要入局元宇宙,这个词汇出现的越来越频繁,而未来会更频繁。


那么“元宇宙”究竟是什么呢?


1000个人眼里有1000个“元宇宙”


什么是“元宇宙”,1000个人眼里有1000个“元宇宙”。


不同的媒体、公司、个人等,对“元宇宙”都有着自己的理解:


有的人认为“元宇宙”代表着人类文明的未来;而还有些人觉得“元宇宙”代表着虚拟世界的“躺平”,是个“邪恶”的东西,Elon Musk的冲向太空,才是人类文明的未来.....


而在这两种观点之间,还有一种观点:“元宇宙”和互联网一样,本身是不带任何属性的,重点还是看我们如何使用它。


要探讨什么是“元宇宙”,我们需要探索人类需求的本源。


影视文学中的元宇宙


首先,我们从影视文学中的元宇宙的角度来挖掘一下:


《雪崩》


对于元宇宙的解释,目前公认翻译自1992年斯蒂芬森科幻小说《雪崩》中“Metaverse”(也译为超元域)一词。元宇宙简单来说,就是现实世界中的所有人和事都被数字化投射在了这个网络云端世界里,你可以在这个世界里做任何你在真实世界中可以做的事情。与此同时,你还可能做你在真实世界里做不到的事情。


《庄周梦蝶》


而在中国2300多年前的百家争鸣时代,庄子梦到了自己变成了蝴蝶在翩翩飞舞,醒来之后不知身在何处,就产生了这样的思考:到底是庄子变成了蝴蝶,还是蝴蝶变成了庄子呢?哪个才是真是的存在?还有《枕中记》一书中的“黄粱一梦”等成语也是类似的哲思探索。


《星球大战》


在《星球大战》中我们看到具备三维全息投影功能的R2D2机器人;以及目前在游戏玩家中很流行的玩意儿:由一个全视角显示头盔和一套感应服构成,感应服可以使玩家从肉体上感觉到游戏中的击打、刀刺和火烧,能产生出酷热和严寒,甚至还能逼真地模拟出身体暴露在风雪中的感觉。在《三体》改编的同名游戏中提到,汪淼走到她后面,由于游戏是在头盔中以全视角方式显示的,在显示器上什么都看不到。


《王牌特工》


在美国影视剧《王牌特工》中,当你带上王牌特工的专属AR眼镜,其他与会人哪怕身在不同国家地区,都能就在身边一样,开一个全息会议。


《阿凡达》


在《阿凡达》影视剧中,科学家尝试将人类DNA和纳威人的DNA结合在一起,制造出一个克隆纳威人。而最神奇的地方在于克隆纳威人可以让人类的意识入驻其中,从而成为人类在这个星球上活动的“化身”(Avatar)。


《黑镜》


在《黑镜》第二季第一集当中,名为“Be Right Back·马上回来”,讲述了一对情侣Martha和Ash搬去了Ash父母居住的远离尘嚣的小镇生活,但是社交网络狂人Ash却在归还搬家租赁的货车时死于非命。在Ash的葬礼上,Martha的朋友Sarah告诉了她一种和死去的人建立联系的新方法,就是用Ash在社交网络中留下的所有信息、状态,更新和Like,Martha可以创造出一个新的“真”Ash,从而帮助她减轻伤痛。


《刀剑神域》


在《刀剑神域》中,当你带上脑机接口设备时,你在游戏中死亡,那么在现实中也将死亡。


《太空堡垒》


《太空堡垒》有3个非常了不起的设定:


1、全息眼镜Holoband和虚拟paradise


2、意识上传到云端


3、意识下载到机器人的身体中,成为第一代具备真正“智慧”的机器人


《头号玩家》


2045年,处于混乱和崩溃边缘的现实世界令人失望,人们将救赎的希望寄托于“绿洲”,一个由鬼才詹姆斯·哈利迪一手打造的虚拟游戏世界。人们只要戴上VR设备,就可以进入这个与现实世界形成强烈反差的虚拟世界。在这个世界中,有繁华的都市,形象各异、光彩照人的玩家,而不同次元的影视游戏中的经典角色也可以在这里齐聚。就算你在现实中是一个挣扎在社会边缘的失败者,在“绿洲”里也依然可以成为超级英雄,再遥远的梦想都变得触手可及。


《失控玩家》


今年上映的一个新的影视剧。其中的主角以为自己是生活的主角,其实只不是这个世界的一个NPC,这个舞台上的提线木偶。


《UPLOAD》


《UPLOAD》中的精彩设定:


1、死前将意识上传到虚拟天堂


2、死后的世界也有2G和5G之分


3、死后通过全息方式参加自己的葬礼


《黑客帝国》


未来的人类生活在机器人所制造的矩阵(Matrix)虚拟世界中,而机器人则得以从人体获取所需的生物能源。但生活在虚拟世界中的人类丝毫没有意识到自己的世界是虚拟的,知道“救世主”的出现。


我们并不是来讲解这个科幻影视作品的,我们要做的,是从中这些科幻影视作品中,看看人们的需求。


一提到需求,我们可能会立刻想到马斯克的“需求层次论”,但是在这里,我们更加抽象一下:



**物质需求:**创造价值与财富,提高生产力


**精神需求:**消费与享受生活,社交娱乐


**永生:**彻底脱离生老病死,实现数字化永生


当然,按照目前科技的发展进程,如果想实现人类生物身体的永生不老是非常难的,但是通过Metaverse的技术未来我们有可能会实现一种数字化的永生。


巨头眼中的元宇宙


介绍完影视剧中的“元宇宙”,我们来看看巨头眼中的元宇宙:


Facebook?Meta!


首先我们来看的是Meta,也就是之前的Facebook。对Meta来说,今年是一个非常重要的一年,因为今年它改元。当然我们都知道在中国的历史上,某个帝王更改自己的年号叫“改元”,其实对Facebook对Meta来说,2021年也是它的改元之年。我们可以看一看在它改元之前,它的虚拟现实以及在元宇宙做了哪些布局。


2014年的时候,其用20亿美元将股份收购了OculusVR。在收购的时候,扎克伯格在自己的Facebook主页上说了这样一句话:沉浸式的虚拟现实游戏,将是虚拟现实第一个重大应用,但是这仅仅只是一个起点,虚拟现实绝不仅仅是游戏,我们希望把它打造成下一个计算和通讯平台。


2018年9月,Facebook在OculusConnect开发者大会上宣布推出独立虚拟现实(VR)头盔Oculus Quest,跟Oculus Go类似,这种头盔无需PC或手机即可提供虚拟现实功能。但是它提供了6自由度的游戏控制器,可以让玩家更愉快的玩耍。


2020年9月,Facebook在开发者大会上宣布推出OculusQuest 2代,定价仅299美元。2021年11月17日,根据高通CEO透露的数据,Oculus Quest2代的累积销量已经突破1000万台!!!


那么1000万台意味着什么?意味着 VR头显的设备已经跨越了所谓的第一个极限点,即将迈向真正的星辰大海。


往后接下来几年还会陆续还有Oculus的3代和4代,而这些都在开发之中,而且价格肯定不会比二代贵,也会解决诸多的技术问题,但是具体的发布时间(可能在2022年圣诞节前)还在猜测当中。除了3代和4代之外,还有传闻中的PRO版本,也就是性能更强,价格更高,那么它可能会对标传说中要发布的苹果的新品。当然Meta也就是之前的Facebook,除了现有的Oculus这条产品之外,还在积极的研发AR眼镜。


看完上述的这些产品之外,我们也来看一看Meta还有其他哪些布局:2018年的Facebook F8大会上,Oculus首席科学家Michael Abrash宣布Oculus研发部门Oculus Research重新命名为Facebook Reality Labs,并同时涉足VR与AR技术的研发。


Facebook AR/VR部门在2021年总人数已逾1万人,占总员工人数的近20%,而2017年该部门仅为1000人。


Facebook有两个开发者的大会是值得我们关注的,分别是每年5月到6月的F8大会,以及每年10月底左右的XR开发者大会。


Facebook于2017年发布了名为AR Studio的AR套件,并一直与全球社区合作,共同塑造和定义Spark AR平台。


2021年8月20日,Facebook推出测试性的VR远程办公APP,名为HorizonWorkrooms,有了该软件,Oculus Quest 2用户可以用虚拟化身参与会议。


2021年11月11日,Meta宣布与微软合作,将Meta旗下的WorkPlace功能与微软的Teams整合,发展元宇宙办公室。


我们可以看到,现在虚拟现实和原有的产品和技术的布局,从之前的以数年为单位,现在已经大大的提速,增加到了一年半年甚至几个月都会有一个新的产品新的功能出来。


那么在内容生态上,Meta一是推出Oculus Store,目前已有超过60款Oculus Quest 游戏的营收超过100万美元;二是和第三方平台SideQuest合作。



其还成立了Oculus Studio,并且收购多家VR内容公司,包括Beat Games、Downpour、Ready at Dawn、Sanzaru Games、BigBox VR。


根据Steam VR平台的统计数据,我们可以看到OcQ设备的市场占有率是非常的高:



那么目前在这个市场上我们可以看到,整个平台已经有了一定数量的相关的VR内容,包括支持各个设备的,但是还仅仅是在以千为单位。



我们都知道,比如说苹果的App Store或者是安卓商城上的应用,都已经是突破上百万甚至几百万个,那么现在目前 VR的应用还处于非常的早期。


所以对于Meta来说,也就对于之前的Facebook来说,最重要的事情就是上个月28号扎克伯格宣布它正式更名成Meta,从此迈向未来的星辰大海。


微软


同样是元宇宙,我们可以看到Meta,也就是Facebook它更偏重于其社交属性。而对于微软来说,它更多是从企业办公、企业生产力方面来看,也就是所谓的“企业元宇宙”。


2015的开发者大会,微软与WIN10一起推出黑科技产品HoloLens;


2019年2月的MWC(世界移动通信大会)上,微软发布HoloLens 2代;


2021年4月,微软拿下美军218.8亿美元的军工版HoloLens合同。


除设备之外,


2018年10月,微软首次启动AzureDigital Twins平台预览版;


2020年12月,微软宣布AzureDigital Twins全面上市;


2021年3月,微软推出了一款具有3D化身和其他XR功能的虚拟平台Mesh,旨在打造能让人们通过AR/VR技术进行远程协作的应用。微软团队将会推出全新的3D虚拟化身,无须使用VR/AR头盔,用户将能够以虚拟任务或动画卡通的形式出现在视频会议中,且通过人工智能能够解读声音,让头像变得活灵活现。


可以看到微软的动作也是不断的加快:11月2日,微软在Ignite大会上宣布,计划将旗下聊天和会议应用Microsoft Teams打造成元宇宙,把混合现实会议平台Microsoft Mesh融入Microsoft Teams中。此外,Xbox游戏平台将来也要加入元宇宙。


萨提亚·纳德拉表示,微软的元宇宙最初专注于企业级应用。


微软(中国)首席技术官官韦青表示,没必要去纠结现在流行的技术叫什么词,无论是叫元宇宙也好,叫数字孪生也罢。永远不要忘记,创造虚拟空间的初衷是为了强化物理世界,让我们在现实生活提高生产效率,降低生产成本。


官韦青指出,像微软、苹果等科技公司的业务是虚拟空间、物理世界两方面业务皆有覆盖,两方面互补,而不是单方面地陷入到某一个领域。元宇宙构筑的逻辑,都是将物理世界的对象和现象变成模型,放到虚拟空间中,进行仿真、预测,最终反馈到物理空间,来强化我们的物理世界。


下图是微软的Metaverse解决方案,包括它的物理世界、连接、建模、位置、数据,还有智能逻辑以及协作平台等等,可以看到它是偏向于提高生产力。



下图是微软去年在AR/VR领域的专利,可以看到在Q1至Q3它都是排在第一的,Q4是Magic Leap跃居第一。



在Meta的眼中,元宇宙可能更多的是社交娱乐,也就是满足我们的精神需求,而在微软的眼中元宇宙做更多的是提升生产效率,满足物质层面的需求,那么,英伟达眼中的元宇宙又是什么呢?


英伟达-OmniVerse


英伟达提出自己的元宇宙叫OmniVerse。它在元宇宙相关的布局及相关产品:


1、NVIDIA RTX系统显卡和虚拟工作站;


2、NVIDIA CloudXR-XR串流平台(和微软Azure以及Amazon AWS开展合作,主要兼容AR和VR设备,包括不限于:1、大部分PCVR、HoloLens 2、VR一体机、支持AR的安卓和iOS设备等);


3、OmniVerse元宇宙平台-数字版老黄


2021年11月9日GTC大会再次升级Omniverse平台,发布了OmniverseAvatar和Omniverse Replicator。Omniverse Avatar是一个用于生成交互式AI化身的技术平台。它集合了英伟达在语音AI、计算机视觉、自然语言理解、推荐引擎和模拟技术方面积累的技术,为创建人工智能助手打开了大门,可以帮助处理数十亿的日常客户服务互动。Omniverse Replicator则是一种合成数据生成引擎,可以基于现有数据持续生成用于训练的合成数据。


Omniverse的门户是USD(通用场景描述)黄仁勋认为Omniverse的本质是一个数字虫洞。未来任何计算机都可以连接到Omniverse就像HTML(一种标记语言,可将网络上的文档格式统一)基于网站。


黄仁勋表示:“如何使用OmniVerse模拟仓库、工厂、物理和生物系统、5G边缘、机器人、自动驾驶汽车,甚至是虚拟形象的数字孪生,是一个永恒的主题。”


总结来说,在英伟达眼中,那么不管是叫OmniVerse,还是MetaVerse也好,它百分之八九十的功能是为了提升生产力。具体的细节大家可以去相关的英伟达的开发网站去看详细的细节(http:developer.nvidia.com/nvidia-omniverse-platform)。


苹果


苹果虽然目前还没有推出相关的产品,但是他在不断的收购相关的公司以及部署了非常多的专利。那么在各个场合其CEO库克也表达了他对元宇宙以及对虚拟现实的一些看法。


I think AR is big and profound. This is oneof those huge thing that we'll look back at and marved at the stat of it. Ithink customers are going to see it in a variety of ways anfd it feeld great toget AR going at a level that can get all of the developers behind it.


Tim Cook, Apple CEO


库克认为,AI,也就是增强现实是一个非常巨大的市场。


在苹果WWDC 2017大会上,苹果发布了AR开发工具ARKit,具备SLAM、平面检测、光照估计、环境理解、图像识别等功能;


2017年9月12日,苹果正式发布的iPhoneX系列手机中使用了A11 Bionic芯片,首次集成了神经网络引擎;以及3D结构光技术FaceID,通过iPhone X的Face ID可以制作3D表情Animoji;


2019年9月11日,苹果发布的iPhone11首次使用了UWB超宽频芯片U1,超宽频技术让iPhone 11系列更具空间感知能力,可精确定位其他配备U1的苹果设备;


2019年10月29日,苹果发布的AirpodsPro无线降噪耳机首次使用了“空间音频”功能,2020年9月苹果发布的iOS 14为AirPods Pro新增了“空间音频”功能;


2020年1月14日,苹果推出USDZ3D格式转换工具Reality Converter;2020年3月18日,苹果官网发布了iPad Pro 2020,首次使用了dTOF激光雷达(LiDAR);


2021年4月21日,苹果春季发布会上推出的iPad Pro 2021搭载M1芯片,令世人震惊;


2021年秋季发布会,苹果推出搭载M1X芯片的14寸和16寸Macbook Pro;


根据彭博社的报告透露,苹果未来的AR/VR设备将集成M系列芯片的高端版本。


那么产业链的消息是2022年的秋季,苹果很可能会发布自己的首款AR/MR头显。与此同时2025年的时候有可能会推出苹果首款AI眼镜。


当然前面也提到了,其实苹果虽然没有推出产品,但是它已经布局了非常多的专利,包括收购了大大小小的各种相关的公司,其实都是公开可以查询到的(http://www.fastscience.tv/collections…


谷歌


看完苹果之后,我们再来看一下谷歌。谷歌在这个领域的布局和定位,可能是没有那么的清晰。比如说我们都知道,Meta的定位是做社交元宇宙;微软做的就是企业元宇宙;苹果面向于 C端消费者市场,定位是做增强现实。


谷歌做了很多尝试性的工作,包括Google DayDream和Google Glass等,但是延续性都不是很强。所以对于谷歌今后将推出什么样的产品,我们无从知晓,目前来说延续性比较好的是ARCORE这一块。


当然它在相关技术的前沿研究上还是做的比较到位的,比如Project Starline,就是一个仿真、全新的社交。“Project Starline”是一个结合了硬件和软件技术进步的技术项目,旨在帮助相隔两地的朋友、家人和同事共聚一起。想象一下,透过一扇神奇的窗户,你可以看到另一个人,真人大小,三维形式。你们可以自然地对话,做手势和进行眼神交流。


华为


2019年推出VR Glass;2021年11月17日推出VR Glass 6dof 游戏套装版本;


2019年11月开源数据虚拟化引擎华为河图Cyberverse, 目的是打造一个“地球级、不断演进,与现实无缝融合的数字新世界”。华为河图有四个核心能力:1、3D高精地图能力;2、全场景空间计算能力;3、强环境理解功能;4、虚拟现实融合渲染能力。


其他公司


**字节跳动:**2021年8月29日字节跳动官宣90亿元人民币收购Pico。


腾讯:提出全真互联网概念。当然他在这个领域的更多是通过投融资投资来布局,比如说投资虚幻引擎,以及做上周又投了一家做触觉手套相关技术的公司。


HTC:


2015年3月在MWC 2015上发布HTC Vive,并于2016年上市;


2017年HTC 将部分手机业务出售给Google后,全面转型VR市场,曾一度占领市场先机。但是近两年C端市场的表现远远落后于Facebook,2021年6月宣布重心转向B端;


2021年5月发布HTC VIVEFocus3商业版和HTC VIVE PRO 2。


Sony:


2016年10月,Sony正式开始发售PSVR,并搭配PS4和下一代的PS5使用;2018年8月,PSVR销售突破了300万台;根据Sony官方透露的消息,PSVR2预计2022年发布


元宇宙百科词典


我们看完了科幻影视作品里面元宇宙,以及巨头对元宇宙之后的看法,接下来我们就看几个关键的核心的名词。


首先是3个R:



VR=一切皆梦幻泡影


VR(Immersive Virtual Reality)= 虚拟世界,沉浸式虚拟现实,忘了现实世界的一切~


VR满足3个特性,分别是沉浸、交互和想象。



AR=向左是真实,向右是虚幻


AR(Augmented Reality)=真实世界 + 数字化信息


MR=真实虚幻傻傻分不清


MR(Mixed Reality)=真实世界 + 虚拟世界+ 数字化信息,假作真时真亦假,无为有处有还无


**数字人:**什么是数字人,什么是虚拟偶像?通过建模、3D扫描以及动作捕捉,把类似真实的人的形象做成一个虚拟的数字人,然后让他做很多相关的初步动作。待会我们会在技术环节给大家讲述数字人是如何实现的。


**数字孪生(Digital Twin):**虚拟和现实的高度融合互通(现实世界的数字复刻)


1.最早用于NASA阿波罗项目,对飞行中的空间飞行器进行实时仿真;


2.实现物理工厂/系统和数字工厂/系统的交互和融合;


3.面向B端-用于工业4.0、智能制造、智慧城市等;


4.AR/VR、IoT、AI是重要的技术支撑。


下图是北京航空航天的陶飞等人从车间组成的角度给出了车间数字孪生的定义,然后提出了车间数字孪生的组成,主要包括:物理车间、虚拟车间、车间服务系统、车间孪生数据几部分。物理车间是真实存在的车间,主要从车间服务系统接收生产任务,并按照虚拟车间仿真优化后的执行策略,执行完成任务;虚拟车间是物理车间的计算机内的等价映射,主要负责对生产活动进行仿真分析和优化,并对物理车间的生产活动进行实时的监测、预测和调控;车间服务系统是车间各类软件系统的总称,主要负责车间数字孪生驱动物理车间的运行,和接受物理车间的生产反馈。



**全真网:**马化腾于2020年底在腾讯集团官方年度特刊《三观》提出


1.移动互联网的接替者;


2.虚拟世界和真实世界的全面融合;


3.全面+真实(全面= 消费互联网+产业互联网 真实= AR/VR交互技术)。


**元宇宙:**源自科幻作品《雪崩》,⼀个⼈们以虚拟形象在三维空间与各种软件进⾏交互的世界。其真正为人所知是今年Roblox上市的时候,把MetaVerse加到了招股说明书,并且提出了元宇宙的八大要素:身份、朋友、沉浸感、低延迟、多元化、随地、经济系统、文明。



可以看出,其对元宇宙的理解,更多也是一个社交娱乐层面的。而维基百科之中,对元宇宙的定义更加的精准和全面:The metaverse (a portmanteau of "" and"universe") is a hypothesized iteration of the internet, supportingpersistent online 3-D virtual environments through conventional personalcomputing, as well as virtual and augmented reality headsets.


一句话概述:元宇宙就是下一代互联网。


元宇宙的技术基础


那么接下来我们来一起看一看元宇宙就是如何构建的,也就是元宇宙的技术基础。


元宇宙的构成技术是非常的多,包括虚拟现实、区块链、AI+人工智能等等.



我们本次重点从虚拟现实和大家分享一下。AR/VR技术的科技树,也就是五大核心技术:近眼显示技术、内容创建技术、网络传输技术、渲染技术、感知和自然交互技术。



1.Near-eye display(近眼显示技术)包括传统的屏幕显示技术(LCOS/OLED/可折叠的AMOLED/Micro LED)和光学技术(光场显示/波导技术等等。


2.Content creation(内容创作技术)包括虚拟角色和场景构建、动作捕捉、全景视频拍摄与编辑等等。


3.Network communication(网络传输技术)


这方面最受人关注的当然就是即将商用的5G技术,以及传说中传输速率可达每秒1T的下一代6G技术了。当然还有一系列的其它技术有待发展。


4.Rendering Processing(渲染技术)包括本地渲染、云渲染、光场渲染、多重视角渲染,以及硬件渲染加速等等技术。


5.Perception&interaction(感知和自然交互)


包括跟踪定位技术、多感官自然交互技术(脑波、语音交互、触感交互等等)、机器视觉技术(SLAM/场景分离与识别等)


AR/VR技术有一个非常著名的科技树(如下图),我们可以看到刚才提到的五大技术都有一个科技树展开,然后每个树也有自己的树干,每个树干上也有非常多的分支和树叶。毫不夸张的说,在其中的任何一个树干,甚至任何一个树叶之上,如果去做深入的研究,都可以在这个领域成为一个非常资深的专家。



下图是AR/VR技术成熟度曲线,可以看到类似跟踪定位、液晶屏显示、云渲染以及OS相关等技术基本上都是属于两年之内可以商用的。


类似另外一些,比如说自由曲面、虚拟化身、混合云渲染等这些可能要2~5年。



接下来我们快速的带大家来一起过一下5大核心技术。


近眼显示技术


首先是**近眼显示技术。**近眼显示技术分两个部分,分别是显示技术以及光学技术。


显示技术其实就指的各种各样的显示屏,比如LED、MicroLED等等。



而对于光学技术,我们可能首先想到就是双眼视差原理。人眼是如何实现立体视觉呢?其实最简单就是因为每个人都有两只眼睛,每个眼睛之间都有一定的间隔,通过间隔每个眼睛看到的图像有所差别,再通过我们的大脑的这种判断,最终都形成了一个立体视觉。



同时,AR/VR的光学系统,包括 Pancake,折返式、自由曲面以及光波导。


然后我们再来看一下还有全息投影技术,3D全息投影技术可以分为投射全息投影和反射全息投影两种,是全息摄影技术的逆向展示。


目前我们经常看到的各类表演中所使用的全息投影技术都需要用到全息膜这种特殊的介质,而且需要提前在舞台上做各种精密的光学布置。虽然看起来效果绚丽无比,但成本高昂,操作复杂,需要专业训练,并非每个普通人都可以轻松享受到的。从某种程度上来说,目前的主流商用全息投影技术只能被称作“伪全息投影”。


内容创建技术


内容创建技术分成360全景拍摄、传统3D建模和3D重建。


全景拍摄,其实也就是全景相机还有全景摄像机。


优点:百分百真实


缺点:无法切换焦点,无法和场景及人物互动


3D建模就是大家熟悉的3D MAX、玛雅等。


优点:精度高,流程成熟


缺点:耗费大量人力、时间、精力


3D重建主要是针对于小物体以及人物角色。它本身就分成基于2D图像、基于3D扫描、基于红外TOF。


惯性动作捕捉技术也是比较主流的动作捕捉技术之一。其基本原理是通过惯性导航传感器和IMU(惯性测量单元)来测量演员动作的加速度、方位、倾斜角等特性。惯性动作捕捉技术的特点是不受环境干扰,不怕遮挡,采样速度高,精度高。2015年10月由奥飞动漫参与B轮投资的诺亦腾就是一家提供惯性动作捕捉技术的国内科技创业公司,其动作捕捉设备曾用在2015年最热门的美剧《冰与火之歌:权力的游戏》中,并帮助该剧勇夺第67届艾美奖的“最佳特效奖”。


我们以英伟达的“虚拟发布会”为例,来讲一下怎么搭建一个虚拟场景。


详细的步骤可以参考下图:



第一步:使用3D扫描构建虚拟场景



第二步:使用体积摄影进行全身3D建模



第三步:使用AI Audio2Face让口型和面部肌肉变化随语音变动



第四步:使用动作捕捉获取身体姿态动画



第五步:使用RTX渲染器进行实时光线追踪



在了解前述知识点之后,我们再来看看3D引擎和SDK技术。当然在这个领域AR/VR里面最常用的3D引擎无非也就是虚幻和Unity。


在此,推荐一本非常经典的书叫《游戏引擎架构》。书里对游戏引擎,从低阶到图形动画,再到高阶的构成做了非常详细的描述和解释说明,目前已经是出到第三版了。



AR/VR相关的SDK比如说Vuforia、APPLE ARKit,GOOGLEARCORE等等。


网络传输技术


我们再来看一下网络传输技术。网络传输技术是虚拟现实的支撑技术。


渲染技术渲染技术包括本地渲染、云渲染、光场渲染、多重视角渲染,以及硬件渲染加速等等技术。


本地的VR渲染流程如下:



云VR渲染流程:


在本地VR渲染的基础上额外增加三个环节(比本地渲染增加20ms左右的延迟):


1.图像压缩编码


2.网络传输


3.图像解压缩


云VR渲染的利弊:


好处:


1.降低对本地硬件处理能力的要求


包括存储空间、性能、散热等,从而让设备增加轻便


缺点:


1.额外增加延迟,影响实际体验


2.清晰度经压缩和传输后无法保证


端云渲染的配合使用


1.对于不追求及时响应的应用


如3dof游戏、VR看房、旅游景点观赏、全景视频播放等,通过ATW+云渲染+本地观看的方式可以获得比较好的效果。


2.对于追求及时响应的6dof游戏和社交互动应用


渲染处理更多还是需要在本地进行,云端用于处理指令型数据(参考大型多人在线游戏MMORPG)。


3.当前的5G网络和设备硬件性能无法支撑强互动型的云VR渲染和数据传输,未来的6G可以完全实现。


感知和自然交互技术


Inside-out技术


基于单目/双目/多目视觉+IMU的inside-out技术取代早期的Outside-in技术开始产品化,特别是在VR一体机设备,如Oculus Quest /Oculus Quest 2,HTC Vive Focus等。


可以实现:


1.追踪定位


2.手势动作识别


FOV眼动追踪技术


眼动追踪的原理其实很简单,就是使用红外摄像头和LED捕捉人眼或脸部的图像,然后用算法实现人脸和人眼的检测、定位和跟踪,从而估算用户的视线变化。目前主要使用光谱成像和红外光谱成像两种图像处理方法,前一种需要捕捉虹膜和巩膜之间的轮廓,而后一种则跟踪瞳孔轮廓。


SLAM


基于RGBD相机和红外TOF、激光雷达和AI算法等实现实时场景3D重建,在机器人、无人机和AR/VR设备如HoloLens中得到普遍应用,


除此之外,还有语音交互和语义理解、触觉反馈,嗅觉及其它感觉及模拟器。


另外还有一个非常亮的亮点——脑机接口(大脑和计算机直接进行交互,有时候又被称为意识-机器交互,神经直连。脑机接口是人或者动物大脑和外部设备间建立的直接连接通道,又分为单向脑机接口和双向脑机接口)。


单向脑机接口只允许单向的信息通讯,比如只允许计算机接受大脑传来的命令,或者只允许计算机向大脑发送信号(比如重建影像)。而双向脑机接口则允许大脑和外部计算机设备间实现双向的信息交换。



如何参与元宇宙


作为开发者也好,作为兴趣者也好,我们如何来参与元宇宙呢?


未来5年产业发展预测


首先看一下产业链的构成,它包括硬件、平台、工具、内容、行业应用,还有服务。


硬件有分很多分支,比如终端设备,还有其中的零部件等等,其中:


1.2022年将是AR/VR行业真正爆发的元年,特别是VR;


2.VR设备从2021年OCQ2代单款突破千万销量后,将开始爆发式增长;


3.苹果新品将让AR/VR从小众精英人群的玩具走向大众,其行业影响力不容小觑;


4.终端设备从2022年开始将成为巨头逐鹿的市场,小型创业团队的窗口期接近关闭,从2021年下半年开始会看到更为密集的战略型投融资或并购事件发生;


5.AR设备在接近诸多技术问题之前,主要仍将面向2B市场,在2025年可能迎来爆发;


6.核心器件方面(芯片、显示屏、光学器件、声光电传感器等模组)投入巨大,不适合初创型团队,目前仍然是巨头以及上市公司体量团队的天下。但该部分也是构成终端设备比例最大的部分;


7.感知交互方面,目前并没有统一的行业标准,空间定位、手势交互、眼动追踪、全身动捕、语音交互、脑机交互都处于发展的早期阶段,该领域有众多的初创企业。而Facebook、苹果等公司收购的重点也在该领域的领先技术团队;


8.其它配套外设,目前全景相机领域已有脱颖而出的领先者,如Insta360,其它领域因生态系统尚未标准化,有较大的空间。


工具平台


1.工具平台中的系统级平台(操作系统/UI)仍将由巨头把控,特别是苹果、Facebook,国内厂商如能接受移动互联网时代的教训,应在第一时间切入底层系统平台的打造,否则仍将受制于人


2.AR/VR内容创建工具目前虽然已经有Unity/UE4等市场领先产品,但是因为设备平台的独特性,仍然有巨大的潜力空间,包括SDK、3D开发引擎、基于AI技术的自动化3D场景和角色建模工具、基于AI技术的高效渲染软件等,都有足够的空间。该部分也给初创型团队留下了足够的机会。


内容-内容创作


随着三大核心产品的爆发,以及C端的量级突破,内容创作方面将迎来全面繁荣,包括影视、游戏、直播、社交、3D/全景等。而内容创作因为其创意和开放性属性,一向是初创团队的首选,在硬件和工具平台领域形成各自王者之后,将有越来越多的团队加入该领域。内容创作和工具平台的交集是类似Roblox的“元宇宙”大型多人在线社交类产品。


内容-内容分发


1.系统级别的内容分发和流量入口仍将占据首要地位,特别是后续的苹果生态


2.类似移动互联网的安卓商城生态,将有众多的第三方内容分发平台涌现,比如专门针对Oculus Quest的SideQuest平台。3.类似于移动互联网时代的微信,后续也将有类似微信的超级APP出现,同样可以扮演内容分发的角色。


行业应用


1.在国内市场,行业应用领域短期内仍将是VR的主要商业变现应用场景,如面向职业教育的教育培训、医疗健康、军事训练等。


2.在可预见的5年周期内,AR的主要应用场景仍然集中在行业领域,特别是智能制造、数字孪生等。


服务


随着行业的爆发式增长,相关的媒体、协会、线下活动等也会更加活跃起来,并逐渐形成集媒体、投融资服务、产品推介等为一体的综合服务,类似移动互联网时代的36kr等。部分媒体也会朝内容分发的方向去尝试。


总结


我们见证了历史,也步入了未来。人人皆可改变世界。



作者:华为云开发者社区
链接:https://juejin.cn/post/7034698377270919198
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

(转载)运营人的元宇宙

元宇宙是什么?是虚拟现实,是现代人逃离现状的美好乌托邦,又或是一个顶级的智商税。到底是什么,我们不做过多深究,既然名字叫元宇宙,那多半类似互联网黑话中的底层逻辑一样,说的人不太明白,听的人更是一头雾水,因为皇帝新装的缘故,没人敢指出来其中的疑惑,这确实太像智商...
继续阅读 »

元宇宙是什么?是虚拟现实,是现代人逃离现状的美好乌托邦,又或是一个顶级的智商税。

到底是什么,我们不做过多深究,既然名字叫元宇宙,那多半类似互联网黑话中的底层逻辑一样,说的人不太明白,听的人更是一头雾水,因为皇帝新装的缘故,没人敢指出来其中的疑惑,这确实太像智商税了

\

01

运营人的元宇宙之美好幻想

元宇宙更像黑客帝国,在残酷现实中构筑美好的幻想世界,与黑客帝国不同的是,现实中的虚拟现实你可以自由设定世界观,你就是自己元宇宙的国王,挥挥手建立起自己的王国。

确实让人充满期待,类似塞尔达那样的开放世界,你可以做任何想做的事,飞天、遁地啥的

运营人的美好幻想是什么?做的每个策略都取得百分百的效果,每个月都完成kpi,篇篇爆文,随便拍的一个视频点赞破百万,直播带货在线10万加,自己主导的项目干翻互联网大厂,成为互联网又一极。

这一切能发生吗?在元宇宙里还真的能发生,就像魔兽争霸里调秘籍一样,有无限的资源可以用,我就是神一样的存在。

在运营人元宇宙中,用户为你所用,资源为你所调用,总之一句话,在虚拟现实世界中,一切都是你说的算

上面说的是幻想,现实中能不能实现呢?其实还是有可能梦想成真的,我说的那些成绩,现实中不乏能做到的人,既然别人能做到,我们当然也有做到的可能。

如何让自己在现实中变的牛掰起来,多看,多做,多思考。能做到这三点,你就是现实中的王者

虚拟现实毕竟不能当饭吃,练好现实中的技能才是主要的,虽然说有的人已经开始利用元宇宙概念割韭菜了,这种行为不提倡,但是这种与时俱进,奋斗不止的精神是大家值得学习的。

问问自己,这个月学会了什么新技能,相比去年能力有没有提升,现实宇宙中务必要做到每天进步一点点,这样即使元宇宙摆在你面前,你也不会想进去,因为现实宇宙中你过得很不错。

\

02

运营人的元宇宙之面对现实

就像网络上给父母的一句忠告:你一定要清晰的认知到,你的孩子未来也就是普罗众生中普普通通的一员。接受这个现实,你或许能过的更幸福。

我接触的一些运营人也是存在很多幻想,动不动颠覆行业,融资千万,那都是在元宇宙里面才能发生的事。

现实中还是要脚踏实地,做好眼下手头的工作才是重要的,当你把工作都做好了,也许升职加薪就找上门来了

大家追逐元宇宙,主要是元宇宙的美好幻想,能让人逃避现实,想想沉溺网络游戏的玩家,在游戏中是叱咤一方的领主,万人敬仰。

现实中根本没人理睬,活在虚拟现实中就变成了这些人的刚需,运营人谁不想任何事情都心想事成,但是生活的本质是幸福与磨难并行,没有任何一个人一生都是顺风顺水的。

看看互联网头部人物,有的流年不利,有的晚节不保,有的三振出局,前半生光鲜,后半生凄惨的例子简直太多,这就是生活

我们要认识到生活就是如此,幻想可以留在晚上做梦时做,也可以寄托于元宇宙中,不过要知道作为碳基生物,吃饭是永远离不开的话题,一日三餐还是要在现实生活中解决。

有的人每餐吃生猛海鲜,有的人吃沙县拉面,这是很正常的,你能带来多大价值,就会获得多少回报,你的回报决定了你的生活水平,不怨天尤人,勤勉待人,这就是你应该做的。

\

03

运营人的元宇宙之割与被割

元宇宙是不是智商税我不知道,有些人已经被割韭菜了我倒是知道的,运营人一般都比较聪明,割韭菜的镰刀很难割到。

同样在线付费课程,非运营行业付费率老高,到了运营这,很难收到钱,有人得出运营人比较穷的结论,我倒不觉得,毕竟运营岗的工资还可以。

其实就是运营人猴精猴精的不好割,咱们什么套路没见过,有的割韭菜套路还是运营发明的,始作俑者运营人也。

这里我想跟所有的互联网人提个醒,元宇宙说不准是西方割东方互联网公司的大镰刀,还记得苏联是如何被漂亮国的虚假星球大战计划拖垮的吗?

当我看到国内几乎所有互联网企业抢注元宇宙商标时,直觉告诉我,这些企业要被割了。

虽然外国的互联网比较发达,但是移动互联网却被我们远远甩在身后,移动支付、网购、短视频厉害的不要不要的,我要是外国互联网公司,技术上拼不过,那肯定要从其他地方想办法。

比如编织一个虚无概念给其他人跳,等你投入了巨量资金,发现人家一开始就没跟时,你不仅白白浪费了百亿千亿资金,更主要的是错过了有价值技术的窗口期

最近也有官方声音出来给元宇宙降降了温,为了大家的财产安全真是操碎了心。

别人公司改个名字就掀起了如此大的波澜,浑水摸鱼者有之,推波助澜者有之,被洗脑者亦有之,你属于哪一类,只能自己去判断了

退一步讲,元宇宙假如真能实现,每个人都能在虚拟世界中畅快的生活,请问,现实中住哪里?如何培育下一代?工资找谁领?

当面对这些具体的问题时,你会发现与其操心元宇宙的未来,不如想想明天午饭吃什么来的实际。

\

04

最后

从心里讲,我还是很希望元宇宙能成为现实的,咱好歹也是游戏行业出身的人儿,元宇宙差不多算是加强版的vr游戏,有体感,有触觉,一切变的那么真实,多嗨皮的

不过技术的成熟需要时间,几年内是不可能的,几十年后也许能成为现实,这让我想起了盛大,当初执意做娱乐盒子,结果没做起来,是方向错了吗?不是的。

后来腾讯给做起来了,失败的原因是什么呢?那时候网络条件还不具备,做的太早了,成了先烈,元宇宙的未来也许是好的,但眼下我觉得不是,这就是我对元宇宙的粗浅理解吧!

作者:老虎讲运营,运营岗书籍《全栈运营高手》作者,运营推广大牛,千万流水项目操盘手,专栏作家,专注产品运营推广,擅长品牌打造和爆款制造,号称运营推广老司机。

原文链接:https://juejin.cn/post/7033417428965163022

收起阅读 »

重新审视前端模块的调用, 执行和加载之间的关系

在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史 如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量. 在最初的时候前端工程师为了分享自己的代码, 往往会通过 wind...
继续阅读 »

在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史


如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量.


在最初的时候前端工程师为了分享自己的代码, 往往会通过 window 来建立联系, 这种古老的做法至今还被很多人使用, 因为简单. 例如我们编写了一个脚本, 通常我们并不认为这是个模块, 但我们会习惯于将这个脚本包装成一个对象. 例如


window.myModule = {
getName(name){
return `hello ${name}`
}
}

当其他人加载这个脚本后, 就可以便捷的通过 window.myModule 来调用 getName 方法.


早期的 JavaScript 脚本主要用于开发一些简单的表单和网页的交互功能, 那个年代的前端工程师数量也极少, 通过 window 来实现模块化并没有什么太大的问题.


直到 ajax 的出现, 将 web 逐步推动到了富客户端的阶段, 随着 spa 的兴起, 前端工程师发现使用 window 模块化代码越来越难以维护, 主要原因有 2 个



  1. 大量的模块加载污染了 window, 导致各种命名冲突和意外覆盖, 这些问题还很难定位.

  2. 模块和模块之间的交互越来越多, 为了保证调用顺序, 需要人为保障 script 标签的加载顺序


为了解决这个问题, 类似 require seajs 这样的模块 loader 被创造出来, 通过模块 loader, 大大缓解了上述的两个问题.


但前端技术和互联网发展的速度远超我们的想象, 随着网页越来越像一个真实的客户端, 这对前端的工程能力提出了极大的挑战, 仅靠单纯的脚本开发已经难以满足项目的需要, 于是 gulp 等用于前端工程管理的脚手架开始进入我们的视野, 不过在这个阶段, 模块 loader 和前端工程流之间尚未有机的结合.


直到 nodejs 问世, 前端拥有了自己的包管理工具 npm, 在此基础上 Webpack 进一步推动了前端工程流和模块之间的整合, 随后前端模块化的进程开始稳固下来, 一直保持至今.


从这个历史上去回顾, 前端模块化的整个进程包括 es6 关于 module 的标准都是一直围绕这个一个核心命题存在的.


无论是 require 还是 Webpack 在这个核心命题上并没有区别, 即前端模块遵循


加载 → 调用 → 执行 这样的一个逻辑关系. 因为模块必须先加载才能调用并执行, 模块加载器和构建工具就必须管理和分析应用中所有模块的依赖关系, 从而确定哪些模块可以拆分哪些可以合并, 以及模块的加载顺序.


但是随着时间的推移, 前端应用的模块越来越多, 应用越来越庞大, 我们的本地的 node_modules 几百兆起步, Webpack 虽然做了很多优化, 但是 rebuild 的时间在大型应用面前依然显得很慢.


今年 2 月份, Webpack 5 发布了他们的模块拆解方案, 模块联邦, 这个插件解决了 Webpack 构建的模块无法在多个工程中复用的问题.


早些时间 yarn 2.0 采用共享 node_moudles 的方法来解决本地模块大量冗余导致的性能问题.
包括 nodejs 作者在 deno 中放弃了 npm 改用网络化加载模块的方式等等.


可以看到社区已经意识到了先行的前端模块化机制再次面临瓶颈, 无论是性能还是维护成本都面临诸多挑战, 各个团队都在想办法开辟一个新的方向.


不过这些努力依然没有超越先行模块化机制中的核心命题, 即模块必须先加载, 后调用执行.


只要这个核心命题不变, 模块的依赖问题依然是无解的. 为此我们尝试提出了一种新的思路


模块为什么不能先调用, 后加载执行呢?


如果 A 模块调用 B 模块, 但并不需要 B 模块立即就绪, 这就意味着, 模块加载器可以不关心模块的依赖关系, 而致力于只解决模块加载的效率和性能问题.


同时对于构建工具来说, 如果 A 模块的执行并不基于 B 模块立即就绪这件事, 那么构建工具可以放心的将 A 和 B 模块拆成两个文件, 如果模块有很多, 就可以利用 http2 的并行加载能力, 大大提升模块的加载性能.


在我们的设想中, 一种新的模块加载方式是这样的


// remoteModule.js 这是一个发布到 cdn 的远程模块, 内部代码是这样

widnow.rdeco.create({
name:'remote-module',
exports:{
getName(name, next){
next(`hello ${name}`)
}
}
})


让我们先不加载这个模块, 而是直接先执行调用端的代码例如这样



window.rdeco 可以理解成类似 Webpack runtime 一样的存在, 不过 rdeco 是一个独立的库, 其功能远不止于此



// localModule.js 这个是本地的模块
window.rdeco.inject('remote-module').getName('world').then(fullName=>{
console.log(fullName)
})

然后我们在 html 中先加载 localModule.js 后加载 remoteModule.js


<scirpt src="localModule.js"></script>
<scirpt src="remoteModule.js"></script>


正常理解, localModule.js 加载完之后会试图去调用 remote-module 的 getName 方法, 但此时 remoteModule 尚未加载, 按照先行的模块化机制, 这种调用会抛出异常. 为了避免这个问题


模块构建工具需要分析两个文件的代码, 从而发现 localModule.js 依赖 remoteModule.js, 然后保存这个依赖顺序, 同时通知模块加载器, 为了让代码正常执行, 必须先加载 remoteModule.js.


但如果模块可以先调用后加载, 那么这个复杂的过程就可以完全避免. 目前我们实现了这一机制, 可以看下这个 demo: codesandbox.io/s/tender-ar…


你可试着先点击 Call remote module's getName method 按钮,


此时文案不会变化只是显示 hello, 但代码并不会抛出异常, 然后你再点击 Load remote module 按钮, 开始加载 remoteModule, 等待加载完成, getName 才会真实执行, 此时文案变成了 hello world



作者:掘金泥石流
链接:https://juejin.cn/post/7034412398261993479

收起阅读 »

CSS实现随机不规则圆角头像

 前言 最近真是彻底爱上了 CSS ,我又又又被 CSS 惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文 给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面 学习本文章,你可以学到:bor...
继续阅读 »

 前言


最近真是彻底爱上了 CSS ,我又又又被 CSS 惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文


给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面


学习本文章,你可以学到:

  • border-radius 实现椭圆效果
  • border-radius 实现不规则圆角头像
  • animation-delay 设置负值
  • 实现随机不规则圆角

📃 预备知识


🎨 border-radius


border-radius 可以设置外边框的圆角。比如我们经常使用的 border-radius: 50% 可以得到一个圆形头像。


radius50.png


border-radius 就只能实现圆形效果吗?当然不是,当使用一个半径是确定圆形,两个半径时则会确定椭圆形。


光说不练假把式,接下来一起试试



  1. 设置 border-radius: 30% 70%,就可以得到椭圆效果


radius3070.png


上面的设置都是针对于四个方向的,也可以只设置一个方向的圆角



  1. 设置 border-top-left-radius: 30% 70%


radius3070top.png


从上图其实可以得出,两个值分别设置水平半径和垂直半径的半径,为了更准确我们验证一下


radiusopa.png


但为啥设置的圆角与 border-radius: 30% 70% 设置有这么大的差距。别急,下面慢慢道来。



  1. 设置 border-radius: 30%/70%,/ 前后的值分别为水平半径和垂直半径



border-radius: 30%/70% 相当于给四个方向都设置 30%/70%,而 border-radius: 30% 70% 是给左上右下设置 30% ,左下右上设置 70%



radius30-70.png



  1. 设置四个方向为四种椭圆角: border-radius: 40% 60% 60% 40% / 60% 30% 70% 40% ,就可以实现简单的不规则圆角效果,小改改的头像是不是看起来舒服了好多。


radiusdisorder.png


💞 animation-delay


animation-delay: 可以定义动画播放的延迟时间。


但如果给 animation-delay 设置负值会发生什么那?



MDN 中指出: 定义一个负值会让动画立即开始。但是动画会从它的动画序列中某位置开始。例如,如果设定值为 -1s ,动画会从它的动画序列的第 1 秒位置处立即开始。



那个,乍看上去,我好像懂了,又好像没懂,咱们还是来自己试一下吧。



  • 创建 div 块,宽高都为 0 ,背景设置为 #000

  • 添加 keyframe 动画,100% 状态宽高都扩展为 1000px


@keyframes extend {
0% {
width: 0;
height: 0;
}
100% {
width: 1000px;
height: 1000px;
}
}


  • div 添加 animationanimation-delay


/* 设置 paused 可以使动画暂停 */
animation: extend 10s linear paused;
animation-delay: -3s;

当我打开浏览器时,浏览器出现 300*300 的黑色块,修改 animation-delay-4s ,浏览器出现 400*400 的黑块。我们使用 linear 匀速作为动画播放函数,10s 后 div 会变为 1000px,设置 -3s 起始为 300px-4s 起始为 400px


这样一对比,我们来把 MDN 的描述翻译一下:
+ animation-delay 设置负值的动画会立即执行
+ 动画起始位置是动画中的一阶段,比如上述案例,定义 10s 的动画,设置 -3s 动画就从 3s 开始执行


🌊 radius 配合 delay 实现


有了上面基础知识的配合,不规则圆角的实现就变得很简单了。


设置 keyframekeyframe 的开始与结束为两种不规则圆角,再使用 :nth-child 进行自然随机设置 animation-delay 的负值延迟时间,就可以得到一组风格各异的不规则圆角效果



自然随机的算法非常有意思,效果开创者为了更好、更自然的随机性,选取序列为 2n+1 3n+2 5n+3 7n+4 11+5 ...




  1. 设置 keyframe 动画


@keyframes morph {
0% {
border-radius: 40% 60% 60% 40% / 60% 30% 70% 40%;
transform: rotate(-5deg);
}
100% {
border-radius: 40% 60%;
transform: rotate(5deg);
}
}


  1. 自然随机设置每个头像的 delay


.avatar:nth-child(n) {
animation-delay: -3.5s;
}
.avatar:nth-child(2n + 1) {
animation-delay: -1s;
}
.avatar:nth-child(3n + 2) {
animation-delay: -2s;
}
.avatar:nth-child(5n + 3) {
animation-delay: -3s;
}
.avatar:nth-child(7n + 5) {
animation-delay: -4s;
}
.avatar:nth-child(11n + 7) {
animation-delay: -5s;
}

当当当当~~~ 效果就实现了! 看着下面这些风格各异的小改改,瞬间心情舒畅了好多。


avater.png


不规则圆角头像的功能实现了,但总感觉缺点什么?如果头像能有点动态效果就更好了。


例如 hover 时,头像圆角会发生变化,用户的体验会更好。


我首先的想法还是在上面的代码基础上面更改,但由于 @keyframe 定义好了终点时的状态,能变化的效果并不多,而且看起来很单调,显得很呆 🤣。


那有没有好的实现方案那?有,最终我找到了张鑫旭大佬的实现方案,大佬还是大佬啊。


🌟 radius 配合 transition 实现


参考博客: “蝉原则”与CSS3随机多背景随机圆角等效果



  1. 按照自然随机给每个头像赋予不同的不规则圆角


/* 举两个例子 */
.list:hover {
border-radius: 95% 70% 100% 80%;
transform: rotate(-2deg);
}
.list:nth-child(2n+1) {
border-radius: 59% 52% 56% 59%;
transform: rotate(-6deg);
}


  1. 设置 hover 时新的不规则圆角


.list:nth-child(2n+1):hover {
border-radius: 51% 67% 56% 64%;
transform: rotate(-4deg);
}

.list:nth-child(3n+2):hover {
border-radius: 69% 64% 53% 70%;
transform: rotate(0deg);
}


  1. list 元素配置 transition


avatar.gif


完成上面的步骤,我们就可以得到更灵动的小改改头像了。



但这种实现方法相比较于 radius 配合 animation-delay 实现具备一定的难点,需要设计多种好看的不规则圆角效果



🛕 源码仓库


传送门: 随机不规则圆角




作者:战场小包
链接:https://juejin.cn/post/7034396555738251301

收起阅读 »

使用 Promise 时的5个常见错误,你占了几个!

Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。 在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。 ...
继续阅读 »

Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。


在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。


1.避免 Promise 地狱


通常,Promise是用来避免回调地狱。但滥用它们也会导致 Promise是地狱。


userLogin('user').then(function(user){
getArticle(user).then(function(articles){
showArticle(articles).then(function(){
//Your code goes here...
});
});
});

在上面的例子中,我们对 userLogingetararticleshowararticle 嵌套了三个promise。这样复杂性将按代码行比例增长,它可能变得不可读。


为了避免这种情况,我们需要解除代码的嵌套,从第一个 then 中返回 getArticle,然后在第二个 then 中处理它。


userLogin('user')
.then(getArticle)
.then(showArticle)
.then(function(){
//Your code goes here...
});

2. 在 Promise 中使用 try/catch


通常情况下,我们使用 try/catch 块来处理错误。然而,不建议在 Promise 对象中使用try/catch


这是因为如果有任何错误,Promise对象会在 catch 内自动处理。


ew Promise((resolve, reject) => {
try {
const data = doThis();
// do something
resolve();
} catch (e) {
reject(e);
}
})
.then(data => console.log(data))
.catch(error => console.log(error));

在上面的例子中,我们在Promise 内使用了 try/catch 块。


但是,Promise本身会在其作用域内捕捉所有的错误(甚至是打字错误),而不需要 try/catch块。它确保在执行过程中抛出的所有异常都被获取并转换为被拒绝的 Promise。


new Promise((resolve, reject) => {
const data = doThis();
// do something
resolve()
})
.then(data => console.log(data))
.catch(error => console.log(error));

**注意:**在 Promise 块中使用 .catch() 块是至关重要的。否则,你的测试案例可能会失败,而且应用程序在生产阶段可能会崩溃。


3. 在 Promise 块内使用异步函数


Async/Await 是一种更高级的语法,用于处理同步代码中的多个Promise。当我们在一个函数声明前使用 async 关键字时,它会返回一个 Promise,我们可以使用 await 关键字来停止代码,直到我们正在等待的Promise解决或拒绝。


但是,当你把一个 Async 函数放在一个 Promise 块里面时,会有一些副作用。


假设我们想在Promise 块中做一个异步操作,所以使用了 async 关键字,但,不巧的是我们的代码抛出了一个错误。


这样,即使使用 catch() 块或在 try/catch 块内等待你的Promise,我们也不能立即处理这个错误。请看下面的例子。


// 此代码无法处理错误
new Promise(async () => {
throw new Error('message');
}).catch(e => console.log(e.message));

(async () => {
try {
await new Promise(async () => {
throw new Error('message');
});
} catch (e) {
console.log(e.message);
}
})();

当我在Promise块内遇到 async 函数时,我试图将 async 逻辑保持在 Promise 块之外,以保持其同步性。10次中有9次都能成功。


然而,在某些情况下,可能需要一个 async 函数。在这种情况下,也别无选择,只能用try/catch 块来手动管理。


new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
}).catch(e => console.log(e.message));


//using async/await
(async () => {
try {
await new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
});
} catch (e) {
console.log(e.message);
}
})();

4.在创建 Promise 后立即执行 Promise 块


至于下面的代码片断,如果我们把代码片断放在调用HTTP请求的地方,它就会被立即执行。


const myPromise = new Promise(resolve => {
// code to make HTTP request
resolve(result);
});

原因是这段代码被包裹在一个Promise构造函数中。然而,有些人可能会认为只有在执行myPromisethen方法之后才被触发。


然而,真相并非如此。相反,当一个Promise被创建时,回调被立即执行。


这意味着在建立 myPromise 之后到达下面一行时,HTTP请求很可能已经在运行,或者至少处于调度状态。


Promises 总是急于执行过程。


但是,如果希望以后再执行 Promises,应该怎么做?如果现在不想发出HTTP请求怎么办?是否有什么神奇的机制内置于 Promises 中,使我们能够做到这一点?


答案就是使用函数。函数是一种耗时的机制。只有当开发者明确地用 () 来调用它们时,它们才会执行。简单地定义一个函数还不能让我们得到什么。所以,让 Promise 变得懒惰的最有效方法是将其包裹在一个函数中!


const createMyPromise = () => new Promise(resolve => {
// HTTP request
resolve(result);
});

对于HTTP请求,Promise 构造函数和回调函数只有在函数被执行时才会被调用。所以现在我们有一个懒惰的Promise,只有在我们需要的时候才会执行。


5. 不一定使用 Promise.all() 方法


如果你已经工作多年,应该已经知道我在说什么了。如果有许多彼此不相关的 Promise,我们可以同时处理它们。


Promise 是并发的,但如你一个一个地等待它们,会太费时间,Promise.all()可以节省很多时间。



记住,Promise.all() 是我们的朋友



const { promisify } = require('util');
const sleep = promisify(setTimeout);

async function f1() {
await sleep(1000);
}

async function f2() {
await sleep(2000);
}

async function f3() {
await sleep(3000);
}


(async () => {
console.time('sequential');
await f1();
await f2();
await f3();
console.timeEnd('sequential');
})();

上述代码的执行时间约为 6 秒。但如果我们用 Promise.all() 代替它,将减少执行时间。


(async () => {
console.time('concurrent');
await Promise.all([f1(), f2(), f3()]);
console.timeEnd('concurrent');
})();

总结


在这篇文章中,我们讨论了使用 Promise 时常犯的五个错误。然而,可能还有很多简单的问题需要仔细解决。



作者:前端小智
链接:https://juejin.cn/post/7034661345148534815

收起阅读 »

没想到吧!这个可可爱爱的游戏居然是用 ECharts 实现的!

前言 echarts是一个很强大的图表库,除了我们常见的图表功能,echarts有一个自定义图形的功能,这个功能可以让我们很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的。 下面我们来一步步实现他。 1 在坐标系中画一只会动的小鸟 首先实例化一...
继续阅读 »

前言


echarts是一个很强大的图表库,除了我们常见的图表功能,echarts有一个自定义图形的功能,这个功能可以让我们很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的。


下面我们来一步步实现他。


1 在坐标系中画一只会动的小鸟


首先实例化一个echart容器,再从网上找一个像素小鸟的图片,将散点图的散点形状,用自定义图片的方式改为小鸟。


const myChart = echarts.init(document.getElementById('main'));
option = {
series: [
{
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};

myChart.setOption(option);

要让小鸟动起来,就需要给一个向右的速度和向下的加速度,并在每一帧的场景中刷新小鸟的位置。而小鸟向上飞的动作,则可以靠角度的旋转来实现,向上飞的触发条件设置为空格事件。


option = {
series: [
{
xAxis: {
show: false,
type: 'value',
min: 0,
max: 200,
},
yAxis: {
show: false,
min: 0,
max: 100
},
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};

// 设置速度和加速度
let a = 0.05;
let vh = 0;
let vw = 0.5

timer = setInterval(() => {
// 小鸟位置和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;

// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;

myChart.setOption(option);
}, 25);

效果如下


GIF1.gif


2 用自定义图形绘制障碍物


echarts自定义系列,渲染逻辑由开发者通过renderItem函数实现。该函数接收两个参数params和api,params包含了当前数据信息和坐标系的信息,api是一些开发者可调用的方法集合,常用的方法有:




  • api.value(...),意思是取出 dataItem 中的数值。例如 api.value(0) 表示取出当前 dataItem 中第一个维度的数值。




  • api.coord(...),意思是进行坐标转换计算。例如 var point = api.coord([api.value(0), api.value(1)]) 表示 dataItem 中的数值转换成坐标系上的点。




  • api.size(...), 可以得到坐标系上一段数值范围对应的长度。




  • api.style(...),可以获取到series.itemStyle 中定义的样式信息。




灵活使用上述api,就可以将用户传入的Data数据转换为自己想要的坐标系上的像素位置。


renderItem函数返回一个echarts中的graphic类,可以多种图形组合成你需要的形状,graphic类型。对于我们游戏中的障碍物只需要使用矩形即可绘制出来,我们使用到下面两个类。




  • type: group, 组合类,可以将多个图形类组合成一个图形,子类放在children中。




  • type: rect, 矩形类,通过定义矩形左上角坐标点,和矩形宽高确定图形。




// 数据项定义为[x坐标,下方水管上侧y坐标, 上方水管下侧y坐标]
data: [
[150, 50, 80],
...
]

renderItem: function (params, api) {
// 获取每个水管主体矩形的起始坐标点
let start1 = api.coord([api.value(0) - 10, api.value(1)]);
let start2 = api.coord([api.value(0) - 10, 100]);
// 获取两个水管头矩形的起始坐标点
let startHead1 = api.coord([api.value(0) - 12, api.value(1)]);
let startHead2 = api.coord([api.value(0) - 12, api.value(2) + 8])
// 水管头矩形的宽高
let headSize = api.size([24, 8])
// 水管头矩形的宽高
let rect = api.size([20, api.value(1)]);
let rect2 = api.size([20, 100 - api.value(2)]);
// 坐标系配置
const common = {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
}
// 水管形状
const rectShape = echarts.graphic.clipRectByRect(
{
x: start1[0],
y: start1[1],
width: rect[0],
height: rect[1]
},common
);
const rectShape2 = echarts.graphic.clipRectByRect(
{
x: start2[0],
y: start2[1],
width: rect2[0],
height: rect2[1]
},
common
)

// 水管头形状
const rectHeadShape = echarts.graphic.clipRectByRect(
{
x: startHead1[0],
y: startHead1[1],
width: headSize[0],
height: headSize[1]
},common
);

const rectHeadShape2 = echarts.graphic.clipRectByRect(
{
x: startHead2[0],
y: startHead2[1],
width: headSize[0],
height: headSize[1]
},common
);

// 返回一个group类,由四个矩形组成
return {
type: 'group',
children: [{
type: 'rect',
shape: rectShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}, {
type: 'rect',
shape: rectShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}]
};
},

颜色定义, 我们为了让水管具有光泽使用了echarts的线性渐变色对象。


itemStyle: {
// 渐变色对象
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [{
offset: 0, color: '#ddf38c' // 0% 处的颜色
}, {
offset: 1, color: '#587d2a' // 100% 处的颜色
}],
global: false // 缺省为 false
},
borderWidth: 3
},

另外,用一个for循环一次性随机出多个柱子的数据


function initObstacleData() {
// 添加minHeight防止空隙太小
let minHeight = 20;
let start = 150;
obstacleData = [];
for (let index = 0; index < 50; index++) {
const height = Math.random() * 30 + minHeight;
const obstacleStart = Math.random() * (90 - minHeight);
obstacleData.push(
[
start + 50 * index,
obstacleStart,
obstacleStart + height > 100 ? 100 : obstacleStart + height
]
)
}
}

再将背景用游戏图片填充,我们就将整个游戏场景,绘制完成:



3 进行碰撞检测


由于飞行轨迹和障碍物数据都很简单,所以我们可以将碰撞逻辑简化为小鸟图片的正方形中,我们判断右上和右下角是否进入了自定义图形的范围内。


对于特定坐标下的碰撞范围,因为柱子固定每格50坐标值一个,宽度也是固定的,所以,可碰撞的横坐标范围就可以简化为 (x / 50 % 1) < 0.6


在特定范围内,依据Math.floor(x / 50)获取到对应的数据,即可判断出两个边角坐标是否和柱子区域有重叠了。在动画帧中判断,如果重叠了,就停止动画播放,游戏结束。


// centerCoord为散点坐标点
function judgeCollision(centerCoord) {
if (centerCoord[1] < 0 || centerCoord[1] > 100) {
return false;
}
let coordList = [
[centerCoord[0] + 15, centerCoord[1] + 1],
[centerCoord[0] + 15, centerCoord[1] - 1],
]

for (let i = 0; i < 2; i++) {
const coord = coordList[i];
const index = coord[0] / 50;
if (index % 1 < 0.6 && obstacleData[Math.floor(index) - 3]) {
if (obstacleData[Math.floor(index) - 3][1] > coord[1] || obstacleData[Math.floor(index) - 3][2] < coord[1]) {
return false;
}
}
}
return false
}

function initAnimation() {
// 动画设置
timer = setInterval(() => {
// 小鸟速度和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;

// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;

// 碰撞判断
const result = judgeCollision(option.series[0].data[0])

if(result) { // 产生碰撞后结束动画
endAnimation();
}

myChart.setOption(option);
}, 25);
}

总结


echarts提供了强大的图形绘制自定义能力,要使用好这种能力,一定要理解好数据坐标点和像素坐标点之间的转换逻辑,这是将数据具象到画布上的重要一步。


运用好这个功能,再也不怕产品提出奇奇怪怪的图表需求。


作者:DevUI团队
链接:https://juejin.cn/post/7034290086111871007

收起阅读 »

最新·前端的工资分布情况 - 你拖后腿了吗?

前言要说我们工作最关心的东西肯定少不了这两个方向:我们前端开发的工资分布情况技术更新的风向今天我就和大家分享小生最近收集的一些数据。关于行业的平均薪资水平我们一定不要拿一些特例当成范例。最能反应行业的平均薪资的指标应该是正态分布的中间值。再说明一点:知乎和脉脉...
继续阅读 »



前言

要说我们工作最关心的东西肯定少不了这两个方向:

  1. 我们前端开发的工资分布情况
  2. 技术更新的风向

今天我就和大家分享小生最近收集的一些数据。

关于行业的平均薪资水平我们一定不要拿一些特例当成范例。最能反应行业的平均薪资的指标应该是正态分布的中间值

再说明一点:

知乎和脉脉上的薪资水平比整体偏高,不建议作为依据。

总体分布情况

我们先看一下每个工作年限对应的平均工资是多少(这里只收集了北上广深杭五个城市)

工作年限应届生1-3年3-5年5年+可信度
北京9.5K13.7K19.5K25.9K较高
上海8.8K12.9K17.6K22.5K较高
深圳9K12K15.9K21.8K较高
杭州8.9K11.7K15.8K20K存疑
广州7.9K10.1K13.6K17.8K不高

数据来源: http://www.jobui.com/salary/?cit…

说明:

  • 应届生的样本数相对来说少了很多,可信度不会很高。
  • 这里没有按学历区分,所以 985 高校的同学可能觉得偏低。

绝大多数人,3-5 年经验,薪资范围基本都在 15k - 20k。所以千万不要妄自菲薄。

北上广深杭平均工资

  • 这部分的数据来源于 - 职友集

全国

数据来源职友集

  • 全国 平均工资为: ¥12330(这个数据基本被一线城市平均了)
  • 分布最多的区间为:10K 到 15K

北京

数据来源职友集

  • 北京 平均工资为: ¥18770
  • 分布最多的区间为:20K 到 30K

上海

数据来源职友集

  • 上海 平均工资为: ¥16220
  • 分布最多的区间为:10K 到 15K

深圳

数据来源职友集

  • 深圳 平均工资为: ¥15090
  • 分布最多的区间为:10K 到 15K

广州

数据来源职友集

  • 广州 平均工资为: ¥11390(广州表示不服,竟然没有全国平均高?)
  • 分布最多的区间为:10K 到 15K

杭州

数据来源职友集

  • 杭州 平均工资为: ¥14350
  • 分布最多的区间为:10K 到 15K

后话

我也看了下看准网发布的数据(不分地区的平均工资达到了 ¥19800)和一些其他来源的数据。结合个人的经验,认为这份数据还是比较客观的。

这次你是否又拖后腿了呢?

作者:小生方勤
来源:https://juejin.cn/post/6844904082193268749 收起阅读 »

不谈元宇宙的行业还有活路吗?

全文共计3063字,预计阅读时间7分钟作者 | 数据观综合(转载请注明来源)来源 | https://mp.weixin.qq.com/s/uE5gmbKxYKgu3ft5ZunnIA        ...
继续阅读 »

图片

全文共计3063字,预计阅读时间7分钟

作者 | 数据观综合(转载请注明来源)

来源 | https://mp.weixin.qq.com/s/uE5gmbKxYKgu3ft5ZunnIA


        从元宇宙第一股Roblox上市,到Facebook改名Meta,微软发布企业元宇宙,英伟达持续技术投入元宇宙,再从国内腾讯提出全真互联网,字节跳动布局VR赛道收购Pico,到网易注册元宇宙商标,元宇宙协会成立等,元宇宙可以说是现在最热门的话题和风口,没有之一。

同一个元宇宙,不同的“飙车方式”

        国内外各大科技企业短期内扎堆涌入元宇宙,虽然都是元宇宙,但是其入局模式却各不相同。
从目前的情况来看,各大科技企业主要还是依托其既有优势来布局元宇宙领域,主要可以分为三种模式:第一种是聚焦核心元器件和基础性平台领域,加快布局元宇宙硬件入口和操作系统。国外有英伟达、Meta、微软等国际数字科技巨头大手笔投资虚拟现实和增强现实技术,开发了适用于虚拟协作的平台、虚拟现实耳机等产品,国内有阿里、腾讯、字节跳动等头部互联网企业相继加注对元宇宙相关硬件的研发。
        第二种是聚焦商业模式与内容场景,探索元宇宙相关应用场景落地。国内外不少科技企业已经开启VR游戏的研发,国内的腾讯、三七互娱、完美世界、恒信东方等企业在此之前均已建立起了相关的产品业务线。
        第三种是政府推动企业入局模式,以韩国企业为主。韩国是全球推进元宇宙产业发展最为积极的国家之一,首尔在今年11月宣布成为首个加入元宇宙的城市政府;同时,韩国元宇宙产业的发展主要是相关政府部门牵头,引导和推动三星、现代汽车、LG等企业组成“元宇宙联盟”,形成企业在元宇宙领域的发展合力,以此推动实现更大范围的虚拟现实连接,并建立韩国国家级元宇宙发展平台。

行业的尽头是“元宇宙”?
        现在打开网页浏览,满屏都是元宇宙,科技企业布局元宇宙还算合理,但是随着资本的热捧,很多看似不沾边的公司也开始疯狂抢注元宇宙商标。
        老牌车企上汽集团在10月15日和10月18日一口气申请了100个“元宇宙”相关的商标类别,其申请的元宇宙商标名称均为“车元宇宙”、“车元宇宙Z-UNIVERSE”,使用范围囊括了汽车研发、生产、制造和销售等方方面面。
        虽然上汽集团动作大,但是动作还不算快,国内最早申请元宇宙商标的车企是理想汽车。理想在9月15日就开始行动,申请的商标名为“理想元宇宙”,主要用于电动汽车、自动驾驶汽车、跑车、房车和公共汽车交通工具。
        随后,小鹏和蔚来也开始大谈元宇宙概念。小鹏汽车申请注册了多项“小鹏元宇宙”商标,国际分类涉及运输工具、机械设备、科学仪器;上海蔚来汽车有限公司也申请注册多个“蔚来元宇宙”“蔚宇宙”商标,国际分类涉及运输工具、网站服务等……
        如果说车企内刮起元宇宙的风是因为智能驾驶、智能车联网等行业发展的需要,还能理解,那“白酒元宇宙”“火腿肠元宇宙”的出现,多少有点蹭热点、炒概念的嫌疑。
今年以来(截至11月23日)被申请的“元宇宙”商标中,涉及第33类酒类商标达23件。除了“白酒元宇宙”,“火腿肠元宇宙”可能离我们也不远了。天眼查显示,近日,河南双汇投资发展股份有限公司申请注册“原生宇宙”商标。
        更令人匪夷所思的还有虚拟土地的拍卖。前不久,在虚拟世界平台Decentraland里,一块数字土地被卖出243万美元的高价,这个价格甚至已经高于现实中美国曼哈顿的平均单套房价,更远高于其他美国行政区和旧金山的单套房价。
        就目前全球对元宇宙的态度来看,元宇宙已经不只是“风口”这么简单,说是“疯口”也不为过。

看看“反元派”怎么说
        在追捧元宇宙的人眼里,元宇宙是“香饽饽”,而在反对的人眼里,元宇宙就是概念炒作,是无中生有。擅长构建虚拟宇宙世界的《三体》的作者刘慈欣,近日在公开演讲中怒怼元宇宙,称“元宇宙将引导人类走向死路。”
        刘慈欣说,“人类的未来,要么是走向星际文明,要么就是常年沉迷在VR的虚拟世界中。如果人类在走向太空文明以前就实现了高度逼真的VR世界,这将是一场灾难。”
        互联网大佬周鸿祎近日也在节目中公开泼元宇宙的冷水。在11月20日晚播出的央视对话节目中,360集团创始人周鸿祎表示,元宇宙的概念最近炒得很热,很多人找到了新的圈钱手段。他认为,第一,元宇宙幻想的虚拟现实,还要假以时日;第二,Facebook的幻想不代表未来,而是代表人类的没落,“如果大家都生活在虚幻的空间里,它不会给人类社会带来真正的发展”。
        美国第一位功能性AR系统的开发者、科学家路易斯·罗森伯格(Louis Rosenberg),日前公开发表了一篇评论,对元宇宙的拥趸们提出了警告——元宇宙可能会改变我们所知道的现实结构。
“所谓元宇宙是一个沉浸式VR和AR的虚拟世界,目前是由Meta(原Facebook)等科技公司领头开发的,可以创造一个看起来很真实的赛博朋克乌托邦。”罗森博格在评论中表示:“我担心这些意图控制基础设施的强大平台供应商,未来是否在意对增强现实的合法使用权。”
        马萨诸塞州大学的数字基础设施项目负责人扎克曼(Ethan Zuckerman),近期也公开发表了一篇专栏文章,怒怼扎克伯克的Meta及元宇宙计划。“Facebook(Meta)推崇的元宇宙,只是让我们从它试图建立的一个(商业)世界中无法自拔。”
        清华大学新媒体研究中心执行主任沈阳表示,有很多公司声称在做元宇宙,实际上是泡沫,很多只是原来移动互联网的产品,谈不上是“元宇宙”。
        “其实,泡沫已经十分明显,歪瓜裂枣的项目特别多。”GGV纪源资本执行董事罗超罗超并不避讳,“元宇宙是个包罗万象的概念,这个词的实际意义不大。现在套元宇宙概念,其实什么都没有的项目很多很多。”
        有些投资人甚至说“只要创业BP(商业计划书)里有元宇宙三个字就不看了。

避免“速生速死”,元宇宙的技术突破要赶上
        技术局限性是当前元宇宙发展所面临的最大瓶颈。相关技术的成熟度,距离元宇宙落地应用的需求仍有一定差距,因此现有元宇宙技术展示场景多为实验性、局部性以及有限性的应用,并且展示成本非常高昂。如今年4月英伟达在其发布会上展示的15秒“数字黄仁勋”,就投入了近50位研发人员,共制作了21个版本。
        作为一种多项数字技术的综合集成应用,元宇宙场景从概念到真正落地需要实现两个技术突破:第一个是XR、数字孪生、区块链、人工智能等单项技术的突破,从不同维度实现立体视觉、深度沉浸、虚拟分身等元宇宙应用的基础功能;第二个突破是多项数字技术的综合应用突破,通过多技术的叠加兼容、交互融合,凝聚形成技术合力推动元宇宙稳定有序发展。
        另外,数据也在元宇宙发展中发挥着关键性作用。一方面,元宇宙相关技术的发展和场景落地,对数据体量和维度将有更高的要求,需要收集用户更多的个人信息;另一方面,数据安全是元宇宙健康有序发展的重要前提。为平衡好数据使用和数据安全,需要前瞻性构建元宇宙相关数据的收集和使用规范。
        元宇宙热潮席卷而来,这场击鼓传花的游戏愈演愈烈,风越大我们越是应该擦亮眼睛,不要被“暴富大饼”收割“智商税”,也不要抱着“自己肯定不会是最后倒霉的那个”这种侥幸心理,想象力在线的同时理智可不要下线。
收起阅读 »

学会了axios封装,世界都是你的

项目中对axios进行二次封装随着前端技术的发展,网络请求这一块,越来越多的程序猿选择使用axios来实现网络请求。但是单纯的axios插件并不能满足我们日常的使用,因此我们使用时,需要根据项目实际的情况来对axios进行二次封装。接下来就我对axios的二次...
继续阅读 »



项目中对axios进行二次封装

随着前端技术的发展,网络请求这一块,越来越多的程序猿选择使用axios来实现网络请求。但是单纯的axios插件并不能满足我们日常的使用,因此我们使用时,需要根据项目实际的情况来对axios进行二次封装。

接下来就我对axios的二次封装详细的说说,主要包括请求之前、返回响应以及使用等。

「1、请求之前」

一般的接口都会有鉴权认证(token)之类的,因此在接口的请求头里面,我们需要带上token值以通过服务器的鉴权认证。但是如果每次请求的时候再去添加,不仅会大大的加大工作量,而且很容易出错。好在axios提供了拦截器机制,我们在请求的拦截器中可以添加token。

// 请求拦截
axios.interceptors.request.use((config) => {
//....省略代码
config.headers.x_access_token = token
return config
}, function (error) {
return Promise.reject(error)
})

当然请求拦截器中,除了处理添加token以外,还可以进行一些其他的处理,具体的根据实际需求进行处理。


「2、响应之后」

请求接口,并不是每一次请求都会成功。那么当接口请求失败的时候,我们又怎么处理呢?每次请求的时候处理?封装axios统一处理?我想一个稍微追求代码质量的码农,应该都会选择封装axios进行统一处理吧。axios不仅提供了请求的拦截器,其也提供了响应的拦截器。在此处,可以获取到服务器返回的状态码,然后根据状态码进行相对应的操作。

// 响应拦截
axios.interceptors.response.use(function (response) {
if (response.data.code === 401 ) {//用户token失效
  //清空用户信息
  sessionStorage.user = ''
  sessionStorage.token = ''
  window.location.href = '/';//返回登录页
  return Promise.reject(msg)//接口Promise返回错误状态,错误信息msg可有后端返回,也可以我们自己定义一个码--信息的关系。
}
if(response.status!==200||response.data.code!==200){//接口请求失败,具体根据实际情况判断
  message.error(msg);//提示错误信息
  return Promise.reject(msg)//接口Promise返回错误状态
}
return response
}, function (error) {
if (axios.isCancel(error)) {
  requestList.length = 0
  // store.dispatch('changeGlobalState', {loading: false})
  throw new axios.Cancel('cancel request')
} else {
  message.error('网络请求失败,请重试')
}
return Promise.reject(error)
})

当然响应拦截器同请求拦截器一样,还可以进行一些其他的处理,具体的根据实际需求进行处理。


「3、使用axios」

axios使用的时候一般有三种方式:

  • 执行get请求

axios.get('url',{
params:{},//接口参数
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
  • 执行post请求

axios.post('url',{
data:xxx//参数
},{
headers:xxxx,//请求头信息
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
  • axios API 通过相关配置传递给axios完成请求

axios({
method:'delete',
url:'xxx',
cache:false,
params:{id:123},
headers:xxx,
})
//------------------------------------------//
axios({
method: 'post',
url: '/user/12345',
data: {
  firstName: 'monkey',
  lastName: 'soft'
}
});

直接使用api的方式虽然简单,但是不同请求参数的名字不一样,在实际开发过程中很容易写错或者忽略,容易为开发造成不必要的时间损失。

前面两种方式虽然没有参数不一致的问题,但是使用的时候,太过于麻烦。那么怎么办呢?

前面两种虽然使用过于麻烦,但是仔细观察,是可以发现有一定的相似点,我们便可以基于这些相似点二次封装,形成适合我们使用的一个请求函数。直接上代码:

/*
*url:请求的url
*params:请求的参数
*config:请求时的header信息
*method:请求方法
*/
const request = function ({ url, params, config, method }) {
// 如果是get请求 需要拼接参数
let str = ''
if (method === 'get' && params) {
  Object.keys(params).forEach(item => {
    str += `${item}=${params[item]}&`
  })
}
return new Promise((resolve, reject) => {
  axios[method](str ? (url + '?' + str.substring(0, str.length - 1)) : url, params, Object.assign({}, config)).then(response => {
    resolve(response.data)
  }, err => {
    if (err.Cancel) {
    } else {
      reject(err)
    }
  }).catch(err => {
    reject(err)
  })
})
}

这样我们需要接口请求的时候,直接调用该函数就好了。不管什么方式请求,传参方式都一样。


作者:monkeysoft
来源:https://juejin.cn/post/6847009771606769677

收起阅读 »

大话WEB前端性能优化基本套路

前言前端性能优化这是一个老生常谈的话题,但是还是有很多人没有真正的重视起来,或者说还没有产生这种意识。当用户打开页面,首屏加载速度越慢,流失用户的概率就越大,在体验产品的时候性能和交互对用户的影响是最直接的,推广拉新是一门艺术,用户的留存是一门技术,拉进来留住...
继续阅读 »

前言

前端性能优化这是一个老生常谈的话题,但是还是有很多人没有真正的重视起来,或者说还没有产生这种意识。

当用户打开页面,首屏加载速度越慢,流失用户的概率就越大,在体验产品的时候性能和交互对用户的影响是最直接的,推广拉新是一门艺术,用户的留存是一门技术,拉进来留住用户,产品体验很关键,这里我以 美柚的页面为例子,用实例展开说明前端优化的基本套路(适合新手上车)。

WEB性能优化套路

基础套路1:减少资源体积

css

  • 压缩

  • 响应头GZIP

大话WEB前端性能优化基本套路_前端_02

js

  • 压缩

  • 响应头GZIP

大话WEB前端性能优化基本套路_优化_03html

  • 输出压缩

  • 响应头GZIP

    hhh

大话WEB前端性能优化基本套路_前端_04

图片

  • 压缩

  • 使用Webp格式

大话WEB前端性能优化基本套路_前端_05

cookie

  • 注意cookie体积,合理设置过期时间

基础套路2:控制请求数

js

  • 合并

css

  • 合并

图片

  • 合并

    事实上

大话WEB前端性能优化基本套路_前端_06

  • base64(常用图标:如logo等)

    hhh

大话WEB前端性能优化基本套路_前端_07

接口

  • 数量控制

  • 异步ajax

合理使用缓存机制

  • 浏览器缓存

js编码

  • Require.JS 按需加载

  • 异步加载js

  • lazyload图片

基础套路3:静态资源CDN

请求走CDN

  • html

  • p_w_picpath

  • js

  • css

综合套路

图片地址独立域名

  • 与业务不同域名可以减少请求头里不必要的cookie传输

提高渲染速度

  • js放到页面底部,body标签底部

  • css放到页面顶部,head标签里

代码

  • 代码优化:css/js/html

  • 预加载,如:分页预加载,快滚动到底部的时候以前加载下一页数据

拓展资料

性能辅助工具


看完上面的套路介绍

可能有人会说:我在前端界混了这么多年,这些我都知道,只不过我不想去做
我答: 知道做不到,等于不知道

也可能有人会说:压缩合并等这些操作好繁琐,因为懒,所以不做
我答: 现在前端构建工具都很强大,如:grunt、gulp、webpack,支持各种插件操作,还不知道就说明你OUT了


因为我主要负责后端相关工作,前端并不是我擅长的,但是平时也喜欢关注前端前沿技术,这里以我的视角和开发经验梳理出基本套路。

套路点到为止,具体实施可以通过拓展资料进行深入了解,如有疑义或者补充请留言怼。

感谢你的支持,我会继续努力!~

作者: SFLYQ

来源:https://blog.51cto.com/sflyq/1947541

收起阅读 »

WEB加载动画之彩条起伏动画

介绍 本期将带给大家一个简单的创意加载效果——彩条起伏加载。顾名思义,我们会通过scss来完成,将会制作做7个不同颜色的矩形,按不同的延迟不断的递减然后再反弹,循环往复。寓意是希望各位同学像这个加载动画一样,生活过的多姿多彩。 接下来,我们先来一睹为快吧: ...
继续阅读 »

介绍


本期将带给大家一个简单的创意加载效果——彩条起伏加载。顾名思义,我们会通过scss来完成,将会制作做7个不同颜色的矩形,按不同的延迟不断的递减然后再反弹,循环往复。寓意是希望各位同学像这个加载动画一样,生活过的多姿多彩。


接下来,我们先来一睹为快吧:


VID_20211124_211507.gif


感觉如何,其实这个动画的实现方案有很多,今天就用障眼法去实现它,希望给你打开书写css的新思路。


正文


1.彩条绘制


<div id="app">
<div class="loading">
<span>l</span>
<span>o</span>
<span>a</span>
<span>d</span>
<span>i</span>
<span>n</span>
<span>g</span>
</div>
</div>

结构非常的简单,我们将会在div#app让div.loading居中显示,然后在loading中平分各个距离,渲染不同的颜色。


@import url("https://fonts.googleapis.com/css?family=Baloo+Bhaijaan&display=swap");

#app{
width: 100%;
height: 100vh;
background-color: #fff;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}

.loading{
width: 350px;
height: 120px;
display: flex;
overflow: hidden;
span{
flex:1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: cursive;
font-weight: bold;
text-transform: uppercase;
font-family: "Baloo Bhaijaan", cursive;
color: white;
font-size: 48px;
position: relative;
box-sizing: border-box;
padding-top: 50px;
@for $i from 1 through 7 {
&:nth-child(#{$i}) {
background: linear-gradient(180deg, hsl($i * 20 , 60%, 50%) 0, hsl($i * 20 , 60%, 90%) 100%);
box-shadow: inset 0 15px 30px hsl($i * 20 , 60%, 50%);
text-shadow: 12px 12px 12px hsl($i * 20 , 60%, 30%);
border-left: 1px solid hsl($i * 20 , 60%, 80%);;
border-right: 1px solid hsl($i * 20 , 60%, 60%);;
}
}
}
}

为了,美观我们还引入了谷歌的一个字体,居中显示是在div#app用了弹性布局。


#app{
display: flex;
justify-content: center;
align-items: center;
}

这三句话目的就是完成元素在上下左右居中。


另外,我们用scss的一大好处就是体现了出来,遍历十分的方便,即**@for ifrom1through7这一句就是遍历了七遍,通过i from 1 through 7** 这一句就是遍历了七遍,通过i就可以拿到下标,还可以参与运算,我们的颜色值就是通过他配合hsl色盘(HSL即色相、饱和度、亮度)去完成的。当然,色盘有360度,我们只取一部分形成清新的渐变,如果整个色盘都平分的话这几个色值出入还是太大会感觉很脏。


微信截图_20211124221851.png


我们发现文字被设置了padding-top: 50px,原因就是一会要完成起伏的动画,上面的部分最先消失,我们为了保证这些字母能显示时间更长一些,就往下移了一些距离。


2.起伏动画


一开始我们说过这个要用障眼法去实现,所以我们这里不改变span的高度或者裁切他。


.loading{
span{
//...
&::after{
content: "";
display: block;
box-sizing: border-box;
position: absolute;
height: 100%;
top: 0;
left: -1px;
right: -1px;
background: linear-gradient(180deg, white 0, rgb(249, 249, 249) 100%);
animation: shorten 2.1s infinite ease-out;
}
@for $i from 1 through 7 {
// ...
&::after{
animation-delay: #{ $i * 0.08s};
}
}
}
}
}
@keyframes shorten {
12% { height: 10px; }
}

微信截图_20211124222854.png


看了刚才的scss代码可以发现,我们其实是通过一个绝对定位的伪类去遮挡了他,做了一个障眼法让人感觉他高度改变了,其实不然。


至于动画,就更容易了就只有一句,就是在初期某个阶段让他变化高度到10px,也就是遮挡块变小了,显示的高度就就多了,然后缓缓增大至整块,来完成起伏效果。另外,我依然通过遍历在其伪类中,给他们不同的延迟显得更有层次感。


讲到这里,我们的这个案例就书写完成了


结语


本次通过一个做加载创意动画的案例,向各位同学讲到了css如何弹性居中,scss的遍历,hsl色盘改变色值的方便之处以及障眼法的一种方式,希望大家会喜欢,多多支持哦~


作者:jsmask
链接:https://juejin.cn/post/7034304330878418980
收起阅读 »

flutter 线上apm监控 远程日志 emas_tlog

emas_tlog ali emas tlog 阿里巴巴flutter版本 远程日志 TLog 介绍 远程日志服务提供远程手机日志拉取功能,解决移动App线上异常排查困难的问题。 远程日志服务支持Android/iOS应用类型。 产品架构 1.移动App集成...
继续阅读 »

emas_tlog


ali emas tlog


阿里巴巴flutter版本 远程日志 TLog


介绍


远程日志服务提供远程手机日志拉取功能,解决移动App线上异常排查困难的问题。
远程日志服务支持Android/iOS应用类型。


产品架构


在这里插入图片描述
1.移动App集成SDK
2.远程日志服务通过心跳探测识别已安装App的移动终端,并进行日志拉取配置。
3.远程日志服务拉取指定移动终端App的用户日志,并对拉取任务进行管理。
4.远程日志服务查看已从终端设备拉取至控制台的用户日志。


官网地址:官网地址


本项目是根据官方来制作的flutter版本


快速开始


#####flutter配置:


  emas_tlog: ^0.0.1

初始化:


方法的声明:
static void init(String appKey,String appSecret,String rsaPublicKey,
String appKeyIos,String appSecretIos,String rsaPublicKeyIos,
{String androidChannel = "line",String userNick = "NoLogin",ApmLogType? type,bool debug = true}){
// xxxxxx
}
方法的调用:
EmasTlog.init("**", "**", "**",
"**","**","**",
androidChannel :"HEHE",userNick: "lalala2");

参数说明:
appKey
appSecret
rsaPublicKey
appKeyIosiOSemas.appKey
appSecretIosiOSemas.appSecret
rsaPublicKeyIosiOSappmonitor.tlog.rsaSecret
androidChannel: 渠道 (iOS指定 App Store
userNick: 用户昵称说明: 默认值NoLogin
type(*日志上传类型,注:iOS若不传,默认是I
debug(底层运行日志答应)true测试环境开启 false正式环境 关闭(iOS可不传)


*星号标记说明


V:可拉取所有级别的日志。(iOS无此类型)


D:可拉取DEBUG/INFO/WARN/ERROR级别的日志。


I:可拉取INFO/WARN/ERROR级别的日志。


W:可拉取WARN/ERROR级别的日志。


E:可拉取ERROR级别的日志。


日常使用:


方法的声明:
static void init(String appKey,String appSecret,String rsaPublicKey,
String appKeyIos,String appSecretIos,String rsaPublicKeyIos,
{String androidChannel = "line",String userNick = "NoLogin",ApmLogType? type,bool debug = true}){
// xxxxx
}

方法的调用:
EmasTlog.log(ApmLogType.I, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.V, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.W, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.E, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.D, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");
EmasTlog.log(ApmLogType.I, "tag2_1231231",module: "hehe2_flutter",tag: "tag_hehe2");

说明:
module为模块业务,可以为空


    EmasTlog.comment(); 主动上传日志
EmasTlog.updateNickName(name) 修改用户名(用于登录切换用户)

#####Android配置:
1、在根项目Android目录build.gradle配置如下代码



ext {
tlog = [
openUtdid : true
]
}


说明:
如果项目编译期报类似如下错误



Duplicate class com.ta.utdid2.a.a.a found in modules jetified-alicloud-android-utdid-2.5.1-proguard (com.aliyun.ams:alicloud-android-utdid:2.5.1-proguard) and jetified-utdid-1.5.2.1 (com.umeng.umsdk:utdid:1.5.2.1)


则代码需要调整为



ext {
tlog = [
openUtdid : false
]
}


配置展示:



ext {
tlog = [
openUtdid : true
]
}

buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
}
}


app AndroidManifest配置:



<manifest **
xmlns:tools="http://schemas.android.com/tools"
** >

<application
**
tools:replace="android:label">


说明需要 配置 tools:replace="android:label"
#####iOS 的配置说明
1、在Flutter项目的iOS端的Podfile中添加如下索引库地址:


# alicloud
source "https://github.com/CocoaPods/Specs.git"
source "https://github.com/aliyun/aliyun-specs.git"

2、在Flutter项目的iOS端的info.plist文件中添加如下代码:


<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_dartobservatory._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Main</string>

3、在iOS端项目Build Setting中,将Allow Non-modular Includes In Framework Modules设置为YES

收起阅读 »

学会这招,轻松优化webpack构建性能

webpack webpack 本质上是一个静态资源打包工具,静态资源打包是指 webpack 会将文件及其通过 import 、require 等方式引入的各项资源,处理成一个资源依赖关系图,也称为 chunk ,这些资源包括 js,css,jpg, 等等。...
继续阅读 »

webpack


webpack 本质上是一个静态资源打包工具,静态资源打包是指 webpack 会将文件及其通过 importrequire 等方式引入的各项资源,处理成一个资源依赖关系图,也称为 chunk ,这些资源包括 jscssjpg, 等等。


然后将这个 chunk 内的资源分别进行处理 ,如 less 编译成 csses6 编译成 es5 ,等等。这个处理过程就是打包,最终将这些处理后的文件输出,输出的文件集合便称为 bundle


bundle 分析


学会优化 webpack 构建性能等优化,我们需要先学会如何分析 bundle,通过对产出的分析,才能有针对性的对过程进行优化。


webpack 官方提供了一个非常好用的 bundle 可视化分析工具:webpack-bundle-analyzer。这个工具会将 bundle 处理一个可视化页面,呈现出资源的依赖关系和体积大小等信息。


这个工具的使用方式也很简单,这需要在通过 npm install webpack-bundle-analyzer yarn install webpack-bundle-analyzer 安装这个插件,然后在 webpack 配置文件的 plugins 配置项中加上这一行代码:


plugins: [
new BundleAnalyzerPlugin(),
]
复制代码

运行 webpack 打包后,会自动在 http://127.0.0.1:8888/ 打开一个可视化页面:


image.png


优化小妙招


接下来我们将会结合对 bundle 的分析,进行一些优化操作。


在讲解如何优化之前,我们需要明确 chunkbundle 的关系:chunk 是一组依赖关系的集合,它不单单指一个文件,可以包含一个或多个文件。而 bundlewebpack 打包的输出结果,它可以包含一个或多个 chunk。而 webpack 打包执行时会以一个个 chunk 进行处理,前端在加载 webpack 打包的资源时,也往往是以一个 chunk 为单位加载的(无论它是一个或多个文件)。


splitChunks


从可视化界面中我们可以看到,经过 webpack 打包后我们得到一个 app.bundle.js,这是个 bundle 中包含了我们项目的所有代码以及从 node_modules 中引入的依赖,而这个 bundle 中包含了项目内的所以依赖关系,因此这个 bundle 也是我们项目中唯一一个 chunk


那么我们在加载页面时,便是加载这一整个 chunk,即需要在页面初始时加载全部的代码。


splitChunks,是由 webpack 提供的插件,通过它能够允许我们自由的配置 chunk 的生成策略,当然也包括允许我们将一个巨大的 chunk 拆分成多个 chunk


在使用 splitChunks 之前我们先介绍一个重要的配置属性 cacheGroups(如果需要,可以在官方文档 splitChunks 中了解更多):


cacheGroups 配置提取 chunk 的方案。里面每一项代表一个提取 chunk 的方案。下面是 cacheGroups 每项中特有的选项:

  • test选项:用来匹配要提取的 chunk 的资源路径或名称,值是正则或函数。

  • name选项:生成的 chunk 名称。

  • chunks选项,决定要提取那些内容。

  • priority选项:方案的优先级,值越大表示提取 chunk 时优先采用此方案,默认值为0。

  • enforce选项:true/false。为true时,chunk 的大小和数量限制。
  • 接下来我们便通过实际的配置操作,将 node_modules 的内容提取成单独的 chunk,下面是我们的配置代码:


      optimization: {
    splitChunks: {
    cacheGroups: {
    vendors: {
    test: /[\\/]node_modules[\\/]/,
    name: 'vendors',
    chunks: 'all',
    enforce: true,
    priority: -3,
    },
    },
    },
    },

    配置完成后,重新运行 webpack ,可以看到,node_modules 相关的依赖关系被提取成一个单独的 chunk vendors.bundle.js,最终我们得到了两个 chunkvendors.bundle.jsapp.bundle.js


    image.png


    那么通过这样的chunk 提取,有什么好处呢?



    1. node_modules 下往往是在项目中不会的变化的第三方依赖,我们将这些固定不变的提取成单独的 chunk 处理,webpack 便可以将这个 chunk 进行一定的缓存策略,而不需要每次都做过多的处理,减少了性能消耗。

    2. 网页加载资源时不需要一次性加载太多的资源,可以通过不同 chunk 分批次加载,从而减少首屏加载的时间。


    除了这里介绍的对 node_modules 的处理外,在实际的项目中也可以根据需要对更多的资源采取这样的提取chunk 策略。



    externals + CDN


    通过对 bundle 的分析,我们不难发现:在我们输出的 bundlereact.development.jsreact-dom.development.js 以及 react-router.js 这三个文件特别的显眼。这表示这几个文件的体积在我们总的输出文件中占的比例特别大,那么有什么方法可以解决这些碍眼的家伙呢?


    当然有! 下面将要介绍的 external + CDN 策略,便可以很好的帮助我们做到这点。


    externalwebpack 的一个重要的配置项,顾名思义,它可以帮助我们将某些资源在 webpack 打包时将其剔除,不参与到资源的打包中。


    external 是一个有多项 key-value 组成的对象,它的的每一项属性表示不需要经过 webpack 打包的资源,key 表示的是我们需要排除在外的依赖名称,value 则告诉 webpack,需要从 window 对象的哪个属性获取到这些被排除在外的依赖。


    下面的代码就是将reactreact-domreact-router-dom 三个依赖不进行 webpack 打包的配置,它告诉 webpack,不将 reactreact-domreact-router-dom 打包进最终的输出中,需要用到这些依赖时从 window对象下的 ReactReactDOMReactRouterDOM 属性获取。


      externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    'react-router-dom': 'ReactRouterDOM',
    },
    复制代码

    那么这些被剔除的依赖,为什么可以从 window 对象获取到呢?答案就是 CDN !


    我们将这些剔除的依赖,通过 script 标签引入对应的 CDN 资源( CDN内容分发网络,我们可以将这些静态资源存储到 CDN 网络中,以便更快的获取资源)。


    这需要我们将引入这些资源的script 标签加在入口 HTML 文件中,这些加载进来的js文件,会将资源挂载在对应的 window 属性 ReactReactDOMReactRouterDOM上。


        <script src="https://cdn.staticfile.org/react/0.0.0-0c756fb-f7f79fd/cjs/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/0.0.0-0c756fb-f7f79fd/cjs/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/react-router-dom/0.0.0-experimental-ffd8c7d0/react-router-dom.development.js"></script>

    接下来看下通过 external + CDN 策略处理后,我们最终输出的bundle:


    image.png


    react.development.jsreact-dom.development.js 以及 react-router.js 这三个文件消失了!



    作者:Promise
    链接:https://juejin.cn/post/7034181462106570759

    收起阅读 »

    前端面试js高频手写大全(下)

    8. 手写call, apply, bind手写callFunction.prototype.myCall=function(context=window){  // 函数的方法,所以写在Fuction原型对象上 if(typeof this !==...
    继续阅读 »



    8. 手写call, apply, bind

    手写call

    Function.prototype.myCall=function(context=window){  // 函数的方法,所以写在Fuction原型对象上
    if(typeof this !=="function"){   // 这里if其实没必要,会自动抛出错误
       throw new Error("不是函数")
    }
    const obj=context||window   //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
    obj.fn=this      //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
    const arg=[...arguments].slice(1)   //第一个为obj所以删除,伪数组转为数组
    res=obj.fn(...arg)
    delete obj.fn   // 不删除会导致context属性越来越多
    return res
    }
    //用法:f.call(obj,arg1)
    function f(a,b){
    console.log(a+b)
    console.log(this.name)
    }
    let obj={
    name:1
    }
    f.myCall(obj,1,2) //否则this指向window

    obj.greet.call({name: 'Spike'}) //打出来的是 Spike

    手写apply(arguments[this, [参数1,参数2.....] ])

    Function.prototype.myApply=function(context){  // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
    let obj=context||window
    obj.fn=this
    const arg=arguments[1]||[]    //若有参数,得到的是数组
    let res=obj.fn(...arg)
    delete obj.fn
    return res
    }
    function f(a,b){
    console.log(a,b)
    console.log(this.name)
    }
    let obj={
    name:'张三'
    }
    f.myApply(obj,[1,2])  //arguments[1]

    手写bind

    this.value = 2
    var foo = {
    value: 1
    };
    var bar = function(name, age, school){
    console.log(name) // 'An'
    console.log(age) // 22
    console.log(school) // '家里蹲大学'
    }
    var result = bar.bind(foo, 'An') //预置了部分参数'An'
    result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中

    简单版本

    Function.prototype.bind = function(context, ...outerArgs) {
    var fn = this;
    return function(...innerArgs) {   //返回了一个函数,...rest为实际调用时传入的参数
    return fn.apply(context,[...outerArgs, ...innerArgs]); //返回改变了this的函数,
    //参数合并
    }
    }

    new失败的原因:

    例:

    // 声明一个上下文
    let thovino = {
    name: 'thovino'
    }

    // 声明一个构造函数
    let eat = function (food) {
    this.food = food
    console.log(`${this.name} eat ${this.food}`)
    }
    eat.prototype.sayFuncName = function () {
    console.log('func name : eat')
    }

    // bind一下
    let thovinoEat = eat.bind(thovino)
    let instance = new thovinoEat('orange')  //实际上orange放到了thovino里面
    console.log('instance:', instance) // {}

    生成的实例是个空对象

    new操作符执行时,我们的thovinoEat函数可以看作是这样:

    function thovinoEat (...innerArgs) {
    eat.call(thovino, ...outerArgs, ...innerArgs)
    }

    在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)时,这里的obj是new操作符自己创建的那个简单空对象{},但它其实并没有替换掉thovinoEat函数内部的那个上下文对象thovino。这已经超出了call的能力范围,因为这个时候要替换的已经不是thovinoEat函数内部的this指向,而应该是thovino对象。

    换句话说,我们希望的是new操作符将eat内的this指向操作符自己创建的那个空对象。但是实际上指向了thovinonew操作符的第三步动作并没有成功

    可new可继承版本

    Function.prototype.bind = function (context, ...outerArgs) {
    let that = this;

    function res (...innerArgs) {
        if (this instanceof res) {
            // new操作符执行时
            // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
            that.call(this, ...outerArgs, ...innerArgs)
        } else {
            // 普通bind
            that.call(context, ...outerArgs, ...innerArgs)
        }
        }
        res.prototype = this.prototype //!!!
        return res
    }

    9. 手动实现new

    new的过程文字描述:

    1. 创建一个空对象 obj;

    2. 将空对象的隐式原型(proto)指向构造函数的prototype。

    3. 使用 call 改变 this 的指向

    4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。

    function Person(name,age){
    this.name=name
    this.age=age
    }
    Person.prototype.sayHi=function(){
    console.log('Hi!我是'+this.name)
    }
    let p1=new Person('张三',18)

    ////手动实现new
    function create(){
    let obj={}
    //获取构造函数
    let fn=[].shift.call(arguments)  //将arguments对象提出来转化为数组,arguments并不是数组而是对象   !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果     或者let arg = [].slice.call(arguments,1)
    obj.__proto__=fn.prototype
    let res=fn.apply(obj,arguments)    //改变this指向,为实例添加方法和属性
    //确保返回的是一个对象(万一fn不是构造函数)
    return typeof res==='object'?res:obj
    }

    let p2=create(Person,'李四',19)
    p2.sayHi()

    细节:

    [].shift.call(arguments)  也可写成:
    let arg=[...arguments]
    let fn=arg.shift()  //使得arguments能调用数组方法,第一个参数为构造函数
    obj.__proto__=fn.prototype
    //改变this指向,为实例添加方法和属性
    let res=fn.apply(obj,arg)

    10. 手写promise(常考promise.all, promise.race)

    // Promise/A+ 规范规定的三种状态
    const STATUS = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected'
    }

    class MyPromise {
    // 构造函数接收一个执行回调
    constructor(executor) {
        this._status = STATUS.PENDING // Promise初始状态
        this._value = undefined // then回调的值
        this._resolveQueue = [] // resolve时触发的成功队列
        this._rejectQueue = [] // reject时触发的失败队列
       
    // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
    const resolve = value => {
        const run = () => {
            // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
            if (this._status === STATUS.PENDING) {
                this._status = STATUS.FULFILLED // 更改状态
                this._value = value // 储存当前值,用于then回调
               
                // 执行resolve回调
                while (this._resolveQueue.length) {
                    const callback = this._resolveQueue.shift()
                    callback(value)
                }
            }
        }
        //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
        setTimeout(run)
    }

    // 同 resolve
    const reject = value => {
        const run = () => {
            if (this._status === STATUS.PENDING) {
            this._status = STATUS.REJECTED
            this._value = value
           
            while (this._rejectQueue.length) {
                const callback = this._rejectQueue.shift()
                callback(value)
            }
        }
    }
        setTimeout(run)
    }

        // new Promise()时立即执行executor,并传入resolve和reject
        executor(resolve, reject)
    }

    // then方法,接收一个成功的回调和一个失败的回调
    function then(onFulfilled, onRejected) {
     // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
     typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
     typeof onRejected !== 'function' ? onRejected = error => error : null

     // then 返回一个新的promise
     return new MyPromise((resolve, reject) => {
       const resolveFn = value => {
         try {
           const x = onFulfilled(value)
           // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
           reject(error)
        }
      }
    }
    }

     const rejectFn = error => {
         try {
           const x = onRejected(error)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
           reject(error)
        }
      }

       switch (this._status) {
         case STATUS.PENDING:
           this._resolveQueue.push(resolveFn)
           this._rejectQueue.push(rejectFn)
           break;
         case STATUS.FULFILLED:
           resolveFn(this._value)
           break;
         case STATUS.REJECTED:
           rejectFn(this._value)
           break;
      }
    })
    }
    catch (rejectFn) {
     return this.then(undefined, rejectFn)
    }
    // promise.finally方法
    finally(callback) {
     return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
       MyPromise.resolve(callback()).then(() => error)
    })
    }

    // 静态resolve方法
    static resolve(value) {
         return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
    }

    // 静态reject方法
    static reject(error) {
         return new MyPromise((resolve, reject) => reject(error))
      }

    // 静态all方法
    static all(promiseArr) {
         let count = 0
         let result = []
         return new MyPromise((resolve, reject) =>       {
           if (!promiseArr.length) {
             return resolve(result)
          }
           promiseArr.forEach((p, i) => {
             MyPromise.resolve(p).then(value => {
               count++
               result[i] = value
               if (count === promiseArr.length) {
                 resolve(result)
              }
            }, error => {
               reject(error)
            })
          })
        })
      }

    // 静态race方法
    static race(promiseArr) {
         return new MyPromise((resolve, reject) => {
           promiseArr.forEach(p => {
             MyPromise.resolve(p).then(value => {
               resolve(value)
            }, error => {
               reject(error)
            })
          })
        })
      }
    }

    11. 手写原生AJAX

    步骤

    1. 创建 XMLHttpRequest 实例

    2. 发出 HTTP 请求

    3. 服务器返回 XML 格式的字符串

    4. JS 解析 XML,并更新局部页面

    不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON

    了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

    version 1.0:

    myButton.addEventListener('click', function () {
    ajax()
    })

    function ajax() {
    let xhr = new XMLHttpRequest() //实例化,以调用方法
    xhr.open('get', 'https://www.google.com') //参数2,url。参数三:异步
    xhr.onreadystatechange = () => { //每当 readyState 属性改变时,就会调用该函数。
      if (xhr.readyState === 4) { //XMLHttpRequest 代理当前所处状态。
        if (xhr.status >= 200 && xhr.status < 300) { //200-300请求成功
          let string = request.responseText
          //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
          let object = JSON.parse(string)
        }
      }
    }
    request.send() //用于实际发出 HTTP 请求。不带参数为GET请求
    }

    promise实现

    function ajax(url) {
     const p = new Promise((resolve, reject) => {
       let xhr = new XMLHttpRequest()
       xhr.open('get', url)
       xhr.onreadystatechange = () => {
         if (xhr.readyState == 4) {
           if (xhr.status >= 200 && xhr.status <= 300) {
             resolve(JSON.parse(xhr.responseText))
          } else {
             reject('请求出错')
          }
        }
      }
       xhr.send()  //发送hppt请求
    })
     return p
    }
    let url = '/data.json'
    ajax(url).then(res => console.log(res))
    .catch(reason => console.log(reason))

    12. 手写节流防抖函数

    函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。

    节流:连续触发事件但是在 n 秒中只执行一次函数

    例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。

    防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

    例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

    防抖的实现:

    function debounce(fn, delay) {
        if(typeof fn!=='function') {
           throw new TypeError('fn不是函数')
        }
        let timer; // 维护一个 timer
        return function () {
            var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
            var args = arguments;
            if (timer) {
               clearTimeout(timer);
            }
            timer = setTimeout(function () {
               fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
            }, delay);
        };
    }

    // 调用
    input1.addEventListener('keyup', debounce(() => {
    console.log(input1.value)
    }), 600)

    节流的实现:

    function throttle(fn, delay) {
     let timer;
     return function () {
       var _this = this;
       var args = arguments;
       if (timer) {
         return;
      }
       timer = setTimeout(function () {
         fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
         // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
         timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
      }, delay)
    }
    }

    div1.addEventListener('drag', throttle((e) => {
     console.log(e.offsetX, e.offsetY)
    }, 100))

    13. 手写Promise加载图片

    function getData(url) {
     return new Promise((resolve, reject) => {
       $.ajax({
         url,
         success(data) {
           resolve(data)
        },
         error(err) {
           reject(err)
        }
      })
    })
    }
    const url1 = './data1.json'
    const url2 = './data2.json'
    const url3 = './data3.json'
    getData(url1).then(data1 => {
     console.log(data1)
     return getData(url2)
    }).then(data2 => {
     console.log(data2)
     return getData(url3)
    }).then(data3 =>
     console.log(data3)
    ).catch(err =>
     console.error(err)
    )

    14. 函数实现一秒钟输出一个数

    (!!!这个题这两天字节校招面试被问到了,问var打印的什么,改为let为什么可以?
    有没有其他方法实现?我自己博客里都写了不用let的写法第二种方法,居然给忘了~~~白学了)

    ES6:用let块级作用域的原理实现

    for(let i=0;i<=10;i++){   //用var打印的都是11
    setTimeout(()=>{
       console.log(i);
    },1000*i)
    }

    不用let的写法: 原理是用立即执行函数创造一个块级作用域

    for(var i = 1; i <= 10; i++){
      (function (i) {
           setTimeout(function () {
               console.log(i);
          }, 1000 * i)
      })(i);
    }

    15. 创建10个标签,点击的时候弹出来对应的序号?

    var a
    for(let i=0;i<10;i++){
    a=document.createElement('a')
    a.innerHTML=i+'<br>'
    a.addEventListener('click',function(e){
        console.log(this)  //this为当前点击的<a>
        e.preventDefault()  //如果调用这个方法,默认事件行为将不再触发。
        //例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
        alert(i)
    })
    const d=document.querySelector('div')
    d.appendChild(a)  //append向一个已存在的元素追加该元素。
    }

    16. 实现事件订阅发布(eventBus)

    实现EventBus类,有 on off once trigger功能,分别对应绑定事件监听器,解绑,执行一次后解除事件绑定,触发事件监听器。 这个题目面字节和快手都问到了,最近忙,答案会在后续更新

    class EventBus {
       on(eventName, listener) {}
       off(eventName, listener) {}
       once(eventName, listener) {}
       trigger(eventName) {}
    }

    const e = new EventBus();
    // fn1 fn2
    e.on('e1', fn1)
    e.once('e1', fn2)
    e.trigger('e1') // fn1() fn2()
    e.trigger('e1') // fn1()
    e.off('e1', fn1)
    e.trigger('e1') // null

    实现:

          //声明类
         class EventBus {
           constructor() {
             this.eventList = {} //创建对象收集事件
          }
           //发布事件
           $on(eventName, fn) {
             //判断是否发布过事件名称? 添加发布 : 创建并添加发布
             this.eventList[eventName]
               ? this.eventList[eventName].push(fn)
              : (this.eventList[eventName] = [fn])
          }
           //订阅事件
           $emit(eventName) {
             if (!eventName) throw new Error('请传入事件名')
             //获取订阅传参
             const data = [...arguments].slice(1)
             if (this.eventList[eventName]) {
               this.eventList[eventName].forEach((i) => {
                 try {
                   i(...data) //轮询事件
                } catch (e) {
                   console.error(e + 'eventName:' + eventName) //收集执行时的报错
                }
              })
            }
          }
           //执行一次
           $once(eventName, fn) {
             const _this = this
             function onceHandle() {
               fn.apply(null, arguments)
               _this.$off(eventName, onceHandle) //执行成功后取消监听
            }
             this.$on(eventName, onceHandle)
          }
           //取消订阅
           $off(eventName, fn) {
             //不传入参数时取消全部订阅
             if (!arguments.length) {
               return (this.eventList = {})
            }
             //eventName传入的是数组时,取消多个订阅
             if (Array.isArray(eventName)) {
               return eventName.forEach((event) => {
                 this.$off(event, fn)
              })
            }
             //不传入fn时取消事件名下的所有队列
             if (arguments.length === 1 || !fn) {
               this.eventList[eventName] = []
            }
             //取消事件名下的fn
             this.eventList[eventName] = this.eventList[eventName].filter(
              (f) => f !== fn
            )
          }
        }
         const event = new EventBus()

         let b = function (v1, v2, v3) {
           console.log('b', v1, v2, v3)
        }
         let a = function () {
           console.log('a')
        }
         event.$once('test', a)
         event.$on('test', b)
         event.$emit('test', 1, 2, 3, 45, 123)
         event.$off(['test'], b)
         event.$emit('test', 1, 2, 3, 45, 123)

    参考:

    数组扁平化 https://juejin.im/post/5c971ee16fb9a070ce31b64e#heading-3

    函数柯里化 https://juejin.im/post/6844903882208837645

    节流防抖 https://www.jianshu.com/p/c8b...

    事件订阅发布实现 https://heznb.com/archives/js...

    浅拷贝深拷贝 https://segmentfault.com/a/11...

    作者:晚起的虫儿

    来源:https://segmentfault.com/a/1190000038910420

    收起阅读 »

    如何写 CSS 重置(RESET)样式?

    很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化! 最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS...
    继续阅读 »

    很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化!


    最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS创作体验。


    像其他CSS重置一样,在设计/化妆品方面,它是不赞成的。您可以将此重置用于任何项目,无论您想要哪种美学。


    在本教程中,我们将介绍我的自定义 CSS 重置。我们将深入研究每个规则,您将了解它的作用以及您可能想要使用它的原因!


    CSS 重置


    事不宜迟,这里是:


    /*
    1. Use a more-intuitive box-sizing model.
    */
    *, *::before, *::after {
    box-sizing: border-box;
    }
    /*
    2. Remove default margin
    */
    * {
    margin: 0;
    }
    /*
    3. Allow percentage-based heights in the application
    */
    html, body {
    height: 100%;
    }
    /*
    Typographic tweaks!
    4. Add accessible line-height
    5. Improve text rendering
    */
    body {
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    }
    /*
    6. Improve media defaults
    */
    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }
    /*
    7. Remove built-in form typography styles
    */
    input, button, textarea, select {
    font: inherit;
    }
    /*
    8. Avoid text overflows
    */
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }
    /*
    9. Create a root stacking context
    */
    #root, #__next {
    isolation: isolate;
    }


    它相对较短,但是这个小样式表中包含了很多东西。让我们开始吧!



    从历史上看,CSS重置的主要目标是确保浏览器之间的一致性,并撤消所有默认样式,从而创建一个空白的石板。我的CSS重置并没有真正做这些事情。




    如今,浏览器在布局或间距方面没有巨大的差异。总的来说,浏览器忠实地实现了CSS规范,并且事情的行为符合您的预期。因此,它不再是必要的了。




    我也不认为有必要剥离所有浏览器默认值。例如,我可能确实想要设置标签!我总是可以在各个项目风格中做出不同的设计决策,但我认为剥离常识性默认值是没有意义的。<em>``font-style: italic




    我的CSS重置可能不符合"CSS重置"的经典定义,但我正在采取这种创造性的自由。



    CSS盒子模型


    测验!通过可见的粉红色边框进行测量,假设未应用其他 CSS,则在以下方案中元素的宽度是多少?.box


    <style>
    .parent {
    width: 200px;
    }
    .box {
    width: 100%;
    border: 2px solid hotpink;
    padding: 20px;
    }
    </style>
    <div>
    <div></div>
    </div>

    我们的元素有.因为它的父级是200px宽,所以100%将解析为200px。.box``width: 100%


    但是它在哪里应用200px宽度? 默认情况下,它会将该大小应用于内容框


    如果您不熟悉,"内容框"是框模型中实际保存内容的矩形,位于边框和填充内:


    一个粉红色的盒子,里面有一个绿色的盒子。粉红色代表边框,绿色代表填充。在内部,一个黑色矩形被标记为"内容框"


    该声明会将 的内容框设置为 200px。填充将添加额外的40px(每侧20px)。边框添加最后一个 4px(每侧 2px)。当我们进行数学计算时,可见的粉红色矩形将是244px宽。width: 100%``.box


    当我们尝试将一个 244px 的框塞入一个 200px 宽的父级中时,它会溢出:


    image.png


    这种行为很奇怪,对吧?幸运的是,我们可以通过设置以下规则来更改它:


    *, *::before, *::after {
    box-sizing: border-box;
    }

    应用此规则后,百分比将基于边框进行解析。在上面的示例中,我们的粉红色框将为 200px,内部内容框将缩小到 156px(200px - 40px - 4px)。


    在我看来,这是一个必须的规则。 它使CSS更适合使用。


    我们使用通配符选择器 () 将其应用于所有元素和伪元素。与普遍的看法相反,这对性能来说并不坏*


    我在网上看到了一些建议,可以代替这样做:


    html {
    box-sizing: border-box;
    }
    *, *:before, *:after {
    box-sizing: inherit;
    }


     删除默认间距


    * {
    margin: 0;
    }

    浏览器围绕保证金做出常识性的假设。例如,默认情况下,将包含比段落更多的边距。h1


    这些假设在文字处理文档的上下文中是合理的,但对于现代 Web 应用程序而言,它们可能不准确。


    Margin是一个棘手的魔鬼,而且我经常发现自己希望元素默认情况下没有任何元素。所以我决定全部删除它。🔥


    如果/当我确实想为特定标签添加一些边距时,我可以在我的自定义项目样式中执行此操作。通配符选择器 () 具有极低的特异性,因此很容易覆盖此规则。*


    基于百分比的高度


    html, body {
    height: 100%;
    }

    你有没有试过在CSS中使用基于百分比的高度,却发现它似乎没有效果?


    下面是一个示例:


    image.png


    元素有,但元素根本不会增长!main``height: 100%


    这不起作用,因为在 Flow 布局(CSS 中的主要布局模式)中,并且操作的原则根本不同。元素的宽度是根据其父级计算的,但元素的高度是根据其子元素计算的。height``width


    这是一个复杂的主题,远远超出了本文的范围。我计划写一篇关于它的博客文章,但与此同时,你可以在我的CSS课程中了解它,CSS for JavaScript Developers。


    作为一个快速演示,在这里我们看到,当我们应用此规则时,我们的元素可以增长:main


    image.png


    如果你使用的是像 React 这样的 JS 框架,你可能还希望向这个规则添加第三个选择器:框架使用的根级元素。


    例如,在我的 Next.js 项目中,我按如下方式更新规则:


    html, body, #__next {
    height: 100%;
    }


    为什么不使用vh?


    您可能想知道:为什么要在基于百分比的高度上大惊小怪?为什么不改用该装置呢?vh


    问题是该单元在移动设备上无法正常工作; 将占用超过100%的屏幕空间,因为移动浏览器在浏览器UI来来去去的地方做那件事。vh``100vh


    将来,新的CSS单元将解决这个问题。在此之前,我继续使用基于百分比的高度。



    调整行高


    body {
    line-height: 1.5;
    }

    line-height控制段落中每行文本之间的垂直间距。默认值因浏览器而异,但往往在 1.2 左右。


    此无单位数字是基于字体大小的比率。它的功能就像设备一样。如果为 1.2,则每行将比元素的字体大小大 20%。em``line-height


    问题是:对于那些有阅读障碍的人来说,这些行挤得太紧,使得阅读起来更加困难。WCAG标准规定行高应至少为1.5


    现在,这个数字确实倾向于在标题和其他具有大类型的元素上产生相当大的行:


    image.png


    您可能希望在标题上覆盖此值。我的理解是,WCAG标准适用于"正文"文本,而不是标题。



    使用"计算"实现更智能的线高


    我一直在尝试一种管理行高的替代方法。在这里:


    * {
    line-height: calc(1em + 0.5rem);
    }

    这是一个非常高级的小片段,它超出了这篇博客文章的范围,但这里有一个快速的解释。



    字体平滑,抗锯齿


    body {
    -webkit-font-smoothing: antialiased;
    }

    好吧,所以这个有点争议。


    在 MacOS 电脑上,浏览器将默认使用"子像素抗锯齿"。这是一种旨在通过利用每个像素内的 R/G/B 灯使文本更易于阅读的技术。


    过去,这被视为可访问性的胜利,因为它提高了文本对比度。您可能已经读过一篇流行的博客文章停止"修复"字体平滑,该帖子主张反对切换到"抗锯齿"。


    问题是:那篇文章写于2012年,在高DPI"视网膜"显示时代之前。今天的像素要小得多,肉眼看不见。


    像素 LED 的物理排列也发生了变化。如果你在显微镜下看一台现代显示器,你不会再看到R/G/B线的有序网格了。


    在2018年发布的MacOS Mojave中 ,Apple在整个操作系统中禁用了子像素抗锯齿。我猜他们意识到它在现代硬件上弊大于利。


    令人困惑的是,像Chrome和Safari这样的MacOS浏览器仍然默认使用子像素抗锯齿。我们需要通过设置为 来显式关闭它。-webkit-font-smoothing``antialiased


    区别如下:


    image.png


    MacOS 是唯一使用子像素抗锯齿的操作系统,因此此规则对 Windows、Linux 或移动设备没有影响。如果您使用的是 MacOS 电脑,则可以尝试实时渲染:


    合理的媒体默认值


    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }

    所以这里有一件奇怪的事情:图像被认为是"内联"元素。这意味着它们应该在段落的中间使用,例如 或 。<em>``<strong>


    这与我大多数时候使用图像的方式不符。通常,我对待图像的方式与处理段落或标题或侧边栏的方式相同;它们是布局元素。


    但是,如果我们尝试在布局中使用内联元素,则会发生奇怪的事情。如果您曾经有过一个神秘的4px间隙,不是边距,填充或边框,那么它可能是浏览器添加的"内联魔术空间"。line-height


    通过默认设置所有图像,我们回避了整个类别的时髦问题。display: block


    我也设置了.这样做是为了防止大图像溢出,如果它们放置在不够宽而无法容纳它们的容器中。max-width: 100%


    大多数块级元素会自动增大/缩小以适应其父元素,但媒体元素是特殊的:它们被称为替换元素,并且它们不遵循相同的规则。<img>


    如果图像的"本机"大小为 800×600,则该元素的宽度也将为 800px,即使我们将其放入 500px 宽的父级中也是如此。<img>


    此规则将防止该图像超出其容器,这对我来说更像是更明智的默认行为。


    继承窗体控件的字体


    input, button, textarea, select {
    font: inherit;
    }

    如果我们想避免这种自动缩放行为,输入的字体大小需要至少为1rem / 16px。以下是解决此问题的一种方法:


    CSS
    input, button, textarea, select {
    font-size: 1rem;
    }

    这解决了自动变焦问题,但它是创可贴。让我们解决根本原因:表单输入不应该有自己的印刷风格!


    CSS
    input, button, textarea, select {
    font: inherit;
    }

    font是一种很少使用的速记,它设置了一堆与字体相关的属性,如 、 和 。通过将其设置为 ,我们指示这些元素与其周围环境中的排版相匹配。font-size``font-weight``font-family``inherit


    只要我们不为正文文本选择令人讨厌的小字体大小,就可以同时解决我们所有的问题。🎉


    自动换行


    CSS
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }

    在 CSS 中,如果没有足够的空间来容纳一行上的所有字符,文本将自动换行。


    默认情况下,该算法将寻找"软包装"机会;这些是算法可以拆分的字符。在英语中,唯一的软包装机会是空格和连字符,但这因语言而异。


    如果某行没有任何软换行机会,并且它不合适,则会导致文本溢出:


    image.png


    这可能会导致一些令人讨厌的布局问题。在这里,它添加了一个水平滚动条。在其他情况下,它可能会导致文本与其他元素重叠,或滑到图像/视频后面。


    该属性允许我们调整换行算法,并允许它在找不到软换行机会时使用硬换行:overflow-wrap


    image.png


    这两种解决方案都不完美,但至少硬包装不会弄乱布局!


    感谢Sophie Alpert提出类似的规则!她建议将其应用于所有元素,这可能是一个好主意,但不是我个人测试过的东西。


    您也可以尝试添加属性:hyphens


    p {
    overflow-wrap: break-word;
    hyphens: auto;
    }

    hyphens: auto使用连字符(在支持连字符的语言中)来指示硬换行。这也使得硬包装更加普遍。


    如果您有非常窄的文本列,这可能是值得的,但它也可能有点分散注意力。我选择不将其包含在重置中,但值得尝试!


    根堆叠上下文


    #root, #__next {
    isolation: isolate;
    }

    最后一个是可选的。通常只有当你使用像 React 这样的 JS 框架时才需要它。


    正如我们在"到底是什么,z-index??"中看到的那样,该属性允许我们创建新的堆叠上下文,而无需设置 .isolation``z-index


    这是有益的,因为它允许我们保证某些高优先级元素(模式,下拉列表,工具提示)将始终显示在应用程序中的其他元素之上。没有奇怪的堆叠上下文错误,没有z指数军备竞赛。


    您应该调整选择器以匹配您的框架。我们希望选择在其中呈现应用程序的顶级元素。例如,create-react-app 使用 一个 ,因此正确的选择器是 。<div id="root">``#root


    最终成品


    下面再次以精简的复制友好格式进行 CSS 重置:


    /*
    Josh's Custom CSS Reset
    https://www.joshwcomeau.com/css/custom-css-reset/
    */
    *, *::before, *::after {
    box-sizing: border-box;
    }
    * {
    margin: 0;
    }
    html, body {
    height: 100%;
    }
    body {
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    }
    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }
    input, button, textarea, select {
    font: inherit;
    }
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }
    #root, #__next {
    isolation: isolate;
    }
    ```
    ```
    /*
    Josh's Custom CSS Reset
    https://www.joshwcomeau.com/css/custom-css-reset/
    */
    *, *::before, *::after {
    box-sizing: border-box;
    }
    * {
    margin: 0;
    }
    html, body {
    height: 100%;
    }
    body {
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    }
    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }
    input, button, textarea, select {
    font: inherit;
    }
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }
    #root, #__next {
    isolation: isolate;
    }
    ```
    `




















    作者:非优秀程序员
    链接:https://juejin.cn/post/7034308682825351176

    收起阅读 »

    前端面试js高频手写大全(上)

    在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。编程题主要分为这几种类型:* 算法题* 涉及js原理的题以及ajax请求* 业务场景题: 实现一个具有某种功能的组件* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别...
    继续阅读 »



    介绍

    在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。

    一般来说,如果代码写的好,即使理论知识答得不够清楚,也能有大概率通过面试。并且其实很多手写往往背后就考察了你对相关理论的认识。

    编程题主要分为这几种类型:

    * 算法题
    * 涉及js原理的题以及ajax请求
    * 业务场景题: 实现一个具有某种功能的组件
    * 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别用面向对象编程,面向过程编程,函数式编程实现把大象放进冰箱等等

    其中前两种类型所占比重最大。
    算法题建议养成每天刷一道leetcode的习惯,重点刷数据结构(栈,链表,队列,树),动态规划,DFS,BFS

    本文主要涵盖了第二种类型的各种重点手写。

    建议优先掌握

    • instanceof (考察对原型链的理解)

    • new (对创建对象实例过程的理解)

    • call&apply&bind (对this指向的理解)

    • 手写promise (对异步的理解)

    • 手写原生ajax (对ajax原理和http请求方式的理解,重点是get和post请求的实现)

    • 事件订阅发布 (高频考点)

    • 其他:数组,字符串的api的实现,难度相对较低。只要了解数组,字符串的常用方法的用法,现场就能写出来个大概。(ps:笔者认为数组的reduce方法比较难,这块有余力可以单独看一些,即使面试没让你实现reduce,写其他题时用上它也是很加分的)


    话不多说,直接开始

    1. 手写instanceof

    instanceof作用:

    判断一个实例是否是其父类或者祖先类型的实例。

    instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false

     let myInstanceof = (target,origin) => {
        while(target) {
            if(target.__proto__===origin.prototype) {
               return true
            }
            target = target.__proto__
        }
        return false
    }
    let a = [1,2,3]
    console.log(myInstanceof(a,Array));  // true
    console.log(myInstanceof(a,Object));  // true

    2. 实现数组的map方法

    数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。

    用法:

    const a = [1, 2, 3, 4];
    const b = array1.map(x => x * 2);
    console.log(b);   // Array [2, 4, 6, 8]

    实现前,我们先看一下map方法的参数有哪些
    image.png
    map方法有两个参数,一个是操作数组元素的方法fn,一个是this指向(可选),其中使用fn时可以获取三个参数,实现时记得不要漏掉,这样才算完整实现嘛

    原生实现:

        // 实现
        Array.prototype.myMap = function(fn, thisValue) {
               let res = []
               thisValue = thisValue||[]
               let arr = this
               for(let i=0; i<arr.length; i++) {
                   res.push(fn.call(thisValue, arr[i],i,arr))   // 参数分别为this指向,当前数组项,当前索引,当前数组
              }
               return res
          }
           // 使用
           const a = [1,2,3];
           const b = a.myMap((a,index)=> {
                   return a+1;
              }
          )
           console.log(b)   // 输出 [2, 3, 4]

    3. reduce实现数组的map方法

    利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握

    Array.prototype.myMap = function(fn,thisValue){
        var res = [];
        thisValue = thisValue||[];
        this.reduce(function(pre,cur,index,arr){
            return res.push(fn.call(thisValue,cur,index,arr));
        },[]);
        return res;
    }

    var arr = [2,3,1,5];
    arr.myMap(function(item,index,arr){
    console.log(item,index,arr);
    })

    4. 手写数组的reduce方法

    reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

    参数:

    • callback(一个在数组中每一项上调用的函数,接受四个函数:)

      • previousValue(上一次调用回调函数时的返回值,或者初始值)

      • currentValue(当前正在处理的数组元素)

      • currentIndex(当前正在处理的数组元素下标)

      • array(调用reduce()方法的数组)

    • initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)

     function reduce(arr, cb, initialValue){
        var num = initValue == undefined? num = arr[0]: initValue;
        var i = initValue == undefined? 1: 0
        for (i; i< arr.length; i++){
           num = cb(num,arr[i],i)
        }
        return num
    }

    function fn(result, currentValue, index){
        return result + currentValue
    }

    var arr = [2,3,4,5]
    var b = reduce(arr, fn,10)
    var c = reduce(arr, fn)
    console.log(b)   // 24

    5. 数组扁平化

    数组扁平化就是把多维数组转化成一维数组

    1. es6提供的新方法 flat(depth)

    let a = [1,[2,3]]; 
    a.flat(); // [1,2,3]
    a.flat(1); //[1,2,3]

    其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。

    let a = [1,[2,3,[4,[5]]]]; 
    a.flat(Infinity); // [1,2,3,4,5] a是4维数组

    2. 利用cancat

    function flatten(arr) {
        var res = [];
        for (let i = 0, length = arr.length; i < length; i++) {
        if (Array.isArray(arr[i])) {
        res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
        //res.push(...flatten(arr[i])); //或者用扩展运算符
        } else {
            res.push(arr[i]);
          }
        }
        return res;
    }
    let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
    flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

    补充:指定deep的flat

    只需每次递归时将当前deep-1,若大于0,则可以继续展开

         function flat(arr, deep) {
           let res = []
           for(let i in arr) {
               if(Array.isArray(arr[i])&&deep) {
                   res = res.concat(flat(arr[i],deep-1))
              } else {
                   res.push(arr[i])
              }
          }
           return res
      }
       console.log(flat([12,[1,2,3],3,[2,4,[4,[3,4],2]]],1));

    6. 函数柯里化

    用的这里的方法 https://juejin.im/post/684490...

    柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

    当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

    有两种思路:

    1. 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数

    2. 在调用柯里化工具函数时,手动指定所需的参数个数

    将这两点结合一下,实现一个简单 curry 函数:

    /**
    * 将函数柯里化
    * @param fn   待柯里化的原函数
    * @param len   所需的参数个数,默认为原函数的形参个数
    */
    function curry(fn,len = fn.length) {
    return _curry.call(this,fn,len)
    }

    /**
    * 中转函数
    * @param fn   待柯里化的原函数
    * @param len   所需的参数个数
    * @param args 已接收的参数列表
    */
    function _curry(fn,len,...args) {
       return function (...params) {
            let _args = [...args,...params];
            if(_args.length >= len){
                return fn.apply(this,_args);
            }else{
             return _curry.call(this,fn,len,..._args)
            }
      }
    }

    我们来验证一下:

    let _fn = curry(function(a,b,c,d,e){
    console.log(a,b,c,d,e)
    });

    _fn(1,2,3,4,5);     // print: 1,2,3,4,5
    _fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
    _fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
    _fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

    我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。

    比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:

    直接看一下官网的例子:

    image.png

    接下来我们来思考,如何实现占位符的功能。

    对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。

    而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符

    使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。

    直接上代码:

    /**
    * @param fn           待柯里化的函数
    * @param length       需要的参数个数,默认为函数的形参个数
    * @param holder       占位符,默认当前柯里化函数
    * @return {Function}   柯里化后的函数
    */
    function curry(fn,length = fn.length,holder = curry){
    return _curry.call(this,fn,length,holder,[],[])
    }
    /**
    * 中转函数
    * @param fn           柯里化的原函数
    * @param length       原函数需要的参数个数
    * @param holder       接收的占位符
    * @param args         已接收的参数列表
    * @param holders       已接收的占位符位置列表
    * @return {Function}   继续柯里化的函数 或 最终结果
    */
    function _curry(fn,length,holder,args,holders){
    return function(..._args){
    //将参数复制一份,避免多次操作同一函数导致参数混乱
    let params = args.slice();
    //将占位符位置列表复制一份,新增加的占位符增加至此
    let _holders = holders.slice();
    //循环入参,追加参数 或 替换占位符
    _args.forEach((arg,i)=>{
    //真实参数 之前存在占位符 将占位符替换为真实参数
    if (arg !== holder && holders.length) {
        let index = holders.shift();
        _holders.splice(_holders.indexOf(index),1);
        params[index] = arg;
    }
    //真实参数 之前不存在占位符 将参数追加到参数列表中
    else if(arg !== holder && !holders.length){
        params.push(arg);
    }
    //传入的是占位符,之前不存在占位符 记录占位符的位置
    else if(arg === holder && !holders.length){
        params.push(arg);
        _holders.push(params.length - 1);
    }
    //传入的是占位符,之前存在占位符 删除原占位符位置
    else if(arg === holder && holders.length){
       holders.shift();
    }
    });
    // params 中前 length 条记录中不包含占位符,执行函数
    if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
    return fn.apply(this,params);
    }else{
    return _curry.call(this,fn,length,holder,params,_holders)
    }
    }
    }

    验证一下:;

    let fn = function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
    }

    let _ = {}; // 定义占位符
    let _fn = curry(fn,5,_); // 将函数柯里化,指定所需的参数个数,指定所需的占位符

    _fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
    _fn(_, 2, 3, 4, 5)(1);             // print: 1,2,3,4,5
    _fn(1, _, 3, 4, 5)(2);             // print: 1,2,3,4,5
    _fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
    _fn(1, _, _, 4)(_, 3)(2)(5);       // print: 1,2,3,4,5
    _fn(_, 2)(_, _, 4)(1)(3)(5);       // print: 1,2,3,4,5

    至此,我们已经完整实现了一个 curry 函数~~

    7. 浅拷贝和深拷贝的实现

    深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。

    浅拷贝和深拷贝的区别:

    浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,如果其中一个对象改变了引用类型的属性,就会影响到另一个对象。

    深拷贝:将一个对象从内存中完整的复制一份出来,从堆内存中开辟一个新区域存放。这样更改拷贝值就不影响旧的对象

    浅拷贝实现:

    方法一:

    function shallowCopy(target, origin){
       for(let item in origin) target[item] = origin[item];
       return target;
    }

    其他方法(内置api):

    1. Object.assign

    var obj={a:1,b:[1,2,3],c:function(){console.log('i am c')}}
    var tar={};
    Object.assign(tar,obj);

    当然这个方法只适合于对象类型,如果是数组可以使用slice和concat方法

    1. Array.prototype.slice

    var arr=[1,2,[3,4]];
    var newArr=arr.slice(0);
    1. Array.prototype.concat

    var arr=[1,2,[3,4]];
    var newArr=arr.concat();

    测试同上(assign用对象测试、slice concat用数组测试),结合浅拷贝深拷贝的概念来理解效果更佳

    深拷贝实现:

    方法一:

    转为json格式再解析
    const a = JSON.parse(JSON.stringify(b))

    方法二:

    // 实现深拷贝  递归
    function deepCopy(newObj,oldObj){
        for(var k in oldObj){
            let item=oldObj[k]
            // 判断是数组、对象、简单类型?
            if(item instanceof Array){
                newObj[k]=[]
                deepCopy(newObj[k],item)
            }else if(item instanceof Object){
                newObj[k]={}
                deepCopy(newObj[k],item)
            }else{  //简单数据类型,直接赋值
                newObj[k]=item
            }
        }
    }
    (未完待续……)

    作者:晚起的虫儿

    来源:https://segmentfault.com/a/1190000038910420







    收起阅读 »

    太震撼了!我把七大JS排序算法做成了可视化!!!太好玩了!

    前言大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。...
    继续阅读 »

    前言

    大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。例如冒泡排序是这样的


    冒泡排序2.gif

    实现思路

    想实现的效果

    从封面可以看到,无论是哪种算法,一开始都是第一张图,而最终目的是要变成第二张图的效果


    截屏2021-09-05 下午6.05.45.png

    截屏2021-09-05 下午6.06.03.png

    极坐标

    讲实现思路之前,我先给大家复习一下高中的一个知识——极坐标。哈哈,不知道还有几个人记得他呢?



    • O:极点,也就是原点
    • ρ:极径
    • θ:极径与X轴夹角
    • x = ρ * cosθ,因为x / ρ = cosθ
    • y = ρ * sinθ,因为y / ρ = sinθ

    截屏2021-09-05 下午6.26.31.png

    那我们想实现的结果,又跟极坐标有何关系呢?其实是有关系的,比如我现在有一个排序好的数组,他具有37个元素,那我们可以把这37个元素转化为极坐标中的37个点,怎么转呢?


    const arr = [
    0, 1, 2, 3, 4, 5, 6, 7, 8,
    9, 10, 11, 12, 13, 14, 15, 16, 17,
    18, 19, 20, 21, 22, 23, 24, 25, 26,
    27, 28, 29, 30, 31, 32, 33, 34, 35, 36
    ]

    我们可以这么转:



    • 元素对应的索引index * 10 -> 角度θ(为什么要乘10呢,因为要凑够360°嘛)
    • 元素对应的值arr[index] -> 极径ρ

    按照上面的规则来转的话,那我们就可以在极坐标上得到这37个点(在canvas中Y轴是由上往下的,下面这个图也是按canvas的,但是Y轴我还是画成正常方向,所以这个图其实是反的,但是是有原因的哈):


    (0 -> θ = 00°,ρ = 0) (1 -> θ = 10°,ρ = 1) (2 -> θ = 20°,ρ = 2) (3 -> θ = 30°,ρ = 3)
    (4 -> θ = 40°,ρ = 4) (5 -> θ = 50°,ρ = 5) (6 -> θ = 60°,ρ = 6) (7 -> θ = 70°,ρ = 7)
    (8 -> θ = 80°,ρ = 8) (9 -> θ = 90°,ρ = 9) (10 -> θ = 100°,ρ = 10) (11 -> θ = 110°,ρ = 11)
    (12 -> θ = 120°,ρ = 12) (13 -> θ = 130°,ρ = 13) (14 -> θ = 140°,ρ = 14) (15 -> θ = 150°,ρ = 15)
    (16 -> θ = 160°,ρ = 16) (17 -> θ = 170°,ρ = 17) (18 -> θ = 180°,ρ = 18) (19 -> θ = 190°,ρ = 19)
    (20 -> θ = 200°,ρ = 20) (21 -> θ = 210°,ρ = 21) (22 -> θ = 220°,ρ = 22) (23 -> θ = 230°,ρ = 23)
    (24 -> θ = 240°,ρ = 24) (25 -> θ = 250°,ρ = 25) (26 -> θ = 260°,ρ = 26) (27 -> θ = 270°,ρ = 27)
    (28 -> θ = 280°,ρ = 28) (29 -> θ = 290°,ρ = 29) (30 -> θ = 300°,ρ = 30) (31 -> θ = 310°,ρ = 31)
    (32 -> θ = 320°,ρ = 32) (33 -> θ = 330°,ρ = 33) (34 -> θ = 340°,ρ = 34) (35 -> θ = 350°,ρ = 35)
    (36 -> θ = 360°,ρ = 36)

    截屏2021-09-05 下午7.11.07.png

    有没有发现,跟咱们想实现的最终效果的轨迹很像呢?


    截屏2021-09-05 下午6.06.03.png

    随机打散

    那说完最终的效果,咱们来下想想如何一开始先把数组的各个元素打散在极坐标上呢?其实很简单,咱们可以先把生成一个乱序的数组,比如


    const arr = [
    25, 8, 32, 1, 19, 14, 0, 29, 17,
    6, 7, 26, 3, 30, 31, 16, 28, 15,
    24, 10, 21, 2, 9, 4, 35, 5, 36,
    33, 11, 27, 34, 22, 13, 18, 23, 12, 20
    ]

    然后还是用上面那个规则,去转换极坐标



    • 元素对应的索引index * 10 -> 角度θ(为什么要乘10呢,因为要凑够360°嘛)
    • 元素对应的值arr[index] -> 极径ρ

    那么我们可以的到这37个点,自然就可以实现打散的效果


    (25 -> θ = 00°,ρ = 25) (8 -> θ = 10°,ρ = 8) (32 -> θ = 20°,ρ = 32) (1 -> θ = 30°,ρ = 1)
    (19 -> θ = 40°,ρ = 19) (14 -> θ = 50°,ρ = 14) (0 -> θ = 60°,ρ = 0) (29 -> θ = 70°,ρ = 29)
    (17 -> θ = 80°,ρ = 17) (6 -> θ = 90°,ρ = 6) (7 -> θ = 100°,ρ = 7) (26 -> θ = 110°,ρ = 26)
    (3 -> θ = 120°,ρ = 3) (30 -> θ = 130°,ρ = 30) (31 -> θ = 140°,ρ = 31) (16 -> θ = 150°,ρ = 16)
    (28 -> θ = 160°,ρ = 28) (15 -> θ = 170°,ρ = 15) (24 -> θ = 180°,ρ = 24) (10 -> θ = 190°,ρ = 10)
    (21 -> θ = 200°,ρ = 21) (2 -> θ = 210°,ρ = 2) (9 -> θ = 220°,ρ = 9) (4 -> θ = 230°,ρ = 4)
    (35 -> θ = 240°,ρ = 35) (5 -> θ = 250°,ρ = 5) (36 -> θ = 260°,ρ = 36) (33 -> θ = 270°,ρ = 33)
    (11 -> θ = 280°,ρ = 11) (27 -> θ = 290°,ρ = 27) (34 -> θ = 300°,ρ = 34) (22 -> θ = 310°,ρ = 22)
    (13 -> θ = 320°,ρ = 13) (18 -> θ = 330°,ρ = 18) (23 -> θ = 340°,ρ = 23) (12 -> θ = 350°,ρ = 12)
    (20 -> θ = 360°,ρ = 20)

    截屏2021-09-05 下午7.32.17.png

    实现效果

    综上所述,咱们想实现效果,也就有了思路



    • 1、先生成一个乱序数组
    • 2、用canvas画布画出此乱序数组所有元素对应的极坐标对应的点
    • 3、对乱序数组进行排序
    • 4、排序过程中不断清空画布,并重画数组所有元素对应的极坐标对应的点
    • 5、直到排序完成,终止画布操作

    截屏2021-09-05 下午7.41.54.png

    开搞!!!

    咱们,做事情一定要有条有理才行,还记得上面说的步骤吗?



    • 1、先生成一个乱序数组
    • 2、用canvas画布画出此乱序数组所有元素对应的极坐标对应的点
    • 3、对乱序数组进行排序
    • 4、排序过程中不断清空画布,并重画数组所有元素对应的极坐标对应的点
    • 5、直到排序完成,终止画布操作

    咱们就按照这个步骤,来一步一步实现效果,兄弟们,冲啊!!!


    生成乱序数组

    咱们上面举的例子是37个元素,但是37个肯定是太少了,咱们搞多点吧,我搞了这么一个数组nums:我先生成一个0 - 179的有序数组,然后打乱,并塞进数组nums中,此操作我执行4次。为什么是0 - 179,因为0 - 179刚好有180个数字


    身位一个程序员,我肯定不可能自己手打这么多元素的啦。。来。。上代码


    let nums = []
    for (let i = 0; i < 4; i++) {
    // 生成一个 0 - 179的有序数组
    const arr = [...Array(180).keys()] // Array.keys()可以学一下,很有用
    const res = []
    while (arr.length) {
    // 打乱
    const randomIndex = Math.random() * arr.length - 1
    res.push(arr.splice(randomIndex, 1)[0])
    }
    nums = [...nums, ...res]
    }

    经过上面操作,也就是我的nums中拥有4 * 180 = 720个元素,nums中的元素都是0 - 179范围内的


    canvas画乱序数组

    画canvas之前,肯定要现在html页面上,编写一个canvas的节点,这里我宽度设置1000,高度也是1000,并且背景颜色是黑色


    <canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>

    上面看到了,极点(原点)是在坐标正中间的,但是canvas的初始原点是在画布的左上角,我们需要把canvas的原点移动到画布的正中间,那正中间的坐标是多少呢?还记得咱们宽高都是1000吗?那画布中心点坐标不就是(500, 500),咱们可以使用canvas的ctx.translate(500, 500)来移动中心点位置。因为咱们画的点都是白色的,所以咱们顺便把ctx.fillStyle设置为white



    有一点注意了哈,canvas里的Y轴是自上向下的,与常规的Y轴的相反的。



    截屏2021-09-05 下午8.55.39.png

    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    ctx.fillStyle = 'white' // 设置画画的颜色
    ctx.translate(500, 500) // 移动中心点到(500, 500)

    那到底该怎么画点呢?按照之前的,其实光计算出角度θ极径ρ是不够的,因为canvas画板不认这两个东西啊。。那canvas认啥呢,他只认(x, y),所以咱们只要通过角度θ极径ρ去算出(x, y),就好了,还记得前面极坐标的公式吗



    • x = ρ * cosθ,因为x / ρ = cosθ
    • y = ρ * sinθ,因为y / ρ = sinθ

    由于咱们是要铺散点是要铺出一个圆形来,那么一个圆形的角度是0° - 360°,但是我们不要360°,咱们只要0° - 359°,因为0°和360°是同一个直线。咱们一个直线上有一个度数就够了。所以咱们要求出0° - 359°每个角度所对应的cosθ和sinθ(这里咱们只算整数角度,不算小数角度)


    const CosandSin = []
    for (let i = 0; i < 360; i++) {
    const jiaodu = i / 180 * Math.PI
    CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
    }

    这时候又有新问题了,咱们一个圆上的整数角度只有0° - 359°360个整数角,但是nums中有720个元素啊,那怎么分配画布呢?很简单啊,一个角度上画2个元素,那不就刚好 2 * 360 = 720


    行,咱们废话不多说,开始画初始散点吧。咱们也知道咱们需要画720个点,对于这种多个相同的东西,咱们要多多使用面向对象这种编程思想


    // 单个长方形构造函数
    function Rect(x, y, width, height) {
    this.x = x // 坐标x
    this.y = y // 坐标y
    this.width = width // 长方形的宽
    this.height = height // 长方形的高
    }

    // 单个长方形的渲染函数
    Rect.prototype.draw = function () {
    ctx.beginPath() // 开始画一个
    ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
    ctx.closePath() // 结束画一个
    }

    const CosandSin = []
    for (let i = 0; i < 360; i++) {
    const jiaodu = i / 180 * Math.PI
    CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
    }

    function drawAll(arr) {
    const rects = [] // 用来存储720个长方形
    for (let i = 0; i < arr.length; i++) {
    const num = arr[i]
    const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
    const x = num * cos // x = ρ * cosθ
    const y = num * sin // y = ρ * sinθ
    rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
    }
    rects.forEach(rect => rect.draw()) // 遍历渲染
    }
    drawAll(nums) // 执行渲染函数

    来页面中看看效果吧。此时就完成了初始的散点渲染


    截屏2021-09-05 下午6.05.45.png

    边排序边重画

    其实很简单,就是排序一次,就清空画布,然后重新执行上面的渲染函数drawAll就行了。由于性能原因,我先把drawAll封装成一个Promise函数


    function drawAll(arr) {
    return new Promise((resolve) => {
    setTimeout(() => {
    ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
    const rects = [] // 用来存储720个长方形
    for (let i = 0; i < arr.length; i++) {
    const num = arr[i]
    const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
    const x = num * cos // x = ρ * cosθ
    const y = num * sin // y = ρ * sinθ
    rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
    }
    rects.forEach(rect => rect.draw()) // 遍历渲染
    resolve('draw success')
    }, 10)
    })
    }

    然后咱们拿一个排序算法例子来讲一讲,就拿个冒泡排序来讲吧


    async function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { //相邻元素两两对比
    var temp = arr[j + 1]; //元素交换
    arr[j + 1] = arr[j];
    arr[j] = temp;
    }
    }
    await drawAll(arr) // 一边排序一边重新画
    }
    return arr;
    }

    然后在页面里放一个按钮,用来执行开始排序


    <button id="btn">开始排序</button>

    document.getElementById('btn').onclick = function () {
    bubbleSort(nums)
    }

    效果如下,是不是很开心哈哈哈!!!


    冒泡排序gift.gif

    完整代码

    这是完整代码


    <canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>
    <button id="btn">开始排序</button>
    复制代码
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    ctx.fillStyle = 'white' // 设置画画的颜色
    ctx.translate(500, 500) // 移动中心点到(500, 500)

    let nums = []
    for (let i = 0; i < 4; i++) {
    // 生成一个 0 - 180的有序数组
    const arr = [...Array(180).keys()]
    const res = []
    while (arr.length) {
    // 打乱
    const randomIndex = Math.random() * arr.length - 1
    res.push(arr.splice(randomIndex, 1)[0])
    }
    nums = [...nums, ...res]
    }

    // 单个长方形构造函数
    function Rect(x, y, width, height) {
    this.x = x // 坐标x
    this.y = y // 坐标y
    this.width = width // 长方形的宽
    this.height = height // 长方形的高
    }

    // 单个长方形的渲染函数
    Rect.prototype.draw = function () {
    ctx.beginPath() // 开始画一个
    ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
    ctx.closePath() // 结束画一个
    }

    const CosandSin = []
    for (let i = 0; i < 360; i++) {
    const jiaodu = i / 180 * Math.PI
    CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
    }

    function drawAll(arr) {
    return new Promise((resolve) => {
    setTimeout(() => {
    ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
    const rects = [] // 用来存储720个长方形
    for (let i = 0; i < arr.length; i++) {
    const num = arr[i]
    const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
    const x = num * cos // x = ρ * cosθ
    const y = num * sin // y = ρ * sinθ
    rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
    }
    rects.forEach(rect => rect.draw()) // 遍历渲染
    resolve('draw success')
    }, 10)
    })
    }
    drawAll(nums) // 执行渲染函数

    async function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { //相邻元素两两对比
    var temp = arr[j + 1]; //元素交换
    arr[j + 1] = arr[j];
    arr[j] = temp;
    }
    }
    await drawAll(arr) // 一边排序一边重新画
    }
    return arr;
    }

    document.getElementById('btn').onclick = function () {
    bubbleSort(nums) // 点击执行
    }

    正片开始!!!

    首先说明,哈哈



    • 我是算法渣渣
    • 每种算法排序,动画都不一样
    • drawAll放在不同地方也可能有不同效果

    冒泡排序

    async function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { //相邻元素两两对比
    var temp = arr[j + 1]; //元素交换
    arr[j + 1] = arr[j];
    arr[j] = temp;
    }
    }
    await drawAll(arr) // 一边排序一边重新画
    }
    return arr;
    }

    document.getElementById('btn').onclick = function () {
    bubbleSort(nums) // 点击执行
    }

    冒泡排序gift.gif

    选择排序

    async function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
    minIndex = i;
    for (var j = i + 1; j < len; j++) {
    if (arr[j] < arr[minIndex]) { //寻找最小的数
    minIndex = j; //将最小数的索引保存
    }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
    await drawAll(arr)
    }
    return arr;
    }
    document.getElementById('btn').onclick = function () {
    selectionSort(nums)
    }

    选择排序gif.gif

    插入排序

    async function insertionSort(arr) {
    if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array') {
    for (var i = 1; i < arr.length; i++) {
    var key = arr[i];
    var j = i - 1;
    while (j >= 0 && arr[j] > key) {
    arr[j + 1] = arr[j];
    j--;
    }
    arr[j + 1] = key;
    await drawAll(arr)
    }
    return arr;
    } else {
    return 'arr is not an Array!';
    }
    }
    document.getElementById('btn').onclick = function () {
    insertionSort(nums)
    }

    插入排序gif.gif

    堆排序

    async function heapSort(array) {
    if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
    //建堆
    var heapSize = array.length, temp;
    for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
    heapify(array, i, heapSize);
    await drawAll(array)
    }

    //堆排序
    for (var j = heapSize - 1; j >= 1; j--) {
    temp = array[0];
    array[0] = array[j];
    array[j] = temp;
    heapify(array, 0, --heapSize);
    await drawAll(array)
    }
    return array;
    } else {
    return 'array is not an Array!';
    }
    }
    function heapify(arr, x, len) {
    if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array' && typeof x === 'number') {
    var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
    if (l < len && arr[l] > arr[largest]) {
    largest = l;
    }
    if (r < len && arr[r] > arr[largest]) {
    largest = r;
    }
    if (largest != x) {
    temp = arr[x];
    arr[x] = arr[largest];
    arr[largest] = temp;
    heapify(arr, largest, len);
    }
    } else {
    return 'arr is not an Array or x is not a number!';
    }
    }
    document.getElementById('btn').onclick = function () {
    heapSort(nums)
    }

    堆排序gif.gif

    快速排序

    async function quickSort(array, left, right) {
    drawAll(nums)
    if (Object.prototype.toString.call(array).slice(8, -1) === 'Array' && typeof left === 'number' && typeof right === 'number') {
    if (left < right) {
    var x = array[right], i = left - 1, temp;
    for (var j = left; j <= right; j++) {
    if (array[j] <= x) {
    i++;
    temp = array[i];
    array[i] = array[j];
    array[j] = temp;
    }
    }
    await drawAll(nums)
    await quickSort(array, left, i - 1);
    await quickSort(array, i + 1, right);
    await drawAll(nums)
    }
    return array;
    } else {
    return 'array is not an Array or left or right is not a number!';
    }
    }
    document.getElementById('btn').onclick = function () {
    quickSort(nums, 0, nums.length - 1)
    }

    快排gif.gif

    基数排序

    async function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    var counter = [];
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for (var j = 0; j < arr.length; j++) {
    var bucket = parseInt((arr[j] % mod) / dev);
    if (counter[bucket] == null) {
    counter[bucket] = [];
    }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for (var j = 0; j < counter.length; j++) {
    var value = null;
    if (counter[j] != null) {
    while ((value = counter[j].shift()) != null) {
    arr[pos++] = value;
    await drawAll(arr)
    }
    }
    }
    }
    return arr;
    }
    document.getElementById('btn').onclick = function () {
    radixSort(nums, 3)
    }

    基数排序gif.gif

    希尔排序

    async function shellSort(arr) {
    var len = arr.length,
    temp,
    gap = 1;
    while (gap < len / 5) { //动态定义间隔序列
    gap = gap * 5 + 1;
    }
    for (gap; gap > 0; gap = Math.floor(gap / 5)) {
    for (var i = gap; i < len; i++) {
    temp = arr[i];
    for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
    arr[j + gap] = arr[j];
    }
    arr[j + gap] = temp;
    await drawAll(arr)
    }
    }
    return arr;
    }
    document.getElementById('btn').onclick = function () {
    shellSort(nums)
    }

    基数排序gif.gif

    参考


    总结

    如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼


    image.png

    作者:Sunshine_Lin
    来源:https://juejin.cn/post/7004454008634998821

    收起阅读 »

    JavaScript复制内容到剪贴板

    最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。 常见方法 查了一下万能的Google,现在常见的方法主要是以下两种:第三方库:clipboard.js原生方法:document.execCommand()分...
    继续阅读 »

    最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。


    常见方法


    查了一下万能的Google,现在常见的方法主要是以下两种:

    • 第三方库:clipboard.js
    • 原生方法:document.execCommand()

    分别来看看这两种方法是如何使用的。


    clipboard.js


    这是clipboard的官网:clipboardjs.com/,看起来就是这么的简单。


    引用


    直接引用: <script src="dist/clipboard.min.js"></script>


    包: npm install clipboard --save ,然后 import Clipboard from 'clipboard';


    使用


    从输入框复制


    现在页面上有一个 <input> 标签,我们需要复制其中的内容,我们可以这样做:


    <input id="demoInput" value="hello world">
    <button class="btn" data-clipboard-target="#demoInput">点我复制</button>
    import Clipboard from 'clipboard';
    const btnCopy = new Clipboard('btn');

    注意到,在 <button> 标签中添加了一个 data-clipboard-target 属性,它的值是需要复制的 <input>id,顾名思义是从整个标签中复制内容。


    直接复制


    有的时候,我们并不希望从 <input> 中复制内容,仅仅是直接从变量中取值。如果在 Vue 中我们可以这样做:


    <button class="btn" :data-clipboard-text="copyValue">点我复制</button>
    import Clipboard from 'clipboard';
    const btnCopy = new Clipboard('btn');
    this.copyValue = 'hello world';

    事件


    有的时候我们需要在复制后做一些事情,这时候就需要回调函数的支持。


    在处理函数中加入以下代码:


    // 复制成功后执行的回调函数
    clipboard.on('success', function(e) {
    console.info('Action:', e.action); // 动作名称,比如:Action: copy
    console.info('Text:', e.text); // 内容,比如:Text:hello word
    console.info('Trigger:', e.trigger); // 触发元素:比如:<button :data-clipboard-text="copyValue">点我复制</button>
    e.clearSelection(); // 清除选中内容
    });

    // 复制失败后执行的回调函数
    clipboard.on('error', function(e) {
    console.error('Action:', e.action);
    console.error('Trigger:', e.trigger);
    });

    小结


    文档中还提到,如果在单页面中使用 clipboard ,为了使得生命周期管理更加的优雅,在使用完之后记得 btn.destroy() 销毁一下。


    clipboard 使用起来是不是很简单。但是,就为了一个 copy 功能就使用额外的第三方库是不是不够优雅,这时候该怎么办?那就用原生方法实现呗。


    document.execCommand()方法


    先看看这个方法在 MDN 上是怎么定义的:



    which allows one to run commands to manipulate the contents of the editable region.



    意思就是可以允许运行命令来操作可编辑区域的内容,注意,是可编辑区域


    定义



    bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)



    方法返回一个 Boolean 值,表示操作是否成功。

    • aCommandName :表示命令名称,比如: copycut 等(更多命令见命令);
    • aShowDefaultUI:是否展示用户界面,一般情况下都是 false
    • aValueArgument:有些命令需要额外的参数,一般用不到;

    兼容性


    这个方法在之前的兼容性其实是不太好的,但是好在现在已经基本兼容所有主流浏览器了,在移动端也可以使用。


    兼容性


    使用


    从输入框复制


    现在页面上有一个 <input> 标签,我们想要复制其中的内容,我们可以这样做:


    <input id="demoInput" value="hello world">
    <button id="btn">点我复制</button>
    const btn = document.querySelector('#btn');
    btn.addEventListener('click', () => {
    const input = document.querySelector('#demoInput');
    input.select();
    if (document.execCommand('copy')) {
    document.execCommand('copy');
    console.log('复制成功');
    }
    })

    其它地方复制


    有的时候页面上并没有 <input> 标签,我们可能需要从一个 <div> 中复制内容,或者直接复制变量。


    还记得在 execCommand() 方法的定义中提到,它只能操作可编辑区域,也就是意味着除了 <input><textarea> 这样的输入域以外,是无法使用这个方法的。


    这时候我们需要曲线救国。


    <button id="btn">点我复制</button>
    const btn = document.querySelector('#btn');
    btn.addEventListener('click',() => {
    const input = document.createElement('input');
    document.body.appendChild(input);
    input.setAttribute('value', '听说你想复制我');
    input.select();
    if (document.execCommand('copy')) {
    document.execCommand('copy');
    console.log('复制成功');
    }
    document.body.removeChild(input);
    })

    算是曲线救国成功了吧。在使用这个方法时,遇到了几个坑。


    遇到的坑


    在Chrome下调试的时候,这个方法时完美运行的。然后到了移动端调试的时候,坑就出来了。


    对,没错,就是你,ios。。。




    1. 点击复制时屏幕下方会出现白屏抖动,仔细看是拉起键盘又瞬间收起


      知道了抖动是由于什么产生的就比较好解决了。既然是拉起键盘,那就是聚焦到了输入域,那只要让输入域不可输入就好了,在代码中添加 input.setAttribute('readonly', 'readonly'); 使这个 <input> 是只读的,就不会拉起键盘了。




    2. 无法复制


      这个问题是由于 input.select() 在ios下并没有选中全部内容,我们需要使用另一个方法来选中内容,这个方法就是 input.setSelectionRange(0, input.value.length);




    完整代码如下:


    const btn = document.querySelector('#btn');
    btn.addEventListener('click',() => {
    const input = document.createElement('input');
    input.setAttribute('readonly', 'readonly');
    input.setAttribute('value', 'hello world');
    document.body.appendChild(input);
    input.setSelectionRange(0, 9999);
    if (document.execCommand('copy')) {
    document.execCommand('copy');
    console.log('复制成功');
    }
    document.body.removeChild(input);
    })


    作者:axuebin
    链接:https://juejin.cn/post/6844903567480848391

    收起阅读 »

    你怎么总是能写出两三千行的controller类?

    你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因: 长函数太多 类里面有特别多的字段和函数 量变引起质变,可能每个函数都很短小,但数量太多 1 程序的modularity 你思考过为什么你不会把all code写到...
    继续阅读 »

    你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因:



    • 长函数太多

    • 类里面有特别多的字段和函数


    量变引起质变,可能每个函数都很短小,但数量太多


    1 程序的modularity


    你思考过为什么你不会把all code写到一个文件?因为你的潜意识里明白:



    • 相同的功能模块无法复用

    • 复杂度远超出个人理解极限


    一个人理解的东西是有限的,在国内互联网敏捷开发环境下,更没有人能熟悉所有代码细节。


    解决复杂的最有效方案就是分而治之。所以,各种程序设计语言都有自己的模块划分(modularity)方案:



    • 从最初的按文件划分

    • 到后来使用OO按类划分


    开发者面对的不再是细节,而是模块,模块数量显然远比细节数量少,理解成本大大降低,开发效率也提高了,再也不用 996, 每天都能和妹纸多聊几句了。


    modularity,本质就是分解问题,其背后原因,就是个人理解能力有限。



    说这么多我都懂,那到底怎么把大类拆成小类?



    2 大类是怎么来的?


    2.1 职责不单一


    最容易产生大类的原因


    CR一段代码:


    该类持有大类的典型特征,包含一坨字段:这些字段都缺一不可吗?



    • userId、name、nickname等应该是一个用户的基本信息

    • email、phoneNumber 也算是和用户相关联


    很多应用都提供使用邮箱或手机号登录方式,所以,这些信息放在这里,也能理解



    • authorType,作者类型,表示作者是签约作者还是普通作者,签约作者可设置作品的付费信息,但普通作者无此权限

    • authorReviewStatus,作者审核状态,作者成为签约作者,需要有一个申请审核的过程,该状态字段就是审核状态

    • editorType,编辑类型,编辑可以是主编,也可以是小编,权限不同


    这还不是 User 类的全部。但只看这些内容就能看出问题:



    • 普通用户既不是作者,也不是编辑


    作者和编辑这些相关字段,对普通用户无意义



    • 对那些成为作者的用户,编辑的信息意义不大


    因为作者不能成为编辑。编辑也不会成为作者,作者信息对成为编辑的用户无意义


    总有一些信息对一部分人毫无意义,但对另一部分人又必需。出现该问题的症结在于只有“一个”用户类。


    普通用户、作者、编辑,三种不同角色,来自不同业务方,关心的是不同内容。仅因为它们都是同一系统的用户,就把它们都放到一个用户类,导致任何业务方的需求变动,都会反复修改该类,严重违反单一职责原则
    所以破题的关键就是职责拆分。


    虽然这是一个类,但它把不同角色关心的东西都放在一起,就愈发得臃肿了。


    只需将不同信息拆分即可:


    public class User {
    private long userId;
    private String name;
    private String nickname;
    private String email;
    private String phoneNumber;
    ...
    }

    public class Author {
    private long userId;
    private AuthorType authorType;
    private ReviewStatus authorReviewStatus;
    ...
    }

    public class Editor {
    private long userId;
    private EditorType editorType;
    ...
    }

    拆出 Author、Editor 两个类,将和作者、编辑相关的字段分别移至这两个类里。
    这俩类分别有个 userId 字段,用于关联该角色和具体用户。


    2.2 字段未分组


    有时觉得有些字段确实都属于某个类,结果就是,这个类还是很大。


    之前拆分后的新 User 类:


    public class User {
    private long userId;
    private String name;
    private String nickname;
    private String email;
    private String phoneNumber;
    ...
    }

    这些字段应该都算用户信息的一部分。但依然也不算是个小类,因为该类里的字段并不属于同一种类型的信息。
    如,userId、name、nickname算是用户的基本信息,而 email、phoneNumber 则属于用户的联系方式。


    需求角度看,基本信息是那种一旦确定一般就不变的内容,而联系方式则会根据实际情况调整,如绑定各种社交账号。把这些信息都放到一个类里面,类稳定程度就差点。


    据此,可将 User 类的字段分组:


    public class User {
    private long userId;
    private String name;
    private String nickname;
    private Contact contact;
    ...
    }

    public class Contact {
    private String email;
    private String phoneNumber;
    ...
    }

    引入一个 Contact 类(联系方式),把 email 和 phoneNumber 放了进去,后面再有任何关于联系方式的调整就都可以放在这个类里面。
    此次调整,把不同信息重新组合,但每个类都比原来要小。


    前后两次拆分到底有何不同?



    • 前面是根据职责,拆分出不同实体

    • 后面是将字段做了分组,用类把不同的信息分别封装


    大类拆解成小类,本质上是个设计工作,依据单一职责设计原则。


    若把大类都拆成小类,类的数量就会增多,那人们理解的成本是不是也会增加呢?
    这也是很多人不拆分大类的借口。


    各种程序设计语言中,本就有如包、命名空间等机制,将各种类组合在一起。在你不需要展开细节时,面对的是一个类的集合。
    再进一步,还有各种程序库把这些打包出来的东西再进一步打包,让我们只要面对简单的接口,而不必关心各种细节。


    软件正这样层层封装构建出来的。


    作者:JavaEdge在掘金
    链接:https://juejin.cn/post/7031442685152100360
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    大红大紫的 Golang 真的是后端开发中的万能药吗?

    前言 城外的人想进去,城里的人想出来。-- 钱钟书《围城》 随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Go...
    继续阅读 »

    前言



    城外的人想进去,城里的人想出来。-- 钱钟书《围城》



    随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Google 的 Golang(Go 语言,简称 Go)越来越受到软件工程师的欢迎和追捧,成为如今炙手可热的后端编程语言。在用 Golang 开发的软件项目列表中,有 Docker(容器技术)、Kubernetes(容器编排)这样的颠覆整个 IT 行业的明星级产品,也有像 Prometheus(监控系统)、Etcd(分布式存储)、InfluxDB(时序数据库)这样的强大实用的知名项目。当然,Go 语言的应用领域也绝不局限于容器和分布式系统。如今很多大型互联网企业在大量使用 Golang 构建后端 Web 应用,例如今日头条、京东、七牛云等;长期被 Python 统治的框架爬虫领域也因为简单而易用的爬虫框架 Colly 的崛起而不断受到 Golang 的挑战。Golang 已经成为了如今大多数软件工程师最想学习的编程语言。下图是 HackerRank 在 2020 年调查程序员技能的相关结果。


    hackerrank-survey-2020


    那么,**Go 语言真的是后端开发人员的救命良药呢?它是否能够有效提高程序员们的技术实力和开发效率,从而帮助他们在职场上更进一步呢?Go 语言真的值得我们花大量时间深入学习么?**本文将详细介绍 Golang 的语言特点以及它的优缺点和适用场景,带着上述几个疑问,为读者分析 Go 语言的各个方面,以帮助初入 IT 行业的程序员以及对 Go 感兴趣的开发者进一步了解这个热门语言。


    Golang 简介


    golang


    Golang 诞生于互联网巨头 Google,而这并不是一个巧合。我们都知道,Google 有一个 20% 做业余项目(Side Project)的企业文化,允许工程师们能够在轻松的环境下创造一些具有颠覆性创新的产品。而 Golang 也正是在这 20% 时间中不断孵化出来。Go 语言的创始者也是 IT 界内大名鼎鼎的行业领袖,包括 Unix 核心团队成员 Rob Pike、C 语言作者 Ken Thompson、V8 引擎核心贡献者 Robert Griesemer。Go 语言被大众所熟知还是源于容器技术 Docker 在 2014 年被开源后的爆发式发展。之后,Go 语言因为其简单的语法以及迅猛的编译速度受到大量开发者的追捧,也诞生了很多优秀的项目,例如 Kubernetes。


    Go 语言相对于其他传统热门编程语言来说,有很多优点,特别是其高效编译速度天然并发特性,让其成为快速开发分布式应用的首选语言。Go 语言是静态类型语言,也就是说 Go 语言跟 Java、C# 一样需要编译,而且有完备的类型系统,可以有效减少因类型不一致导致的代码质量问题。因此,Go 语言非常适合构建对稳定性灵活性均有要求的大型 IT 系统,这也是很多大型互联网公司用 Golang 重构老代码的重要原因:传统的静态 OOP 语言(例如 Java、C#)稳定性高但缺乏灵活性;而动态语言(例如 PHP、Python、Ruby、Node.js)灵活性强但缺乏稳定性。因此,“熊掌和鱼兼得” 的 Golang,受到开发者们的追捧是自然而然的事情,毕竟,“天下苦 Java/PHP/Python/Ruby 们久矣“。


    不过,Go 语言并不是没有缺点。用辩证法的思维方式可以推测,Golang 的一些突出特性将成为它的双刃剑。例如,Golang 语法简单的优势特点将限制它处理复杂问题的能力。尤其是 Go 语言缺乏泛型(Generics)的问题,导致它构建通用框架的复杂度大增。虽然这个突出问题在 2.0 版本很可能会有效解决,但这也反映出来明星编程语言也会有缺点。当然,Go 的缺点还不止于此,Go 语言使用者还会吐槽其啰嗦的错误处理方式(Error Handling)、缺少严格约束的鸭子类型(Duck Typing)、日期格式问题等。下面,我们将从 Golang 语言特点开始,由浅入深多维度深入分析 Golang 的优缺点以及项目适用场景。


    语言特点


    简洁的语法特征


    Go 语言的语法非常简单,至少在变量声明、结构体声明、函数定义等方面显得非常简洁。


    变量的声明不像 Java 或 C 那样啰嗦,在 Golang 中可以用 := 这个语法来声明新变量。例如下面这个例子,当你直接使用 := 来定义变量时,Go 会自动将赋值对象的类型声明为赋值来源的类型,这节省了大量的代码。


    func main() {
    valInt := 1 // 自动推断 int 类型
    valStr := "hello" // 自动推断为 string 类型
    valBool := false // 自动推断为 bool 类型
    }

    Golang 还有很多帮你节省代码的地方。你可以发现 Go 中不会强制要求用 new 这个关键词来生成某个类(Class)的新实例(Instance)。而且,对于公共和私有属性(变量和方法)的约定不再使用传统的 publicprivate 关键词,而是直接用属性变量首字母的大小写来区分。下面一些例子可以帮助读者理解这些特点。


    // 定义一个 struct 类
    type SomeClass struct {
    PublicVariable string // 公共变量
    privateVariable string // 私有变量
    }

    // 公共方法
    func (c *SomeClass) PublicMethod() (result string) {
    return "This can be called by external modules"
    }

    // 私有方法
    func (c *SomeClass) privateMethod() (result string) {
    return "This can only be called in SomeClass"
    }

    func main() {
    // 生成实例
    someInstance := SomeClass{
    PublicVariable: "hello",
    privateVariable: "world",
    }
    }

    如果你用 Java 来实现上述这个例子,可能会看到冗长的 .java 类文件,例如这样。


    // SomeClass.java
    public SomeClass {
    public String PublicVariable; // 公共变量
    private String privateVariable; // 私有变量

    // 构造函数
    public SomeClass(String val1, String val2) {
    this.PublicVariable = val1;
    this.privateVariable = val2;
    }

    // 公共方法
    public String PublicMethod() {
    return "This can be called by external modules";
    }

    // 私有方法
    public String privateMethod() {
    return "This can only be called in SomeClass";
    }
    }

    ...

    // Application.java
    public Application {
    public static void main(String[] args) {
    // 生成实例
    SomeClass someInstance = new SomeClass("hello", "world");
    }
    }

    可以看到,在 Java 代码中除了容易看花眼的多层花括号以外,还充斥着大量的 publicprivatestaticthis 等修饰用的关键词,显得异常啰嗦;而 Golang 代码中则靠简单的约定,例如首字母大小写,避免了很多重复性的修饰词。当然,Java 和 Go 在类型系统上还是有一些区别的,这也导致 Go 在处理复杂问题显得有些力不从心,这是后话,后面再讨论。总之,结论就是 Go 的语法在静态类型编程语言中非常简洁。


    内置并发编程


    Go 语言之所以成为分布式应用的首选,除了它性能强大以外,其最主要的原因就是它天然的并发编程。这个并发编程特性主要来自于 Golang 中的协程(Goroutine)和通道(Channel)。下面是使用协程的一个例子。


    func asyncTask() {
    fmt.Printf("This is an asynchronized task")
    }

    func syncTask() {
    fmt.Printf("This is a synchronized task")
    }

    func main() {
    go asyncTask() // 异步执行,不阻塞
    syncTask() // 同步执行,阻塞
    go asyncTask() // 等待前面 syncTask 完成之后,再异步执行,不阻塞
    }

    可以看到,关键词 go 加函数调用可以让其作为一个异步函数执行,不会阻塞后面的代码。而如果不加 go 关键词,则会被当成是同步代码执行。如果读者熟悉 JavaScript 中的 async/awaitPromise 语法,甚至是 Java、Python 中的多线程异步编程,你会发现它们跟 Go 异步编程的简单程度不是一个量级的!


    异步函数,也就是协程之间的通信可以用 Go 语言特有的通道来实现。下面是关于通道的一个例子。


    func longTask(signal chan int) {
    // 不带参数的 for
    // 相当于 while 循环
    for {
    // 接收 signal 通道传值
    v := <- signal

    // 如果接收值为 1,停止循环
    if v == 1 {
    break
    }

    time.Sleep(1 * Second)
    }
    }

    func main() {
    // 声明通道
    sig := make(chan int)

    // 异步调用 longTask
    go longTask(sig)

    // 等待 1 秒钟
    time.Sleep(1 * time.Second)

    // 向通道 sig 传值
    sig <- 1

    // 然后 longTask 会接收 sig 传值,终止循环
    }

    面向接口编程


    Go 语言不是严格的面向对象编程(OOP),它采用的是面向接口编程(IOP),是相对于 OOP 更先进的编程模式。作为 OOP 体系的一部分,IOP 更加强调规则和约束,以及接口类型方法的约定,从而让开发人员尽可能的关注更抽象的程序逻辑,而不是在更细节的实现方式上浪费时间。很多大型项目采用的都是 IOP 的编程模式。如果想了解更多面向接口编程,请查看 “码之道” 个人技术博客的往期文章《为什么说 TypeScript 是开发大型前端项目的必备语言》,其中有关于面向接口编程的详细讲解。


    Go 语言跟 TypeScript 一样,也是采用鸭子类型的方式来校验接口继承。下面这个例子可以描述 Go 语言的鸭子类型特性。


    // 定义 Animal 接口
    interface Animal {
    Eat() // 声明 Eat 方法
    Move() // 声明 Move 方法
    }

    // ==== 定义 Dog Start ====
    // 定义 Dog 类
    type Dog struct {
    }

    // 实现 Eat 方法
    func (d *Dog) Eat() {
    fmt.Printf("Eating bones")
    }

    // 实现 Move 方法
    func (d *Dog) Move() {
    fmt.Printf("Moving with four legs")
    }
    // ==== 定义 Dog End ====

    // ==== 定义 Human Start ====
    // 定义 Human 类
    type Human struct {
    }

    // 实现 Eat 方法
    func (h *Human) Eat() {
    fmt.Printf("Eating rice")
    }

    // 实现 Move 方法
    func (h *Human) Move() {
    fmt.Printf("Moving with two legs")
    }
    // ==== 定义 Human End ====

    可以看到,虽然 Go 语言可以定义接口,但跟 Java 不同的是,Go 语言中没有显示声明接口实现(Implementation)的关键词修饰语法。在 Go 语言中,如果要继承一个接口,你只需要在结构体中实现该接口声明的所有方法。这样,对于 Go 编译器来说你定义的类就相当于继承了该接口。在这个例子中,我们规定,只要既能吃(Eat)又能活动(Move)的东西就是动物(Animal)。而狗(Dog)和人(Human)恰巧都可以吃和动,因此它们都被算作动物。这种依靠实现方法匹配度的继承方式,就是鸭子类型:如果一个动物看起来像鸭子,叫起来也像鸭子,那它一定是鸭子。这种鸭子类型相对于传统 OOP 编程语言显得更灵活。但是,后面我们会讨论到,这种编程方式会带来一些麻烦。


    错误处理


    Go 语言的错误处理是臭名昭著的啰嗦。这里先给一个简单例子。


    package main

    import "fmt"

    func isValid(text string) (valid bool, err error){
    if text == "" {
    return false, error("text cannot be empty")
    }
    return text == "valid text", nil
    }

    func validateForm(form map[string]string) (res bool, err error) {
    for _, text := range form {
    valid, err := isValid(text)
    if err != nil {
    return false, err
    }
    if !valid {
    return false, nil
    }
    }
    return true, nil
    }

    func submitForm(form map[string]string) (err error) {
    if res, err := validateForm(form); err != nil || !res {
    return error("submit error")
    }
    fmt.Printf("submitted")
    return nil
    }

    func main() {
    form := map[string]string{
    "field1": "",
    "field2": "invalid text",
    "field2": "valid text",
    }
    if err := submitForm(form); err != nil {
    panic(err)
    }
    }

    虽然上面整个代码是虚构的,但可以从中看出,Go 代码中充斥着 if err := ...; err != nil { ... } 之类的错误判断语句。这是因为 Go 语言要求开发者自己管理错误,也就是在函数中的错误需要显式抛出来,否则 Go 程序不会做任何错误处理。因为 Go 没有传统编程语言的 try/catch 针对错误处理的语法,所以在错误管理上缺少灵活度,导致了 “err 满天飞” 的局面。


    不过,辩证法则告诉我们,这种做法也是有好处的。第一,它强制要求 Go 语言开发者从代码层面来规范错误的管理方式,这驱使开发者写出更健壮的代码;第二,这种显式返回错误的方式避免了 “try/catch 一把梭”,因为这种 “一时爽” 的做法很可能导致 Bug 无法准确定位,从而产生很多不可预测的问题;第三,由于没有 try/catch 的括号或额外的代码块,Go 程序代码整体看起来更清爽,可读性较强。


    其他


    Go 语言肯定还有很多其他特性,但笔者认为以上的特性是 Go 语言中比较有特色的,是区分度比较强的特性。Go 语言其他一些特性还包括但不限于如下内容。



    • 编译迅速

    • 跨平台

    • defer 延迟执行

    • select/case 通道选择

    • 直接编译成可执行程序

    • 非常规依赖管理(可以直接引用 Github 仓库作为依赖,例如 import "github.com/crawlab-team/go-trace"

    • 非常规日期格式(格式为 "2006-01-02 15:04:05",你没看错,据说这就是 Golang 的创始时间!)


    优缺点概述


    前面介绍了 Go 的很多语言特性,想必读者已经对 Golang 有了一些基本的了解。其中的一些语言特性也暗示了它相对于其他编程语言的优缺点。Go 语言虽然现在很火,在称赞并拥抱 Golang 的同时,不得不了解它的一些缺点。


    这里笔者不打算长篇大论的解析 Go 语言的优劣,而是将其中相关的一些事实列举出来,读者可以自行判断。以下是笔者总结的 Golang 语言特性的不完整优缺点对比列表。














































    特性 优点 缺点
    语法简单 提升开发效率,节省时间 难以处理一些复杂的工程问题
    天然支持并发 极大减少异步编程的难度,提高开发效率 不熟悉通道和协程的开发者会有一些学习成本
    类型系统
  • Go 语言是静态类型,相对于动态类型语言更稳定和可预测

  • IOP 鸭子类型比严格的 OOP 语言更简洁



    • 没有继承、抽象、静态、动态等特性

    • 缺少泛型,导致灵活性降低

    • 难以快速构建复杂通用的框架或工具


    错误处理 强制约束错误管理,避免 “try/catch 一把梭” 啰嗦的错误处理代码,充斥着 if err := ...
    编译迅速 这绝对是一个优点 怎么可能是缺点?
    非常规依赖管理

    • 可以直接引用发布到 Github 上的仓库作为模块依赖引用,省去了依赖托管的官方网站

    • 可以随时在 Github 上发布 Go 语言编写的第三方模块

    • 自由的依赖发布意味着 Golang 的生态发展将不受官方依赖托管网站的限制


    严重依赖 Github,在 Github 上搜索 Go 语言模块相对不精准
    非常规日期格式 按照 6-1-2-3-4-5(2006-01-02 15:04:05),相对来说比较好记 对于已经习惯了 yyyy-MM-dd HH:mm:ss 格式的开发者来说非常不习惯

    其实,每一个特性在某种情境下都有其相应的优势和劣势,不能一概而论。就像 Go 语言采用的静态类型和面向接口编程,既不缺少类型约束,也不像严格 OOP 那样冗长繁杂,是介于动态语言和传统静态类型 OOP 语言之间的现代编程语言。这个定位在提升 Golang 开发效率的同时,也阉割了不少必要 OOP 语法特性,从而缺乏快速构建通用工程框架的能力(这里不是说 Go 无法构建通用框架,而是它没有 Java、C# 这么容易)。另外,Go 语言 “奇葩” 的错误处理规范,让 Go 开发者们又爱又恨:可以开发出更健壮的应用,但同时也牺牲了一部分代码的简洁性。要知道,Go 语言的设计理念是为了 “大道至简”,因此才会在追求高性能的同时设计得尽可能简单。


    无可否认的是,Go 语言内置的并发支持是非常近年来非常创新的特性,这也是它被分布式系统广泛采用的重要原因。同时,它相对于动辄编译十几分钟的 Java 来说是非常快的。此外,Go 语言没有因为语法简单而牺牲了稳定性;相反,它从简单的约束规范了整个 Go 项目代码风格。因此,**“快”(Fast)、“简”(Concise)、“稳”(Robust)**是 Go 语言的设计目的。我们在对学习 Golang 的过程中不能无脑的接纳它的一切,而是应该根据它自身的特性判断在实际项目应用中的情况。


    适用场景


    经过前文关于 Golang 各个维度的讨论,我们可以得出结论:Go 语言并不是后端开发的万能药。在实际开发工作中,开发者应该避免在任何情况下无脑使用 Golang 作为后端开发语言。相反,工程师在决定技术选型之前应该全面了解候选技术(语言、框架或架构)的方方面面,包括候选技术与业务需求的切合度,与开发团队的融合度,以及其学习、开发、时间成本等因素。笔者在学习了包括前后端的一些编程语言之后,发现它们各自有各自的优势,也有相应的劣势。如果一门编程语言能广为人知,那它绝对不会是一门糟糕语言。因此,笔者不会断言 “XXX 是世界上最好的语言“,而是给读者分享个人关于特定应用场景下技术选型的思路。当然,本文是针对 Go 语言的技术文,接下来笔者将分享一下个人认为 Golang 最适合的应用场景。


    分布式应用


    Golang 是非常适合在分布式应用场景下开发的。分布式应用的主要目的是尽可能多的利用计算资源和网络带宽,以求最大化系统的整体性能和效率,其中重要的需求功能就是并发(Concurrency)。而 Go 是支持高并发异步编程方面的佼佼者。前面已经提到,Go 语言内置了协程(Goroutine)通道(Channel)两大并发特性,这使后端开发者进行异步编程变得非常容易。Golang 中还内置了sync,包含 Mutex(互斥锁)、WaitGroup(等待组)、Pool(临时对象池)等接口,帮助开发者在并发编程中能更安全的掌控 Go 程序的并发行为。Golang 还有很多分布式应用开发工具,例如分布式储存系统(Etcd、SeaweedFS)、RPC 库(gRPC、Thrift)、主流数据库 SDK(mongo-driver、gnorm、redigo)等。这些都可以帮助开发者有效的构建分布式应用。


    网络爬虫


    稍微了解网络爬虫的开发者应该会听说过 Scrapy,再不济也是 Python。市面上关于 Python 网络爬虫的技术书籍数不胜数,例如崔庆才的 《Python 3 网络开发实战》 和韦世东的《Python 3 网络爬虫宝典 用 Python 编写的高性能爬虫框架 Scrapy》,自发布以来一直是爬虫工程师的首选。


    不过,由于近期 Go 语言的迅速发展,越来越多的爬虫工程师注意到用 Golang 开发网路爬虫的巨大优势。其中,用 Go 语言编写的 Colly 爬虫框架,如今在 Github 上已经有 13k+ 标星。其简洁的 API 以及高效的采集速度,吸引了很多爬虫工程师,占据了爬虫界一哥 Scrapy 的部分份额。前面已经提到,Go 语言内置的并发特性让严重依赖网络带宽的爬虫程序更加高效,很大的提高了数据采集效率。另外,Go 语言作为静态语言,相对于动态语言 Python 来说有更好的约束下,因此健壮性和稳定性都更好。


    后端 API


    Golang 有很多优秀的后端框架,它们大部分都非常完备的支持了现代后端系统的各种功能需求:RESTful API、路由、中间件、配置、鉴权等模块。而且用 Golang 写的后端应用性能很高,通常有非常快的响应速度。笔者曾经在开源爬虫管理平台 Crawlab 中用 Golang 重构了 Python 的后端 API,响应速度从之前的几百毫秒优化到了几十毫秒甚至是几毫秒,用实践证明 Go 语言在后端性能方面全面碾压动态语言。Go 语言中比较知名的后端框架有 GinBeegoEchoIris


    当然,这里并不是说用 Golang 写后端就完全是一个正确的选择。笔者在工作中会用到 Java 和 C#,用了各自的主流框架(SpringBoot 和 .Net Core)之后,发现这两门传统 OOP 语言虽然语法啰嗦,但它们的语法特性很丰富,特别是泛型,能够轻松应对一些逻辑复杂、重复性高的业务需求。因此,笔者认为在考虑用 Go 来编写后端 API 时候,可以提前调研一下 Java 或 C#,它们在写后端业务功能方面做得非常棒。


    总结


    本篇文章从 Go 语言的主要语法特性入手,循序渐进分析了 Go 语言作为后端编程语言的优点和缺点,以及其在实际软件项目开发中的试用场景。笔者认为 Go 语言与其他语言的主要区别在于语法简洁天然支持并发面向接口编程错误处理等方面,并且对各个语言特性在正反两方面进行了分析。最后,笔者根据之前的分析内容,得出了 Go 语言作为后端开发编程语言的适用场景,也就是分布式应用网络爬虫以及后端API。当然,Go 语言的实际应用领域还不限于此。实际上,不少知名数据库都是用 Golang 开发的,例如时序数据库 Prometheus 和 InfluxDB、以及有 NewSQL 之称的 TiDB。此外,在机器学习方面,Go 语言也有一定的优势,只是目前来说,Google 因为 Swift 跟 TensorFlow 的意向合作,似乎还没有大力推广 Go 在机器学习方面的应用,不过一些潜在的开源项目已经涌现出来,例如 GoLearn、GoML、Gorgonia 等。


    在理解 Go 语言的优势和适用场景的同时,我们必须意识到 Go 语言并不是全能的。它相较于其他一些主流框架来说也有一些缺点。开发者在准备采用 Go 作为实际工作开发语言的时候,需要全面了解其语言特性,从而做出最合理的技术选型。就像打网球一样,不仅需要掌握正反手,还要会发球、高压球、截击球等技术动作,这样才能把网球打好。


    作者:MarvinZhang
    链接:https://juejin.cn/post/6943524790636544036
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    写给前端工程师的 Flutter 教程

    最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。 最爱跨端的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。 图啥? 低成本地为用户带来更优秀的用户...
    继续阅读 »

    最爱折腾的就是前端工程师了,从 jQuery 折腾到 AngularJs,再折腾到 Vue、React。 最爱跨端的也是前端工程师,从 phonegap,折腾到 React Native,这不又折腾到了 Flutter。


    图啥?


    低成本地为用户带来更优秀的用户体验


    目前来说Flutter可能是其中最优秀的一种方案了。


    Flutter 是什么?



    Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.



    Flutter是由原 Google Chrome 团队成员,利用 Chrome 2D 渲染引擎,然后精简 CSS 布局演变而来。


    Flutter 架构


    或者更详细的版本




    • Flutter 在各个原生的平台中,使用自己的 C++的引擎渲染界面,没有使用 webview,也不像 RN、NativeScript 一样使用系统的组件。简单来说平台只是给 Flutter 提供一个画布。

    • 界面使用 Dart 语言开发,貌似唯一支持 JIT,和 AOT 模式的强类型语言。

    • 写法非常的现代,声明式,组件化,Composition > inheritance,响应式……就是现在前端流行的这一套 😄

    • 一套代码搞定所有平台。


    Flutter 为什么快?Flutter 相比 RN 的优势在哪里?


    从架构中实际上已经能看出 Flutter 为什么快,至少相比之前的当红炸子鸡 React Native 快的原因了。



    • Skia 引擎,Chrome, Chrome OS,Android,Firefox,Firefox OS 都以此作为渲染引擎。

    • Dart 语言可以 AOT 编译成 ARM Code,让布局以及业务代码运行的最快,而且 Dart 的 GC 针对 Flutter 频繁销毁创建 Widget 做了专门的优化。

    • CSS 的的子集 Flex like 的布局方式,保留强大表现能力的同时,也保留了性能。

    • Flutter 业务书写的 Widget 在渲染之前 diff 转化成 Render Object,对,就像 React 中的 Virtual DOM,以此来确保开发体验和性能。


    而相比 React Native:



    • RN 使用 JavaScript 来运行业务代码,然后 JS Bridge 的方式调用平台相关组件,性能比有损失,甚至平台不同 js 引擎都不一样。

    • RN 使用平台组件,行为一致性会有打折,或者说,开发者需要处理更多平台相关的问题。


    而具体两者的性能测试,可以看这里,结论是 Flutter,在 CPU,FPS,内存稳定上均优于 ReactNative。


    Dart 语言


    在开始 Flutter 之前,我们需要先了解下 Dart 语言……


    Dart 是由 Google 开发,最初是想作为 JavaScript 替代语言,但是失败沉寂之后,作为 Flutter 独有开发语言又焕发了第二春 😂。


    实际上即使到了 2.0,Dart 语法和 JavaScriptFlutter非常的相像。单线程,Event Loop……


    Dart Event Loop模型


    当然作为一篇写给前端工程师的教程,我在这里只想写写 JavaScript 中暂时没有的,Dart 中更为省心,也更“甜”的东西。



    • 不会飘的this

    • 强类型,当然前端现在有了 TypeScript 😬

    • 强大方便的操作符号:

      • ?. 方便安全的foo?.bar取值,如果 foo 为null,那么取值为null

      • ?? condition ? expr1 : expr2 可以简写为expr1 ?? expr2

      • =和其他符号的组合: *=~/=&=|= ……

      • 级联操作符(Cascade notation ..)




    // 想想这样省了多少变量声明
    querySelect('#button')
    ..text ="Confirm"
    ..classes.add('important')
    ..onClick.listen((e) => window.alert('Confirmed'))

    甚至可以重写操作符


    class Vector {
    final int x, y;

    Vector(this.x, this.y);

    Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
    Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

    // Operator == and hashCode not shown. For details, see note below.
    // ···
    }

    void main() {
    final v = Vector(2, 3);
    final w = Vector(2, 2);

    assert(v + w == Vector(4, 5));
    assert(v - w == Vector(0, 1));
    }

    注:重写==,也需要重写 Object hashCodegetter


    class Person {
    final String firstName, lastName;

    Person(this.firstName, this.lastName);

    // Override hashCode using strategy from Effective Java,
    // Chapter 11.
    @override
    int get hashCode {
    int result = 17;
    result = 37 * result + firstName.hashCode;
    result = 37 * result + lastName.hashCode;
    return result;
    }

    // You should generally implement operator == if you
    // override hashCode.
    @override
    bool operator ==(dynamic other) {
    if (other is! Person) return false;
    Person person = other;
    return (person.firstName == firstName &&
    person.lastName == lastName);
    }
    }

    void main() {
    var p1 = Person('Bob', 'Smith');
    var p2 = Person('Bob', 'Smith');
    var p3 = 'not a person';
    assert(p1.hashCode == p2.hashCode);
    assert(p1 == p2);
    assert(p1 != p3);
    }

    这点在 diff 对象的时候尤其有用。


    lsolate


    Dart 运行在独立隔离的 iSolate 中就类似 JavaScript 一样,单线程事件驱动,但是 Dart 也开放了创建其他 isolate,充分利用 CPU 的多和能力。


    loadData() async {
       // 通过spawn新建一个isolate,并绑定静态方法
       ReceivePort receivePort =ReceivePort();
       await Isolate.spawn(dataLoader, receivePort.sendPort);
       
       // 获取新isolate的监听port
       SendPort sendPort = await receivePort.first;
       // 调用sendReceive自定义方法
    List dataList = await sendReceive(sendPort, 'https://hicc.me/posts');
       print('dataList $dataList');
    }

    // isolate的绑定方法
    static dataLoader(SendPort sendPort) async{
       // 创建监听port,并将sendPort传给外界用来调用
       ReceivePort receivePort =ReceivePort();
       sendPort.send(receivePort.sendPort);
       
       // 监听外界调用
       await for (var msg in receivePort) {
         String requestURL =msg[0];
         SendPort callbackPort =msg[1];
       
         Client client = Client();
         Response response = await client.get(requestURL);
         List dataList = json.decode(response.body);
         // 回调返回值给调用者
         callbackPort.send(dataList);
      }    
    }

    // 创建自己的监听port,并且向新isolate发送消息
    Future sendReceive(SendPort sendPort, String url) {
       ReceivePort receivePort =ReceivePort();
       sendPort.send([url, receivePort.sendPort]);
       // 接收到返回值,返回给调用者
       return receivePort.first;
    }

    当然 Flutter 中封装了compute,可以方便的使用,譬如在其它 isolate 中解析大的 json


    Dart UI as Code


    在这里单独提出来的意义在于,从 React 开始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 声明式组件写法越发流行,Web 前端使用 JSX 来让开发者更方便的书写,而 Flutter,SwiftUI 则直接从优化语言本身着手。


    函数类的命名参数


    void test({@required int age,String name}) {
    print(name);
    print(age);
    }
    // 解决函数调用时候,参数不明确的问题
    test(name:"hicc",age: 30)

    // 这样对于组件的使用尤为方便
    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(),
    body: Container(),
    floatingActionButton:FloatingActionButton()
    );
    }
    }

    大杀器:Collection If 和 Collection For


    // collection If
    Widget build(BuildContext context) {
    return Row(
    children: [
    IconButton(icon: Icon(Icons.menu)),
    Expanded(child: title),
    if (!isAndroid)
    IconButton(icon: Icon(Icons.search)),
    ],
    );
    }
    // Collect For
    var command = [
    engineDartPath,
    frontendServer,
    for (var root in fileSystemRoots) "--filesystem-root=$root",
    for (var entryPoint in entryPoints)
    if (fileExists("lib/$entryPoint.json")) "lib/$entryPoint",
    mainPath
    ];

    更多 Dart 2.3 对此的优化看这里


    Flutter 怎么写


    到这里终于到正题了,如果熟悉 web 前端,熟悉 React 的话,你会对下面要讲的异常的熟悉。


    UI=F(state)


    Flutter App 的一切从lib/main.dart文件的 main 函数开始:


    import 'package:flutter/material.dart';

    void main() => runApp(MyApp());

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Welcome to Flutter',
    home: Scaffold(
    appBar: AppBar(
    title: Text('Welcome to Flutter'),
    ),
    body: Center(
    child: Text('Hello World'),
    ),
    ),
    );
    }
    }

    Dart 类 build 方法返回的便是 Widget,在 Flutter 中一切都是 Widget,包括但不限于



    • 结构性元素,menu,button 等

    • 样式类元素,font,color 等

    • 布局类元素,padding,margin 等

    • 导航

    • 手势


    Widget 是 Dart 中特殊的类,通过实例化(Dart 中new 是可选的)相互嵌套,你的这个 App 就是形如下图的一颗组件树(Dart 入口函数的概念,main.dart -> main())。


    Flutter Widget Tree


    Widget 布局


    上说过 Flutter 布局思路来自 CSS,而 Flutter 中一切皆 Widget,因此整体布局也很简单:



    • 容器组件 Container

      • decoration 装饰属性,设置背景色,背景图,边框,圆角,阴影和渐变等

      • margin

      • padding

      • alignment

      • width

      • height



    • Padding,Center

    • Row,Column,Flex

    • Wrap, Flow 流式布局

    • stack, z 轴布局

    • ……


    更多可以看这里


    Flutter 中 Widget 可以分为三类,形如 React 中“展示组件”、“容器组件”,“context”。


    StatelessWidget


    这个就是 Flutter 中的“展示组件”,自身不保存状态,外部参数变化就销毁重新创建。Flutter 建议尽量使用无状态的组件。


    StatefulWidget


    状态组件就是类似于 React 中的“容器组件”了,Flutter 中状态组件写法会稍微不一样。


    class Counter extends StatefulWidget {
    // This class is the configuration for the state. It holds the
    // values (in this case nothing) provided by the parent and used by the build
    // method of the State. Fields in a Widget subclass are always marked "final".

    @override
    _CounterState createState() => _CounterState();
    }

    class _CounterState extends State {
    int _counter = 0;

    void _increment() {
    setState(() {
    // This call to setState tells the Flutter framework that
    // something has changed in this State, which causes it to rerun
    // the build method below so that the display can reflect the
    // updated values. If you change _counter without calling
    // setState(), then the build method won't be called again,
    // and so nothing would appear to happen.
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance
    // as done by the _increment method above.
    // The Flutter framework has been optimized to make rerunning
    // build methods fast, so that you can just rebuild anything that
    // needs updating rather than having to individually change
    // instances of widgets.
    return Row(
    children: [
    RaisedButton(
    onPressed: _increment,
    child: Text('Increment'),
    ),
    Text('Count: $_counter'),
    ],
    );
    }
    }

    可以看到 Flutter 中直接使用了和 React 中同名的setState方法,不过不会有变量合并的东西,当然也有生命周期


    Flutter StatefulWidget 声明周期


    可以看到一个有状态的组件需要两个 Class,这样写的原因在于,Flutter 中 Widget 都是 immmutable 的,状态组件的状态保存在 State 中,组件仍然每次重新创建,Widget 在这里只是一种对组件的描述,Flutter 会 diff 转换成 Element,然后转换成 RenderObject 才渲染。


    Flutter render object


    Flutter Widget 更多的渲染流程可以看这里


    实际上 Widget 只是作为组件结构一种描述,还可以带来的好处是,你可以更方便的做一些主题性的组件, Flutter 官方提供的Material Components widgetsCupertino (iOS-style) widgets质量就相当高,再配合 Flutter 亚秒级的Hot Reload,开发体验可以说挺不错的。




    State Management


    setState()可以很方便的管理组件内的数据,但是 Flutter 中状态同样是从上往下流转的,因此也会遇到和 React 中同样的问题,如果组件树太深,逐层状态创建就显得很麻烦了,更不要说代码的易读和易维护性了。


    InheritedWidget


    同样 Flutter 也有个context一样的东西,那就是InheritedWidget,使用起来也很简单。


    class GlobalData extends InheritedWidget {
    final int count;
    GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);

    @override
    bool updateShouldNotify(GlobalData oldWidget) {
    return oldWidget.count != count;
    }

    static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);
    }

    class MyApp extends StatelessWidget {
    // This widget is the root of your application.
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
    }
    }

    class MyHomePage extends StatefulWidget {
    MyHomePage({Key key, this.title}) : super(key: key);

    final String title;

    @override
    _MyHomePageState createState() => _MyHomePageState();
    }

    class _MyHomePageState extends State {
    int _counter = 0;

    void _incrementCounter() {
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: GlobalData(
    count: _counter,
    child: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
    Text(
    'You have pushed the button this many times:',
    ),
    Text(
    '$_counter',
    style: Theme.of(context).textTheme.display1,
    ),
    Body(),
    Body2()
    ],
    ),
    ),
    ),
    floatingActionButton: FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: Icon(Icons.add),
    ),
    );
    }
    }

    class Body extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    GlobalData globalData = GlobalData.of(context);
    return Text(globalData.count.toString());
    }
    }

    class Body2 extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    // TODO: implement build
    GlobalData globalData = GlobalData.of(context);
    return Text(globalData.count.toString());
    }

    具体实现原理可以参考这里,不过 Google 封装了一个更为上层的库provider,具体使用可以看这里


    BlOC


    BlOC是 Flutter team 提出建议的另一种更高级的数据组织方式,也是我最中意的方式。简单来说:


    Bloc = InheritedWidget + RxDart(Stream)


    Dart 语言中内置了 Steam,Stream ~= Observable,配合RxDart, 然后加上StreamBuilder会是一种异常强大和自由的模式。


    class GlobalData extends InheritedWidget {
    final int count;
    final Stream timeInterval$ = new Stream.periodic(Duration(seconds: 10)).map((time) => new DateTime.now().toString());
    GlobalData({Key key, this.count,Widget child}):super(key:key,child:child);

    @override
    bool updateShouldNotify(GlobalData oldWidget) {
    return oldWidget.count != count;
    }

    static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData);

    }

    class TimerView extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
    GlobalData globalData = GlobalData.of(context);
    return StreamBuilder(
    stream: globalData.timeInterval$,
    builder: (context, snapshot) {
    return Text(snapshot?.data ?? '');
    }
    );
    }
    }

    当然 Bloc 的问题在于



    • 学习成本略高,Rx 的概念要吃透,不然你会抓狂

    • 自由带来的问题是,可能代码不如 Redux 类的规整。


    顺便,今年 Apple 也拥抱了响应式,Combine(Rx like) + SwiftUI 也基本等于 Bloc 了。


    所以,Rx 还是要赶紧学起来 😬


    除去 Bloc,Flutter 中还是可以使用其他的方案,譬如:



    展开来说现在的前端开发使用强大的框架页面组装已经不是难点了。开发的难点在于如何组合富交互所需的数据,也就是上面图中的state部分。


    更具体来说,是怎么优雅,高效,易维护地处理短暂数据(ephemeral state)setState()和需要共享的 App State 的问题,这是个工程性的问题,但往往也是日常开发最难的事情了,引用 Redux 作者 Dan 的一句:



    “The rule of thumb is:Do whatever is less awkward.”



    到这里,主要的部分已经讲完了,有这些已经可以开发出一个不错的 App 了。剩下的就当成一个 bonus 吧。




    测试


    Flutter debugger,测试都是出场自带,用起来也不难。


    // 测试在/test/目录下面
    void main() {

    testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
    });
    }

    包管理,资源管理


    类似与 JavaScript 的 npm,Flutter,也就是 Dart 也有自己的包仓库。不过项目包的依赖使用 yaml 文件来描述:


    name: app
    description: A new Flutter project.
    version: 1.0.0+1

    environment:
    sdk: ">=2.1.0 <3.0.0"

    dependencies:
    flutter:
    sdk: flutter

    cupertino_icons: ^0.1.2

    生命周期


    移动应用总归需要应用级别的生命周期,flutter 中使用生命周期钩子,也非常的简单:


    class MyApp extends StatefulWidget {
    @override
    _MyAppState createState() => new _MyAppState();
    }

    class _MyAppState extends State with WidgetsBindingObserver {
    @override
    void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    }

    @override
    void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
    }

    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
    case AppLifecycleState.inactive:
    print('AppLifecycleState.inactive');
    break;
    case AppLifecycleState.paused:
    print('AppLifecycleState.paused');
    break;
    case AppLifecycleState.resumed:
    print('AppLifecycleState.resumed');
    break;
    case AppLifecycleState.suspending:
    print('AppLifecycleState.suspending');
    break;
    }
    super.didChangeAppLifecycleState(state);
    }

    @override
    Widget build(BuildContext context) {
    return Container();
    }
    }

    使用原生能力


    和 ReactNative 类似,Flutter 也是使用类似事件的机制来使用平台相关能力。


    Flutter platform channels


    Flutter Web, Flutter Desktop


    这些还在开发当中,鉴于对 Dart 喜欢,以及对 Flutter 性能的乐观,这些倒是很值得期待。


    Flutter web 架构


    还记得平台只是给 Flutter 提供一个画布么,Flutter Desktop 未来更是可以大有可为 😄,相关可以看这里


    最后每种方案,每种技术都有优缺点,甚至技术的架构决定了,有些缺陷可能永远都没法改进,所以 🤔


    作者:小刀c
    链接:https://juejin.cn/post/6844903906800041998
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Flutter | 求求你们了,切换 Widget 的时候加上动画吧

    平时我们在切换 Widget 的时候是怎样的呢?有没有动画效果?是不是直接改变了一个 Widget?类似于这样的:如果是的话,那么今天所说的 Widget,绝对符合你的口味。那如何在 Flutter 当中切换 Widget 的时候加上特效?完成这样的效果?An...
    继续阅读 »

    平时我们在切换 Widget 的时候是怎样的呢?

    有没有动画效果?是不是直接改变了一个 Widget?

    类似于这样的:

    如果是的话,那么今天所说的 Widget,绝对符合你的口味。

    那如何在 Flutter 当中切换 Widget 的时候加上特效?完成这样的效果?

    AnimatedSwitcher 了解一下。

    AnimatedSwitcher

    官方介绍

    话不多说,功能我们已经了解,再来看一下官方的介绍:

    A widget that by default does a FadeTransition between a new widget and the widget previously set on the AnimatedSwitcher as a child.

    If they are swapped fast enough (i.e. before duration elapses), more than one previous child can exist and be transitioning out while the newest one is transitioning in.

    If the "new" child is the same widget type and key as the "old" child, but with different parameters, then AnimatedSwitcher will not do a transition between them, since as far as the framework is concerned, they are the same widget and the existing widget can be updated with the new parameters. To force the transition to occur, set a Key on each child widget that you wish to be considered unique (typically a ValueKey on the widget data that distinguishes this child from the others).

    大致意思就是:

    默认情况下是执行透明度的动画。

    如果交换速度足够快,则存在多个子级,但是在新子级传入的时候将它移除。

    如果新 Widget 和 旧 Widget 的类型和键相同,但是参数不同,那么也不会进行转换。如果想要进行转换,那么要添加一个 Key。

    构造函数

    再来看构造函数,来确定如何使用:

    const AnimatedSwitcher({
    Key key,
    this.child,
    @required this.duration,
    this.reverseDuration,
    this.switchInCurve = Curves.linear,
    this.switchOutCurve = Curves.linear,
    this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
    }) : assert(duration != null),
    assert(switchInCurve != null),
    assert(switchOutCurve != null),
    assert(transitionBuilder != null),
    assert(layoutBuilder != null),
    super(key: key);
    复制代

    来解释一下每个参数:

    1. child:不用多说
    2. duration:动画持续时间
    3. reverseDuration:从新的 Widget 到旧的 Widget 动画持续时间,如果不设置则为 duration 的值
    4. switchInCurve:动画效果
    5. switchOutCurve:同上
    6. transitionBuilder:设置一个新的转换动画
    7. layoutBuilder:包装新旧 Widget 的组件,默认是一个 Stack

    其中必要参数就是一个 duration,那既然知道如何使用了,那就开撸。

    简单例子

    前面我们看的图,就是在对 AppBar上的 actions 进行操作,

    其实这个例子在实际开发当中经常存在,肯定要删除一些东西的嘛,然后选中了以后批量删除。

    那这里也不多说,直接上代码,然后解释:

    class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
    IconData _actionIcon = Icons.delete;

    @override
    void initState() {
    super.initState();
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('AnimatedSwitcherPage'),
    actions: <Widget>[
    AnimatedSwitcher(
    transitionBuilder: (child, anim){
    return ScaleTransition(child: child,scale: anim);
    },
    duration: Duration(milliseconds: 300),
    child: IconButton(
    key: ValueKey(_actionIcon),
    icon: Icon(_actionIcon),
    onPressed: () {
    setState(() {
    if (_actionIcon == Icons.delete)
    _actionIcon = Icons.done;
    else
    _actionIcon = Icons.delete;
    });
    }),
    )
    ],
    ),
    body: Container());
    }
    }
    复制代

    我们定义的是一个 StatefulWidget,因为在切换 Widget 的时候要调用 setState()

    下面来说一下整个流程:

    1. 首先定义好我们初始化的 Icon的数据为 Icons.delete
    2. 在 AppBar 的 actions 里面加入 AnimatedSwitcher
    3. 设置 transitionBuilder 为 缩放动画 ScaleTransition
    4. 给 AnimatedSwitcher 的 child 为 IconButton
    5. 因为前面官方文档说过,如果 Widget 类型一样,只是数据不一样,那么想要动画,就必须添加 Key。
    6. 所以我们给 IconButton 添加了一个 ValueKey,值就为定义好的 IconData
    7. 最后在点击事件中切换两个 Icon 就完成了

    最后再看一下效果:

    总结

    使用该控件最应该注意的点就是 Key 的问题,一定要记住:

    如果新 Widget 和 旧 Widget 的类型和键相同,但是参数不同,那么也不会进行转换。如果想要进行转换,那么要添加一个 Key。

    完整代码已经传至GitHub:github.com/wanglu1209/…


    作者:Flutter笔记
    链接:https://juejin.cn/post/6844903897912311821
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    苍老师的 "码" 是怎么打上的

    --OpenCV初体验,Swift和C++混编 文档更新说明 2017年10月27日 v1.0 初稿 2017年10月28日 v1.1 添加Objective-C++编译方法 ...
    继续阅读 »

    --OpenCV初体验,Swift和C++混编


    文档更新说明



    • 2017年10月27日 v1.0 初稿

    • 2017年10月28日 v1.1 添加Objective-C++编译方法



    提到OpenCV,相信大多数人都听说过,应用领域非常广泛,使用C++开发,天生具有跨平台的优势,我们学习一次,就可以在各个平台使用,这个还是很具有诱惑力的
    本文主要记录我第一次使用OpenCV,在iOS开发平台上面搭建开发环境,并且实现一个简单的马赛克功能
    开发环境:Swift4XCode 9.0



    1、什么是OpenCV?



    • 由英特尔公司于1999年发起并参与开发,至今已有18年历史

    • OpenCV的全称是Open Source Computer Vision Library

    • 是一个跨平台开源计算机视觉库,可用于开发实时的图像处理计算机视觉以及模式识别程序。

    • 支持C/C++JavaPythonOCSwiftRuby等等语言

    • 支持WindowsAndroidMaemoFreeBSDOpenBSDiOSLinuxMac OS


    2、难点,思路



    • 由于我们使用的是Swift,由于目前还不能在Swift中使用C++的类,所以我们得想一个方法,在Swift中调用C++的类

    • 其实方法很简单,Swift天生具有跟Objective-C++混编的能力,而Objective-C++里面是可以直接使用C++的类的,上面的问题也就解决了


    swift-c++handle


    3、马赛克原理



    • 其实把图片的像素密度调低,就可以出现马赛克效果了

    • 开始做马赛克之前,需要定一个马赛克的级别,表示原图中每几个像素变成新图里面的一个像素

    • 取一小块区域左上角的一个像素,并把这个像素填充到整个小区域内

    • 如下图,左边是原图,右边是经过变换之后的图,假设马赛克级别为3,每个数字表示的区域就是处理的一个小单元,取这个最小单元左上角的颜色,填充整个小单元就OK了


    马赛克原理


    4、开动工程


    4.1、搭建c++和swift混编环境



    我们首先要搭建一个c++的环境,然后才能进行c++的开发,而c++环境可以通过iostream里面的cout函数验证




    1. 首先,我们使用xCode新建一个swiftiOS项目

    2. 在工程内,新建一个Objective-C类,继承NSObject,这里会自动提示我们是否为项目添加桥接文件,选择添加即可(桥接文件是用来向Swift暴露Objective-C方法的)


    3. 因为我们要使用Objective-C++,而把Objective-C转成Objective-C++的方法有两种



      • .m文件的后缀名改为.mm,xCode就会自动识别我们的代码为Objective-C++了(xCode会通过后缀名自动识别源文件类型)


      • 选中要修改的.m文件,在右边的Type属性修改成:Objective-C++ Source(也可以手动指定源文件类型)







    4. 在刚才的.mm文件中,添加一个测试方法,在这里测试一下C++环境是否搭建成功


      #import "MyUtil.h"
      #import <iostream> // 记得导入iostrem头文件

      using namespace std;

      @implementation MyUtil

      + (void)testCpp {
      cout << "Hello Swift and Cpp" << endl;
      }


    5. 在前面xCode自动创建的桥接文件中暴露我们的测试方法头文件




    6. Swift中调用测试方法,控制台输出 "Hello Swift and Cpp" 就正常了


      import UIKit

      class ViewController: UIViewController {
      override func viewDidLoad() {
      super.viewDidLoad()
      // 测试方法
      MyUtil.testCpp()
      }
      }


    4.3、导入OpenCV动态库



    iOS开发中导入OpenCV的库其实非常简单,直接拖拽到工程文件就行了




    1. 首先去OpenCV官网下载我们需要的framework,下载地址:opencv.org/releases.ht…,选择最新版本的iOS pack即可


    2. 下载下来之后解压,然后拖拽到我们的工程目录,设置如下图




    3. 设置我们的工程链接OpenCV动态库




    4. build一下,确认不会报错




    4.4、实现马赛克函数



    接下来就是干代码的时候了





    1. 首先要在.m文件中,导入OpenCV的头文件,导入头文件之后代码如下,这里有几个坑要注意:



      • 不要在.h文件中去导入OpenCV的相关头文件,否则会报错,错误信息: Core.hpp header must be compiled as C++,看到这个问题,赶紧把头文件移动到.m文件中去

      • 还有就是OpenCV的头文件最好放在#import <UIKit/UIKit.h>之前,否则也会报一个错误: enum { NO, FEATHER, MULTI_BAND }; Expected identifier


      //导入OpenCV框架 最好放在Foundation.h UIKit.h之前
      //核心头文件
      #import <opencv2/opencv.hpp>
      //对iOS支持
      #import <opencv2/imgcodecs/ios.h>
      //导入矩阵帮助类
      #import <opencv2/highgui.hpp>
      #import <opencv2/core/types.hpp>

      #import "MyUtil.h"
      #import <iostream>

      using namespace std;
      using namespace cv;


    2. 实现马赛克函数


      +(UIImage*)opencvImage:(UIImage*)image level:(int)level{
      //实现功能
      //第一步:将iOS图片->OpenCV图片(Mat矩阵)
      Mat mat_image_src;
      UIImageToMat(image, mat_image_src);

      //第二步:确定宽高
      int width = mat_image_src.cols;
      int height = mat_image_src.rows;

      //在OpenCV里面,必须要先把ARGB的颜色空间转换成RGB的,否则处理会失败(官方例程里面,每次处理都会有这个操作)
      //ARGB->RGB
      Mat mat_image_dst;
      cvtColor(mat_image_src, mat_image_dst, CV_RGBA2RGB, 3);

      //为了不影响原始图片,克隆一张保存
      Mat mat_image_clone = mat_image_dst.clone();

      //第三步:马赛克处理
      int xMax = width - level;
      int yMax = height - level;

      for (int y = 0; y <= yMax; y += level) {
      for (int x = 0; x <= xMax; x += level) {
      //让整个矩形区域颜色值保持一致
      //mat_image_clone.at<Vec3b>(i, j)->像素点(颜色值组成->多个)->ARGB->数组
      //mat_image_clone.at<Vec3b>(i, j)[0]->R值
      //mat_image_clone.at<Vec3b>(i, j)[1]->G值
      //mat_image_clone.at<Vec3b>(i, j)[2]->B值
      Scalar scalar = Scalar(
      mat_image_clone.at<Vec3b>(y, x)[0],
      mat_image_clone.at<Vec3b>(y, x)[1],
      mat_image_clone.at<Vec3b>(y, x)[2]);

      //取出要处理的矩形区域
      Rect2i mosaicRect = Rect2i(x, y, level, level);
      Mat roi = mat_image_dst(mosaicRect);

      //将前面处理的小区域拷贝到要处理的区域
      //CV_8UC3的含义
      //CV_:表示框架命名空间
      //8表示:32位色->ARGB->8位 = 1字节 -> 4个字节
      //U: 无符号类型
      //C分析:char类型
      //3表示:3个通道->RGB
      Mat roiCopy = Mat(mosaicRect.size(), CV_8UC3, scalar);
      roiCopy.copyTo(roi);
      }
      }

      //第四步:将OpenCV图片->iOS图片
      return MatToUIImage(mat_image_dst);
      }


    4.5、在swift中调用马赛克函数



    函数已经实现了,接下来就是在Swift中调用了





    1. 为了便于测试,我们在storyboard中搭一个简单的界面,在按钮中切换马赛克图片和原图,界面如下:
      苍井空




    2. 在按钮点击事件中调用上面的马赛克函数即可


      @IBOutlet weak var imageView: UIImageView!
      /// 显示原图按钮
      @IBAction func origImageBtnClick(_ sender: Any) {
      imageView.image = UIImage(named: "pic.jpg")
      }

      /// 显示马赛克图片
      @IBAction func mosaicImageBtnClick(_ sender: Any) {
      guard let origImage = imageView.image else {
      return
      }

      let mosaicImage = MyUtil.opencvImage(origImage, level: 20)
      imageView.image = mosaicImage
      }


    3. 效果如下,左边的是原图,右边的是马赛克之后的图片,就这样,苍老师的码就打上去啦~





    5、后记


    对于C++,很多人并不陌生,不过我想对于iOS开发者来说,用过C++的童鞋并不多吧,我一直很崇拜那些C++大神,因为通过C++,我们可以很方便的实现跨平台开发,就我们今天的马赛克代码来说,移植到安卓平台,里面的东西也只需要做很小部分的修改,就可以非常完美的适配(当然,安卓的开发环境么有iOS这么简单),所以,掌握和使用C++的性价比还是很高的。


    完整代码已经上传到github: github.com/fengqiangbo…,不过移除了OpenCV.framework,因为太多传不上去,欢迎大家给Star

    作者:加勒比海带王
    链接:https://juejin.cn/post/6844903506944458760
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Jetpack Compose 动画初步了解和使用

    Animatable compose 使用 Animatable 来实现动画效果,Animatable 可以理解为一个可以作为动画属性的 Value 持有者。当它持有的 Value 通过 animateTo 更新时,可以自动以动画的形式对这一过程进行演变。与传...
    继续阅读 »

    Animatable


    compose 使用 Animatable 来实现动画效果,Animatable 可以理解为一个可以作为动画属性的 Value 持有者。当它持有的 Value 通过 animateTo 更新时,可以自动以动画的形式对这一过程进行演变。与传统基于 View 实现的动画不同, 其内部使用协程计算动画的中间过程,所以触发函数 animateTo() 是用suspend 这大大保障了动画运行时的性能。基本的使用方式:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    val animate = remember { Animatable(32.dp, Dp.VectorConverter) }
    // 通过协程触发 animateTo()
    LaunchedEffect(key1 = flag) {
    animate.animateTo(if (flag) 32.dp else 144.dp)
    }
    Row(
    Modifier
    .size(animate.value) // size 在 animate 中取值
    .background(Color.Magenta)
    .clickable { flag = !flag }
    ) {}
    }
    }

    首先看 Animatable :


    androidx.compose.animation.core.Animatable

    public constructor Animatable<T, V : AnimationVector>(
    initialValue: T,
    typeConverter: TwoWayConverter<T, V>,
    visibilityThreshold: T?
    )


    • initialValue 很好理解,作为它的初始值传入,所谓的 Value 持有者持有的就是它。

    • typeConverter 是用来统一动画行为,可以做属性动画的值都通过这个converter 把不同类型的值都转化成 Float 进行动画计算,与对应的 AnimationVector 进行互相转化。

    • visibilityThreshold 判断动画逐渐变为目标值得阈值,可空,暂且按下不表。


    详细了解一下其中的 TwoWayConverter


    /**
    * [TwoWayConverter] class contains the definition on how to convert from an arbitrary type [T]
    * to a [AnimationVector], and convert the [AnimationVector] back to the type [T]. This allows
    * animations to run on any type of objects, e.g. position, rectangle, color, etc.
    */
    interface TwoWayConverter<T, V : AnimationVector> {
    /**
    * Defines how a type [T] should be converted to a Vector type (i.e. [AnimationVector1D],
    * [AnimationVector2D], [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of
    * type T).
    */
    val convertToVector: (T) -> V
    /**
    * Defines how to convert a Vector type (i.e. [AnimationVector1D], [AnimationVector2D],
    * [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of type T) back to type
    * [T].
    */
    val convertFromVector: (V) -> T
    }

    TwoWayConverter 用于定义如何把任意类型的值与可供动画使用的 AnimationVector 之前互相转化的方法,这样通过对它的封装就可以进行对任意属性类型做统一的动画计算。同时,根据动画所需的维度数据返回对应维度的封装 AnimationVectorXD ,这里所说的XD 是指数据维度的个数。例如:



    • androidx.compose.ui.unit.Dp 值转化为 AnimationVector 只有一个维度,也就是它的 value ,所以转化为与之对应的 AnimationVector1D

    • androidx.compose.ui.geometry.Size 中包含两个维度的数据:widthheight , 所以对转化为 AnimationVector2D

    • androidx.compose.ui.geometry.Rect 中包含四个数据维度:lefttoprightbottom,对应 AnimationVector4D


    同时,Compose 还对常用与动画的对象非常贴心的做了默认实现:



    • Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>

    • Int.Companion.VectorConverter: TwoWayConverter<Int, AnimationVector1D>

    • Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>

    • Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>

    • DpOffset.Companion.VectorConverter: TwoWayConverter<DpOffset, AnimationVector2D>

    • Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>

    • Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>

    • IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>

    • IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>


    至此,Animatable 有了初始值, 也有了值类型与对应动画数据的转换方式,那么只需要一个目标值,就满足触发动画的条件了。又因为动画数据的计算在协程中进行,那么我们此时只需在协程中触发 animateTo() 就可以了:


    // 通过协程触发 animateTo()
    LaunchedEffect(key = flag) {
    animate.animateTo(if (flag) 32.dp else 144.dp)
    }

    注意此处的协程 CoroutineScope 是通过 Composable 函数 LaunchedEffect 提供的,该函数内部实现了对于 composer 的优化,同时通过 remember 函数缓存状态,所以不会由于 recompose 的主动或被动调用而多次执行。


    AnimationSpec


    AnimationSpec 顾名思义支持对动画定义规范,以此实现自定义动画。


    查看 animateTo 函数的定义可以发现其第二个参数可以设置 animationSpec,它有一个默认的实现 defaultSpringSpec ,所以上面的例子中没有明确指定 animationSpec


    androidx.compose.animation.core.Animatable 
    public final suspend fun animateTo(
    targetValue: T,
    animationSpec: AnimationSpec<T> = defaultSpringSpec,
    initialVelocity: T = velocity,
    block: (Animatable<T, V>.() → Unit)? = null
    ): AnimationResult<T, V>

    spring


    defaultSpringSpec 是一个通过 spring 创建的基于弹簧的物理特性的动画:


    androidx.compose.animation.core AnimationSpec.kt 
    @Stable
    public fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy,
    stiffness: Float = Spring.StiffnessMedium,
    visibilityThreshold: T? = null
    ): SpringSpec<T>

    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
    dampingRatio = Spring.DampingRatioHighBouncy,
    stiffness = Spring.StiffnessMedium
    )
    )

    spring 接受两个参数,dampingRatiostiffness 。前者定义弹簧的弹性,默认值为 Spring.DampingRatioNoBouncy 。后者定义弹簧向 targetVaule 移动的速度。 默认值为 Spring.StiffnessMedium。基于物理特性的 spring 无法设置 duration。具体效果参考下图:


    animation-spring.gif


    tween


    androidx.compose.animation.core AnimationSpec.kt 
    @Stable
    public fun <T> tween(
    durationMillis: Int = DefaultDurationMillis,
    delayMillis: Int = 0,
    easing: Easing = FastOutSlowInEasing
    ): TweenSpec<T>

    tween 在指定的 durationMillis 内使用缓和曲线在起始值和结束值之间添加动画效果。动画曲线通过 Easing 添加。


    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = tween(
    durationMillis = 300,
    delayMillis = 50,
    easing = LinearOutSlowInEasing
    )
    )

    keyframes


    keyframes 会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,可以指定 Easing 来确定插值曲线:


    val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = keyframes {
    durationMillis = 375
    0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
    0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
    0.4f at 75 // ms
    0.4f at 225 // ms
    }
    )

    snapTo(targetValue: T)


    androidx.compose.animation.core.Animatable
    public final suspend fun snapTo(
    targetValue: T
    ): Unit

    Animatable 还提供了一个 snapTo(targetValue) 的函数,这个函数允许直接设置它内部持有的 value 值,此过程不会产生任何动画,正在进行的动画也会被取消,某些场景可能需要动画开始前有一个初始值,可以使用此函数。


    一种更方便的使用方式:animate*AsState


    设置某一个属性的目标值,当对应属性值发生变化后,自动触发动画,过度到对应值。



    This Composable function is overloaded for different parameter types such as Float, Color, Offset, etc. When the provided targetValue is changed, the animation will run automatically. If there is already an animation in-flight when targetValue changes, the on-going animation will adjust course to animate towards the new target value.



    compose 提供了这几个覆盖基本场景的函数:



    • animateFloatAsState

    • animateDpAsState

    • animateSizeAsState

    • animateOffsetAsState

    • animateRectAsState

    • animateIntAsState

    • animateIntOffsetAsState

    • animateIntSizeAsState


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier.fillMaxSize().background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {

    val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
    // 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
    Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
    }
    Row(
    Modifier
    .size(size)
    .background(Color.Magenta)
    .clickable { flag = !flag }
    ) {}
    }
    }

    演示效果:


    demo_animateDpAsState.gif


    作为 compose 动画的最基本操作,与我们平时使用动画的方式不太一样,你会发现你能影响动画的核心只能是选一个属性和一个目标值。甚至连属性的初始值都不能预设,动画的时长没有办法干预。


    深入一点点


    查看 animateDpState() 函数的实现:


    /**
    * ... ...
    *
    * [animateDpAsState] returns a [State] object. The value of the state object will continuously be
    * updated by the animation until the animation finishes.
    *
    * Note, [animateDpAsState] cannot be canceled/stopped without removing this composable function
    * from the tree. See [Animatable] for cancelable animations.
    *
    * @sample androidx.compose.animation.core.samples.DpAnimationSample
    *
    * @param targetValue Target value of the animation
    * @param animationSpec The animation that will be used to change the value through time. Physics animation will be used by default.
    * @param finishedListener An optional end listener to get notified when the animation is finished.
    * @return A [State] object, the value of which is updated by animation.
    */
    @Composable
    fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
    ): State<Dp> {
    return animateValueAsState(
    targetValue,
    Dp.VectorConverter,
    animationSpec,
    finishedListener = finishedListener
    )
    }

    查看 animateIntAsState 的实现:


    /**
    * ... ...
    *
    * [animateIntAsState] returns a [State] object. The value of the state object will continuously be
    * updated by the animation until the animation finishes.
    *
    * Note, [animateIntAsState] cannot be canceled/stopped without removing this composable function
    * from the tree. See [Animatable] for cancelable animations.
    *
    * @param targetValue Target value of the animation
    * @param animationSpec The animation that will be used to change the value through time. Physics
    * animation will be used by default.
    * @param finishedListener An optional end listener to get notified when the animation is finished.
    * @return A [State] object, the value of which is updated by animation.
    */
    @Composable
    fun animateIntAsState(
    targetValue: Int,
    animationSpec: AnimationSpec<Int> = intDefaultSpring,
    finishedListener: ((Int) -> Unit)? = null
    ): State<Int> {
    return animateValueAsState(
    targetValue, Int.VectorConverter, animationSpec, finishedListener = finishedListener
    )
    }




    • targetValue是某一个以Dp为单位的属性的目标值,顾名思义就是你希望这个属性变化为某一个具体的值;




    • animationSpec 该属性的值如何跟随时间的变化而变化,有默认实现;




    • finishedListener 动画结束函数,可空;




    • anumate*AsState 系列函数的实现都很相似,统一在内部调用了 animateValueAsState(...)




    这是一个基于 State 的实现,联系 Compose 中对于数据的封装和订阅方式,可以理解为当程序的某一个行为触发动画启动后,compose 会自主启动,并根据时间来计算对应的属性应该是什么值,再通过 State 返回,Composable 函数在一次次 recompose 行为中不断通过 State 获取到该属性的最新值,并刷新到界面上,知道这个值变化到目标值状态,更新也就结束了。也就是动画结束。


    继续深入 animateValueAsState 的实现:


    @Composable
    fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember {
    spring(visibilityThreshold = visibilityThreshold)
    },
    visibilityThreshold: T? = null,
    finishedListener: ((T) -> Unit)? = null
    ): State<T> {

    val animatable = remember { Animatable(targetValue, typeConverter) }
    val listener by rememberUpdatedState(finishedListener)
    val animSpec by rememberUpdatedState(animationSpec)

    ... ...

    return animatable.asState()
    }

    你会发现其内部其实还是使用 Animatable 来实现。anumate*AsState 虽然基于 Animatable ,不但没有扩充 Animatable 的用法,反而还有了局限,怎会如此?个人认为 animate*AsState 是专门为确定性的简单使用场景进行的封装,这些场景有明确的状态变化,需要做动画的值也不会很复杂,在这些场景中如果能极为方便的快速定义动画,也会是一种非常实用的设计,即使场景变得复杂,再用 Animatable 兜底也能满足需求。


    updateTransition


    在实际的使用场景中,很多情况下的动画设计都不是单一参数可以完成的,比如大小变化的同时对颜色进行过渡、大小与圆角同时变化,形状与颜色同时变化等。这些情况需要组合多个动画同时进行:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {

    val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
    // 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
    Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
    }
    val color by animateColorAsState(
    targetValue = if (flag) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
    )
    Row(
    Modifier
    .size(size)
    .background(color)
    .clickable { flag = !flag }
    ) {}
    }
    }

    但是上面的实现存在一个问题,就是每一个属性值的动画过程都是单独计算的,同时每个属性动画也都要考单独的状态进行管理,这显然在性能上是有浪费的,而却也很不方便。这种情况可以引入 Transition 来进行动画的统一管理:


    @Composable
    fun Demo() {
    var flag by remember { mutableStateOf(false) }
    Box(
    Modifier
    .fillMaxSize()
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    val transition = updateTransition(flag)
    val size = transition.animateDp { if (it) 32.dp else 96.dp }
    val color = transition.animateColor { if (it) MaterialTheme.colors.primary else MaterialTheme.colors.secondary }
    Row(
    Modifier
    .size(size.value)
    .background(color.value)
    .clickable { flag = !flag }
    ) {}
    }
    }

    这样当 flag 触发 transition 状态改变时,sizecolor 的值就可以同时在 transition 内部进行计算,性能又节省了亿点点🤏🏻


    @Composable
    fun <T> updateTransition(
    targetState: T,
    label: String? = null
    ): Transition<T> {
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
    onDispose {
    // Clean up on the way out, to ensure the observers are not stuck in an in-between
    // state.
    transition.onTransitionEnd()
    }
    }
    return transition
    }

    updateTransition 可以创建并保存状态,其内部使用 remember 实现。


    作者:User_20211124110632
    链接:https://juejin.cn/post/7034107134551785503
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    kotlin 进阶教程:核心概念

    1 空安全 // ? 操作符,?: Elvis 操作符 val length = b?.length ?: -1 // 安全类型转换 val code = res.code as? Int // StringsKt val code = res.code?.t...
    继续阅读 »

    1 空安全


    // ? 操作符,?: Elvis 操作符
    val length = b?.length ?: -1
    // 安全类型转换
    val code = res.code as? Int
    // StringsKt
    val code = res.code?.toIntOrNull()
    // CollectionsKt
    val list1: List<Int?> = listOf(1, 2, 3, null)
    val list2 = listOf(1, 2, 3)
    val a = list2.getOrNull(5)
    // 这里注意 null 不等于 false
    if(a?.hasB == false) {}

    2 内联函数



    使用 inline 操作符标记的函数,函数内代码会编译到调用处。


    // kotlin
    val list = listOf("a", "b", "c", null)
    list.getOrElse(4) { "d" }?.let {
    println(it)
    }

    // Decompile,getOrElse 方法会内联到调用处
    List list = CollectionsKt.listOf(new String[]{"a", "b", "c", (String)null});
    byte var3 = 4;
    Object var10000;
    if (var3 <= CollectionsKt.getLastIndex(list)) {
    var10000 = list.get(var3);
    } else {
    var10000 = "d";
    }

    String var9 = (String)var10000;
    if (var9 != null) {
    String var2 = var9;
    System.out.print(var2);
    }

    noline: 禁用内联,用于标记参数,被标记的参数不会参与内联。


    // kotlin
    inline fun sync(lock: Lock, block1: () -> Unit, noinline block2: () -> Unit) {}

    // Decompile,block1 会内联到调用处,但是 block2 会生成函数对象并生成调用
    Function0 block2$iv = (Function0)null.INSTANCE;
    ...
    block2.invoke()


    @kotlin.internal.InlineOnly: kotlin 内部注解,这个注解仅用于内联函数,用于防止 java 类调用(原理是编译时会把这个函数标记为 private,内联对于 java 类来说没有意义)。


    如果扩展函数的方法参数包含高阶函数,需要加上内联。


    非局部返回:
    lambda 表达式内部是禁止使用裸 return 的,因为 lambda 表达式不能使包含它的函数返回。但如果 lambda 表达式传给的函数是内联的,那么该 return 也可以内联,所以它是允许的,这种返回称为非局部返回。
    但是可以通过 crossinline 修饰符标记内联函数的表达式参数禁止非局部返回。


    public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
    ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
    override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
    }

    3 泛型



    (1) 基本用法


    class A<T> {
    }
    fun <T> T.toString(): String {
    }
    // 约束上界
    class Collection<T : Number, R : CharSequence> : Iterable<T> {
    }
    fun <T : Iterable> T.toString() {
    }
    // 多重约束
    fun <T> T.eat() where T : Animal, T : Fly {
    }


    (2) 类型擦除
    为了兼容 java 1.5 以前的版本,带不带泛型编译出来的字节码都是一样的,泛型的特性是通过编译器类型检查和强制类型转换等方式实现的,所以 java 的泛型是伪泛型。
    虽然运行时会擦除泛型,但也是有办法拿到的。


    (javaClass.genericSuperclass as? ParameterizedType)
    ?.actualTypeArguments
    ?.getOrNull(0)
    ?: Any::class.java

    fastjsonTypeReferencegsonTypeToken 都是用这种方式来获取泛型的。


    // fastjson 
    HttpResult<PrivacyInfo> httpResult = JSON.parseObject(
    json,
    new TypeReference<HttpResult<PrivacyInfo>>() {
    }
    );
    // gson
    Type type = new TypeToken<ArrayList<JsonObject>>() {
    }.getType();
    ArrayList<JsonObject> srcJsonArray = new Gson().fromJson(sourceJson, type);


    Reified 关键字
    在 kotlin 里,reified 关键字可以让泛型能够在运行时被获取到。reified 关键字必须结合内联函数一起用。


    // fastjson
    inline fun <reified T : Any> parseObject(json: String) {
    JSON.parseObject(json, T::class.java)
    }
    // gson
    inline fun <reified T : Any> fromJson(json: String) {
    Gson().fromJson(json, T::class.java)
    }
    // 获取 bundle 中的 Serializable
    inline fun <reified T> Bundle?.getSerializableOrNull(key: String): T? {
    return this?.getSerializable(key) as? T
    }
    // start activity
    inline fun <reified T : Context> Context.startActivity() {
    startActivity(Intent(this, T::class.java))
    }


    (3) 协变(out)和逆变(in)


    javaList 是不变的,下面的操作不被允许。


    List<String> strList = new ArrayList<>();
    List<Object> objList = strList;


    但是 kotlinList 是协变的,可以做这个操作。


    public interface List<out E> : Collection<E> { ... }
    val strList = arrayListOf<String>()
    val anyList: List<Any> = strList

    注意这里赋值之后 anyList 的类型还是 List<Any> , 如果往里添加数据,那个获取的时候就没法用 String 接收了,这是类型不安全的,所以协变是不允许写入的,是只读的。在 kotlin 中用 out 表示协变,用 out 声明的参数类型不能作为方法的参数类型,只能作为返回类型,可以理解成“生产者”。相反的,kotlin 中用 in 表示逆变,只能写入,不能读取,用 in 声明的参数类型不能作为返回类型,只能用于方法参数类型,可以理解成 “消费者”。


    注意 kotlin 中的泛型通配符 * 也是协变的。


    4 高阶函数



    高阶函数: 将函数用作参数或返回值的函数。


    写了个 test 方法,涵盖了常见的高阶函数用法。


    val block4 = binding?.title?.test(
    block1 = { numer ->
    setText(R.string.app_name)
    println(numer)
    },
    block2 = { numer, checked ->
    "$numer : $checked"
    },
    block3 = {
    toIntOrNull() ?: 0
    }
    )
    block4?.invoke(2)

    fun <T: View, R> T.test(
    block1: T.(Int) -> Unit,
    block2: ((Int, Boolean) -> String)? = null,
    block3: String.() -> R
    ): (Int) -> Unit {
    block1(1)
    block2?.invoke(2, false)
    "5".block3()
    return { number ->
    println(number)
    }
    }

    5 作用域函数


    // with,用于共用的场景
    with(View.OnClickListener {
    it.setBackgroundColor(Color.WHITE)
    }) {
    tvTitle.setOnClickListener(this)
    tvExpireDate.setOnClickListener(this)
    }

    // apply,得到值后会修改这个值的属性
    return CodeLoginFragment().apply {
    arguments = Bundle().apply {
    putString(AppConstants.INFO_EYES_EVENT_ID_FROM, eventFrom)
    }
    }

    // also,得到值后还会继续用这个值
    tvTitle = view.findViewById<TextView?>(R.id.tvTitle).also {
    displayTag(it)
    }

    // run,用于需要拿内部的属性的场景
    tvTitle?.run {
    text = "test"
    visibility = View.VISIBLE
    }

    // let,用于使用它自己的场景
    tvTitle?.let {
    handleTitle(it)
    }

    fun <T> setListener(listenr: T.() -> Unit) {
    }

    6 集合


    list.reversed().filterNotNull()
    .filter {
    it % 2 != 0
    }
    .map {
    listOf(it, it * 2)
    }
    .flatMap {
    it.asSequence()
    }.onEach {
    println(it)
    }.sortedByDescending {
    it
    }
    .forEach {
    println(it)
    }

    7 操作符重载



    重载(overload)操作符的函数都需要使用 operator 标记,如果重载的操作符被重写(override),可以省略 operator 修饰符。
    这里列几个比较常用的。


    索引访问操作符:


    a[i, j] => a.get(i, j)
    a[i] = b => a.set(i, b)


    注意 i、j 不一定是数字,也可以是 String 等任意类型。


    public interface List<out E> : Collection<E> {
    public operator fun get(index: Int): E
    }
    public interface MutableList<E> : List<E>, MutableCollection<E> {
    public operator fun set(index: Int, element: E): E
    }


    调用操作符:
    invoke 是调用操作符函数名,调用操作符函数可以写成函数调用表达式。


    val a = {}
    a() => a.invoke()
    a(i, j) => a.invoke(i, j)


    变量 block: (Int) -> Unit 调用的时候可以写成 block.invoke(2),也可以写成 block(2),原因是重载了 invoke 函数:


    public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
    }


    getValuesetValueprovideDelegate 操作符:
    用于委托属性,变量的 get() 方法会委托给委托对象的 getValue 操作符函数,相对应的变量的 set() 方法会委托给委托对象的 setValue 操作符函数。


    class A(var name: String? = null) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = name
    operator fun setValue(thisRef: Any?, property: KProperty<*>, name: String?) {
    this.name = name
    }
    }
    // 翻译
    var b by A() =>
    val a = A()
    var b:String?
    get() = a.getValue(this, ::b)
    set(value) = a.setValue(this, ::b, value)


    表达式 ::b 求值为 KProperty 类型的属性对象。



    跟前面的操作符函数有所区别的是,这两个操作符函数的参数格式都是严格要求的,一个类中的函数格式符合特定要求才可以被当做委托对象。


    provideDelegate 主要用于对委托对象通用处理,比如多个变量用了同一个委托对象时需要验证变量名的场景。


    var b by ALoader()

    class A(var name: String? = null) : ReadWriteProperty<Any?, String?>{
    override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
    return name
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
    this.name = value
    }
    }

    class ALoader : PropertyDelegateProvider<Any?, A> {
    override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) : A {
    property.run {
    when {
    isConst -> {}
    isLateinit -> {}
    isFinal -> {}
    isSuspend -> {}
    !property.name.startsWith("m") -> {}
    }
    }
    return A()
    }
    }

    // 翻译
    var b by ALoader() =>
    val a = ALoader().provideDelegate(this, this::b)
    var b: String?
    get() = a.getValue(this, ::b)
    set(value) = a.setValue(this, ::b, value)

    8 委托


    8.1 委托模式




    // 单例
    companion object {
    @JvmStatic
    val instance by lazy { FeedManager() }
    }

    // 委托实现多继承
    interface BaseA {
    fun printA()
    }

    interface BaseB {
    fun printB()
    }

    class BaseAImpl(val x: Int) : BaseA {
    override fun printA() {
    print(x)
    }
    }

    class BaseBImpl() : BaseB {
    override fun printB() {
    print("printB")
    }
    }

    class Derived(a: BaseA, b: BaseB) : BaseA by a, BaseB by b {
    override fun printB() {
    print("world")
    }
    }

    fun main() {
    val a = BaseAImpl(10)
    val b = BaseBImpl()
    Derived(a, b).printB()
    }

    // 输出:world



    这里 Derived 类相当于同时继承了 BaseAImplBaseBImpl 类,并且重写了 printB() 方法。
    在实际开发中,一个接口有多个实现,如果想复用某个类的实现,可以使用委托的形式。
    还有一种场景是,一个接口有多个实现,需要动态选择某个类的实现:


    interface IWebView {
    fun load()
    }

    // SDK 内部 SystemWebView
    class SystemWebView : IWebView {
    override fun load() {
    ...
    }

    fun stopLoading() {
    ...
    }
    }

    // SDK 内部 X5WebView
    class X5WebView : IWebView {
    override fun load() {
    ...
    }

    fun stopLoading() {
    ...
    }
    }

    abstract class IWebViewAdapter(webview: IWebView) : IWebView by webview{
    abstract fun stopLoading()
    }

    class SystemWebViewAdapter(private val webview: SystemWebView) : IWebViewAdapter(webview){
    override fun stopLoading() {
    webview.stopLoading()
    }
    }

    class X5WebViewAdapter(private val webview: X5WebView) : IWebViewAdapter(webview){
    override fun stopLoading() {
    webview.stopLoading()
    }
    }


    8.2 委托属性



    格式:


    import kotlin.reflect.KProperty

    public interface ReadOnlyProperty<in R, out T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T
    }

    public interface ReadWriteProperty<in R, T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T

    public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    }

    public fun interface PropertyDelegateProvider<in T, out D> {
    public operator fun provideDelegate(thisRef: T, property: KProperty<*>): D
    }

    自定义委托对象, getValue 方法的参数跟上面完全一致即可,返回值类型必须是属性类型;setValue 方法的前两个参数跟上面完全一致即可,第三个参数类型必须是属性类型;provideDelegate 方法的参数跟上卖弄完全一致即可,返回值类型必须是属性类型。
    ReadOnlyPropertyReadWritePropertyPropertyDelegateProvider 都是 kotlin 标准库里的类,需要自定义委托对象时直接继承他们会更方便。


    9 怎么写单例?



    不用 object 的写法可能是:


    // 不带参单例
    class A {
    companion object {
    @JvmStatic
    val instance by lazy { A() }
    }
    }

    // 带参的单例,不推荐
    class Helper private constructor(val context: Context) {

    companion object {
    @Volatile
    private var instance: Helper? = null

    @JvmStatic
    fun getInstance(context: Context?): Helper? {
    if (instance == null && context != null) {
    synchronized(Helper::class.java) {
    if (instance == null) {
    instance = Helper(context.applicationContext)
    }
    }
    }
    return instance
    }
    }
    }

    先说带参的单例,首先不推荐写带参数的单例,因为单例就是全局共用,初始化一次之后保持不变,需要的参数应该在第一次使用前设置好(比如通过 by lazy{ A().apply { ... } }),或者单例内部拿应用内全局的参数,然后上例中 context 作为静态变量,Android Studio 会直接报黄色警告,这是个内存泄漏。context 可以设置一个全局的 applicationContext 变量获取。


    然后上面不带参的单例可以
    直接用 object 代替或者直接不用 object 封装,写在文件顶层,可以对比下编译后的代码:


    // kotlin
    object A{
    fun testA(){}
    }

    // 编译后:
    public final class A {
    @NotNull
    public static final A INSTANCE;

    public final void testA() {
    }

    private A() {
    }

    static {
    A var0 = new A();
    INSTANCE = var0;
    }
    }


    // kotlin
    var a = "s"
    fun testB(a: String){
    print(a)
    }

    // 编译后:
    public final class TKt {
    @NotNull
    private static String a = "s";

    @NotNull
    public static final String getA() {
    return a;
    }

    public static final void setA(@NotNull String var0) {
    Intrinsics.checkNotNullParameter(var0, "<set-?>");
    a = var0;
    }

    public static final void testB(@NotNull String a) {
    Intrinsics.checkNotNullParameter(a, "a");
    boolean var1 = false;
    System.out.print(a);
    }
    }

    可以发现,直接文件顶层写,不会创建对象,都是静态方法,如果方法少且评估不需要封装(主要看调用的时候是否需要方便识别哪个对象的方法)可以直接写在文件顶层。


    同理,伴生对象也尽量非必要不创建。


    // kotlin
    class A {
    companion object {
    const val TAG = "A"

    @JvmStatic
    fun newInstance() = A()
    }
    }

    // 编译后
    public final class A {
    @NotNull
    public static final String TAG = "A";
    @NotNull
    public static final A.Companion Companion = new A.Companion((DefaultConstructorMarker)null);

    @JvmStatic
    @NotNull
    public static final A newInstance() {
    return Companion.newInstance();
    }

    public static final class Companion {
    @JvmStatic
    @NotNull
    public final A newInstance() {
    return new A();
    }

    private Companion() {
    }

    // $FF: synthetic method
    public Companion(DefaultConstructorMarker $constructor_marker) {
    this();
    }
    }
    }

    可以发现,伴生对象会创建一个对象(废话...),知道这个很重要,因为如果伴生对象里没有函数,只有常量,那还有必要创建这个对象吗?函数也只是为了 newInstance 这种方法调用的时候看起来统一一点,如果是别的方法,完全可以写在类所在文件的顶层。


    作者:流沙三七
    链接:https://juejin.cn/post/7034110955571609607
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    前端vue面霸修炼手册!!

    一、对MVVM的理解MVVM全称是Model-View-ViewModelModel 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示...
    继续阅读 »



    一、对MVVM的理解

    MVVM全称是Model-View-ViewModel

    Model 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示;视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;

    Vue是以数据为驱动的,Vue自身将DOM和数据进行绑定,一旦创建绑定,DOM和数据将保持同步,每当数据发生变化,DOM会跟着变化。 ViewModel是Vue的核心,它是Vue的一个实例。Vue实例时作用域某个HTML元素上的这个HTML元素可以是body,也可以是某个id所指代的元素。

    二、vue常见指令

    1. v-textv-text 主要用来更新 textContent,可以等同于 JS 的 text 属性。

    <span v-text="name"></span>

    <span插值表达式{{name}}</span>
    1. v-html等同于 JS 的 innerHtml 属性

    <div v-html="content"></div>
    1. v-cloak用来保持在元素上直到关联实例结束时进行编译 解决闪烁问题

    <div id="app" v-cloak>
       <div>
          {{msg}}
       </div>
    </div>
    <script type="text/javascript">
       new Vue({
         el:'#app',
         data:{
           msg:'hello world'
        }
      })
    </script>

    正常在页面加载时会闪烁,先显示:

    <div>
      {{msg}}
    </div>

    编译后才显示:

    <div>
      hello world!
    </div>

    可以用 v-cloak 指令解决插值表达式闪烁问题,v-cloak 在 css 中用属性选择器设置为 display: none;

    1. v-oncev-once 关联的实例,只会渲染一次。之后的重新渲染,实例极其所有的子节点将被视为静态内容跳过,这可以用于优化更新性能

    <span v-once>This will never change:{{msg}}</span>  //单个元素
    <div v-once>//有子元素
       <h1>comment</h1>
       <p>{{msg}}</p>
    </div>
    <my-component v-once:comment="msg"></my-component> //组件
    <ul>
       <li v-for="i in list">{{i}}</li>
    </ul>

    上面的例子中,msg,list 即使产生改变,也不会重新渲染。

    1. v-ifv-if 可以实现条件渲染,Vue 会根据表达式的值的真假条件来渲染元素

    <a v-if="true">show</a>
    1. v-elsev-else 是搭配 v-if 使用的,它必须紧跟在 v-if 或者 v-else-if 后面,否则不起作用

    <a v-if="true">show</a>
    <a v-else>hide</a>
    1. v-else-ifv-else-if 充当 v-if 的 else-if 块, 可以链式的使用多次。可以更加方便的实现 switch 语句。

    <div v-if="type==='A'">
      A
    </div>
    <div v-else-if="type==='B'">
      B
    </div>
    <div v-else-if="type==='C'">
      C
    </div>
    <div v-else>
      Not A,B,C
    </div>
    1. v-show也是用于根据条件展示元素。和 v-if 不同的是,如果 v-if 的值是 false,则这个元素被销毁,不在 dom 中。但是 v-show 的元素会始终被渲染并保存在 dom 中,它只是简单的切换 css 的 dispaly 属性。

    <span v-show="true">hello world</span >

    注意:v-if 有更高的切换开销 v-show 有更高的初始渲染开销。因此,如果要非常频繁的切换, 则使用 v-show 较好;如果在运行时条件不太可能改变,则 v-if 较好

    1. v-for用 v-for 指令根据遍历数组来进行渲染

    <div v-for="(item,index) in items"></div>   //使用in,index是一个可选参数,表示当前项的索引
    1. v-bindv-bind 用来动态的绑定一个或者多个特性。没有参数时,可以绑定到一个包含键值对的对象。常用于动态绑定 class 和 style。以及 href 等。简写为一个冒号【 :】

    <div id="app">
       <div :class="{'is-active':isActive, 'text-danger':hasError}"></div>
    </div>
    <script>
       var app = new Vue({
           el: '#app',
           data: {
               isActive: true,  
               hasError: false    
          }
      })
    </script>

    编译后

    <div class = "is-active"></div>
    1. v-model用于在表单上创建双向数据绑定

    <div id="app">
       <input v-model="name">
       <p>hello {{name}}</p>
    </div>
    <script>
       var app = new Vue({
           el: '#app',
           data: {
               name:'小明'
          }
      })
    </script>

    model 修饰符有

    .lazy(在 change 事件再同步) > v-model.lazy .number(自动将用户的输入值转化为数值类型) > v-model.number .trim(自动过滤用户输入的首尾空格) > v-model.trim

    1. v-onv-on 主要用来监听 dom 事件,以便执行一些代码块。表达式可以是一个方法名。 简写为:【 @ 】

    <div id="app">
      <button @click="consoleLog"></button>
    </div>
    <script>
      var app = new Vue({
          el: '#app',
          methods:{
              consoleLog:function (event) {
                  console.log(1)
              }
          }
      })
    </script>

    事件修饰符

    .stop 阻止事件继续传播 .prevent 事件不再重载页面 .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 .self 只当在 event.target 是当前元素自身时触发处理函数 .once 事件将只会触发一次 .passive 告诉浏览器你不想阻止事件的默认行为

    三 、v-if 和 v-show 有什么区别?

    共同点:v-if 和 v-show 都能实现元素的显示隐藏

    区别:

    v-show 只是简单的控制元素的 display 属性 而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件为假,元素会被销毁) 2. v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多 3. v-if 有更高的切换开销,v-show 切换开销小 4. v-if 有配套的 v-else-if 和 v-else,而 v-show 没有 5. v-if 可以搭配 template 使用,而 v-show 不行

    四、如何让CSS只在当前组件中起作用?

    将组件样式加上 scoped

    <style scoped>
    ...
    </style>

    五、 keep-alive的作用是什么?

    keep-alive包裹动态组件时,会缓存不活动的组件实例, 主要用于保留组件状态或避免重新渲染。

    六、在Vue中使用插件的步骤

    采用ES6的 import … from … 语法 或 CommonJSd的 require() 方法引入插件 2、使用全局方法 Vue.use( plugin ) 使用插件,可以传入一个选项对象 Vue.use(MyPlugin, { someOption: true })

    七、Vue 生命周期

    八、Vue 组件间通信有哪几种方式

    Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信

    九、computed 和 watch 的区别和运用的场景

    computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值

    watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作

    运用场景:

    • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed

    • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch

    十、vue-router 路由模式有几种

    1. Hash: 使用 URL 的 hash 值来作为路由。支持所有浏览器。

    2. History: 以来 HTML5 History API 和服务器配置。参考官网中 HTML5 History 模式

    3. Abstract: 支持所有 javascript 运行模式。如果发现没有浏览器的 API,路由会自动强制进入这个模式。

    十一、SPA 单页面的理解,它的优缺点分别是什么

    SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS 一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转 取而代之的是利用路由机制实现 HTML 内容的变换, UI 与用户的交互,避免页面的重新加载。

    优点:

    1、用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
    2、基于上面一点,SPA 相对对服务器压力小
    3、前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理

    缺点:

    1、初次加载耗时多:为实现单页 Web 应用功能及显示效果, 需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
    2、前进后退路由管理:由于单页应用在一个页面中显示所有的内容, 所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
    3、SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
    作者:不要搞偷袭
    来源:https://blog.51cto.com/u_15115139/2675806


    收起阅读 »

    Swift路由组件(一)使用路由的目的和实现思想

    iOS
    Swift路由组件(一)使用路由的目的和实现思想这个为本人原创,转载请注明出处:juejin.cn/post/703216…目的项目开发到一定程度,功能之间的调用会变的越来越复杂这里用一个商品购买的逻辑举例从图上看,问题就是业务之间的跳转很多,而且乱。还有就是...
    继续阅读 »

    Swift路由组件(一)使用路由的目的和实现思想

    这个为本人原创,转载请注明出处:juejin.cn/post/703216…

    目的

    项目开发到一定程度,功能之间的调用会变的越来越复杂

    这里用一个商品购买的逻辑举例

    image.png

    从图上看,问题就是业务之间的跳转很多,而且乱。还有就是当跳同一个页面时,跳转要带的参数都一致,如何保证?如果代码分散到各个业务里面去跳就难免会到处维护的问题。

    这就需要路由了。

    而且路由做好了,还能有一个好处就是后端或者前端,他们按路由协议统一处理跳转,app就可以不考虑业务之间的跳转了。

    下面是加上路由模块的跳转图。

    image.png

    这下清晰了。

    从图上来看,路由,他主要负责业务的跳转,从一个页面跳转到另一个页面等。

    实现的思想

    为了能跳,那么就需要知道路。所以可以这样理解,路由他需要知道你要跳转到哪里去,去的地方需要什么入参。

    所以得有一个key,map到一个ViewController,然后ViewController需要什么入参,就顺便带过来。

    解决这个key的问题,业界比较常见的做法是有一个路由表

    1. 比如维护一个plist文件,开发的时候把对应的key映射controller维护到plist里面,运行的时候一次性load到内存中。然后路由要跳转的时候就只需要查表来跳。
    2. 或者在运行的时候通过业务注册,每个业务把key注册到路由里面去,在内存中维护一个路由表。

    两种方法都可以。结果大概是这样。

    keyvalue
    to_home_pageHomeViewController
    to_buy_pageBuyViewController
    ......

    路由跳转他要解决三种跳转逻辑

    1. 通过后端下发,直接让App打开某个原生或者Web页面
      • 比如推送消息,点击消息就可以进入某个原生或者Web页面
      • 比如后端返回的商品卡片,点击商品进入某个原生或者Web页面
    2. 比如活动页面,点击按钮进入某个原生或者Web页面
    3. 比如原生页面的某个按钮,点击按钮进入某个原生或者Web页面

    总结起来也就两种,

    1. 一种是远程调用,
    2. 一种是app内部调用。

    所谓远程调用就是app提供的跳转能力,允许外面调用的。再者理解,可以被别的app打开调用,比如微信的分享,支付等。相对的内部调用就是app内部由A页面跳转到B页面的。

    所以针对上面的用处,从命名上可以做好区分,比如内部调用加native://做为开头,表示是内部调用。外部就加weixinapp://(用app名更容易调用者理解),或者加http/https,毕竟可以直接兼容http://www.baidu.com 这样的网页

    之所以要好明确区分,是因为可以利用路由做好统一的权限管理。比如外部调用可以加某一种校验后直接打开,内部调用就加另一种检验,特别是内部跳转要做好权限控制,确保真的是你自己的app调用的内部调用才能打开,防止别人只是用URL Schemes就打开了你的内部页面。

    想到这,那是不是可以加多种前缀呢,答案肯定是可以的,具体看不同公司的业务。这里就先加两种先。 如下:

    keyvalue
    native://to_home_pageHomeViewController
    native://to_buy_pageBuyViewController
    httpWebViewController
    httpsWebViewController
    ......

    上面是说,

    1. 当key是native://to_home_page的时候,就进入主页,打开HomeViewController这个页面。
    2. 当key是http的时候。就进入网页,打开WebViewController这个页面渲染。

    看到这,那么路由的定义也就出来了。 统一的入口和传参,如:

    YYRouter.push(jumpParams: [:])

    然后调用上面的路由表如下:

    YYRouter.push(jumpParams: ["to":"native://to_home_page"]) // 去到首页
    YYRouter.push(jumpParams: ["to":"http://www.baidu.com"]) // 打开网页
    YYRouter.push(jumpParams: ["to":"https://www.baidu.com"]) // 打开网页

    想传参数,那就这样。

    YYRouter.push(jumpParams: ["to":"native://to_home_page", "name": "名字"])
    YYRouter.push(jumpParams: ["to":"http://www.baidu.com&a=1&b=2", "name": "名字"])
    YYRouter.push(jumpParams: ["to":"https://www.baidu.com&c=3&d=4", "name": "名字"])

    终上一个路由的定义就出来了。

    下一编,再讲一个路由的具体实现。 Swift路由组件(二)路由的实现

    链接:https://juejin.cn/post/7032164814210203685/

    收起阅读 »

    Metal 框架之设置加载和存储操作

    iOS
    Metal 框架之设置加载和存储操作「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」 概述 通过设置 MTLLoadAction 和 MTLStoreAction 属性,可以定义渲染通道加载和存储 MTLRenderPassAtt...
    继续阅读 »

    Metal 框架之设置加载和存储操作


    「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战


    概述


    通过设置 MTLLoadAction 和 MTLStoreAction 属性,可以定义渲染通道加载和存储 MTLRenderPassAttachmentDescriptor 对象 的方式。为渲染目标设置适当的操作,在渲染通道的开始(加载)或结束(存储)时,可以避免昂贵且不必要的工作。


    在 texture 属性上设置渲染目标的纹理,在 loadAction 和 storeAction 属性上设置它的动作:



    let renderPassDescriptor = MTLRenderPassDescriptor()


    // Color render target

    renderPassDescriptor.colorAttachments[0].texture = colorTexture

    renderPassDescriptor.colorAttachments[0].loadAction = .clear

    renderPassDescriptor.colorAttachments[0].storeAction = .store

    // Depth render target

    renderPassDescriptor.colorAttachments[0].texture = depthTexture

    renderPassDescriptor.colorAttachments[0].loadAction = .dontCare

    renderPassDescriptor.colorAttachments[0].storeAction = .dontCare


    // Stencil render target

    renderPassDescriptor.colorAttachments[0].texture = stencilTexture

    renderPassDescriptor.colorAttachments[0].loadAction = .dontCare

    renderPassDescriptor.colorAttachments[0].storeAction = .dontCare



    选择加载操作


    有多个加载操作选项可用,选择哪一个选项,取决于渲染目标的加载需求。



    • 不需要渲染目标的先前内容,而是渲染到其所有像素时,选择 MTLLoadAction.dontCare


    此操作不会产生任何成本,并且在渲染通道开始时像素值始终未定义。


    不需要考虑加载操作.png



    • 不需要渲染目标的先前内容,只需渲染其部分像素时,选择 MTLLoadAction.clear


    此操作会产生将渲染目标的清除值写入每个像素的成本。


    清楚成本.png



    • 需要渲染目标的先前内容,并且只渲染到它的一些像素时,选择 MTLLoadAction.load


    此操作会产生从内存中加载每个像素的先前值的成本,明显慢于 MTLLoadAction.dontCare 或 MTLLoadAction.clear。


    加载成本.png


    选择存储操作


    有多个存储操作选项可用,选择哪一个选项,取决渲染目标的存储需求。



    • 不需要保留渲染目标的内容,选择 MTLStoreAction.dontCare


     此操作不会产生任何成本,并且在渲染通道结束时像素值始终未定义。 在渲染通道中,为中间渲染目标选择此操作,之后不需要该中间的结果。 对于深度和模板渲染目标这是正确的选择。


    不关心存储.png



    • 确实需要保留渲染目标的内容,选择 MTLStoreAction.store


    此操作将每个像素的值存储到内存,会产生成本。 对于可绘制对象,这始终是正确的选择。


    存储成本.png



    • 渲染目标是多重采样纹理


    当执行多重采样时,可以选择存储渲染目标的多重采样或解析数据。 对于多重采样数据,其存储在渲染目标的 texture 属性中。 对于解析的数据,其存储在渲染目标的 resolveTexture 属性中。 多重采样时,请参考此表选择存储操作: 





































    多重采样数据存储解析数据存储需要解析纹理需要的存储操作
    是 是 是  MTLStoreAction.storeAndMultisampleResolve
     MTLStoreAction.store
     MTLStoreAction.multisampleResolve
      MTLStoreAction.dontCare

    要在单个渲染通道中存储和解析多采样纹理,请始终选择 MTLStoreAction.storeAndMultisampleResolve 操作并使用单个渲染命令编码器。



    • 需要推迟存储选择 


    在某些情况下,在收集更多渲染通道信息之前,可能不知道要对特定渲染目标使用哪个存储操作。 要推迟存储操作选择,请在创建 MTLRenderPassAttachmentDescriptor 对象时设置 MTLStoreAction.unknown 值。 设置未知的存储操作,可以避免消耗潜在的成本(因为设置另一个存储操作成本更高)。 但是,在完成对渲染通道的编码之前,必须指定有效的存储操作; 否则,会发生错误。


    评估渲染通道之间的操作


    可以在多个渲染过程中使用相同的渲染目标。 对于任何两个渲染通道之间的同一渲染目标,可能有多种加载和存储组合,选择哪一种组合,取决于渲染目标从一个渲染通道到另一个渲染通道的需求。



    • 下一个渲染通道中,不需要渲染目标的先前内容 


    在第一个渲染通道中,选择 MTLStoreAction.dontCare 以避免存储渲染目标的内容。 在第二个渲染通道中,选择 MTLLoadAction.dontCare 或 MTLLoadAction.clear 以避免加载渲染目标的内容。


    渲染通道的评估1.png


    渲染通道的评估2.png



    • 需要在下一个渲染通道中使用渲染目标的先前内容


    在第一个渲染通道中,选择 MTLStoreAction.store、MTLStoreAction.multisampleResolve 或 MTLStoreAction.storeAndMultisampleResolve 来存储渲染目标的内容。 在第二个渲染通道中,选择 MTLLoadAction.load 以加载渲染目标的内容。


    渲染通道间内容传递.png


    总结


    本文介绍了根据渲染的需要,来设置渲染目标的加载和存储操作,合理的设置这些操作,可以避免昂贵且不必要的工作。通过图文详细介绍了,根据不同的渲染需求,设置不同的加载和存储操作达到的渲染效果。


    作者:__sky
    链接:https://juejin.cn/post/7033731322850148366
    来源:稀土掘金

    收起阅读 »

    iOS 封装一个简易 UITableView 链式监听点击事件的功能思路与实现

    iOS
    废话开篇:RxSwift 对于其功能可以说是 swift 语言的高度封装了,但是它里面也用到了一些 OC 特性,比如交换方法实现。RxSwift 对于 UITableView 的点击事件就进行了二次封装,里面就交换了 respondsToSelector 方法...
    继续阅读 »

    废话开篇:RxSwift 对于其功能可以说是 swift 语言的高度封装了,但是它里面也用到了一些 OC 特性,比如交换方法实现。RxSwift 对于 UITableView 的点击事件就进行了二次封装,里面就交换了 respondsToSelector 方法及重写了消息转发机制下的 forwardInvocationRxSwift 源码太复杂,因此,简单用 OC 写一个 demo,来理解一下 RxSwift 对于 UITableView 的点击事件的绑定。


    1、实现原理


    1、修改一个对象的 respondsToSelector 方法,当进来判断的 sel 是要求继续进行的方法时,返回 YES,这里很明显就是判断 tableView:didSelectRowAtIndexPath: 这个方法。这里注意的是,一个对象即使没有遵循代理协议而只要你实现了代理方法,那么,它也是可以正常执行的,代理协议的遵守只是方便开发通过编译器提示去实现代理方法的。


    也就是说,让对象作为 UITableView 可执行 tableView:didSelectRowAtIndexPath: 方法的代理,但是不去实现这个代理方法。


    2、修改一个对象的 forwardInvocation 方法,当一个对象调用方法出现没有实现的时候就要进行消息转发了,那么,在转发的时候截获 tableView:didSelectRowAtIndexPath: 方法的参数,进而转到别的对象去执行后续操作。


    2、代码效果


    image.png


    当点击 cell 的时候,就通过上图中的 block 进行响应。这里其实从风格上有点类似 RX,但是,这里并没有对创建中对象的内存进行管理,下面有提到,一般 RxSwift 会有 Disposable 对象的返回,它就是来控制序列中创建的对象何时释放的类,可以用属性保存 DisposeBag,让序列与当前使用类生命周期一致,也可以在方法执行的最后面直接执行 dispose 销毁。


    3、UITableView 的 rxRegistSelected 方法的实现


    这里创建一个 UITableView 的分类:


    UITableView+KDS.h


    image.png


    UITableView+KDS.m


    image.png


    这里圈出1KDSDelegateProxy 类就是 tableView:didSelectRowAtIndexPath: 代理方法处理类,并且圈出2UITableView 提供了一个 delegate,并在此之前调用 saveNeedDelegateSel 保存了需要消息转发的代理方法,这个方法后面解释。


    到了这里,UITableView 就可以执行 rxRegistSelected 这个方法了,并且要为这个方法返回的 block1 传一个 UITableViewCell 点击事件响应的 block2block2 是真正执行的点击事件具体实现,block1 仅为仿写 RAC 而写。


    4、KDSDelegateProxy 对象的实现内容


    KDSDelegateProxy.h


    image.png


    KDSDelegateProxy.m


    image.png


    image.png


    4、KDSTableViewDelegateProxy


    KDSTableViewDelegateProxy 是遵循 UITableViewDelegate 协议的对象,并为该对象保存外界传进来的 cell 点击的代理方法


    image.png


    5、总结与思考


    RxSwift 远比上述复杂的多,换句话说个人能力有限说很难从一个百米高楼中去推断夯实地基的具体细节,因为毕竟不是参与施工人员,所以,这里也仅仅是个人思路。那么,说一下为什么没有类似 Disposable 对象,因为 demo 代码的的对象用的是 static 修饰的,如果不用全局变量,那么,作用域外对象就会销毁,代码也就无法运行了,所以,完全可以封装一个 WSLDisposable 类,在最里层的 block 里作为返回值,在最外层进行 dispose 销毁操作,来临时控制中间过程中的对象生命周期。或者封装类似 WSLDisposeBag,将它作为属性保存在例如控制器下,生命周期与当前 控制器一致。


    好了,文章本意也仅分享,代码拙劣,大神勿笑。


    作者:头疼脑胀的代码搬运工
    链接:https://juejin.cn/post/7033679440613736456
    收起阅读 »

    iOS中的事件

    iOS
    iOS中的事件「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」iOS中的事件在用户使用APP过程中,会产生各种各样的事件,可以分为三大类触摸事件(如点击...)加速器事件(如摇一摇...)远程控制事件(如耳机可以控制手机音量......
    继续阅读 »

    iOS中的事件

    「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

    iOS中的事件

    • 在用户使用APP过程中,会产生各种各样的事件,可以分为三大类
    • 触摸事件(如点击...)
    • 加速器事件(如摇一摇...)
    • 远程控制事件(如耳机可以控制手机音量...)

    响应者对象(UIResponder)

    说到触摸事件,首先需要了解一个概念:响应者对象

    • 在iOS中不是任何对象都能处理事件,只有继承了 UIResponder 的对象才能接收并处理事件,通常被称为“响应者对象”。如 UIApplicationUIViewControllerUIView 等等

    • UIResponder 内部提供了以下方法来处理事件

    • 触摸事件

      - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
      - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
      - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
      - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

    • 加速器事件

      - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
      - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
      - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

    • 远程控制方法

      - (void)remoteControlReceivedWithEvent:(UIEvent *)event;


    UIView 的触摸事件处理

    • UIView 是 UIResponder 的子类,可以覆盖以下4个方法处理不同的触摸事件

    • 一根或者多根手指开始触摸 view,系统会自动调用 view 的下面方法

      - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;

    • 一根或者多根手指在 view 上移动,系统会自动调用 view 的下面方法

      - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

    • 一根或者多根手指离开 view,系统会自动调用 view 的下面方法

          - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

    • 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用 view 的下面方法

      - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;


    UITouch

    • 当用户用一根手指触摸屏幕时,会创建一个与手指关联的 UITouch 对象
    • 一根手指对应一个 UITouch 对象
    • UITouch 的作用:保存着跟手指相关的信息,比如触摸的位置、时间、阶段
    • 当手指移动时,系统会更新同一个 UITouch 对象,使之能够一直保存该手指在的触摸位置
    • 当手指离开屏幕时,系统会销毁相应的 UITouch 对象
    • UITouch 相关属性
      • window 触摸产生时所处的窗口
      • view 触摸产生时所处的视图
      • tapCount 短时间内按屏幕的次数,根据 tapCount 判断单击、双击或更多点击
      • timestamp 记录了触摸事件产生或变化的时间,单位是秒
      • phase 当前触摸事件所处的状态
    • UITouch 相关方法
      • 返回值表示触摸在 view 上的位置,这里返回的位置是针对view的坐标系的(以 view 的左上角为原点(0,0)),调用时如果传入的 view 参数是 nil 的话,返回的时触摸点在 window 的位置

        [touch locationInView:view];

      • 该方法记录了上一个点的位置

        [touch previousLocationInView:view];

    注:
    iPhone开发中,要避免使用双击事件
    如果要在一个 view 中监听多个手指,需要设置属性

    //需要view支持多个手
    view.multipleTouchEnabled = YES;

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"%ld",(long)touches.count); //2
    }


    UIEvent

    • 每产生一个事件,就会产生一个 UIEvent 对象
    • UIEvent 被称为事件对象,用于记录事件产生的时刻和类型
    • UIEvent 相关属性
      • 事件类型
        • type 枚举类型(触摸事件、加速器事件、远程控制事件)
        • subtype
      • timestamp 事件产生时间
    • UIEvent 相关方法
      • UIEvent 提供相应方法用于获取在某个 view 上面的接触对象(UITouch

    简单示例

    实现需求:一个按钮可以在屏幕任务拖拽

    Kapture 2021-11-22 at 22.49.40.gif

    1.自定义一个 UIImageView

    @implementation InputImageView

    - (instancetype)initWithFrame:(CGRect)frame{
        self = [super initWithFrame:frame];
        if (self) {        
            UIImage *image = [UIImage imageNamed:@"inputButton"];
            self.image = image;
            self.userInteractionEnabled = YES;
        }
        return self;
    }
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UITouch *touch = touches.anyObject;
    //获取当前点
    CGPoint currentPoint = [touch locationInView:self];
    //获取上一个点的位置
    CGPoint previousPoint = [touch previousLocationInView:self];
    //获取x轴偏移量
        CGFloat offsetX = currentPoint.x - previousPoint.x;
        CGFloat offsetY = currentPoint.y - previousPoint.y;
        //修改view的位置
        self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
    }
    @end

    2.实际调用

    #import "ViewController.h"
    #import "InputImageView.h"

    @interface ViewController ()
    @property (nonatomic,strong) InputImageView *redView;
    @end

    @implementation ViewController

    - (void)viewDidLoad {
        [super viewDidLoad];
        InputImageView *inputImageView = [[InputImageView alloc]initWithFrame:CGRectMake(150, 150, 56, 56)];
        [self.view addSubview:inputImageView];
    }
    @end


    收起阅读 »

    iOS 获取图片的主题色

    iOS
    iOS 获取图片的主题色目录1.需求背景2.代码部分3.使用效果及代码地址需求背景有时候我们会有这样的需求,用户从相册选择一张照片,返回展示的时候,除了展示照片还要让整体背景也是和照片相近颜色,最近自己写了一个图片加水印的项目,想加上此功能,然鹅谷歌搜了一圈发...
    继续阅读 »

    iOS 获取图片的主题色

    目录

    1.需求背景
    2.代码部分
    3.使用效果及代码地址

    需求背景

    • 有时候我们会有这样的需求,用户从相册选择一张照片,返回展示的时候,除了展示照片还要让整体背景也是和照片相近颜色,最近自己写了一个图片加水印的项目,想加上此功能,然鹅谷歌搜了一圈发现全是OC代码写的,直接使用好像还存在一些问题,所以本文分别用swift和OC实现相关功能。

    代码部分

    • 主要逻辑:
    1. 将图片按比例缩小,因为后续遍历图片每个像素点,循环次数是图片width x height,如果直接原图去遍历,可能一次循环就要跑几十万、百万次,需要时间非常久,所以要将图片缩小。
    2. 获取图片的所有像素的RGB值,每组RGB使用数组存储(可以根据自己的需求过滤部分颜色),然后用Set将数组装起来。
    3. 统计Set里面相同次数最多的色值,即是整个图片的主题色

    swift实现代码:

    ssss.png

    调用:

    selectedImage.subjectColor({[unowned self] color in
    guard let subjectColor = color else { return }
    self.view.backgroundColor = subjectColor
    })

    因为里面是两个for循环,时间复杂度是On^2,如果设置的width和Height比较大的话,会比较耗时,在主线程里面执行可能会卡住,所以使用了gcd开启子线程去执行,完成后回到主线程执行回调。

    OC实现代码:

    Snipaste_2021-11-24_16-19-45.png

    使用效果及代码地址

    54786116-bc1d-45ab-8eb7-2af3fe2a5520.gif

    demon地址

    收起阅读 »

    Fiddler抓取抖音视频数据

    本文仅供参考学习,禁止用于任何形式的商业用途,违者自行承担责任。准备工作:手机(安卓、ios都可以)/安卓模拟器,今天主要以安卓模拟器为主,操作过程一致。抓包工具:Fiddel 下载地址:(https://www.telerik.com/download/fi...
    继续阅读 »



    本文仅供参考学习,禁止用于任何形式的商业用途,违者自行承担责任。

    准备工作:

    1. 手机(安卓、ios都可以)/安卓模拟器,今天主要以安卓模拟器为主,操作过程一致。

    2. 抓包工具:Fiddel 下载地址:(https://www.telerik.com/download/fiddler

    3. 编程工具:pycharm

    4. 安卓模拟器上安装抖音(逍遥安装模拟器)

    一、fiddler配置

    在tools中的options中,按照图中勾选后点击Actions


    配置远程链接:

    选择允许监控远程链接,端口可以随意设置,只要别重复就行,默认8888


    然后:重启fiddler!!!这样配置才能生效。

    二、安卓模拟器/手机配置

    首先查看本机的IP:在cmd中输入ipconfig,记住这个IP


    手机确保和电脑在同一局域网下。

    手机配置:配置已连接的WiFi,代理选择手动,然后输入上图ip端口号为8888

    模拟器配置:设置中长按已连接wifi,代理选择手动,然后输入上图ip端口号为8888



    代理设置好后,在浏览器中输入你设置的ip:端口,例如10.10.16.194:8888,就会打开fiddler的页面。然后点击fiddlerRoot certificate安装证书,要不手机会认为环境不安全。

    证书名称随便设,可能还需要设置一个锁屏密码。


    接下来就可以在fiddler中抓到手机/模拟器软件的包了。

    三、抖音抓包

    打开抖音,然后观察fiddler中所有的包


    其中有个包,包类型为json(json就是网页返回的数据,具体百度),主机地址如图,包大小一般不小,这个就是视频包。

    点击这个json包,在fidder右侧,点击解码,我们将视频包的json解码

    解码后:点击aweme_list,其中每个大括号代表一个视频,这个和bilibili弹幕或者快手一样,每次加载一点出来,等你看完预加载的,再重新加载一些。


    Json是一个字典,我们的视频链接在:aweme_list中,每个视频下的video下的play_addr下的url_list中,一共有6个url,是完全一样的视频,可能是为了应付不同环境,但是一般第3或4个链接的视频不容易出问题,复制链接,浏览器中粘贴就能看到视频了。


    接下来解决几个问题

    1、视频数量,每个包中只有这么几个视频,那如何抓取更多呢?

    这时候需要借助模拟器的模拟鼠标翻页,让模拟器一直翻页,这样就不断会出现json包了。


    2、如何json保存在本地使用

    一种方法可以手动复制粘贴,但是这样很low。

    所以我们使用fidder自带的脚本,在里面添加规则,当视频json包刷出来后自动保存json包。

    自定义规则包:

    链接:https://pan.baidu.com/s/1wmtUUMChzuSDZFYGSyUhCg

    提取码:7z0l

    点击规则脚本,然后将自定义规则放在如图所示位置:


    这个脚本有两点需要修改的:

    (1)第一行的网址

    这个是从视频包的url中摘出来的,抖音会时不时更新这个url,所以不能用了也要去更新:

    比如现在的已经和昨天不同了,记着修改。

    (2)路径,那个是我设置json包保存的地址,自己一定要去修改,并创建文件夹,修改完记着点保存。


    打开设置好模拟器和脚本后,等待一会,就可以看到文件夹中保存的包了:

    四、爬虫脚本

    接下来在pycharm中写脚本获取json包里的视频链接:

    导包:

    import os,json,requests

    伪装头:

    headers = {‘User-Agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36’}

    逻辑代码:

    运行代码:


    效果:

    源码:

    import os, json, requests# 伪装头
    headers = {
       'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
    }
    videos_list = os.listdir(
       'C:/Users/HEXU/Desktop/抖音数据爬取/抖音爬取资料/raw_data/')# 获取文件夹内所有json包名
    count = 1# 计数, 用来作为视频名字
    for videos in videos_list: #循环json列表, 对每个json包进行操作
    a = open('./抖音爬取资料/raw_data/{}'.format(videos), encoding = 'utf-8')# 打开json包
    content = json.load(a)['aweme_list']# 取出json包中所有视频
    for video in content: #循环视频列表, 选取每个视频
    video_url = video['video']['play_addr']['url_list'][4]# 获取视频url, 每个视频有6个url, 我选的第5个
    videoMp4 = requests.request('get', video_url, headers = headers).content# 获取视频二进制代码
    with open('./抖音爬取资料/VIDEO/{}.mp4'.format(count), 'wb') as f: #
       以二进制方式写入路径, 记住要先创建路径
    f.write(videoMp4)# 写入
    print('视频{}下载完成'.format(count))# 下载提示
    count += 1# 计数 + 1
    作者:冬晨夕阳
    来源:https://blog.51cto.com/lixi/3022373 收起阅读 »

    不想加班,你就背会这 10 条 JS 技巧

    为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧1. 数组分割const listChunk = (list = [], chunkSize = 1) => {const result = [];const tmp = [...l...
    继续阅读 »



    为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧

    1. 数组分割

    const listChunk = (list = [], chunkSize = 1) => {
    const result = [];
    const tmp = [...list];
    if (!Array.isArray(list) || !Number.isInteger(chunkSize) || chunkSize <= 0) {
    return result;
      };
    while (tmp.length) {
    result.push(tmp.splice(0, chunkSize));
      };
    return result;
    };
    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
    // [['a'], ['b'], ['c'], ['d'], ['e'], ['f'], ['g']]

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3);
    // [['a', 'b', 'c'], ['d', 'e', 'f'], ['g']]

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 0);
    // []

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], -1);
    // []

    2. 求数组元素交集

    const listIntersection = (firstList, ...args) => {
    if (!Array.isArray(firstList) || !args.length) {
    return firstList;
      }
    return firstList.filter(item => args.every(list => list.includes(item)));
    };
    listIntersection([1, 2], [3, 4]);
    // []

    listIntersection([2, 2], [3, 4]);
    // []

    listIntersection([3, 2], [3, 4]);
    // [3]

    listIntersection([3, 4], [3, 4]);
    // [3, 4]

    3. 按下标重新组合数组

    const zip = (firstList, ...args) => {
    if (!Array.isArray(firstList) || !args.length) {
    return firstList
      };
    return firstList.map((value, index) => {
    const newArgs = args.map(arg => arg[index]).filter(arg => arg !== undefined);
    const newList = [value, ...newArgs];
    return newList;
      });
    };
    zip(['a', 'b'], [1, 2], [true, false]);
    // [['a', 1, true], ['b', 2, false]]

    zip(['a', 'b', 'c'], [1, 2], [true, false]);
    // [['a', 1, true], ['b', 2, false], ['c']]

    4. 按下标组合数组为对象

    const zipObject = (keys, values = {}) => {
    const emptyObject = Object.create({});
    if (!Array.isArray(keys)) {
    return emptyObject;
      };
    return keys.reduce((acc, cur, index) => {
    acc[cur] = values[index];
    return acc;
      }, emptyObject);
    };
    zipObject(['a', 'b'], [1, 2])
    // { a: 1, b: 2 }
    zipObject(['a', 'b'])
    // { a: undefined, b: undefined }

    5. 检查对象属性的值

    const checkValue = (obj = {}, objRule = {}) => {
    const isObject = obj => {
    return Object.prototype.toString.call(obj) === '[object Object]';
      };
    if (!isObject(obj) || !isObject(objRule)) {
    return false;
      }
    return Object.keys(objRule).every(key => objRule[key](obj[key]));
    };

    const object = { a: 1, b: 2 };

    checkValue(object, {
    b: n => n > 1,
    })
    // true

    checkValue(object, {
    b: n => n > 2,
    })
    // false

    6. 获取对象属性

    const get = (obj, path, defaultValue) => {
    if (!path) {
    return;
      };
    const pathGroup = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);
    return pathGroup.reduce((prevObj, curKey) => prevObj && prevObj[curKey], obj) || defaultValue;
    };

    const obj1 = { a: { b: 2 } }
    const obj2 = { a: [{ bar: { c: 3 } }] }

    get(obj1, 'a.b')
    // 2
    get(obj2, 'a[0].bar.c')
    // 3
    get(obj2, ['a', '0', 'bar', 'c'])
    // 2
    get(obj1, 'a.bar.c', 'default')
    // default
    get(obj1, 'a.bar.c', 'default')
    // default

    7. 将特殊符号转成字体符号

    const escape = str => {
    const isString = str => {
    return Object.prototype.toString.call(str) === '[string Object]';
      };
    if (!isString(str)) {
    return str;
      }
    return (str.replace(/&/g, '&')
    .replace(/"/g, '"')
    .replace(/'/g, '&#x27;')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/\//g, '&#x2F;')
    .replace(/\\/g, '&#x5C;')
    .replace(/`/g, '&#96;'));
    };

    8. 利用注释创建一个事件监听器

    class EventEmitter {
    #eventTarget;
    constructor(content = '') {
    const comment = document.createComment(content);
    document.documentElement.appendChild(comment);
    this.#eventTarget = comment;
      }
    on(type, listener) {
    this.#eventTarget.addEventListener(type, listener);
      }
    off(type, listener) {
    this.#eventTarget.removeEventListener(type, listener);
      }
    once(type, listener) {
    this.#eventTarget.addEventListener(type, listener, { once: true });
      }
    emit(type, detail) {
    const dispatchEvent = new CustomEvent(type, { detail });
    this.#eventTarget.dispatchEvent(dispatchEvent);
      }
    };

    const emmiter = new EventEmitter();
    emmiter.on('biy', () => {
    console.log('hello world');
    });
    emmiter.emit('biu');
    // hello world

    9. 生成随机的字符串

    const genRandomStr = (len = 1) => {
    let result = '';
    for (let i = 0; i < len; ++i) {
    result += Math.random().toString(36).substr(2)
      }
    return result.substr(0, len);
    }
    genRandomStr(3)
    // u2d
    genRandomStr()
    // y
    genRandomStr(10)
    // qdueun65jb

    10. 判断是否是指定的哈希值

    const isHash = (type = '', str = '') => {
    const isString = str => {
    return Object.prototype.toString.call(str) === '[string Object]';
      };
    if (!isString(type) || !isString(str)) {
    return str;
      };
    const algorithms = {
    md5: 32,
    md4: 32,
    sha1: 40,
    sha256: 64,
    sha384: 96,
    sha512: 128,
    ripemd128: 32,
    ripemd160: 40,
    tiger128: 32,
    tiger160: 40,
    tiger192: 48,
    crc32: 8,
    crc32b: 8,
      };
    const hash = new RegExp(`^[a-fA-F0-9]{${algorithms[type]}}$`);
    return hash.test(str);
    };

    isHash('md5', 'd94f3f016ae679c3008de268209132f2');
    // true
    isHash('md5', 'q94375dj93458w34');
    // false

    isHash('sha1', '3ca25ae354e192b26879f651a51d92aa8a34d8d3');
    // true
    isHash('sha1', 'KYT0bf1c35032a71a14c2f719e5a14c1');
    // false

    后记

    如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。

    全文完

    作者:酸菜鱼+黄焖鸡

    来源:https://blog.51cto.com/u_15291238/4538068

    收起阅读 »

    Three.js 随着元宇宙开启WEB3D之路

    元宇宙设想了一个由虚拟世界和3D技术广泛应用重塑的未来。 Three.js 是一个非常令人印象深刻的 JavaScript 3D 库,它也使用 WebGL(或 2d Canvas)进行渲染。随着 WebGL API 标准的改进,以及对 WebXR 的支持, T...
    继续阅读 »

    当视频游戏遇到Web 2.0时会发生什么? 当虚拟世界相遇地球的地理空间地图?当模拟变成现实,生活和商业变成虚拟?当你使用虚拟地球在物理地球上导航时,你的化身就变成了你的在线代理?这一切发生的就是元宇宙。

    元宇宙设想了一个由虚拟世界和3D技术广泛应用重塑的未来。 Three.js 是一个非常令人印象深刻的 JavaScript 3D 库,它也使用 WebGL(或 2d Canvas)进行渲染。随着 WebGL API 标准的改进,以及对 WebXR 的支持, Three.js 成为了一个可以用来营造沉浸式体验的主流工具。与此同时,浏览器对 3D 渲染和 WebXR 设备 API 的支持也得到提升,使得 web 成为一个越来越有吸引力的 3D 内容平台。

    Three.js

    Three.js Ricardo Cabello(@mrdoob) 在2010年开发的一个JavaScript库(如今它在Github上有许多贡献者)。这个令人难以置信的工具让用户可以在浏览器上处理 3D 图形,使用 WebGL 技术非常简单和直观的方式来实现。而 WebGL 技术已经非常普及和被浏览器广泛支持。


    WebGL 在许多设备和浏览器中创建丰富的交互体验, 点击查看浏览器支持程度。


    开始

    本文将以 Vue 为基础框架来构建 Three.js 示例,关于代码可以查阅 GitHub

    首先安装依赖:

    npm install three --save

    构建

    现在开始添加代码到页面,如下:

    <template>
    <div id="app"></div>
    </template>

    <script>
    import * as THREE from "three";
    export default {
    name: "App",
    data() {
    return {};
    },
    mounted() {
    this.init();
    },
    methods: {
    init() {
    const scene = new THREE.Scene();

    // 创建一个基本透视相机 camera
    const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
    );
    camera.position.z = 4;

    // 创建一个抗锯齿渲染器
    const renderer = new THREE.WebGLRenderer({ antialias: true });

    // 配置渲染器清除颜色
    renderer.setClearColor("#000000");

    // 配置渲染器尺寸
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 添加渲染器到DOM
    document.body.appendChild(renderer.domElement);

    // 创建一个立方体网格
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: "#433F81" });
    const cube = new THREE.Mesh(geometry, material);

    // 将立方体到场景中
    scene.add(cube);

    const render = function () {
    requestAnimationFrame(render);

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    renderer.render(scene, camera);
    };

    render();
    },
    },
    };
    </script>

    然后再添加简单的样式:

    body {
    margin: 0;
    }
    canvas {
    width: 100%;
    height: 100%;
    }

    执行脚本 yarn serve ,打开浏览器将看到如下效果:


    这是每个 Three.js 应用程序一种常见模式,包括 WebGlRendererSceneCamera ,如下:

    1. 创建渲染器WebGlRenderer

    2. 创建 Scene

    3. 创建Camera

    渲染器是放置场景结果的地方。在 Three.js 中,可以有多个场景,每个场景可以有不同的对象。


    在示例中创建 WebGLRenderer,再将窗口的大小作为参数传递给它,然后将它附加到 DOM 中。

    // 创建一个抗锯齿渲染器
    const renderer = new THREE.WebGLRenderer({ antialias: true });

    // 配置渲染器清除颜色
    renderer.setClearColor("#000000");

    // 配置渲染器尺寸
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 添加渲染器到DOM
    document.body.appendChild(renderer.domElement);

    首先需要创建一个空场景,将在其中添加创建的立方体对象:

    const scene = new THREE.Scene();

    最后创建一个相机 camera ,将 视野 、纵横比以及近平面和远平面作为参数:

    const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
    );

    到此 Three.js 应用程序的 3 个基本元素已经完成了。

    几何、材料和网格

    第二种常见模式是向场景添加对象:

    1. 创建几何

    2. 创建材质

    3. 创建网格

    4. 将网格添加到场景中。

    在 Three.js 中,网格是几何体与材质的组合。


    几何是对象的数学公式,在 Three.js 中有很多几何,将在以后的章节中探索它,几何体提供了要添加到场景中的对象的顶点。


    材质可以定义为对象的属性及其与场景光源的行为。如下图所示,有不同类型的材料。


    现在知道了网格、几何体和材质是什么,将把它们添加到场景中。在示例中,使用基本材质创建一个立方体:

    // 创建一个立方体网格
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: "#1b55e3" });
    const cube = new THREE.Mesh(geometry, material);

    // 将立方体到场景中
    scene.add(cube);

    请求动画帧

    最后一段代码是为场景设置动画,使用 requestAnimationFrame,它允许有一个以每秒 60 帧(最多)运行的函数。

    const render = () => {
      requestAnimationFrame(render);
      renderer.render(scene, camera);
    };

    render();

    立方体动画

    为了在渲染循环中为立方体设置动画,需要更改它的属性。当创建一个网格时,可以访问一组在动画制作时非常有用的属性。

    // Rotation (XYZ) 弧度
    cube.rotation.x;
    cube.rotation.y;
    cube.rotation.z;

    // Position (XYZ)
    cube.position.x;
    cube.position.y;
    cube.position.z;

    // Scale (XYZ)
    cube.scale.x;
    cube.scale.y;
    cube.scale.z;

    在示例中,为立方体设置 X 和 Y 旋转动画:

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    控制台

    作为前端,控制台是最好的调试工具。当使用 Three.js 时,控制台是必不可少的工具。

    效果增持

    现在理解了示例的逻辑,将向场景中添加更多片段,目的是生成更复杂的片段。

    /**
    * 创建材质的方法
    */
    const createMesh = (boxOptions, meshOptions) => {
      const geometry = new THREE.BoxGeometry(...boxOptions);
      const material = new THREE.MeshBasicMaterial(meshOptions);
      return new THREE.Mesh(geometry, material);
    };

    const cube01 = createMesh([1, 1, 1], {
      color: "#A49FEF",
      wireframe: true,
      transparent: true,
    });
    scene.add(cube01);

    const cube01_wireframe = createMesh([3, 3, 3], {
      color: "#433F81",
      wireframe: true,
      transparent: true,
    });
    scene.add(cube01_wireframe);

    const cube02 = createMesh([1, 1, 1], {
      color: "#A49FEF",
    });
    scene.add(cube02);

    const cube02_wireframe = createMesh([3, 3, 3], {
      color: "#433F81",
      wireframe: true,
      transparent: true,
    });

    scene.add(cube02_wireframe);

    const bar01 = createMesh([10, 0.05, 0.5], {
      color: "#00FFBC",
    });
    bar01.position.z = 0.5;
    scene.add(bar01);

    const bar02 = createMesh([10, 0.05, 0.5], {
      color: "#ffffff",
    });
    bar02.position.z = 0.5;
    scene.add(bar02);

    const render = () => {
      requestAnimationFrame(render);

      cube01.rotation.x += 0.01;
      cube01.rotation.y += 0.01;

      cube01_wireframe.rotation.x += 0.01;
      cube01_wireframe.rotation.y += 0.01;

      cube02.rotation.x -= 0.01;
      cube02.rotation.y -= 0.01;

      cube02_wireframe.rotation.x -= 0.01;
      cube02_wireframe.rotation.y -= 0.01;

      bar01.rotation.z -= 0.01;
      bar02.rotation.z += 0.01;

      renderer.render(scene, camera);
    };

    render();

    运行后效果如图:


    作者:天行无忌
    来源:http://blog.51cto.com/u_15088848/4670782

    收起阅读 »

    尤大亲自解释vue3源码中为什么不使用?.可选链式操作符?

    vue
    阅读本文🦀 1.什么是可选链式操作符号 2.为什么vue3源码中不使用可选链式操作符 什么是可选链式操作符号❓ 可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之...
    继续阅读 »

    阅读本文🦀


    1.什么是可选链式操作符号


    2.为什么vue3源码中不使用可选链式操作符


    什么是可选链式操作符号❓


    可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined


    当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。


    const adventurer = {
    name: 'Alice',
    cat: {
    name: 'Dinah'
    }
    };

    const dogName = adventurer.dog?.name;
    console.log(dogName);
    // expected output: undefined

    console.log(adventurer.someNonExistentMethod?.());
    // expected output: undefined


    短路效应


    如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)。


    所以,如果后面有任何函数调用或者副作用,它们均不会执行。


    let user = null; 
    let x = 0;
    user?.sayHi(x++);
    // 没有 "sayHi",因此代码执行没有触达 x++ alert(x); // 0,值没有增加

    Vue3源码中为什么不采用这么方便的操作符


    image-20211114120351836


    看看这样是不是代码更简洁了,但是为什么这个PR没有被合并呢


    来自尤大的亲自解释


    image-20211114120545284


    (我们有意避免在代码库中使用可选链,因为我们的目标是 ES2016,而 TS 会将其转换为更加冗长的内容)


    从尤大的话中我们可以得知由于Vu3打包后的代码是基于ES2016的,虽然我们在编写代码时看起来代码比较简洁了,实际打包之后反而更冗余了,这样会增大包的体积,影响Vu3的加载速度。由此可见一个优秀的前端框架真的要考虑的东西很多,语法也会考虑周到~✨



    作者:速冻鱼
    链接:https://juejin.cn/post/7033167068895641637

    收起阅读 »

    想知道一个20k级别前端在项目中是怎么使用LocalStorage的吗?

    前言 大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage,才能更规范,更高大上,更能让人眼前一亮。 用处 在平时的开发中,lo...
    继续阅读 »

    前言


    大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage,才能更规范,更高大上,更能让人眼前一亮。


    用处


    在平时的开发中,localStorage、sessionStorage的用途是非常的多的,在我们的开发中发挥着非常重要的作用:



    • 1、登录完成后token的存储

    • 2、用户部分信息的存储,比如昵称、头像、简介

    • 3、一些项目通用参数的存储,例如某个id、某个参数params

    • 4、项目状态管理的持久化,例如vuex的持久化、redux的持久化

    • 5、项目整体的切换状态存储,例如主题颜色、icon风格、语言标识

    • 6、等等、、、、、、、、、、、、、、、、、、、、、、、、、、


    普通使用


    那么,相信我们各位平时使用都是这样的(拿localStorage举例)


    1、基础变量


    // 当我们存基本变量时
    localStorage.setItem('基本变量', '这是一个基本变量')
    // 当我们取值时
    localStorage.getItem('基本变量')
    // 当我们删除时
    localStorage.removeItem('基本变量')

    2、引用变量


    // 当我们存引用变量时
    localStorage.setItem('引用变量', JSON.stringify(data))
    // 当我们取值时
    const data = JSON.parse(localStorage.getItem('引用变量'))
    // 当我们删除时
    localStorage.removeItem('引用变量')

    3、清空


    localStorage.clear()

    暴露出什么问题?


    1、命名过于简单



    • 1、比如我们存用户信息会使用user作为 key 来存储

    • 2、存储主题的时候用theme 作为 key 来存储

    • 3、存储令牌时使用token作为 key 来存储


    其实这是很有问题的,咱们都知道,同源的两个项目,它们的localStorage是互通的。


    我举个例子吧比如我现在有两个项目,它们在同源https://www.sunshine.com下,这两个项目都需要往localStorage中存储一个 key 为name的值,那么这就会造成两个项目的name互相顶替的现象,也就是互相污染现象


    截屏2021-11-10 下午10.19.09.png


    2、时效性


    咱们都知道localStorage、sessionStorage这两个的生命周期分别是



    • localStorage:除非手动清除,否则一直存在

    • sessionStorage:生命结束于当前标签页的关闭或浏览器的关闭


    其实平时普通的使用时没什么问题的,但是给某些指定缓存加上特定的时效性,是非常重要的!比如某一天:



    • 后端:”兄弟,你一登录我就把token给你“

    • 前端:”好呀,那你应该会顺便判断token过期没吧?“

    • 后端:”不行哦,放在你前端判断过期呗“

    • 前端:”行吧。。。。。“


    那这时候,因为需要在前端判断过期,所以咱们就得给token设置一个时效性,或者是1天,或者是7天


    截屏2021-11-10 下午10.48.50.png


    3、隐秘性


    其实这个好理解,你们想想,当咱们把咱们想缓存的东西,存在localStorage、sessionStorage中,在开发过程中,确实有利于咱们的开发,咱们想看的时候也是一目了然,点击Application就可以看到。


    但是,一旦产品上线了,用户也是可以看到缓存中的东西的,而咱们肯定是会想:有些东西可以让用户看到,但是有些东西我不想让你看到


    截屏2021-11-10 下午11.02.24.png


    或者咱们在做状态管理持久化时,需要把数据先存在localStorage中,这个时候就很有必要对缓存进行加密了。


    解决方案


    1、命名规范


    我个人的看法是项目名 + 当前环境 + 项目版本 + 缓存key,如果大家有其他规则的,可以评论区告诉林三心,让林三心学学


    截屏2021-11-11 下午9.12.32.png


    2、expire定时


    思路:设置缓存key时,将value包装成一个对象,对象中有相应的时效时段,当下一次想获取缓存值时,判断有无超时,不超时就获取value,超时就删除这个缓存


    截屏2021-11-11 下午9.33.00.png


    3、crypto加密


    加密很简单,直接使用crypto-js进行对数据的加密,使用这个库里的encrypt、decrypyt进行加密、解密


    截屏2021-11-11 下午9.43.16.png


    实践


    其实实践的话比较简单啦,无非就是四步



    • 1、与团队商讨一下key的格式

    • 2、与团队商讨一下expire的长短

    • 3、与团队商讨一下使用哪个库来对缓存进行加密(个人建议crypto-js

    • 4、代码实施(不难,我这里就不写了)


    结语


    有人可能觉得没必要,但是严格要求自己其实是很有必要的,平时严格要求自己,才能做到每到一个公司都能更好的做到向下兼容难度。


    如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑


    image.png


    作者:Sunshine_Lin
    链接:https://juejin.cn/post/7033749571939336228

    收起阅读 »

    巧用渐变实现高级感拉满的背景光动画

    实现 这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。 其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。 观察这个效果: 它的核心其实就是角向渐变 -- conic...
    继续阅读 »

    141609598-e0a1e420-2967-4ce4-8086-bfef1233f5f6.gif


    实现


    这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。


    其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。


    观察这个效果:



    它的核心其实就是角向渐变 -- conic-gradient(),利用角向渐变,我们可以大致实现这样一个效果:


    <div></div>

    div {
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at 400px 300px,
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    transparent),
    linear-gradient(-45deg, #060d5e, #002268);
    }

    看看效果:



    有点那意思了。当然,仔细观察,渐变的颜色并非是由一种颜色到透明就结束了,而是颜色 A -- 透明 -- 颜色 B,这样,光源的另一半并非就不会那么生硬,改造后的 CSS 代码:


    div {
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at 400px 300px,
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    hsla(219deg, 90%, 80%, .5) 100%),
    linear-gradient(-45deg, #060d5e, #002268);
    }

    我们在角向渐变的最后多加了一种颜色,得到观感更好的一种效果:



    emm,到这里,我们会发现,仅仅是角向渐变 conic-gradient() 是不够的,它无法模拟出光源阴影的效果,所以必须再借助其他属性实现光源阴影的效果。


    这里,我们会很自然的想到 box-shadow。这里有个技巧,利用多重 box-shadow, 实现 Neon 灯的效果。


    我们再加个 div,通过它实现光源阴影:


    <div class="shadow"></div>

    .shadow {
    width: 200px;
    height: 200px;
    background: #fff;
    box-shadow:
    0px 0 .5px hsla(170deg, 95%, 80%, 1),
    0px 0 1px hsla(170deg, 91%, 80%, .95),
    0px 0 2px hsla(171deg, 91%, 80%, .95),
    0px 0 3px hsla(171deg, 91%, 80%, .95),
    0px 0 4px hsla(171deg, 91%, 82%, .9),
    0px 0 5px hsla(172deg, 91%, 82%, .9),
    0px 0 10px hsla(173deg, 91%, 84%, .9),
    0px 0 20px hsla(174deg, 91%, 86%, .85),
    0px 0 40px hsla(175deg, 91%, 86%, .85),
    0px 0 60px hsla(175deg, 91%, 86%, .85);
    }


    OK,光是有了,但问题是我们只需要一侧的光,怎么办呢?裁剪的方式很多,这里,我介绍一种利用 clip-path 进行对元素任意空间进行裁切的方法:


    .shadow {
    width: 200px;
    height: 200px;
    background: #fff;
    box-shadow: .....;
    clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
    }

    原理是这样的:



    这样,我们就得到了一侧的光:



    这里,其实 CSS 也是有办法实现单侧阴影的(你所不知道的 CSS 阴影技巧与细节),但是实际效果并不好,最终采取了上述的方案。


    接下来,就是利用定位、旋转等方式,将上述单侧光和角向渐变重叠起来,我们就可以得到这样的效果:


    image


    这会,已经挺像了。接下来要做的就是让整个图案,动起来。这里技巧也挺多的,核心还是利用了 CSS @Property,实现了角向渐变的动画,并且让光动画和角向渐变重叠起来。


    我们需要利用 CSS @Property 对代码渐变进行改造,核心代码如下:


    <div class="wrap">
    <div class="shadow"></div>
    </div>

    @property --xPoint {
    syntax: '<length>';
    inherits: false;
    initial-value: 400px;
    }
    @property --yPoint {
    syntax: '<length>';
    inherits: false;
    initial-value: 300px;
    }

    .wrap {
    position: relative;
    margin: auto;
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at var(--xPoint) var(--yPoint),
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    hsla(219deg, 90%, 80%, .5) 100%),
    linear-gradient(-45deg, #060d5e, #002268);
    animation: pointMove 2.5s infinite alternate linear;
    }

    .shadow {
    position: absolute;
    top: -300px;
    left: -330px;
    width: 430px;
    height: 300px;
    background: #fff;
    transform-origin: 100% 100%;
    transform: rotate(225deg);
    clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
    box-shadow: ... 此处省略大量阴影代码;
    animation: scale 2.5s infinite alternate linear;
    }

    @keyframes scale {
    50%,
    100% {
    transform: rotate(225deg) scale(0);
    }
    }

    @keyframes pointMove {
    100% {
    --xPoint: 100px;
    --yPoint: 0;
    }
    }

    这样,我们就实现了完整的一处光的动画:



    我们重新梳理一下,实现这样一个动画的步骤:



    1. 利用角向渐变 conic-gradient 搭出基本框架,并且,这里也利用了多重渐变,角向渐变的背后是深色背景色;

    2. 利用多重 box-shadow 实现光及阴影的效果(又称为 Neon 效果)

    3. 利用 clip-path 对元素进行任意区域的裁剪

    4. 利用 CSS @Property 实现渐变的动画效果


    剩下的工作,就是重复上述的步骤,补充其他渐变和光源,调试动画,最终,我们就可以得到这样一个简单的模拟效果:



    由于原效果是 .mp4,无法拿到其中的准确颜色,无法拿到阴影的参数,其中颜色是直接用的色板取色,阴影则比较随意的模拟了下,如果有源文件,准确参数,可以模拟的更逼真。


    完整的代码你可以戳这里:CodePen -- iPhone 13 Pro Gradient


    作者:chokcoco
    链接:https://juejin.cn/post/7033952765151805453

    收起阅读 »

    vite对浏览器的请求做了什么

    工作原理:type="module" 浏览器中ES Module原生native支持。 如果浏览器支持type="module" ,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开...
    继续阅读 »

    工作原理:

    • type="module" 浏览器中ES Module原生native支持。 如果浏览器支持type="module" ,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开发阶段不需要打包
    • 第三方依赖预打包
    • 启动一个开发服务器处理资源请求

    一图详解vite原理:


    6F2(QRN))B}6D@~KQN8CYD0.png


    浏览器做的什么事啊


    宿主文件index.html


    <script type="module" src="/src/main.js"></script>

    浏览器获取到宿主文件中的资源后,发现还要再去请求main.js文件。会再向服务端发送一次main.js的资源请求。


    image.png


    main.js


    在main中,可以发现,浏览器又再次发起对vue.js?v=d253a66cApp.vue?t=1637479953836两个文件的资源请求。


    服务器会将App.vue中的内容进行编译然后返回给浏览器,下图可以看出logo图片和文字都被编译成_hoisted_ 的静态节点。


    image.png
    从请求头中,也可以看出sfc文件已经变成浏览器可以识别的js文件(app.vue文件中要存在script内容才会编译成js)。对于浏览器来说,执行的就是一段js代码。


    image.png


    其他裸模块


    如果vue依赖中还存在其他依赖的话,浏览器依旧会再次发起资源请求,获取相应资源。


    了解一下预打包


    对于第三方依赖(裸模块)的加载,vite对其提前做好打包工作,将其放到node_modules/.vite下。当启动项目的时候,直接从该路径下下载文件。


    1637397635556.png
    通过上图,可以看到再裸模块的引入时,路径发生了改变。


    服务器做的什么事啊


    总结一句话:服务器把特殊后缀名的文件进行处理返回给前端展示


    我们可以模拟vite的devServe,使用koa中间件启动一个本地服务。


    // 引入依赖
    const Koa = require('koa')
    const app = new Koa()
    const fs = require('fs')
    const path = require('path')
    const compilerSfc = require('@vue/compiler-sfc')
    const compilerDom = require('@vue/compiler-dom')

    app.use(async (ctx) => {
    const { url, query } = ctx.request
    // 处理请求资源代码都写这
    })
    zaiz都h这z都he在
    app.listen(3001, () => {
    console.log('dyVite start!!')
    })

    请求首页index.html


     if (url === '/') {
    const p = path.join(__dirname, './index.html') // 绝对路径
    // 首页
    ctx.type = 'text/html'
    ctx.body = fs.readFileSync(p, 'utf8')
    }

    1637475203111.png


    看到上面这张图,就知道我们的宿主文件已经请求成功了。只是浏览器又给服务端发送的一个main.js文件的请求。这时,我们还需要判断处理一下main.js文件。


    请求以.js结尾的文件


    我们处理上述情况后,emmmm。。。发现main中还是存在好多其他资源请求。


    基础js文件


    main文件:


    console.log(1)

    处理main:


    else if (url.endsWith('.js')) {
       // 响应js请求
       const p = path.join(__dirname, url)
       ctx.type = 'text/javascript'
       ctx.body = rewriteImport(fs.readFileSync(p, 'utf8')) // 处理依赖函数
    }

    对main中的依赖进行处理


    你以为main里面就一个输出吗?太天真了。这样的还能处理吗?


    main文件:


    import { createApp, h } from 'vue'
    createApp({ render: () => h('div', 'helllo dyVite!') }).mount('#app')

    emmm。。。应该可以!


    我们可以将main中导入的地址变成相对地址。

    在裸模块路径添加上/@modules/。再去识别/@modules/的文件即(裸模块文件)。


    // 把能读出来的文件地址变成相对地址
    // 正则替换 重写导入 变成相对地址
    // import { createApp } from 'vue' => import { createApp } from '/@modules/vue'
    function rewriteImport(content) {
    return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
    // s0匹配字符串,s1分组内容
    // 是否是相对路径
    if (s1.startsWith('./') || s1.startsWith('/') || s1.startsWith('../')) {
    // 直接返回
    return s0
    } else {
    return ` from '/@modules/${s1}'`
    }
    })
    }

    对于第三方依赖,vite内部是使用预打包请求自己服务器/node_modules/.vite/下的内部资源。
    我们可以简单化一点,将拿到的依赖名去客户端下的node_modules下拿相应的资源。


      else if (url.startsWith('/@modules/')) {
    // 裸模块的加载
    const moduleName = url.replace('/@modules/', '')
    const pre![1637477009328](imgs/1637477009328.png)![1637477009368](imgs/1637477009368.png)的地址
    const module = require(prefix + '/package.json').module
    const filePath = path.join(prefix, module) // 拿到文件加载的地址
    // 读取相关依赖
    const ret = fs.readFileSync(filePath, 'utf8')
    ctx.type = 'text/javascript'
    ctx.body = rewriteImport(ret) //依赖内部可能还存在依赖,需要递归
    }

    在main中进行render时,会报下图错误:


    1637477015346.png


    我们加载的文件都是服务端执行的库,内部可能会产生node环境的代码,需要判断一下环境变量。如果开发时,会输出一些警告信息,但是在前端是没有的。所以我们需要mock一下,告诉浏览器我们当前的环境。


    给html加上process环境变量。


      <script>
       window.process = { env: { NODE_ENV: 'dev' } }
     </script>

    此时main文件算是加载出来了。


    但是这远远打不到我们的目的啊!


    我们需要的是可以编译vue文件的服务器啊!


    处理.vue文件


    main.js文件:


    import { createApp, h } from 'vue'
    import App from './App.vue'
    createApp(App).mount('#app')

    在vue文件中,它是模块化加载的。


    1637477806326.png


    我们需要在处理vue文件的时候,对.vue后面携带的参数做处理。


    在此,我们简化只考虑template和sfc情况。


    else if (url.indexOf('.vue') > -1) {
    // 处理vue文件 App.vue?vue&type=style&index=0&lang.css
    // 读取vue内容
    const p = path.join(__dirname, url.split('?')[0])
    // compilerSfc解析sfc 获得ast
    const ret = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
    // App.vue?type=template
    // 如果请求没有query.type 说明是sfc
    if (!query.type) {
    // 处理内部的script
    const scriptContent = ret.descriptor.script.content
    // 将默认导出配置对象转为常量
    const script = scriptContent.replace(
    'export default ',
    'const __script = ',
    )
    ctx.type = 'text/javascript'
    ctx.body = `
    ${rewriteImport(script)}
    // template解析转换为单独请求一个资源
    import {render as __render} from '${url}?type=template'
    __script.render = __render
    export default __script
    `
    } else if (query.type === 'template') {
    const tpl = ret.descriptor.template.content
    // 编译包含render模块
    const render = compilerDom.compile(tpl, { mode: 'module' }).code
    ctx.type = 'text/javascript'
    ctx.body = rewriteImport(render)
    }
    }

    处理图片路径


    直接从客户端读取返回。


     else if (url.endsWith('.png')) {
       ctx.body = fs.readFileSync('src' + url)
    }


    作者:ClyingDeng
    链接:https://juejin.cn/post/7033713960784248868

    收起阅读 »

    别再问我 new 字符串创建了几个对象了!我来证明给你看!

    我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有...
    继续阅读 »

    我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有拿出干掉对方的证据,这就让我们这帮吃瓜群众们陷入了两难之中,不知道到底该信谁得。


    但是今天,老王就斗胆和大家聊聊这个话题,顺便再拿出点证据


    以目前的情况来看,关于 new String("xxx") 创建对象个数的答案有 3 种:



    1. 有人说创建了 1 个对象;

    2. 有人说创建了 2 个对象;

    3. 有人说创建了 1 个或 2 个对象。


    而出现多个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。


    那我们就先来说说这个「字符串常量池」。


    字符串常量池


    字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。


    字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:


    字符串常量池示意图.png


    以上说法可以通过如下代码进行证明:


    public class StringExample {
    public static void main(String[] args) {
    String s1 = "Java";
    String s2 = "Java";
    System.out.println(s1 == s2);
    }
    }

    以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。


    在这里我们顺便说一下字符串常量池的再不同 JDK 版本的变化。


    这里,顺便送大家一份经典学习资料,我把大学和工作中用的经典电子书库(包含数据结构、操作系统、C++/C、网络经典、前端编程经典、Java相关、程序员认知、职场发展)、面试找工作的资料汇总都打包放在这。



    戳这里直接获取:


    计算机经典必读书单(含下载方式)


    Java 入门到精通含面试最全资料包(含下载方式)


    常量池的内存布局


    JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上


    JDK 1.7 内存布局如下图所示:


    JDK 1.7 内存布局.png


    JDK 1.8 内存布局如下图所示:


    JDK 1.8 内存布局.png


    JDK 1.8 与 JDK 1.7 最大的区别是 JDK 1.8 将永久代取消,并设立了元空间。官方给的说明是由于永久代内存经常不够用或发生内存泄露,会爆出 java.lang.OutOfMemoryError: PermGen 的异常,所以把将永久区废弃而改用元空间了,改为了使用本地内存空间,官网解释详情:openjdk.java.net/jeps/122


    答案解密


    认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。


    认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。


    认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:


    new 字符串常量池.png


    老王认为正确的答案:创建 1 个或者 2 个对象


    技术论证


    解铃还须系铃人,回到问题的那个争议点上,new String 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:


    public class StringExample {
    public static void main(String[] args) {
    String s1 = new String("javaer-wang");
    String s2 = "wang-javaer";
    String s3 = "wang-javaer";
    }
    }

    首先我们使用 javac StringExample.java 编译代码,然后我们再使用 javap -v StringExample 查看编译的结果,相关信息如下:


    Classfile /Users/admin/github/blog-example/blog-example/src/main/java/com/example/StringExample.class
    Last modified 2020年4月16日; size 401 bytes
    SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e
    Compiled from "StringExample.java"
    public class com.example.StringExample
    minor version: 0
    major version: 58
    flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    this_class: #16 // com/example/StringExample
    super_class: #2 // java/lang/Object
    interfaces: 0, fields: 0, methods: 2, attributes: 1
    Constant pool:
    #1 = Methodref #2.#3 // java/lang/Object."<init>":()V
    #2 = Class #4 // java/lang/Object
    #3 = NameAndType #5:#6 // "<init>":()V
    #4 = Utf8 java/lang/Object
    #5 = Utf8 <init>
    #6 = Utf8 ()V
    #7 = Class #8 // java/lang/String
    #8 = Utf8 java/lang/String
    #9 = String #10 // javaer-wang
    #10 = Utf8 javaer-wang
    #11 = Methodref #7.#12 // java/lang/String."<init>":(Ljava/lang/String;)V
    #12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V
    #13 = Utf8 (Ljava/lang/String;)V
    #14 = String #15 // wang-javaer
    #15 = Utf8 wang-javaer
    #16 = Class #17 // com/example/StringExample
    #17 = Utf8 com/example/StringExample
    #18 = Utf8 Code
    #19 = Utf8 LineNumberTable
    #20 = Utf8 main
    #21 = Utf8 ([Ljava/lang/String;)V
    #22 = Utf8 SourceFile
    #23 = Utf8 StringExample.java
    {
    public com.example.StringExample();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    LineNumberTable:
    line 3: 0

    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
    stack=3, locals=4, args_size=1
    0: new #7 // class java/lang/String
    3: dup
    4: ldc #9 // String javaer-wang
    6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V
    9: astore_1
    10: ldc #14 // String wang-javaer
    12: astore_2
    13: ldc #14 // String wang-javaer
    15: astore_3
    16: return
    LineNumberTable:
    line 5: 0
    line 6: 10
    line 7: 13
    line 8: 16
    }
    SourceFile: "StringExample.java"


    备注:以上代码的运行也编译环境为 jdk1.8.0_101。



    其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("javaer-wang"); 定义的“javaer-wang”字符,在信息 #10 = Utf8 javaer-wang 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。


    那么问题来了,以下这段代码的执行结果为 true 还是 false?


    String s1 = new String("javaer-wang");
    String s2 = new String("javaer-wang");
    System.out.println(s1 == s2);

    既然 new String 会在常量池中创建字符串,那么执行的结果就应该是 true 了。其实并不是,这里对比的变量 s1 和 s2 堆上地址,因为堆上的地址是不同的,所以结果一定是 false,如下图所示:


    字符串引用.png


    从图中可以看出 s1 和 s2 的引用一定是相同的,而 s3 和 s4 的引用是不同的,对应的程序代码如下:


    public static void main(String[] args) {
    String s1 = "Java";
    String s2 = "Java";
    String s3 = new String("Java");
    String s4 = new String("Java");
    System.out.println(s1 == s2);
    System.out.println(s3 == s4);
    }

    程序执行的结果也符合预期:



    true false



    扩展知识


    我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:


    public static void main(String[] args) {
    String s1 = "abc";
    String s2 = "ab" + "c";
    String s3 = "a" + "b" + "c";
    System.out.println(s1 == s2);
    System.out.println(s1 == s3);
    }

    按照 String 不能被修改的思想来看,s2 应该会在字符串常量池创建两个字符串“ab”和“c”,s3 会创建三个字符串,他们的引用对比结果也一定是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。


    同样我们使用 javac StringExample.java 先编译代码,再使用 javap -c StringExample 命令查看编译的代码如下:


    警告: 文件 ./StringExample.class 不包含类 StringExample
    Compiled from "StringExample.java"
    public class com.example.StringExample {
    public com.example.StringExample();
    Code:
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return

    public static void main(java.lang.String[]);
    Code:
    0: ldc #7 // String abc
    2: astore_1
    3: ldc #7 // String abc
    5: astore_2
    6: ldc #7 // String abc
    8: astore_3
    9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    12: aload_1
    13: aload_2
    14: if_acmpne 21
    17: iconst_1
    18: goto 22
    21: iconst_0
    22: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
    25: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
    28: aload_1
    29: aload_3
    30: if_acmpne 37
    33: iconst_1
    34: goto 38
    37: iconst_0
    38: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
    41: return
    }

    从 Code 3、6 可以看出字符串都被编译器优化成了字符串“abc”了。




    总结


    本文我们通过 javap -v XXX 的方式查看编译的代码发现 new String 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。我们还将了字符串常量池在 JDK 1.7 和 JDK 1.8 的变化以及编译器对确定字符串的优化,希望能帮你正在的理解字符串的比较。


    最后的话 原创不易,本篇近 3000 的文字描述,以及大量精美的图片,耗费了作者大概 5 个多小时的时间,写作是一件很酷,并且能帮助他人的事,作者希望一直能坚持下去。如果觉得有用,请随手点击一个赞吧,谢谢


    作者:码农出击666
    链接:https://juejin.cn/post/7033653572898127879
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    2020-iOS最新面试题解析(原理篇)

    runtime怎么添加属性、方法等ivar表示成员变量class_addIvarclass_addMethodclass_addPropertyclass_addProtocolclass_replaceProperty是否可以把比较耗时的操作放在NSNoti...
    继续阅读 »

    runtime怎么添加属性、方法等


    • ivar表示成员变量
    • class_addIvar
    • class_addMethod
    • class_addProperty
    • class_addProtocol
    • class_replaceProperty


    是否可以把比较耗时的操作放在NSNotificationCenter中


    • 首先必须明确通知在哪个线程中发出,那么处理接受到通知的方法也在这个线程中调用
    • 如果在异步线程发的通知,那么可以执行比较耗时的操作;
    • 如果在主线程发的通知,那么就不可以执行比较耗时的操作


    runtime 如何实现 weak 属性


    首先要搞清楚weak属性的特点


    weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。
    为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;
    然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)


    那么runtime如何实现weak变量的自动置nil?


    runtime对注册的类,会进行布局,会将 weak 对象放入一个 hash 表中。
    用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会调用对象的 dealloc 方法,
    假设 weak 指向的对象内存地址是a,那么就会以a为key,在这个 weak hash表中搜索,找到所有以a为key的 weak 对象,从而设置为 nil。


    weak属性需要在dealloc中置nil么


    • 在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
    • 即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
    • 在属性所指的对象遭到摧毁时,属性值也会清空


    // 模拟下weak的setter方法,大致如下
    - (void)setObject:(NSObject *)object
    {
    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
    [object cyl_runAtDealloc:^{
    _object = nil;
    }];
    }


    一个Objective-C对象如何进行内存布局?(考虑有父类的情况)


    • 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
    • 父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中
    • 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的如下信息
      • 对象方法列表
      • 成员变量的列表
      • 属性列表
    • 每个 Objective-C 对象都有相同的结构,如下图所示

    Objective-C 对象的结构图

    ISA指针

    根类(NSObject)的实例变量

    倒数第二层父类的实例变量

    ...

    父类的实例变量

    类的实例变量


    • 根类对象就是NSObject,它的super class指针指向nil
    • 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类


    一个objc对象的isa的指针指向什么?有什么作用?


    • 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型
    • 根据这个指针就能知道将来调用哪个类的方法


    下面的代码输出什么?


    @implementation Son : Father
    - (id)init
    {
    self = [super init];
    if (self) {
    NSLog(@"%@", NSStringFromClass([self class]));
    NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
    }
    @end


    • 答案:都输出 Son
    • 这个题目主要是考察关于objc中对 self 和 super 的理解:
      • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者
      • 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
      • 而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法
      • 调用[self class] 时,会转化成 objc_msgSend函数
    id objc_msgSend(id self, SEL op, ...)
      • 调用 [super class]时,会转化成 objc_msgSendSuper函数
    id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
      • 第一个参数是 objc_super 这样一个结构体,其定义如下
    struct objc_super {
    __unsafe_unretained id receiver;
    __unsafe_unretained Class super_class;
    };
      • 第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
      • 第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
      • objc Runtime开源代码对- (Class)class方法的实现


        -(Class)class {
    return object_getClass(self);
    }



    收起阅读 »

    iOS 面试策略之算法基础1-3节

    1. 基本数据结构数组数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:...
    继续阅读 »

    1. 基本数据结构


    数组


    数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:


    • ContiguousArray:效率最高,元素分配在连续的内存上。如果数组是值类型(栈上操作),则 Swift 会自动调用 Array 的这种实现;如果注重效率,推荐声明这种类型,尤其是在大量元素是类时,这样做效果会很好。
    • Array:会自动桥接到 Objective-C 中的 NSArray 上,如果是值类型,其性能与 ContiguousArray 无差别。
    • ArraySlice:它不是一个新的数组,只是一个片段,在内存上与原数组享用同一区域。


    下面是数组最基本的一些运用。


    // 声明一个不可修改的数组
    let nums = [1, 2, 3]
    let nums = [Int](repeating: 0, count: 5)

    // 声明一个可以修改的数组
    var nums = [3, 1, 2]
    // 增加一个元素
    nums.append(4)
    // 对原数组进行升序排序
    nums.sort()
    // 对原数组进行降序排序
    nums.sort(by: >)
    // 将原数组除了最后一个以外的所有元素赋值给另一个数组
    // 注意:nums[0..<nums.count - 1] 返回的是 ArraySlice,不是 Array
    let anotherNums = Array(nums[0 ..< nums.count - 1])


    不要小看这些简单的操作:数组可以依靠它们实现更多的数据结构。Swift 虽然不像 Java 中有现成的队列和栈,但我们完全可以用数组配合最简单的操作实现这些数据结构,下面就是用数组实现栈的示例代码。


    // 用数组实现栈
    struct Stack<Element> {
    private var stack: [Element]
    var isEmpty: Bool { return stack.isEmpty }
    var peek: AnyObject? { return stack.last }

    init() {
    stack = [Element]()
    }

    mutating func push(_ element: Element) {
    stack.append(object)
    }

    mutating func pop() -> Element? {
    return stack.popLast()
    }
    }

    // 初始化一个栈
    let stack = Stack<String>()


    最后特别强调一个操作:reserveCapacity()。它用于为原数组预留空间,防止数组在增加和删除元素时反复申请内存空间或是创建新数组,特别适用于创建和 removeAll() 时候进行调用,为整段代码起到提高性能的作用。


    字典和集合


    字典和集合(这里专指HashSet)经常被使用的原因在于,查找数据的时间复杂度为 O(1)。

    一般字典和集合要求它们的 Key 都必须遵守 Hashable 协议,Cocoa 中的基本数据类型都

    满足这一点;自定义的 class 需要实现 Hashable,而又因为 Hashable 是对 Equable 的扩展,

    所以还要重载 == 运算符。


    下面是关于字典和集合的一些实用操作:


    let primeNums: Set = [3, 5, 7, 11, 13]
    let oddNums: Set = [1, 3, 5, 7, 9]

    // 交集、并集、差集
    let primeAndOddNum = primeNums.intersection(oddNums)
    let primeOrOddNum = primeNums.union(oddNums)
    let oddNotPrimNum = oddNums.subtracting(primeNums)

    // 用字典和高阶函数计算字符串中每个字符的出现频率,结果 [“h”:1, “e”:1, “l”:2, “o”:1]
    Dictionary("hello".map { ($0, 1) }, uniquingKeysWith: +)


    集合和字典在实战中经常与数组配合使用,请看下面这道算法题:


    给一个整型数组和一个目标值,判断数组中是否有两个数字之和等于目标值


    这道题是传说中经典的 “2Sum”,我们已经有一个数组记为 nums,也有一个目标值记为 target,最后要返回一个 Bool 值。


    最粗暴的方法就是每次选中一个数,然后遍历整个数组,判断是否有另一个数使两者之和为 target。这种做法时间复杂度为 O(n^2)。


    采用集合可以优化时间复杂度。在遍历数组的过程中,用集合每次保存当前值。假如集合中已经有了目标值减去当前值,则证明在之前的遍历中一定有一个数与当前值之和等于目标值。这种做法时间复杂度为 O(n),代码如下。


    func twoSum(nums: [Int], _ target: Int) -> Bool {
    var set = Set<Int>()

    for num in nums {
    if set.contains(target - num) {
    return true
    }

    set.insert(num)
    }

    return false
    }


    如果把题目稍微修改下,变为


    给定一个整型数组中有且仅有两个数字之和等于目标值,求两个数字在数组中的序号


    思路与上题基本类似,但是为了方便拿到序列号,我们采用字典,时间复杂度依然是 O(n)。代码如下。


    func twoSum(nums: [Int], _ target: Int) -> [Int] {
    var dict = [Int: Int]()

    for (i, num) in nums.enumerated() {
    if let lastIndex = dict[target - num] {
    return [lastIndex, i]
    } else {
    dict[num] = i
    }
    }

    fatalError("No valid output!")
    }


    字符串和字符


    字符串在算法实战中极其常见。在 Swift 中,字符串不同于其他语言(包括 Objective-C),它是值类型而非引用类型,它是多个字符构成的序列(并非数组)。首先还是列举一下字符串的通常用法。


    // 字符串和数字之间的转换
    let str = "3"
    let num = Int(str)
    let number = 3
    let string = String(num)

    // 字符串长度
    let len = str.count

    // 访问字符串中的单个字符,时间复杂度为O(1)
    let char = str[str.index(str.startIndex, offsetBy: n)]

    // 修改字符串
    str.remove(at: n)
    str.append("c")
    str += "hello world"

    // 检测字符串是否是由数字构成
    func isStrNum(str: String) -> Bool {
    return Int(str) != nil
    }

    // 将字符串按字母排序(不考虑大小写)
    func sortStr(str: String) -> String {
    return String(str.sorted())
    }

    // 判断字符是否为字母
    char.isLetter

    // 判断字符是否为数字
    char.isNumber

    // 得到字符的 ASCII 数值
    char.asciiValue


    关于字符串,我们来一起看一道以前的 Google 面试题。


    给一个字符串,将其按照单词顺序进行反转。比如说 s 是 "the sky is blue",

    那么反转就是 "blue is sky the"。


    这道题目一看好简单,不就是反转字符串的翻版吗?这种方法有以下两个问题


    • 每个单词长度不一样
    • 空格需要特殊处理
      这样一来代码写起来会很繁琐而且容易出错。不如我们先实现一个字符串翻转的方法。


    fileprivate func reverse<T>(_ chars: inout [T], _ start: Int, _ end: Int) {
    var start = start, end = end

    while start < end {
    swap(&chars, start, end)
    start += 1
    end -= 1
    }
    }

    fileprivate func swap<T>(_ chars: inout [T], _ p: Int, _ q: Int) {
    (chars[p], chars[q]) = (chars[q], chars[p])
    }


    有了这个方法,我们就可以实行下面两种字符串翻转:


    • 整个字符串翻转,"the sky is blue" -> "eulb si yks eht"
    • 每个单词作为一个字符串单独翻转,"eulb si yks eht" -> "blue is sky the"
      整体思路有了,我们就可以解决这道问题了


    func reverseWords(s: String?) -> String? {
    guard let s = s else {
    return nil
    }

    var chars = Array(s), start = 0
    reverse(&chars, 0, chars.count - 1)

    for i in 0 ..< chars.count {
    if i == chars.count - 1 || chars[i + 1] == " " {
    reverse(&chars, start, i)
    start = i + 2
    }
    }

    return String(chars)
    }


    时间复杂度还是 O(n),整体思路和代码简单很多。


    总结


    在 Swift 中,数组、字符串、集合以及字典是最基本的数据结构,但是围绕这些数据结构的问题层出不穷。而在日常开发中,它们使用起来也非常高效(栈上运行)和安全(无需顾虑线程问题),因为他们都是值类型。


    2. 链表


    本节我们一起来探讨用 Swift 如何实现链表以及链表相关的技巧。


    基本概念


    对于链表的概念,实在是基本概念太多,这里不做赘述。我们直接来实现链表节点。


    class ListNode { 
    var val: Int
    var next: ListNode?

    init(_ val: Int) {
    self.val = val
    }
    }


    有了节点,就可以实现链表了。


    class LinkedList {
    var head: ListNode?
    var tail: ListNode?

    // 头插法
    func appendToHead(_ val: Int) {
    let node = ListNode(val)

    if let _ = head {
    node.next = head
    } else {
    tail = node
    }

    head = node
    }

    // 头插法
    func appendToTail(_ val: Int) {
    let node = ListNode(val)

    if let _ = tail {
    tail!.next = node
    } else {
    head = node
    }

    tail = node
    }
    }


    有了上面的基本操作,我们来看如何解决复杂的问题。


    Dummy 节点和尾插法


    话不多说,我们直接先来看下面一道题目。


    给一个链表和一个值 x,要求将链表中所有小于 x 的值放到左边,所有大于等于 x 的值放到右边。原链表的节点顺序不能变。

    例:1->5->3->2->4->2,给定x = 3。则我们要返回1->2->2->5->3->4


    直觉告诉我们,这题要先处理左边(比 x 小的节点),然后再处理右边(比 x 大的节点),最后再把左右两边拼起来。


    思路有了,再把题目抽象一下,就是要实现这样一个函数:


    func partition(_ head: ListNode?, _ x: Int) -> ListNode? {}


    即我们有给定链表的头节点,有给定的x值,要求返回新链表的头结点。接下来我们要想:怎么处理左边?怎么处理右边?处理完后怎么拼接?


    先来看怎么处理左边。我们不妨把这个题目先变简单一点:


    给一个链表和一个值 x,要求只保留链表中所有小于 x 的值,原链表的节点顺序不能变。


    例:1->5->3->2->4->2,给定x = 3。则我们要返回 1->2->2


    我们只要采用尾插法,遍历链表,将小于 x 值的节点接入新的链表即可。代码如下:


    func getLeftList(_ head: ListNode?, _ x: Int) -> ListNode? { 
    let dummy = ListNode(0)
    var pre = dummy, node = head

    while node != nil {
    if node!.val < x {
    pre.next = node
    pre = node!
    }
    node = node!.next
    }

    // 防止构成环
    pre.next = nil
    return dummy.next
    }


    注意,上面的代码我们引入了 Dummy 节点,它的作用就是作为一个虚拟的头前结点。我们引入它的原因是我们不知道要返回的新链表的头结点是哪一个,它有可能是原链表的第一个节点,可能在原链表的中间,也可能在最后,甚至可能不存在(nil)。而 Dummy 节点的引入可以巧妙的涵盖所有以上情况,我们可以用 dummy.next 方便得返回最终需要的头结点。


    现在我们解决了左边,右边也是同样处理。接着只要让左边的尾节点指向右边的头结点即可。全部代码如下:


    func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
    // 引入Dummy节点
    let prevDummy = ListNode(0), postDummy = ListNode(0)
    var prev = prevDummy, post = postDummy

    var node = head

    // 用尾插法处理左边和右边
    while node != nil {
    if node!.val < x {
    prev.next = node
    prev = node!
    } else {
    post.next = node
    post = node!
    }
    node = node!.next
    }

    // 防止构成环
    post.next = nil
    // 左右拼接
    prev.next = postDummy.next

    return prevDummy.next
    }


    注意这句 post.next = nil,这是为了防止链表循环指向构成环,是必须的但是很容易忽略的一步。

    刚才我们提到了环,那么怎么检测链表中是否有环存在呢?



    快行指针


    笔者理解快行指针,就是两个指针访问链表,一个在前一个在后,或者一个移动快另一个移动慢,这就是快行指针。来看一道简单的面试题:


    如何检测一个链表中是否有环?


    答案是用两个指针同时访问链表,其中一个的速度是另一个的 2 倍,如果他们相等了,那么这个链表就有环了,这就是快行指针的实际使用。代码如下:


    func hasCycle(_ head: ListNode?) -> Bool { 
    var slow = head
    var fast = head

    while fast != nil && fast!.next != nil {
    slow = slow!.next
    fast = fast!.next!.next

    if slow === fast {
    return true
    }
    }

    return false
    }


    再举一个快行指针一前一后的例子,看下面这道题。


    删除链表中倒数第 n 个节点。例:1->2->3->4->5,n = 2。返回1->2->3->5。

    注意:给定 n 的长度小于等于链表的长度。


    解题思路依然是快行指针,这次两个指针移动速度相同。但是一开始,第一个指针(指向头结点之前)就落后第二个指针 n 个节点。接着两者同时移动,当第二个移动到尾节点时,第一个节点的下一个节点就是我们要删除的节点。代码如下:


    func removeNthFromEnd(head: ListNode?, _ n: Int) -> ListNode? {
    guard let head = head else {
    return nil
    }

    let dummy = ListNode(0)
    dummy.next = head
    var prev: ListNode? = dummy
    var post: ListNode? = dummy

    // 设置后一个节点初始位置
    for _ in 0 ..< n {
    if post == nil {
    break
    }
    post = post!.next
    }

    // 同时移动前后节点
    while post != nil && post!.next != nil {
    prev = prev!.next
    post = post!.next
    }

    // 删除节点
    prev!.next = prev!.next!.next

    return dummy.next
    }


    这里还用到了 Dummy 节点,因为有可能我们要删除的是头结点。


    总结


    这次我们用 Swift 实现了链表的基本结构,并且实战了链表的几个技巧。在结尾处,我还想强调一下 Swift 处理链表问题的两个细节问题:


    • 一定要注意头结点可能就是 nil。所以给定链表,我们要看清楚 head 是不是 optional,在判断是不是要处理这种边界条件。
    • 注意每个节点的 next 可能是 nil。如果不为 nil,请用"!"修饰变量。在赋值的时候,也请注意"!"将 optional 节点传给非 optional 节点的情况。


    3. 栈和队列


    这期我们来讨论一下栈和队列。在 Swift 中,没有内设的栈和队列,很多扩展库中使用 Generic Type 来实现栈或是队列。正规的做法使用链表来实现,这样可以保证加入和删除的时间复杂度是 O(1)。然而笔者觉得最实用的实现方法是使用数组,因为 Swift 没有现成的链表,而数组又有很多的 API 可以直接使用,非常方便。


    基本概念


    对于栈来说,我们需要了解以下几点:


    • 栈是后进先出的结构。你可以理解成有好几个盘子要垒成一叠,哪个盘子最后叠上去,下次使用的时候它就最先被抽出去。
    • 在 iOS 开发中,如果你要在你的 App 中添加撤销操作(比如删除图片,恢复删除图片),那么栈是首选数据结构
    • 无论在面试还是写 App 中,只关注栈的这几个基本操作:push, pop, isEmpty, peek, size。


    protocol Stack {
    /// 持有的元素类型
    associatedtype Element

    /// 是否为空
    var isEmpty: Bool { get }
    /// 栈的大小
    var size: Int { get }
    /// 栈顶元素
    var peek: Element? { get }

    /// 进栈
    mutating func push(_ newElement: Element)
    /// 出栈
    mutating func pop() -> Element?
    }

    struct IntegerStack: Stack {
    typealias Element = Int

    var isEmpty: Bool { return stack.isEmpty }
    var size: Int { return stack.count }
    var peek: Element? { return stack.last }

    private var stack = [Element]()

    mutating func push(_ newElement: Element) {
    stack.append(newElement)
    }

    mutating func pop() -> Element? {
    return stack.popLast()
    }
    }


    对于队列来说,我们需要了解以下几点:


    • 队列是先进先出的结构。这个正好就像现实生活中排队买票,谁先来排队,谁先买到票。
    • iOS 开发中多线程的 GCD 和 NSOperationQueue 就是基于队列实现的。
    • 关于队列我们只关注这几个操作:enqueue, dequeue, isEmpty, peek, size。


    protocol Queue {
    /// 持有的元素类型
    associatedtype Element

    /// 是否为空
    var isEmpty: Bool { get }
    /// 队列的大小
    var size: Int { get }
    /// 队首元素
    var peek: Element? { get }

    /// 入队
    mutating func enqueue(_ newElement: Element)
    /// 出队
    mutating func dequeue() -> Element?
    }

    struct IntegerQueue: Queue {
    typealias Element = Int

    var isEmpty: Bool { return left.isEmpty && right.isEmpty }
    var size: Int { return left.count + right.count }
    var peek: Element? { return left.isEmpty ? right.first : left.last }

    private var left = [Element]()
    private var right = [Element]()

    mutating func enqueue(_ newElement: Element) {
    right.append(newElement)
    }

    mutating func dequeue() -> Element? {
    if left.isEmpty {
    left = right.reversed()
    right.removeAll()
    }
    return left.popLast()
    }
    }


    栈和队列互相转化


    处理栈和队列问题,最经典的一个思路就是使用两个栈/队列来解决问题。也就是说在原栈/队列的基础上,我们用一个协助栈/队列来帮助我们简化算法,这是一种空间换时间的思路。下面是示例代码:


    // 用栈实现队列
    struct MyQueue {
    var stackA: Stack
    var stackB: Stack

    var isEmpty: Bool {
    return stackA.isEmpty
    }

    var peek: Any? {
    get {
    shift()
    return stackB.peek
    }
    }

    var size: Int {
    get {
    return stackA.size + stackB.size
    }
    }

    init() {
    stackA = Stack()
    stackB = Stack()
    }

    func enqueue(object: Any) {
    stackA.push(object);
    }

    func dequeue() -> Any? {
    shift()
    return stackB.pop();
    }

    fileprivate func shift() {
    if stackB.isEmpty {
    while !stackA.isEmpty {
    stackB.push(stackA.pop()!);
    }
    }
    }
    }

    // 用队列实现栈
    struct MyStack {
    var queueA: Queue
    var queueB: Queue

    init() {
    queueA = Queue()
    queueB = Queue()
    }

    var isEmpty: Bool {
    return queueA.isEmpty
    }

    var peek: Any? {
    get {
    if isEmpty {
    return nil
    }

    shift()
    let peekObj = queueA.peek
    queueB.enqueue(queueA.dequeue()!)
    swap()
    return peekObj
    }
    }

    var size: Int {
    return queueA.size
    }

    func push(object: Any) {
    queueA.enqueue(object)
    }

    func pop() -> Any? {
    if isEmpty {
    return nil
    }

    shift()
    let popObject = queueA.dequeue()
    swap()
    return popObject
    }

    private func shift() {
    while queueA.size > 1 {
    queueB.enqueue(queueA.dequeue()!)
    }
    }

    private func swap() {
    (queueA, queueB) = (queueB, queueA)
    }
    }


    上面两种实现方法都是使用两个相同的数据结构,然后将元素由其中一个转向另一个,从而形成一种完全不同的数据。


    面试题实战


    给一个文件的绝对路径,将其简化。举个例子,路径是 "/home/",简化后为 "/home";路径是"/a/./b/../../c/",简化后为 "/c"。


    这是一道 Facebook 的面试题。这道题目其实就是平常在终端里面敲的 cd、pwd 等基本命令所得到的路径。


    根据常识,我们知道以下规则:


    • “. ” 代表当前路径。比如 “ /a/. ” 实际上就是 “/a”,无论输入多少个 “ . ” 都返回当前目录
    • “..”代表上一级目录。比如 “/a/b/.. ” 实际上就是 “ /a”,也就是说先进入 “a” 目录,再进入其下的 “b” 目录,再返回 “b” 目录的上一层,也就是 “a” 目录。


    然后针对以上信息,我们可以得出以下思路:


    1. 首先输入是个 String,代表路径。输出要求也是 String, 同样代表路径;
    2. 我们可以把 input 根据 “/” 符号去拆分,比如 "/a/b/./../d/" 就拆成了一个String数组["a", "b", ".", "..", "d"];
    1. 创立一个栈然后遍历拆分后的 String 数组,对于一般 String ,直接加入到栈中,对于 ".." 那我们就对栈做 pop 操作,其他情况不错处理。


    思路有了,代码也就有了


    func simplifyPath(path: String) -> String {
    // 用数组来实现栈的功能
    var pathStack = [String]()
    // 拆分原路径
    let paths = path.split(separatedBy: "/")

    for path in paths {
    // 对于 "." 我们直接跳过
    guard path != "." else {
    continue
    }
    // 对于 ".." 我们使用pop操作
    if path == ".." {
    if (!pathStack.isEmpty) {
    pathStack.removeLast()
    }
    // 对于太注意空数组的特殊情况
    } else if path != "" {
    pathStack.append(path)
    }
    }
    // 将栈中的内容转化为优化后的新路径
    return "/" + pathStack.joined(separator: "/")
    }


    上面代码除了完成了基本思路,还考虑了大量的特殊情况、异常情况。这也是硅谷面试考察的一个方面:面试者思路的严谨,对边界条件的充分考虑,以及代码的风格规范。


    总结


    在 Swift 中,栈和队列是比较特殊的数据结构,笔者认为最实用的实现和运用方法是利用数组。虽然它们本身比较抽象,却是很多复杂数据结构和 iOS 开发中的功能模块的基础。这也是一个工程师进阶之路理应熟练掌握的两种数据结构。

    收起阅读 »

    iOS 面试简单准备

    1.简历的准备在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。1.简洁的艺术互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被...
    继续阅读 »

    1.简历的准备


    在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。


    1.简洁的艺术


    互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被认为不够专业。


    更麻烦的是,多数超过一页的简历很可能在 HR 手中就被过滤掉了。因为 HR 每天会收到大量的简历,一般情况下每份简历在手中的停留时间也就 10 秒钟左右。而超过一页的简历会需要更多的时间去寻找简历中的有价值部分,对于 HR 来说,她更倾向于认为这种人通常是不靠谱的,因为写个简历都不懂行规,为什么还要给他面试机会呢?


    那么我们应该如何精简简历呢? 简单说来就是一个字:删!


    删掉不必要的自我介绍信息。很多求职者会将自己在学校所学的课程罗列上去,例如:C 语言,数据结构,数学分析⋯⋯好家伙,一写就是几十门,还放在简历的最上面,就怕面试官看不见。对于这类信息,一个字:删!面试官不关心你上了哪些课程,而且在全中国,大家上的课程也都大同小异,所以没必要写出来。


    删除不必要的工作或实习、实践经历。如果你找一份程序员的工作,那么你参加了奥运会的志愿者活动,并且拿到了奖励或者你参加学校的辩论队,获得了最佳辩手这些经历通常是不相关的。诸如此类的还有你帮导师代课,讲了和工作不相关的某某专业课,或者你在学生会工作等等。删除不相关的工作、实习或实践内容可以保证你的简历干净。当然,如果你实在没得可写,比如你是应届生,一点实习经历都没有,那可以适当写一两条,保证你能写够一页的简历,但是那两条也要注意是强调你的团队合作能力或者执行力之类的技能,因为这些才是面试官感兴趣的。


    删除不必要的证书。最多写个 4、6 级的证书,什么教师资格证,中高级程序员证,还有国内的各种什么认证,都是没有人关心的。


    删除不必要的细节。作为 iOS 开发的面试官,很多求职者在介绍自己的 iOS 项目经历的时候,介绍了这个工程用的工作环境是 Mac OS,使用的机器是 Mac Mini,编译器是 Xcode,能够运行在 iOS 什么版本的环境。还有一些人,把这个项目用到的开源库都写上啦,什么 AFNetworking, CocoaPods 啥的。这些其实都不是重点,请删掉。后面我会讲,你应该如何介绍你的 iOS 项目经历。


    自我评价,这个部分是应届生最喜欢写的,各种有没有的优点都写上,例如:


    性格开朗、稳重、有活力,待人热情、真诚;工作认真负责,积极主动,能吃苦耐劳,勇于承受压力,勇于创新;有很强的组织能力和团队协作精神,具有较强的适应能力;纪律性强,工作积极配合;意志坚强,具有较强的无私奉献精神。对待工作认真负责,善于沟通、协调有较强的组织能力与团队精神;活泼开朗、乐观上进、有爱心并善于施教并行;上进心强、勤于学习能不断提高自身的能力与综合素质。


    这些内容在面试的时候不太好考查,都可以删掉。通常如果有 HR 面的话,HR 自然会考查一些你的沟通,抗压,性格等软实力。


    我相信,不管你是刚毕业的学生,还是工作十年的老手,你都可以把你的简历精简到一页 A4 纸上。记住,简洁是一种美,一种效率,也是一种艺术。


    2.重要的信息写在最前面


    将你觉得最吸引人的地方写在最前面。如果你有牛逼公司的实习,那就把实习经历写在最前面,如果你在一个牛逼的实验室里面做科研,就把研究成果和论文写出来,如果你有获得过比较牛逼的比赛名次(例如 Google code jam, ACM 比赛之类),写上绝对吸引眼球。


    所以,每个人的简历的介绍顺序应该都是不一样的,不要在网上下载一个模板,然后就一项一项地填:教育经历,实习经历,得奖经历,个人爱好,这样的简历毫无吸引力,也无法突出你的特点。


    除了你的个人特点是重要信息外,你的手机号、邮箱、毕业院校、专业以及毕业时间这些也都是非常重要的,一定要写在简历最上面。


    3.不要简单地罗列工作经历


    不要简单地说你开发了某某 iOS 客户端。这样简单的罗列你的作品集并不能让面试官很好地了解你的能力,当然,真正在面试时面试官可能会仔细询问,但是一份好的简历,应该省去一些面试官额外询问你的工作细节的时间。


    具体的做法是:详细的描述你对于某某 iOS 客户端的贡献。主要包括:你参与了多少比例功能的开发? 你解决了哪些开发中的有挑战的问题? 你是不是技术负责人?


    而且,通过你反思这些贡献,你也可以达到自我审视,如果你发现这个项目你根本什么有价值的贡献都没做,就打了打酱油,那你最好不要写在简历上,否则当面试官在面试时问起时,你会很难回答,最终让他发现你的这个项目经历根本一文不值时,肯定会给一个负面的印象。


    4.不要写任何虚假或夸大的信息


    刚刚毕业的学生都喜欢写精通 Java,精通 C/C++,其实代码可能写了不到 1 万行。我觉得你要精通某个语言,至少得写 50 万行这个语言的代码才行,而且要对语言的各种内部机制和原理有了解。那些宣称精通 Java 的同学,连 Java 如何做内存回收,如何做泛型支持,如何做自动 boxing 和 unboxing 的都不知道,真不知道为什么要写精通二字。


    任何夸大或虚假的信息,在面试时被发现,会造成极差的面试印象。所以你如果对某个知识一知半解,要么就写 “使用过” 某某,要么就干脆不写。如果你简历实在太单薄,没办法写上了一些自己打酱油的项目,被问起来怎么办? 请看看下面的故事:


    我面试过一个同学,他在面试时非常诚实。我问他一些简历上的东西,他如果不会,就会老实说,这个我只是使用了一下,确实不清楚细节。对于一些没有技术含量的项目,他也会老实说,这个项目他做的工作比较少,主要是别人在做。最后他还会补充说,“我自认为自己数据结构和算法还不错,要不你问我这方面的知识吧。”


    这倒是一个不错的办法,对于一个没有项目经验,但是聪明并且数据结构和算法基础知识扎实的应届生,其实我们是非常愿意培养的。很多人以为公司面试是看经验,希望招进来就能干活,其实不是的,至少我们现在以及我以前在网易招人,面试的是对方的潜力,潜力越大,可塑性好,未来进步得也更快;一些资质平庸,却经验稍微丰富一点的开发者,相比聪明好学的面试者,后劲是不足的。


    总之,不要写任何虚假或夸大的信息,即使你最终骗得过面试官,进了某公司,如果能力不够,在最初的试用期内,也很可能因为能力不足而被开掉。


    5.留下更多信息


    刚刚说到,简历最好写够一张 A4 纸即可,那么你如果想留下更多可供面试官参考的信息怎么办呢?其实你可以附上更多的参考链接,这样如果面试官对你感兴趣,自然会仔细去查阅这些链接。对于 iOS 面试来说,GitHub 上面的开源项目地址、博客地址都是不错的参考信息。如果你在微博上也频繁讨论技术,也可以附上微博地址。


    我特别建议大家如果有精力,可以好好维护一下自己的博客或者 GitHub 上的开源代码。因为如果你打算把这些写到简历上,让面试官去上面仔细评价你的水平,你就应该对上面的内容做到足够认真的准备。否则,本来面试完面试官还挺感兴趣的,结果一看你的博客和开源代码,评价立刻降低,就得不偿失了。


    6.不要附加任何可能带来负面印象的信息


    任何与招聘工作无关的东西,尽量不要提。有些信息提了可能有加分,也可能有减分,取决于具体的面试官。下面我罗列一下我认为是减分的信息。


    1)个人照片


    不要在简历中附加个人照片。个人长相属于与工作能力不相关的信息,也许你觉得你长得很帅,那你怎么知道你的样子不和面试官的情敌长得一样? 也许你长得很漂亮,那么你怎么知道 HR 是否被你长得一样的小三把男朋友抢了? 我说得有点极端,那人们对于长相的评价标准确实千差万别,萝卜青菜各有所爱,加上可能有一些潜在的极端情况,所以没必要附加这部分信息。这属于加了可能有加分,也可能有减分的情况。


    2)有风险的爱好


    不要写各种奇怪的爱好。喜欢打游戏、抽烟、喝酒,这类可能带来负面印象的爱好最好不要写。的确有些公司会有这种一起联机玩游戏或者喝酒的文化,不过除非你明确清楚对于目标公司,写上会是加分项,否则还是不写为妙。


    3)使用 PDF 格式


    不要使用 Word 格式的简历,要使用 PDF 的格式。我在招 iOS 程序员时,好多人的简历都是 Word 格式的,我都怀疑这些人是否有 Mac 电脑。因为 Mac 下的 office 那么难用,公司好多人机器上都没有 Mac 版 office。我真怀疑这些人真是的想投简历么? PDF 格式的简历通常能展现出简历的专业性。


    4)QQ号码邮箱


    不要使用 QQ 号开头的 QQ 邮箱,例如 12345@qq.com ,邮箱的事情我之前简单说过,有些人很在乎这个,有些人觉得无所谓,我个人对用数字开头的 QQ 邮箱的求职者不会有加分,但是对使用 Gmail 邮箱的求职者有加分。因为这涉及到个人的工作效率,使用 Gmail 的人通常会使用邮件组,过滤器,IMAP 协议,标签,这些都有助于提高工作效率。如果你非要使用 QQ 邮箱,也应该申请一个有意义的邮箱名,例如 tangqiaoboy@qq.com 。


    7.职业培训信息


    不要写参加过某某培训公司的 iOS 培训,特别是那种一、两个月的速成培训。这对于我和身边很多面试官来说,绝对是负分。


    这个道理似乎有点奇怪,因为我们从小都是由老师教授新知识的。我自己也实验过,掌握同样的高中课本上的知识,自己自学的速度通常比老师讲授的速度要慢一倍的时间。即一个知识点,如果你自己要看 2 小时的书才能理解的话,有好的老师给你讲解的话,只需要一个小时就够了。所以,我一直希望在学习各种东西的时候都能去听一些课程,因为我认为这样节省了我学习的时间。


    但是这个道理在程序员这个领域行不通,为什么这么说呢?原因有两点:


    1. 计算机编程相关的知识更新速度很快。同时,国内的 IT 类资料的翻译质量相当差,原创的优秀书籍也很少。所以,我们通常需要靠阅读英文才能掌握最新的资料。拿 iOS 来说,每年 WWDC 的资料都非常重要,而这些内容涉及版权,国内培训机构很难快速整理成教材。
    2. 计算机编程知识需要较多的专业知识积累和实践。学校的老师更多只能做入门性的教学工作。
      如果一个培训机构有一个老师,他强到能够通过自己做一些项目来积累很多专业知识和实践,并且不断地从国外资料上学习最新的技术。那么这个人在企业里面会比在国内的培训机构更有施展自己能力的空间。国内的培训机构因为受众面的原因,基本上还是培养那种初级的程序员新手,所以对老师的新技术学习速度要求不会那么高,自然老师也不会花那么时间在新技术研究上。但是企业就不一样了,企业需要不停地利用新技术来增强自己的产品竞争力,所以对于 IT 企业来说,产品的竞争就是人才的竞争,所以给优秀的人能够开出很高的薪水。
      所以,我们不能期望从 IT 类培训机构中学习到最新的技术,一切只能通过我们自学。当然,自学之后在同行之间相互交流,对于我们的技术成长也是很有用的。小结



    上图是本节讨论的总结,在简历准备上,我们需要考虑简历的简洁性等各种注意事项。


    2.寻找机会


    1.寻找内推机会

    其实,最好的面试机会都不是公开渠道的。好的机会都隐藏于各种内部推荐之中。通过内部推荐,你可以更加了解目标工作的团队和内容,另外内部推荐通常也可以跳过简历筛选环节,直接参加笔试或面试。我所在的猿辅导公司为内推设立了非常高的奖金激励,因为我们发现,综合各种渠道来看,内推的渠道的人才的简历质量最高,面试通过率最高的。


    所以,如果你是学生,找找你在各大公司的师兄师姐内推;如果你已经工作了,你可以找前同事或者通过一些社交活动认识的技术同行内推。


    大部分情况下,如果在目标公司你完全没有认识的人,你也可以找机会来认识几个。比如你可以通过微博、知乎、Twitter、GitHub 来结交新的朋友。然后双方聊天如果愉快的话,我相信内推这种举手之劳的事情对方不会拒绝的。


    如果你都工作 5 年以上,还是没有建立足够好的社交圈子帮助你内推,那可能你需要做很多的社交活动交一些朋友。


    2.其它常见的渠道

    内推之外,其它的公开招聘渠道通常都要差一些。现在也有一些专门针对互联网行业的招聘网站,例如拉勾、100offer 这类,它们也是不错的渠道,可以找到相关的招聘信息。


    但因为这类公开渠道简历投放数量巨大,通常 HR 那边就会比较严格地筛选简历,拿我们公司来说,通常在这些渠道看 20 份简历,才会有 1 份愿意约面的简历。而且 HR 会只挑比较好的学校或者公司的候选人,也不排除还有例如笔试这种更多的面试流程。但是面试经验都是慢慢积累的,建议你也可以尝试这些渠道。


    3.面试流程


    1.流程简述


    就我所知,大部分的 iOS 公司的面试流程都大同小异。我们先简述一下大体的流程,然后再详细讨论。


    在面试的刚开始,面试官通常会要求你做一个简短的自我介绍。然后面试官可能会和你聊聊你过去的实习项目或者工作内容。接着面试官可能会问你一些具体的技术问题,有经验的面试官会尽量找一些和你过去工作相关的技术问题。最后,有些公司会选择让你当场写写代码,Facebook 会让你在白板上写代码,国内的更多是让你在 A4 纸上写。有一些公司也会问一些系统设计方面的问题,考查你的整体架构能力。在面试快要结束时,通常面试官还会问你有没有别的什么问题。


    以上这些流程,不同公司可能会跳过某些环节。比如有一些公司就不会考察当场在白板或 A4 纸上写代码。有些公司可能跳过问简历的环节直接写代码,特别是校园招聘的时候,因为应届生通常项目经验较少。面试流程图如下所示:



    2.自我介绍


    自我介绍通常是面试中最简单、最好准备的环节。


    一个好的自我介绍应该结合公司的招聘职位来做定制。比如公司有硬件的背景,就应该介绍一下在硬件上的经验或者兴趣;公司如果注重算法能力,则介绍自己的算法练习;公司如果注重团队合作,那么你介绍一下自己的社会活动都是可以的。


    一个好的自我介绍应该提前写下来,并且背熟。因为候选人通常的紧张感都是来自于面试刚开始的几分钟,如果你刚开始的几分钟讲的结结巴巴,那么这种负面情绪会加剧你面试时的紧张感,从而影响你正常发挥。如果你提前把自我介绍准备得特别流利,那么开始几分钟的紧张感过去之后,你很可能就会很快进入状态,而忘记紧张这个事情了。


    即使做到了以上这些仍然是不够的,候选者常见的问题还包括:


    • 太简短
    • 没有重点
    • 太拖沓
    • 不熟练


    我们在展开讨论上面这些问题之前,我们可以站在面试官的立场考虑一下:如果你是面试官,你为什么要让候选人做自我介绍?你希望在自我介绍环节考察哪些信息?


    在我看来,自我介绍环节相当重要,因为:


    • 首先它考察了候选人的表达能力。大部分的程序员表达能力可能都一般,但是如果连自我介绍都说不清楚,通常就说明表达沟通能力稍微有点问题了。面试官通过这个环节可以初步考察到候选人的表达能力。
    • 它同样也考察了候选人对于面试的重视程度。一般情况下,表达再差的程序员,也可以通过事先拟稿的方式,把自我介绍内容背下来。如果一个人自我介绍太差,说明他不但表达差,而且不重视这次面试。
    • 最后,自我介绍对之后的面试环节起到了支撑作用。因为自我介绍中通常会涉及自己的项目经历,自己擅长的技术等。这些都很可能吸引面试官追问下去。好的候选人会通过自我介绍 “引导” 面试官问到自己擅长的领域知识。如果面试官最后对问题答案很满意,通过面试的几率就会大大增加。


    所以我如果是面试官,我希望能得到一个清晰流畅的自我介绍。下面我们来看看候选人在面试中的常见问题。


    1)太简短


    一个好的自我介绍大概是 3~5 分钟。过短的自我介绍没法让面试官了解你的大致情况,也不足以看出来你的基本表达能力。


    如果你发现自己没法说出足够时间的自我介绍。可以考虑在介绍中加入:自己的简单的求学经历,项目经历,项目中有亮点的地方,参与或研究过的一些开源项目,写过的博客,其它兴趣爱好,自己对新工作的期望和目标公司的理解。


    我相信每个人经过准备,都可以做到一个 5 分钟长度的自我介绍。


    2)没有重点


    突破了时间的问题,接下来就需要掌握介绍的重点。通常一个技术面试,技术相关的介绍才是重点。所以求学经历,兴趣爱好之类的内容可以简单提到即可。


    对于一个工作过的开发者,你过去做的项目哪个最有挑战,最能展示出你的水平其实自己应该是最清楚的。所以大家可以花时间把这个内容稍微强调一下。


    当然你也没必要介绍得太细致,面试官如果感兴趣,自然会在之后的面试过程中和你讨论。


    3)太拖沓


    有些工作了几年的人,做过的项目差不多有个 3~5 个,面试的时候就忍不住一个一个说。单不说这么多项目在自我介绍环节不够介绍。就是之后的详细讨论环节,面试官也不可能讨论完你的所有项目经历。


    所以千万不要做这种 “罗列经历” 的事情,你要做的就是挑一个或者最多两个项目,介绍一下项目大致的背景和你解决的主要问题即可。至于具体的解决过程,可以不必介绍。


    4)不熟练


    即便你的内容完全合适,时间长度完全合理,你也需要保证一个流利的陈述过程。适当在面试前多排练几次,所有人都可以做到一个流利的自我介绍。


    还有一点非常神奇,就是一个人在做一件事情的时候,通常都是开始的前以及刚开始几分钟特别紧张。比如考试,演讲或者面试,通常这几分钟之后,人们进入 “状态” 了,就会忘记紧张了。


    将自己的自我介绍背下来,可以保证一个流利顺畅的面试开局,这可以极大舒缓候选人的紧张情绪。相反,一开始自我介绍就结结巴巴,会加剧候选人的紧张情绪,而技术面试如果不能冷静的话,是很难在写代码环节保证逻辑清晰正确的。


    所以,请大家务必把这个小环节重视起来,做出一个完美的开局。


    3.项目介绍


    自我介绍之后,就轮到讨论更加具体的内容环节了,通常面试官都会根据自我介绍或者你的简历,选一个他感兴趣的项目来详细讨论。


    这个时候,大家务必需要提前整理出自己参与的项目的具体挑战,以及自己做得比较好的地方。切忌不要说:“这个项目很简单,没什么挑战,那个项目也很简单,没什么好说的”。再简单的事情,都可以做到极致的,就看你有没有一个追求完美的心。


    比如你的项目可能在你看来就是摆放几个 iOS 控件。但是,这些控件各自有什么使用上的技巧,有什么优化技巧?其实如果专心研究,还是有很多可以学习的。拿 UITableView 来说,一个人如果能够做到把它的滑动流程度优化好,是非常不容易的。这里面涉及网络资源的异步加载、图片资源的缓存、后台线程的渲染、CALayer 层的优化等等。


    这其实也要求我们做工作要精益求精,不求甚解。所以一场成功的面试最最本质上,看得还是自己平时的积累。如果自己平时只是糊弄工作,那么面试时就很容易被看穿。


    在这一点上,我奉劝大家在自己的简历上一定要老实。不要在建简历上弄虚作假,把自己没有做过的项目写在里面。


    顺便我在这里也教一下大家如何面试别人。如果你是面试官,考察简历的真假最简单的方法就是问细节。一个项目的细节如果问得很深入,候选人有没有做过很容易可以看出来。


    举个例子,如果候选人说他在某公司就职期间做了某某项目。你就可以问他:


    • 这个工作具体的产品需求是什么样的?
    • 大概做了多长时间?
    • 整体的软件架构是什么样的?
    • 涉及哪些人合作?几个开发和测试?
    • 项目的时间排期是怎么定的?
    • 你主要负责的部分和合作的人?
    • 项目进行中有没有遇到什么问题?
    • 这个项目最后最大的收获是什么?遗憾是什么?
    • 项目最困难的一个需求是什么?具体什么实现的?


    面试官如果做过类似项目,还可以问问通常这个项目常见的坑,看看候选人是什么解决的。


    4.写代码


    编程能力,说到底还是一个实践的能力,所以说大部分公司都会考察当场写代码。我面试过上百人,见到过很多候选人在自我介绍和项目讨论时都滔滔不绝,侃侃而谈,感觉非常好。但是一到写代码环节就怂了,要么写出来各种逻辑问题和细节问题没处理好,要么就是根本写不出来。


    由于人最终招进来就是干活写代码的,所以如果候选人当场写代码表现很差的话,基本上面试就挂掉了。


    程序员这个行业,说到底就是一个翻译的工作,把产品经理的产品文档和设计师的 UI 设计,翻译成计算机能够理解的形式,这个形式通常就是一行一行的源码。


    当面试官考察你写代码的时候,他其实在考察:


    • 你对语言的熟悉程度。如果候选人连常见的变量定义和系统函数都不熟悉,说明他肯定经验还是非常欠缺。
    • 你对逻辑的处理能力。产品经理关注的是用户场景和核心需求,而程序员关注的是逻辑边界和异常情况。程序的 bug 往往就是边界和特殊情况没有处理好。虽然说写出没有 bug 的程序几乎不可能,但是逻辑清晰的程序员能够把思路理清楚,减少 bug 发生的概率。
    • 设计和架构能力。好的代码需要保证易于维护和修改。这里面涉及很多知识,从简单的 “单一职责” 原则,到复杂的 “好的组合优于继承” 原则,其中设计模式相关的知识最多。写代码的时候多少还是能够看出这方面的能力。另外有些公司,例如 Facebook,会有专门的系统设计(System Design)面试环节,专注于考察设计能力。


    5.系统设计


    有一些公司喜欢考查一些系统设计的问题,简单来说,就是让你解决一个具体的业务需求,看看你是否能够将业务逻辑梳理清楚,并且拆解成各个模块,设计好模块间的关系。举几个例子,面试官可能让你:


    • 设计一个类似微博的信息流应用。
    • 设计一个本地数据缓存架构。
    • 设计一个埋点分析系统。
    • 设计一个直播答题系统。
    • 设计一个多端的数据同步系统。
    • 设计一个动态补丁的方案。


    这些系统的设计都非常考查一个人知识的全面性。通常情况下,如果一个人只知道 iOS 开发的知识,是很难做出相关的设计的。为了能够解决这些系统设计题,我们首先需要有足够的知识面,适度了解一下 Android 端、Web 端以及服务器端的各种技术方案背后的原理。你可以不写别的平台的代码,但是一定要理解它们在技术上能做到什么,不能做到什么。你也不必过于担心,面试官在考查的时候,还是会重点考查 iOS 相关的部分。


    我们将在下一小节,展开讨论如何准备和回答系统设计题。


    6.提问


    提问环节通常在面试结束前,取决于前面的部分是否按时结束,有些时候前面的环节占用了太多时间,也可能没有提问环节了。在后面的章节,我们会展开讨论一下如何提问。



    收起阅读 »

    腾讯抖音iOS岗位三面面经

    1.进程和线程的区别2.死锁的原因3.介绍虚拟内存4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么5.TCP可靠性6.http+https算法Z字遍历二叉树,归并排序后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这...
    继续阅读 »

    1.进程和线程的区别


    2.死锁的原因


    3.介绍虚拟内存


    4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么


    5.TCP可靠性


    6.http+https


    算法


    Z字遍历二叉树,归并排序


    后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这个组是java客户端)


    腾讯PCG iOS一面(1h)


    1.聊项目,聊了很久,一开始没有意会面试官想知道什么,最后说是想知道我这么做比起从客户端自己去实现的区别(这个项目是小米实习时候的项目,做的浏览器内核,页面翻译功能,


    基本每一个客户端应用都会有一个类似于浏览器内核的东西,对页面进行渲染,呈现,也可以叫渲染引擎,学前端的肯定知道这个东西,他主要是解释html,css,js的。


    我做的这个页面翻译功能可以不经过内核直接由客户端工程师用安卓客户端实现整套逻辑,所以这么问我了)


    2.实现string类,实现构造,析构,里面加一个kmp


    3.介绍智能指针,智能指针保存引用计数的变量存在哪里,引用计数是否线程安全


    4.算法:两个只有0和1的数字序列,只能0  1互换,每次当前位互换都会使后面的也换掉(比如,011000,换第二位,成了000111),计算从一个变到另一个需要几步操作。


    5.https,验证公钥有效的方法,为什么非对称对称并用


    腾讯PCG iOS二面 (40min)


    1.算法:


    合并排序链表


    2.static关键字的作用


    3.const关键字的作用


    4.成员初始化列表的作用


    5.指针和引用的区别


    6.又是很久的项目,怎么去学习浏览器内核(chromium内核的代码量有几千万行,而且写的很难懂,用了大量的设计模式,作为一个菜鸡真的很痛苦)


    ,怎么去调试项目中遇到的问题(这里主要是一个ipc接口没用对),你觉得人家google的是怎么去调的,你为什么和人家做法不一样?


    腾讯PCG iOS三面(2h)


    1.还是聊了很久项目(已经麻了,做过的东西一定要能说出口)


    2.浏览器呈现一个页面经过了哪几步(DOM树,layoutobject树,browser进程绘制)


    3.C++多态的实现


    4.DNS解析,递归与迭代的区别


    5.chromium用的渲染引擎是什么,这个渲染引擎对应的js解释引擎是什么(blink和v8,前几个问题表现有些差,这会在问一些1+1的问题了,哭)


    6.平时怎么学习技术的,看过哪些书,有过哪些输出(我把实习时写的项目wiki给截了个图)


    然后反问,打开牛客让我写了个代码,他不知道去哪了,我自己在这写,写了一个多钟头,你以为这是道很难的题吗?no,是我那会确实很菜,哈哈


    题目是,给一个字符串插入最少的字符,让这个字符串变成回文


    腾讯hr面 (40min)


    1.有哪些缺点


    2.投了哪些,为什么不投阿里头条(实习忙的我面你们都要面不过来了)


    3.如何选择offer


    4.家是哪的,为什么愿意去深圳


    每一个问题都不是简单的答完就完事了,他会跟着问很多


    然后反问时我问问题给我说了20分钟


    抖音 iOS一面 (1h20min)


    上来闲聊了一会


    1.算法:


    字符串大数相加


    写完问我有没有需要优化的地方(内存可以优化一下)


    2.string类赋值运算符是深拷贝还是浅拷贝


    3.算法:


    根据前序和中序输出二叉树的后续遍历


    4.C++ deque底层,deque有没有重载[]


    5.为什么要内存对齐,内存对齐的规则


    6.算法:


    上台阶,加了个条件,这次上两级,下次就只能上一级


    7.反问+闲聊


    抖音 iOS二面 (1h)


    十分钟不到的项目


    1.进程和线程的区别和联系


    2.线程共享哪些内存空间


    3.进程内存模型


    4.进程间通信方式


    5. 虚拟内存,为什么要有虚拟内存,虚拟内存如何映射到物理内存


    后面还挖了一些操作系统的问题,记不太清了


    5.TCP为什么四次挥手


    6.https客户端验证公钥的方法


    7.描述并写一下LRU


    8.说一下怎么学chromium的,怎么上手项目的


    9.C++内存分配,写了一段代码,看里面申请了哪部分内存,申请了多少,代码有什么问题


    10.代码里面的内存泄漏怎么解决,智能指针的引用计数怎么实现,那些成员函数会影响到引用计数


    11.代码里面有无线程安全问题,线程安全问题的是否会导致程序崩溃,为什么


    12.C++虚函数的实现原理,纯虚函数


    13.C++引用和指针的区别,引用能否为空


    抖音 iOS三面 (1h)


    1.lambda表达式,它应用表达式外变量的方式和区别


    2.decltype的作用,他和auto有什么不同


    3.C++的所有智能指针介绍一下


    4.C++thread里面的锁,条件变量,讲一下怎么用他们实现生产者消费者模型


    5.C++20有什么新东西(我就知道支持了协程,然后他说我就想问你协程,然后我说,其实我具体不了解,丢人了)


    6.右值引用是什么,移动构造函数有什么好处


    7.操作系统微内核宏内核(懵)


    8.进程间通信的共享内存,如何保证安全性(信号量),结合epoll讲讲共享内存


    9.TCP协议切片(懵)


    10.TCP协议的流量控制机制,滑窗为0时,怎么办


    11.算法


    合并K个排序链表


    12.合并K个排序数组,讲思路,我说归并,他说,传输参数是数组,不是vector,你如何判断数组的大小



    收起阅读 »

    iOS 整理出一份高级iOS面试题

    1、NSArray与NSSet的区别?NSArray内存中存储地址连续,而NSSet不连续NSSet效率高,内部使用hash查找;NSArray查找需要遍历NSSet通过anyObject访问元素,NSArray通过下标访问2、NSHashTable与NSMa...
    继续阅读 »

    1、NSArray与NSSet的区别?


    • NSArray内存中存储地址连续,而NSSet不连续
    • NSSet效率高,内部使用hash查找;NSArray查找需要遍历
    • NSSet通过anyObject访问元素,NSArray通过下标访问


    2、NSHashTable与NSMapTable?


    • NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
    • NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;可以在访问成员时copy


    (注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。

    NSMapTable与NSDictionary的区别:同上)


    3、属性关键字assign、retain、weak、copy


    • assign:用于基本数据类型和结构体。如果修饰对象的话,当销毁时,属性值不会自动置nil,可能造成野指针。
    • weak:对象引用计数为0时,属性值也会自动置nil
    • retain:强引用类型,ARC下相当于strong,但block不能用retain修饰,因为等同于assign不安全。
    • strong:强引用类型,修饰block时相当于copy。


    4、weak属性如何自动置nil的?


    • Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。


    5、Block的循环引用、内部修改外部变量、三种block


    • block强引用self,self强引用block
    • 内部修改外部变量:block不允许修改外部变量的值,这里的外部变量指的是栈中指针的内存地址。__block的作用是只要观察到变量被block使用,就将外部变量在栈中的内存地址放到堆中。
    • 三种block:NSGlobalBlack(全局)、NSStackBlock(栈block)、NSMallocBlock(堆block)


    6、KVO底层实现原理?手动触发KVO?swift如何实现KVO?


    • KVO原理:当观察一个对象时,runtime会动态创建继承自该对象的类,并重写被观察对象的setter方法,重写的setter方法会负责在调用原setter方法前后通知所有观察对象值得更改,最后会把该对象的isa指针指向这个创建的子类,对象就变成子类的实例。
    • 如何手动触发KVO:在setter方法里,手动实现NSObject两个方法:willChangeValueForKey、didChangeValueForKey
    • swift的kvo:继承自NSObject的类,或者直接willset/didset实现。


    7、categroy为什么不能添加属性?怎么实现添加?与Extension的区别?category覆盖原类方法?多个category调用顺序


    • Runtime初始化时categroy的内存布局已经确定,没有ivar,所以默认不能添加属性。
    • 使用runtime的关联对象,并重写setter和getter方法。
    • Extenstion编译期创建,可以添加成员变量ivar,一般用作隐藏类的信息。必须要有类的源码才可以添加,如NSString就不能创建Extension。
    • category方法会在runtime初始化的时候copy到原来前面,调用分类方法的时候直接返回,不再调用原类。如何保持原类也调用(https://www.jianshu.com/p/40e28c9f9da5)。
    • 多个category的调用顺序按照:Build Phases ->Complie Source 中的编译顺序。


    8、load方法和initialize方法的异同。——主要说一下执行时间,各自用途,没实现子类的方法会不会调用父类的?

    load initialize 调用时机 app启动后,runtime初始化的时候 第一个方法调用前调用 调用顺序 父类->本类->分类 父类->本类(如果有分类直接调用分类,本类不会调用) 没实现子类的方法会不会调用父类的 否 是 是否沿用父类实现 否 是




    9、对 runtime 的理解。——主要是方法调用时如何查找缓存,如何找到方法,找不到方法时怎么转发,对象的内存布局


    OC中向对象发送消息时,runtime会根据对象的isa指针找到对象所属的类,然后在该类的方法列表和父类的方法列表中寻找方法执行。如果在最顶层父类中没找到方法执行,就会进行消息转发:Method resoution(实现方法)、fast forwarding(转发给其他对象)、normal forwarding(完整消息转发。可以转发给多个对象)


    10、runtime 中,SEL和IMP的区别?


    每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址


    11、autoreleasepool的原理和使用场景?


    • 若干个autoreleasepoolpage组成的双向链表的栈结构,objc_autoreleasepoolpush、objc_autoreleasepoolpop、objc_autorelease
    • 使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
    • autoreleasepoolpage的内存结构:4k存储大小



    12、Autorelase对象什么时候释放


    在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。


    13、Runloop与线程的关系?Runloop的mode? Runloop的作用?内部机制?


    • 每一个线程都有一个runloop,主线程的runloop默认启动。
    • mode:主要用来指定事件在运行时循环的优先级
    • 作用:保持程序的持续运行、随时处理各种事件、节省cpu资源(没事件休息释放资源)、渲染屏幕UI


    14、iOS中使用的锁、死锁的发生与避免


    • @synchronized、信号量、NSLock等
    • 死锁:多个线程同时访问同一资源,造成循环等待。GCD使用异步线程、并行队列


    15、NSOperation和GCD的区别


    • GCD底层使用C语言编写高效、NSOperation是对GCD的面向对象的封装。对于特殊需求,如取消任务、设置任务优先级、任务状态监听,NSOperation使用起来更加方便。
    • NSOperation可以设置依赖关系,而GCD只能通过dispatch_barrier_async实现
    • NSOperation可以通过KVO观察当前operation执行状态(执行/取消)
    • NSOperation可以设置自身优先级(queuePriority)。GCD只能设置队列优先级(DISPATCH_QUEUE_PRIORITY_DEFAULT),无法在执行的block中设置优先级
    • NSOperation可以自定义operation如NSInvationOperation/NSBlockOperation,而GCD执行任务可以自定义封装但没有那么高的代码复用度
    • GCD高效,NSOperation开销相对高


    16、oc与js交互


    • 拦截url
    • JavaScriptCore(只适用于UIWebView)
    • WKScriptMessageHandler(只适用于WKWebView)
    • WebViewJavaScriptBridge(第三方框架)


    17、swift相比OC有什么优势?


    18、struct、Class的区别


    • class可以继承,struct不可以
    • class是引用类型,struct是值类型
    • struct在function里修改property时需要mutating关键字修饰


    19、访问控制关键字(public、open、private、filePrivate、internal)


    • public与open:public在module内部中,class和func都可以被访问/重载/继承,外部只能访问;而open都可以
    • private与filePrivate:private修饰class/func,表示只能在当前class源文件/func内部使用,外部不可以被继承和访问;而filePrivate表示只能在当前swift源文件内访问
    • internal:在整个模块或者app内都可以访问,默认访问级别,可写可不写


    20、OC与Swift混编


    • OC调用swift:import "工程名-swift.h” @objc 
    • swift调用oc:桥接文件


    21、map、filter、reduce?map与flapmap的区别?


    • map:数组中每个元素都经过某个方法转换,最后返回新的数组(xx.map({$0 * $0}))
    • flatmap:同map类似,区别在flatmap返回的数组不存在nil,并且会把optional解包;而且还可以把嵌套的数组打开变成一个([[1,2],[2,3,4],[5,6]] ->[1,2,2,3,4,5,6])
    • filter:用户筛选元素(xxx.filter({$0 > 25}),筛选出大于25的元素组成新数组)
    • reduce:把数组元素组合计算为一个值,并接收初始值()




    22、guard与defer


    • guard用于提前处理错误数据,else退出程序,提高代码可读性
    • defer延迟执行,回收资源。多个defer反序执行,嵌套defer先执行外层,后执行内层


    23、try、try?与try!


    • try:手动捕捉异常
    • try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
    • try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash


    24、@autoclosure:把一个表达式自动封装成闭包


    25、throws与rethrows:throws另一个throws时,将前者改为rethrows


    26、App启动优化策略?main函数执行前后怎么优化


    • 启动时间 = pre-main耗时+main耗时
    • pre-main阶段优化:
    • 删除无用代码
    • 抽象重复代码
    • +load方法做的事情延迟到initialize中,或者+load的事情不宜花费太多时间
    • 减少不必要的framework,或者优化已有framework
    • Main阶段优化
    • didFinishLauchingwithOptions里代码延后执行
    • 首次启动渲染的页面优化


    27、crash防护?


    • unrecognized selector crash
    • KVO crash
    • NSNotification crash
    • NSTimer crash
    • Container crash(数组越界,插nil等)
    • NSString crash (字符串操作的crash)
    • Bad Access crash (野指针)
    • UI not on Main Thread Crash (非主线程刷UI (机制待改善))


    28、内存泄露问题?


    主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。


    29、UI卡顿优化?


    30、架构&设计模式


    • MVC设计模式介绍
    • MVVM介绍、MVC与MVVM的区别?
    • ReactiveCocoa的热信号与冷信号
    • 缓存架构设计LRU方案
    • SDWebImage源码,如何实现解码
    • AFNetWorking源码分析
    • 组件化的实施,中间件的设计
    • 哈希表的实现原理?如何解决冲突


    31、数据结构&算法


    • 快速排序、归并排序
    • 二维数组查找(每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数)
    • 二叉树的遍历:判断二叉树的层数
    • 单链表判断环


    32、计算机基础


    1. http与https?socket编程?tcp、udp?get与post?
    2. tcp三次握手与四次握手
    1. 进程与线程的区别



    收起阅读 »