文为图灵社区对刘博文的专访,专访时间为2019年5月。
图片:刘博文友情提供
采访:乐馨,李冰
撰文:李冰
2012年,他17岁,中专毕业。
2015年,他20岁,加入国内知名互联网公司360,成为360最大前端团队奇舞团的一员。现任360导航事业部资深前端工程师。
2019年,他24岁,出版首部技术书籍《深入浅出Vue.js》。
目前负责360导航首页及二级页创新项目等亿级PV站点的设计与优化,推动Vue.js成为部门内广泛使用的核心技术栈,独立研发相关开发工具与技术解决方案并使之成功落地。
从中专毕业的门外汉,到360前端工程师并出版技术书籍,他完成了职业生涯的巨大跨越。他是怎样自主学习,快速成长的?本期图灵访谈对话刘博文,一起来了解他的前端之路。
「虽然也很努力,但我觉得更多的是靠运气。」
17岁那年我中专毕业,是学计算机的。但当时的中专就是告诉我什么是计算机,有个职业可以用计算机去工作。
刚毕业的时候,我到沈阳的一家企业去上班,不会JS,CSS也只是略懂。面试的时候,他们可能不知道我的水平,觉得我还能干活,就让我去了。结果入职后发现,我啥也不会啊,又不好意思直接让我走。就给了我两个选择,一个就是接着干,当学徒,学徒就没有工资;还有一个就是别在这干了。我想了想自己确实挺菜,那就跟着学吧。
同学都说我傻,不给钱还给人干活。现在回头看,是我运气好。如果没有这个当学徒的机会,我就没办法踏入这个行业。
2013年,我18岁,想自己来北京。那时家里人有一点担心。他们是不支持的,但还是给了我5000块钱,觉得等我钱花完了就自己回来了,总不能在那饿死。
但他们肯定想不到,我就用这5000块钱,加上自己攒的,一共8000块钱,一直坚持到现在。
当时我算好了这8000块钱,要在北京租个房子,押一付三,一个月房租按1500算,一共需要6000。我包都拎来北京了,还没找工作呢。就是住一个小旅店,每天还要花200块钱。也就是说我要在10天内找到工作并租个房子。
当时也是命好,真的找到一个特别小的公司,让我去上班。因为我知道自己的水平,能找到工作还挺高兴的,先能养活自己就行。而且公司最好的一点就是供住,员工可以每个月花200块钱住在宿舍里面。这就直接解决了我的生存问题,也是我在北京迈开的第一步。
工作了一段时间后,我还是只会写页面,切图,而且忙到没有时间学习。这样下去肯定不行,我就换了一个比较轻松的公司,为了有时间去学 JS。后来学到一定阶段的时候,我遇到瓶颈了,因为学的东西没办法在工作中用上。
我就又换了一个公司,这家公司只用一种语言就是 JS,服务端是用 Node.js 写的,比较符合我的需要。我可以充分去实践和提升技术,而且工作任务很繁重,那是我成长最快的一个阶段。
再后来,就到了现在的公司360。360给我的感觉像学校,整体工作氛围是比较轻松和自由的,任务不会把人压到一点时间都没有,我们有充分的时间自己去学东西。而且像月影、成银、李松峰老师和屈屈这种大牛会经常在公司内部讲课,有什么不懂的还可以去请教。
初识Vue.js时,它还未被众人认可。想不到5年后,为它写了一本书。
我接触 Vue.js 比较早,大概是2014年。因为上一份工作接手了一个同事的项目,就是用 Vue.js 写的。当时它是零点几的版本,还没有正式的一点零版本。我简单了解了一下,发现它和 Angular 1 很像,挺轻,挺优雅的。需求都能满足,学习成本还不高。
当时用 Vue.js 的人比较少,大家都没怎么听说过。它不火到什么程度呢?我们组新来了一个人,跟我一起写项目,我说项目是用 Vue.js 写的,就给他看了一下代码,讲了一下项目。然后,第二天他就离职了。
有半年的时间吧,我都在想是不是因为我们组的这个项目,用 Vue.js 他觉得太 low 了,所以不想干了?直到后来 Vue.js 被大家广泛认可,我才打消了这个想法。
我刚入职360的时候,我们组的项目都运行了很长时间,很稳定。一次偶然的机会,我们打算新开发一个后台管理系统,大家开会讨论技术选型。就业务来讲,我认为使用框架和对应的组件库会极大降低开发成本,就强烈推荐使用 Vue.js 技术栈,因为考虑到学习成本比较低,而且我对 Vue.js 比较熟。
现在大家已经习惯了使用框架开发,但在当时,我的提议遭到了非常强烈的反对。大家不停地提出各种问题,我也不停地给出解决方案,会议室现场变成了辩论会。
最终,我的 leader 给了我一个机会,如果想使用 Vue.js,就要在短时间解决两个最重要的问题,登录和部署流程。因为公司的统一登陆中心是结合后端来实现的,纯单页静态项目就意味着之前的登陆完全不能用了。部署流程也需要全新的解决方案。
当时我还有其他任务,所以就只能利用下班和周末的时间去做,好在最终问题解决了。这就是我们组正式使用 Vue.js 技术栈的时间点,也算是在后台项目中的一次试水。后来,我们组开发一个新产品,是面向C端的项目。在技术选型时,我又一次强烈推荐 Vue.js。因为上一个项目,有一些同学已经熟悉了 Vue.js 的开发模式,这次我也解决了一些遇到的问题(由于产品是图文内容类,存在 SEO 问题,等等)。就是这个项目真正推动 Vue.js 成为了我们的核心技术栈。
对 Vue.js 越来越熟悉,我在博客上陆续发布了一些梳理它内部原理的文章,作为总结和记录。2018年,王军花老师看到了我的博客,找到了我,问我有没有兴趣写本书。当时我感觉很突然,这东西我写不了啊。心里边是很想写的,又担心写不好,内心很挣扎。一天后,我和军花老师说可以试一下。
可能跟性格有关系,我从来都不是等把一切都准备好了,再去做一件事。一般都是机会来了,先干着再说。中间有问题再去解决问题。
当时我给自己定了的目标是6月份交稿。我列了一个大纲,然后倒推,一个月为一个节点。写作过程中,每个节点的进度可能比预期的快或慢,但总体在可控的范围内。
写作时间就是午休和下了班之后,一天差不多要写两三个小时。其实后期也会觉得枯燥,没有灵感。开始怀疑到底值不值得,这件事真的这么重要吗?我是不是用这些时间做其他事更划算?但没有真正想过放弃,就这样坚持下来了。
在写作过程中,我对一些 API 原理的细节理解得更深入了。
举个例子,我发现 Vue.js 对函数报错这方面做了很严谨的处理。当我们使用 Vue.js 开发项目时,编写的所有代码都是 Vue.js 调用并执行的,所以它在执行用户的代码时,做了错误的捕获处理。
还有就是计算属性。一个函数,可以返回计算后的结果。它要实现一个很重要的功能,就是当计算属性所依赖的某个状态发生变化时,计算属性的返回结果也需要做相应的变化,这个我之前确实没想过 Vue.js 是怎么做到的。
「我自己特别在意的事,多苦多累都要把它干完。」
一直以来,驱动我做事情的都是「我想」,而不是外界的期望。
这种性格有优点,也有缺点。比如说上学的时候,我学习不好,倒不是因为笨,而是因为我当时觉得,学习的结果就是分高分低一点,而这个分数什么用都没有。优点就是我自己特别在意的事,多苦多累都要把它干完。包括来北京弄这个计算机。
我觉得我的职业生涯,更像是一条没有终点的赛道。而且这条赛道是不公平的,大家不是在同一条起跑线上开枪往前跑。当我刚开始跑的时候,可能别人已经跑了五年了。别人跑了好几万米了,我才刚开始第一米。
好多人想问我怎么才能跑得更快,把这场比赛跑赢。其实没有任何方法和经验可以让谁跑得更快。即使在短期内快一些,但在这条没有终点的赛道上,没有任何意义。大部分人跑到中途就主动放弃了,这就是为什么大牛那么少。唯一能决定这场比赛输赢的,只有两个字叫坚持。在这条赛道上跑赢的,不是那些跑得快的人,而是为数不多坚持跑的人。他们能跑赢,只是因为他们还在跑。
读代码其实是一种能力,可以锻炼。你一开始可能读不懂比较复杂的源码,可以读像 Underscore 那种简单一点的工具函数,重要的是训练你的大脑。经常阅读代码的人,理解力会逐渐上升。如果你不经常看代码,一段就研究老半天。尤其是框架,不是一段代码,是一坨,你直接就蒙了。
对于习惯计划与记录的人,时间的脉络变得清晰和可控。
我每年都会给自己定目标,应该在哪个技术方向上深入一些,然后把相关的经典的书买下来,看一看。平时也会读一些所谓没有用的书来调节一下,比如哲学类和心理学的书。
几年前,我发现一个人很难把所有东西都学会。如果漫无目的地去学,很多东西看完之后就忘了。我会挑比较感兴趣的领域去研究,这个领域中的所有问题都看一下,但是对于其他领域的比较深入的知识,可能就先放一放,以后再去研究。
如果自己有目标的话,哪些东西是没看的,哪些东西是应该看的,其实心里都有数。
平时做事我有一个小技巧,是使用番茄工作法。不是用作秒表,到点了就停。对我来说,它是统计的工具。比如说以一周,一个月为周期,记录我每天有效的专注时间是多长。据我统计,我每天专注的时间很短,也就两三个小时,差不多四五个番茄钟。
一旦得到了这个信息,我就可以规划,每天把专注的时间用于哪些重要的事。不重要的,或者一些不太需要脑力的工作,都可以往后放。
除了这种大目标,一年中我还会定几次小目标。比如说一个月或两个月,坚持做一件事。每天我都会为它分配一部分专注的时间,持续下去,直到把这件事做完。
可能是我的性格原因,没有办法同时做很多事。我更适合一次只做一件事。
不管是写本书,还是平时自己学习,我觉得做一件大事要比无数小事要好。前几年,我就是学得很杂,看什么火学什么,没事就看论坛的各种文章。过了一年,都不知道自己看了啥,完全记不住。就像一个漏斗,我细碎的时间全部漏下去,什么都留不住。我应该把我的时块变大,才能卡在这。
未来我想成为一名真正的工程师,而不只是前端工程师,打算涉猎计算机其他领域。现在前端一些颠覆式的工具和创新,比如 Webpack,Babel,都不是一个纯粹的前端工程师能创造的。好多超大型的项目,都需要前端后端综合的解决方案。如果只做前端,只能写个 JS 的工具函数,仅此而已,解决不了真正的复杂场景下的问题。
生活方面的话,我比较喜欢旅游,每年都会计划出去玩两趟,未来还是会出去多玩一玩。我喜欢去自己没去过的地方,看一看,接受一些新鲜的东西。要是有缘分的话,找个女朋友。
世上并没有偶然,如果一个人务必要得到什么,并最终得到了,这就不是偶然,而是他自己的功劳,他的意愿将他领向了那里。——赫尔曼黑塞
推荐阅读:
《深入浅出 Vue.js》 作者:刘博文
《深入浅出 Vue.js》获得360奇舞团团长月影和《JavaScript高级程序设计》译者李松峰作序推荐,从源码层面分析Vue.js。
本书首先简要介绍了Vue.js;然后详细讲解了其内部核心技术“变化侦测”,这里带领大家从0到1实现一个简单的“变化侦测”系统;接着详细介绍了虚拟DOM技术,其中包括虚拟DOM的原理及其patching算法;紧接着详细讨论了模板编译技术,其中包括模板解析器的实现原理、优化器的原理以及代码生成器的原理;最后详细介绍了其整体架构以及提供给我们使用的各种API的内部原理,同时还介绍了生命周期、错误处理、指令系统与模板过滤器等功能的原理。
何选择基准代码?
我开始进行基准测试的原因是,在我们能够并且应该开始优化代码之前,我们应该首先了解我们当前的位置。这对于我们验证我们的变化是否具有我们所希望的影响至关重要,最重要的是,不会使我们的表现更糟。根据我的经验,绩效工作是一个非常迭代的过程或测量,进行小的改变并再次测量以检查变化的影响。
可以说,我可以在本系列中开始其他地方,可能还有分析,跟踪或指标收集。所有这些可能都是必要的,以便针对应该优化的服务以及代码级别,应该是您的目标的类和方法。我现在决定跳过这些更高级别的技术,部分原因是因为它们是我不完全自信的领域,当然是我能够为他们提供良好指导的水平。此外,它们是他们自己的大量主题,我觉得这会分散我对语言和框架功能的关注。
对于真实场景,您可能需要使用此类技术来首先缩小您应该花时间优化的地方。有时可以做出好的猜测,但只要有可能,最好是在你的努力中保持科学,并用实际数据支持理论。我有一天可能会回到这些更广泛的领域,但就目前而言,我将假设您对要改进的代码路径有所了解。如果您想了解有关分析代码的更多信息,我从Konrad Kokosa 阅读“ Pro .NET内存管理:更好的代码,性能和可伸缩性 ” 中学到了很多东西。
基线是在代码的典型条件下建立当前性能的过程。在.NET中,在代码级别,有许多技术可以使用。有时,使用简单的秒表将是收集一般时间数据的起点。请注意,许多条件可能会影响您的测量及其准确性。一个好处是秒表使用简单,可以提供快速的结果。只要能够理解准确性的妥协,我认为以这种方式收集一些基本数据并没有错。
一旦您将注意力集中到代码的特定区域,您就开始深入到方法级别。此时,开始为现有方法和代码记录更准确和特定的基准测试非常有用。这是基准测试应该成为您的首选工具。在C#中,我们以Benchmark.NET的形式提供了一个很棒的选择。该库提供了大量基准测试工具,可用于测量和测试.NET代码。Microsoft的团队现在经常使用Benchmark .NET来测量他们的代码。
什么是基准?
基准测试只是与某些代码的执行相关的一组测量或一组测量。基准测试允许您在开始努力提高性能时比较代码的相对性能。基准测试的范围可能非常广泛,或者通常情况下您可能会发现自己测试微基准测试中的微小变化。主要的是确保您有一种机制来将建议的更改与原始代码进行比较,然后指导您的优化工作。在优化代码时使用数据非常重要,而不是假设。
如何基准C#代码
希望到现在为止你已经按照基准测试的概念出售了,所以让我们从一个简单的例子开始。如果您想跟进,可以在此示例存储库的“基准”分支中找到此帖子的完整代码。
让我们假设我们已经将以下NameParser识别为我们在重负载和潜在性能瓶颈下的应用领域。
public class NameParser { public string GetLastName(string fullName) { var names = fullName.Split(" "); var lastName = names.LastOrDefault(); return lastName ?? string.Empty; } }
此代码是一个简单的实现,用于从输入字符串返回姓氏,该输入字符串被假定为人的全名。出于本演示的目的,它假设最后一个单词,在任何空格表示姓氏之后。现在这是一个非常简单的示例,您可能想要进行基准测试的方法可能会做更复杂的工作!有时,您将能够直接引用现有代码库中的代码并对其进行基准测试,这些代码库的方法足够小且公开。在其他时候,我发现自己通过将相关的代码部分复制到我的基准项目中来创建基准,以便将焦点缩小到特定的代码行。这是我需要花费更多时间的一个领域,以确定围绕构建基准的良好实践。
第一步是安装Benchmark.NET库。通常,您可能已经在进行单元测试,您将创建一个单独的项目来保存您的基准测试。在此基准测试项目中,您将引用包含要进行基准测试的代码的项目。为了使我的样本非常简单,我现在已将所有内容都保留在一个项目中。
对于一般基准测试,您只需要NuGet的主要BenchmarkDotNet软件包。我通过从命令行使用“dotnet add package BenchmarkDotNet -version 0.11.3”将它添加到我的示例项目中来安装我的。
下一步是通过创建一个包含它们的新类来创建基准。基准类将由Benchmark.NET运行,任何基准方法的结果都将包含在输出中。这是我的NameParserBenchmarks类。
[MemoryDiagnoser] public class NameParserBenchmarks { private const string FullName = "Steve J Gordon"; private static readonly NameParser Parser = new NameParser(); [Benchmark(Baseline = true)] public void GetLastName() { Parser.GetLastName(FullName); } }
该类本身标有BenchmarkDotNet.Attributes命名空间中的属性。Benchmark.NET具有诊断器的概念,用于控制测量和包含在结果中的事物。如果没有附加任何额外的诊断程序,它将为正在进行基准测试的代码提供恰好的时序数据。内存诊断程序支持额外的分配和GC集合测量,这在优化代码时非常有用。
我在叫前面的代码的单一方法GetLastName它通过调用它在基准测试我NameParser类现有GetLastName方法。我已使用Benchmark属性标记此方法,以便它由Benchmark.NET执行并包含在结果中。我可以提供基线属性的值,因为我在这里将此特定方法标记为我的基线。这是我们正在测量的现有代码,这将在以后有用,因为所有其他基准测试将与此初始代码进行比较。
为了支持基准测试,我在基准测试中包含了要解析的名称的静态字符串值。我还包含一个静态字段,其中包含对新NameParser实例的引用。我不想在Benchmark方法本身中包含这些内容,因为我想单独测量GetLastName方法的性能和分配。
最后一步是设置并触发Benchmark.NET的运行器。在这个示例中,我正在运行单个项目中的所有内容,因此我将更新Program类的Main方法。
public class Program { public static void Main(string[] args) { var summary = BenchmarkRunner.Run<NameParserBenchmarks>(); } }
对通用BenchmarkRunner.Run方法的调用接受应运行任何基准测试的类。默认情况下,基准测试的结果将记录到控制台。
执行基准
在这个阶段,我们已准备好运行基准测试。为了获得最佳效果,建议您在设备上执行此操作,尽可能少运行。关闭所有其他应用程序并杀死不必要的进程将产生最稳定的结果。在我的开发机器上,一旦关闭所有内容,我将触发从命令行运行基准测试。
应根据发布代码运行基准测试,以确保包含所有优化。从我的项目目录,我将运行“dotnet build -c Release”来创建发布版本。
构建完成后,我可以导航到包含构建代码的文件夹:“cd bin / Release / netcoreapp2.2”
最后,我可以通过使用“dotnet BenchmarkAndSpanExample.dll”为我的示例应用程序运行构建的程序集来运行基准测试。
运行基准测试所需的时间长短取决于您的机器和测试代码。Benchmark.NET执行许多阶段来预热代码并确保运行多次迭代以提供一致的统计数据。它使用一个试验阶段计算出要运行的最佳迭代次数,尽管您可以根据需要进行配置。
解释结果
完成后,您应该将摘要结果写入控制台窗口。如果您愿意,可以在运行应用程序的位置下的BenchmarkDotNet.Artifacts文件夹中生成各种输出。这包括摘要的HTML版本,可以更容易地共享。
我的机器的摘要如下所示:
// * Summary * BenchmarkDotNet=v0.11.3, OS=Windows 10.0.16299.904 (1709/FallCreatorsUpdate/Redstone3) Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores Frequency=3117195 Hz, Resolution=320.8012 ns, Timer=TSC .NET Core SDK=3.0.100-preview-010184 [Host] : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT DefaultJob : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | ------------ |---------:|---------:|---------:|------:|------------:|------------:|------------:|--------------------:| GetLastName | 125.8 ns | 2.306 ns | 2.157 ns | 1.00 | 0.0253 | - |
对于每个基准测试方法,您将获得一行结果数据。在这里,我有一行用于GetLastName方法的基准测试。它的平均执行时间是125.8纳秒; 不是太寒酸!其他统计数据可用于迭代中的定时数据的误差和标准偏差。
因为我包含了memory diagnoser属性,所以我有一些包含内存相关统计信息的额外列。前三列与GC集合相关。它们被缩放以显示每1,000次操作的数量。在这种情况下,我必须经常调用我的方法来触发Gen 0集合,并且不太可能导致Gen 1或Gen 2集合。最后一列非常有用,它显示了每个操作分配的内存。我的名称解析器代码每次调用时都会分配160字节。在宏伟的计划中,这根本不多,但我们将在未来的帖子中看到我们如何减少这一点。请记住,尽管.NET中的分配很便宜,但GC工作可能会对收集和清理这些对象造成更大的影响。在热门路径(高度称为方法)中,这很快就会增加。
我们已经知道如何用javascript代码编写html dom元素,和html一样的网页。这不足为奇,网页的神奇之处,在于它是动的,可以交互。要网页变化,前面我们在textNode一章中,已经演示了秒表页面,通过文字变化改变网页。另一个改变网页的手段是改变元素节点的属性。我们通过Observable来封装网页的变化。
本文及示例代码都在github上,见xp44mm/hyperscript-rxjs-test仓库。
运行示例代码的方法,见第0章,框架的创建。
当属性值是observable时,此元素的该属性就订阅了observable发出的数据值,作为属性值。Observable一切输出的变化都会反映在元素的属性上。
我们实现一个网页,这个网页的用背景颜色的变化,提示给用户成功或失败。css
.success {
background: #A5D6A7
}
.failed {
background: #EF9A9A
}
import { p, textNode } from 'hyperscript-rxjs';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
import '../css/classprop.css';
export function observableprop1() {
const numbers = interval(1000) //1
return p({
className: numbers //2
|> map(n => n % 2 === 1 ? 'success' : 'failed')
}, textNode('observable prop'))
}
本程序要注意的是className直接使用可观察流管道来配置。这个程序有个小遗憾,程序启动瞬间背景是非红非绿的白色,如果比较完美主义,我们可以调整一下。
export function observableprop1() {
const numbers = interval(1000)
let elem = p({
className: numbers |> map(i => i % 2 === 1 ? 'success' : 'failed')
}, textNode('observable prop'))
elem.className = 'success' //*
return elem
}
这段代码说明,不是元素属性等于observable本身,而是元素的属性值被observable流中的数据来修改。
当元素绿色时<p class="successs" ...
当元素红色时<p class="faild" ...
利用类选择器来实现同一功能:
export function observableprop1() {
const numbers = interval(1000)
let elem = p({
className: 'success',
'.failed': numbers |> map(n => n % 2 === 1)
}, textNode('observable prop'))
return elem
}
虽然页面效果一样,但是背后的html输出却有差别。成功类一直在,失败类可能添加到类末尾,也可能从类中移除。
当元素绿色时<p class="successs" ...
当元素红色时<p class="successs faild" ...
这里的原理是,同级样式表冲突,后来者赢。
利用样式属性来实现同一功能:
export function observableprop1() {
const numbers = interval(1000)
let elem = p({
className: 'success',
'style.backgroundColor': numbers
|> map(n => n % 2 === 1 ? '#A5D6A7' : '#EF9A9A')
}, textNode('observable prop'))
return elem
}
直接用样式属性设置背景色,亦可以实现同样的功能。相比较而言,语义性弱一些,颜色值在js中是一串文本,也无法像在css中一样可以直观的预览。
至此,我们设置了textNode文本内容nodeValue的变化,也设置了元素属性的变化,网页文档另一半重要的变化主角就是事件,hyperscript-rxjs仍然可以无缝的封装变化。详见下一章。
*请认真填写需求信息,我们会在24小时内与您取得联系。