文為圖靈社區對劉博文的專訪,專訪時間為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小時內與您取得聯系。