关于原理:使用第一性原理思维思考如何打造提高生产力的平台-京东云技术团队

引言当初全社会都在搞数字化转型,从政府到企业,那么为什么要进行数字化转型呢?实质上还是社会治理和企业经营难度变得更大了。 以企业来说,转型的指标是为了实现有品质的活着,比方能赚更多的钱或者持续保持持重经营,转型的外围是冀望借助数字化技术构建一个管理体系,以应答外部环境动荡、企业竞争变动和技术更新倒退带来的不确定性。 数字化转型会带来大量的研发需要,如何更好更快的交付这些需要成为一个突出问题,该怎么打造一个平台去解决该问题?能不能用第一性原理思维去推导出倒退方向? 什么是第一性原理第一性原理指的是,将问题拆分成最根本的事实或法则,依据这些已知信息,一直推演和计算,从而找到解决问题最优门路的办法。 该思维比拟闻名的利用例子有 埃隆·马斯克升高火箭发射老本的故事。将火箭发射老本进行细分,倒退火箭可重复使用发射技术,简化设计和改良工艺以升高火箭制作老本亨利·福特升高汽车制作老本的故事。将汽车拆解为最根本的部件,利用装配线和流水线工人批量制作汽车埃隆·马斯克升高汽车电池组老本的故事。剖析电池的原材料市场价格,一直迫近这个价格极限,采纳自建工厂、改良电池设计、改良生产过程等措施来降低成本乔布斯去掉手机物理键盘的故事。剖析手机的组成部件,通过去掉物理键盘来扩充触摸屏区域,带来更好的体验和更低的制作老本如果采纳类推思维去思考类推思维是指借助本人的感觉和教训积攒常识的办法 以国内外做得好的平台为例,微软的 Power Platform、守业公司轻流这些平台都以帮忙企业疾速开发利用、应答市场倒退为目标。 微软 Power Platform[]() 除了上述 5 大产品外,还具备以下性能 Dataverse:能够进行数据的治理,相当于是个更易于应用的数据库连接器:能够和大量的 API、服务、零碎进行通信Common Data Model:标准化和可扩大的数据模型,能不便数据的流转和利用,由微软和行业合作伙伴多年积攒而成生态系统:Power Platform 能够连贯到微软的生态系统,例如 Azure(寰球第二大云计算平台)、Microsoft 365(蕴含 Office 办公套件和 Teams)、Dynamics 365(蕴含 ERP 和 CRM 性能)Power Fx:低代码语言,相似 Excel 的公式,相比业余的编程语言更易于应用Power Platform 的产品理念是让没有技术背景的人、开发者都能轻松高效的解决业务问题。产品架构上以 Dataverse 为外围,内置数据模型(Common Data Model)升高业务建模老本,可视化的 UI 编辑器(Power Apps、Power Pages)可升高利用开发成本,报表制作工具(Power BI)可挖掘出数据的价值,效率工具(Power Automate 和 Power Virtual Agents)可将员工从干燥反复的工作中解放出来,这一整套组合拳下来,冀望帮忙企业更好的倒退业务,节俭经营老本 微软这么做了,咱们是不是也要这么做呢?即 开发一套非专业开发者也能学会应用的数据库开发可视化的 UI 编辑器,反对挪动利用、门户、报表的制作开发效率工具,实现工作的自动化,反对桌面软件、Web、即时聊天窗口开发一种非专业开发者也能学会应用的编程语言轻流All-in-One 利用开发平台 []() 专有轻流,面向多角色的企业级平台产品 []() 轻流是从表单+流程治理这种高频场景切入的,面向的是业务人员而不是开发者,随着行业案例的积攒,客户的增多, 逐渐倒退为疾速解决业务问题的利用开发平台和面向中大客户的企业级平台这两个套餐分级迭代。 值得一提的是轻流和神策一样,也采纳了订阅制免费模式,这样保障了不论是私有云还是私有化部署都能以高迭代速度去服务客户 轻流这么做了,咱们是不是也要这么做呢?即 开发一套通过表单来进行数据建模的工具,反对流程治理,反对自动化工作开发可视化的 UI 编辑器,反对挪动利用、门户、报表的制作提供插件能力,不便 IT 人员进行系统集成和性能扩大继续集成内部支流零碎和业务能力,丰盛平台生态思考这些平台都提供了数据建模能力,一种是相似数据库的建模过程,一种是表单的建模过程这些平台都提供了 UI 编辑器,能够进行前端页面的可视化搭建,反对多平台(挪动端、Web 端)这些平台都提供了代码开发能力,能够进行代码级别的逻辑编写这些平台都提供了扩大机制,能够集成内部零碎和业务能力看来,如果要做一个生产力平台,就必须提供这四大性能 ...

July 6, 2023 · 1 min · jiezi

关于原理:PC-GWPASan方案原理-堆破坏问题排查实践

背景家喻户晓,堆crash dump是最难剖析的dump类型之一。此类crash最大的问题在于,造成谬误的代码无奈在产生堆毁坏时被发现。线上采集到的minidump,仅能提供非常无限的信息。当调试工具报告了堆毁坏、堆内存拜访违例后,即使是有教训的开发人员也会感觉头疼。 剪映专业版及其依赖的音视频编辑SDK、特效模块均采纳MD的形式链接规范库,这意味着任何一个模块呈现了堆损坏都会相互影响。从crash的地位回溯堆毁坏的源头,是一个十分有挑战性的工作。剪映业务模块较常见的是Use-after-free,而音视频编辑SDK和特效模块这类底层算法特效模块更多的是Buffer-overflow,不同团队模块间的堆谬误相互影响,导致问题难以定位。 GWP-ASan是Google主导开发的用于检测堆内存问题的调试工具。它基于经典的Electric Fence Malloc调试器原理,概率采样内存调配行为,抓取内存问题并生成上传解体报告。说到这里,兴许你会好奇它和ASan(Address Sanitizer)的区别。ASan是一种编译器调试工具,监控所有内存调配行为,能够发现栈、堆和全局内存问题,但它性能开销很高(2-3倍),不适宜线上应用。GWP-ASan相较于ASan,尽管无奈发现栈内存和全局内存问题,但因为它是采样监控,性能耗费能够忽略不计,更实用于线上场景。目前,GWP-ASan可检测的谬误有: Use-after-freeBuffer-underflowBuffer-overflowDouble-freefree-invalid-address Electric Fence Malloc调试器:https://linux.die.net/man/3/efenceGWP-ASan有多种实现计划,本计划基于Windows平台阐明,字节外部APM-PC平台相较于市面上其余计划的亮点有: 无侵入式接入,能够检测特定类型三方库的内存调配。反对无感知监测,发现异常后过程可持续运行。反对调整检测所用的堆页面个数配置和采样率配置,灵便调整性能耗费。剪映专业版接入字节外部APM-PC平台的GWP-ASan性能后,帮忙业务、音视频编辑SDK、特效模块解决30余例疑难堆crash。GWP-ASan dump比原生dump提供了更丰盛的信息,并指出了堆crash关联的信息细节,升高了疑难crash的排查难度,无效缩短了研发排查、修复问题的工夫。 技术计划监控原理检测原理概述创立受爱护内存池:首先,咱们须要保留一块间断的n*page size的受爱护内存池。其中,可分配内存的page是Slot,不可分配内存的page是Guard Page。Slot和Guard Page距离散布,整个内存池最前和最初都是Guard Page,所有的Slot都受到Guard Page爱护,之后利用调配的堆内存将随机采样调配到这些Slot上。 采样监控内存调配行为,记录堆栈:之后,hook利用堆内存调配行为,每次调配堆内存时,随机决定指标内存是走GWP-ASan调配——调配在一个闲暇的Slot上,还是走零碎原生调配。如果走GWP-ASan调配,那么指标内存会被随机左对齐/右对齐调配在一个闲暇的Slot上,同时记录分配内存的堆栈信息。 而当开释内存时,会先判断指标内存是否在GWP-ASan受爱护内存池上,如果是,那么开释这块内存和其所在的Slot,同时记录开释内存的堆栈。slot闲暇后,能够从新被用于调配。堆栈信息记录在metadata中。 继续监测,记录异样:   首先,咱们须要晓得Guard Page和闲暇的Slot都是不可读写的。接下来咱们看看GWP-ASan是如何发现异常的:Use-after-free: Slot上未分配内存时,是不可读写的。当拜访到不可读写的Slot时,利用抛出异样,此时查看该Slot是否刚开释过内存,如果开释过内存,那么能够断定此异样为Use-after-free。Buffer-underflow:当内存左对齐调配在Slot上时,如果产生了underflow,利用会拜访到Slot左侧不可读写的Guard Page,利用抛出异样,此异样为Buffer-underflow。Buffer-overflow:当内存右对齐调配在Slot上时,如果产生了overflow,利用会拜访到Slot右侧不可读写的Guard Page,利用抛出异样,此异样为Buffer-overflow。Double-free:利用开释内存时,首先查看指标内存地址是否位于受爱护内存池区间内,如是,由GWP-ASan开释内存,开释前查看指标内存地址所在Slot是否曾经被开释,如是,那么能够断定此异样为Double-free。Free-invalid-address: 利用开释内存时,首先查看指标内存地址是否位于受爱护内存池区间内,如是,由GWP-ASan开释内存,开释前先查看要开释的内存地址和之前调配返回的内存地址是否相等,如果不相等,那阐明指标开释地址是非法地址。此异样为Free-invalid-address。堆内存调配API后面曾经提到,GWP-ASan用于检测堆内存问题,为了检测堆内存问题,必须先感知利用内存调配行为。很天然的,咱们会想到hook内存调配办法,然而该hook哪个办法呢? 下图形容了Windows利用调配堆内存的可用办法: GlobalAlloc/LocalAlloc是为了兼容Windows旧版本的API,当初根本不实用,所以不监控。HeapAlloc/HeapFree个别用于过程分配内存,不监控。VirtualAlloc是应用层内存调配的底层实现,开发个别不间接用此API分配内存,它离利用调配堆内存行为太远,堆栈参考意义不大;且Windows GWP-ASan须要基于此实现,因而,也不监控。 最终选定Hook malloc/free等系列办法,hook malloc/free后,能感知到用户调配的堆内存。 ### Hook计划 上面的计划都是应用层的Hook计划,内核层Hook仅实用于x86平台。 Detours库作为微软官网出品的hook库,兼容性佳,稳定性好,是最佳抉择。然而还须要留神的是,Windows下,运行时库配置会影响hook后果,Detours只能无侵入式hook/MD库的内存调配行为,/MT库须要提供本身内存调配的函数指针能力hook。 堆栈记录首先要阐明的是,GWP-ASan监控依赖解体监控。Use-after-free、Buffer-underflow、Buffer-overflow都是在客户端产生异样后,联合GWP-ASan的metadata去断定的。目前字节外部APM-PC平台的解体报告格局为minidump。一个minidump文件由多种streams组成,如thread_list_stream、module_list_stream和exception_stream等等。不同stream记录了不同信息,咱们能够将GWP-ASan采集到的异样信息视为独自的gwpasan_stream,附加到minidump文件中。 GWP-ASan采集的信息次要包含:谬误类型、调配地址和大小、调配堆栈、开释堆栈(如有)、受爱护内存池起止地址。这些信息基于Protobuf协定序列化后,被增加到minidump文件中。GWP-ASan通过Windows native API CaptureStackBackTrace API在客户端回溯 “开释/调配” 堆栈。minidump上传到平台后,平台抽取出GWP-ASan信息,联合minidump中loaded module list,联合相干模块的符号表,符号化GWP-ASan调配/开释堆栈。GWP-ASan信息联合minidump本来的信息,根本就能定位问题。 监控流程 拓展场景无解体计划GWP-ASan检测到异样后,会被动解体导致客户端过程退出,给用户带来了不良体验。无解体的GWP-ASan检测到异样后,再将对应内存页标注为可读写的(如为use-after-free/buffer-underflow/buffer-overflow),仅生成上传解体报告,不被动终结过程,客户端标注异样已解决。用户无感知,程序持续运行。须要留神的是,客户端在UEF里标记拜访区域内存页为可读写内存页可能影响后续的GWP-ASan检测。 实战分享Use-After-Free:开释后应用理论案例 1咱们看下惯例的dump输入,windbg告知咱们程序crash在25行。 因为12行有空指针查看,能够排除空指针问题。 执行.ecxr复原异样现场也能够证实,此crash和空指针无关。只是一个内存拜访违例。 汇编指定地址,能够晓得这个crash动作是在读取类的虚指针,读取内存的过程中crash了。 00007ffb`d422e4a0 498b06 mov rax,qword ptr [r14]00007ffb`d422e4a3 488bd5 mov rdx,rbp00007ffb`d422e4a6 498bce mov rcx,r1400007ffb`d422e4a9 ff10 call qword ptr [rax]查看问题代码: ...

April 12, 2023 · 4 min · jiezi

关于原理:得物技术深入理解synchronzied底层原理

一、synchronized简介synchronized是Java中的关键字,是一种同步锁。在多线程编程中,有可能会呈现多个线程同时争抢同一个共享资源的状况,这个资源个别被称为临界资源。这种共享资源能够被多个线程同时拜访,且又能够同时被多个线程批改,然而线程的执行是须要CPU的资源调度,其过程是不可控的,所以须要采纳一种同步机制来管制对共享资源的拜访,于是线程同步锁——synchronized就应运而生了。 二、如何解决线程并发平安问题多线程并发读写访问临界资源的状况下,是会存在线程平安问题的,能够采纳的同步互斥拜访的形式,就是在同一时刻,只能有同一个线程可能拜访到临界资源。当多个线程执行同一个办法时,该办法外部的局部变量并不是临界资源,因为这些局部变量会在类加载的时候存在每个线程的公有栈的局部变量表中,因而不属于共享资源,所有不会导致线程平安问题。 三、synchronized用法synchronized关键字最次要有以下3种应用形式: 润饰类办法,作用于以后类加锁,如果多个线程不同对象拜访该办法,则无奈保障同步。润饰静态方法,作用于以后类对象加锁,进入同步代码前要取得以后类对象的锁,锁的是蕴含这个办法的类,也就是类对象,这样如果多个线程不同对象拜访该静态方法,也是能够保障同步的。润饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要取得给定对象的锁。四、Synchronized原理剖析能够先通过一个简略的案例看一下同步代码块: public class SynchTestDemo { public void print() { synchronized ("得物") { System.out.println("Hello World"); } } }synchronized属于Java关键字,没方法间接看到其底层源码,所以只能通过class文件进行反汇编。 先通过javac SynchTestDemo.java指令间接SynchTestDemo.java文件编译成SynchTestDemo.class文件;再通过javap -v SynchTestDemo.class指令再对SynchTestDemo.class文件进行反汇编,能够失去上面的字节码指令: 这些反编译的字节码指令这里就不具体解释了,对照着JVM指令手册也能看懂是什么意思。通过上图反编译的后果能够看出,monitorexit指令实际上是执行了两次,第一次是失常状况下开释锁,第二次为产生异常情况时开释锁,这样做的目标在于保障线程不死锁。 monitorenter首先能够看一下JVM标准中对于monitorenter的形容: 翻译过去就是:任何一个对象都有一个monitor与其相关联,当且有一个monitor被持有后,它将处于锁定的状态,其余线程无奈来获取该monitor。当JVM执行某个线程的某个办法外部的monitorenter时,他会尝试去获取以后对应的monitor的所有权。其过程如下: 如果monitor的进入数为0,则该线程进入monitor,而后将进入数设置为1,该线程即为monitor的所有者;如果线程曾经占有该monitor,只是从新进入,则进入monitor的进入数加1;如果其余线程曾经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再从新尝试获取monitor的所有权;monitorexit也能够先看一下JVM标准中对monitorexit的形容: 翻译过去就是: 能执行monitorexit指令的线程肯定是领有以后对象的monitor的所有权的线程;执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,以后线程退出monitor,不再领有monitor的所有权,此时其余被这个monitor阻塞的线程能够尝试去获取这个monitor的所有权;synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令别离在同步块逻辑代码的起始地位与完结地位,如下图所示: 每个同步对象都有一个本人的Monitor(监视器锁),加锁过程如下图所示: 通过下面的形容能够看出synchronized的实现原理:synchronized的底层理论是通过一个monitor对象来实现的,其实wait/notify办法也是依赖于monitor对象来实现的,这就是为什么只有在同步代码块或者办法中能力调用该办法,否则就会抛出出java.lang.IllegalMonitorStateException的异样的起因。 上面能够再通过一个简略的案例看一下同步办法: public class SynchTestDemo { public synchronized void print() { System.out.println("Hello World"); } }与下面同理能够查看到,该办法的字节码指令: 从字节码反编译的能够看出,同步办法并没有通过指令monitorenter和monitorexit来实现的,然而绝对于一般办法来说,其常量池多了了 ACC_SYNCHRONIZED 标示符。JVM理论就是依据该标识符来实现办法的同步的。 当办法被调用时,会查看ACC_SYNCHRONIZED标记是否被设置,若被设置,线程会先获取monitor,获取胜利能力执行办法体,办法执行实现后会再次开释monitor。在办法执行期间,其余线程都无奈取得同一个monitor对象。 其实两种同步形式从实质上看是没有区别的,两个指令的执行都是JVM调用操作系统的互斥原语mutex来实现的,被阻塞的线程会被挂起、期待从新调度,会导致线程在“用户态”和“内核态”进行切换,就会对性能有很大的影响。 五、什么是monitor?monitor通常被形容为一个对象,能够将其了解为一个同步工具,或者能够了解为一种同步机制。所有的Java对象自打new进去的时候就自带了一把锁,就是monitor锁,也就是对象锁,存在于对象头(Mark Word),锁标识位为10,指针指向的是monitor对象起始地址。在Java虚拟机(HotSpot)中,Monitor是由其底层理论是由C++对象ObjectMonitor实现的: ObjectMonitor() { _header = NULL; _count = 0; //用来记录该线程获取锁的次数 _waiters = 0, _recursions = 0; // 线程的重入次数 _object = NULL; // 存储该monitor的对象 _owner = NULL; // 标识领有该monitor的线程 _WaitSet = NULL; // 处于wait状态的线程,会被退出到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL; _succ = NULL; _cxq = NULL; // 多线程竞争锁时的单向队列 FreeNext = NULL; _EntryList = NULL; // 处于期待锁block状态的线程,会被退出到该列表 _SpinFreq = 0; _SpinClock = 0; OwnerIsThread = 0;}_owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的惟一标识。当线程开释monitor时,owner又复原为NULL。owner是一个临界资源,JVM是通过CAS操作来保障其线程平安的;_cxq:竞争队列,所有申请锁的线程首先会被放在这个队列中(单向链接)。cxq是一个临界资源,JVM通过CAS原子指令来批改cxq队列。批改前cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因而_cxq是一个后进先出的stack(栈);_EntryList:_cxq队列中有资格成为候选资源的线程会被挪动到该队列中;_WaitSet:因为调用wait办法而被阻塞的线程会被放在该队列中。举个例子具体分析一下_cxq队列与_EntryList队列的区别: ...

September 29, 2021 · 7 min · jiezi

关于原理:JS-原生方法原理探究五如何实现-instanceof

这是JS 原生办法原理探索系列的第五篇文章。本文会介绍如何实现 instanceof 办法。 typeof 操作符返回一个示意数据类型的字符串,它能够应酬惯例场景下的数据类型判断。对根本数据类型 undefined、 boolean、string、 number、Symbol 和援用数据类型 function 都能够正确判断,然而对 null、数组、对象等则对立返回 "object"。 比如说: function F1(){}function F2(){}const obj1 = new F1()const obj2 = new F2()typeof obj1 // ‘object’typeof obj2 // 'object' 这里只能看出 obj1 和 obj2 是对象,但不晓得具体是哪个构造函数创立的对象。 但应用 instanceof 之后,就高深莫测了: console.log(obj1 instanceof F1) // trueconsole.log(obj1 instanceof F2) // falseconsole.log(obj2 instanceof F2) // true依据 MDN 的形容: instanceof 运算符用于检测构造函数的 prototype 属性是否呈现在某个实例对象的原型链上。instanceof 运算符有两个操作数,左操作数通常是一个实例对象,它的类型能够是对象或者函数,也能够是根本类型(这种状况下不会报错,但总返回 false),右操作数通常是一个可调用的(callable)对象,咱们能够间接认为它的类型应该是一个函数。 那么 instanceof 的实现原理是什么呢?从定义中咱们能够看到,它的原理和原型链的机制无关,具体地说,它会拿到右操作数的原型对象,而后在左操作数上通过 __proto__ 一直查找实例的原型链,只有右操作数的 prototype 呈现在左操作数的原型链上时,就返回 true。如果原型链始终查找到止境 —— 也就是 null,还没有找到右操作数的原型,就返回 false。 ...

June 8, 2021 · 2 min · jiezi

关于原理:JS-原生方法原理探究三从规范解读如何实现-new-操作符

这是 JS 原生办法原理探索系列的第三篇文章。本文会介绍如何模仿实现 new 操作符。对于 new 的具体用法,MDN 曾经形容得很分明了,这里咱们不说废话,间接讲如何模仿实现。 new 操作符的标准留神:上面展现的所有标准都是 ES5 版本的,与当初最新的标准有些区别首先看一下依据标准的形容, new 操作符做了什么事: 全是英文,不过没关系,我简略翻译一下: 我在应用 new 操作符的时候,前面跟着的构造函数可能带参数,也可能不带参数,如果不带参数的话,比如说 new Fn(),那么这里这个 Fn 就是一个 NewExpression;如果带参数,比如说 new Fn(name,age),那么这里的 Fn 就是一个 MemberExpression。 这两种状况下应用 new 操作符所进行的操作有点点不同,这里拿带参数的状况阐明一下: 首先会对 Fn 这个 MemberExpression 求值,其后果是指向理论函数对象的一个援用,咱们把这个援用作为 ref接着调用 GetValue(ref) 进行求值,失去理论的函数对象,把这个对象作为 constructor对 Arguments 也就是传进来的参数求值,失去一个参数列表,作为 argList如果 constructor 不是对象,则抛出类型谬误如果 constructor 没有实现外部的 [[Constructor]] 办法,也抛出类型谬误调用 constructor 的 [[Constructor]]办法,并将 argList 传入作为参数,返回调用后果从这些形容能够看出,更多的实现细节放在函数的 [[Constructor]] 办法里。那么这个办法具体是做什么用的呢? [[Constructor]] 的标准在 JS 中,函数有两种调用形式,一种是失常调用,这将调用函数的外部办法 [[Call]],还有一种是通过 new 调用,此时的函数作为一个构造函数,这将调用函数的另一个外部办法 [[Consturct]]。所以,要实现 new 操作的话,咱们得先搞懂 [[Construct]] 外部办法做了什么事。 ...

June 8, 2021 · 2 min · jiezi

Webpack-是怎样运行的

在平时开发中我们经常会用到Webpack这个时下最流行的前端打包工具。它打包开发代码,输出能在各种浏览器运行的代码,提升了开发至发布过程的效率。 我们知道一份Webpack配置文件主要包含入口(entry)、输出文件(output)、模式、加载器(Loader)、插件(Plugin)等几个部分。但如果只需要组织 JS 文件的话,指定入口和输出文件路径即可完成一个迷你项目的打包。下面我们来通过一个简单的项目来看一下Webpack是怎样运行的。 同步加载本文使用 webpack ^4.30.0 作示例.为了更好地观察产出的文件,我们将模式设置为 development 关闭代码压缩,再开启 source-map 支持原始源代码调试。除此之外。我们还简单的写了一个插件MyPlugin来去除源码中的注释。新建src/index.js console.log('Hello webpack!');新建webpack配置文件webpack.config.js const path = require('path');const MyPlugin = require('./src/MyPlugin.js')module.exports = { mode: 'development', devtool: 'source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist') }, plugins:[ new MyPlugin() ]};新建src/MyPlugin.js。了解webpack插件更多信息 class MyPlugin { constructor(options) { this.options = options this.externalModules = {} } apply(compiler) { var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g compiler.hooks.emit.tap('CodeBeautify', (compilation)=> { Object.keys(compilation.assets).forEach((data)=> { let content = compilation.assets[data].source() // 欲处理的文本 content = content.replace(reg, function (word) { // 去除注释后的文本 return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; }); compilation.assets[data] = { source(){ return content }, size(){ return content.length } } }) }) }}module.exports = MyPlugin现在我们运行命令 webpack --config webpack.config.js ,打包完成后会多出一个输出目录 dist:dist/main.js。main 是 webpack 默认设置的输出文件名,我们快速瞄一眼这个文件: ...

May 9, 2019 · 7 min · jiezi

libuv核心开发者眼中的Nodejs-Event-loop

文中提到的event loop均是指node.js中的。一般网络上讲解的event loop,都会出现这张图: libuv的核心开发者Bert Belder觉得不太正确,他认为下面这张图更接近libuv的event loop原理,但也不完全正确: 他认为真正的event loop应该差不多是这样的: 图中左侧分别有入口和出口箭头,入口代表node js文件开始执行,出口代表执行完成;黄色JS方块代表同步JS的执行;其他图标分别对应node.js官网中event loop的讲解[1] ┌───────────────────────────┐┌─>│ timers ││ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐│ │ pending callbacks ││ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐│ │ idle, prepare ││ └─────────────┬─────────────┘ ┌───────────────┐│ ┌─────────────┴─────────────┐ │ incoming: ││ │ poll │<─────┤ connections, ││ └─────────────┬─────────────┘ │ data, etc. ││ ┌─────────────┴─────────────┐ └───────────────┘│ │ check ││ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐└──┤ close callbacks │ └───────────────────────────┘闹钟图标代表:timers (setTimeout, setInterval)独角兽图标代表:poll感叹号图标代表:check (setImmediately)扫帚图标代表:close callbacks(比如socket.on('close', callback)) 每个图标是一种类型的callback任务队列,图标之间都会执行JS同步代码;同步代码中出现异步的API时,对一个全局计数器ref++,然后交给相应图标对应的模块去处理;处理完成之后放到对应的图标中,同时对全局计数器ref--;扫帚图标(close callbacks)之后,如果ref为0,则结束js运行,如果大于0,则继续loop; 参考来源:[1] Everything You Need to Know About Node.js Event Loop - Bert Belder[2] The Node.js Event Loop ...

May 9, 2019 · 1 min · jiezi

干货|Spring Cloud Stream 体系及原理介绍

Spring Cloud Stream 在 Spring Cloud 体系内用于构建高度可扩展的基于事件驱动的微服务,其目的是为了简化消息在 Spring Cloud 应用程序中的开发。Spring Cloud Stream (后面以 SCS 代替 Spring Cloud Stream) 本身内容很多,而且它还有很多外部的依赖,想要熟悉 SCS,必须要先了解 Spring Messaging 和 Spring Integration 这两个项目,接下来文章将从以下几点跟大家进行介绍:什么是 Spring Messaging;什么是 Spring Integration;什么是 SCS及其功能;Spring MessagingSpring Messaging 是 Spring Framework 中的一个模块,其作用就是统一消息的编程模型。比如消息 Messaging 对应的模型就包括一个消息体 Payload 和消息头 Header:package org.springframework.messaging;public interface Message<T> { T getPayload(); MessageHeaders getHeaders();}消息通道 MessageChannel 用于接收消息,调用 send 方法可以将消息发送至该消息通道中 :@FunctionalInterfacepublic interface MessageChannel { long INDEFINITE_TIMEOUT = -1; default boolean send(Message<?> message) { return send(message, INDEFINITE_TIMEOUT); } boolean send(Message<?> message, long timeout);}消息通道里的消息如何被消费呢?由消息通道的子接口可订阅的消息通道 SubscribableChannel 实现,被 MessageHandler 消息处理器所订阅:public interface SubscribableChannel extends MessageChannel { boolean subscribe(MessageHandler handler); boolean unsubscribe(MessageHandler handler);}由MessageHandler 真正地消费/处理消息:@FunctionalInterfacepublic interface MessageHandler { void handleMessage(Message<?> message) throws MessagingException;}Spring Messaging 内部在消息模型的基础上衍生出了其它的一些功能,如:消息接收参数及返回值处理:消息接收参数处理器 HandlerMethodArgumentResolver 配合 @Header, @Payload 等注解使用;消息接收后的返回值处理器 HandlerMethodReturnValueHandler 配合 @SendTo 注解使用;消息体内容转换器 MessageConverter;统一抽象的消息发送模板 AbstractMessageSendingTemplate;消息通道拦截器 ChannelInterceptor;Spring IntegrationSpring Integration 提供了 Spring 编程模型的扩展用来支持企业集成模式(Enterprise Integration Patterns),是对 Spring Messaging 的扩展。它提出了不少新的概念,包括消息的路由 MessageRoute、消息的分发 MessageDispatcher、消息的过滤 Filter、消息的转换 Transformer、消息的聚合 Aggregator、消息的分割 Splitter 等等。同时还提供了 MessageChannel 和MessageHandler 的实现,分别包括 DirectChannel、ExecutorChannel、PublishSubscribeChannel 和MessageFilter、ServiceActivatingHandler、MethodInvokingSplitter 等内容。首先为大家介绍几种消息的处理方式:消息的分割:消息的聚合:消息的过滤:消息的分发:接下来,我们以一个最简单的例子来尝试一下 Spring Integration:SubscribableChannel messageChannel = new DirectChannel(); // 1messageChannel.subscribe(msg -> { // 2 System.out.println(“receive: " + msg.getPayload());});messageChannel.send(MessageBuilder.withPayload(“msg from alibaba”).build()); // 3构造一个可订阅的消息通道 messageChannel;使用 MessageHandler 去消费这个消息通道里的消息;发送一条消息到这个消息通道,消息最终被消息通道里的 MessageHandler 所消费,最后控制台打印出: receive: msg from alibaba;DirectChannel 内部有个 UnicastingDispatcher 类型的消息分发器,会分发到对应的消息通道 MessageChannel 中,从名字也可以看出来,UnicastingDispatcher 是个单播的分发器,只能选择一个消息通道。那么如何选择呢? 内部提供了 LoadBalancingStrategy 负载均衡策略,默认只有轮询的实现,可以进行扩展。我们对上段代码做一点修改,使用多个 MessageHandler 去处理消息:SubscribableChannel messageChannel = new DirectChannel();messageChannel.subscribe(msg -> { System.out.println(“receive1: " + msg.getPayload());});messageChannel.subscribe(msg -> { System.out.println(“receive2: " + msg.getPayload());});messageChannel.send(MessageBuilder.withPayload(“msg from alibaba”).build());messageChannel.send(MessageBuilder.withPayload(“msg from alibaba”).build());由于 DirectChannel 内部的消息分发器是 UnicastingDispatcher 单播的方式,并且采用轮询的负载均衡策略,所以这里两次的消费分别对应这两个 MessageHandler。控制台打印出:receive1: msg from alibabareceive2: msg from alibaba既然存在单播的消息分发器 UnicastingDispatcher,必然也会存在广播的消息分发器,那就是 BroadcastingDispatcher,它被 PublishSubscribeChannel 这个消息通道所使用。广播消息分发器会把消息分发给所有的 MessageHandler:SubscribableChannel messageChannel = new PublishSubscribeChannel();messageChannel.subscribe(msg -> { System.out.println(“receive1: " + msg.getPayload());});messageChannel.subscribe(msg -> { System.out.println(“receive2: " + msg.getPayload());});messageChannel.send(MessageBuilder.withPayload(“msg from alibaba”).build());messageChannel.send(MessageBuilder.withPayload(“msg from alibaba”).build());发送两个消息,都被所有的 MessageHandler 所消费。控制台打印:receive1: msg from alibabareceive2: msg from alibabareceive1: msg from alibabareceive2: msg from alibabaSpring Cloud StreamSCS与各模块之间的关系是:SCS 在 Spring Integration 的基础上进行了封装,提出了 Binder, Binding, @EnableBinding, @StreamListener 等概念;SCS 与 Spring Boot Actuator 整合,提供了 /bindings, /channels endpoint;SCS 与 Spring Boot Externalized Configuration 整合,提供了 BindingProperties, BinderProperties 等外部化配置类;SCS 增强了消息发送失败的和消费失败情况下的处理逻辑等功能。SCS 是 Spring Integration 的加强,同时与 Spring Boot 体系进行了融合,也是 Spring Cloud Bus 的基础。它屏蔽了底层消息中间件的实现细节,希望以统一的一套 API 来进行消息的发送/消费,底层消息中间件的实现细节由各消息中间件的 Binder 完成。Binder 是提供与外部消息中间件集成的组件,为构造 Binding提供了 2 个方法,分别是 bindConsumer 和 bindProducer ,它们分别用于构造生产者和消费者。目前官方的实现有 Rabbit Binder 和 Kafka Binder, Spring Cloud Alibaba 内部已经实现了 RocketMQ Binder。从图中可以看出,Binding 是连接应用程序跟消息中间件的桥梁,用于消息的消费和生产。我们来看一个最简单的使用 RocketMQ Binder 的例子,然后分析一下它的底层处理原理:启动类及消息的发送:@SpringBootApplication@EnableBinding({ Source.class, Sink.class }) // 1public class SendAndReceiveApplication { public static void main(String[] args) { SpringApplication.run(SendAndReceiveApplication.class, args); } @Bean // 2 public CustomRunner customRunner() { return new CustomRunner(); } public static class CustomRunner implements CommandLineRunner { @Autowired private Source source; @Override public void run(String… args) throws Exception { int count = 5; for (int index = 1; index <= count; index++) { source.output().send(MessageBuilder.withPayload(“msg-” + index).build()); // 3 } } }}消息的接收:@Servicepublic class StreamListenerReceiveService { @StreamListener(Sink.INPUT) // 4 public void receiveByStreamListener1(String receiveMsg) { System.out.println(“receiveByStreamListener: " + receiveMsg); }}这段代码很简单,没有涉及到 RocketMQ 相关的代码,消息的发送和接收都是基于 SCS 体系完成的。如果想切换成 RabbitMQ 或 kafka,只需修改配置文件即可,代码无需修改。我们分析这段代码的原理:@EnableBinding 对应的两个接口属性 Source 和 Sink 是 SCS 内部提供的。SCS 内部会基于 Source 和 Sink 构造 BindableProxyFactory,且对应的 output 和 input 方法返回的 MessageChannel 是 DirectChannel。output 和 input 方法修饰的注解对应的 value 是配置文件中 binding 的 name。public interface Source { String OUTPUT = “output”; @Output(Source.OUTPUT) MessageChannel output();}public interface Sink { String INPUT = “input”; @Input(Sink.INPUT) SubscribableChannel input();}配置文件里 bindings 的 name 为 output 和 input,对应 Source 和 Sink 接口的方法上的注解里的 value:spring.cloud.stream.bindings.output.destination=test-topicspring.cloud.stream.bindings.output.content-type=text/plainspring.cloud.stream.rocketmq.bindings.output.producer.group=demo-groupspring.cloud.stream.bindings.input.destination=test-topicspring.cloud.stream.bindings.input.content-type=text/plainspring.cloud.stream.bindings.input.group=test-group1构造 CommandLineRunner,程序启动的时候会执行 CustomRunner 的 run 方法。调用 Source 接口里的 output 方法获取 DirectChannel,并发送消息到这个消息通道中。这里跟之前 Spring Integration 章节里的代码一致。Source 里的 output 发送消息到 DirectChannel 消息通道之后会被 AbstractMessageChannelBinder#SendingHandler 这个 MessageHandler 处理,然后它会委托给 AbstractMessageChannelBinder#createProducerMessageHandler 创建的 MessageHandler 处理(该方法由不同的消息中间件实现);不同的消息中间件对应的 AbstractMessageChannelBinder#createProducerMessageHandler 方法返回的 MessageHandler 内部会把 Spring Message 转换成对应中间件的 Message 模型并发送到对应中间件的 broker;使用 @StreamListener 进行消息的订阅。请注意,注解里的 Sink.input 对应的值是 “input”,会根据配置文件里 binding 对应的 name 为 input 的值进行配置:不同的消息中间件对应的 AbstractMessageChannelBinder#createConsumerEndpoint 方法会使用 Consumer 订阅消息,订阅到消息后内部会把中间件对应的 Message 模型转换成 Spring Message;消息转换之后会把 Spring Message 发送至 name 为 input 的消息通道中;@StreamListener 对应的 StreamListenerMessageHandler 订阅了 name 为 input 的消息通道,进行了消息的消费;这个过程文字描述有点啰嗦,用一张图总结一下(黄色部分涉及到各消息中间件的 Binder 实现以及 MQ 基本的订阅发布功能):SCS 章节的最后,我们来看一段 SCS 关于消息的处理方式的一段代码:@StreamListener(value = Sink.INPUT, condition = “headers[‘index’]==‘1’")public void receiveByHeader(Message msg) { System.out.println(“receive by headers[‘index’]==‘1’: " + msg);}@StreamListener(value = Sink.INPUT, condition = “headers[‘index’]==‘9999’")public void receivePerson(@Payload Person person) { System.out.println(“receive Person: " + person);}@StreamListener(value = Sink.INPUT)public void receiveAllMsg(String msg) { System.out.println(“receive allMsg by StreamListener. content: " + msg);}@StreamListener(value = Sink.INPUT)public void receiveHeaderAndMsg(@Header(“index”) String index, Message msg) { System.out.println(“receive by HeaderAndMsg by StreamListener. content: " + msg);}有没有发现这段代码跟 Spring MVC Controller 中接收请求的代码很像? 实际上他们的架构都是类似的,Spring MVC 对于 Controller 中参数和返回值的处理类分别是 org.springframework.web.method.support.HandlerMethodArgumentResolver、 org.springframework.web.method.support.HandlerMethodReturnValueHandler。Spring Messaging 中对于参数和返回值的处理类之前也提到过,分别是 org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver、org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler。它们的类名一模一样,甚至内部的方法名也一样。总结上图是 SCS 体系相关类说明的总结,关于 SCS 以及 RocketMQ Binder 更多相关的示例,可以参考 RocketMQ Binder Demos,包含了消息的聚合、分割、过滤;消息异常处理;消息标签、sql过滤;同步、异步消费等等。下一篇文章,我们将分析消息总线(Spring Cloud Bus) 在 Spring Cloud 体系中的作用,并逐步展开,分析 Spring Cloud Alibaba 中的 RocketMQ Binder 是如何实现 Spring Cloud Stream 标准的。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 9, 2019 · 4 min · jiezi

javascript 面向对象(实现继承的几种方式)

1、原型链继承核心: 将父类的实例作为子类的原型缺点: 父类新增原型方法/原型属性,子类都能访问到,父类一变其它的都变了 function Person (name) { this.name = name; }; Person.prototype.getName = function () { //对原型进行扩展 return this.name; }; function Parent (age) { this.age = age; }; Parent.prototype = new Person(‘老明’); //这一句是关键 //通过构造器函数创建出一个新对象,把老对象的东西都拿过来。 Parent.prototype.getAge = function () { return this.age; };// Parent.prototype.getName = function () { //可以重写从父类继承来的方法,会优先调用自己的。// console.log(222);// }; var result = new Parent(22); console.log(result.getName()); //老明 //调用了从Person原型中继承来的方法(继承到了当前对象的原型中) console.log(result.getAge()); //22 //调用了从Parent原型中扩展来的方法2、构造继承基本思想借用构造函数的基本思想就是利用call或者apply把父类中通过this指定的属性和方法复制(借用)到子类创建的实例中。因为this对象是在运行时基于函数的执行环境绑定的。也就是说,在全局中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。call、apply 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。所以,这个借用构造函数就是,new对象的时候(new创建的时候,this指向创建的这个实例),创建了一个新的实例对象,并且执行Parent里面的代码,而Parent里面用call调用了Person,也就是说把this指向改成了指向新的实例,所以就会把Person里面的this相关属性和方法赋值到新的实例上,而不是赋值到Person上面,所以所有实例中就拥有了父类定义的这些this的属性和方法。因为属性是绑定到this上面的,所以调用的时候才赋到相应的实例中,各个实例的值就不会互相影响了。核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)缺点: 方法都在构造函数中定义, 只能继承父类的实例属性和方法,不能继承原型属性/方法,无法实现函数复用,每个子类都有父类实例函数的副本,影响性能 function Person (name) { this.name = name; this.friends = [‘小李’,‘小红’]; this.getName = function () { return this.name; } };// Person.prototype.geSex = function () { //对原型进行扩展的方法就无法复用了// console.log(“男”);// }; function Parent = (age) { Person.call(this,‘老明’); //这一句是核心关键 //这样就会在新parent对象上执行Person构造函数中定义的所有对象初始化代码, // 结果parent的每个实例都会具有自己的friends属性的副本 this.age = age; }; var result = new Parent(23); console.log(result.name); //老明 console.log(result.friends); //[“小李”, “小红”] console.log(result.getName()); //老明 console.log(result.age); //23 console.log(result.getSex()); //这个会报错,调用不到父原型上面扩展的方法3、组合继承组合继承(所有的实例都能拥有自己的属性,并且可以使用相同的方法,组合继承避免了原型链和借用构造函数的缺陷,结合了两个的优点,是最常用的继承方式)核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后再通过将父类实例作为子类原型,实现函数复用缺点:调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了) function Person (name) { this.name = name; this.friends = [‘小李’,‘小红’]; }; Person.prototype.getName = function () { return this.name; }; function Parent (age) { Person.call(this,‘老明’); //这一步很关键 this.age = age; }; Parent.prototype = new Person(‘老明’); //这一步也很关键 var result = new Parent(24); console.log(result.name); //老明 result.friends.push(“小智”); // console.log(result.friends); //[‘小李’,‘小红’,‘小智’] console.log(result.getName()); //老明 console.log(result.age); //24 var result1 = new Parent(25); //通过借用构造函数都有自己的属性,通过原型享用公共的方法 console.log(result1.name); //老明 console.log(result1.friends); //[‘小李’,‘小红’]4、寄生组合继承核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点缺点:堪称完美,但实现较为复杂 function Person(name) { this.name = name; this.friends = [‘小李’,‘小红’]; } Person.prototype.getName = function () { return this.name; }; function Parent(age) { Person.call(this,“老明”); this.age = age; } (function () { var Super = function () {}; // 创建一个没有实例方法的类 Super.prototype = Person.prototype; Parent.prototype = new Super(); //将实例作为子类的原型 })(); var result = new Parent(23); console.log(result.name); console.log(result.friends); console.log(result.getName()); console.log(result.age); ...

April 2, 2019 · 2 min · jiezi

JS中的逻辑运算符&&、||,位运算符|,&

1、JS中的||符号:运算方法: 只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值。 只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值。总结:真前假后2、JS中的&&符号:运算方法: 只要“&&”前面是false,无论“&&”后面是true还是false,结果都将返“&&”前面的值; 只要“&&”前面是true,无论“&&”后面是true还是false,结果都将返“&&”后面的值;总结:假前真后弄懂了以上说的还应该知道: js的6个蛋蛋:在js逻辑运算中,0、”“、null、false、undefined、NaN都会判为false,其他都为true。举个栗子:3、位运算符:|运算方法: 两个位只要有一个为1,那么结果都为1。否则就为0继续举栗子|运算符还能进行取整运算4、位运算符:&运算方法: 两个数值的个位分别相与,同时为1才得1,只要一个为0就为0。还是举栗子:

March 27, 2019 · 1 min · jiezi

vue组件之间8种组件通信方式总结

对于vue来说,组件之间的消息传递是非常重要的,下面是我对组件之间消息传递的各种方式的总结,总共有8种方式。1.props和$emit父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的。Vue.component(‘child’,{ data(){ return { mymessage:this.message } }, template: &lt;div&gt; &lt;input type="text" v-model="mymessage" @input="passData(mymessage)"&gt; &lt;/div&gt; , props:[‘message’],//得到父组件传递过来的数据 methods:{ passData(val){ //触发父组件中的事件 this.$emit(‘getChildData’,val) } } }) Vue.component(‘parent’,{ template: &lt;div&gt; &lt;p&gt;this is parent compoent!&lt;/p&gt; &lt;child :message="message" v-on:getChildData="getChildData"&gt;&lt;/child&gt; &lt;/div&gt; , data(){ return { message:‘hello’ } }, methods:{ //执行子组件触发的事件 getChildData(val){ console.log(val) } } }) var app=new Vue({ el:’#app’, template: &lt;div&gt; &lt;parent&gt;&lt;/parent&gt; &lt;/div&gt; })在上面的例子中,有父组件parent和子组件child。 1).父组件传递了message数据给子组件,并且通过v-on绑定了一个getChildData事件来监听子组件的触发事件; 2).子组件通过props得到相关的message数据,最后通过this.$emit触发了getChildData事件。2.$attrs和$listeners第一种方式处理父子组件之间的数据传输有一个问题:如果父组件A下面有子组件B,组件B下面有组件C,这时如果组件A想传递数据给组件C怎么办呢? 如果采用第一种方法,我们必须让组件A通过prop传递消息给组件B,组件B在通过prop传递消息给组件C;要是组件A和组件C之间有更多的组件,那采用这种方式就很复杂了。Vue 2.4开始提供了$attrs和$listeners来解决这个问题,能够让组件A之间传递消息给组件C。Vue.component(‘C’,{ template: &lt;div&gt; &lt;input type="text" v-model="$attrs.messagec" @input="passCData($attrs.messagec)"&gt; &lt;/div&gt; , methods:{ passCData(val){ //触发父组件A中的事件 this.$emit(‘getCData’,val) } } }) Vue.component(‘B’,{ data(){ return { mymessage:this.message } }, template: &lt;div&gt; &lt;input type="text" v-model="mymessage" @input="passData(mymessage)"&gt; &lt;!-- C组件中能直接触发getCData的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性 --&gt; &lt;!-- 通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的) --&gt; &lt;C v-bind="$attrs" v-on="$listeners"&gt;&lt;/C&gt; &lt;/div&gt; , props:[‘message’],//得到父组件传递过来的数据 methods:{ passData(val){ //触发父组件中的事件 this.$emit(‘getChildData’,val) } } }) Vue.component(‘A’,{ template: &lt;div&gt; &lt;p&gt;this is parent compoent!&lt;/p&gt; &lt;B :messagec="messagec" :message="message" v-on:getCData="getCData" v-on:getChildData="getChildData(message)"&gt;&lt;/B&gt; &lt;/div&gt; , data(){ return { message:‘hello’, messagec:‘hello c’ //传递给c组件的数据 } }, methods:{ getChildData(val){ console.log(‘这是来自B组件的数据’) }, //执行C子组件触发的事件 getCData(val){ console.log(“这是来自C组件的数据:"+val) } } }) var app=new Vue({ el:’#app’, template: &lt;div&gt; &lt;A&gt;&lt;/A&gt; &lt;/div&gt; })3.中央事件总线上面两种方式处理的都是父子组件之间的数据传递,而如果两个组件不是父子关系呢?这种情况下可以使用中央事件总线的方式。新建一个Vue事件bus对象,然后通过bus.$emit触发事件,bus.$on监听触发的事件。Vue.component(‘brother1’,{ data(){ return { mymessage:‘hello brother1’ } }, template: &lt;div&gt; &lt;p&gt;this is brother1 compoent!&lt;/p&gt; &lt;input type="text" v-model="mymessage" @input="passData(mymessage)"&gt; &lt;/div&gt; , methods:{ passData(val){ //触发全局事件globalEvent bus.$emit(‘globalEvent’,val) } } }) Vue.component(‘brother2’,{ template: &lt;div&gt; &lt;p&gt;this is brother2 compoent!&lt;/p&gt; &lt;p&gt;brother1传递过来的数据:{{brothermessage}}&lt;/p&gt; &lt;/div&gt; , data(){ return { mymessage:‘hello brother2’, brothermessage:’’ } }, mounted(){ //绑定全局事件globalEvent bus.$on(‘globalEvent’,(val)=>{ this.brothermessage=val; }) } }) //中央事件总线 var bus=new Vue(); var app=new Vue({ el:’#app’, template: &lt;div&gt; &lt;brother1&gt;&lt;/brother1&gt; &lt;brother2&gt;&lt;/brother2&gt; &lt;/div&gt; })4.provide和inject父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。Vue.component(‘child’,{ inject:[‘for’],//得到父组件传递过来的数据 data(){ return { mymessage:this.for } }, template: &lt;div&gt; &lt;input type="tet" v-model="mymessage"&gt; &lt;/div&gt; }) Vue.component('parent',{ template: <div> <p>this is parent compoent!</p> <child></child> </div> , provide:{ for:'test' }, data(){ return { message:'hello' } } }) var app=new Vue({ el:'#app', template: <div> <parent></parent> </div> })5.v-model父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,在子组件中通过this.$emit(‘input’,val)自动修改v-model绑定的值Vue.component('child',{ props:{ value:String, //v-model会自动传递一个字段为value的prop属性 }, data(){ return { mymessage:this.value } }, methods:{ changeValue(){ this.$emit('input',this.mymessage);//通过如此调用可以改变父组件上v-model绑定的值 } }, template: <div> <input type=“text” v-model=“mymessage” @change=“changeValue”> </div> }) Vue.component(‘parent’,{ template: &lt;div&gt; &lt;p&gt;this is parent compoent!&lt;/p&gt; &lt;p&gt;{{message}}&lt;/p&gt; &lt;child v-model="message"&gt;&lt;/child&gt; &lt;/div&gt; , data(){ return { message:‘hello’ } } }) var app=new Vue({ el:’#app’, template: &lt;div&gt; &lt;parent&gt;&lt;/parent&gt; &lt;/div&gt; })6.$parent和$childrenVue.component(‘child’,{ props:{ value:String, //v-model会自动传递一个字段为value的prop属性 }, data(){ return { mymessage:this.value } }, methods:{ changeValue(){ this.$parent.message = this.mymessage;//通过如此调用可以改变父组件的值 } }, template: &lt;div&gt; &lt;input type="text" v-model="mymessage" @change="changeValue"&gt; &lt;/div&gt; }) Vue.component('parent',{ template: <div> <p>this is parent compoent!</p> <button @click=“changeChildValue”>test</button > <child></child> </div> , methods:{ changeChildValue(){ this.$children[0].mymessage = 'hello'; } }, data(){ return { message:'hello' } } }) var app=new Vue({ el:'#app', template: <div> <parent></parent> </div> ` })7.boradcast和dispatchvue1.0中提供了这种方式,但vue2.0中没有,但很多开源软件都自己封装了这种方式,比如min ui、element ui和iview等。 比如如下代码,一般都作为一个mixins去使用, broadcast是向特定的父组件,触发事件,dispatch是向特定的子组件触发事件,本质上这种方式还是on和on和emit的封装,但在一些基础组件中却很实用。function broadcast(componentName, eventName, params) { this.$children.forEach(child => { var name = child.$options.componentName; if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)); } else { broadcast.apply(child, [componentName, eventName].concat(params)); } });}export default { methods: { dispatch(componentName, eventName, params) { var parent = this.$parent; var name = parent.$options.componentName; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.componentName; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } }};8.vuex处理组件之间的数据交互如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候才有上面这一些方法可能不利于项目的维护,vuex的做法就是将这一些公共的数据抽离出来,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。 详情可参考:https://vuex.vuejs.org/zh-cn/ ...

February 28, 2019 · 3 min · jiezi

Vue生命周期的理解

当面试官问:“谈谈你对vue的生命周期的理解”,听到这句话你是不是心里暗自窃喜:这也太容易了吧,不就是beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed 这几个钩子函数么,创建=>挂载=>更新=>销毁,So easy !!!非也非也。如果你只是简单罗列出这几个钩子函数的名称,不具体深入阐述的话,你这样的回答很难令面试官满意。如何才能以点带面深入阐述自己对vue的生命周期理解,从而让面试官对你留下好印象呢?别急,闰土大叔来告诉你,下次再碰到这个问题,你可以直接甩给面试官下面这张Image就OK了~当然,甩张Image给面试官这句话肯定是开玩笑的(适度幽默,缓解紧张气氛)。不过这张流程图还是有用的,因为它是我从Vue官网上拷贝下来的,只要你能理解了这张图,也就对Vue的生命周期有了一个大致的了解。那么接下来,闰土大叔将手摸手教你如何深入浅出地说出令面试官满意的、有亮点的回答。在谈到Vue的生命周期的时候,我们首先需要创建一个实例,也就是在 new Vue ( ) 的对象过程当中,首先执行了init(init是vue组件里面默认去执行的),在init的过程当中首先调用了beforeCreate,然后在injections(注射)和reactivity(反应性)的时候,它会再去调用created。所以在init的时候,事件已经调用了,我们在beforeCreate的时候千万不要去修改data里面赋值的数据,最早也要放在created里面去做(添加一些行为)。当created完成之后,它会去判断instance(实例)里面是否含有“el”option(选项),如果没有的话,它会调用vm.$mount(el)这个方法,然后执行下一步;如果有的话,直接执行下一步。紧接着会判断是否含有“template”这个选项,如果有的话,它会把template解析成一个render function ,这是一个template编译的过程,结果是解析成了render函数:render (h) { return h(‘div’, {}, this.text)}解释一下,render函数里面的传参h就是Vue里面的createElement方法,return返回一个createElement方法,其中要传3个参数,第一个参数就是创建的div标签;第二个参数传了一个对象,对象里面可以是我们组件上面的props,或者是事件之类的东西;第三个参数就是div标签里面的内容,这里我们指向了data里面的text。使用render函数的结果和我们之前使用template解析出来的结果是一样的。render函数是发生在beforeMount和mounted之间的,这也从侧面说明了,在beforeMount的时候,$el还只是我们在HTML里面写的节点,然后到mounted的时候,它就把渲染出来的内容挂载到了DOM节点上。这中间的过程其实是执行了render function的内容。在使用.vue文件开发的过程当中,我们在里面写了template模板,在经过了vue-loader的处理之后,就变成了render function,最终放到了vue-loader解析过的文件里面。这样做有什么好处呢?原因是由于在解析template变成render function的过程,是一个非常耗时的过程,vue-loader帮我们处理了这些内容之后,当我们在页面上执行vue代码的时候,效率会变得更高。beforeMount在有了render function的时候才会执行,当执行完render function之后,就会调用mounted这个钩子,在mounted挂载完毕之后,这个实例就算是走完流程了。后续的钩子函数执行的过程都是需要外部的触发才会执行。比如说有数据的变化,会调用beforeUpdate,然后经过Virtual DOM,最后updated更新完毕。当组件被销毁的时候,它会调用beforeDestory,以及destoryed。这就是vue实例从新建到销毁的一个完整流程,以及在这个过程中它会触发哪些生命周期的钩子函数。那说到这儿,可能很多童鞋会问,钩子函数是什么意思?钩子函数,其实和回调是一个概念,当系统执行到某处时,检查是否有hook,有则回调。说的更直白一点,每个组件都有属性,方法和事件。所有的生命周期都归于事件,在某个时刻自动执行。其实,当你跟面试官阐述到这儿的时候,面试官基本上已经满意你的回答了,隐约看到了你的技术功底。当然,如果你还想更进一步,让面试官对你刮目相看,达到加分的效果,你还可以这样说:在这个过程当中,Vue为我们提供了renderError方法,这个方法只有在开发的时候它才会被调用,在正式打包上线的过程当中,它是不会被调用的。它主要是帮助我们调试render里面的一些错误。renderError (h, err) { return h(‘div’, {}, err.stack)}有且只有当render方法里面报错了,才会执行renderError方法。所以我们主动让render函数报个错:render (h) { throw new TypeError(‘render error’)}如图所示,渲染出来的就是Error信息了。还有一点,renderError只有在本组件的render方法报错的情况下它才会被调用。

February 28, 2019 · 1 min · jiezi

轻松理解JS基本包装对象

今天来讨论一下JS中的基本包装对象(也叫基本包装类型),之前刚学到这里的时候,自己也是一头雾水,不明白这个基本包装对象到底是个什么鬼,后来找了很多资料,终于看清了它的真面目。首先呢,我们现在复习一下JS的数据类型,JS数据类型被分为了两大门派,基本类型和引用类型。 基本类型:Undefined,Null,Boolean,Number,String 引用类型:Object,Array,Date,RegExp等,说白了就是对象。。。我们都知道,引用类型有方法和属性,但是基本类型是木有的,但是你一定见过这样的代码var str = ‘hello’; //string 基本类型var s2 = str.charAt(0);alert(s2); // h毫无疑问上面的string是一个基本类型,但是它却能召唤出一个charAt()的方法,这是什么原因呢?主要是因为在基本类型中,有三个比较特殊的存在就是:String Number Boolean,这三个基本类型都有自己对应的包装对象。并且随时等候召唤。包装对象呢,其实就是对象,有相应的属性和方法。至于这个过程是怎么发生呢,其实是在后台偷偷发生的。来看个栗子//我们平常写程序的过程:var str = ‘hello’; //string 基本类型var s2 = str.charAt(0); //在执行到这一句的时候 后台会自动完成以下动作 ://相当于:( var str = new String(‘hello’); // 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象 var s2 = str.chaAt(0); // 2 然后这个对象就可以调用包装对象下的方法,并且返回结给s2. str = null; // 3 之后这个临时创建的对象就被销毁了, str =null; ) `alert(s2);//h alert(str);//hello 注意这是一瞬间的动作 实际上我们没有改变字符串本身的值。就是做了下面的动作.这也是为什么每个字符串具有的方法并没有改变字符串本身的原因。由此我们可以知道,引用类型和基本包装对象的区别在于:生存期引用类型所创建的对象,在执行的期间一直在内存中,而基本包装对象只是存在了一瞬间。所以我们无法直接给基本类型添加方法:举个栗子var str = ‘hello’;str.number = 10; //假设我们想给字符串添加一个属性number ,后台会有如下步骤// 相当于{ var str = new String(‘hello’); // 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象 str.number = 10; // 2 通过这个对象调用包装对象下的方法 但结果并没有被任何东西保存 str =null; // 3 这个对象又被销毁 }alert(str.number); //undefined 当执行到这一句的时候,因为基本类型本来没有属性,后台又会重新重复上面的步骤//相当于{ var str = new String(‘hello’); // 1 找到基本包装对象,然后又新开辟一个内存,创建一个值为hello对象 str.number = undefined // 2 因为包装对象下面没有number这个属性,所以又会重新添加,因为没有值,所以值是未定 ;然后弹出结果 str =null; // 3 这个对象又被销毁 }那么我们怎么才能给基本类型添加方法或者属性呢?答案是在基本包装对象的原型下面添加,每个对象都有原型。来看个栗子//给字符串添加方法 要写到对应的包装对象的原型下才行var str = ‘hello’;String.prototype.last= fuction(){ return this.charAt(this.length);}; str.last(); // 5 执行到这一句,后台依然会偷偷的干这些事//相当于{ var str = new String(‘hello’);// 找到基本包装对象,new一个和字符串值相同的对象, str.last(); // 通过这个对象找到了包装对象下的方法并调用 str =null; // 这个对象被销毁}看注释相信能看出创建在基本包装对象原型下面的方法和属性才能被保存。 ...

February 25, 2019 · 1 min · jiezi

JS中数据类型、内置对象、包装类型对象、typeof关系

平时在复习JS基础知识时,经常会遇到JS数据类型、基础数据类型、内置对象、包装类型对象,检测数据类型时,用到的typeof值,感觉都差不多,但是又有差异。今天特地整理下,方便理解。JS数据类型基础数据类型和引用数据类型JS数据类型分为 基础数据类型 和 引用数据类型基础数据类型又分为undefined null boolean number string引用数据类型即object为什么编程语言要有数据类型 概念一句话概括的话,就是JS数据类型,是对外的,是面向JS编译器的,定义编译器对相应类型的处理方式;具体参考 为什么编程语言的都要定义数据类型JS内置对象JS内置对象 包含 Boolean String Number Array Function Date Math Object RegExp Error Global包装类型对象JS内置对象包含包装类型对象,包装类型对象 指的是 Boolean String Number三个内置对象为什么会有JS内置对象 概念一句话概括的话,就是JS内置对象,是对内的,是指这个语言自带的一些对象,供开发者使用,这些对象提供了一些常用的或是最基本而必要的功能。typeof值typeof是用来判断数据类型的,其中它的值有undefined boolean number string function symbol object;JS内置对象和JS数据类型关系JS数据类型是对外的,面对编译器;JS内置对象是对内的,日常编程与我们打交道最多的,其实是JS内置对象。至于包装数据类型对象的特点,可以参考 轻松理解JS基本包装对象,打个比方就是:JS就像一个国家,平时对外交流(与JS编译器交流),由6位副总统(JS基本数据类型,undefined null boolean number string object)去操作。其中,undefined副总统、null副总统年老体衰,平时掌管的国家事务不多。object副总统能力比较强,下设了11位部长(JS内置对象),平时国内治理(前端日常开发)由这些部长执行。另外,boolean副总统、number副总统、string副总统,平时施政时,必须由Boolean部长、Number部长、String部长三位部长(包装类型对象)辅助,命令才能正常实行。当然,Boolean部长、Number部长、String部长三位部长也可以单独执行,绕过三位副总统。typeof值与JS数据类型关系对比下图,即可知typeof值 相较于 JS基础数据类型 少null多function

February 25, 2019 · 1 min · jiezi

为什么编程语言的都要定义数据类型

对于一个程序员来讲,写代码的第一件是请,恐怕就是需要定义一些数据类型。而程序本身,就是对这些数据类型进行操作,有没有人思考过,为什么每种语言编写的程序,开始都需要定义数据类型呢?以下面的C代码为例,我们来说明这个问题:#include<stdio.h>int main(){ int a=100; int b=200; double a1=10.1; double b1=10.2; a=a+b; a1=a1+b1; getchar(); return 0;}这段代码非常简单,定义了四个数据,两个类型。大家看,a=a+b;和a1=a1+b1;这两个语句,几乎一样,那么,我问你,这两个语句,在编译时,编译器会用同一段代码来替换这两个语句吗?显然不可是同一段代码,因为浮点数和整数,在计算机里面,使用了不同的处理器,整数使用普通的CPU,而浮点数必须使用浮点运算器。所以,这两句话,产生的机器代码完全不同!那么问题来了,编译器如何知道,在碰到两个数相加的时候,是使用浮点运算器的机器指令,还是使用普通CPU的机器指令?此时,编译器就会检查进行加法操作的两个加数的数据类型,根据他们的数据类型,来确定到底使用哪一个运算器的机器代码。此时,数据类型定义的意义就凸显出来了。总结:通过上述简单的描述,我们就能够清楚,任何编程语言(除了汇编,汇编只规定数据的字长),都会有自己的数据类型,数据类型背后,隐藏的是编译器或者解释器对数据处理方式的定义。知道了这个以后,我们在定义数据类型的时候,就应该知道,我们定义的这种数据类型,可以进行哪些操作,这些操作的规则是什么,这样我们才算真正掌握了这个数据类型。更高级的语言,例如C++可以定义自己的数据类型和数据类型的算法,类的重载操作符就是一个例子。

February 25, 2019 · 1 min · jiezi

JS中的call、apply、bind方法详解

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。apply、call在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。function fruits() {} fruits.prototype = { color: “red”, say: function() { console.log(“My color is " + this.color); }} var apple = new fruits;apple.say(); //My color is red但是如果我们有一个对象banana= {color : “yellow”} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:banana = { color: “yellow”}apple.say.call(banana); //My color is yellowapple.say.apply(banana); //My color is yellow所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。apply、call 区别对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:var func = function(arg1, arg2) { };就可以通过如下方式来调用:func.call(this, arg1, arg2);func.apply(this, [arg1, arg2])其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。 为了巩固加深记忆,下面列举一些常用用法:apply、call实例数组之间追加var array1 = [12 , “foo” , {name:“Joe”} , -2458]; var array2 = [“Doe” , 555 , 100]; Array.prototype.push.apply(array1, array2); // array1 值为 [12 , “foo” , {name:“Joe”} , -2458 , “Doe” , 555 , 100] 获取数组中的最大值和最小值var numbers = [5, 458 , 120 , -215 ]; var maxInNumbers = Math.max.apply(Math, numbers), //458 maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。验证是否是数组(前提是toString()方法没有被重写过)functionisArray(obj){ return Object.prototype.toString.call(obj) === ‘[object Array]’ ;}类(伪)数组使用数组方法var domNodes = Array.prototype.slice.call(document.getElementsByTagName(”"));Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop 等方法。但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。面试题定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:function log(msg) { console.log(msg);}log(1); //1log(1,2); //1上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:function log(){ console.log.apply(console, arguments);};log(1); //1log(1,2); //1 2接下来的要求是给每一个 log 消息添加一个"(app)“的前辍,比如:log(“hello world”); //(app)hello world该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift,像这样:function log(){ var args = Array.prototype.slice.call(arguments); args.unshift(’(app)’); console.log.apply(console, args);};bind在讨论bind()方法之前我们先来看一道题目:var altwrite = document.write;altwrite(“hello”);结果:Uncaught TypeError: Illegal invocationaltwrite()函数改变this的指向global或window对象,导致执行时提示非法调用异常,正确的方案就是使用bind()方法:altwrite.bind(document)(“hello”)当然也可以使用call()方法:altwrite.call(document, “hello”)绑定函数bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值。常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望this指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用bind()方法能够很漂亮的解决这个问题:this.num = 9; var mymodule = { num: 81, getNum: function() { console.log(this.num); }};mymodule.getNum(); // 81var getNum = mymodule.getNum;getNum(); // 9, 因为在这个例子中,“this"指向全局对象var boundGetNum = getNum.bind(mymodule);boundGetNum(); // 81bind() 方法与 apply 和 call 很相似,也是可以改变函数体内 this 的指向。MDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。直接来看看具体如何使用,在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:var foo = { bar : 1, eventBind: function(){ var _this = this; $(’.someClass’).on(‘click’,function(event) { / Act on the event / console.log(_this.bar); //1 }); }}由于 Javascript 特有的机制,上下文环境在 eventBind:function(){ } 过渡到 $(’.someClass’).on(‘click’,function(event) { }) 发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind() 可以更加优雅的解决这个问题:var foo = { bar : 1, eventBind: function(){ $(’.someClass’).on(‘click’,function(event) { / Act on the event */ console.log(this.bar); //1 }.bind(this)); }}在上述代码里,bind() 创建了一个函数,当这个click事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的栗子:var bar = function(){console.log(this.x);}var foo = {x:3}bar(); // undefinedvar func = bar.bind(foo);func(); // 3这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。偏函数(Partial Functions)Partial Functions也叫Partial Applications,这里截取一段关于偏函数的定义:Partial application can be described as taking a function that accepts some number of arguments, binding values to one or more of those arguments, and returning a new function that only accepts the remaining, un-bound arguments.这是一个很好的特性,使用bind()我们设定函数的预定义参数,然后调用的时候传入其他参数即可:function list() { return Array.prototype.slice.call(arguments);}var list1 = list(1, 2, 3); // [1, 2, 3]// 预定义参数37var leadingThirtysevenList = list.bind(undefined, 37);var list2 = leadingThirtysevenList(); // [37]var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]和setTimeout一起使用function Bloomer() { this.petalCount = Math.ceil(Math.random() * 12) + 1;}// 1秒后调用declare函数Bloomer.prototype.bloom = function() { window.setTimeout(this.declare.bind(this), 100);};Bloomer.prototype.declare = function() { console.log(‘我有 ’ + this.petalCount + ’ 朵花瓣!’);};var bloo = new Bloomer();bloo.bloom(); //我有 5 朵花瓣!注意:对于事件处理函数和setInterval方法也可以使用上面的方法绑定函数作为构造函数绑定函数也适用于使用new操作符来构造目标函数的实例。当使用绑定函数来构造实例,注意:this会被忽略,但是传入的参数仍然可用。function Point(x, y) { this.x = x; this.y = y;}Point.prototype.toString = function() { console.log(this.x + ‘,’ + this.y);};var p = new Point(1, 2);p.toString(); // ‘1,2’var emptyObj = {};var YAxisPoint = Point.bind(emptyObj, 0/x/);// 实现中的例子不支持,// 原生bind支持:var YAxisPoint = Point.bind(null, 0/x/);var axisPoint = new YAxisPoint(5);axisPoint.toString(); // ‘0,5’axisPoint instanceof Point; // trueaxisPoint instanceof YAxisPoint; // truenew Point(17, 42) instanceof YAxisPoint; // true捷径bind()也可以为需要特定this值的函数创造捷径。例如要将一个类数组对象转换为真正的数组,可能的例子如下:var slice = Array.prototype.slice;// …slice.call(arguments);如果使用bind()的话,情况变得更简单:var unboundSlice = Array.prototype.slice;var slice = Function.prototype.call.bind(unboundSlice);// …slice(arguments);实现上面的几个小节可以看出bind()有很多的使用场景,但是bind()函数是在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。这就需要我们自己实现bind()函数了。首先我们可以通过给目标函数指定作用域来简单实现bind()方法:Function.prototype.bind = function(context){ self = this; //保存this,即调用bind方法的目标函数 return function(){ return self.apply(context,arguments); };};考虑到函数柯里化的情况,我们可以构建一个更加健壮的bind():Function.prototype.bind = function(context){ var args = Array.prototype.slice.call(arguments, 1), self = this; return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return self.apply(context,finalArgs); };};这次的bind()方法可以绑定对象,也支持在绑定的时候传参。继续,Javascript的函数还可以作为构造函数,那么绑定后的函数用这种方式调用时,情况就比较微妙了,需要涉及到原型链的传递:Function.prototype.bind = function(context){ var args = Array.prototype.slice(arguments, 1), F = function(){}, self = this, bound = function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return self.apply((this instanceof F ? this : context), finalArgs); }; F.prototype = self.prototype; bound.prototype = new F(); return bound;};这是《JavaScript Web Application》一书中对bind()的实现:通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用instanceof,因此这是最严谨的bind()实现。对于为了在浏览器中能支持bind()函数,只需要对上述函数稍微修改即可:Function.prototype.bind = function (oThis) { if (typeof this !== “function”) { throw new TypeError(“Function.prototype.bind - what is trying to be bound is not callable”); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply( this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)) ); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; };有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:var bar = function(){ console.log(this.x);}var foo = { x:3}var sed = { x:4}var func = bar.bind(foo).bind(sed);func(); //? var fiv = { x:5}var func = bar.bind(foo).bind(sed).bind(fiv);func(); //?答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。apply、call、bind比较那么 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:var obj = { x: 81,}; var foo = { getX: function() { return this.x; }} console.log(foo.getX.bind(obj)()); //81console.log(foo.getX.call(obj)); //81console.log(foo.getX.apply(obj)); //81三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。再总结一下:apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;apply 、 call 、bind 三者都可以利用后续参数传参;bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。 ...

February 25, 2019 · 4 min · jiezi

深入理解JavaScript中的属性和特性

深入理解JavaScript中的属性和特性 JavaScript中属性和特性是完全不同的两个概念,这里我将根据自己所学,来深入理解JavaScript中的属性和特性。 主要内容如下:理解JavaScript中对象的本质、对象与类的关系、对象与引用类型的关系对象属性如何进行分类属性中特性的理解第一部分:理解JavaScript中对象的本质、对象与类的关系、对象与引用类型的关系 对象的本质:ECMA-262把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。即对象是一组没有特定顺序的值,对象的每个属性或方法都有一个名字,而这个名字都映射到一个值。故对象的本质是一个散列表:其中是一组名值对,值可以是数据或函数。 对象和类的关系:在JavaScript中,对象和类没有任何关系。这是因为ECMAScript中根本就没有类的概念,它的对象与其他基于类的语言中的对象是不同的。 对象和引用类型的关系:对象和引用类型并不是等价的,因为每个对象都是基于一个引用类型创建的。第二部分:对象属性如何进行分类 由构造函数或对象字面量方法创建的对象中具有属性和方法(只要提到属性和方法,它们一定是属于对象的;只要提到对象,它一定是具有属性和方法的(自定义除外)),其中属性又可分为数据属性和访问器属性,他们的区别如下:数据属性一般用于存储数据数值,访问器属性不包含数据值访问器属性多用于get/set操作第三部分:属性中特性的理解 ECMAScript为了描述对象属性(property)的各种特征,定义了特性(attribute)这个概念。也就是说特性不同于属性,特性是为了描述属性的。下面,我将分别讲解:数据属性及其特性访问器属性及其特性如何利用Object.defineProperties()方法定义多个特性如何利用Object.getOwnPropertyDescripter()方法读取属性的描述符以读取属性的特性1.数据属性及其特性 刚刚我们说过,数据属性是用于存储数据数值的,因此数据属性具有一个数据值的位置,在这个位置可以读取和写入值。数据属性有4个描述其行为的特性,由于ECMAScript规定:在JavaScript中不能直接访问属性的特性(注意:不是不能访问),所以我们把它放在两组方括号中。如下:[[Configurable]]:默认值为true,a、表示能否通过delete删除属性从而重新定义属性 b、能否修改属性的特性 c、能够把属性由数据属性修改为访问器属性[[Enumerable]]:默认值为true,表示能否通过for-in循环返回该属性(所以:如果为false,那么for-in循环没法枚举它所在的属性)[[Writable]]:默认值为true,表示能否修改属性的值,这是与[[Configurable]]不同之处。[[Value]]:默认值为undefined,这个值即为属性的属性值,我们可以在这个位置上读取属性值,也可以在这个位置上写入属性值。注意:上述的默认是指通过构造函数或对象字面量创建的对象所自身拥有的属性,而不是下面要介绍的Object.defineProperty()方法这些特性都具有默认值,但是如果这些默认值不是我们想要的,该怎么办呢?当然就是修改啦!我们可以通过Object.defineProperty()方法来修改属性默认的特性。英文difineProperty即为定义属性的意思。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中第三个参数描述符对象是对象字面量的方法创建的,里面的属性和属性值实际上保存的是要修改的特性和特性值。下面通过几个例子来深入理解。avar person={};Object.defineProperty(person,“name”,{ writable:false, value:“zhuzhenwei”});console.log(person.name);//zhuzhenweiperson.name=“heting”;console.log(person.name);//zhuzhenwei 这里我用对象字面量的方法创建了一个对象,但是没有同时创建方法和属性。而是利用了Object.defineProperty()方法来创建了属性和修改了默认值。这里将writable设置为false,于是后面我试图修改person.name时,是无效的。bvar person={};Object.defineProperty(person,“name”,{ value:“zhuzhenwei”});console.log(person.name);//zhuzhenweiperson.name=“heting”;console.log(person.name);//zhuzhenwei 注意看这个例子,这个例子中我删去了writable:false,为什么还是不能修改呢?这是因为之前我在介绍特性时,前三个默认为ture,是在创建对象并创建属性的情况下得到的。对于通过调用Object.defineProperty()方法创建的属性,其前三个特性的默认值均为false,这里需要注意。cvar person={};Object.defineProperty(person,“name”,{ value:“zhuzhenwei”, configurable:false});console.log(person.name);//zhuzhenweidelete person.name;console.log(person.name);//zhuzhenwei 这里我们将新建的属性name的特性设置为了configurable:false;因此下面删除属性的操作是无效的。根据b,可知configurable,默认就是false,即使去掉也不可修改。dvar person={};Object.defineProperty(person,“name”,{ value:“zhuzhenwei”, configurable:true});console.log(person.name);//zhuzhenweidelete person.name;console.log(person.name);//undefined 在这里我将默认的configurable的值由默认的false修改为了true,于是变成了可配置的,那么最后就成功删除了。evar person={};Object.defineProperty(person,“name”,{ value:“zhuzhenwei”, configurable:false});console.log(person.name);//zhuzhenweiObject.defineProperty(person,“name”,{ value:“zhuzhenwei”, configurable:true});console.log(person.name);//Uncaught TypeError: Cannot redefine property: name(…) 如果之前已经设置成为了false,那么后面再改成true也是徒劳的,即:一旦把属性设置成为不可配置的,就不能再把它变回可配置了。fconsole.log(person.name);//Uncaught TypeError: Cannot redefine property: name(…)var person={};Object.defineProperty(person,“name”,{ value:“zhuzhenwei”,});console.log(person.name);//zhuzhenweiObject.defineProperty(person,“name”,{ value:“zhuzhenwei”, configurable:true});console.log(person.name);//Uncaught TypeError: Cannot redefine property: name(…) 这里可以说明,即使前一步我们不管默认的configurable:false,后面得到的仍是不可配置。于是,可以得出结论,为了可配置,必须在第一次调用Object.defineProperty()函数时就将默认的值修改为true。 2.访问器属性及其特性 之前提到,访问器属性不包含数据值,他们包含一对getter函数和setter函数(这两个函数不是必须的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性是,会调用setter函数并传入新值,这个函数负责决定如何处理数据。同样,由于不能通过JavaScript来直接访问得到访问器属性的特性,所以下面列出的特性将由[[]]括起来以作区分。[[Configurable]]:默认值为true,a、表示能否通过delete删除属性从而重新定义属性 b、能否修改属性的特性c、能够把属性由访问器属性修改为数据属性[[Enumerable]]:默认值为true,表示能否通过for-in循环返回该属性(所以:如果为false,那么for-in循环没法枚举它所在的属性)[[Get]]:在读取属性时调用的函数。默认值为undefined 关键:特性可以是一个函数[[Set]]: 在写入属性时调用的函数。默认值为undefined 关键:特性可以是一个函数 由于get和set函数也属于属性的特性,那么他们就有可能(说有可能是因为这两个函数也不是必须的)出现在Object.defineproperty的第三个参数描述符对象的属性中。注意:1.相对于数据属性,我们发现访问器属性中没有writable特性和value特性。这是因为访问器属性不包含数据值,那么我们怎么当然就不可修改属性的值(用不到writable特性),更不用考虑value了。 2.访问器属性不能直接定义,必须是用Object.defineProperty()来定义。(通过这个规定我们就能准确地判断出访问器属性和数据属性了)通过下面这个例子来深入理解:var book={ _year:2004, edition:1};Object.defineProperty(book,“year”,{ get:function(){<br> return this._year; }, set:function(newValue){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } }});book.year=2005;console.log(book.edition);//2几个需要深入理解的地方:1.访问器属性不能直接定义,必须使用Object.defineProperty()来定义,且该属性具有set和get特性,于是可以判断,year和edition是数据属性,而year是访问器属性。2.我们看到_year这个数据属性前面是以(下划线)开头的,这个一种常用的记号,用于表示只能通过对象方法访问的属性。从上面的例子中可以看到get相当于描述符对象的一个方法,而_year正是在这个对象方法访问的属性。而edition既可以通过对象方法访问,也可以由对象直接访问。book.year表示正在读取访问器属性,这时会调用get函数,并返回了2004这个有效的值。book.year=2005表示写入访问器属性,这时会调用set函数并传入新值,即将2005传给newValue,这个函数决定如何处理数据。这时使用访问器属性的常见方法-即设置一个属性的值会导致其他属性发生变化。3.如何利用Object.defineProperties()方法定义多个特性 显然,一个对象不可能只具有一个属性,因此,定义多个属性的可能性很大,于是JavaScript提供了Object.defineProperties()方法解决这个问题。这个方法接收两个参数,第一个是要定义属性所在的对象,第二个是一个对象字面量方法创建的对象,对象的属性名即为要定义的特姓名,对象的属性值又是一个对象,这个对象里的属性名和属性值分别是特性名和特性值(这里不是很好理解,看例子即可)。var book={};Object.defineProperties(book,{ _year:{ writable:true, value:2004 }, edition:{ writable:true, value:1 }, year:{ get:function(){ return this._year; }, set:function(){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } } }});4.如何利用Object.getOwnPropertyDescripter()方法读取属性的描述符以读取属性的特性 我们可以使用Object.getOwnPropertyDescripter()方法来取得给定属性的描述符。getOwnPropertyDescripter即为取得自身属性描述符的意思。这个方法接收两个参数:属性所在的对象要要读取其描述符的属性名称。返回一个对象。 对于访问器属性而言,这个对象的属性有configurable、enumerable、get和set; 对于数据属性而言,这个对象的属性有configurable、enumerable、writable和value。var book={};Object.defineProperties(book,{ _year:{ value:2004 }, edition:{ value:1 }, year:{ get:function(){ return this._year; }, set:function(){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } } }});var descriptor=Object.getOwnPropertyDescriptor(book,"_year");console.log(descriptor.value);//2004console.log(descriptor.configurable);//false 因为通过Object.defineProperties()方法创建的属性的特性configurable enumerable都是falseconsole.log(typeof descriptor.get);//undefined 注意:这是数据属性,是不具有get特性的 var descriptor=Object.getOwnPropertyDescriptor(book,“year”);console.log(descriptor.value);//undefinedconsole.log(descriptor.enumerable);//falseconsole.log(typeof descriptor.get);//function get虽然是属性的一个特性,但是它也是函数。 ...

February 25, 2019 · 1 min · jiezi

构造函数内的方法与构造函数prototype属性上方法的对比

挺有用的一篇文章,今天还有人在问我关于构造函数的方法和原型,构造函数的方法是定义在函数内容,作为一个私有方法,不对外开放,而prototype则可以通过对象定义,在外面访问,更加深入请看本文。本文的目的是让大家理解什么情况下把函数的方法写在JavaScript的构造函数上,什么时候把方法写在函数的prototype属性上;以及这样做的好处.为了阅读方便,我们约定一下:把方法写在构造函数内的情况我们简称为函数内方法,把方法写在prototype属性上的情况我们简称为prototype上的方法首先我们先了解一下这篇文章的重点:函数内的方法: 使用函数内的方法我们可以访问到函数内部的私有变量,如果我们通过构造函数new出来的对象需要我们操作构造函数内部的私有变量的话, 我们这个时候就要考虑使用函数内的方法.prototype上的方法: 当我们需要通过一个函数创建大量的对象,并且这些对象还都有许多的方法的时候;这时我们就要考虑在函数的prototype上添加这些方法. 这种情况下我们代码的内存占用就比较小.在实际的应用中,这两种方法往往是结合使用的;所以我们要首先了解我们需要的是什么,然后再去选择如何使用.我们还是根据下面的代码来说明一下这些要点吧,下面是代码部分:// 构造函数Afunction A(name) { this.name = name || ‘a’; this.sayHello = function() { console.log(‘Hello, my name is: ’ + this.name); }}// 构造函数Bfunction B(name) { this.name = name || ‘b’;}B.prototype.sayHello = function() { console.log(‘Hello, my name is: ’ + this.name);};var a1 = new A(‘a1’);var a2 = new A(‘a2’);a1.sayHello();a2.sayHello();var b1 = new B(‘b1’);var b2 = new B(‘b2’);b1.sayHello();b2.sayHello();我们首先写了两个构造函数,第一个是A,这个构造函数里面包含了一个方法sayHello;第二个是构造函数B, 我们把那个方法sayHello写在了构造函数B的prototype属性上面.需要指出的是,通过这两个构造函数new出来的对象具有一样的属性和方法,但是它们的区别我们可以通过下面的一个图来说明:我们通过使用构造函数A创建了两个对象,分别是a1,a2;通过构造函数B创建了两个对象b1,b2;我们可以发现b1,b2这两个对象的那个sayHello方法 都是指向了它们的构造函数的prototype属性的sayHello方法.而a1,a2都是在自己内部定义了这个方法. 定义在构造函数内部的方法,会在它的每一个实例上都克隆这个方法;定义在构造函数的prototype属性上的方法会让它的所有示例都共享这个方法,但是不会在每个实例的内部重新定义这个方法. 如果我们的应用需要创建很多新的对象,并且这些对象还有许多的方法,为了节省内存,我们建议把这些方法都定义在构造函数的prototype属性上当然,在某些情况下,我们需要将某些方法定义在构造函数中,这种情况一般是因为我们需要访问构造函数内部的私有变量.下面我们举一个两者结合的例子,代码如下:function Person(name, family) { this.name = name; this.family = family; var records = [{type: “in”, amount: 0}]; this.addTransaction = function(trans) { if(trans.hasOwnProperty(“type”) && trans.hasOwnProperty(“amount”)) { records.push(trans); } } this.balance = function() { var total = 0; records.forEach(function(record) { if(record.type === “in”) { total += record.amount; } else { total -= record.amount; } }); return total; };};Person.prototype.getFull = function() { return this.name + " " + this.family;};Person.prototype.getProfile = function() { return this.getFull() + “, total balance: " + this.balance();};在上面的代码中,我们定义了一个Person构造函数;这个函数有一个内部的私有变量records,这个变量我们是不希望通过函数内部以外的方法 去操作这个变量,所以我们把操作这个变量的方法都写在了函数的内部.而把一些可以公开的方法写在了Person的prototype属性上,比如方法getFull和getProfile.把方法写在构造函数的内部,增加了通过构造函数初始化一个对象的成本,把方法写在prototype属性上就有效的减少了这种成本. 你也许会觉得,调用对象上的方法要比调用它的原型链上的方法快得多,其实并不是这样的,如果你的那个对象上面不是有很多的原型的话,它们的速度其实是差不多的另外,需要注意的一些地方:首先如果是在函数的prototype属性上定义方法的话,要牢记一点,如果你改变某个方法,那么由这个构造函数产生的所有对象的那个方法都会被改变.还有一点就是变量提升的问题,我们可以稍微的看一下下面的代码:func1(); // 这里会报错,因为在函数执行的时候,func1还没有被赋值. error: func1 is not a functionvar func1 = function() { console.log(‘func1’);};func2(); // 这个会被正确执行,因为函数的声明会被提升.function func2() { console.log(‘func2’);}关于对象序列化的问题.定义在函数的prototype上的属性不会被序列化,可以看下面的代码:function A(name) { this.name = name;}A.prototype.sayWhat = ‘say what…’;var a = new A(‘dreamapple’);console.log(JSON.stringify(a));我们可以看到输出结果是{“name”:“dreamapple”} ...

February 24, 2019 · 1 min · jiezi

JS中typeof与instanceof的区别

JavaScript 中typeof 和 instanceof 常用来判断一个变量是否为空,或者是什么类型的。但它们之间还是有区别的:typeoftypeof 是一个一元运算,放在一个运算数之前,运算数可以是任意类型。它返回值是一个字符串,该字符串说明运算数的类型。(typeof 运算符返回一个用来表示表达式的数据类型的字符串。 )typeof其实就是判断参数是什么类型的实例,就一个参数typeof 一般只能返回如下几个结果:“number”、“string”、“boolean”、“object”、“function” 和 “undefined”。运算数为数字 typeof(x) = “number” 字符串 typeof(x) = “string"布尔值 typeof(x) = “boolean” 对象,数组和null typeof(x) = “object” 函数 typeof(x) = “function"console.log(typeof (123));//typeof(123)返回"number” console.log(typeof (“123”));//typeof(“123”)返回"string"var param1 = “string”;var param2 = new Object();var param3 = 10;console.log(typeof(param1)+"\n”+typeof(param2)+"\n"+typeof(param3)); // string object number我们可以使用 typeof 来获取一个变量是否存在,如 if(typeof a!=“undefined”){alert(“ok”)},而不要去使用 if(a) 因为如果 a 不存在(未声明)则会出错,对于 Array,Null 等特殊对象使用 typeof 一律返回 object,这正是 typeof 的局限性。经常会在js里用到数组,比如 多个名字相同的input, 若是动态生成的, 提交时就需要判断其是否是数组. if(document.mylist.length != “undefined” ) {} //这个用法有误. 正确的是 if( typeof(document.mylist.length) != "undefined" ) {} 或 if( !isNaN(document.mylist.length) ) {} typeof的运算数未定义,返回的就是 “undefined”. 在 JavaScript 中,判断一个变量的类型尝尝会用 typeof 运算符,在使用 typeof 运算符时采用引用类型存储值会出现一个问题,无论引用的是什么类型的对象,它都返回 “object”。这就需要用到instanceof来检测某个对象是不是另一个对象的实例。instanceofinstanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。语法:object instanceof constructor参数:object(要检测的对象.)constructor(某个构造函数)描述:instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。instance:实例,例子a instanceof b?alert(“true”):alert(“false”); //a是b的实例?真:假instanceof 用于判断一个变量是否某个对象的实例,如 :var a=new Array();alert(a instanceof Array); // true,同时 alert(a instanceof Object) //也会返回 true;这是因为 Array 是 object 的子类。再如:function test(){};var a=new test();alert(a instanceof test) 会返回truealert(a==b); //flase案例:另外,更重的一点是 instanceof 可以在继承关系中用来判断一个实例是否属于它的父类型。例如:function Foo(){} Foo.prototype = new Aoo();//JavaScript 原型继承 var foo = new Foo(); console.log(foo instanceof Foo)//true console.log(foo instanceof Aoo)//true上面的代码中是判断了一层继承关系中的父类,在多层继承关系中,instanceof 运算符同样适用。又如:console.log(Object instanceof Object);//true console.log(Function instanceof Function);//true console.log(Number instanceof Number);//false console.log(String instanceof String);//false console.log(Function instanceof Object);//true console.log(Foo instanceof Function);//true console.log(Foo instanceof Foo);//false// 定义构造函数function C(){} function D(){} var o = new C();// true,因为 Object.getPrototypeOf(o) === C.prototypeo instanceof C; // false,因为 D.prototype不在o的原型链上o instanceof D; o instanceof Object; // true,因为Object.prototype.isPrototypeOf(o)返回trueC.prototype instanceof Object // true,同上C.prototype = {};var o2 = new C();o2 instanceof C; // trueo instanceof C; // false,C.prototype指向了一个空对象,这个空对象不在o的原型链上.D.prototype = new C(); // 继承var o3 = new D();o3 instanceof D; // trueo3 instanceof C; // true谈到 instanceof 我们要多插入一个问题,就是 function 的 arguments,我们大家也许都认为 arguments 是一个 Array,但如果使用 instaceof 去测试会发现 arguments 不是一个 Array 对象,尽管看起来很像。另外:测试 var a=new Array();if (a instanceof Object) alert(‘Y’);else alert(‘N’);得’Y’但 if (window instanceof Object) alert(‘Y’);else alert(‘N’);得’N’所以,这里的 instanceof 测试的 object 是指 js 语法中的 object,不是指 dom 模型对象。使用 typeof 会有些区别alert(typeof(window)) 会得 object需要注意的是,如果表达式 obj instanceof Foo 返回true,则并不意味着该表达式会永远返回ture,因为Foo.prototype属性的值有可能会改变,改变之后的值很有可能不存在于obj的原型链上,这时原表达式的值就会成为false。另外一种情况下,原表达式的值也会改变,就是改变对象obj的原型链的情况,虽然在目前的ES规范中,我们只能读取对象的原型而不能改变它,但借助于非标准的__proto__魔法属性,是可以实现的。比如执行obj.proto = {}之后,obj instanceof Foo就会返回false了。例子: 表明String对象和Date对象都属于Object类型下面的代码使用了instanceof来证明:String和Date对象同时也属于Object类型。例子: 表明String对象和Date对象都属于Object类型下面的代码使用了instanceof来证明:String和Date对象同时也属于Object类型。var simpleStr = “This is a simple string”; var myString = new String();var newStr = new String(“String created with constructor”);var myDate = new Date();var myObj = {};simpleStr instanceof String; // returns false, 检查原型链会找到 undefinedmyString instanceof String; // returns truenewStr instanceof String; // returns truemyString instanceof Object; // returns truemyObj instanceof Object; // returns true, despite an undefined prototype({}) instanceof Object; // returns true, 同上myString instanceof Date; // returns falsemyDate instanceof Date; // returns truemyDate instanceof Object; // returns truemyDate instanceof String; // returns false ...

February 24, 2019 · 2 min · jiezi

JS难点之hoist

这篇博文是之前在CSDN写的,现在移至sf。有过C或者Java类编程经验的同学,对于“先声明后使用”的规则很熟悉,如果使用未声明的变量或者函数,编译时程序会报错!但是,JavaScript却是一个‘大奇葩’,可以在变量或者函数声明之前使用,现在根据我的理解在做一下说明。首先说明JS的hoist分为变量hoist和函数hoist两种。一、变量hoist看一段程序 var a=10; function fun(){ console.log(a); var a=100; console.log(a); } fun();//undefined 100 console.log(a);//10 我们知道在js中,作用域分为全局作用域和函数作用域两种(ES6新特性,增加了块级作用域,另做说明)。全局变量声明有三种方式:var(关键字)+变量名(标识符)方式在function外部声明,显示声明没有使用var,直接给标识符赋值,隐式声明使用window全局对象来声明,全局对象的属性也应是全局变量 eg:window.test=50; alert(test); 好,很显然,var a=10;在程序中是全局变量。那么,按照我们正常的逻辑输出结果为:10 100 10。但是,实际输出结果为:undefined 100 10,其实是JS解析器的解析原因,它会将当前作用域中声明的所有变量和函数,放在作用域的最开始处。但是变量只有其声明被提前在作用域的最开始处,赋值结果仍然还在原来位置。上述代码对于解析器来说,其实是: var a=10; function fun(){ var a; console.log(a); a=100; console.log(a); } fun();//undefined 100 console.log(a);//10二、函数hoist 讲完变量hoist,现在再讲一下函数hoist,函数hoist又分为两种情况。一种是函数声明,另一种是函数作为值赋值给变量。先说第一种情况: fun();//2 function fun(){console.log(2);} 在这种情况下,可以看出,函数JS解释器允许在函数声明之前使用函数,其实也就说明,在这种情况,不仅函数名提前了,同时,函数体也被提前。所以可以上述代码可以执行。再说第二种情况: fun(); var fun=function(){ console.log(2); }结果为:Uncaught TypeError: fun is not a function 可以看出在此例中,函数只是变量声明声明提前,但是赋值没有提前,并且被提前的变量默认为undefined,所以报的错误类型为“typeerror”,因为undefined不是函数,不能被调用。三、变量名和函数名相同时的hoist function fun(){console.log(1);} fun();//2 function fun(){console.log(2);} fun();//2 var fun=100; console.log(fun);//100 fun();//报错在此例中,函数名和变量名相同,都是fun,都会提前,那么在提前时,有什么需要注意的地方呢? 函数声明比变量声明更置顶声明过得变量不会重复声明 所以上述代码等效于: function fun(){console.log(1);} function fun(){console.log(2);}//函数体覆盖上一层函数体 var fun;//实际无效 fun(); fun(); fun=100; console.log(fun); fun();以上就是本人对于JS的hoist问题的理解,如果哪位同学,发现其中有误,欢迎指正!我的微信号为:Alfred-kai。 ...

February 21, 2019 · 1 min · jiezi

vue:虚拟dom的实现

那么为什么要用 VDOM:现代 Web 页面的大多数逻辑的本质就是不停地修改DOM,但是 DOM 操作太慢了,直接导致整个页面掉帧、卡顿甚至失去响应。然而仔细想一想,很多 DOM 操作是可以打包(多个操作压成一个)和合并(一个连续更新操作只保留最终结果)的,同时 JS 引擎的计算速度要快得多,能不能把 DOM 操作放到 JS 里计算出最终结果来一发终极 DOM 操作?答案——当然可以!Vitual DOM是一种虚拟dom技术,本质上是基于javascript实现的,相对于dom对象,javascript对象更简单,处理速度更快,dom树的结构,属性信息都可以很容易的用javascript对象来表示:let element={ tagName:‘ul’,//节点标签名 props:{//dom的属性,用一个对象存储键值对 id:’list’ }, children:[//该节点的子节点 {tagName:’li’,props:{class:‘item’},children:[‘aa’]}, {tagName:’li’,props:{class:‘item’},children:[‘bb’]}, {tagName:’li’,props:{class:‘item’},children:[‘cc’]} ]}对应的html写法是:<ul id=‘list’> <li class=‘item’>aa</li> <li class=‘item’>aa</li> <li class=‘item’>aa</li></ul>Virtual DOM并没有完全实现DOM,Virtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性. 你给我一个数据,我根据这个数据生成一个全新的Virtual DOM,然后跟我上一次生成的Virtual DOM去 diff,得到一个Patch,然后把这个Patch打到浏览器的DOM上去。我们可以通过javascript对象表示的树结构来构建一棵真正的dom树,当数据状态发生变化时,可以直接修改这个javascript对象,接着对比修改后的javascript对象,记录下需要对页面做的dom操作,然后将其应用到真正的dom树,实现视图的更新,这个过程就是Virtual DOM的核心思想。VNode的数据结构图:VNode生成最关键的点是通过render有2种生成方式,第一种是直接在vue对象的option中添加render字段。第二种是写一个模板或指定一个el根元素,它会首先转换成模板,经过html语法解析器生成一个ast抽象语法树,对语法树做优化,然后把语法树转换成代码片段,最后通过代码片段生成function添加到option的render字段中。ast语法优的过程,主要做了2件事:会检测出静态的class名和attributes,这样它们在初始化渲染后就永远不会再被比对了。会检测出最大的静态子树(不需要动态性的子树)并且从渲染函数中萃取出来。这样在每次重渲染时,它就会直接重用完全相同的vnode,同时跳过比对。src/core/vdom/create-element.jsconst SIMPLE_NORMALIZE = 1const ALWAYS_NORMALIZE = 2function createElement (context, tag, data, children, normalizationType, alwaysNormalize) { // 兼容不传data的情况 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } // 如果alwaysNormalize是true // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值 if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE // 调用_createElement创建虚拟节点 return _createElement(context, tag, data, children, normalizationType)}function _createElement (context, tag, data, children, normalizationType) { /** * 如果存在data.ob,说明data是被Observer观察的数据 * 不能用作虚拟节点的data * 需要抛出警告,并返回一个空节点 * 被监控的data不能被用作vnode渲染的数据的原因是: * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作 / if (data && data.ob) { process.env.NODE_ENV !== ‘production’ && warn( Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n + ‘Always create fresh vnode data objects in each render!’, context ) return createEmptyVNode() } // 当组件的is属性被设置为一个falsy的值 // Vue将不会知道要把这个组件渲染成什么 // 所以渲染一个空节点 if (!tag) { return createEmptyVNode() } // 作用域插槽 if (Array.isArray(children) && typeof children[0] === ‘function’) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 根据normalizationType的值,选择不同的处理方法 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns // 如果标签名是字符串类型 if (typeof tag === ‘string’) { let Ctor // 获取标签名的命名空间 ns = config.getTagNamespace(tag) // 判断是否为保留标签 if (config.isReservedTag(tag)) { // 如果是保留标签,就创建一个这样的vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) // 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义 } else if ((Ctor = resolveAsset(context.$options, ‘components’, tag))) { // 如果找到了这个标签的定义,就以此创建虚拟组件节点 vnode = createComponent(Ctor, data, context, children, tag) } else { // 兜底方案,正常创建一个vnode vnode = new VNode( tag, data, children, undefined, undefined, context ) } // 当tag不是字符串的时候,我们认为tag是组件的构造类 // 所以直接创建 } else { vnode = createComponent(tag, data, context, children) } // 如果有vnode if (vnode) { // 如果有namespace,就应用下namespace,然后返回vnode if (ns) applyNS(vnode, ns) return vnode // 否则,返回一个空节点 } else { return createEmptyVNode() }}方法的功能是给一个Vnode对象对象添加若干个子Vnode,因为整个Virtual DOM是一种树状结构,每个节点都可能会有若干子节点。然后创建一个VNode对象,如果是一个reserved tag(比如html,head等一些合法的html标签)则会创建普通的DOM VNode,如果是一个component tag(通过vue注册的自定义component),则会创建Component VNode对象,它的VnodeComponentOptions不为Null.创建好Vnode,下一步就是要把Virtual DOM渲染成真正的DOM,是通过patch来实现的,源码如下:src/core/vdom/patch.js return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // oldVnoe:dom||当前vnode,vnode:vnoder=对象类型,hydration是否直接用服务端渲染的dom元素 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // 空挂载(可能是组件),创建新的根元素。 isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch 现有的根节点 patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // 安装到一个真实的元素。 // 检查这是否是服务器渲染的内容,如果我们可以执行。 // 成功的水合作用。 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== ‘production’) { warn( ‘The client-side rendered virtual DOM tree is not matching ’ + ‘server-rendered content. This is likely caused by incorrect ’ + ‘HTML markup, for example nesting block-level elements inside ’ + ‘<p>, or missing <tbody>. Bailing hydration and performing ’ + ‘full client-side render.’ ) } } // 不是服务器呈现,就是水化失败。创建一个空节点并替换它。 oldVnode = emptyNodeAt(oldVnode) } // 替换现有的元素 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // 极为罕见的边缘情况:如果旧元素在a中,则不要插入。 // 离开过渡。只有结合过渡+时才会发生。 // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 递归地更新父占位符节点元素。 if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroyi } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // 调用插入钩子,这些钩子可能已经被创建钩子合并了。 // 例如使用“插入”钩子的指令。 const insert = ancestor.data.hook.insert if (insert.merged) { // 从索引1开始,以避免重新调用组件挂起的钩子。 for (let i = 1; i < insert.fns.length; i++) { insert.fnsi } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }patch支持的3个参数,其中oldVnode是一个真实的DOM或者一个VNode对象,它表示当前的VNode,vnode是VNode对象类型,它表示待替换的VNode,hydration是bool类型,它表示是否直接使用服务器端渲染的DOM元素,下面流程图表示patch的运行逻辑:patch运行逻辑看上去比较复杂,有2个方法createElm和patchVnode是生成dom的关键,源码如下:/* * @param vnode根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createElm方法, * @param insertedVnodeQueue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法 / let inPre = 0 function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { vnode.isRootInsert = !nested // 过渡进入检查 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== ‘production’) { if (data && data.pre) { inPre++ } if ( !inPre && !vnode.ns && !( config.ignoredElements.length && config.ignoredElements.some(ignore => { return isRegExp(ignore) ? ignore.test(tag) : ignore === tag }) ) && config.isUnknownElement(tag) ) { warn( ‘Unknown custom element: <’ + tag + ‘> - did you ’ + ‘register the component correctly? For recursive components, ’ + ‘make sure to provide the “name” option.’, vnode.context ) } } vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) / istanbul ignore if / if (WEEX) { // in Weex, the default insertion order is parent-first. // List items can be optimized to use children-first insertion // with append=“tree”. const appendAsTree = isDef(data) && isTrue(data.appendAsTree) if (!appendAsTree) { if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } createChildren(vnode, children, insertedVnodeQueue) if (appendAsTree) { if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } if (process.env.NODE_ENV !== ‘production’ && data && data.pre) { inPre– } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }方法会根据vnode的数据结构创建真实的DOM节点,如果vnode有children,则会遍历这些子节点,递归调用createElm方法,InsertedVnodeQueue是记录子节点创建顺序的队列,每创建一个DOM元素就会往这个队列中插入当前的VNode,当整个VNode对象全部转换成为真实的DOM树时,会依次调用这个队列中的VNode hook的insert方法。/* * 比较新旧vnode节点,根据不同的状态对dom做合理的更新操作(添加,移动,删除)整个过程还会依次调用prepatch,update,postpatch等钩子函数,在编译阶段生成的一些静态子树,在这个过程 * @param oldVnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧vnode同时存在children,通过updateChildren方法对子节点做更新, * @param vnode * @param insertedVnodeQueue * @param removeOnly */ function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) { return } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 用于静态树的重用元素。 // 注意,如果vnode是克隆的,我们只做这个。 // 如果新节点不是克隆的,则表示呈现函数。 // 由热重加载api重新设置,我们需要进行适当的重新渲染。 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ‘’) addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ‘’) } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }updateChildren方法解析在此:vue:虚拟DOM的patch ...

February 21, 2019 · 6 min · jiezi

什么是闭包?闭包的优缺点?

什么是闭包?闭包的优缺点? 闭包(closure)是javascript的一大难点,也是它的特色。很多高级应用都要依靠闭包来实现。1、变量作用域要理解闭包,首先要理解javascript的特殊的变量作用域。变量的作用域无非就两种:全局变量和局部变量。javascript语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。注意点:在函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明的是一个全局变量!2、如何从外部读取函数内部的局部变量?出于种种原因,我们有时候需要获取到函数内部的局部变量。但是,上面已经说过了,正常情况下,这是办不到的!只有通过变通的方法才能实现。那就是在函数内部,再定义一个函数。 function f1(){ var n=999; function f2(){ alert(n); // 999 } }在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!3、闭包的概念上面代码中的f2函数,就是闭包。各种专业文献的闭包定义都非常抽象,我的理解是: 闭包就是能够读取其他函数内部变量的函数。由于在javascript中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成“定义在一个函数内部的函数“。所以,在本质上,闭包是将函数内部和函数外部连接起来的桥梁。4、闭包的用途闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在f1调用后被自动清除。为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}“这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。5、使用闭包的注意点(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

February 21, 2019 · 1 min · jiezi

webpack打包原理(待续)

打包工具要解决的问题:文件依赖管理 梳理文件之间的依赖关系资源加载管理 处理文件的加载顺序(先后时机)和文件的加载数量(合并、嵌入、拆分)效率与优化管理 提高开发效率,完成页面优化

February 21, 2019 · 1 min · jiezi

这一次,彻底弄懂 JavaScript 执行机制

本文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我。不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。因为javascript是一门单线程语言,所以我们可以得出结论:javascript是按照语句出现的顺序执行的看到这里读者要打人了:我难道不知道js是一行一行执行的?还用你说?稍安勿躁,正因为js是一行一行执行的,所以我们以为js都是这样的: let a = ‘1’; console.log(a); let b = ‘2’; console.log(b);然而实际上js是这样的:setTimeout(function(){ console.log(‘定时器开始啦’)});new Promise(function(resolve){ console.log(‘马上执行for循环啦’); for(var i = 0; i < 10000; i++){ i == 99 && resolve(); }}).then(function(){ console.log(‘执行then函数啦’)});console.log(‘代码执行结束’);依照js是按照语句出现的顺序执行这个理念,我自信的写下输出结果://“定时器开始啦”//“马上执行for循环啦”//“执行then函数啦”//“代码执行结束"去chrome上验证下,结果完全不对,瞬间懵了,说好的一行一行执行的呢?我们真的要彻底弄明白javascript的执行机制了。1.关于javascriptjavascript是一门 单线程 语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!2.javascript事件循环既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:同步任务异步任务当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。关于这部分有严格的文字定义,但本文的目的是用最小的学习成本彻底弄懂执行机制,所以我们用导图来说明:导图要表达的内容用文字来表述的话:同步和异步任务分别进入不同的执行"场所”,同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的Event Loop(事件循环)。我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。说了这么多文字,不如直接一段代码更直白:let data = [];$.ajax({ url:www.javascript.com, data:data, success:() => { console.log(‘发送成功!’); }})console.log(‘代码执行结束’);上面是一段简易的ajax请求代码:ajax进入Event Table,注册回调函数success。执行console.log(‘代码执行结束’)。ajax事件完成,回调函数success进入Event Queue。主线程从Event Queue读取回调函数success并执行。相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来我们来研究进阶话题:setTimeout。3.又爱又恨的setTimeout大名鼎鼎的setTimeout无需再多言,大家对他的第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:setTimeout(() => { console.log(‘延时3秒’);},3000)渐渐的setTimeout用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?先看一个例子:setTimeout(() => { task();},3000)console.log(‘执行console’);根据前面我们的结论,setTimeout是异步的,应该先执行console.log这个同步任务,所以我们的结论是://执行console//task()去验证一下,结果正确!然后我们修改一下前面的代码:setTimeout(() => { task()},3000)sleep(10000000)乍一看其实差不多嘛,但我们把这段代码在chrome执行一下,却发现控制台执行task()需要的时间远远超过3秒,说好的延时三秒,为啥现在需要这么长时间啊?这时候我们需要重新理解setTimeout的定义。我们先说上述代码是怎么执行的:task()进入Event Table并注册,计时开始。执行sleep函数,很慢,非常慢,计时仍在继续。3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。sleep终于执行完了,task()终于从Event Queue进入了主线程执行。上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明://代码1console.log(‘先执行这里’);setTimeout(() => { console.log(‘执行啦’)},0);//代码2console.log(‘先执行这里’);setTimeout(() => { console.log(‘执行啦’)},3000);代码1的输出结果是://先执行这里//执行啦代码2的输出结果是://先执行这里// … 3s later// 执行啦关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。有兴趣的同学可以自行了解。4.又恨又爱的setInterval上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。5.Promise与process.nextTick(callback)传统的定时器我们已经研究过了,接着我们探究Promise与process.nextTick(callback)的表现。Promise的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise。而process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:macro-task(宏任务):包括整体代码script,setTimeout,setIntervalmicro-task(微任务):Promise,process.nextTick不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:setTimeout(function() { console.log(‘setTimeout’);})new Promise(function(resolve) { console.log(‘promise’);}).then(function() { console.log(’then’);})console.log(‘console’);这段代码作为宏任务,进入主线程。先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。遇到console.log(),立即执行。好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。 结束。事件循环,宏任务,微任务的关系如图所示:我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:console.log(‘1’);setTimeout(function() { console.log(‘2’); process.nextTick(function() { console.log(‘3’); }) new Promise(function(resolve) { console.log(‘4’); resolve(); }).then(function() { console.log(‘5’) })})process.nextTick(function() { console.log(‘6’);})new Promise(function(resolve) { console.log(‘7’); resolve();}).then(function() { console.log(‘8’)})setTimeout(function() { console.log(‘9’); process.nextTick(function() { console.log(‘10’); }) new Promise(function(resolve) { console.log(‘11’); resolve(); }).then(function() { console.log(‘12’) })})第一轮事件循环流程分析如下:整体script作为第一个宏任务进入主线程,遇到console.log,输出1。遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。宏任务Event Queue | 微任务Event QueuesetTimeout1 | process1setTimeout2 | then1上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。我们发现了process1和then1两个微任务。执行process1,输出6。执行then1,输出8。好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。宏任务Event Queue | 微任务Event QueuesetTimeout2 | process2null | then2第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。输出3。输出5。第二轮事件循环结束,第二轮输出2,4,3,5。第三轮事件循环开始,此时只剩setTimeout2了,执行。直接输出9。将process.nextTick()分发到微任务Event Queue中。记为process3。直接执行new Promise,输出11。将then分发到微任务Event Queue中,记为then3。宏任务Event Queue | 微任务Event Queuenull | process3 null | then3第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。输出10。输出12。第三轮事件循环结束,第三轮输出9,11,10,12。整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)6.写在最后(1)js的异步我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。(2)事件循环Event Loop事件循环是js实现异步的一种方法,也是js的执行机制。(3)javascript的执行和运行执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。(4)setImmediate微任务和宏任务还有很多种类,比如setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。(5)最后的最后javascript是一门单线程语言Event Loop是javascript的执行机制牢牢把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想! ...

February 21, 2019 · 1 min · jiezi

JS中定时器线程理解

最近在准备面试,对于JS原理性的文章,感觉很有必要系统整理下,不必每一次都要查询资料,节约时间。问题setTimeout(function(){ console.log(“开始执行定时器回调: “+ new Date()) console.log(“我是定时器”)},0)大家觉得这个定时器定时时间设为0,有意义吗?是否觉得上述代码效果等同于console.log(“开始执行定时器回调: “+ new Date())console.log(“我是定时器”)实践是检验真理的最好途径。我们不排斥拿来主义,但是如果能自己实践验证,对于提升自身格物致知的精神很有裨益。针对上述问题,我们用两个实验来解开答案:实验一:console.log(“1”)console.log(“我是定时器”)console.log(“2”)打印结果实验二:console.log(“1”)setTimeout(function(){ console.log(“我是定时器”)},0)console.log(“2”)打印结果通过上述两个实验结果,我们可以得知 定时器定时为0时,JS执行到定时器这一步,并不是直接开始执行定时回调,而是执行了后续代码之后,才执行。那为什么会这样呢?我们仍然拿两个例子来说明:实验三: console.log(“1”) console.log(“定时器线程开始计时: “+ new Date()) setTimeout(function(){ console.log(“开始执行定时器回调: “+ new Date()) },5000) for(var i=0;i<500;i++){ console.log(“我是循环”) } console.log(“事件队列最后一位: “+ new Date())打印结果:从结果中可以看出,从定时器线程开始定时,到定时5秒结束后,将定时回调事件放入事件队列中执行,用了5秒。实验四: console.log(“1”) console.log(“定时器线程开始计时: “+ new Date()) setTimeout(function(){ console.log(“开始执行定时器回调: “+ new Date()) },5000) for(var i=0;i<50000;i++){ console.log(“我是循环”) } console.log(“事件队列最后一位: “+ new Date())打印结果:从结果中可以看出,从定时器线程开始定时,到定时5秒结束后,将定时回调事件放入事件队列中执行,用了9秒。两次结果不一致,是因为JS代码执行到定时器时,此时定时器线程开始定时,定时时间到之后,将定时回调事件推入事件队列而最后,JS线程依据事件队列中顺序执行。而之所以有的延时5秒,有的延时9秒,是因为如果定时器开始计时时,JS事件队列中执行剩余的事件小于5秒,则定时结束后,将定时回调事件推入队列中,JS能够立即执行定时回调事件,所以是5秒;而如果JS事件队列中执行剩余的事件大于5秒,那么在定时结束后,将定时回调事件推入队列后,还需一些时间来执行定时回调事件之前的事件,所以为9秒。

February 20, 2019 · 1 min · jiezi

Golang - 调度剖析【第三部分】

本篇是调度剖析的第三部分,将重点关注并发特性。回顾:第一部分第二部分简介首先,在我平时遇到问题的时候,特别是如果它是一个新问题,我一开始并不会考虑使用并发的设计去解决它。我会先实现顺序执行的逻辑,并确保它能正常工作。然后在可读性和技术关键点都 Review 之后,我才会开始思考并发执行的实用性和可行性。有的时候,并发执行是一个很好的选择,有时则不一定。在本系列的第一部分中,我解释了系统调度的机制和语义,如果你打算编写多线程代码,我认为这些机制和语义对于实现正确的逻辑是很重要的。在第二部分中,我解释了Go 调度的语义,我认为它能帮助你理解如何在 Go 中编写高质量的并发程序。在这篇文章中,我会把系统调度和Go 调度的机制和语义结合在一起,以便更深入地理解什么才是并发以及它的本质。什么是并发并发意味着乱序执行。拿一组原来是顺序执行的指令,而后找到一种方法,使这些指令乱序执行,但仍然产生相同的结果。那么,顺序执行还是乱序执行?根本在于,针对我们目前考虑的问题,使用并发必须是有收益的!确切来说,是并发带来的性能提升要大于它带来的复杂性成本。当然有些场景,代码逻辑就已经约束了我们不能执行乱序,这样使用并发也就没有了意义。并发与并行理解并发与并行的不同也非常重要。并行意味着同时执行两个或更多指令,简单来说,只有多个CPU核心之间才叫并行。在 Go 中,至少要有两个操作系统硬件线程并至少有两个 Goroutine 时才能实现并行,每个 Goroutine 在一个单独的系统线程上执行指令。如图:我们看到有两个逻辑处理器P,每个逻辑处理器都挂载在一个系统线程M上,而每个M适配到计算机上的一个CPU处理器Core。其中,有两个 Goroutine G1 和 G2 在并行执行,因为它们同时在各自的系统硬件线程上执行指令。再看,在每一个逻辑处理器中,都有三个 Goroutine G2 G3 G5 或 G1 G4 G6 轮流共享各自的系统线程。看起来就像这三个 Goroutine 在同时运行着,没有特定顺序地执行它们的指令,并在系统线程上共享时间。那么这就会发生竞争,有时候如果只在一个物理核心上实现并发则实际上会降低吞吐量。还有有意思的是,有时候即便利用上了并行的并发,也不会给你带来想象中更大的性能提升。工作负载我们怎么判断在什么时候并发会更有意义呢?我们就从了解当前执行逻辑的工作负载类型开始。在考虑并发时,有两种类型的工作负载是很重要的。两种类型CPU-Bound:这是一种不会导致 Goroutine 主动切换上下文到等待状态的类型。它会一直不停地进行计算。比如说,计算 到第 N 位的 Goroutine 就是 CPU-Bound 的。IO-Bound:与上面相反,这种类型会导致 Goroutine 自然地进入到等待状态。它包括请求通过网络访问资源,或使用系统调用进入操作系统,或等待事件的发生。比如说,需要读取文件的 Goroutine 就是 IO-Bound。我把同步事件(互斥,原子),会导致 Goroutine 等待的情况也包含在此类。在 CPU-Bound 中,我们需要利用并行。因为单个系统线程处理多个 Goroutine 的效率不高。而使用比系统线程更多的 Goroutine 也会拖慢执行速度,因为在系统线程上切换 Goroutine 是有时间成本的。上下文切换会导致发生STW(Stop The World),意思是在切换期间当前工作指令都不会被执行。在 IO-Bound 中,并行则不是必须的了。单个系统线程可以高效地处理多个 Goroutine,是因为Goroutine 在执行这类指令时会自然地进入和退出等待状态。使用比系统线程更多的 Goroutine 可以加快执行速度,因为此时在系统线程上切换 Goroutine 的延迟成本并不会产生STW事件。进入到IO阻塞时,CPU就闲下来了,那么我们可以使不同的 Goroutine 有效地复用相同的线程,不让系统线程闲置。我们如何评估一个系统线程匹配多少 Gorountine 是最合适的呢?如果 Goroutine 少了,则会无法充分利用硬件;如果 Goroutine 多了,则会导致上下文切换延迟。这是一个值得考虑的问题,但此时暂不深究。现在,更重要的是要通过仔细推敲代码来帮助我们准确识别什么情况需要并发,什么情况不能用并发,以及是否需要并行。加法我们不需要复杂的代码来展示和理解这些语义。先来看看下面这个名为add的函数:1 func add(numbers []int) int {2 var v int3 for _, n := range numbers {4 v += n5 }6 return v7 }在第 1 行,声明了一个名为add的函数,它接收一个整型切片并返回切片中所有元素的和。它从第 2 行开始,声明了一个v变量来保存总和。然后第 3 行,线性地遍历切片,并且每个数字被加到v中。最后在第 6 行,函数将最终的总和返回给调用者。问题:add函数是否适合并发执行?从大体上来说答案是适合的。可以将输入切片分解,然后同时处理它们。最后将每个小切片的执行结果相加,就可以得到和顺序执行相同的最终结果。与此同时,引申出另外一个问题:应该分成多少个小切片来处理是性能最佳的呢?要回答此问题,我们必须知道它的工作负载类型。add函数正在执行 CPU-Bound 工作负载,因为实现算法正在执行纯数学运算,并且它不会导致 Goroutine 进入等待状态。这意味着每个系统线程使用一个 Goroutine 就可以获得不错的吞吐量。并发版本下面来看一下并发版本如何实现,声明一个 addConcurrent 函数。代码量相比顺序版本增加了很多。1 func addConcurrent(goroutines int, numbers []int) int {2 var v int643 totalNumbers := len(numbers)4 lastGoroutine := goroutines - 15 stride := totalNumbers / goroutines67 var wg sync.WaitGroup8 wg.Add(goroutines)910 for g := 0; g < goroutines; g++ {11 go func(g int) {12 start := g * stride13 end := start + stride14 if g == lastGoroutine {15 end = totalNumbers16 }1718 var lv int19 for _, n := range numbers[start:end] {20 lv += n21 }2223 atomic.AddInt64(&v, int64(lv))24 wg.Done()25 }(g)26 }2728 wg.Wait()2930 return int(v)31 }第 5 行:计算每个 Goroutine 的子切片大小。使用输入切片总数除以 Goroutine 的数量得到。第 10 行:创建一定数量的 Goroutine 执行子任务第 14-16 行:子切片剩下的所有元素都放到最后一个 Goroutine 执行,可能比前几个 Goroutine 处理的数据要多。第 23 行:将子结果追加到最终结果中。然而,并发版本肯定比顺序版本更复杂,但和增加的复杂性相比,性能有提升吗?值得这么做吗?让我们用事实来说话,下面运行基准测试。基准测试下面的基准测试,我使用了1000万个数字的切片,并关闭了GC。分别有顺序版本add函数和并发版本addConcurrent函数。func BenchmarkSequential(b *testing.B) { for i := 0; i < b.N; i++ { add(numbers) }}func BenchmarkConcurrent(b *testing.B) { for i := 0; i < b.N; i++ { addConcurrent(runtime.NumCPU(), numbers) }}无并行以下是所有 Goroutine 只有一个硬件线程可用的结果。顺序版本使用 1 Goroutine,并发版本在我的机器上使用runtime.NumCPU或 8 Goroutines。在这种情况下,并发版本实际正跑在没有并行的机制上。10 Million Numbers using 8 goroutines with 1 core2.9 GHz Intel 4 Core i7Concurrency WITHOUT Parallelism—————————————————————————–$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-boundBenchmarkSequential 1000 5720764 ns/op : ~10% FasterBenchmarkConcurrent 1000 6387344 ns/opBenchmarkSequentialAgain 1000 5614666 ns/op : ~13% FasterBenchmarkConcurrentAgain 1000 6482612 ns/op结果表明:当只有一个系统线程可用于所有 Goroutine 时,顺序版本比并发快约10%到13%。这和我们之前的理论预期相符,主要就是因为并发版本在单核上的上下文切换和 Goroutine 管理调度的开销。有并行以下是每个 Goroutine 都有单独可用的系统线程的结果。顺序版本使用 1 Goroutine,并发版本在我的机器上使用runtime.NumCPU或 8 Goroutines。在这种情况下,并发版本利用上了并行机制。10 Million Numbers using 8 goroutines with 8 cores2.9 GHz Intel 4 Core i7Concurrency WITH Parallelism—————————————————————————–$ GOGC=off go test -cpu 8 -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-boundBenchmarkSequential-8 1000 5910799 ns/opBenchmarkConcurrent-8 2000 3362643 ns/op : ~43% FasterBenchmarkSequentialAgain-8 1000 5933444 ns/opBenchmarkConcurrentAgain-8 2000 3477253 ns/op : ~41% Faster结果表明:当为每个 Goroutine 提供单独的系统线程时,并发版本比顺序版本快大约41%到43%。这才也和预期一致,所有 Goroutine 现都在并行运行着,意味着他们真的在同时执行。排序另外,我们也要知道并非所有的 CPU-Bound 都适合并发。当切分输入或合并结果的代价非常高时,就不太合适。下面展示一个冒泡排序算法来说明此场景。顺序版本01 package main0203 import “fmt"0405 func bubbleSort(numbers []int) {06 n := len(numbers)07 for i := 0; i < n; i++ {08 if !sweep(numbers, i) {09 return10 }11 }12 }1314 func sweep(numbers []int, currentPass int) bool {15 var idx int16 idxNext := idx + 117 n := len(numbers)18 var swap bool1920 for idxNext < (n - currentPass) {21 a := numbers[idx]22 b := numbers[idxNext]23 if a > b {24 numbers[idx] = b25 numbers[idxNext] = a26 swap = true27 }28 idx++29 idxNext = idx + 130 }31 return swap32 }3334 func main() {35 org := []int{1, 3, 2, 4, 8, 6, 7, 2, 3, 0}36 fmt.Println(org)3738 bubbleSort(org)39 fmt.Println(org)40 }这种排序算法会扫描每次在交换值时传递的切片。在对所有内容进行排序之前,可能需要多次遍历切片。那么问题:bubbleSort函数是否适用并发?我相信答案是否定的。原始切片可以分解为较小的,并且可以同时对它们排序。但是!在并发执行完之后,没有一个有效的手段将子结果的切片排序合并。下面我们来看并发版本是如何实现的。并发版本01 func bubbleSortConcurrent(goroutines int, numbers []int) {02 totalNumbers := len(numbers)03 lastGoroutine := goroutines - 104 stride := totalNumbers / goroutines0506 var wg sync.WaitGroup07 wg.Add(goroutines)0809 for g := 0; g < goroutines; g++ {10 go func(g int) {11 start := g * stride12 end := start + stride13 if g == lastGoroutine {14 end = totalNumbers15 }1617 bubbleSort(numbers[start:end])18 wg.Done()19 }(g)20 }2122 wg.Wait()2324 // Ugh, we have to sort the entire list again.25 bubbleSort(numbers)26 }bubbleSortConcurrent它使用多个 Goroutine 同时对输入的一部分进行排序。我们直接来看结果:Before: 25 51 15 57 87 10 10 85 90 32 98 53 91 82 84 97 67 37 71 94 26 2 81 79 66 70 93 86 19 81 52 75 85 10 87 49After: 10 10 15 25 32 51 53 57 85 87 90 98 2 26 37 67 71 79 81 82 84 91 94 97 10 19 49 52 66 70 75 81 85 86 87 93由于冒泡排序的本质是依次扫描,第 25 行对 bubbleSort 的调用将掩盖使用并发解决问题带来的潜在收益。结论是:在冒泡排序中,使用并发不会带来性能提升。读取文件前面已经举了两个 CPU-Bound 的例子,下面我们来看 IO-Bound。顺序版本01 func find(topic string, docs []string) int {02 var found int03 for _, doc := range docs {04 items, err := read(doc)05 if err != nil {06 continue07 }08 for _, item := range items {09 if strings.Contains(item.Description, topic) {10 found++11 }12 }13 }14 return found15 }第 2 行:声明了一个名为 found 的变量,用于保存在给定文档中找到指定主题的次数。第 3-4 行:迭代文档,并使用read函数读取每个文档。第 8-11 行:使用 strings.Contains 函数检查文档中是否包含指定主题。如果包含,则found加1。然后来看一下read是如何实现的。01 func read(doc string) ([]item, error) {02 time.Sleep(time.Millisecond) // 模拟阻塞的读03 var d document04 if err := xml.Unmarshal([]byte(file), &d); err != nil {05 return nil, err06 }07 return d.Channel.Items, nil08 }此功能以 time.Sleep 开始,持续1毫秒。此调用用于模拟在我们执行实际系统调用以从磁盘读取文档时可能产生的延迟。这种延迟的一致性对于准确测量find顺序版本和并发版本的性能差距非常重要。然后在第 03-07 行,将存储在全局变量文件中的模拟 xml 文档反序列化为struct值。最后,将Items返回给调用者。并发版本01 func findConcurrent(goroutines int, topic string, docs []string) int {02 var found int640304 ch := make(chan string, len(docs))05 for _, doc := range docs {06 ch <- doc07 }08 close(ch)0910 var wg sync.WaitGroup11 wg.Add(goroutines)1213 for g := 0; g < goroutines; g++ {14 go func() {15 var lFound int6416 for doc := range ch {17 items, err := read(doc)18 if err != nil {19 continue20 }21 for _, item := range items {22 if strings.Contains(item.Description, topic) {23 lFound++24 }25 }26 }27 atomic.AddInt64(&found, lFound)28 wg.Done()29 }()30 }3132 wg.Wait()3334 return int(found)35 }第 4-7 行:创建一个channel并写入所有要处理的文档。第 8 行:关闭这个channel,这样当读取完所有文档后就会直接退出循环。第 16-26 行:每个 Goroutine 都从同一个channel接收文档,read 并 strings.Contains 逻辑和顺序的版本一致。第 27 行:将各个 Goroutine 计数加在一起作为最终计数。基准测试同样的,我们再次运行基准测试来验证我们的结论。func BenchmarkSequential(b *testing.B) { for i := 0; i < b.N; i++ { find(“test”, docs) }}func BenchmarkConcurrent(b *testing.B) { for i := 0; i < b.N; i++ { findConcurrent(runtime.NumCPU(), “test”, docs) }}无并行10 Thousand Documents using 8 goroutines with 1 core2.9 GHz Intel 4 Core i7Concurrency WITHOUT Parallelism—————————————————————————–$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-boundBenchmarkSequential 3 1483458120 ns/opBenchmarkConcurrent 20 188941855 ns/op : ~87% FasterBenchmarkSequentialAgain 2 1502682536 ns/opBenchmarkConcurrentAgain 20 184037843 ns/op : ~88% Faster当只有一个系统线程时,并发版本比顺序版本快大约87%到88%。与预期一致,因为所有 Goroutine 都有效地共享单个系统线程。有并行10 Thousand Documents using 8 goroutines with 8 core2.9 GHz Intel 4 Core i7Concurrency WITH Parallelism—————————————————————————–$ GOGC=off go test -run none -bench . -benchtime 3sgoos: darwingoarch: amd64pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-boundBenchmarkSequential-8 3 1490947198 ns/opBenchmarkConcurrent-8 20 187382200 ns/op : ~88% FasterBenchmarkSequentialAgain-8 3 1416126029 ns/opBenchmarkConcurrentAgain-8 20 185965460 ns/op : ~87% Faster有意思的来了,使用额外的系统线程提供并行能力,实际代码性能却没有提升。也印证了开头的说法。结语我们可以清楚地看到,使用 IO-Bound 并不需要并行来获得性能上的巨大提升。这与我们在 CPU-Bound 中看到的结果相反。当涉及像冒泡排序这样的算法时,并发的使用会增加复杂性而没有任何实际的性能优势。所以,我们在考虑解决方案时,首先要确定它是否适合并发,而不是盲目认为使用更多的 Goroutine 就一定会提升性能。 ...

December 11, 2018 · 5 min · jiezi

Golang - 调度剖析【第二部分】

回顾本系列的第一部分,重点讲述了操作系统调度器的各个方面,这些知识对于理解和分析 Go 调度器的语义是非常重要的。在本文中,我将从语义层面解析 Go 调度器是如何工作的,并重点介绍其高级特性。Go 调度器是一个非常复杂的系统,我们不会过分关注一些细节,而是侧重于剖析它的设计模型和工作方式。我们通过学习它的优点以便够做出更好的工程决策。开始当 Go 程序启动时,它会为主机上标识的每个虚拟核心提供一个逻辑处理器(P)。如果处理器每个物理核心可以提供多个硬件线程(超线程),那么每个硬件线程都将作为虚拟核心呈现给 Go 程序。为了更好地理解这一点,下面实验都基于如下配置的 MacBook Pro 的系统。可以看到它是一个 4 核 8 线程的处理器。这将告诉 Go 程序有 8 个虚拟核心可用于并行执行系统线程。用下面的程序来验证一下:package mainimport ( “fmt” “runtime”)func main() { // NumCPU 返回当前可用的逻辑处理核心的数量 fmt.Println(runtime.NumCPU())}当我运行该程序时,NumCPU() 函数调用的结果将是 8 。意味着在我的机器上运行的任何 Go 程序都将被赋予 8 个 P。每个 P 都被分配一个系统线程 M 。M 代表机器(machine),它仍然是由操作系统管理的,操作系统负责将线程放在一个核心上执行。这意味着当在我的机器上运行 Go 程序时,有 8 个线程可以执行我的工作,每个线程单独连接到一个 P。每个 Go 程序都有一个初始 G。G 代表 Go 协程(Goroutine),它是 Go 程序的执行路径。Goroutine 本质上是一个 Coroutine,但因为是 Go 语言,所以把字母 “C” 换成了 “G”,我们得到了这个词。你可以将 Goroutines 看作是应用程序级别的线程,它在许多方面与系统线程都相似。正如系统线程在物理核心上进行上下文切换一样,Goroutines 在 M 上进行上下文切换。最后一个重点是运行队列。Go 调度器中有两个不同的运行队列:全局运行队列(GRQ)和本地运行队列(LRQ)。每个 P 都有一个LRQ,用于管理分配给在P的上下文中执行的 Goroutines,这些 Goroutine 轮流被和P绑定的M进行上下文切换。GRQ 适用于尚未分配给P的 Goroutines。其中有一个过程是将 Goroutines 从 GRQ 转移到 LRQ,我们将在稍后讨论。下面图示展示了它们之间的关系:协作式调度器正如我们在第一篇文章中所讨论的,OS 调度器是一个抢占式调度器。从本质上看,这意味着你无法预测调度程序在任何给定时间将执行的操作。由内核做决定,一切都是不确定的。在操作系统之上运行的应用程序无法通过调度控制内核内部发生的事情,除非它们利用像 atomic 指令 和 mutex 调用之类的同步原语。Go 调度器是 Go 运行时的一部分,Go 运行时内置在应用程序中。这意味着 Go 调度器在内核之上的用户空间中运行。Go 调度器的当前实现不是抢占式调度器,而是协作式调度器。作为一个协作的调度器,意味着调度器需要明确定义用户空间事件,这些事件发生在代码中的安全点,以做出调度决策。Go 协作式调度器的优点在于它看起来和感觉上都是抢占式的。你无法预测 Go 调度器将会执行的操作。这是因为这个协作调度器的决策不掌握在开发人员手中,而是在 Go 运行时。将 Go 调度器视为抢占式调度器是非常重要的,并且由于调度程序是非确定性的,因此这并不是一件容易的事。Goroutine 状态就像线程一样,Goroutines 有相同的三个高级状态。它们标识了 Go 调度器在任何给定的 Goroutine 中所起的作用。Goroutine 可以处于三种状态之一:Waiting(等待状态)、Runnable(可运行状态)或Executing(运行中状态)。Waiting:这意味着 Goroutine 已停止并等待一些事情以继续。这可能是因为等待操作系统(系统调用)或同步调用(原子和互斥操作)等原因。这些类型的延迟是性能下降的根本原因。Runnable :这意味着 Goroutine 需要M上的时间片,来执行它的指令。如果同一时间有很多 Goroutines 在竞争时间片,它们都必须等待更长时间才能得到时间片,而且每个 Goroutine 获得的时间片都缩短了。这种类型的调度延迟也可能导致性能下降。Executing :这意味着 Goroutine 已经被放置在M上并且正在执行它的指令。与应用程序相关的工作正在完成。这是每个人都想要的。上下文切换Go 调度器需要有明确定义的用户空间事件,这些事件发生在要切换上下文的代码中的安全点上。这些事件和安全点在函数调用中表现出来。函数调用对于 Go 调度器的运行状况是至关重要的。现在(使用 Go 1.11或更低版本),如果你运行任何未进行函数调用的紧凑循环,你会导致调度器和垃圾回收有延迟。让函数调用在合理的时间范围内发生是至关重要的。注意:在 Go 1.12 版本中有一个提议被接受了,它可以使 Go 调度器使用非协作抢占技术,以允许抢占紧密循环。在 Go 程序中有四类事件,它们允许调度器做出调度决策:使用关键字 go垃圾回收系统调用同步和编配使用关键字 go关键字 go 是用来创建 Goroutines 的。一旦创建了新的 Goroutine,它就为调度器做出调度决策提供了机会。垃圾回收由于 GC 使用自己的 Goroutine 运行,所以这些 Goroutine 需要在 M 上运行的时间片。这会导致 GC 产生大量的调度混乱。但是,调度程序非常聪明地了解 Goroutine 正在做什么,它将智能地做出一些决策。系统调用如果 Goroutine 进行系统调用,那么会导致这个 Goroutine 阻塞当前M,有时调度器能够将 Goroutine 从M换出并将新的 Goroutine 换入。然而,有时需要新的M继续执行在P中排队的 Goroutines。这是如何工作的将在下一节中更详细地解释。同步和编配如果原子、互斥量或通道操作调用将导致 Goroutine 阻塞,调度器可以将之切换到一个新的 Goroutine 去运行。一旦 Goroutine 可以再次运行,它就可以重新排队,并最终在M上切换回来。异步系统调用当你的操作系统能够异步处理系统调用时,可以使用称为网络轮询器的东西来更有效地处理系统调用。这是通过在这些操作系统中使用 kqueue(MacOS),epoll(Linux)或 iocp(Windows)来实现的。基于网络的系统调用可以由我们今天使用的许多操作系统异步处理。这就是为什么我管它叫网络轮询器,因为它的主要用途是处理网络操作。通过使用网络轮询器进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞M。这可以让M执行P的 LRQ 中其他的 Goroutines,而不需要创建新的M。有助于减少操作系统上的调度负载。下图展示它的工作原理:G1正在M上执行,还有 3 个 Goroutine 在 LRQ 上等待执行。网络轮询器空闲着,什么都没干。接下来,情况发生了变化:G1想要进行网络系统调用,因此它被移动到网络轮询器并且处理异步网络系统调用。然后,M可以从 LRQ 执行另外的 Goroutine。此时,G2就被上下文切换到M上了。最后:异步网络系统调用由网络轮询器完成,G1被移回到P的 LRQ 中。一旦G1可以在M上进行上下文切换,它负责的 Go 相关代码就可以再次执行。这里的最大优势是,执行网络系统调用不需要额外的M。网络轮询器使用系统线程,它时刻处理一个有效的事件循环。同步系统调用如果 Goroutine 要执行同步的系统调用,会发生什么?在这种情况下,网络轮询器无法使用,而进行系统调用的 Goroutine 将阻塞当前M。这是不幸的,但是没有办法防止这种情况发生。需要同步进行的系统调用的一个例子是基于文件的系统调用。如果你正在使用 CGO,则可能还有其他情况,调用 C 函数也会阻塞M。注意:Windows 操作系统确实能够异步进行基于文件的系统调用。从技术上讲,在 Windows 上运行时,可以使用网络轮询器。让我们来看看同步系统调用(如文件I/O)会导致M阻塞的情况:G1将进行同步系统调用以阻塞M1。调度器介入后:识别出G1已导致M1阻塞,此时,调度器将M1与P分离,同时也将G1带走。然后调度器引入新的M2来服务P。此时,可以从 LRQ 中选择G2并在M2上进行上下文切换。阻塞的系统调用完成后:G1可以移回 LRQ 并再次由P执行。如果这种情况需要再次发生,M1将被放在旁边以备将来使用。任务窃取(负载均衡思想)调度器的另一个方面是它是一个任务窃取的调度器。这有助于在一些领域保持高效率的调度。首先,你最不希望的事情是M进入等待状态,因为一旦发生这种情况,操作系统就会将M从内核切换出去。这意味着P无法完成任何工作,即使有 Goroutine 处于可运行状态也不行,直到一个M被上下文切换回核心。任务窃取还有助于平衡所有P的 Goroutines 数量,这样工作就能更好地分配和更有效地完成。看下面的一个例子:这是一个多线程的 Go 程序,其中有两个P,每个P都服务着四个 Goroutine,另在 GRQ 中还有一个单独的 Goroutine。如果其中一个P的所有 Goroutines 很快就执行完了会发生什么?如你所见:P1的 Goroutines 都执行完了。但是还有 Goroutines 处于可运行状态,在 GRQ 中有,在P2的 LRQ 中也有。这时P1就需要窃取任务。窃取的规则在这里定义了:https://golang.org/src/runtim…if gp == nil { // 1/61的概率检查一下全局可运行队列,以确保公平。否则,两个 goroutine 就可以通过不断地相互替换来完全占据本地运行队列。 if g.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { lock(&sched.lock) gp = globrunqget(g.m.p.ptr(), 1) unlock(&sched.lock) } } if gp == nil { gp, inheritTime = runqget(g.m.p.ptr()) if gp != nil && g.m.spinning { throw(“schedule: spinning with local work”) } } if gp == nil { gp, inheritTime = findrunnable() }根据规则,P1将窃取P2中一半的 Goroutines,窃取完成后的样子如下:我们再来看一种情况,如果P2完成了对所有 Goroutine 的服务,而P1的 LRQ 也什么都没有,会发生什么?P2完成了所有任务,现在需要窃取一些。首先,它将查看P1的 LRQ,但找不到任何 Goroutines。接下来,它将查看 GRQ。在那里它会找到G9,P2从 GRQ 手中抢走了G9并开始执行。以上任务窃取的好处在于它使M不会闲着。在窃取任务时,M是自旋的。这种自旋还有其他的好处,可以参考 work-stealing 。实例有了相应的机制和语义,我将向你展示如何将所有这些结合在一起,以便 Go 调度程序能够执行更多的工作。设想一个用 C 编写的多线程应用程序,其中程序管理两个操作系统线程,这两个线程相互传递消息。下面有两个线程,线程 T1 在内核 C1 上进行上下文切换,并且正在运行中,这允许 T1 将其消息发送到 T2。当 T1 发送完消息,它需要等待响应。这将导致 T1 从 C1 上下文换出并进入等待状态。当 T2 收到有关该消息的通知,它就会进入可运行状态。现在操作系统可以执行上下文切换并让 T2 在一个核心上执行,而这个核心恰好是 C2。接下来,T2 处理消息并将新消息发送回 T1。然后,T2 的消息被 T1 接收,线程上下文切换再次发生。现在,T2 从运行中状态切换到等待状态,T1 从等待状态切换到可运行状态,再被执行变为运行中状态,这允许它处理并发回新消息。所有这些上下文切换和状态更改都需要时间来执行,这限制了工作的完成速度。由于每个上下文切换可能会产生 50 纳秒的延迟,并且理想情况下硬件每纳秒执行 12 条指令,因此你会看到有差不多 600 条指令,在上下文切换期间被停滞掉了。并且由于这些线程也在不同的内核之间跳跃,因 cache-line 未命中引起额外延迟的可能性也很高。下面我们还用这个例子,来看看 Goroutine 和 Go 调度器是怎么工作的:有两个goroutine,它们彼此协调,来回传递消息。G1在M1上进行上下文切换,而M1恰好运行在C1上,这允许G1执行它的工作。即向G2发送消息。G1发送完消息后,需要等待响应。M1就会把G1换出并使之进入等待状态。一旦G2得到消息,它就进入可运行状态。现在 Go 调度器可以执行上下文切换,让G2在M1上执行,M1仍然在C1上运行。接下来,G2处理消息并将新消息发送回G1。当G2发送的消息被G1接收时,上下文切换再次发生。现在G2从运行中状态切换到等待状态,G1从等待状态切换到可运行状态,最后返回到执行状态,这允许它处理和发送一个新的消息。表面上看起来没有什么不同。无论使用线程还是 Goroutine,都会发生相同的上下文切换和状态变更。然而,使用线程和 Goroutine 之间有一个主要区别:在使用 Goroutine 的情况下,会复用同一个系统线程和核心。这意味着,从操作系统的角度来看,操作系统线程永远不会进入等待状态。因此,在使用系统线程时的开销在使用 Goroutine 时就不存在了。基本上,Go 已经在操作系统级别将 IO-Bound 类型的工作转换为 CPU-Bound 类型。由于所有的上下文切换都是在应用程序级别进行的,所以在使用线程时,每个上下文切换(平均)不至于迟滞 600 条指令。该调度程序还有助于提高 cache-line 效率和 NUMA。在 Go 中,随着时间的推移,可以完成更多的工作,因为 Go 调度器尝试使用更少的线程,在每个线程上做更多的工作,这有助于减少操作系统和硬件的负载。结论Go 调度器在设计中考虑到复杂的操作系统和硬件的工作方式,真是令人惊叹。在操作系统级别将 IO-Bound 类型的工作转换为 CPU-Bound 类型的能力是我们在利用更多 CPU 的过程中获得巨大成功的地方。这就是为什么不需要比虚拟核心更多的操作系统线程的原因。你可以合理地期望每个虚拟内核只有一个系统线程来完成所有工作(CPU和IO)。对于网络应用程序和其他不会阻塞操作系统线程的系统调用的应用程序来说,这样做是可能的。作为一个开发人员,你当然需要知道程序在运行中做了什么。你不可能创建无限数量的 Goroutine ,并期待惊人的性能。越少越好,但是通过了解这些 Go 调度器的语义,您可以做出更好的工程决策。在下一篇文章中,我将探讨以保守的方式利用并发性以获得更好的性能,同时平衡可能需要增加到代码中的复杂性。 ...

October 9, 2018 · 2 min · jiezi

Go 程序是如何编译成目标机器码的

今天我们一起来研究 Go 1.11 的编译器,以及它将 Go 程序代码编译成可执行文件的过程。以便了解我们日常使用的工具是如何工作的。本文还会带你了解 Go 程序为什么这么快,以及编译器在这中间起到了什么作用。首先,编译器的三个阶段:逐行扫描源代码,将之转换为一系列的 token,交给 parser 解析。parser,它将一系列 token 转换为 AST(抽象语法树),用于下一步生成代码。最后一步,代码生成,会利用上一步生成的 AST 并根据目标机器平台的不同,生成目标机器码。注意:下面使用的代码包(go/scanner,go/parser,go/token,go/ast)主要是让我们可以方便地对 Go 代码进行解析和生成,做出更有趣的事情。但是 Go 本身的编译器并不是用这些代码包实现的。扫描代码,进行词法分析任何编译器的第一步都是将源代码文本分解成 token,由扫描程序(也称为词法分析器)完成。token 可以是关键字,字符串,变量名,函数名等等。每一个有效的词都由 token 表示。在 Go 中,我们写在代码上的 “package”,“main”,“func” 这些都是 token。token 由代码中的位置,类型和原始文本组成。我们可以使用 go/scanner 和 go/token 包在 Go 程序中自己执行扫描程序。这意味着我们可以像编译器那样扫描检视自己的代码。下面,我们将通过一个打印 Hello World 的示例来展示 token。package mainimport ( “fmt” “go/scanner” “go/token”)func main() { src := []byte(package mainimport "fmt"func main() { fmt.Println("Hello, world!")}) var s scanner.Scanner fset := token.NewFileSet() file := fset.AddFile("", fset.Base(), len(src)) s.Init(file, src, nil, 0) for { pos, tok, lit := s.Scan() fmt.Printf("%-6s%-8s%q\n", fset.Position(pos), tok, lit) if tok == token.EOF { break } }}首先通过源代码字符串创建 token 集合并初始化 scan.Scanner,它将逐行扫描我们的源代码。接下来循环调用 Scan() 并打印每个 token 的位置,类型和文本字符串,直到遇到文件结束(EOF)标记。输出:2:1 package “package"2:9 IDENT “main"2:13 ; “\n"4:1 import “import"4:8 STRING “"fmt"“4:13 ; “\n"6:1 func “func"6:6 IDENT “main"6:10 ( ““6:11 ) ““6:13 { ““7:2 IDENT “fmt"7:5 . ““7:6 IDENT “Println"7:13 ( ““7:14 STRING “"Hello, world!"“7:29 ) ““7:30 ; “\n"8:1 } ““8:2 ; “\n"8:3 EOF ““以第一行为例分析这个输出,第一列 2:1 表示扫描到了源代码第二行第一个字符,第二列 package 表示 token 是 package,第三列 “package” 表示源代码文本。我们可以看到在 Scanner 执行过程中将 \n 换行符标记成了 ; 分号,像在 C 语言中是用分号表示一行结束的。这就解释了为什么 Go 不需要分号:它们是在词法分析阶段由 Scanner 智能地解释的。语法分析源代码扫描完成后,扫描结果将被传递给语法分析器。语法分析是编译的一个阶段,它将 token 转换为 抽象语法树(AST)。 AST 是源代码的结构化表示。在 AST 中,我们将能够看到程序结构,比如函数和常量声明。我们使用 go/parser 和 go/ast 来打印完整的 AST:package mainimport ( “go/ast” “go/parser” “go/token” “log”)func main() { src := []byte(package mainimport "fmt"func main() { fmt.Println("Hello, world!")}) fset := token.NewFileSet() file, err := parser.ParseFile(fset, “”, src, 0) if err != nil { log.Fatal(err) } ast.Print(fset, file)}输出: 0 *ast.File { 1 . Package: 2:1 2 . Name: *ast.Ident { 3 . . NamePos: 2:9 4 . . Name: “main” 5 . } 6 . Decls: []ast.Decl (len = 2) { 7 . . 0: *ast.GenDecl { 8 . . . TokPos: 4:1 9 . . . Tok: import 10 . . . Lparen: - 11 . . . Specs: []ast.Spec (len = 1) { 12 . . . . 0: *ast.ImportSpec { 13 . . . . . Path: *ast.BasicLit { 14 . . . . . . ValuePos: 4:8 15 . . . . . . Kind: STRING 16 . . . . . . Value: “"fmt"” 17 . . . . . } 18 . . . . . EndPos: - 19 . . . . } 20 . . . } 21 . . . Rparen: - 22 . . } 23 . . 1: *ast.FuncDecl { 24 . . . Name: *ast.Ident { 25 . . . . NamePos: 6:6 26 . . . . Name: “main” 27 . . . . Obj: *ast.Object { 28 . . . . . Kind: func 29 . . . . . Name: “main” 30 . . . . . Decl: *(obj @ 23) 31 . . . . } 32 . . . } 33 . . . Type: *ast.FuncType { 34 . . . . Func: 6:1 35 . . . . Params: *ast.FieldList { 36 . . . . . Opening: 6:10 37 . . . . . Closing: 6:11 38 . . . . } 39 . . . } 40 . . . Body: *ast.BlockStmt { 41 . . . . Lbrace: 6:13 42 . . . . List: []ast.Stmt (len = 1) { 43 . . . . . 0: *ast.ExprStmt { 44 . . . . . . X: *ast.CallExpr { 45 . . . . . . . Fun: *ast.SelectorExpr { 46 . . . . . . . . X: *ast.Ident { 47 . . . . . . . . . NamePos: 7:2 48 . . . . . . . . . Name: “fmt” 49 . . . . . . . . } 50 . . . . . . . . Sel: *ast.Ident { 51 . . . . . . . . . NamePos: 7:6 52 . . . . . . . . . Name: “Println” 53 . . . . . . . . } 54 . . . . . . . } 55 . . . . . . . Lparen: 7:13 56 . . . . . . . Args: []ast.Expr (len = 1) { 57 . . . . . . . . 0: *ast.BasicLit { 58 . . . . . . . . . ValuePos: 7:14 59 . . . . . . . . . Kind: STRING 60 . . . . . . . . . Value: “"Hello, world!"” 61 . . . . . . . . } 62 . . . . . . . } 63 . . . . . . . Ellipsis: - 64 . . . . . . . Rparen: 7:29 65 . . . . . . } 66 . . . . . } 67 . . . . } 68 . . . . Rbrace: 8:1 69 . . . } 70 . . } 71 . } 72 . Scope: *ast.Scope { 73 . . Objects: map[string]*ast.Object (len = 1) { 74 . . . “main”: *(obj @ 27) 75 . . } 76 . } 77 . Imports: []*ast.ImportSpec (len = 1) { 78 . . 0: *(obj @ 12) 79 . } 80 . Unresolved: []*ast.Ident (len = 1) { 81 . . 0: *(obj @ 46) 82 . } 83 }分析这个输出,在 Decls 字段中,包含了代码中所有的声明,例如导入、常量、变量和函数。在本例中,我们只有两个:导入fmt包 和 主函数。为了进一步理解它,我们可以看看下面这个图,它是上述数据的表示,但只包含类型,红色代表与节点对应的代码:main函数由三个部分组成:Name、Type 和 Body。Name 是值为 main 的标识符。由 Type 字段指定的声明将包含参数列表和返回类型(如果我们指定了的话)。正文由一系列语句组成,里面包含了程序的所有行,在本例中只有一行fmt.Println(“Hello, world!")。我们的一条 fmt.Println 语句由 AST 中很多部分组成。该语句是一个 ExprStmt表达式语句(expression statement),例如,它可以像这里一样是一个函数调用,它可以是字面量,可以是一个二元运算(例如加法和减法),当然也可以是一元运算(例如自增++,自减–,否定!等)等等。同时,在函数调用的参数中可以使用任何表达式。然后,ExprStmt 又包含一个 CallExpr,它是我们实际的函数调用。里面又包括几个部分,其中最重要的部分是 Fun 和 Args。 Fun 包含对函数调用的引用,在这种情况下,它是一个 SelectorExpr,因为我们从 fmt 包中选择 Println 标识符。但是至此,在 AST 中,编译器还不知道 fmt 是一个包,它也可能是 AST 中的一个变量。Args 包含一个表达式列表,它是函数的参数。这里,我们将一个文本字符串传递给函数,因而它由一个类型为 STRING 的 BasicLit 表示。显然,AST 包含了许多信息,我们不仅可以分析出以上结论,还可以进一步检查 AST 并查找文件中的所有函数调用。下面,我们将使用 go/ast 包中的 Inspect 函数来递归地遍历树,并分析所有节点的信息。package mainimport ( “fmt” “go/ast” “go/parser” “go/printer” “go/token” “os”)func main() { src := []byte(package mainimport "fmt"func main() { fmt.Println("Hello, world!")}) fset := token.NewFileSet() file, err := parser.ParseFile(fset, “”, src, 0) if err != nil { fmt.Println(err) } ast.Inspect(file, func(n ast.Node) bool { call, ok := n.(ast.CallExpr) if !ok { return true } printer.Fprint(os.Stdout, fset, call.Fun) return false })}输出:fmt.Println上面代码的作用是查找所有节点以及它们是否为 ast.CallExpr 类型,上面也说过这种类型是函数调用。如果是,则使用 go/printer 包打印 Fun 中存在的函数的名称。构建出 AST 后,将使用 GOPATH 或者在 Go 1.11 及更高版本中的 modules 解析所有导入。然后,执行类型检查,并做一些让程序运行更快的初级优化。代码生成在解析导入并做了类型检查之后,我们可以确认程序是合法的 Go 代码,然后就走到将 AST 转换为(伪)目标机器码的过程。此过程的第一步是将 AST 转换为程序的低级表示,特别是转换为 静态单赋值(SSA)表单。这个中间表示不是最终的机器代码,但它确实代表了最终的机器代码。 SSA 具有一组属性,会使应用优化变得更容易,其中最重要的是在使用变量之前总是定义变量,并且每个变量只分配一次。在生成 SSA 的初始版本之后,将执行一些优化。这些优化适用于某些代码,可以使处理器执行起来更简单且更快速。例如,可以做 死码消除。还有比如可以删除某些 nil 检查,因为编译器可以证明这些检查永远不会出错。现在通过最简单的例子来说明 SSA 和一些优化过程:package mainimport “fmt"func main() { fmt.Println(2)}如你所见,此程序只有一个函数和一个导入。它会在运行时打印 2。但是,此例足以让我们了解SSA。为了显示生成的 SSA,我们需要将 GOSSAFUNC 环境变量设置为我们想要跟踪的函数,在本例中为main 函数。我们还需要将 -S 标识传递给编译器,这样它就会打印代码并创建一个HTML文件。我们还将编译Linux 64位的文件,以确保机器代码与您在这里看到的相同。在终端执行下面的命令:GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags -S main.go会在终端打印出所有的 SSA,同时也会生成一个交互式的 ssa.html 文件,我们用浏览器打开它。当你打开 ssa.html 时,将显示很多阶段,其中大部分都已折叠。start 阶段是从 AST 生成的SSA;lower 阶段将非机器特定的 SSA 转换为机器特定的 SSA,最后的 genssa 就是生成的机器代码。start 阶段的代码如下:b1: v1 = InitMem <mem> v2 = SP <uintptr> v3 = SB <uintptr> v4 = ConstInterface <interface {}> v5 = ArrayMake1 <[1]interface {}> v4 v6 = VarDef <mem> {.autotmp_0} v1 v7 = LocalAddr <[1]interface {}> {.autotmp_0} v2 v6 v8 = Store <mem> {[1]interface {}} v7 v5 v6 v9 = LocalAddr <[1]interface {}> {.autotmp_0} v2 v8 v10 = Addr <*uint8> {type.int} v3 v11 = Addr <*int> {”".statictmp_0} v3 v12 = IMake <interface {}> v10 v11 v13 = NilCheck <void> v9 v8 v14 = Const64 <int> [0] v15 = Const64 <int> [1] v16 = PtrIndex <interface {}> v9 v14 v17 = Store <mem> {interface {}} v16 v12 v8 v18 = NilCheck <void> v9 v17 v19 = IsSliceInBounds <bool> v14 v15 v24 = OffPtr <[]interface {}> [0] v2 v28 = OffPtr <*int> [24] v2If v19 → b2 b3 (likely) (line 6)b2: ← b1 v22 = Sub64 <int> v15 v14 v23 = SliceMake <[]interface {}> v9 v22 v22 v25 = Copy <mem> v17 v26 = Store <mem> {[]interface {}} v24 v23 v25 v27 = StaticCall <mem> {fmt.Println} [48] v26 v29 = VarKill <mem> {.autotmp_0} v27Ret v29 (line 7)b3: ← b1 v20 = Copy <mem> v17 v21 = StaticCall <mem> {runtime.panicslice} v20Exit v21 (line 6)这个简单的程序就已经产生了相当多的 SSA(总共35行)。然而,很多都是引用,可以消除很多(最终的SSA版本有28行,最终的机器代码版本有18行)。每个 v 都是一个新变量,可以点击来查看它被使用的位置。b 是块,这里有三块:b1,b2,b3。b1 始终会执行,b2 和 b3 是条件块,满足条件才执行。我们来看 b1 结尾处的 If v19 → b2 b3 (likely)。单击该行中的 v19 可以查看它定义的位置。可以看到它定义为 IsSliceInBounds <bool> v14 v15,通过 Go 编译器源代码,我们知道 IsSliceInBounds 的作用是检查 0 <= arg0 <= arg1。然后单击 v14 和 v15 看看在哪定义的,我们会看到 v14 = Const64 <int> [0],Const64 是一个常量 64 位整数。 v15 定义一样,放在 args1 的位置。所以,实际执行的是 0 <= 0 <= 1,这显然是正确的。编译器也能够证明这一点,当我们查看 opt 阶段(“机器无关优化”)时,我们可以看到它已经重写了 v19 为 ConstBool <bool> [true]。结果就是,在 opt deadcode 阶段,b3 条件块被删除了,因为永远也不会执行到 b3。下面来看一下 Go 编译器在把 SSA 转换为 机器特定的SSA 之后所做的另一个更简单的优化,基于amd64体系结构的机器代码。下面,我们将比较 lower 和 lowered deadcode。lower:b1: BlockInvalid (6)b2: v2 (?) = SP <uintptr> v3 (?) = SB <uintptr> v10 (?) = LEAQ <*uint8> {type.int} v3 v11 (?) = LEAQ <int> {”".statictmp_0} v3 v15 (?) = MOVQconst <int> [1] v20 (?) = MOVQconst <uintptr> [0] v25 (?) = MOVQconst <uint8> [0] v1 (?) = InitMem <mem> v6 (6) = VarDef <mem> {.autotmp_0} v1 v7 (6) = LEAQ <[1]interface {}> {.autotmp_0} v2 v9 (6) = LEAQ <[1]interface {}> {.autotmp_0} v2 v16 (+6) = LEAQ <*interface {}> {.autotmp_0} v2 v18 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2 v21 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2 v30 (6) = LEAQ <*int> [16] v2 v19 (6) = LEAQ <*int> [8] v2 v23 (6) = MOVOconst <int128> [0] v8 (6) = MOVOstore <mem> {.autotmp_0} v2 v23 v6 v22 (6) = MOVQstore <mem> {.autotmp_0} v2 v10 v8 v17 (6) = MOVQstore <mem> {.autotmp_0} [8] v2 v11 v22 v14 (6) = MOVQstore <mem> v2 v9 v17 v28 (6) = MOVQstoreconst <mem> [val=1,off=8] v2 v14 v26 (6) = MOVQstoreconst <mem> [val=1,off=16] v2 v28 v27 (6) = CALLstatic <mem> {fmt.Println} [48] v26 v29 (5) = VarKill <mem> {.autotmp_0} v27Ret v29 (+7)在HTML中,某些行是灰色的,这意味着它们将在下一个阶段中被删除或修改。例如,v15 (?) = MOVQconst <int> [1] 显示为灰色。点击 v15,我们看到它在其他地方都没有使用,而 MOVQconst 基本上与我们之前看到的 Const64 相同,只针对amd64的特定机器。我们把 v15 设置为1。但是,v15 在其他地方都没有使用,所以它是无用的(死的)代码并且可以消除。Go 编译器应用了很多这类优化。因此,虽然 AST 生成的初始 SSA 可能不是最快的实现,但编译器将SSA优化为更快的版本。 HTML 文件中的每个阶段都有可能发生优化。如果你有兴趣了解 Go 编译器中有关 SSA 的更多信息,请查看 Go 编译器的 SSA 源代码。这里定义了所有的操作以及优化。结论Go 是一种非常高效且高性能的语言,由其编译器及其优化支撑。要了解有关 Go 编译器的更多信息,源代码的 README 是不错的选择。 ...

September 27, 2018 · 8 min · jiezi