关于源码学习:Java-SPIDubbo-SPISpring-SPI-三种SPI分析

零碎整顿下Java SPI,Dubbo SPI,Spring SPI。 Java SPI简述有点相似于策略设计模式,定义好接口,在文件中写实现类的全路径名。调用ServiceLoader.load的时候返回一个迭代器,他外部是一个懒加载,当调用hasNext的时候才会依据全路径名读取文件,调用next的时候才会实例化。实质上就是,获取接口全路径名,安标准去该门路下按行读取文件,而后用同一个类加载器加载类,返回。(源码很简略,就不多说了,应用办法看图) 长处相似于模板办法,定义好接口,实现能够齐全由第三方来写,依照标准就行。 毛病人家本人也说了,一个简略的服务提供者的加载工具。人家源码开发者就没想写的多好用,就是写个demo秀操作呢。 Dubbo SPI简介官网:https://dubbo.apache.org/zh/d... 长处JDK 规范的 SPI 会一次性实例化扩大点所有实现,如果有扩大实现初始化很耗时,但如果没用上也加载,会很浪费资源。如果扩大点加载失败,连扩大点的名称都拿不到了。比方:JDK 规范的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败起因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不反对 ruby,而不是真正失败的起因。减少了对扩大点 IoC 和 AOP 的反对,一个扩大点能够间接 setter 注入其它扩大点。然而我跟了源码,不是一次性加载所有,是调用的时候创立(java 版本1.8.0_211) 剖析既然是增强,那其实实质上也是差不多的,就是获取到自定义实现类。我先看了下目录下的文件,是key,value的模式,那么猜想就是依据增强点应该就是依据key获取到指定的实现类。看了下源码,调用com.alibaba.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension的时候,就会进行解析对应的文件,后果如下 adaptive然而外面没有自适应的,比方ExtensionFactory获取的时候,解析到adaptive标签,会缓存到cachedAdaptiveClass如果没有获取到他会拼接字符串+编译的形式生成一个,代码如下package com.alibaba.dubbo.rpc;import com.alibaba.dubbo.common.extension.ExtensionLoader;public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);return extension.export(arg0);}public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class { if (arg1 == null) throw new IllegalArgumentException("url == null"); com.alibaba.dubbo.common.URL url = arg1; String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() ); if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])"); com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName); return extension.refer(arg0, arg1);}public void destroy() {throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");}public int getDefaultPort() {throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");}}而后就是依据url中的协定,去找到对应实现,对应的这一行 ...

March 19, 2022 · 1 min · jiezi

关于源码学习:源码学习思绪

源码学习思路Koa的核心文件一共有四个:application.js、context.js、request.js、response.js。所有的代码加起来不到 2000 行,十分轻便,而且大量代码集中在 request.js 和 response.js 对于请求头和响应头的处理,真正的核心代码只有几百行。 另外,为了更直观地梳理 koa 的运行原理和逻辑,还是通过调试来走一遍流程,本文将拆散调试源码进行分析。 以上面代码调试为例: const Koa = require('koa')const app = new Koa()const convert = require('koa-convert');// 日志中间件app.use(async(ctx, next) => { console.log('middleware before await'); const start = new Date() await next(); console.log('middleware after await'); const ms = new Date() - start console.log(${ctx.method} ${ctx.url} - ${ms}ms)})app.use(async(ctx, next) => { console.log('response'); ctx.body = "response"})app.listen(3000);复制代码node 的调试形式比较多,可参考 Node.js 调试大法稍做了解。 一、applicationapplication.js 是 koa 的入口文件,外面导出了 koa 的结构函数,结构函数中蕴含了 koa 的次要功能实现。 导出一个结构函数 Application,这个结构函数对外提供功能 API 方法,从次要 API 方法入手分析功能实现。 ...

January 5, 2022 · 5 min · jiezi

关于源码学习:聊聊读源码这件事

前言对于读源码这件事,每个人心中都有一个哈姆雷特,明天这篇文章聊聊我对读源码这件事的一点高见 为什么读源码读源码的时候,能够先问一下本人为什么读源码?是为了解决问题,还是单纯只是想理解源码的前因后果,更甚者仅仅只是为了面试,毕竟面试造火箭,不懂点源码,都没法去忽悠面试官 读源码首先要弄清楚本人的读源码的动机,以及通过读源码想达到什么目标 读源码的心态读源码是一件很干燥的事件,很多时候咱们会因为一时鸡血,翻下源码,而后发现跟看天书一样 如何让读源码不那么干燥,咱们能够先定一个小指标 咱们能够先把源码拆分成几个小章节,每浏览完,能够给本人处分下,比方玩把游戏,吃个大餐,或者干一下你想做却没做的事件,让大脑失去一个正向反馈,即我做完这个事件,我会失去一些益处。源码有时候干燥,是因为咱们没有失去一个踊跃的反馈,更多时候是 读源码不能抱有浮躁的心态,心急吃不了热豆腐 什么时候适宜读源码在我看来,读源码是建设在你对这个源码的编程语言曾经很相熟的状况,比方你想看k8s的源码,然而你对go语言无所不通,你就一头钻进去钻研,我想你大略前面的终局是 其次是你曾经对这个源码的基本功能有个分明的认知,晓得他的利用场景,能利用他的一些个性来做一些编码,比方你会应用spring的依赖注入,AOP等 最初你对这个源码曾经产生一点趣味,有钻研的欲望了,而不是他人强制你去读,外在的自驱力以及趣味,是做好事件的原动力 如何读源码源码分为两种状况 第一种:很多人曾经在钻研的源码像这种其实没必要一开始就钻进源码看,而是你能够针对你这个源码感兴趣的点,通过搜索引擎查找一下。你也能够通过processon搜一下相干的源码图,比方 不过这种有个不好中央,等于是他人把货色嚼碎了再喂给你,你想想那个画面。最初你还得通过源码验证一下,因为不确定他人剖析的是不是正确的,而且看源码的时候,必须得分外关注一下你看源码的版本,比方你看spring2.5和spring5的版本,会有发现会有很多区别。 间接通过搜索引擎找源码,有个益处就是你能够很快找到你感兴趣调试的源码入口,以及一些绝对比拟外围的代码块。 如果这种大家都钻研过的源码,你也不想通过搜寻找答案,就是只想速成,也有办法的,就是花钱 找个培训机构或者付费教程,基本上这些机构都会教一些支流技术的源码。如果你只想白嫖,也能够去B站搜一下 但不管怎样,你排汇这些后,最初还得本人去跟踪调试验证下,因为只有源码不会骗人 第二种:偏门或者新出的技术如果你钻研的货色,大家钻研比拟少,要么是这个技术不是支流技术,要么就是这个技术将来可能是支流,只是目前受众比拟少。 像这种源码,你也不必急着一开始就钻进源码,而是去官网溜一圈,看看这个技术的一些背景,个性,利用场景,入门案例。基本上每个技术的呈现,是要解决某些问题,而这种技术的呈现,失常会随同和竞品的比照,而这种竞品失常也是大家耳熟能详的货色。比方redisJson和mongdb的比照。对这个技术有个整体的理解后,你能够看下他们的代码仓是否有提供残缺的example或者单元测试,这些example失常提供就是这个技术的一个个性能点,你对这些性能点理解后,你就能够通过断点调试,通过调用栈查看相应的类。具体调试你就能够间接通过在单元测试或者提供的example那边打断点 总结看完源码后,最好能造成一张源码常识地图,以便后续查阅。 聊完源码后,我说一点题外话,原本这篇文章是对之前自定义spi文章的收尾总结,前面是因为有敌人说想晓得我是怎么读源码的,就罗唆写一篇文章。我很少写这种实践的货色,心愿对大家有一点帮忙。 上面贴一下我那个spi我的项目demo链接以及自定义spi相干的文章 demo链接https://github.com/lyb-geek/springboot-learning/tree/master/springboot-spi-enhance 自定义spi相干文章1、聊聊如何实现一个反对键值对的SPI 2、聊聊如何实现一个带有拦截器性能的SPI 3、聊聊自定义实现的SPI如何与spring进行整合 4、聊聊自定义SPI如何应用自定义标签注入到spring容器中 5、聊聊自定义SPI如何与sentinel整合实现熔断限流

December 21, 2021 · 1 min · jiezi

关于源码学习:如何阅读源码-以-Vetur-为例

全文近万字。。。来都来了,点个赞再走吧我很早就意识到,能纯熟、高效浏览开源前端框架源码是成为一个高级前端工程师必须具备的基本技能之一,所以在我职业生涯的最晚期,就曾经开始做了很屡次相干的尝试,但后果通常都以失败告终,起因形形色色: 不足必要的背景常识,犹如浏览天书不了解我的项目架构、设计理念,始终不得要领指标不够聚焦,浏览过程容易复杂化容易陷入细节,在不重要的问题上纠结半天容易追着分支流程跑,扩散注意力没有及时记录笔记和总结,没有把常识碾碎、重组、内化成本人的货色没有解决过特地简单问题的经验,潜在的不自信心理集体毅力、韧性有余,或者指标感不够强烈,遇到困难容易放弃等等这个列表还能够持续往下拉很长很长,总之既有我本人主观认知上的限度又有切切实实的客观原因。起初因为工作的契机硬着头皮看完 Vue 和 mxGraph 的源码,发现事件并没有本人设想中那么艰难,起初前前后后陆续看了很多框架源码,包含 Webpack、Webpack-sources、Vite、Eslint、Babel、Vue-cli、Vuex、Uniapp、Lodash、Vetur、Echarts、Emmet 等等,迟钝如我也缓缓摸索出了一些普适的形式办法,进而斗胆撰下这篇文章,不敢说授人以渔,但至多也该抛砖引玉吧。 所以这是一篇为哪些无意,或筹备,或曾经在浏览前端框架源码的同学而写的文章,我会在这里抛出一些通过我集体屡次实际总结进去的浏览技巧和准则,并联合 Vetur 源码,具体地解说我在浏览源码的各个阶段所思所想,心愿能给读者带来一些启发。 弄清楚指标在介绍具体的技巧之前,有必要先跟读者探讨一下浏览源码的动机,想分明到底需不需要通过这种形式晋升本身技能,尽管学习优良框架源码的确有十分多不言自明的益处,但每个人的教训、所处的语境、诉求、思维习惯不同,理论学习效果在不同个体或个体的不同期间必然存在极大的差别,这外面最大的变量一是教训,二是指标,教训因人而异,且很难在短时间内补齐,没有太多探讨空间;倒是指标方面值得盘道盘道。 第一层,先弄清楚为啥要浏览源码?可能的起因有很多,例如: 为了增进对框架的认知深度,晋升集体能力为了应答面试为了解决当下某个辣手的 bug 或性能问题基于某些起因,须要对框架做二次革新反正闲着,也不晓得该学点啥,试试呗。。。好奇这外面有一些很形象,例如最初一个“好奇”;有一些很具体,例如“为了做二次革新”;还有一些在具体与形象之间。依照 SMART 准则的说法,越具体、可掂量的指标越容易达成,如果读者的指标还处在比拟不置可否,不够具体具体的阶段,那执行过程大概率会翻车,毕竟这是一件特地耗费精力与耐性的活儿。 对于这种状况,我的倡议是无妨往更细节的档次再想一想,例如对于最初一点“好奇”,能够想想具体有哪些个性让你特地神奇,值得花工夫精力去粗疏地摸索,放在 Vetur 语境下能够是“我想理解 Vetur 的 template 谬误提醒与 eslint 如何联合在一起,实现模板层面的谬误提醒性能”,这就很具体很容易掂量了。 第二层,读者如果曾经有了明确、具体、可掂量的指标,无妨在开始之前先自问几个问题: 当下的确须要以浏览源码的形式增进本人对框架的认知深度吗?有没有一些更轻量级,迭代速度更快的学习形式?你所选定的框架,其复杂度、技术难度是否与你当下的能力匹配?最好的状态是你自认为踮踮脚就可能到,过高,不具备可行性;过低,ROI 不值当。如果通过这番斟酌之后,必要性、可行性、相关性都与集体指标符合,那就没啥可犹豫的。 第三层,须要辩证地去对待所谓“指标” —— 不是把整个我的项目残缺读完读通才叫胜利,如果能从一些语句、片段、部分模块中习得新的设计思维、工具办法,甚至仅仅是命名标准都能够算作集体的一点提高,千里之行;始于足下远比拔苗助长靠谱的多。所以一开始没必要把指标定的太高,能刚刚好满足本身需要是最好的,过程中如果发现问题域的复杂度在一直收缩变大,继续投入很多工夫却始终没有显著功效的话,那倡议果决放弃或者申请外援,从新评估指标与可行性之后再做决定。 总之,这是一个预期治理的问题,咱们能够多参考 SMART 准则,多从具体、可掂量、可行性、相关性几个维度思考,一直斟酌是否须要做这件事;如何拆解指标,用指标反推打算,一直推动集体胜利。 浏览技巧理解背景常识常识是造成了解的必要条件,开展学习任何一个开源我的项目之前都有必要花点工夫调研我的项目相干的基础知识,渐进构建起一套属于你本人的常识体系,这包含: 优质参考资料 —— 收集一波品质较高的学习材料,收集过程能够同步通读一遍框架是如何运行的 —— 也就是所谓的入口IO —— 框架如何与内部交互?它通常承受什么状态的运行参数?输入什么模式的后果?生态 —— 优良的框架背地通常都带有一套成熟的生态系统,例如 Vue,框架衍生品如何补齐框架自身的性能缺失?它们以何种形式,以什么样的 IO 与主框架交互?遵循怎么样的写法规定?如何断点调试 —— 这简直是最无效的分析方法,断点调试可能帮忙你粗疏地理解每一行代码的作用。留神,这里的指标是迅速构建起对于这个开源我的项目的形象 —— 甚至不太精确的常识框架,有意思地防止陷入无尽的细节中,就像在浏览一篇文章的时候,能够先看看目录构造粗略地理解文章的信息框架,理解文章大略内容。 例如,我刚开始学习 Vetur 的时候只晓得这是一个 VS Code 插件,但齐全不理解插件怎么写、怎么运行、怎么实现语言个性,所以我做的第一件事件是仔仔细细浏览 VS Code 的官网文档(所幸文档十分齐全,不像某驰名打包工具),学习对于插件开发的基本知识,包含: 进一步总结对于 VS Code 语言插件的因素: 怎么写插件:通过 package.json 文件的 contributes 、main 等属性,申明插件的性能与入口怎么运行:开发阶段应用 F5 启动调试怎么编写语言个性:应用 词法高亮、Language API、Language Server Protocol 三类技术实现VS Code 畛域的常识量还是很宏大的,学习背景常识并梳理成这种高度结构化、高度形象的脑图可能给你一个更高层、全面的视角,现实状态下,后续理论剖析源码的时候这些骨架脉络可能让你十分本能地映射到某一个切面的知识点,事倍功半。 ...

September 15, 2021 · 4 min · jiezi

关于源码学习:Nebula-Graph-源码解读系列-|-Vol00-序言

本文首发于 Nebula Graph Community 公众号 Nebula Graph 是由杭州欧若数网科技有限公司(官网:https://www.vesoft.com/cn/)开源的一款分布式图数据库,它次要用来解决随同着海量数据产生,在关联数据分析、开掘方面面临的新挑战。自 2019 年 5 月开源以来,Nebula Graph 受到了宽泛的关注,许多企业、技术团队、开发者将 Nebula Graph 利用到业务上构建常识图谱、风控、数据治理、反欺诈、实时举荐等场景。在 Nebula 社区中,越来越多用户从案例分享中把握 Nebula Graph 的应用办法,与此同时,呈现了一种声音,局部用户心愿能理解 Nebula Graph 背地的实现思路和原理。 在这样的背景下,咱们心愿通过 Nebula Graph 源码解读系列,剖析设计思路和实现原理,帮忙大家深刻理解 Nebula Graph,更好地应用 Nebula Graph,同样的,这也将有利于你和 Nebula Graph 社区一块共建更好的 Nebula Graph。 内容概述源码解读系列次要从 Nebula Graph 零碎架构和外围模块开展,此外针对社区用户关怀的架构限度带来的性能问题将在最初一个章节剖析此类问题。 源码解读系列虽名为源码解读,但并非只是对代码实现的剖析和函数解说,更侧重于从设计角度带你把握 Nebula Graph 实现原理,透过实现的代码来理解背地的设计思路。(因为 Nebula Graph 目前仍处于疾速迭代阶段,继续有新性能进入主分支,故局部一直迭代的外围模块的解说不会过多地深刻代码细节)。 目前源码解读系列章节布局如下: Nebula Graph Overview:带你理解 Nebula Graph 架构和代码仓散布、代码构造和模块布局;外围模块解说:讲述语义剖析、优化、调度等零碎模块,Java、Python 等各类客户端的运行原理;组件通信:讲述 Nebula Graph 中通信机制;2.0 新个性解说:从 Variable Length Pattern Match 和索引抉择两个点切入讲述 Match 实现的原理;架构限度和解决方案:针对社区的慢查问停止、超大点解决的问题讲述对应的解决方案;心愿大家读完本系列内容之后,对 Nebula Graph 有肯定的理解,明确 Nebula Graph 新性能的实现原理,遇到问题时能从实现角度更快定位问题解决问题。以及,在 Nebula Graph 仓库奉献代码时能更好地写出合乎 Nebula Graph 设计思路的代码。 ...

September 1, 2021 · 1 min · jiezi

关于WPF:WPF源码阅读-InkCanvas选中笔迹

本文接上一篇WPF源码浏览 -- InkCanvas抉择模式,本文介绍笔迹的抉择过程及选中后的高亮显示办法,文中若有了解谬误的中央,欢送大家斧正。抉择成果如下图所示: InkCanvas是WPF中用于墨迹书写的控件,其具备书写、抉择、擦除等模式。依据上图,能够看出笔迹的抉择性能由如下三局部组成: 抉择笔迹(Lasso Stroke)动静抉择选中后高亮显示本文将首先介绍抉择模式的激活过程,而后介绍如上三局部内容WPF是如何实现的。 抉择模式的激活从图中能够看出,切换到抉择模式后,鼠标按下挪动绘制的成果为黄色点状虚线(Lasso),依据Lasso及肯定的算法进行笔迹的选中与勾销选中。 先看InkCanvas切换到抉择模式后的动作。切换到抉择模式后,EditingMode扭转,调用OnEditingModeChanged办法,该办法调用RaiseEditingModeChanged办法。RaiseEditingModeChanged办法中,调用了_editingCoordinator.UpdateEditingState办法,并通过OnEditingModeChanged引发事件。 切换到EditingCoordinator类,能够看到顺次调用UpdateEditingState() -> ChangeEditingBehavior() -> PushEditingBehavior()。UpdateEditingState办法调用GetBehavior办法拿到新Behavior(SelectionEditor)。PushEditingBehavior办法会撤消之前的Behavior,并激活新的Behavior,即SelectionEditor会被激活。 切换到SelectionEditor类,在其OnActivate办法中监听了事件,在OnAdornerMouseButtonDownEvent办法中,调用了EditingCoordinator.ActivateDynamicBehavior办法激活了LassoSelectionBehavior。至此,抉择模式已激活,并将随着设施挪动绘制Lasso。 // InkCanvasprivate static void OnEditingModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){ ( (InkCanvas)d ).RaiseEditingModeChanged( new RoutedEventArgs( InkCanvas.EditingModeChangedEvent, d));}private void RaiseEditingModeChanged(RoutedEventArgs e){ Debug.Assert(e != null, "EventArg can not be null"); _editingCoordinator.UpdateEditingState(false /* EditingMode */); this.OnEditingModeChanged(e);}// EditingCoordinatorinternal void UpdateEditingState(bool inverted){ // ... EditingBehavior newBehavior = GetBehavior(ActiveEditingMode); ... ChangeEditingBehavior(newBehavior); // ...}private void ChangeEditingBehavior(EditingBehavior newBehavior){ // ... PushEditingBehavior(newBehavior); // ...}private void PushEditingBehavior(EditingBehavior newEditingBehavior){ // ... behavior.Deactivate(); ... newEditingBehavior.Activate();}// SelectionEditorprivate void OnAdornerMouseButtonDownEvent(object sender, MouseButtonEventArgs args){ // ... EditingCoordinator.ActivateDynamicBehavior(EditingCoordinator.LassoSelectionBehavior, args.StylusDevice != null ? args.StylusDevice : args.Device);}抉择笔迹(Lasso Stroke)LassoSelectionBehavior继承自StylusEditingBehavior,随着设施的挪动,会调用AddStylusPoints办法。该办法会调用StylusInputBegin、StylusInputContinue办法。StylusInputBegin会调用StartLasso办法,该办法创立了LassoHelper对象,该对象将绘制Lasso。 ...

April 6, 2021 · 2 min · jiezi

关于源码学习:ioredis源码阅读0

最近因为工作须要,要去搞一个 Node.js 端的 Redis Client 组件进去,临时抉择通过 ioredis 来作为 fork 对象。 因为之前有遇到过 Redis 在应用 twemproxy 时会始终呈现无奈连贯服务器的问题,详情见 issues:https://github.com/luin/ioredis/issues/573 所以会批改源码批改这一问题,不过在批改实现之后跑单元测试发现,事件没有那么简略,并不只是 info -> ping 这样,所以只好去相熟源码,而后针对性地调整一下逻辑。<!--more--> ioredis 我的项目构造从我的项目中看,源码都在 lib 文件夹下,是一个纯正的 TS 我的项目。 lib 目录下的文件次要是一些通用能力的提供,比方 command、pipeline以及数据的传输等。 .├── DataHandler.ts # 数据处理├── ScanStream.ts├── SubscriptionSet.ts├── autoPipelining.ts├── cluster # Redis Cluster 模式的实现│   ├── ClusterOptions.ts│   ├── ClusterSubscriber.ts│   ├── ConnectionPool.ts│   ├── DelayQueue.ts│   ├── index.ts│   └── util.ts├── command.ts # 命令的具体实现├── commander.ts # command 的调度方├── connectors # 网络连接相干│   ├── AbstractConnector.ts│   ├── SentinelConnector│   ├── StandaloneConnector.ts│   └── index.ts├── errors # 异样信息相干│   ├── ClusterAllFailedError.ts│   ├── MaxRetriesPerRequestError.ts│   └── index.ts├── index.ts # 入口文件├── pipeline.ts # 管道逻辑├── promiseContainer.ts # Promise 的一个封装├── Redis # `Redis 实例的实现`│   ├── RedisOptions.ts│   ├── event_handler.ts│   └── index.ts├── script.ts├── transaction.ts├── types.ts└── utils # 一些工具函数的实现 ├── debug.ts ├── index.ts └── lodash.ts而下分的两个文件夹,redis 与 cluster 都是具体的 redis client 实现,cluster 是对应的 cluster 集群化实现。 所以在看 README 的时候咱们会发现有两种实例能够应用,https://www.npmjs.com/package/ioredis ...

December 15, 2020 · 6 min · jiezi

AtomicReference源码学习

接着前两篇的AtomicBoolean和AtomicInteger再来看看AtomicReference类上的注释说明:An object refenrence that may be updated atomically.用来原子更新对象的引用。一、AtomicRefenrence属性 private static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;private volatile V value;static{ try{ valueOffset = unsafe.objectFieldOffset (AtomicReference.class.getDeclaredField("value")); }catch(Exception e){ throw new Error(ex); }}二、构造器 /***Creates a new AtomicReference with the given initial value*/public AtomicReference(V initialValue){ value = initialValue;}/*** Creates a new AtomicReference with null initail value*/public AtomicReference(){}除了上面的属性、构造器外,AtomicReference与AtomicBoolean、AtomicInteger在方法上都差不多,这里只提一个方法: /***Atomically sets to the given value and returns the old value.*/public final V getAndSet(V newValue){ return(V)unsafe.getAndSetObject(this,valueOffset,newValue);}这个方法同AtomicInteger一样是在Unsafe类里进行自旋,与AtomicBoolean略有不同。 ...

July 5, 2020 · 1 min · jiezi

SOFAJRaft-日志复制-pipeline-实现剖析-SOFAJRaft-实现原理

SOFAStack(Scalable Open Financial  Architecture Stack)是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 本文为《剖析 | SOFAJRaft 实现原理》第六篇,本篇作者徐家锋,来自专伟信息,力鲲,来自蚂蚁金服。《剖析 | SOFAJRaft 实现原理》系列由 SOFA 团队和源码爱好者们出品,项目代号:<SOFA:JRaftLab/>,文章尾部有参与方式,欢迎同样对源码热情的你加入。 SOFAJRaft :https://github.com/sofastack/sofa-jraft 本文的目的是要介绍 SOFAJRaft 在日志复制中所采用的 pipeline 机制,但是作者落笔时突然觉得这个题目有些唐突,我们不应该假设读者理所应当的对日志复制这个概念已经了然于胸,所以作为一篇解析,我觉得还是应该先介绍一下 SOFAJRaft 中的日志复制是要解决什么问题。 概念介绍SOFAJRaft 是对 Raft 共识算法的 Java 实现。既然是共识算法,就不可避免的要对需要达成共识的内容在多个服务器节点之间进行传输,在 SOFAJRaft 中我们将这些内容封装成一个个日志块 (LogEntry),这种服务器节点间的日志传输行为在 SOFAJRaft 中也就有了专门的术语:日志复制。 为了便于阅读理解,我们用一个象棋的故事来类比日志复制的流程和可能遇到的问题。 假设我们穿越到古代,要为一场即将举办的象棋比赛设计直播方案。当然所有电子通讯技术此时都已经不可用了,幸好象棋比赛是一种能用精简的文字描述赛况的项目,比如:“炮二平五”, “马8进7”, “车2退3”等,我们将这些描述性文字称为棋谱。这样只要我们在场外同样摆上棋盘 (可能很大,方便围观),通过棋谱就可以把棋手的对弈过程直播出来。 图1 - 通过棋谱直播 所以我们的直播方案就是:赛场内两位棋手正常对弈,设一个专门的记录员来记录棋手走出的每一步,安排一个旗童飞奔于赛场内外,棋手每走一步,旗童就将其以棋谱的方式传递给场外,这样观众就能在场外准实时的观看对弈的过程,获得同观看直播相同的体验。 图2 - 一个简单的直播方案 这便是 SOFAJRaft 日志复制的人肉版,接下来我们完善一下这个“直播系统”,让它逐步对齐真实的日志复制。 改进1. 增加记录员的数量假设我们的比赛获得了很高的关注度,我们需要在赛场外摆出更多的直播场地以供更多的观众观看。 这样我们就要安排更多的旗童来传递棋谱,场外的每一台直播都需要一个旗童来负责,这些旗童不停的在赛场内外奔跑传递棋谱信息。有的直播平台离赛场远一些,旗童要跑很久才行,相应的直播延迟就会大一些,而有些直播平台离得很近,对应的旗童就能很快的将对弈情况同步到直播。 随着直播场地的增加,负责记录棋局的记录员的压力就会增加,因为他要针对不同的旗童每次提供不同的棋谱内容,有的慢有的快。如果记录员一旦记混了或者眼花了,就会出现严重的直播事故(观众看到的不再是棋手真正的棋局)。 图4 - 压力很大的记录员 ...

August 7, 2019 · 2 min · jiezi

createreactapp-源码学习上

原文地址Nealyang/personalBlog前言对于前端工程构建,很多公司、BU 都有自己的一套构建体系,比如我们正在使用的 def,或者 vue-cli 或者 create-react-app,由于笔者最近一直想搭建一个个人网站,秉持着呼吸不停,折腾不止的原则,编码的过程中,还是不想太过于枯燥。在 coding 之前,搭建自己的项目架构的时候,突然想,为什么之前搭建过很多的项目架构不能直接拿来用,却还是要从 0 到 1 的去写 webpack 去下载相关配置呢?遂!学习下 create-react-app 源码,然后自己搞一套吧~ create-react-app 源码代码的入口在 packages/create-react-app/index.js下,核心代码在createReactApp.js中,虽然有大概 900+行代码,但是删除注释和一些友好提示啥的大概核心代码也就六百多行吧,我们直接来看 index.js index.js 的代码非常的简单,其实就是对 node 的版本做了一下校验,如果版本号低于 8,就退出应用程序,否则直接进入到核心文件中,createReactApp.js中 createReactApp.jscreateReactApp 的功能也非常简单其实,大概流程: 命令初始化,比如自定义create-react-app --info 的输出等判断是否输入项目名称,如果有,则根据参数去跑安装,如果没有,给提示,然后退出程序修改 package.json拷贝 react-script 下的模板文件准备工作:配置 vscode 的 debug 文件 { "type": "node", "request": "launch", "name": "CreateReactApp", "program": "${workspaceFolder}/packages/create-react-app/index.js", "args": [ "study-create-react-app-source" ] }, { "type": "node", "request": "launch", "name": "CreateReactAppNoArgs", "program": "${workspaceFolder}/packages/create-react-app/index.js" }, { "type": "node", "request": "launch", "name": "CreateReactAppTs", "program": "${workspaceFolder}/packages/create-react-app/index.js", "args": [ "study-create-react-app-source-ts --typescript" ] }这里我们添加三种环境,其实就是 create-react-app 的不同种使用方式 ...

June 18, 2019 · 7 min · jiezi

Go-Redigo-源码分析一-实现Protocol协议请求redis

概述Redis是我们日常开发中使用的最常见的一种Nosql,是一个key-value存储系统,但是redis不止支持key-value,还自持很多存储类型包括字符串、链表、集合、有序集合和哈希。在go使用redis中有很多的开源库可以使用,我经常使用的是redigo这个库,它封装很多对redis的api、网络链接和连接池。分析Redigo之前我觉得需要知道如果不用redigo,我们该如何访问redis。之后才能更加简单方便的理解Redigo是做了一些什么事。 Protocol协议官方对protocol协议的定义:链接 网络层:客户端和服务端用通过TCP链接来交互 请求*<参数数量> CR LF$<参数 1 的字节数量> CR LF<参数 1 的数据> CR LF...$<参数 N 的字节数量> CR LF<参数 N 的数据> CR LF举个例子 get aaa = *2rn$3\r\nget\r\n$3rn$aaarn每个参数结尾用rn $之后是参数的字节数 这样组成的一串命令通过tcp发送到redis服务端之后就是redis的返回了返回Redis的返回有5中情况: 状态回复(status reply)的第一个字节是 "+"错误回复(error reply)的第一个字节是 "-"整数回复(integer reply)的第一个字节是 ":"批量回复(bulk reply)的第一个字节是 "$"多条批量回复(multi bulk reply)的第一个字节是 "*"下面按照5中情况各自举一个例子 状态回复:请求: set aaa aaa 回复: +OKrn 错误回复:请求: set aaa 回复: -ERR wrong number of arguments for 'set' commandrn整数回复:请求:llen list 回复::5rn批量回复请求: get aaa回复: $3rnaaarn多条批量回复请求: lrange list 0 -1回复: *3rn$3\r\naaa\r\n$3rndddrn$3rncccrn ...

May 25, 2019 · 3 min · jiezi

为什么建议你常阅读源码

作者:谢伟授权 LeanCloud 转载 我叫谢伟,是一名侧重在后端的程序员,进一步定位现阶段是 Web 后台开发。 由于自身智力一般,技术迭代又非常快,为不至于总处于入门水平,经常会尝鲜新技术。 为保持好奇心,日常除技术以外,还会涉猎摄影、演示设计、拍视频、自媒体写作等。 如果此刻我是一个成功人士,看到上面的领域,有人会羡慕说:「斜杠」,遗憾的是,在下没有成功,所以,上面的领域都一定程度上会被人认为:「不务正业」,不过不重要,我本职还是一名后端程序员。 记忆记忆有遗忘曲线,这是大家都懂的道理,所以为了防止忘记,最重要的方法是经常使用、反复使用。这也是为什么,有些人说:在工作中学最容易进步。因为工作的流程、项目不会频繁变动,你会经常性的关注一个或者多个项目进行开发,假以时日,你会越来越熟悉,理所当然,你会越做越快。这个时候,就达到了所谓的:舒适圈。要再想进步,你得跳到「学习区」。再反复这个动作。 问题是,除了工作之外,你很少有其他机会再进行技能锻炼了。 创造机会主动承接更为复杂的任务这个比较容易理解,因为更为复杂的任务,你才可能尝试使用新的技术栈,有机会进行其他技能的锻炼,这样就能进入「学习区」。 如果公司项目就这么点,没有太复杂的,或者说新项目和你接触的相差不多,只不过应用场景不同而已。这个时候,任务如果一定需要你的参与,你最好尝试新的架构,尝试新的技术点,尽管大体相同,可以将你认为原系统不合理的地方改进,这样也能创造机会进入「学习区」。 但就我认为,一般项目开发时间都非常紧,开发人员有可能没有充足的时间进行考虑,会依然使用原有技术点,这样进入学习区的机会就被浪费了,你只是使用一份经验,做了两个类型的项目而已。 旧知识补全刚进入职场,核心位置就那么几个人占着,论经验、论资历,你都不如别人,你接触到的资源有限,没有新项目让你独立开发,只有旧项目的 Bug 让你修复,那该怎么办? 换坑吗?怕不怕另外一个也是坑? 补知识体系即使是你能完成的任务,你有没有尝试过自己独立写一个,你有没有尝试过自己弥补下不懂的知识点,你有没有尝试过总结下自己的开发流程是否是最优的,你有没有尝试过总结下项目的技术要点,你有没有尝试过提炼可以复用的技术点... 如果你都没有,恭喜你,你又找到了一个进入「学习区」的点,即:补充原有技术栈。 也许你工作中已经有一门常用编程语言,但都是靠 Google、StackOverFlow,你是不是要尝试梳理下整个编程语言的知识体系,当然梳理的切入点依然是和工作相关为先,因为这最迫切,最能反复,使用频率最高。 也许你对数据库相关知识略懂,对优化数据知识点却不是很懂,你是不是要尝试下找相关资料弥补下。 也许... 也许你还可以翻阅源码,比如内置库的实现,之前我还不太会关注这些,写起代码来不是很有底气,后来经常性的查看源码,借助 IDE 的跳转功能实现对源码的阅读,再结合 IDE 的 structure ,可以对文件的函数、结构体、方法等进行组织。这样从整体观看一目了然,看得多了,你甚至可以总结出一些共性: 比如包的错误处理一般定义在包的顶部几行,而且格式都统一比如 Interface 是方法的集合,内置的常用的 Interface 其实不多,很多内置包都相互实现比如包的结构体,可以实例化一个默认的,这样可以直接调用函数,比如 http.DefaultClient...阅读库的源码,我一般是怎么做的呢?(不要太关注具体的实现,除非你完全能看懂) 官方文档:了解常用使用方法思维导图:输出可导出的结构体、函数、方法等,依然选择最为常用的IDE 的 structure 功能,查看文件的具体组织形式,看可导出的结构体、函数、方法等持续总结举个例子net/http 包几乎奠定了 Go 领域所有 Web 框架、网络请求库的基础。由此来看下我是如何梳理的。 了解 HTTP 相关知识随意找本相关的书,发现是个大块知识啊。结合一般的历史经验,你可能作出这么张思维导图。 整个过程像是:你从一本书总摘出的目录,前提是看过书的内容而得出来的。 net/http 客户端网络请求分为两个层面: 客户端发起网络请求服务端提供网络请求访问资源func getHandle(rawString string) { response, err := http.Get(rawString) if err != nil { return } defer response.Body.Close() content, _ := ioutil.ReadAll(response.Body) fmt.Println(string(content))}看上去发起网络请求很简单,只需要使用 http.Get 即可。 ...

May 22, 2019 · 2 min · jiezi

Go-Gin源码学习五

Gin路由主要流程实现经过上一篇的学习笔记,我们已经知道了Gin router的主要流程。但是我们看到代码和方法体总体很长,其中大部分是参数路由的判断。这些零散的小逻辑,让我们阅读源码的时候更难理解了一些。但是其实基数树的逻辑兵没有这么的复杂,所以我们还是按照老规矩,自己实现以下这个简单的基数树值包含主流程。代码如下: package myginimport "fmt"type Trees map[string]*nodetype node struct { path string indices string children []*node handlers HandlerList}func (n *node) addRoute(path string, handlers HandlerList) { if len(n.path) > 0 || len(n.children) > 0 { walk: for { //找到相等的index i := 0 max := min(len(path), len(n.path)) for max > i && path[i] == n.path[i] { i++ } //需要把原来的作为子node放到新node中 if i < len(n.path) { //新建node child := node{ path: n.path[i:], indices: n.indices, handlers: n.handlers, children: n.children, } n.children = []*node{&child} n.indices = string([]byte{n.path[i]}) n.path = path[:i] n.handlers = nil } // 判断子节点如果有相同开头的字符 则从新跳入循环 if i < len(path) { c := path[i] for index := 0; index < len(n.indices); index++ { if c == n.indices[index] { n = n.children[index] path = path[i:] continue walk } } //把新请求的path加入到router中 n.insertChild(path[i:], path, handlers, i) return } return } } else { //如果为空 n.path = path n.handlers = handlers }}func (n *node) insertChild(path, fullPath string, handlers HandlerList, index int) { child := node{} child.handlers = handlers child.indices = "" child.path = path n.indices += string([]byte{fullPath[index]}) n.children = append(n.children, &child)}func min(a, b int) int { if a > b { return b } return a}func (n *node) getValue(path string) (handlers HandlerList) { index := 1walk: for { fmt.Println("loop num: ", index) if len(path) > len(n.path) { path = path[len(n.path):] c := path[0] for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { n = n.children[i] index++ goto walk } } } else if len(path) == len(n.path) { handlers = n.handlers return } }}总结上面的代码已经不需要太多的注释了,去掉了参数节点的代码整个流程已经很明确了。 ...

May 15, 2019 · 2 min · jiezi

Go-Gin源码学习四

基数树这次学习的是Gin中的路由,在学习源码一种我们看到了Gin的路由是它的特色。然而基础数据使用了基数树也提供了性能的保障。因为路由这部分比较独立而且逻辑相对复杂,所以需要单独学习。首先我们需要了解的是基数树,百度百科中的解释其中有一个图可以让我们更加直观的看到数据是如何存储的。基数树,相当于是一种前缀树。对于基数树的每个节点,如果该节点是确定的子树的话,就和父节点合并。基数树可用来构建关联数组。在上面的图里也可以看到,数据结构会把所有相同前缀都提取 剩余的都作为子节点。 基数树在Gin中的应用从上面可以看到基数树是一个前缀树,图中也可以看到数据结构。那基数树在Gin中是如何应用的呢?举一个例子其实就能看得出来router.GET("/support", handler1)router.GET("/search", handler2)router.GET("/contact", handler3)router.GET("/group/user/", handler4)router.GET("/group/user/test", handler5)最终的内存结构为: / (handler = nil, indices = "scg") s (handler = nil, indices = "ue") upport (handler = handler1, indices = "") earch (handler = handler2, indices = "") contact (handler = handler3, indices = "") group/user/ (handler = handler4, indices = "u") test (handler = handler5, indices = "")可以看到 router使用get方法添加了5个路由,实际存储结果就是上面显示的。我特地在后面加上了每个节点中的handler和indices。 indices是有序保存所有子节点的第一个字符形成的字符串。为什么要特意突出这个字段,因为在查找子节点下面是否包含path的时候不需要循环子节点,只需要循环这个字段就可以知道是否包含。这样的操作也可以提升一些效率。 源码查看先看一下节点的对象的定义和如何调用的,需要注意的是indices这个字段 上面已经提到了它的作用 type node struct { // 保存这个节点上的URL路径 // 例如上图中的search和support, 共同的parent节点的path="s" // 后面两个节点的path分别是"earch"和"upport" path string // 判断当前节点路径是不是参数节点, 例如上图的:post部分就是wildChild节点 wildChild bool // 节点类型包括static, root, param, catchAll // static: 静态节点, 例如上面分裂出来作为parent的s // root: 如果插入的节点是第一个, 那么是root节点 // catchAll: 有*匹配的节点 // param: 除上面外的节点 nType nodeType // 记录路径上最大参数个数 maxParams uint8 // 和children[]对应, 保存的是分裂的分支的第一个字符 // 例如search和support, 那么s节点的indices对应的"eu" // 代表有两个分支, 分支的首字母分别是e和u indices string // 保存孩子节点 children []*node // 当前节点的处理函数 handle Handle // 优先级 priority uint32}//RouterGrou实现的GET方法调用了handlerfunc (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("GET", relativePath, handlers)}func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { //方法计算出路径,把group中的basepath和relativepath 合并在一起 absolutePath := group.calculateAbsolutePath(relativePath) //合并handler 把group中添加的中间件和传入的handlers合并起来 handlers = group.combineHandlers(handlers) //调用addRoute 添加router group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj()}接下来我们需要看的是addRoute这个方法了,方法体比较长。其实大多的逻辑都在处理带参数的节点,真正核心的逻辑其实并不多。我把主要的逻辑都写上了注释应该还是比较容易理解的。如果看不懂其实一步步debug几次也能帮助理解。 ...

May 12, 2019 · 8 min · jiezi

Go-Gin源码学习三

学习目标在第一篇中看到了Gin提供了很多的获取和解析参数的方法: // **** 输入数据//从URL中拿值,比如 /user/:id => /user/johnParam(key string) string //从GET参数中拿值,比如 /path?id=johnGetQueryArray(key string) ([]string, bool) GetQuery(key string)(string, bool)Query(key string) stringDefaultQuery(key, defaultValue string) stringGetQueryArray(key string) ([]string, bool)QueryArray(key string) []string//从POST中拿数据GetPostFormArray(key string) ([]string, bool)PostFormArray(key string) []string GetPostForm(key string) (string, bool)PostForm(key string) stringDefaultPostForm(key, defaultValue string) string// 文件FormFile(name string) (*multipart.FileHeader, error)MultipartForm() (*multipart.Form, error)SaveUploadedFile(file *multipart.FileHeader, dst string) error// 数据绑定Bind(obj interface{}) error //根据Content-Type绑定数据BindJSON(obj interface{}) errorBindQuery(obj interface{}) error//--- Should ok, else return errorShouldBindJSON(obj interface{}) error ShouldBind(obj interface{}) errorShouldBindJSON(obj interface{}) errorShouldBindQuery(obj interface{}) error其中从url中获取 从get参数中获取 从post拿数据相信我们都可以想象的到,基本就是从request中的url或者body中获取数据然后返回但是其中的数据绑定我自己开始是很疑惑的,到底是怎么实现的。疑惑的是如果object中我客户端少输入了参数 或者多输入的参数会是怎么样。举个例子: ...

May 10, 2019 · 2 min · jiezi

Go-Gin源码学习一

Gin的基本使用Gin是一个比较轻量级的http框架,主要是提供了几个便于使用的功能: 简单的中间件注册,可以很方便的实现通用中间件的使用注册提供了比较方便和全面的路由注册,方便的实现RESTful接口的实现提供了便捷的获取参数的方法,包括get、post兵可以可以把数据直接转换成对象对路由的分组,Gin可以对一组路由做统一的中间件注册等操作可以手机所有错误,统一在统一的地方写日志性能方面: 是路由的基础数据格式为基数树没有使用反射,所以性能方面也是比较低消耗内存低上下文context使用了对象池,fasthttp中也同样使用了sync.pool使用方便也是比较简单的,下面有一个很简单的例子 package mainimport ( "fmt" "github.com/gin-gonic/gin" "test/gin/middleware/model")func main() { //创建router router := gin.Default() //创建组 group := router.Group("/api") //为组加中间件 group.Use(func(context *gin.Context) { fmt.Println("api group url:", context.Request.URL.String()) }) //为组加路由方法 group.GET("/test", func(context *gin.Context) { context.JSON(200, model.Message{Message:"ok"}) }) //运行 router.Run(":3333")}例子中是一个最简单的Gin框架的应用。创建了一个engine,创建了组并且为组添加了中间件之后在这个group下的路由方法都将使用这个中间件,方便对api最系统的管理对不同的api做不同的处理。在Terminal中访问可以看到下面的结果 curl http://localhost:3333/api/test{"Message":"ok"}panleiMacBook-Pro:test 主要流程的源码首先我们先看例子中的gin.Default() 返回的engine对象,这是Gin的主要对象。只对最主要的属性加了注释 type Engine struct { //路由组 RouterGroup RedirectTrailingSlash bool RedirectFixedPath bool HandleMethodNotAllowed bool ForwardedByClientIP bool AppEngine bool UseRawPath bool UnescapePathValues bool MaxMultipartMemory int64 delims render.Delims secureJsonPrefix string HTMLRender render.HTMLRender FuncMap template.FuncMap allNoRoute HandlersChain allNoMethod HandlersChain noRoute HandlersChain noMethod HandlersChain // 对象池 用来创建上下文context pool sync.Pool //记录路由方法的 比如GET POST 都会是数组中的一个 每个方法对应一个基数树的一个root的node trees methodTrees}然后我们来看Default方法,其实很简单就是创建一个engine对象并且添加默认的两个中间件,一个是做log显示,显示每次请求可以再console中看到。每次创建engine对象的时候回默认的添加一个routergroup地址为默认的"/" 代码如下: ...

May 7, 2019 · 3 min · jiezi

Golang-源码探究strings

golang源码探究-stringsContain()func Contains(s, substr string) boolContains()返回一个布尔值,若substr存在于s中,则返回true,不存在则返回false。 // Contains reports whether substr is within sfunc Contains(s, substr string) bool { return Index(s, substr) >= 0}Index()我们再来看Index(),func Index(s, substr string) intIndex()返回substr出现在原始string s 中的 位置,如果s中meiyousubstr,则返回-1 // Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.func Index(s, substr string) int { n := len(substr) //先获取substr的长度 赋给n switch { case n == 0: //如果 substr的长度为0 ,则返回0, return 0 case n == 1: return IndexByte(s, substr[0]) // 后面再看一下IndexByte()的源码 case n == len(s): // 如果 s和substr长度相等,直接判断俩字符串是否一模一样 if substr == s { return 0 } return -1 case n > len(s): // 如果 substr的长度大于s的长度,那肯定不存在了,返回-1,说明substr不存在于s中 return -1 case n <= bytealg.MaxLen: // 后面得看bytealg.MaxLen // Use brute force when s and substr both are small if len(s) <= bytealg.MaxBruteForce { // const型 :const MaxBruteForce = 64 return bytealg.IndexString(s, substr) } c0 := substr[0] c1 := substr[1] i := 0 t := len(s) - n + 1 fails := 0 for i < t { if s[i] != c0 { // IndexByte is faster than bytealg.IndexString, so use it as long as // we're not getting lots of false positives. o := IndexByte(s[i:t], c0) if o < 0 { return -1 } i += o } if s[i+1] == c1 && s[i:i+n] == substr { return i } fails++ i++ // Switch to bytealg.IndexString when IndexByte produces too many false positives. if fails > bytealg.Cutover(i) { r := bytealg.IndexString(s[i:], substr) if r >= 0 { return r + i } return -1 } } return -1 } c0 := substr[0] c1 := substr[1] i := 0 t := len(s) - n + 1 fails := 0 for i < t { if s[i] != c0 { o := IndexByte(s[i:t], c0) if o < 0 { return -1 } i += o } if s[i+1] == c1 && s[i:i+n] == substr { return i } i++ fails++ if fails >= 4+i>>4 && i < t { // See comment in ../bytes/bytes_generic.go. j := indexRabinKarp(s[i:], substr) if j < 0 { return -1 } return i + j } } return -1}internel/bytealg中:MaxLen is the maximum length of the string to be searched for (argument b) in Index. ...

May 2, 2019 · 5 min · jiezi

Golang channel 源码分析

之前知道go团队在实现channel这种协程间通信的大杀器时只用了700多行代码就解决了,所以就去膜拜读了一把,但之后复盘总觉得多少有点绕,直到有幸找到一个神级PPT https://speakerdeck.com/kavya… 生动形象的解释了channel底层是怎么工作和实现的,于是就带着这篇PPT再来复盘一遍channel的源码Hchan 数据结构初始化make(chan task, 3)初始化channel在调用方有两种, 一种是带缓冲的一种是非缓冲的,其初始化的具体实现除了缓冲非缓冲,还分channel的元素是否是指针类型Send满足send条件下往这个channel发送数据的代码, 假设当前没有另一个goroutine来接收channel的数据G1:for task := range tasks { ch <- task}Send to a full channel当channel满了之后 c.qcount > c.dataqsiz 如果还有数据发送到该channel则获取当前运行的goroutine封装成sudog,将其插入sendq 队列并通知系统将当前goroutine停止此时hchan的结构大致长这样sendq 和 recvq 都是一个由链表实现的FIFO队列这里涉及到三个没见过的东西1.sudogsudog 是对当前运行的goroutine和需要发送数据的封装,有一个前驱指正和后驱指针,hchan的sendq和recvq队列则是由sudog形成的双向链表2.goparkunlock —> goparkgopark 将当前goroutine置为等待状态3.goready —> readygoready 将某个goroutine 唤醒释放阻塞的sender goroutine 上面说到,channel容量已满后, 会阻塞当前goroutine并加到发送队列中, 那么什么时候会释放这个阻塞的goroutine呢。 之前看channel的学习文章时都说 发送者和接受者必须是成双成对的 (现在理解为一个gopark, 一个goready),在下面channel的接收端代码中可以看到因为当从channel中接收数据时, 如果sendq队列上有等待的的goroutine, 则将它pop出来, 执行接收操作(一会儿再讲)后调用goready将其唤醒这里可以看到 虽然 golang 有一句名言叫做 “Do not communicate by sharing memory; instead, share memory by communicating.” 告诉我们用通信的方式来共享内存而不是用共享内存的方式来通信,在channel的内部, 接收者和发送者两个goroutine却是通过共享hchan来实现通信的 (但是发送和接收的数据是通过拷贝来传递的)。send channel 小结当hchan 上没有等待的接收队列 (recvq) 的情况下, 往channel 发送数据可以总结成以下步骤hchan 上锁判断当前hchan 是否有足够的buf空间如果有, 拷贝数据到buf中对应的位置如果buf空间不够,或者初始化的是无缓冲channel, 阻塞当前goroutine并将其封装成sudog插到sendq中等待被接受者唤醒hchan 解锁这里只列出了当“hchan 的接收者队列上没有等待的goroutine” 时这种情况, 因为在上一句打引号的的情况中有一种之后需要解释的骚操作。Rcevchannel 的接收实现实质上和发送区别不大, 如果当前没有阻塞等待发送的goroutine 并且buf中有数据, 则从buf中将当前recvx索引初将需要接收的数据拷贝出来, 然后将其在buf中清除Recv from Sender and wakeup Sender如果在从channel接收时,发送队列上有正在阻塞等待的goroutine, 就是上一节中提到的send groutine如何被唤醒的那块内容, 拷贝 + 唤醒Recv from empty channel如果当前无阻塞等待发送数据的goroutine, 并且buf中没有等待接收的数据, 则同send一样,将当前的goroutine, 需要接收的数据指针,封装成sudog插入recvQ队列尾部, 调用gopark停止当前goroutine上一节说到, 发送端在接收队列中无阻塞等待的goroutine时会阻塞并插到sendq队列中,并留下了一个悬链说当接收队列上有goroutine时会发生一个骚操作。按上面的代码来看,这种情况接受者收到的数据也应该是从sendq中取出发送方的sudog并将其发送的值拷贝出来,但是在channel的实现中,当往一个 ”空buf(或者非缓冲)但是接收者队列上有阻塞goroutine的” channel发送数据时, 发送方会直接把数据写到接收队列中那个等待接收的goroutine中。比起等接收者从buf中拷贝数据或者从sendq队列中pop出sudog再拷贝数据,这样做少了一次拷贝的过程非正常情况下的sender, recver未初始化的channel往已经关闭的channel发送数据从已关闭的channel接受数据LAST带着这篇PPT来看channel的源码感觉一切都一目了然了, 反正这篇PPT一定要看,而且里面还包含了channel在阻塞goroutine时 go调度器运行状态的描述。 ...

April 15, 2019 · 1 min · jiezi

vue数据初始化--initState

数据初始化Vue 实例在建立的时候会运行一系列的初始化操作,而在这些初始化操作里面,和数据绑定关联最大的是 initState。首先,来看一下他的代码:function initState(vm) { vm._watchers = []; var opts = vm.$options; if(opts.props) { initProps(vm, opts.props); //初始化props } if(opts.methods) { initMethods(vm, opts.methods); //初始化methods } if(opts.data) { initData(vm); //初始化data } else { observe(vm._data = {}, true /* asRootData */ ); } if(opts.computed) { initComputed(vm, opts.computed); //初始化computed } if(opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); //初始化watch }}在这么多的数据的初始化中,props、methods和data是比较简单的(所以我就不详细介绍了☺),而computed 和 watch则相对较难,逻辑较复杂,所以我下面主要讲下computed 和 watch(以下代码部分为简化后的)。initState里面主要是对vue实例中的 props, methods, data, computed 和 watch 数据进行初始化。在初始化props的时候(initProps),会遍历props中的每个属性,然后进行类型验证,数据监测等(提供为props属性赋值就抛出警告的钩子函数)。在初始化methods的时候(initMethods),主要是监测methods中的方法名是否合法。在初始化data的时候(initData),会运行 observe 函数深度遍历数据中的每一个属性,进行数据劫持。在初始化computed的时候(initComputed),会监测数据是否已经存在data或props上,如果存在则抛出警告,否则调用defineComputed函数,监听数据,为组件中的属性绑定getter及setter。如果computed中属性的值是一个函数,则默认为属性的getter函数。此外属性的值还可以是一个对象,他只有三个有效字段set、get和cache,分别表示属性的setter、getter和是否启用缓存,其中get是必须的,cache默认为true。function initComputed(vm, computed) { var watchers = vm._computedWatchers = Object.create(null); for(var key in computed) { var userDef = computed[key]; var getter = typeof userDef === ‘function’ ? userDef : userDef.get; //创建一个计算属性 watcher watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ); if(!(key in vm)) { //如果定义的计算属性不在组件实例上,对属性进行数据劫持 //defineComputed 很重要,下面我们再说 defineComputed(vm, key, userDef); } else { //如果定义的计算属性在data和props有,抛出警告 } }}在初始化watch的时候(initWatch),会调用vm.$watch函数为watch中的属性绑定setter回调(如果组件中没有该属性则不能成功监听,属性必须存在于props、data或computed中)。如果watch中属性的值是一个函数,则默认为属性的setter回调函数,如果属性的值是一个数组,则遍历数组中的内容,分别为属性绑定回调,此外属性的值还可以是一个对象,此时,对象中的handler字段代表setter回调函数,immediate代表是否立即先去执行里面的handler方法,deep代表是否深度监听。vm.$watch函数会直接使用Watcher构建观察者对象。watch中属性的值作为watcher.cb存在,在观察者update的时候,在watcher.run函数中执行。想了解这一过程可以看我上一篇的 vue响应式系统–observe、watcher、dep中关于Watcher的介绍。function initWatch(vm, watch) { //遍历watch,为每一个属性创建侦听器 for(var key in watch) { var handler = watch[key]; //如果属性值是一个数组,则遍历数组,为属性创建多个侦听器 //createWatcher函数中封装了vm.$watch,会在vm.$watch中创建侦听器 if(Array.isArray(handler)) { for(var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); } } else { //为属性创建侦听器 createWatcher(vm, key, handler); } }}function createWatcher(vm, expOrFn, handler, options) { //如果属性值是一个对象,则取对象的handler属性作为回调 if(isPlainObject(handler)) { options = handler; handler = handler.handler; } //如果属性值是一个字符串,则从组件实例上寻找 if(typeof handler === ‘string’) { handler = vm[handler]; } //为属性创建侦听器 return vm.$watch(expOrFn, handler, options)} computedcomputed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值下面将围绕这一句话来做解释。上面代码中提到过,当计算属性中的数据存在与data和props中时,会被警告,也就是这种做法是错误的。所以一般的,我们都会直接在计算属性中声明数据。还是那个代码片段中,如果定义的计算属性不在组件实例上,会运行defineComputed函数对数据进行数据劫持。下面我们来看下defineComputed函数中做了什么。function defineComputed(target, key, userDef) { //是不是服务端渲染 var shouldCache = !isServerRendering(); //如果我们把计算属性的值写成一个函数,这时函数默认为计算属性的get if(typeof userDef === ‘function’) { sharedPropertyDefinition.get = shouldCache ? //如果不是服务端渲染,则默认使用缓存,设置get为createComputedGetter创建的缓存函数 createComputedGetter(key) : //否则不使用缓存,直接设置get为userDef这个我们定义的函数 userDef; //设置set为空函数 sharedPropertyDefinition.set = noop; } else { //如果我们把计算属性的值写成一个对象,对象中可能包含set、get和cache三个字段 sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? //如果我们传入了get字段,且不是服务端渲染,且cache不为false,设置get为createComputedGetter创建的缓存函数 createComputedGetter(key) : //如果我们传入了get字段,但是是服务端渲染或者cache设为了false,设置get为userDef这个我们定义的函数 userDef.get : //如果没有传入get字段,设置get为空函数 noop; //设置set为我们传入的传入set字段或空函数 sharedPropertyDefinition.set = userDef.set ? userDef.set : noop; } //虽然这里可以get、set都可以设置为空函数 //但是在项目中,get为空函数对数据取值会报错,set为空函数对数据赋值会报错 //而computed主要作用就是计算取值的,所以get字段是必须的 //数据劫持 Object.defineProperty(target, key, sharedPropertyDefinition);}在上一篇的 vue响应式系统–observe、watcher、dep 中,我有关于Watcher的介绍中提到,计算属性 watcher实例化的时候,会把options.lazy设置为true,这里是计算属性惰性求值,且可缓存的关键,当然前提是cache不为false。cache不为false,会调用createComputedGetter函数创建计算属性的getter函数computedGetter,先来看一段代码function createComputedGetter(key) { return function computedGetter() { var watcher = this._computedWatchers && this._computedWatchers[key]; if(watcher) { if(watcher.dirty) { //watcher.evaluate中更新watcher的值,并把watcher.dirty设置为false //这样等下次依赖更新的时候才会把watcher.dirty设置为true,然后进行取值的时候才会再次运行这个函数 watcher.evaluate(); } //依赖追踪 if(Dep.target) { watcher.depend(); } //返回watcher的值 return watcher.value } }}//对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true//说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。Watcher.prototype.evaluate = function evaluate() { this.value = this.get(); this.dirty = false;};//当一个依赖改变的时候,通知它updateWatcher.prototype.update = function update() { //三种watcher,只有计算属性 watcher的lazy设置了true,表示启用惰性求值 if(this.lazy) { this.dirty = true; } else if(this.sync) { //标记为同步计算的直接运行run,三大类型暂无,所以基本会走下面的queueWatcher this.run(); } else { //将watcher推入观察者队列中,下一个tick时调用。 //也就是数据变化不是立即就去更新的,而是异步批量去更新的 queueWatcher(this); }};当options.lazy设置为true之后(仅计算属性watcher的options.lazy设置为true),每次依赖更新,都不会主动触发run函数,而是把watcher.dirty设置为true。这样,当对计算属性进行取值时,就会运行computedGetter函数,computedGetter函数中有一个关于watcher.dirty的判断,当watcher.dirty为true时会运行watcher.evaluate进行值的更新,并把watcher.dirty设置为false,这样就完成了惰性求值的过程。后面只要依赖不更新,就不会运行update,就不会把watcher.dirty为true,那么再次取值的时候就不会运行watcher.evaluate进行值的更新,从而达到了缓存的效果。综上,我们了解到cache不为false的时候,计算属性都是惰性求值且具有缓存性的,而cache默认是true,我们也大多使用这个默认值,所以我们说 computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值。 ...

April 10, 2019 · 2 min · jiezi

Vuex源码学习(八)模块的context如何被创建以及它的作用

你不知道action与mutation怎么被调用的?赶紧回去看啊Vuex源码学习(七)action和mutation如何被调用的(调用篇))上两个小节已经讲述了commit与dispatch如何调用mutation与action的,但是action中有几个参数感觉涉及到了一些我们遗漏(故意不讲)的点。模块的context在installModule的时候 给每个模块绑定了一个属性context。通过makeLocalContext函数创建的,在注册action、mutation和getters都有使用。这个context是什么呢?makeLocalContext函数创建了一个什么东西返回值local对象 由两个方法、两个属性构成的。这个目的是什么?创建局部模块的dispatch、commit、getters、state也就是这个东西我们按照类型分析dispatch与commit// 查看全名,如果没有全名 可能是根模块或者没有设置命名空间const noNamespace = namespace === ‘’;// 如果没有全名 就使用全局(store)上的disptach// 有全名的话 构建一个新的dispatch // 这个新的dispatch仍然接收三个参数(与store上的dispatch一样)// dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { //unifyObjectStyle 对额外传入的_options没有任何处理 只是确定一下位置 const args = unifyObjectStyle(_type, _payload, _options) // options 值没有发生变化 const { payload, options } = args let { type } = args // 在disptach的时候是否指定选择root(根) // 如果options设置为{root : true} 那么就会跳过下面 if (!options || !options.root) { // 拼接真正的名字 type = namespace + type if (process.env.NODE_ENV !== ‘production’ && !store._actions[type]) { console.error([vuex] unknown local action type: ${args.type}, global type: ${type}) return } } // 调用(补全名字后)的action return store.dispatch(type, payload) },这段代码我们可以看出来,local对象(也就是模块的context属性)中的dispacth会在未指定使用根模块名字时,会把dispatch调用的名字强行加上这个模块的全名,用这个dispatch调用的action都会变成你这个模块下的action所以local中的dispatch与store中的disptach有什么不同通俗的讲我们想要调用A模块(有命名空间的某个action B)需要做的是this.$store.dispatch(‘A模块的全名/B的名字’); 在A模块的action中想要使用dispatch来做一些事情。actions.jsexport const ajaxGetUserName = ({dispatch})=>{ // 这个时候用dispatch调用自己模块内的其余的action不需要加上全名 dispatch(‘ajaxGetUserAge’); // 想要变成和根模块一样的dispatch 需要加上一个options注明{root : true} // 这个时候dispatch就会变成全局的 不会主动帮你拼接全名了}export const ajaxGetUserAge = () => { // do something}同理local对象下的commit也是做了同样的事情,这里就不多加解释了,相信聪明的你早就可以举一反三了。两个方法说完了,下面该讲两个属性了getters与state这两个属性就是我们的getters与state,但是这是我们local对象中,也就是局部模块下的getters与state。getters与state如何创建的getters首先判断全名是不是为空,为空就返回store对象的getters,有的话就创建局部getters。与其说是创建不如说是代理如何创建局部的getters? 代理的方式makeLocalGetters源码function makeLocalGetters (store, namespace) { // 设计思想 //其实我们并不需要创建一套getters, // 只要我们在local中通过getters来获取一些局部模块的值的时候, // 可以被代理到真正存放这些getters的地方。 // 创建代理对象 const gettersProxy = {} // 找到切割点 const splitPos = namespace.length Object.keys(store.getters).forEach(type => { // skip if the target getter is not match this namespace // 得去getters里面找一下有没有这个namespace为前缀的getter。 // 没有就找不到了 if (type.slice(0, splitPos) !== namespace) return // extract local getter type // 拿到模块内注册的那个局部的getter名字 // 全名是set/getName // localType就是getName const localType = type.slice(splitPos) // Add a port to the getters proxy. // Define as getter property because // we do not want to evaluate the getters in this time. // 完成代理任务, // 在查询局部名字是被代理到对应的store.getters中的(全名)getter Object.defineProperty(gettersProxy, localType, { get: () => store.getters[type], enumerable: true }) }) //返回代理对象 return gettersProxy}创建局部的getters就是一个代理的过程,在使用模块内使用(没有加上命名空间的)getters的名字,会被代理到,store实例上那个真正的(全名的)getters。state这个相对来说就简单很多了与代理类似,只是state只需要代理到state中对应那个模块的state,这个就比较简单了。创建完毕context是如何被创建的大家已经比较了解了。context的作用是什么?(local就是contenxt)之前说过注册mutation、action、getters都用到了local。用他们干什么?一一介绍1. 注册mutation我们注册的mutation在被commit调用时,使用的state是局部的state,当前模块内的state,所以不用特殊方式mutation无法更新父(祖先)模块和兄弟模块的内容。2. 注册dispatchdispatch是可以调用到模块内的mutation、disptach,也就是说它有更新模块内数据的能力,但是只给了dispatch传入了store的getters与state(虽然有了这俩你想要什么放在vuex的数据都能得到),并没有给store的dispatch与mutation。这就说名dispatch可以查看store中的所有数据,你放在vuex里面的数据我都可以看,但是你想改不使用特殊手段,不好意思只能改自己模块的。3. 注册gettersgetters并没有改变数据的能力,你愿意怎么操作数据都可以,模块内的数据,全模块的数据都可以给你,你愿意怎么计算都可以。在注册中我们可以看到,vuex对这个改变数据的权限控制的很严格,但是查看数据控制的很松,改只能改自己模块的,查你愿意怎么看都OK。总结context(也是local)是经过一个makeLocalContext的函数创建的,里面有局部的dispatch、commit方法和getters、state属性。局部的方法属性都是只能访问局部模块内的,除非在使用时额外传入options({root:true})来解开局部模块的限制。局部的getters是通过makeLocalGetters来实现的,主要思想是依靠代理的方式,把局部的名字的getter代理到store的getters中那个全名的getter。context 的作用可以帮助dispatch与commit控制更新数据的权限,帮助模块内getters拿到局部与全模块的数据。这个章节结束,我们所有和模块有关的内容就已经完结了。对于模块做一个小的总结。模块的意义模块与模块链接把Vuex初始化传入的内容,整理成一个方便处理的模块树(方便)模块让action、mutation、getters、state都有了自己的全名(设置namespaced为true),起名字不再被约束,减少了命名冲突。模块还给action、mutation、getters提供了局部上下文(context)让模块内的这些方法和属性,可以方便的修改模块内的数据以及获取全模块与模块内的数据。dispatch与commit也对模块进行了全力的支持(不支持不白做了吗),所以模块为Vuex提供了很多方便,方便的去获取数据、修改数据。那么Vuex真正的数据仓库在哪里?数据都存储在哪里?我们下一章见我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

April 2, 2019 · 2 min · jiezi

Vuex源码学习(七)action和mutation如何被调用的(调用篇)

前置篇不会那可不行!Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)在前置准备篇我们已经知道被处理好的action与mutation都被集中放置在哪里了。下面就要看dispacth和commit如何去调用它们。dispatch与commit的实现commit:首先呢我们要校正参数,把传入的参数整理下主要是处理这种形式// 接收一个对象this.$store.commit({type : ‘setName’,name : ‘xLemon’});this.$store.commit(‘setName’,{name : ‘xLemon’});这两个基本等价。只是第一种方式mutation接收的payload会比第二种多一个type属性,整理的部分并不关键type是我们要找的mutation的名字,如何找到mutation呢?通过 this._mutations[type] 找到要执行的mutation所以type一定要是mutation的全名所以我们通过commit找mutation的时候有命名空间的时候就要输入全名,(那种带很多/的)。没有这个名字的mutation容错处理,然后在withCommit函数的包裹下,完成了mutation的执行(所有mutation啊,全名相同的mutation都会被执行)。然后呢遍历_subscribers里面的函数进行执行。_subscribers这是什么?在一开始我们可以注册一些内容(函数),在commit完成时被通知执行。(观察者模式)如何注册在这一章就不多讲了!后面章节会统一讲述。这就是commit做的事情。dispatch呢?与commit大同小异,也有一个_actionSubscribers的属性,在dispatch执行之前全部调用。对于dispatch Vuex推荐的是放置异步任务,在注册action的时候已经被强制promise化了,所以有多个同名action会通过Promise.all来处理。在action的前后都有对应的钩子函数执行。固定disptach与commit的this指向//在vue组件内一个方法如果多次使用dispatch和commit,就会很麻烦。this.$store.dispatch(‘xxx’,payload);this.$store.commit(‘xxx’,payload);const {dispatch,commit} = this.$store;//这相当于创建变量,然后把this.$store的dispatch与commit赋值给它们。//有经验的都应该知道,函数dispatch和commit的this指向在严格模式下指向undefined。// 非严格模式下指向window,// 刚才的源码中我们也看到了,dispatch和commit都依赖于Store实例。怎么办??解决方法如下:dispatch和commit是Store原型链上的方法,在constructor内注册了构造函数内的方法,把原型上的dispatch和commit进行了一个this指向的强制绑定,通过call让两个方法永远指向Store的实例上,保证了dispatch和commit不会因为赋值给其余变量等操作导致this指向改变从而发生问题action与mutation函数的参数都有哪些?怎么来的?看一个简单的mutation:export const setName = function(state,payload) { state.name = payload;};这个时候不经意间有了一个疑惑?state哪里来的。这就要从mutation被注册的函数内找原因了handle是我们要被注册的一个mutation,entry是这个同名mutation的容器(存储所有的这个名字的mutation,一般只有一个)在吧handle放入entry的过程中,我们发现,entry是被一个函数包裹起来,然后将local.store和payload绑定到这个handle的参数上,然后把handle的this指向锁定到了Store实例上,所以mutation在被commit调用的时候只传入了一个参数payload,但是mutation函数执行的时候就有了两个参数。下面看一下action:按照刚才的分析action在被dispatch调用的时候会接收一个参数,但是action执行的时候会接收两个参数,第一个参数是个对象里面有六项,真的是包罗万象啊。。。我们看一下这个对象的六项{ dispatch : local.dispatch, commit:local.commit, getter: local.getters, state: local.state, rootGetters:store.getters, rootState:store.state}分为两种一种是local的、一种是store的。mutation中好像也有使用local,那么local的意义是什么呢?我们下一节会讲述local的含义以及makeLocalContext、makeLocalGetters两个函数的作用。还是要给个小线索,在模块树的层级很高的时候,我们在使用state的时候要一层一层找寻吗?总结dispatch与commit在执行的时候,都是根据传入的全名action、mutation去Store实例的_actions、_mutations中找到对应的方法,进行执行的。dispatch和commit中都使用了this(指向Store实例),为了防止this的指向改变从而出现问题,就把原型上的dispatch与commit在constructor中处理到了实例本身,这个过程做了this指向的绑定(call)。action和mutation在执行的时候,参数都是在注册的时候就绑定了一部分,所以action与mutation在使用的时候可以访问到state等很多内容。下一章;离开action与mutation 来讨论一下local的含义以及makeLocalContext、makeLocalGetters两个函数的作用我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

April 1, 2019 · 1 min · jiezi

Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)

module与moduleCollection你一定要会啊!Vuex源码学习(五)加工后的module在组件中使用vuex的dispatch和commit的时候,我们只要把action、mutation注册好,通过dispatch、commit调用一下方法名就可以做到。使用方式vue组件内//in vue componentthis.$store.commit(‘setName’,{name : ‘xLemon’});this.$store.commit(’list/setName’,{name : ‘xLemon’});vuex的mutation中// in mutation.jsexport const setName = function(state,payload){ state.name = payload.name;}// in list mutation.js 在名为list的模块下的mutation(有自己的命名空间)export const setName = function(state,payload){ state.name = payload.name;}我们传递的只是一个字符串,commit是如何找到注册mutation时的同名方法的呢?有命名空间的这种mutation是如何被找到并且执行的呢?上源码属性的意义_actions用于存放所有注册的action_mutations用于存放所有注册的mutation被注册的action和mutation如何被放到对应的属性中的呢?轮到installModule函数要出马了。installModule的意义是初始化根模块然后递归的初始化所有模块,并且收集模块树的所有getters、actions、mutations、以及state。看一下installModule的代码,installModule并不是在类的原型上,并不暴露出来,属于一个私有的方法,接收五个参数。store(Vuex.store的实例对象。rootState (根结点的state数据)。path 被初始化模块的path(前两张讲过path的意义)。module 被初始化的模块。hot 热更新(并不关键)function installModule (store, rootState, path, module, hot) { const isRoot = !path.length // 获取全名 const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { // 设置命名空间对照map store._modulesNamespaceMap[namespace] = module //console.log(store._modulesNamespaceMap); } // set state if (!isRoot && !hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] // 把子模块的state(数据)绑定到父模块上(按照层级) store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) } const local = module.context = makeLocalContext(store, namespace, path) // 使用模块暴露出来的方法来注册mutation、action、getter module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })}这个函数虽然只有40多行,但处于一个承上启下的关键点,这一章只会分析如何收集mutation与action其余的内容会再下一章讲述。installModule首先获取一下这个模块的命名(我称之为全名)依赖_modules(ModuleCollection实例对象)的getNamespace方法。根据模块的path,path有从根结点到当前节点这条路径上按顺序排序的所有模块名(根结点没有模块名,并没有设置在path,所以根模块注册时是个空数组,他的子模块的path就是[子模块的名字])。那么Vuex如何整理模块名的呢?效果:如果这个模块有自己的命名空间(namespaced为true)这个模块的全名就是父模块的全名+自己的模块名,如果这个模块没有自己的命名空间(namespaced为false)这个模块的全名就是父模块的全名为什么会是这样?分析一下代码getNamespace (path) { let module = this.root //根模块 return path.reduce((namespace, key) => { //根模块的path是个空数组不执行 // path的第一项是根模块的儿子模块。 // 获取儿子模块 并且将替换module (path的下一项就是儿子模块中的子模块了) // 下次累加 就是这个module(轮到子模块了)去找它(子模块)的子模块 module = module.getChild(key) // 查看儿子模块是不是设置了命名空间 //如果设置了这个模块的全名就增加自己的模块名和一个’/‘分割后面的模块名, //没有的话返回一个’’, // reduce累加可以把这个名称进行累加 return namespace + (module.namespaced ? key + ‘/’ : ‘’) }, ‘’)}获取完模块的全名了,之后我们看一下这两个函数module.forEachActionmodule.forEachMutation在上一章节module提供了遍历自己内部的action、mutation的方法。 module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local)})module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) })const namespacedType = namespace + key 这句话 就是拼接出真正的mutation、action的名字模块全名+mutation/action的名字。也就是一开始我举例的list/setName是这个mutation的全名(被调用的时候用)this.$store.commit(’list/setName’,{name : ‘xLemon’});名称已经获取到了,下一步怎么办?把这些函数按照对应名字放到之前说的_actions、_mutations属性中啊看一下这个名字的mutation有没有被注册过,没有就声明一下,然后push进去。如果这个名字的mutation被注册过,就push进去。action同理小彩蛋 设置两个不同模块的同名mutation(全名一样哦)这两个mutation都会执行,action也是一样的。总结action和mutation在被dispatch和commit调用前,首先遍历模块树获取每个模块的全名。把模块内的action和mutation加上模块全名,整理成一个全新的名字放入_actions 、 _mutations属性中。dispacth和commit如何调用aciton和mutation 的将在下章讲述下一章:我们讨论action和mutation如何被调用的(调用篇)。我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

March 31, 2019 · 2 min · jiezi

Vuex源码学习(五)加工后的module

没有看过moduleCollection那可不行!Vuex源码学习(四)module与moduleCollection代码块和截图的区别代码块部分希望大家按照我的引导一行行认真的读代码的截图是希望大家能记住图中的结构与方法,下面会对整体进行一个分析,而不会一行一行的分析。但是以后的文章会更偏向于使用代码块,希望大家喜欢。上一章我们讲述了ModuleCollection类的作用,帮助我们把伪(未加工的)模块变成真正的模块,然后把每个模块按照父子与兄弟关系链接起来。那么真正的模块相比于伪(未加工的)模块多了哪些能力呢?module提供的方法这是module暴露出来的所有方法,以及一个属性。先看一下constructorconstructor (rawModule, runtime) { this.runtime = runtime // Store some children item // 创建一个容器存放该模块所有的子模块 this._children = Object.create(null) // Store the origin module object which passed by programmer // 存放自己未被加工的模块内容。 this._rawModule = rawModule const rawState = rawModule.state // Store the origin module‘s state // 创建这个模块的数据容器 this.state = (typeof rawState === ‘function’ ? rawState() rawState) || {}}模块的初始化主要是做了以下三件事情创建_children属性用于存放子模块创建_rawModule属性存储自己模块的伪(未被加工)模块时的内容创建state属性存储自己模块的数据内容 每个模块都有自己的state。模块的初始化并没有做什么事情,模块提供的方法和属性才是它的核心,模块提供了一个namespaced的属性,以及很多方法,我将模块提供的方法分成两类。先说属性get namespaced () { // 获取模块的namespaced属性 确定这个模块有没有自己的命名空间 return !!this._rawModule.namespaced}判断是否有命名空间有什么用?在以后设置getters、mutation、actions时有很大作用,以后再讲。再说方法模块提供的所有方法都是为了给外部的调用,这些方法没有一个是让模块在自己的内部使用的。所以我把方法划分的纬度是,按照这个方法是用于构建模块树还是用于抽取模块中的内容,构建模块树的方法:1.addChild:给模块添加子模块。addChild (key, module) { this._children[key] = module}这个方法实现上很简单,它是在哪里被调用的呢?大家可以翻开上一章的moduleCollection的内容,在ModuleCollection中完成模块之间的链接,就是使用这个方法给父模块添加子模块。removeChild:移除子模块 Vuex初始化的时候未使用,但可以给你提供灵活的处理模块的能力removeChild (key) { delete this._children[key]}getChild:获取子模块 获取子模块的意义是什么?在以后配置模块的名字时,需要获取模块的是否设置了命名空间,获取命名空间的属性模块提供了,再提供一个获取子模块就都Ok了getChild (key) { return this._children[key]}updateChild:更新模块的_ra wModule属性(更新模块的未加工前的模块内容),Vuex中未使用update (rawModule) { this._rawModule.namespaced = rawModule.namespaced if (rawModule.actions) { this._rawModule.actions = rawModule.actions } if (rawModule.mutations) { this._rawModule.mutations = rawModule.mutations } if (rawModule.getters) { this._rawModule.getters = rawModule.getters }}Vuex在链接与整合模块的时候使用了其中两个方法,addChild、getChild。类ModuleCollection在链接时需要找到模块(getChild)然后给模块添加子模块(addChild)的功能,所以这两个方法是在整合模块时最重要的。抽取模块中的内容上面的一组方法,是为了更好的完成模块的链接,给散落的单一模块整理成一个模块树可以提供便捷的封装方法,下面要说的方法什么叫做抽取模块中的内容?将这些方法暴露给外面可以方便的去获取这个模块内的一些内容来使用。forEachValue是Vuex封装的工具方法,用于遍历对象的。export function forEachValue (obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key))}这四个方法作用:forEachChild : 遍历模块的子模块forEachGetter : 遍历模块中_rawModule.getters 这块就应该知道 _rawModule的作用了,我把模块未加工时会有getters属性,存放这个模块所有的getters方法(vuex的基本用法就不多讲了),然后遍历,forEachMutation : 和forEachGetter类似,只是换成了遍历mutationsforEachAction : 和forEachGetter类似,只是换成了遍历actions这四个方法就是遍历这些内容,有意义吗?意义很大,目前_rawModule上这些getters、mutations、actions属性并不会生效,只是单纯的一个属性,如何让他们可以成为那种,被dispatch、commit使用的那种方法呢?先给大家一个小提示,mutations、actions都是要被注册的,注册之前总要获取到这些内容,具体的实现方式后面的章节会详细讲述,总结加工后真正的module(我们称由Module这个类实例化出来为真正的module)只是缓存了一些内容,并且给外界提供了一堆方便高效的方法。这些方便高效的方法为之后的注册action、mutation。整合state都起了很关键的作用。所以说module这个小单元为下面的代码提供了很大便利,额外思考我们对一段内容需要频繁的处理并且处理方式大同小异的时候,是不是可以像module一样整理成一个对象,然后给外界提供一些方法。(有一种面向对象思想)下一章讲述action和mutation是如何调用的我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

March 27, 2019 · 1 min · jiezi

Vuex源码学习(四)module与moduleCollection

如果你还不知道Vuex是怎么安装的,请移步Vuex源码学习(三)install都做了哪些事情整合模块这一节该分析模块的是怎么被整合的,以及要整合成什么样子。在Vuex的constructor中比较靠前的位置有这么两行代码,_modules属性是ModuleCollection的实例对象,之后 _modules属性被频繁使用,这块就是对Vuex的模块进行了一次整合,整合出一个可以被使用的 _modules,而_moduleNamespaceMap是一个空对象该怎么整合模块?先看一下我们我们项目中Store的结构store/index.jsmoduleList:moduleSet:结构就是这样的在以上代码中的modules下的数据,我都称它是伪(未加工)模块,因为它还不具有模块的功能。当我们实例化Vuex.Store这个类的时候接收的参数options就会直接交给moduleCollection来处理。参数options是什么呢?就是上面图中这样结构的数据,想要处理成什么样子?下面看一下ModuleCollection是怎么处理的export default class ModuleCollection { constructor (rawRootModule) { // register root module (Vuex.Store options) // 注册模块并链接 this.register([], rawRootModule, false) } … register (path, rawModule, runtime = true) { if (process.env.NODE_ENV !== ‘production’) { // 不符合规则的模块会报错。 assertRawModule(path, rawModule) } // 创建一个模块 const newModule = new Module(rawModule, runtime) if (path.length === 0) { this.root = newModule } else { // path.slice(0,-1)就可以拿到父模块的path。 // get方法可以根据path来找到对应的模块。 const parent = this.get(path.slice(0, -1)) // 将子模块挂载到父模块上 parent.addChild(path[path.length - 1], newModule) } // register nested modules if (rawModule.modules) { // 遍历每个模块的modules(目的是获取所有子模块) forEachValue(rawModule.modules, (rawChildModule, key) => { // 为什么要path.concat(key)? // 依次注册子模块。 this.register(path.concat(key), rawChildModule, runtime) }) } }}在Vuex与vue-router的源码中,命名变量是很有规律的,在开发人员使用这两个框架的时候,传递进去的参数,在使用时命名的变量名都是raw开头的,代表是未经过加工的。将未经过加工的伪模块处理成真正可以使用的模块。在初始化的时候直接开始注册模块,moduleCollection的这个类的任务是把生成的模块合理的链接起来,而模块的生成交给了Module这个类。所以register方法就是把根模块以及所有的子模块从一个伪(未加工)模块变成一个真正的模块并且链接起来。遍历树形结构用什么方法? 递归!register都做了什么?筛选出不符合规则的模块,报错提示。将伪(未加工)模块加工成一个真正的模块。将加工好的模块挂载在它的父模块上。如果这个模块有modules属性(模块有自己的子模块)让每个子模块重复以上操作递归的出口:rawModule.modules为false(模块没有子模块) ,也就是每个模块都没有子模块需要注册了,那就代表全部加工与链接完毕。分析register的三个参数register接收三个参数,path、rawModule、hot。hot这个参数目前看来不关键。rawModule是伪(未加工)模块那path的作用是什么呢?path的作用很大,大家类比下前端页面的dom树的Xpath,如果我想知道这个节点的位置,需要知道这个父节点的位置,然后一层一层的向上知道根结点,有了Xpath就可以直接找到这个节点,Vuex也是一样的想知道某个模块的位置,只需要提供根结点到他的一个path,path按顺序存储着根模块到它本身的所有祖先模块(根模块没有名字,又不能把第一个放一个空,所以path里 面没有根模块),在每次注册的时候,这个模块有子模块,就把它的path加上(concat)子模块的名字,在子模块执行register方法时,path就比它的父模块多一个父模块的名字,所以根模块注册的时候传入path就是[](空数组)了。ModuleCollection的get方法可以根据path来获取指定的模块,在挂载的时候十分有用,,使用reduce的方法,按照数组的顺序,一层一层的找目标模块。path对以后要讲的设置命名空间也很有帮助。总结ModuleCollection这个类,主要完成了模块的链接与整合,生成模块的任务交给了Module这个类。模块的链接与整合通过递归完成。path可以让moduleCollection快速找到对应模块。下一章讲述生成的module具体可以做什么我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

March 26, 2019 · 1 min · jiezi

vue-router 源码阅读 - 文件结构与注册机制

前端路由是我们前端开发日常开发中经常碰到的概念,在下在日常使用中知其然也好奇着所以然,因此对 vue-router 的源码进行了一些阅读,也汲取了社区的一些文章优秀的思想,于本文记录总结作为自己思考的输出,本人水平有限,欢迎留言讨论~目标 vue-rouer 版本:3.0.2vue-router源码注释:vue-router-analysis声明:文章中源码的语法都使用 Flow,并且源码根据需要都有删节(为了不被迷糊 @_@),如果要看完整版的请进入上面的 github地址 ~ 本文是系列文章,链接见底部 0. 前备知识FlowES6语法设计模式 - 外观模式HTML5 History Api如果你对这些还没有了解的话,可以看一下本文末尾的推介阅读。1. 文件结构首先我们来看看文件结构:.├── build // 打包相关配置├── scripts // 构建相关├── dist // 构建后文件目录├── docs // 项目文档├── docs-gitbook // gitbook配置├── examples // 示例代码,调试的时候使用├── flow // Flow 声明├── src // 源码目录│ ├── components // 公共组件│ ├── history // 路由类实现│ ├── util // 相关工具库│ ├── create-matcher.js // 根据传入的配置对象创建路由映射表│ ├── create-route-map.js // 根据routes配置对象创建路由映射表 │ ├── index.js // 主入口│ └── install.js // VueRouter装载入口├── test // 测试文件└── types // TypeScript 声明我们主要关注的就是 src 中的内容。2. 入口文件2.1 rollup 出口与入口按照惯例,首先从 package.json 看起,这里有两个命令值得注意一下:{ “scripts”: { “dev:dist”: “rollup -wm -c build/rollup.dev.config.js”, “build”: “node build/build.js” }}dev:dist 用配置文件 rollup.dev.config.js 生成 dist 目录下方便开发调试相关生成文件,对应于下面的配置环境 development;build 是用 node 运行 build/build.js 生成正式的文件,包括 es6、commonjs、IIFE 方式的导出文件和压缩之后的导出文件;这两种方式都是使用 build/configs.js 这个配置文件来生成的,其中有一段语义化比较不错的代码挺有意思,跟 Vue 的配置生成文件比较类似:// vue-router/build/configs.jsmodule.exports = [{ // 打包出口 file: resolve(‘dist/vue-router.js’), format: ‘umd’, env: ‘development’ },{ file: resolve(‘dist/vue-router.min.js’), format: ‘umd’, env: ‘production’ },{ file: resolve(‘dist/vue-router.common.js’), format: ‘cjs’ },{ file: resolve(‘dist/vue-router.esm.js’), format: ’es’ }].map(genConfig)function genConfig (opts) { const config = { input: { input: resolve(‘src/index.js’), // 打包入口 plugins: […] }, output: { file: opts.file, format: opts.format, banner, name: ‘VueRouter’ } } return config}可以清晰的看到 rollup 打包的出口和入口,入口是 src/index.js 文件,而出口就是上面那部分的配置,env 是开发/生产环境标记,format 为编译输出的方式:es: ES Modules,使用ES6的模板语法输出cjs: CommonJs Module,遵循CommonJs Module规范的文件输出umd: 支持外链规范的文件输出,此文件可以直接使用script标签,其实也就是 IIFE 的方式那么正式输出是使用 build 方式,我们可以从 src/index.js 看起// src/index.jsimport { install } from ‘./install’export default class VueRouter { … }VueRouter.install = install首先这个文件导出了一个类 VueRouter,这个就是我们在 Vue 项目中引入 vue-router 的时候 Vue.use(VueRouter) 所用到的,而 Vue.use 的主要作用就是找注册插件上的 install 方法并执行,往下看最后一行,从一个 install.js 文件中导出的 install 被赋给了 VueRouter.install,这就是 Vue.use 中执行所用到的 install 方法。2.2 Vue.use可以简单看一下 Vue 中 Vue.use 这个方法是如何实现的:// vue/src/core/global-api/use.jsexport function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { // … 省略一些判重操作 const args = toArray(arguments, 1) args.unshift(this) // 注意这个this,是vue对象 if (typeof plugin.install === ‘function’) { plugin.install.apply(plugin, args) } return this }}上面可以看到 Vue.use 这个方法就是执行待注册插件上的 install 方法,并将这个插件实例保存起来。值得注意的是 install 方法执行时的第一个参数是通过 unshift 推入的 this,因此 install 执行时可以拿到 Vue 对象。对应上一小节,这里的 plugin.install 就是 VueRouter.install。3. 路由注册3.1 install接之前,看一下 install.js 里面是如何进行路由插件的注册:// vue-router/src/install.js/* vue-router 的注册过程 Vue.use(VueRouter) /export function install(Vue) { _Vue = Vue // 这样拿到 Vue 不会因为 import 带来的打包体积增加 const isDef = v => v !== undefined const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode // 至少存在一个 VueComponent 时, _parentVnode 属性才存在 // registerRouteInstance 在 src/components/view.js if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } // new Vue 时或者创建新组件时,在 beforeCreate 钩子中调用 Vue.mixin({ beforeCreate() { if (isDef(this.$options.router)) { // 组件是否存在$options.router,该对象只在根组件上有 this._routerRoot = this // 这里的this是根vue实例 this._router = this.$options.router // VueRouter实例 this._router.init(this) Vue.util.defineReactive(this, ‘_route’, this._router.history.current) } else { // 组件实例才会进入,通过$parent一级级获取_routerRoot this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed() { registerInstance(this) } }) // 所有实例中 this.$router 等同于访问 this._routerRoot._router Object.defineProperty(Vue.prototype, ‘$router’, { get() { return this._routerRoot._router } }) // 所有实例中 this.$route 等同于访问 this._routerRoot._route Object.defineProperty(Vue.prototype, ‘$route’, { get() { return this._routerRoot._route } }) Vue.component(‘RouterView’, View) // 注册公共组件 router-view Vue.component(‘RouterLink’, Link) // 注册公共组件 router-link const strats = Vue.config.optionMergeStrategies strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created}install 方法主要分为几个部分:通过 Vue.mixin 在 beforeCreate、 destroyed 的时候将一些路由方法挂载到每个 vue 实例中给每个 vue 实例中挂载路由对象以保证在 methods 等地方可以通过 this.$router、this.$route 访问到相关信息注册公共组件 router-view、router-link注册路由的生命周期函数Vue.mixin 将定义的两个钩子在组件 extend 的时候合并到该组件的 options 中,从而注册到每个组件实例。看看 beforeCreate,一开始访问了一个 this.$options.router 这个是 Vue 项目里面 app.js 中的 new Vue({ router }) 这里传入的这个 router,当然也只有在 new Vue 这时才会传入 router,也就是说 this.$options.router 只有根实例上才有。这个传入 router 到底是什么呢,我们看看它的使用方式就知道了:const router = new VueRouter({ mode: ‘hash’, routes: [{ path: ‘/’, component: Home }, { path: ‘/foo’, component: Foo }, { path: ‘/bar’, component: Bar }]})new Vue({ router, template: &lt;div id="app"&gt;&lt;/div&gt;}).$mount(’#app’)可以看到这个 this.$options.router 也就是 Vue 实例中的 this._route 其实就是 VueRouter 的实例。剩下的一顿眼花缭乱的操作,是为了在每个 Vue 组件实例中都可以通过 _routerRoot 访问根 Vue 实例,其上的 _route、_router 被赋到 Vue 的原型上,这样每个 Vue 的实例中都可以通过 this.$route、this.$router 访问到挂载在根实例 _routerRoot 上的 _route、_router,后面用 Vue 上的响应式化方法 defineReactive 来将 _route 响应式化,另外在根组件上用 this._router.init() 进行了初始化操作。随便找个 Vue 组件,打印一下其上的 _routerRoot:可以看到这是 Vue 的根组件。3.2 VueRouter在之前我们已经看过 src/index.js 了,这里来详细看一下 VueRouter 这个类// vue-router/src/index.jsexport default class VueRouter { constructor(options: RouterOptions = {}) { } / install 方法会调用 init 来初始化 / init(app: any / Vue组件实例 /) { } / createMatcher 方法返回的 match 方法 / match(raw: RawLocation, current?: Route, redirectedFrom?: Location) { } / 当前路由对象 / get currentRoute() { } / 注册 beforeHooks 事件 / beforeEach(fn: Function): Function { } / 注册 resolveHooks 事件 / beforeResolve(fn: Function): Function { } / 注册 afterHooks 事件 / afterEach(fn: Function): Function { } / onReady 事件 / onReady(cb: Function, errorCb?: Function) { } / onError 事件 / onError(errorCb: Function) { } / 调用 transitionTo 跳转路由 / push(location: RawLocation, onComplete?: Function, onAbort?: Function) { } / 调用 transitionTo 跳转路由 / replace(location: RawLocation, onComplete?: Function, onAbort?: Function) { } / 跳转到指定历史记录 / go(n: number) { } / 后退 / back() { } / 前进 / forward() { } / 获取路由匹配的组件 / getMatchedComponents(to?: RawLocation | Route) { } / 根据路由对象返回浏览器路径等信息 / resolve(to: RawLocation, current?: Route, append?: boolean) { } / 动态添加路由 / addRoutes(routes: Array<RouteConfig>) { }}VueRouter 类中除了一坨实例方法之外,主要关注的是它的构造函数和初始化方法 init。首先看看构造函数,其中的 mode 代表路由创建的模式,由用户配置与应用场景决定,主要有三种 History、Hash、Abstract,前两种我们已经很熟悉了,Abstract 代表非浏览器环境,比如 Node、weex 等;this.history 主要是路由的具体实例。实现如下:// vue-router/src/index.jsexport default class VueRouter { constructor(options: RouterOptions = {}) { let mode = options.mode || ‘hash’ // 路由匹配方式,默认为hash this.fallback = mode === ‘history’ && !supportsPushState && options.fallback !== false if (this.fallback) { mode = ‘hash’ } // 如果不支持history则退化为hash if (!inBrowser) { mode = ‘abstract’ } // 非浏览器环境强制abstract,比如node中 this.mode = mode switch (mode) { // 外观模式 case ‘history’: // history 方式 this.history = new HTML5History(this, options.base) break case ‘hash’: // hash 方式 this.history = new HashHistory(this, options.base, this.fallback) break case ‘abstract’: // abstract 方式 this.history = new AbstractHistory(this, options.base) break default: … } }}init 初始化方法是在 install 时的 Vue.mixin 所注册的 beforeCreate 钩子中调用的,可以翻上去看看;调用方式是 this._router.init(this),因为是在 Vue.mixin 里调用,所以这个 this 是当前的 Vue 实例。另外初始化方法需要负责从任一个路径跳转到项目中时的路由初始化,以 Hash 模式为例,此时还没有对相关事件进行绑定,因此在第一次执行的时候就要进行事件绑定与 popstate、hashchange 事件触发,然后手动触发一次路由跳转。实现如下:// vue-router/src/index.jsexport default class VueRouter { / install 方法会调用 init 来初始化 / init(app: any / Vue组件实例 */) { const history = this.history if (history instanceof HTML5History) { // 调用 history 实例的 transitionTo 方法 history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() // 设置 popstate/hashchange 事件监听 } history.transitionTo( // 调用 history 实例的 transitionTo 方法 history.getCurrentLocation(), // 浏览器 window 地址的 hash 值 setupHashListener, // 成功回调 setupHashListener // 失败回调 ) } }}除此之外,VueRouter 还有很多实例方法,用来实现各种功能的,剩下的将在系列文章分享 本文是系列文章,随后会更新后面的部分,共同进步vue-router 源码阅读 - 文件结构与注册机制网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出推介阅读:H5 History Api - MDNECMAScript 6 入门 - 阮一峰JS 静态类型检查工具 Flow - SegmentFault 思否JS 外观模式 - SegmentFault 思否前端路由跳转基本原理 - 掘金参考:Vue.js 技术揭秘 ...

February 24, 2019 · 5 min · jiezi

Laravel核心解读--完结篇

过去一年时间写了20多篇文章来探讨了我认为的Larave框架最核心部分的设计思路、代码实现。通过更新文章自己在软件设计、文字表达方面都有所提高,在刚开始决定写Laravel源码分析地文章的时候我地期望是自己和读者通过学习Laravel核心的代码能在软件设计上带来提高,这些提高主要是指两方面:通过学习Laravel核心的代码来辅助理解软件设计行业中经常提及的核心概念,通过学习像IocContainer、面向对象的五大原则SOLID 是怎么应用到框架设计中去的来指导应该如何去做软件开发设计。这方面对你的收益应该是跳出Laravel框架和PHP语言层面的,当你需要切换到其他框架和语言时这些收益仍会反馈给你。熟练掌握Laravel的使用,虽然很多人说框架只是一个工具不应该花太多时间在工具的研究上,但是现实时开发者群体大部分人并没有在头部的那几家大公司,也不架构师,我们多数的工作还是在写业务代码,那么既然你需要Laravel这个工具帮你完成每天的任务,那么为了尽可能高效率高质量的完成项目,确实是需要多了去看看框架的源码,了解一些框架常用的方法在positive和negative时的行为到底是什么(各种情况下的返回值和抛出的异常),知道怎么使用ORM才能让查询更高效等等,这些内容往往在框架的文档都是很少提及的,需要去看源码了解一下,如果你只会文档里提到的那些典型的用法显然不能算是熟练掌握的。Laravel整个框架设计到的内容有很多,其他的组件我也就不再一一去写文章梳理了, 相信你在认真看完这个系列的文章后,假如你在使用其他组件过程中遇到了诡异的问题,或者好奇框架是怎么帮你实现功能的?你完全有能力去梳理其他组件的源码实现来解决你的疑惑。为了大家阅读方便,我把这些源码学习的文章汇总到这里。类地反射和依赖注入IocContainer服务提供者FacadesRouteMiddleware控制器RequestResponseDatabase基础QueryBuilder模型CRUD模型关联事件系统Auth认证系统(基础介绍)Auth认证系统(实现细节)自定义你的Auth认证系统SessionCookieContracts契约加载ENV配置HTTP内核Console内核异常处理最后还是回到上面说的,框架只是工具如果想要在软件行业有所发展还是要把更多的精力投入到内功修炼上,所谓内功就是这些经过时间沉淀下来的基础知识,框架层出不穷,但是它们应用的基础知识却甚少改变。数据库、HTTP、算法和数据结构这些都是编程的内功,只有内功深厚了才能解决遇到的复杂问题。推荐几个我认为挺好的修炼内功的专栏给大家:程序员的数据基础课MySQL实战45讲数据结构与算法算法面试通关40讲Spring boot和Spring Cloud实战教程当然还有日新月异的前端知识也是需要会基础的用法的,最起码了解一下团队内部使用的前端框架的基础知识,这样对咱们做系统设计也会有帮助,最近在另外一个平台上看到分享的一个免费教程使用Laravel和Vue构建API驱动的应用,讲的非常好,希望Vue能快速入门的可以跟着教程一起动手练习练习。

February 22, 2019 · 1 min · jiezi

PHP 之 SplObjectStorage对象存储

定义php.net上的定义 The SplObjectStorage class provides a map from objects to data or, by ignoring data, an object set. This dual purpose can be useful in many cases involving the need to uniquely identify objects. 翻译:SplObjectStorage类提供从对象到数据映射功能,或者,通过忽视数据来提供对象集合,在很多涉及需要唯一对象的许多情况下,这两点是十分有用的。2. 接口说明class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess { //省略,下边详细解释以及翻译}此类实现了 Countable, Iterator, Serializable, ArrayAccess 四个接口,分别对应统计,迭代,序列化和数组访问,四个接口分别说明如下2.1 Countable此接口中只有一方法count(),看SplObjectStorage 类中此方法的说明(源码位置在php.jar/stubs/SPL/SPL_c1.php文件的1979行,可以用phpstorm按住command鼠标左键跳转过去)/** * Returns the number of objects in the storage //返回存储中的对象数量 * @link https://php.net/manual/en/splobjectstorage.count.php * @return int The number of objects in the storage. * @since 5.1.0 /public function count () {}翻译注释:Returns the number of objects in the storage //返回存储中的对象数量2.2 Iterator接口注释Interface for external iterators or objects that can be iterated 的翻译为外部迭代器或可以迭代的对象的接口,此接口中有5个方法分别如下(对应注释中有翻译)/* * Rewind the iterator to the first storage element //将迭代器回到第一个存储的元素 * @link https://php.net/manual/en/splobjectstorage.rewind.php * @return void * @since 5.1.0 /public function rewind () {}/* * Returns if the current iterator entry is valid //返回当前迭代器条目是否有效 * @link https://php.net/manual/en/splobjectstorage.valid.php * @return bool true if the iterator entry is valid, false otherwise. * @since 5.1.0 /public function valid () {}/* * Returns the index at which the iterator currently is//返回当前迭代对应的索引 * @link https://php.net/manual/en/splobjectstorage.key.php * @return int The index corresponding to the position of the iterator. * @since 5.1.0 /public function key () {}/* * Returns the current storage entry //返回当前存储的条目 * @link https://php.net/manual/en/splobjectstorage.current.php * @return object The object at the current iterator position. * @since 5.1.0 /public function current () {}/* * Move to the next entry //移到下一个条目 * @link https://php.net/manual/en/splobjectstorage.next.php * @return void * @since 5.1.0 /public function next () {}2.3 Serializable接口注释Interface for customized serializing.的翻译为用于自定义序列化的接口,此接口中有2个方法分别如下(对应注释中有翻译)/* * Serializes the storage //序列化存储 * @link https://php.net/manual/en/splobjectstorage.serialize.php * @return string A string representing the storage. //返回表示存储的字符串 * @since 5.2.2 /public function serialize () {}/* * Unserializes a storage from its string representation //从一个字符串表示中对存储反序列化 * @link https://php.net/manual/en/splobjectstorage.unserialize.php * @param string $serialized <p> * The serialized representation of a storage. * </p> * @return void * @since 5.2.2 /public function unserialize ($serialized) {}2.4 ArrayAccess接口注释Interface to provide accessing objects as arrays.的翻译为提供像访问数组一样访问对象的接口,此接口中有4个方法分别如下(对应注释中有翻译)/* * Checks whether an object exists in the storage //检查存储中是否存在找个对象 * @link https://php.net/manual/en/splobjectstorage.offsetexists.php * @param object $object <p> * The object to look for. * </p> * @return bool true if the object exists in the storage, * and false otherwise. * @since 5.3.0 /public function offsetExists ($object) {}/* * Associates data to an object in the storage //给存储中的对象赋值 * @link https://php.net/manual/en/splobjectstorage.offsetset.php * @param object $object <p> * The object to associate data with. * </p> * @param mixed $data [optional] <p> * The data to associate with the object. * </p> * @return void * @since 5.3.0 /public function offsetSet ($object, $data = null) {}/* * Removes an object from the storage //从存储中删除一个对象 * @link https://php.net/manual/en/splobjectstorage.offsetunset.php * @param object $object <p> * The object to remove. * </p> * @return void * @since 5.3.0 /public function offsetUnset ($object) {}/* * Returns the data associated with an <type>object</type> //从存储中获得一个对象 * @link https://php.net/manual/en/splobjectstorage.offsetget.php * @param object $object <p> * The object to look for. * </p> * @return mixed The data previously associated with the object in the storage. * @since 5.3.0 /public function offsetGet ($object) {}此接口的功能用代码简单说明如下$collection = new Supor\Collection();//假设有一Collection类,并且已经实现了ArrayAccess 接口$collection[‘a’] = 10;//我们可以像给数组赋值一样给此对象赋值var_dump($collection[‘a’]);//也可以使用取数组值的方法取得对象的属性 而不用 ‘->’//输出 int(10)3. 方法说明在每个方法的注释中有对应翻译,来说明这个方法的作用/* * Adds an object in the storage //向存储中添加一个对象 * @link https://php.net/manual/en/splobjectstorage.attach.php * @param object $object <p> * The object to add. * </p> * @param mixed $data [optional] <p> * The data to associate with the object. * </p> * @return void * @since 5.1.0 /public function attach ($object, $data = null) {}/* * Removes an object from the storage //从存储中删除一个对象 * @link https://php.net/manual/en/splobjectstorage.detach.php * @param object $object <p> * The object to remove. * </p> * @return void * @since 5.1.0 /public function detach ($object) {}/* * Checks if the storage contains a specific object //检查存储中是否包含特定的对象 * @link https://php.net/manual/en/splobjectstorage.contains.php * @param object $object <p> * The object to look for. * </p> * @return bool true if the object is in the storage, false otherwise. * @since 5.1.0 /public function contains ($object) {}/* * Adds all objects from another storage //添加一个存储中所有对象 * @link https://php.net/manual/en/splobjectstorage.addall.php * @param SplObjectStorage $storage <p> * The storage you want to import. * </p> * @return void * @since 5.3.0 /public function addAll ($storage) {}/* * Removes objects contained in another storage from the current storage //从当前存储中删除另一个存储中包含的对象 * @link https://php.net/manual/en/splobjectstorage.removeall.php * @param SplObjectStorage $storage <p> * The storage containing the elements to remove. * </p> * @return void * @since 5.3.0 /public function removeAll ($storage) {}/* 从当前存储中删除另一个存储中不包含的对象 * Removes all objects except for those contained in another storage from the current storage * @link https://php.net/manual/en/splobjectstorage.removeallexcept.php * @param SplObjectStorage $storage <p> * The storage containing the elements to retain in the current storage. * </p> * @return void * @since 5.3.6 /public function removeAllExcept ($storage) {}/ * Returns the data associated with the current iterator entry //返回当前迭代器条目相关的数据 * @link https://php.net/manual/en/splobjectstorage.getinfo.php * @return mixed The data associated with the current iterator position. * @since 5.3.0 /public function getInfo () {}/* * Sets the data associated with the current iterator entry//设置当前迭代器条目相关的数据 * @link https://php.net/manual/en/splobjectstorage.setinfo.php * @param mixed $data <p> * The data to associate with the current iterator entry. * </p> * @return void * @since 5.3.0 /public function setInfo ($data) {}/* * Calculate a unique identifier for the contained objects //给包含的对象计算一个唯一ID * @link https://php.net/manual/en/splobjectstorage.gethash.php * @param $object <p> * object whose identifier is to be calculated. * @return string A string with the calculated identifier. * @since 5.4.0*/public function getHash($object) {} 4. 使用SplObjectStorage的对象操作//假设有三个Collection对象$collection1 = new Supor\Collection([‘a’ => ‘aa’, ‘b’ => ‘bb’]);$collection2 = new Supor\Collection([‘c’ => ‘cc’, ’d’ => ‘dd’]);$collection3 = new Supor\Collection([’e’ => ’ee’, ‘f’ => ‘ff’]);$splStorage = new SplObjectStorage();$splStorage->attach($collection1);//传入相同的对象会被替代$splStorage->attach($collection1);$splStorage->attach($collection2);$splStorage->attach($collection3);//统计$splStorage中有多少个对象$count = $splStorage->count();var_dump($count);//得到某一对象的哈希值$hash1 = $splStorage->getHash($collection1);var_dump($hash1);//检查存储中是否包含$collection3$contains3 = $splStorage->contains($collection3);var_dump($contains3);//将指针后移$splStorage->next();//读取移动后的key$key = $splStorage->key();var_dump($key);//删除某个对象$splStorage->detach($collection3);//统计删除后的数量$count = $splStorage->count();var_dump($count);//遍历$splStorage所有对象//遍历前先重置一下指针$splStorage->rewind();//当当前迭代器条目返回真时while ($splStorage->valid()) { //打印当前条目 var_dump($splStorage->current()); //指针后移 $splStorage->next();}代码执行结果如下:

January 24, 2019 · 5 min · jiezi

Vue源码探究-组件的持久活跃

Vue源码探究-组件的持久活跃本篇代码位于vue/src/core/components/keep-alive.js较新版本的Vue增加了一个内置组件 keep-alive,用于存储组件状态,即便失活也能保持现有状态不变,切换回来的时候不会恢复到初始状态。由此可知,路由切换的钩子所触发的事件处理是无法适用于 keep-alive 组件的,那如果需要根据失活与否来给予组件事件通知,该怎么办呢?如前篇所述,keep-alive 组件有两个特有的生命周期钩子 activated 和 deactivated,用来响应失活状态的事件处理。来看看 keep-alive 组件的实现,代码文件位于 components 里,目前入口文件里也只有 keep-alive 这一个内置组件,但这个模块的分离,会不会预示着官方将在未来开发更多具有特殊功能的内置组件呢?// 导入辅助函数import { isRegExp, remove } from ‘shared/util’import { getFirstComponentChild } from ‘core/vdom/helpers/index’// 定义VNodeCache静态类型// 它是一个包含key名和VNode键值对的对象,可想而知它是用来存储组件的type VNodeCache = { [key: string]: ?VNode };// 定义getComponentName函数,用于获取组件名称,传入组件配置对象function getComponentName (opts: ?VNodeComponentOptions): ?string { // 先尝试获取配置对象中定义的name属性,或无则获取标签名称 return opts && (opts.Ctor.options.name || opts.tag)}// 定义matches函数,进行模式匹配,传入匹配的模式类型数据和name属性function matches (pattern: string | RegExp | Array<string>, name: string): boolean { // 匹配数组模式 if (Array.isArray(pattern)) { // 使用数组方法查找name,返回结果 return pattern.indexOf(name) > -1 } else if (typeof pattern === ‘string’) { // 匹配字符串模式 // 将字符串转换成数组查找name,返回结果 return pattern.split(’,’).indexOf(name) > -1 } else if (isRegExp(pattern)) { // 匹配正则表达式 // 使用正则匹配name,返回结果 return pattern.test(name) } / istanbul ignore next */ // 未匹配正确模式则返回false return false}// 定义pruneCache函数,修剪keep-alive组件缓存对象// 接受keep-alive组件实例和过滤函数function pruneCache (keepAliveInstance: any, filter: Function) { // 获取组件的cache,keys,_vnode属性 const { cache, keys, _vnode } = keepAliveInstance // 遍历cache对象 for (const key in cache) { // 获取缓存资源 const cachedNode: ?VNode = cache[key] // 如果缓存资源存在 if (cachedNode) { // 获取该资源的名称 const name: ?string = getComponentName(cachedNode.componentOptions) // 当名称存在 且不匹配缓存过滤时 if (name && !filter(name)) { // 执行修剪缓存资源操作 pruneCacheEntry(cache, key, keys, _vnode) } } }}// 定义pruneCacheEntry函数,修剪缓存条目// 接受keep-alive实例的缓存对象和键名缓存对象,资源键名和当前资源function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode) { // 检查缓存对象里是否已经有以key值存储的资源 const cached = cache[key] // 如果有旧资源并且没有传入新资源参数或新旧资源标签不同 if (cached && (!current || cached.tag !== current.tag)) { // 销毁该资源 cached.componentInstance.$destroy() } // 置空key键名存储资源 cache[key] = null // 移除key值的存储 remove(keys, key)}// 定义模式匹配接收的数据类型const patternTypes: Array<Function> = [String, RegExp, Array]// 导出keep-alive组件实例的配置对象export default { // 定义组件名称 name: ‘keep-alive’, // 设置abstract属性 abstract: true, // 设置组件接收的属性 props: { // include用于包含模式匹配的资源,启用缓存 include: patternTypes, // exclude用于排除模式匹配的资源,不启用缓存 exclude: patternTypes, // 最大缓存数 max: [String, Number] }, created () { // 实例创建时定义cache属性为空对象,用于存储资源 this.cache = Object.create(null) // 设置keys数组,用于存储资源的key名 this.keys = [] }, destroyed () { // 实例销毁时一并销毁存储的资源并清空缓存对象 for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // DOM加载完成后,观察include和exclude属性的变动 // 回调执行修改缓存对象的操作 this.$watch(‘include’, val => { pruneCache(this, name => matches(val, name)) }) this.$watch(’exclude’, val => { pruneCache(this, name => !matches(val, name)) }) }, render () { // 实例渲染函数 // 获取keep-alive包含的子组件结构 // keep-alive组件并不渲染任何真实DOM节点,只渲染嵌套在其中的组件资源 const slot = this.$slots.default // 将嵌套组件dom结构转化成虚拟节点 const vnode: VNode = getFirstComponentChild(slot) // 获取嵌套组件的配置对象 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 如果配置对象存在 if (componentOptions) { // 检查是否缓存的模式匹配 // check pattern // 获取嵌套组件名称 const name: ?string = getComponentName(componentOptions) // 获取传入keep-alive组件的include和exclude属性 const { include, exclude } = this // 如果有included,且该组件不匹配included中资源 // 或者有exclude。且该组件匹配exclude中的资源 // 则返回虚拟节点,不继续执行缓存 if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } // 获取keep-alive组件的cache和keys对象 const { cache, keys } = this // 获取嵌套组件虚拟节点的key const key: ?string = vnode.key == null // 同样的构造函数可能被注册为不同的本地组件,所以cid不是判断的充分条件 // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? ::${componentOptions.tag} : ‘’) : vnode.key // 如果缓存对象里有以key值存储的组件资源 if (cache[key]) { // 设置当前嵌套组件虚拟节点的componentInstance属性 vnode.componentInstance = cache[key].componentInstance // make current key freshest // 从keys中移除旧key,添加新key remove(keys, key) keys.push(key) } else { // 缓存中没有该资源,则直接存储资源,并存储key值 cache[key] = vnode keys.push(key) // 如果设置了最大缓存资源数,从最开始的序号开始删除存储资源 // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 设置该资源虚拟节点的keepAlive标识 vnode.data.keepAlive = true } // 返回虚拟节点或dom节点 return vnode || (slot && slot[0]) }}keep-alive 组件的实现也就这百来行代码,分为两部分:第一部分是定义一些处理具体实现的函数,比如修剪缓存对象存储资源的函数,匹配组件包含和过滤存储的函数;第二部分是导出一份 keep-alive 组件的应用配置对象,仔细一下这跟我们在实际中使用的方式是一样的,但这个组件具有已经定义好的特殊功能,就是缓存嵌套在它之中的组件资源,实现持久活跃。那么实现原理是什么,在代码里可以清楚得看到,这里是利用转换组件真实DOM节点为虚拟节点将其存储到 keep-alive 实例的 cache 对象中,另外也一并存储了资源的 key 值方便查找,然后在渲染时检测其是否符合缓存条件再进行渲染。keep-alive 的实现就是以上这样简单。最初一瞥此段代码时,不知所云。然而当开始逐步分析代码之后,才发现原来只是没有仔细去看,误以为很深奥,由此可见,任何不用心的行为都不能直抵事物的本质,这是借由探索这一小部分代码而得到的教训。因为在实际中有使用过这个功能,所以体会更深,有时候难免会踩到一些坑,看了源码的实现之后,发现原来是自己使用方式不对,所以了解所用轮子的实现还是很有必要的。 ...

January 9, 2019 · 3 min · jiezi

Laravel核心解读--Console内核

Console内核上一篇文章我们介绍了Laravel的HTTP内核,详细概述了网络请求从进入应用到应用处理完请求返回HTTP响应整个生命周期中HTTP内核是如何调动Laravel各个核心组件来完成任务的。除了处理HTTP请求一个健壮的应用经常还会需要执行计划任务、异步队列这些。Laravel为了能让应用满足这些场景设计了artisan工具,通过artisan工具定义各种命令来满足非HTTP请求的各种场景,artisan命令通过Laravel的Console内核来完成对应用核心组件的调度来完成任务。 今天我们就来学习一下Laravel Console内核的核心代码。内核绑定跟HTTP内核一样,在应用初始化阶有一个内核绑定的过程,将Console内核注册到应用的服务容器里去,还是引用上一篇文章引用过的bootstrap/app.php里的代码<?php// 第一部分: 创建应用实例$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));// 第二部分: 完成内核绑定$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);// console内核绑定$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);return $app;Console内核 \App\Console\Kernel继承自Illuminate\Foundation\Console, 在Console内核中我们可以注册artisan命令和定义应用里要执行的计划任务。/*** Define the application’s command schedule.** @param \Illuminate\Console\Scheduling\Schedule $schedule* @return void*/protected function schedule(Schedule $schedule){ // $schedule->command(‘inspire’) // ->hourly();}/*** Register the commands for the application.** @return void*/protected function commands(){ $this->load(DIR.’/Commands’); require base_path(‘routes/console.php’);}在实例化Console内核的时候,内核会定义应用的命令计划任务(shedule方法中定义的计划任务)public function __construct(Application $app, Dispatcher $events){ if (! defined(‘ARTISAN_BINARY’)) { define(‘ARTISAN_BINARY’, ‘artisan’); } $this->app = $app; $this->events = $events; $this->app->booted(function () { $this->defineConsoleSchedule(); });}应用解析Console内核查看aritisan文件的源码我们可以看到, 完成Console内核绑定的绑定后,接下来就会通过服务容器解析出console内核对象$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);$status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, new Symfony\Component\Console\Output\ConsoleOutput);执行命令任务解析出Console内核对象后,接下来就要处理来自命令行的命令请求了, 我们都知道PHP是通过全局变量$_SERVER[‘argv’]来接收所有的命令行输入的, 和命令行里执行shell脚本一样(在shell脚本里可以通过$0获取脚本文件名,$1 $2这些依次获取后面传递给shell脚本的参数选项)索引0对应的是脚本文件名,接下来依次是命令行里传递给脚本的所有参数选项,所以在命令行里通过artisan脚本执行的命令,在artisan脚本中$_SERVER[‘argv’]数组里索引0对应的永远是artisan这个字符串,命令行里后面的参数会依次对应到$_SERVER[‘argv’]数组后续的元素里。因为artisan命令的语法中可以指定命令参数选项、有的选项还可以指定实参,为了减少命令行输入参数解析的复杂度,Laravel使用了Symfony\Component\Console\Input对象来解析命令行里这些参数选项(shell脚本里其实也是一样,会通过shell函数getopts来解析各种格式的命令行参数输入),同样地Laravel使用了Symfony\Component\Console\Output对象来抽象化命令行的标准输出。引导应用在Console内核的handle方法里我们可以看到和HTTP内核处理请求前使用bootstrapper程序引用应用一样在开始处理命令任务之前也会有引导应用这一步操作其父类 「IlluminateFoundationConsoleKernel」 内部定义了属性名为 「bootstrappers」 的 引导程序 数组:protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\SetRequestForConsole::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class,];数组中包括的引导程序基本上和HTTP内核中定义的引导程序一样, 都是应用在初始化阶段要进行的环境变量、配置文件加载、注册异常处理器、设置Console请求、注册应用中的服务容器、Facade和启动服务。其中设置Console请求是唯一区别于HTTP内核的一个引导程序。执行命令执行命令是通过Console Application来执行的,它继承自Symfony框架的Symfony\Component\Console\Application类, 通过对应的run方法来执行命令。name Illuminate\Foundation\Console;class Kernel implements KernelContract{ public function handle($input, $output = null) { try { $this->bootstrap(); return $this->getArtisan()->run($input, $output); } catch (Exception $e) { $this->reportException($e); $this->renderException($output, $e); return 1; } catch (Throwable $e) { $e = new FatalThrowableError($e); $this->reportException($e); $this->renderException($output, $e); return 1; } }}namespace Symfony\Component\Console;class Application{ //执行命令 public function run(InputInterface $input = null, OutputInterface $output = null) { …… try { $exitCode = $this->doRun($input, $output); } catch { …… } …… return $exitCode; } public function doRun(InputInterface $input, OutputInterface $output) { //解析出命令名称 $name = $this->getCommandName($input); //解析出入参 if (!$name) { $name = $this->defaultCommand; $definition = $this->getDefinition(); $definition->setArguments(array_merge( $definition->getArguments(), array( ‘command’ => new InputArgument(‘command’, InputArgument::OPTIONAL, $definition->getArgument(‘command’)->getDescription(), $name), ) )); } …… try { //通过命令名称查找出命令类(命名空间、类名等) $command = $this->find($name); } …… //运行命令类 $exitCode = $this->doRunCommand($command, $input, $output); return $exitCode; } protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) { …… //执行命令类的run方法来处理任务 $exitCode = $command->run($input, $output); …… return $exitcode; }}执行命令时主要有四步操作:通过命令行输入解析出命令名称和参数选项。通过命令名称查找命令类的命名空间和类名。执行命令类的run方法来完成任务处理并返回状态码。和命令行脚本的规范一样,如果执行命令任务程序成功会返回0, 抛出异常退出则返回1。还有就是打开命令类后我们可以看到并没有run方法,我们把处理逻辑都写在了handle方法中,仔细查看代码会发现run方法定义在父类中,在run方法会中会调用子类中定义的handle方法来完成任务处理。 严格遵循了面向对象程序设计的SOLID 原则。结束应用执行完命令程序返回状态码后, 在artisan中会直接通过exit($status)函数输出状态码并结束PHP进程,接下来shell进程会根据返回的状态码是否为0来判断脚本命令是否执行成功。到这里通过命令行开启的程序进程到这里就结束了,跟HTTP内核一样Console内核在整个生命周期中也是负责调度,只不过Http内核最终将请求落地到了Controller程序中而Console内核则是将命令行请求落地到了Laravel中定义的各种命令类程序中,然后在命令类里面我们就可以写其他程序一样自由地使用Laravel中的各个组件和注册到服务容器里的服务了。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

December 1, 2018 · 2 min · jiezi

Scrapy-实用的命令行工具实现方法

其实这篇文章是scrapy源码学习的(一),加载器那篇才是(二)scrapy的命令行工具本文环境:wind7 64bitspython 3.7scrapy 1.5.1scrapy拥有非常灵活的低耦合的命令行工具,如果自己想要重新实现覆盖掉scrapy自带的命令也是可以的。使用它的命令行工具可以大致分为两种情况:在创建的project路径下不在project路径下先看下不在scrapy项目路径下的命令行有哪些:Scrapy 1.5.1 - no active projectUsage: scrapy <command> [options] [args]Available commands: bench Run quick benchmark test fetch Fetch a URL using the Scrapy downloader genspider Generate new spider using pre-defined templates runspider Run a self-contained spider (without creating a project) settings Get settings values shell Interactive scraping console startproject Create new project version Print Scrapy version view Open URL in browser, as seen by Scrapy [ more ] More commands available when run from project directoryUse “scrapy <command> -h” to see more info about a command在项目路径下的命令行新增了check、crawl、edit、list、parse这些命令,具体:Scrapy 1.5.1 - project: myspider01Usage: scrapy <command> [options] [args]Available commands: bench Run quick benchmark test check Check spider contracts crawl Run a spider edit Edit spider fetch Fetch a URL using the Scrapy downloader genspider Generate new spider using pre-defined templates list List available spiders parse Parse URL (using its spider) and print the results runspider Run a self-contained spider (without creating a project) settings Get settings values shell Interactive scraping console startproject Create new project version Print Scrapy version view Open URL in browser, as seen by ScrapyUse “scrapy <command> -h” to see more info about a command也即是说scrapy可以根据当前路径是否是scrapy项目路径来判断提供可用的命令给用户。创建一个scrapy项目在当前路径下创建一个scrapy项目,DOS下输入:scrapy startproject myproject可以查看刚刚创建的项目myproject的目录结构:├── scrapy.cfg //scrapy项目配置文件├── myproject ├── spiders // 爬虫脚本目录 ├── init.py ├── init.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py // 项目设置 可以断定,在我们使用"startproject"这个scrapy命令时,scrapy会把一些项目默认模板拷贝到我们创建项目的路径下,从而生成我们看到的类似上面的目录结构。我们可以打开scrapy的包,看看这些模板在哪个地方。切换至scrapy的安装路径(比如:..Python37Libsite-packagesscrapy),可以看到路径下有templates文件夹,而此文件夹下的project文件夹便是创建项目时拷贝的默认模板存放目录。那么scrapy是怎么实现类似“startproject”这样的命令的呢?打开scrapy源码找到入口scrapy是使用命令行来启动脚本的(当然也可以调用入口函数来启动),查看其命令行实现流程必须先找到命令行实行的入口点,这个从其安装文件setup.py中找到。打开setup.py 找到entry_points:… entry_points={ ‘console_scripts’: [‘scrapy = scrapy.cmdline:execute’] },…可以看到scrapy开头的命令皆由模块scrapy.cmdline的execute函数作为入口函数。分析入口函数先浏览一下execute函数源码,这里只贴主要部分:def execute(argv=None, settings=None): if argv is None: argv = sys.argv … #主要部分:获取当前项目的设置 if settings is None: settings = get_project_settings() # set EDITOR from environment if available try: editor = os.environ[‘EDITOR’] except KeyError: pass else: settings[‘EDITOR’] = editor #检查提醒已不被支持的设置项目 check_deprecated_settings(settings) … #主要部分:判断是否在项目路径下,加载可见命令,解析命令参数 inproject = inside_project() cmds = _get_commands_dict(settings, inproject) cmdname = _pop_command_name(argv) parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter(), \ conflict_handler=‘resolve’) if not cmdname: _print_commands(settings, inproject) sys.exit(0) elif cmdname not in cmds: _print_unknown_command(settings, cmdname, inproject) sys.exit(2) cmd = cmds[cmdname] parser.usage = “scrapy %s %s” % (cmdname, cmd.syntax()) parser.description = cmd.long_desc() settings.setdict(cmd.default_settings, priority=‘command’) cmd.settings = settings cmd.add_options(parser) opts, args = parser.parse_args(args=argv[1:]) _run_print_help(parser, cmd.process_options, args, opts) cmd.crawler_process = CrawlerProcess(settings) _run_print_help(parser, _run_command, cmd, args, opts) sys.exit(cmd.exitcode)阅读cmdline.py的execute函数,大概了解了命令行实现的基本流程:1.获取命令参数命令参数的获取可以通过两种方式传递:第一种是调用execute,比如:from scrapy.cmdline import executeexecute(argv=[‘scrapy’,‘startproject’,‘myproject’,’-a’,‘xxxx’])这样就相当于第二种方式:命令控制台执行scrapy startproject myproject -a xxxx传递的参数都是[‘scrapy’,‘startproject’,‘myproject’,’-a’,‘xxxx’]2.获取scrapy项目配置如果当前不是调用的方式传递settings给execute入口,而是一般的命令控制台启动scrapy,那么scrapy会在当前路径下搜索加载可能存在的项目配置文件。主要是通过函数get_project_settings执行。ENVVAR = ‘SCRAPY_SETTINGS_MODULE’def get_project_settings(): #获取配置 if ENVVAR not in os.environ: #初始化获取项目的default级配置,即是scrapy生成的默认配置 project = os.environ.get(‘SCRAPY_PROJECT’, ‘default’) #初始化项目环境,设置系统环境变量SCRAPY_SETTINGS_MODULE的值为配置模块路径 init_env(project) settings = Settings() settings_module_path = os.environ.get(ENVVAR) if settings_module_path: settings.setmodule(settings_module_path, priority=‘project’) … return settings获取的配置文件主要是scrapy.cfg,我们可以看下他的内容:[settings]default = myproject.settings[deploy]#url = http://localhost:6800/project = myproject在生成项目myproject的时候,这个配置文件就已经指定了项目设置模块的路径"myproject.settings",所以上面的get_project_settings函数获取便是配置文件settings字段中的default键值,然后导入该设置模块来生成配置。具体实现在init_env函数中。def init_env(project=‘default’, set_syspath=True): “““在当前项目路径下初始化项目环境. 并且通过配置系统环境来让python能够定位配置模块 "”” #在项目路径下进入命令行,才能准确获取配置 #获取可能存在scrapy.cfg配置文件的模块路径 cfg = get_config() #获取到配置文件后设置系统环境变量SCRAPY_SETTINGS_MODULE为配置模块路径, #如: myproject.settings,默认项目级别均为default,即是配置文件字段settings中的键 if cfg.has_option(‘settings’, project): os.environ[‘SCRAPY_SETTINGS_MODULE’] = cfg.get(‘settings’, project) #将最近的scrapy.cfg模块路径放入系统路径使Python能够找到该模块导入 closest = closest_scrapy_cfg() if closest: projdir = os.path.dirname(closest) if set_syspath and projdir not in sys.path: #加入项目设置模块路径到系统路径让Python能够定位到 sys.path.append(projdir)def get_config(use_closest=True): "”" SafeConfigParser.read(filenames) 尝试解析文件列表,如果解析成功返回文件列表。如果filenames是string或Unicode string, 将会按单个文件来解析。如果在filenames中的文件不能打开,该文件将被忽略。这样设计的目的是, 让你能指定本地有可能是配置文件的列表(例如,当前文件夹,用户的根目录,及一些全系统目录), 所以在列表中存在的配置文件都会被读取。""" sources = get_sources(use_closest) cfg = SafeConfigParser() cfg.read(sources) return cfgdef get_sources(use_closest=True): ‘‘‘先获取用户的根目录,及一些全系统目录下的有scrapy.cfg的路径加入sources 最后如果使用最靠近当前路径的scrapy.cfg的标志use_closest为True时加入该scrapy.cfg路径’’’ xdg_config_home = os.environ.get(‘XDG_CONFIG_HOME’) or \ os.path.expanduser(’/.config’) sources = [’/etc/scrapy.cfg’, r’c:\scrapy\scrapy.cfg’, xdg_config_home + ‘/scrapy.cfg’, os.path.expanduser(’/.scrapy.cfg’)] if use_closest: sources.append(closest_scrapy_cfg()) return sourcesdef closest_scrapy_cfg(path=’.’, prevpath=None): """ 搜索最靠近当前当前路径的scrapy.cfg配置文件并返回其路径。 搜索会按照当前路径–>父路径的递归方式进行,到达顶层没有结果则返回‘’ """ if path == prevpath: return ’’ path = os.path.abspath(path) cfgfile = os.path.join(path, ‘scrapy.cfg’) if os.path.exists(cfgfile): return cfgfile return closest_scrapy_cfg(os.path.dirname(path), path)通过init_env来设置os.environ[‘SCRAPY_SETTINGS_MODULE’]的值,这样的话#将项目配置模块路径设置进系统环境变量os.environ[‘SCRAPY_SETTINGS_MODULE’] = ‘myproject.settings’初始化后返回到原先的get_project_settings,生成一个设置类Settings实例,然后再将设置模块加载进实例中完成项目配置的获取这一动作。3.判断是否在scrapy项目路径下判断当前路径是否是scrapy项目路径,其实很简单,因为前面已经初始化过settings,如果在项目路径下,那么os.environ[‘SCRAPY_SETTINGS_MODULE’]的值就已经被设置了,现在只需要判断这个值是否存在便可以判断是否在项目路径下。具体实现在inside_project函数中实现:def inside_project(): scrapy_module = os.environ.get(‘SCRAPY_SETTINGS_MODULE’) if scrapy_module is not None: try: import_module(scrapy_module) except ImportError as exc: warnings.warn(“Cannot import scrapy settings module %s: %s” % (scrapy_module, exc)) else: return True return bool(closest_scrapy_cfg())4.获取命令集合,命令解析知道了当前是否在项目路径下,还有初始化了项目配置,这个时候就可以获取到在当前路径下能够使用的命令行有哪些了。获取当前可用命令集合比较简单,直接加载模块scrapy.commands下的所有命令行类,判断是否需要在项目路径下才能使用该命令,是的话直接实例化加入一个字典(格式:<命令名称>:<命令实例>)返回,具体实现通过_get_commands_dict:def _get_commands_dict(settings, inproject): cmds = _get_commands_from_module(‘scrapy.commands’, inproject) cmds.update(_get_commands_from_entry_points(inproject)) #如果有新的命令行模块在配置中设置,会自动载入 cmds_module = settings[‘COMMANDS_MODULE’] if cmds_module: cmds.update(_get_commands_from_module(cmds_module, inproject)) return cmdsdef _get_commands_from_module(module, inproject): d = {} for cmd in _iter_command_classes(module): #判断是否需要先创建一个项目才能使用该命令, #即目前是否位于项目路径下(inproject)的可用命令有哪些,不是的有哪些 if inproject or not cmd.requires_project: cmdname = cmd.module.split(’.’)[-1] #获取该命令名称并实例化 加入返回字典 #返回{<命令名称>:<命令实例>} d[cmdname] = cmd() return d def _iter_command_classes(module_name): #获取scrapy.commands下所有模块文件中属于ScrapyCommand子类的命令行类 for module in walk_modules(module_name): for obj in vars(module).values(): if inspect.isclass(obj) and \ issubclass(obj, ScrapyCommand) and \ obj.module == module.name and \ not obj == ScrapyCommand: yield obj其中判断是否是命令类的关键在于该命令模块中的命令类是否继承了命令基类ScrapyCommand,只要继承了该基类就可以被检测到。这有点类似接口的作用,ScrapyCommand基类其实就是一个标识类(该类比较简单,可以查看基类代码)。而该基类中有一个requires_project标识,标识是否需要在scrapy项目路径下才能使用该命令,判断该值就可以获得当前可用命令。获取到了可用命令集合,接下来会加载Python自带的命令行解析模块optparser.OptionParser的命令行参数解析器,通过实例化获取该parser,传入当前命令实例的add_options属性方法中来加载当前命令实例附加的解析命令,如:-a xxx, -p xxx, –dir xxx 之类的类似Unix命令行的命令。这些都是通过parser来实现解析。5.判断当前命令是否可用其实在加载解析器之前,会去判断当前的用户输入命令是否是合法的,是不是可用的,如果可用会接下去解析执行该命令,不可用便打印出相关的帮助提示。比如:Usage===== scrapy startproject <project_name> [project_dir]Create new projectOptions=======–help, -h show this help message and exitGlobal Options—————-logfile=FILE log file. if omitted stderr will be used–loglevel=LEVEL, -L LEVEL log level (default: DEBUG)–nolog disable logging completely–profile=FILE write python cProfile stats to FILE–pidfile=FILE write process ID to FILE–set=NAME=VALUE, -s NAME=VALUE set/override setting (may be repeated)–pdb enable pdb on failure至此,scrapy命令行工具的实现流程基本结束。学习点scrapy的命令行工具实现了低耦合,需要删减增加哪个命令行只需要在scrapy.commands模块中修改增删就可以实现。但是实现的关键在于该模块下的每一个命令行类都得继承ScrapyCommand这个基类,这样在导入的时候才能有所判断,所以我说ScrapyCommand是个标识类。基于标识类来实现模块的低耦合。下一篇将会记录根据借鉴scrapy命令行工具实现方法来实现自己的命令行 ...

November 21, 2018 · 3 min · jiezi

Laravel核心解读--HTTP内核

Http KernelHttp Kernel是Laravel中用来串联框架的各个核心组件来网络请求的,简单的说只要是通过public/index.php来启动框架的都会用到Http Kernel,而另外的类似通过artisan命令、计划任务、队列启动框架进行处理的都会用到Console Kernel, 今天我们先梳理一下Http Kernel做的事情。内核绑定既然Http Kernel是Laravel中用来串联框架的各个部分处理网络请求的,我们来看一下内核是怎么加载到Laravel中应用实例中来的,在public/index.php中我们就会看见首先就会通过bootstrap/app.php这个脚手架文件来初始化应用程序:下面是 bootstrap/app.php 的代码,包含两个主要部分创建应用实例和绑定内核至 APP 服务容器<?php// 第一部分: 创建应用实例$app = new Illuminate\Foundation\Application( realpath(DIR.’/../’));// 第二部分: 完成内核绑定$app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class);$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class);$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class);return $app;HTTP 内核继承自 IlluminateFoundationHttpKernel类,在 HTTP 内核中 内它定义了中间件相关数组, 中间件提供了一种方便的机制来过滤进入应用的 HTTP 请求和加工流出应用的HTTP响应。<?phpnamespace App\Http;use Illuminate\Foundation\Http\Kernel as HttpKernel;class Kernel extends HttpKernel{ /** * The application’s global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array / protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\TrustProxies::class, ]; /* * The application’s route middleware groups. * * @var array / protected $middlewareGroups = [ ‘web’ => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ‘api’ => [ ’throttle:60,1’, ‘bindings’, ], ]; /* * The application’s route middleware. * * These middleware may be assigned to groups or used individually. * * @var array / protected $routeMiddleware = [ ‘auth’ => \Illuminate\Auth\Middleware\Authenticate::class, ‘auth.basic’ => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, ‘bindings’ => \Illuminate\Routing\Middleware\SubstituteBindings::class, ‘can’ => \Illuminate\Auth\Middleware\Authorize::class, ‘guest’ => \App\Http\Middleware\RedirectIfAuthenticated::class, ’throttle’ => \Illuminate\Routing\Middleware\ThrottleRequests::class, ];}在其父类 「IlluminateFoundationHttpKernel」 内部定义了属性名为 「bootstrappers」 的 引导程序 数组:protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class,];引导程序组中 包括完成环境检测、配置加载、异常处理、Facades 注册、服务提供者注册、启动服务这六个引导程序。有关中间件和引导程序相关内容的讲解可以浏览我们之前相关章节的内容。应用解析内核在将应用初始化阶段将Http内核绑定至应用的服务容器后,紧接着在public/index.php中我们可以看到使用了服务容器的make方法将Http内核实例解析了出来:$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);在实例化内核时,将在 HTTP 内核中定义的中间件注册到了 路由器,注册完后就可以在实际处理 HTTP 请求前调用路由上应用的中间件实现过滤请求的目的:namespace Illuminate\Foundation\Http;…class Kernel implements KernelContract{ /* * Create a new HTTP kernel instance. * * @param \Illuminate\Contracts\Foundation\Application $app * @param \Illuminate\Routing\Router $router * @return void / public function __construct(Application $app, Router $router) { $this->app = $app; $this->router = $router; $router->middlewarePriority = $this->middlewarePriority; foreach ($this->middlewareGroups as $key => $middleware) { $router->middlewareGroup($key, $middleware); } foreach ($this->routeMiddleware as $key => $middleware) { $router->aliasMiddleware($key, $middleware); } }}namespace Illuminate/Routing;class Router implements RegistrarContract, BindingRegistrar{ /* * Register a group of middleware. * * @param string $name * @param array $middleware * @return $this / public function middlewareGroup($name, array $middleware) { $this->middlewareGroups[$name] = $middleware; return $this; } /* * Register a short-hand name for a middleware. * * @param string $name * @param string $class * @return $this / public function aliasMiddleware($name, $class) { $this->middleware[$name] = $class; return $this; }}处理HTTP请求通过服务解析完成Http内核实例的创建后就可以用HTTP内核实例来处理HTTP请求了//public/index.php$response = $kernel->handle( $request = Illuminate\Http\Request::capture());在处理请求之前会先通过Illuminate\Http\Request的 capture() 方法以进入应用的HTTP请求的信息为基础创建出一个 Laravel Request请求实例,在后续应用剩余的生命周期中Request请求实例就是对本次HTTP请求的抽象,关于Laravel Request请求实例的讲解可以参考以前的章节。将HTTP请求抽象成Laravel Request请求实例后,请求实例会被传导进入到HTTP内核的handle方法内部,请求的处理就是由handle方法来完成的。namespace Illuminate\Foundation\Http;class Kernel implements KernelContract{ /* * Handle an incoming HTTP request. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response / public function handle($request) { try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } $this->app[’events’]->dispatch( new Events\RequestHandled($request, $response) ); return $response; }}handle 方法接收一个请求对象,并最终生成一个响应对象。其实handle方法我们已经很熟悉了在讲解很多模块的时候都是以它为出发点逐步深入到模块的内部去讲解模块内的逻辑的,其中sendRequestThroughRouter方法在服务提供者和中间件都提到过,它会加载在内核中定义的引导程序来引导启动应用然后会将使用Pipeline对象传输HTTP请求对象流经框架中定义的HTTP中间件们和路由中间件们来完成过滤请求最终将请求传递给处理程序(控制器方法或者路由中的闭包)由处理程序返回相应的响应。关于handle方法的注解我直接引用以前章节的讲解放在这里,具体更详细的分析具体是如何引导启动应用以及如何将传输流经各个中间件并到达处理程序的内容请查看服务提供器、中间件还有路由这三个章节。protected function sendRequestThroughRouter($request){ $this->app->instance(‘request’, $request); Facade::clearResolvedInstance(‘request’); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter());} /引导启动Laravel应用程序1. DetectEnvironment 检查环境2. LoadConfiguration 加载应用配置3. ConfigureLogging 配置日至4. HandleException 注册异常处理的Handler5. RegisterFacades 注册Facades 6. RegisterProviders 注册Providers 7. BootProviders 启动Providers/public function bootstrap(){ if (! $this->app->hasBeenBootstrapped()) { /**依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数 $bootstrappers = [ ‘Illuminate\Foundation\Bootstrap\DetectEnvironment’, ‘Illuminate\Foundation\Bootstrap\LoadConfiguration’, ‘Illuminate\Foundation\Bootstrap\ConfigureLogging’, ‘Illuminate\Foundation\Bootstrap\HandleExceptions’, ‘Illuminate\Foundation\Bootstrap\RegisterFacades’, ‘Illuminate\Foundation\Bootstrap\RegisterProviders’, ‘Illuminate\Foundation\Bootstrap\BootProviders’, ];/ $this->app->bootstrapWith($this->bootstrappers()); }}发送响应经过上面的几个阶段后我们最终拿到了要返回的响应,接下来就是发送响应了。//public/index.php$response = $kernel->handle( $request = Illuminate\Http\Request::capture());// 发送响应$response->send();发送响应由 Illuminate\Http\Response的send()方法完成父类其定义在父类Symfony\Component\HttpFoundation\Response中。public function send(){ $this->sendHeaders();// 发送响应头部信息 $this->sendContent();// 发送报文主题 if (function_exists(‘fastcgi_finish_request’)) { fastcgi_finish_request(); } elseif (!\in_array(PHP_SAPI, array(‘cli’, ‘phpdbg’), true)) { static::closeOutputBuffers(0, true); } return $this;}关于Response对象的详细分析可以参看我们之前讲解Laravel Response对象的章节。终止应用程序响应发送后,HTTP内核会调用terminable中间件做一些后续的处理工作。比如,Laravel 内置的「session」中间件会在响应发送到浏览器之后将会话数据写入存储器中。// public/index.php// 终止程序$kernel->terminate($request, $response);//Illuminate\Foundation\Http\Kernelpublic function terminate($request, $response){ $this->terminateMiddleware($request, $response); $this->app->terminate();}// 终止中间件protected function terminateMiddleware($request, $response){ $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge( $this->gatherRouteMiddleware($request), $this->middleware ); foreach ($middlewares as $middleware) { if (! is_string($middleware)) { continue; } list($name, $parameters) = $this->parseMiddleware($middleware); $instance = $this->app->make($name); if (method_exists($instance, ’terminate’)) { $instance->terminate($request, $response); } }}Http内核的terminate方法会调用teminable中间件的terminate方法,调用完成后从HTTP请求进来到返回响应整个应用程序的生命周期就结束了。总结本节介绍的HTTP内核起到的主要是串联作用,其中设计到的初始化应用、引导应用、将HTTP请求抽象成Request对象、传递Request对象通过中间件到达处理程序生成响应以及响应发送给客户端。这些东西在之前的章节里都有讲过,并没有什么新的东西,希望通过这篇文章能让大家把之前文章里讲到的每个点串成一条线,这样对Laravel整体是怎么工作的会有更清晰的概念。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。 ...

November 11, 2018 · 3 min · jiezi