三年面試經驗分享:前端面試的四個階段和三個決定因素

2022-12-16 18:00:47
今年的就業形式簡直一片黑暗,本著明年會比今年還差的「僥倖心理」,我還是毫不猶豫地選擇裸辭了,歷經一個半月的努力,收到了還算不錯的 offer,薪資和平臺都有比較大的提升,但還是和自己的心理預期有著很大的差距。所以得出最大的結論就是:不要裸辭!不要裸辭!不要裸辭!因為面試期間帶給人的壓力,和現實與理想的落差對心理的摧殘是不可估量的,在這樣一個環境苟著是不錯的選擇。

相關推薦:

接下來總結一般情況下前端面試中會經歷的以下四個階段和三個決定因素:

image-20221204185636679.png作為前端人員,技術的深度廣度是排在第一位的,三年是一個分割線,一定要在這個時候找準自己的定位和方向。

php入門到就業線上直播課:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

其次良好的溝通表達能力、著裝和表現等場外因素能提高面試官對你的認可度。

有的人技術很牛逼,但是面試時讓面試官覺得不爽,覺得你盛氣逼人/形象邋遢/自以為是/表述不清,就直接不要你,那是最得不償失的。

以下是我整個面試準備以及被問到過的問題的一個凝練,因為在面試過程中,和麵試官的交流很大程度並不是簡單的背書,最好是要將知識通過自己的總結和凝練表達出來,再根據面試官的提問臨場發揮將之補足完善。【推薦學習:、】

一、自我介紹

面試官讓你自我介紹,而且不限定自我介紹的範圍,肯定是面試官想從你的自我介紹中瞭解到你,所以介紹一定要保證簡短和流暢,面對不同的面試官,自我介紹的內容可以是完全一樣的,所以提前準備好說辭很重要,並且一定要注意:不要磕磕巴巴,要自信!流暢的表達和溝通能力,同樣是面試官會對候選人考核點之一。我也曾當過面試官,自信大方的候選人,往往更容易受到青睞。

1、個人介紹(基本情況),主要的簡歷都有了,這方面一定要短

2、個人擅長什麼,包括技術上的和非技術上的。技術上可以瞭解你的轉場,非技術可以瞭解你這個人

3、做過的專案,撿最核心的專案說,不要把所有專案像背書一樣介紹

4、自己的一些想法、興趣或者是觀點,甚至包括自己的職業規劃。這要是給面試官一個感覺:熱衷於"折騰"或者"思考"

範例:

面試官您好,我叫xxx,xx年畢業於xx大學,自畢業以來一直從事著前端開發的相關工作。

我擅長的技術棧是 vue 全家桶,對 vue2 和 vue3 在使用上和原始碼都有一定程度的鑽研;打包工具對 webpack 和 vite 都比較熟悉;有從零到一主導中大型專案落地的經驗和能力。

在上家公司主要是xx產品線負責人的角色,主要職責是。。。。。。

除了開發相關工作,還有一定的技術管理經驗:比如擔任需求評審、UI/UE互動評審評委,負責開發排期、成員共同作業、對成員程式碼進行review、組織例會等等

平常會在自己搭建的部落格上記錄一些學習文章或者學習筆記,也會寫一些原創的技術文章發表到掘金上,獲得過xx獎。

總的來說自我介紹儘量控制在3 - 5分鐘之間,簡明扼要為第一要義,其次是突出自己的能力和長處。

對於普通的技術面試官來說,自我介紹只是習慣性的面試前的開場白,一般簡歷上列舉的基本資訊已經滿足他們對你的基本瞭解了。但是對於主管級別的面試官或者Hr,會看重你的性格、行為習慣、抗壓能力等等綜合能力。所以要讓自己在面試過程儘可能表現的積極向上,愛好廣泛、喜歡持續學習,喜歡團隊合作,可以無條件加班等等。當然也不是說讓你去欺騙,只是在現在這種環境中,這些「側面能力」也是能在一定程度提升自己競爭力的法寶。

二、專案挖掘

在目前這個行情,當你收到面試通知時,有很大概率是因為你的專案經驗和所招聘的崗位比較符合。 所以在專案的準備上要額外上心,比如:

  • 對專案中使用到的技術的深挖

  • 對專案整體設計思路的把控

  • 對專案運作流程的管理

  • 團隊共同作業的能力。

  • 專案的優化點有哪些

這些因人而異就不做贅述了,根據自己的情況好好挖掘即可。

三、個人

先說個人,當你通過了技術面試,到了主管和hr這一步,不管你當前的技術多牛逼,他們會額外考察你個人的潛力、學習能力、性格與團隊的磨合等軟實力,這裡列出一些很容易被問到的:

為什麼跳槽?

直接從個人發展入手錶現出自己的上進心:

  • 一直想去更大的平臺,不但有更好的技術氛圍,而且學到的東西也更多

  • 想擴充套件一下自己的知識面,之前一致是做x端的 xx 產品,技術棧比較單一一點,相對xx進行學習。
  • 之前的工作陷入了舒適圈,做來做去也就那些東西,想要換個平臺擴寬自己的技術廣度,接觸和學習一些新的技術體系,為後續的個人發展更有利

講講你和普通前端,你的亮點有哪些?

1、善於規劃和總結,我會對自己經手的專案進行一個全面的分析,一個是業務拆解,對個各模組的業務通過腦圖進行拆解;另一個就是對程式碼模組的拆解,按功能去區分各個程式碼模組。再去進行開發。我覺得這是很多隻會進行盲目業務開發的前端做不到的

2、喜歡專研技術,平常對 vue 的原始碼一直在學習,也有輸出自己的技術文章,像之前寫過一篇逐行精讀 ,花了大約有三十個小時才寫出來的,對每一行原始碼的功能和作用進行了解讀(但是為啥閱讀和點贊這麼低)。

你有什麼缺點?

性子比較沉,更偏內向一點,所以我也會嘗試讓自己變得外向一點。

一個是要開各種評審會,作為前端代表需要我去準備各種材料和進行發言。

所以在團隊內做比較多的技術分享,每週主持例會,也讓我敢於去表達和探討。

最近有關注什麼新技術嗎?

  • 包依賴管理工具 pnpm(不會重複安裝依賴,非扁平的node_modules結構,符號連結方式新增依賴包)

  • 打包工具 vite (極速的開發環境)

  • flutter (Google推出並開源的移動應用程式(App)開發框架,主打跨平臺、高保真、高效能)

  • rust(聽說是js未來的基座)

  • turbopack,webpack的繼任者,說是比 vite快10倍,webpack快700倍,然後尤雨溪親自驗證其實並沒有比 vite 快10倍

  • webcomponents

你是偏向於走各個方向探索還是一直向某個方向研究下去?

我對個人的規劃是這樣的:

3 - 5 年在提高自己的技術深度的同時,擴寬自己的知識面,就是深度和廣度上都要有提升,主要是在廣度上,充分對大前端有了認知才能更好的做出選擇

5 - 7 年就是當有足夠的知識積累之後再選擇某一個感興趣方向深研下去,爭取成為那個領域的專家

團隊規模,團隊規範和開發流程

這個因人而異,如實準備即可,因為不同規模團隊的研發模式差別是很大的。

程式碼 review 的目標

1、最注重的是程式碼的可維護性(變數命名、註釋、函數單一性原則等)

2、擴充套件性:封裝能力(元件、程式碼邏輯是否可複用、可延伸性)

3、ES 新特性(es6+ 、ES2020, ES2021 可選鏈、at)

4、函數使用規範(比如遇到用 map 拿來當 forEach 用的)

5、效能提升,怎樣運用演演算法,寫出更加優雅,效能更好的程式碼

如何帶領團隊的

我在上家公司是一個技術管理的角色。

0、落實開發規範,我在公司內部 wiki 上有發過,從命名、最佳實踐到各種工具庫的使用。新人進來前期我會優先跟進他們的程式碼品質

1、團隊分工:每個人單獨負責一個產品的開發,然後公共模組一般我會指定某幾個人開發

2、程式碼品質保證:每週會review他們的程式碼,也會組織交叉 review 程式碼,將修改結果輸出文章放到 wiki中

3、組織例會:每週組織例會同步各自進度和風險,根據各自的進度調配工作任務

4、技術分享:還會組織不定時的技術分享。一開始就是單純的我做分享,比如微前端的體系,ice stark 的原始碼

5、公共需求池:比如webpack5/vite的升級;vue2.7的升級引入setup語法糖;pnpm的使用;拓撲圖效能優化

6、優化專項:在第一版產品出來之後,我還發起過效能優化專項,首屏載入效能,打包體積優化;讓每個人去負責對應的優化項

對加班怎麼看?

我覺得加班一般會有兩種情況:

一是專案進度比較緊,那當然以專案進度為先,畢竟大家都靠這個吃飯

二是自身能力問題,對業務不熟啊或者引入一個全新的技術棧,那麼我覺得不僅要加班跟上,還要去利用空閒時間抓緊學習,彌補自己的不足

有什麼興趣愛好?

我平常喜歡閱讀,就是在微信閱讀裡讀一些心理學、時間管理、還有一些演講技巧之類的書

然後是寫文章,因為我發現單純的記筆記很容易就忘了,因為只是記載別人的內容,而寫自己的原創文章,在這個過程中能將知識非常高的比例轉換成自身的東西,所以除了自個發掘金的文章,我也經常會對專案的產出有文章輸出到 wiki 上

其他愛好就是和朋友約著打籃球、唱歌

四、技術

技術面試一定要注意:簡明扼要,詳略得當,不懂的就說不懂。因為在面試過程中是一個和麵試官面對面交流的過程,沒有面試官會喜歡一個絮絮叨叨半天說不到重點候選人,同時在說話過程中,聽者會被動的忽略自己不感興趣的部分,所以要著重突出某個技術的核心特點,並圍繞著核心適當展開。

大廠基本都會通過演演算法題來篩選候選人,演演算法沒有捷徑,只能一步一步地刷題再刷題,這方面薄弱的要提前規劃進行個學習了。

技術面過程主要會對前端領域相關的技術進行提問,一般面試官會基於你的建立,而更多的是,面試官基於他之前準備好的面試題,或者所在專案組比較熟悉的技術點進行提問,因為都是未知數,所以方方面面都還是要求比較足的。

如果想進入一箇中大型且發展前景不錯的公司,並不是照著別人的面經背一背就能糊弄過去的,這裡作出的總結雖然每一條都很簡短,但都是我對每一個知識點進行全面學習後才提煉出來的部分核心知識點,所以不懼怕面試官的「發散一下思維」。

面試過程一般會涉及到以下八大知識型別的考量:

JS/CSS/TypeScript/框架(Vue、React)/瀏覽器與網路/效能優化/前端工程化/架構/其他

image.png

所以面試前的技術準備絕不是一蹴而就,還需要平時的積累,比如可以利用每天十到二十分鐘對其中一個小知識點進行全面的學習,長此以往,無論是幾年的面試,都足夠侃侃而談。

JS篇

JS的學習梭哈紅包書和冴羽老師的就基本ok了

常見的JS面試題一般會有這些

image-20221206221056905.png

什麼是原型/原型鏈?

原型的本質就是一個物件

當我們在建立一個建構函式之後,這個函數會預設帶上一個prototype屬性,而這個屬性的值就指向這個函數的原型物件。

這個原型物件是用來為通過該建構函式建立的範例物件提供共用屬性,即用來實現基於原型的繼承和屬性的共用

所以我們通過建構函式建立的範例物件都會從這個函數的原型物件上繼承上面具有的屬性

當讀取範例的屬性時,如果找不到,就會查詢與物件關聯的原型中的屬性,如果還查不到,就去找原型的原型,一直找到最頂層為止(最頂層就是Object.prototype的原型,值為null)。

所以通過原型一層層相互關聯的鏈狀結構就稱為原型鏈

什麼是閉包?

定義:閉包是指參照了其他函數作用域中變數的函數,通常是在巢狀函數中實現的。

從技術角度上所有 js 函數都是閉包。

從實踐角度來看,滿足以下倆個條件的函數算閉包

  • 即使建立它的上下文被銷燬了,它依然存在。(比如從父函數中返回)

  • 在程式碼中參照了自由變數(在函數中使用的既不是函數引數也不是函數區域性變數的變數稱作自由變數)

使用場景:

  • 建立私有變數

    vue 中的data,需要是一個閉包,保證每個data中資料唯一,避免多次參照該元件造成的data共用

  • 延長變數的生命週期

    一般函數的詞法環境在函數返回後就被銷燬,但是閉包會儲存對建立時所在詞法環境的參照,即便建立時所在的執行上下文被銷燬,但建立時所在詞法環境依然存在,以達到延長變數的生命週期的目的

應用

  • 柯里化函數
  • 例如計數器、延遲呼叫、回撥函數等

this 的指向

在絕大多數情況下,函數的呼叫方式決定了 this 的值(執行時繫結)

1、全域性的this非嚴格模式指向window物件,嚴格模式指向 undefined

2、物件的屬性方法中的this 指向物件本身

3、apply、call、bind 可以變更 this 指向為第一個傳參

4、箭頭函數中的this指向它的父級作用域,它自身不存在 this

瀏覽器的事件迴圈?

js 程式碼執行過程中,會建立對應的執行上下文並壓入執行上下文棧中。

如果遇到非同步任務就會將任務掛起,交給其他執行緒去處理非同步任務,當非同步任務處理完後,會將回撥結果加入事件佇列中。

當執行棧中所有任務執行完畢後,就是主執行緒處於閒置狀態時,才會從事件佇列中取出排在首位的事件回撥結果,並把這個回撥加入執行棧中然後執行其中的程式碼,如此反覆,這個過程就被稱為事件迴圈。

事件佇列分為了宏任務佇列和微任務佇列,在當前執行棧為空時,主執行緒回先檢視微任務佇列是否有事件存在,存在則依次執行微任務佇列中的事件回撥,直至微任務佇列為空;不存在再去宏任務佇列中處理。

常見的宏任務有setTimeout()setInterval()setImmediate()、I/O、使用者互動操作,UI渲染

常見的微任務有promise.then()promise.catch()new MutationObserverprocess.nextTick()

宏任務和微任務的本質區別

  • 宏任務有明確的非同步任務需要執行和回撥,需要其他非同步執行緒支援

  • 微任務沒有明確的非同步任務需要執行,只有回撥,不需要其他非同步執行緒支援。

javascript中資料在棧和堆中的儲存方式

1、基本資料型別大小固定且操作簡單,所以放入棧中儲存

2、參照資料型別大小不確定,所以將它們放入堆記憶體中,讓它們在申請記憶體的時候自己確定大小

3、這樣分開儲存可以使記憶體佔用最小。棧的效率高於堆

4、棧記憶體中變數在執行環境結束後會立即進行垃圾回收,而堆記憶體中需要變數的所有參照都結束才會被回收

講講v8垃圾回收

1、根據物件的存活時間將記憶體的垃圾回收進行不同的分代,然後對不同分代採用不同的回收演演算法

2、新生代採用空間換時間的 scavenge 演演算法:整個空間分為兩塊,變數僅存在其中一塊,回收的時候將存活變數複製到另一塊空間,不存活的回收掉,周而復始輪流操作

3、老生代使用標記清除和標記整理,標記清除:遍歷所有物件標記標記可以存取到的物件(活著的),然後將不活的當做垃圾進行回收。回收完後避免記憶體的斷層不連續,需要通過標記整理將活著的物件往記憶體一端進行移動,移動完成後再清理邊界記憶體

函數呼叫的方法

1、普通function直接使用()呼叫並傳參,如:function test(x, y) { return x + y}test(3, 4)

2、作為物件的一個屬性方法呼叫,如:const obj = { test: function (val) { return val } }, obj.test(2)

3、使用callapply呼叫,更改函數 this 指向,也就是更改函數的執行上下文

4、new可以間接呼叫建構函式生成物件範例

defer和async的區別

一般情況下,當執行到 script 標籤時會進行下載 + 執行兩步操作,這兩步會阻塞 HTML 的解析;

async 和 defer 能將script的下載階段變成非同步執行(和 html解析同步進行);

async下載完成後會立即執行js,此時會阻塞HTML解析;

defer會等全部HTML解析完成且在DOMContentLoaded 事件之前執行。

瀏覽器事件機制

DOM 事件流三階段:

  • 捕獲階段:事件最開始由不太具體的節點最早接受事件, 而最具體的節點(觸發節點)最後接受事件。為了讓事件到達最終目標之前攔截事件。

    比如點選一個div,則 click 事件會按這種順序觸發: document => <html> => <body> => <div>,即由 document 捕獲後沿著 DOM 樹依次向下傳播,並在各節點上觸發捕獲事件,直到到達實際目標元素。

  • 目標階段

    當事件到達目標節點的,事件就進入了目標階段。事件在目標節點上被觸發(執行事件對應的函數),然後會逆向迴流,直到傳播至最外層的檔案節點。

  • 冒泡階段

    事件在目標元素上觸發後,會繼續隨著 DOM 樹一層層往上冒泡,直到到達最外層的根節點。

所有事件都要經歷捕獲階段和目標階段,但有些事件會跳過冒泡階段,比如元素獲得焦點 focus 和失去焦點 blur 不會冒泡

擴充套件一

e.target 和 e.currentTarget 區別?

  • e.target 指向觸發事件監聽的物件。
  • e.currentTarget 指向新增監聽事件的物件。

例如:

<ul>
  <li><span>hello 1</span></li>
</ul>

let ul = document.querySelectorAll('ul')[0]
let aLi = document.querySelectorAll('li')
ul.addEventListener('click',function(e){
  let oLi1 = e.target  
  let oLi2 = e.currentTarget
  console.log(oLi1)   //  被點選的li
  console.log(oLi2)   // ul
  console.og(oLi1===oLi2)  // false
})
登入後複製

給 ul 繫結了事件,點選其中 li 的時候,target 就是被點選的 li, currentTarget 就是被繫結事件的 ul

事件冒泡階段(上述例子),e.currenttargete.target是不相等的,但是在事件的目標階段,e.currenttargete.target是相等的

作用:

e.target可以用來實現,該原理是通過事件冒泡(或者事件捕獲)給父元素新增事件監聽,e.target指向引發觸發事件的元素

擴充套件二

addEventListener 引數

語法:

addEventListener(type, listener);
addEventListener(type, listener, options || useCapture);
登入後複製
  • type: 監聽事件的型別,如:'click'/'scroll'/'focus'

  • listener: 必須是一個實現了 介面的物件,或者是一個。當監聽的事件型別被觸發時,會執行

  • options:指定 listerner 有關的可選引數物件

    • capture: 布林值,表示 listener 是否在事件捕獲階段傳播到 EventTarget 時觸發
    • once:布林值,表示 listener 新增之後最多呼叫一次,為 true 則 listener 在執行一次後會移除
    • passive: 布林值,表示 listener 永遠不會呼叫 preventDefault()
    • signal:可選,AbortSignal,當它的abort()方法被呼叫時,監聽器會被移除
  • useCapture:布林值,預設為 false,listener 在事件冒泡階段結束時執行,true 則表示在捕獲階段開始時執行。作用就是更改事件作用的時機,方便攔截/不被攔截。

Vue篇

筆主是主要從事 Vue相關開發的,也做過 react 相關的專案,當然 react 也只是能做專案的水平,所以在簡歷中屬於一筆帶過的那種,框架貴不在多而在精,對Vue原始碼系列的學習讓我對Vue還是十分自信的。學習過程也是如此,如果你能夠對一門框架達到精通原理的掌握程度,學習其他框架不過只是花時間的事情。

image.png

vue和react的區別

1、資料可變性

  • React 推崇函數語言程式設計,資料不可變以及單向資料流,只能通過setState或者onchange來實現檢視更新
  • Vue 基於資料可變,設計了響應式資料,通過監聽資料的變化自動更新檢視

2、寫法

  • React 推薦使用 jsx + inline style的形式,就是 all in js
  • Vue 是單檔案元件(SFC)形式,在一個元件內分模組(tmplate/script/style),當然vue也支援jsx形式,可以在開發vue的ui元件庫時使用

3、diff 演演算法

  • Vue2採用雙端比較,Vue3採用快速比較
  • react主要使用diff佇列儲存需要更新哪些DOM,得到patch樹,再統一操作批次更新DOM。,需要使用shouldComponentUpdate()來手動優化react的渲染。

擴充套件:瞭解 react hooks嗎

元件類的寫法很重,層級一多很難維護。

函陣列件是純函數,不能包含狀態,也不支援生命週期方法,因此無法取代類。

React Hooks 的設計目的,就是加強版函陣列件,完全不使用"類",就能寫出一個全功能的元件

React Hooks 的意思是,元件儘量寫成純函數,如果需要外部功能和副作用,就用勾點把外部程式碼"鉤"進來。

vue元件通訊方式

  • props / $emit
  • ref / $refs
  • parent/parent /root
  • attrs / listeners
  • eventBus / vuex / pinia / localStorage / sessionStorage / Cookie / window
  • provide / inject

vue 渲染列表為什麼要加key?

Vue 在處理更新同型別 vnode 的一組子節點(比如v-for渲染的列表節點)的過程中,為了減少 DOM 頻繁建立和銷燬的效能開銷:

對沒有 key 的子節點陣列更新是通過就地更新的策略。它會通過對比新舊子節點陣列的長度,先以比較短的那部分長度為基準,將新子節點的那一部分直接 patch 上去。然後再判斷,如果是新子節點陣列的長度更長,就直接將新子節點陣列剩餘部分掛載;如果是新子節點陣列更短,就把舊子節點多出來的那部分給解除安裝掉)。所以如果子節點是元件或者有狀態的 DOM 元素,原有的狀態會保留,就會出現渲染不正確的問題

有 key 的子節點更新是呼叫的patchKeyedChildren,這個函數就是大家熟悉的實現核心 diff 演演算法的地方,大概流程就是同步頭部節點、同步尾部節點、處理新增和刪除的節點,最後用求解最長遞增子序列的方法區處理未知子序列。是為了最大程度實現對已有節點的複用,減少 DOM 操作的效能開銷,同時避免了就地更新帶來的子節點狀態錯誤的問題。

綜上,如果是用 v-for 去遍歷常數或者子節點是諸如純文字這類沒有」狀態「的節點,是可以使用不加 key 的寫法的。但是實際開發過程中更推薦統一加上 key,能夠實現更廣泛場景的同時,避免了可能發生的狀態更新錯誤,我們一般可以使用 ESlint 設定 key 為 v-for 的必需元素。

想詳細瞭解這個知識點的可以去看看我之前寫的文章:

vue3 相對 vue2的響應式優化

vue2使用的是Object.defineProperty去監聽物件屬性值的變化,但是它不能監聽物件屬性的新增和刪除,所以需要使用$set$delete這種語法糖去實現,這其實是一種設計上的不足。

所以 vue3 採用了proxy去實現響應式監聽物件屬性的增刪查改。

其實從api的原生效能上proxy是比Object.defineProperty要差的。

而 vue 做的響應式效能優化主要是在將巢狀層級比較深的物件變成響應式的這一過程。

vue2的做法是在元件初始化的時候就遞迴執行Object.defineProperty把子物件變成響應式的;

而vue3是在存取到子物件屬性的時候,才會去將它轉換為響應式。這種延時定義子物件響應式會對效能有一定的提升

Vue 核心diff流程

前提:當同型別的 vnode 的子節點都是一組節點(陣列型別)的時候,

步驟:會走核心 diff 流程

Vue3是快速選擇演演算法

  • 同步頭部節點
  • 同步尾部節點
  • 新增新的節點
  • 刪除多餘節點
  • 處理未知子序列(貪心 + 二分處理最長遞增子序列)

Vue2是雙端比較演演算法

在新舊位元組點的頭尾節點,也就是四個節點之間進行對比,找到可複用的節點,不斷向中間靠攏的過程

diff目的:diff 演演算法的目的就是為了儘可能地複用節點,減少 DOM 頻繁建立和刪除帶來的效能開銷

vue雙向繫結原理

基於 MVVM 模型,viewModel(業務邏輯層)提供了資料變化後更新檢視檢視變化後更新資料這樣一個功能,就是傳統意義上的雙向繫結。

Vue2.x 實現雙向繫結核心是通過三個模組:Observer監聽器、Watcher訂閱者和Compile編譯器。

首先監聽器會監聽所有的響應式物件屬性,編譯器會將模板進行編譯,找到裡面動態繫結的響應式資料並初始化檢視;watchr 會去收集這些依賴;當響應式資料發生變更時Observer就會通知 Watcher;watcher接收到監聽器的訊號就會執行更新函數去更新檢視;

vue3的變更是資料劫持部分使用了porxy 替代 Object.defineProperty,收集的依賴使用元件的副作用渲染函數替代watcher

v-model 原理

vue2 v-model 原理剖析

V-model 是用來監聽使用者事件然後更新資料的語法糖。

其本質還是單向資料流,內部是通過繫結元素的 value 值向下傳遞資料,然後通過繫結 input 事件,向上接收並處理更新資料。

單向資料流:父元件傳遞給子元件的值子元件不能修改,只能通過emit事件讓父元件自個改。

// 比如
<input v-model="sth" />
// 等價於
<input :value="sth" @input="sth = $event.target.value" />
登入後複製

給元件新增 v-model 屬性時,預設會把value 作為元件的屬性,把 input作為給元件繫結事件時的事件名:

// 父元件
<my-button v-model="number"></my-button>

// 子元件
<script>
export default {
  props: {
    value: Number, //  屬性名必須是 value
  },

  methods: {
    add() {
      this.$emit('input', this.value + 1) // 事件名必須是 input
    },
  }
}
</script>
登入後複製

如果想給繫結的 value 屬性和 input 事件換個名稱呢?可以這樣:

在 Vue 2.2 及以上版本,你可以在定義元件時通過 model 選項的方式來客製化 prop/event:

<script>
export default {
  model: {
    prop: 'num', // 自定義屬性名
    event: 'addNum' // 自定義事件名
  }
}
登入後複製

vue3 v-model 原理

實現和 vue2 基本一致

<Son v-model="modalValue"/>
登入後複製

等同於

<Son v-model="modalValue"/> <Son :modalValue="modalValue" @update:modalValue="modalUpdate=$event.target.value"/>
登入後複製

自定義 model 引數

<Son v-model:visible="visible"/>
setup(props, ctx){
    ctx.emit("update:visible", false)
}
登入後複製

vue 響應式原理

不管vue2 還是 vue3,響應式的核心就是觀察者模式 + 劫持資料的變化,在存取的時候做依賴收集和在修改資料的時候執行收集的依賴並更新資料。具體點就是:

vue2 的話採用的是 Object.definePorperty劫持物件的 get 和 set 方法,每個元件範例都會在渲染時初始化一個 watcher 範例,它會將元件渲染過程中所接觸的響應式變數記為依賴,並且儲存了元件的更新方法 update。當依賴的 setter 觸發時,會通知 watcher 觸發元件的 update 方法,從而更新檢視。

Vue3 使用的是 ES6 的 proxy,proxy 不僅能夠追蹤屬性的獲取和修改,還可以追蹤物件的增刪,這在 vue2中需要 set/set/delete 才能實現。然後就是收集的依賴是用元件的副作用渲染函數替代 watcher 範例。

效能方面,從原生 api 角度,proxy 這個方法的效能是不如 Object.property,但是 vue3 強就強在一個是上面提到的可以追蹤物件的增刪,第二個是對巢狀物件的處理上是存取到具體屬性才會把那個物件屬性給轉換成響應式,而 vue2 是在初始化的時候就遞迴呼叫將整個物件和他的屬性都變成響應式,這部分就差了。

擴充套件一

vue2 通過陣列下標更改陣列檢視為什麼不會更新?

尤大:效能不好

注意:vue3 是沒問題的

why 效能不好?

我們看一下響應式處理:

export class Observer {
  this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 這裡對陣列進行單獨處理
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // 對物件遍歷所有鍵值
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
登入後複製

對於物件是通過Object.keys()遍歷全部的鍵值,對陣列只是observe監聽已有的元素,所以通過下標更改不會觸發響應式更新。

理由是陣列的鍵相較物件多很多,當陣列資料大的時候效能會很拉胯。所以不開放

computed 和 watch

Computed 的大體實現和普通的響應式資料是一致的,不過加了延時計算和快取的功能:

在存取computed物件的時候,會觸發 getter ,初始化的時候將 computed 屬性建立的 watcher (vue3是副作用渲染函數)新增到與之相關的響應式資料的依賴收集器中(dep),然後根據裡面一個叫 dirty 的屬性判斷是否要收集依賴,不需要的話直接返回上一次的計算結果,需要的話就執行更新重新渲染檢視。

watchEffect?

watchEffect會自動收集回撥函數中響應式變數的依賴。並在首次自動執行

推薦在大部分時候用 watch 顯式的指定依賴以避免不必要的重複觸發,也避免在後續程式碼修改或重構時不小心引入新的依賴。watchEffect 適用於一些邏輯相對簡單,依賴源和邏輯強相關的場景(或者懶惰的場景 )

$nextTick 原理?

vue有個機制,更新 DOM 是非同步執行的,當資料變化會產生一個非同步更行佇列,要等非同步佇列結束後才會統一進行更新檢視,所以改了資料之後立即去拿 dom 還沒有更新就會拿不到最新資料。所以提供了一個 nextTick 函數,它的回撥函數會在DOM 更新後立即執行。

nextTick 本質上是個非同步任務,由於事件迴圈機制,非同步任務的回撥總是在同步任務執行完成後才得到執行。所以原始碼實現就是根據環境建立非同步函數比如 Promise.then(瀏覽器不支援promise就會用MutationObserver,瀏覽器不支援MutationObserver就會用setTimeout),然後呼叫非同步函數執行回撥佇列。

所以專案中不使用$nextTick的話也可以直接使用Promise.then或者SetTimeout實現相同的效果

Vue 例外處理

1、全域性錯誤處理:Vue.config.errorHandler

Vue.config.errorHandler = function(err, vm, info) {};

如果在元件渲染時出現執行錯誤,錯誤將會被傳遞至全域性Vue.config.errorHandler 設定函數 (如果已設定)。

比如前端監控領域的 sentry,就是利用這個勾點函數進行的 vue 相關異常捕捉處理

2、全域性警告處理:Vue.config.warnHandler

Vue.config.warnHandler = function(msg, vm, trace) {};
登入後複製

注意:僅在開發環境生效

像在模板中參照一個沒有定義的變數,它就會有warning

3、單個vue 範例錯誤處理:renderError

const app = new Vue({
    el: "#app",
    renderError(h, err) {
        return h("pre", { style: { color: "red" } }, err.stack);
    }
});
登入後複製

和元件相關,只適用於開發環境,這個用處不是很大,不如直接看控制檯

4、子孫元件錯誤處理:errorCaptured

Vue.component("cat", {
    template: `<div><slot></slot></div>`,
    props: { name: { type: string } },
    errorCaptured(err, vm, info) {
        console.log(`cat EC: ${err.toString()}\ninfo: ${info}`);
        return false;
    }
});
登入後複製

注:只能在元件內部使用,用於捕獲子孫元件的錯誤,一般可以用於元件開發過程中的錯誤處理

5、終極錯誤捕捉:window.onerror

window.onerror = function(message, source, line, column, error) {};
登入後複製

它是一個全域性的例外處理函數,可以抓取所有的 JavaScript 異常

Vuex 流程 & 原理

Vuex 利用 vue 的mixin 機制,在beforeCreate 勾點前混入了 vuexinit 方法,這個方法實現了將 store 注入 vue 範例當中,並註冊了 store 的參照屬性 store,所以可以使用this.store ,所以可以使用 `this.store.xxx`去引入vuex中定義的內容。

然後 state 是利用 vue 的 data,通過new Vue({data: {$$state: state}} 將 state 轉換成響應式物件,然後使用 computed 函數實時計算 getter

Vue.use函數裡面具體做了哪些事

概念

可以通過全域性方法Vue.use()註冊外掛,並能阻止多次註冊相同外掛,它需要在new Vue之前使用。

該方法第一個引數必須是ObjectFunction型別的引數。如果是Object那麼該Object需要定義一個install方法;如果是Function那麼這個函數就被當做install方法。

Vue.use()執行就是執行install方法,其他傳參會作為install方法的引數執行。

所以**Vue.use()本質就是執行需要注入外掛的install方法**。

原始碼實現

export function initUse (Vue: GlobalAPI) {
 Vue.use = function (plugin: Function | Object) {
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  // 避免重複註冊
  if (installedPlugins.indexOf(plugin) > -1) {
   return this
  }
  // 獲取傳入的第一個引數
  const args = toArray(arguments, 1)
  args.unshift(this)
  if (typeof plugin.install === 'function') {
   // 如果傳入物件中的install屬性是個函數則直接執行
   plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
   // 如果傳入的是函數,則直接(作為install方法)執行
   plugin.apply(null, args)
  }
  // 將已經註冊的外掛推入全域性installedPlugins中
  installedPlugins.push(plugin)
  return this
 }
}
登入後複製

使用方式

installedPlugins import Vue from 'vue'
import Element from 'element-ui'
Vue.use(Element)
登入後複製

怎麼編寫一個vue外掛

要暴露一個install方法,第一個引數是Vue構造器,第二個引數是一個可選的設定項物件

Myplugin.install = function(Vue, options = {}) {
  // 1、新增全域性方法或屬性
  Vue.myGlobalMethod = function() {}
  // 2、新增全域性服務
  Vue.directive('my-directive', {
    bind(el, binding, vnode, pldVnode) {}
  })
  // 3、注入元件選項
  Vue.mixin({
    created: function() {}
  })
  // 4、新增實體方法
  Vue.prototype.$myMethod = function(methodOptions) {}
}
登入後複製

CSS篇

Css直接面試問答的題目相對來說比較少,更多的是需要你能夠當場手敲程式碼實現功能,一般來說備一些常見的佈局,熟練掌握flex基本就沒有什麼問題了。

什麼是 BFC

Block Formatting context,塊級格式上下文

BFC 是一個獨立的渲染區域,相當於一個容器,在這個容器中的樣式佈局不會受到外界的影響。

比如浮動元素、絕對定位、overflow 除 visble 以外的值、display 為 inline/tabel-cells/flex 都能構建 BFC。

常常用於解決

  • 處於同一個 BFC 的元素外邊距會產生重疊(此時需要將它們放在不同 BFC 中);

  • 清除浮動(float),使用 BFC 包裹浮動的元素即可

  • 阻止元素被浮動元素覆蓋,應用於兩列式佈局,左邊寬度固定,右邊內容自適應寬度(左邊float,右邊 overflow)

偽類和偽元素及使用場景

偽類

偽類即:當元素處於特定狀態時才會運用的特殊類

開頭為冒號的選擇器,用於選擇處於特定狀態的元素。比如:first-child選擇第一個子元素;:hover懸浮在元素上會顯示;:focus用鍵盤選定元素時啟用;:link + :visted點選過的連結的樣式;:not用於匹配不符合引數選擇器的元素;:fist-child匹配元素的第一個子元素;:disabled 匹配禁用的表單元素

偽元素

偽元素用於建立一些不在檔案樹中的元素,併為其新增樣式。比如說,我們可以通過::before 來在一個元素前增加一些文字,併為這些文字新增樣式。雖然使用者可以看到這些文字,但是這些文字實際上不在檔案樹中。範例:

::before 在被選元素前插入內容。需要使用 content 屬性來指定要插入的內容。被插入的內容實際上不在檔案樹中

h1:before {
    content: "Hello ";
}
登入後複製

::first-line 匹配元素中第一行的文字

src 和 href 區別

  • href是Hypertext Reference的簡寫,表示超文字參照,指向網路資源所在位置。href 用於在當前檔案和參照資源之間確立聯絡

  • src是source的簡寫,目的是要把檔案下載到html頁面中去。src 用於替換當前內容

  • 瀏覽器解析方式

    當瀏覽器遇到href會並行下載資源並且不會停止對當前檔案的處理。(同時也是為什麼建議使用 link 方式載入 CSS,而不是使用 @import 方式)

    當瀏覽器解析到src ,會暫停其他資源的下載和處理,直到將該資源載入或執行完畢。(這也是script標籤為什麼放在底部而不是頭部的原因)

不定寬高元素的水平垂直居中

  • flex

    <div class="wrapper flex-center">
      <p>horizontal and vertical</p>
    </div>
    
    .wrapper {
        width: 900px;
        height: 300px;
        border: 1px solid #ccc;
    }
    .flex-center {  // 注意是父元素
        display: flex;
        justify-content: center;  // 主軸(豎線)上的對齊方式
        align-items: center;      // 交叉軸(橫軸)上的對齊方式
    }
    登入後複製
  • flex + margin

    <div class="wrapper">
      <p>horizontal and vertical</p>
    </div>
    
    .wrapper {
        width: 900px;
        height: 300px;
        border: 1px solid #ccc;
        display: flex;
    }
    .wrapper > p {
        margin: auto;
    }
    登入後複製
  • Transform + absolute

    <div class="wrapper">
        <img src="test.png">
    </div>
    
    .wrapper {
        width: 300px;
        height: 300px;
        border: 1px solid #ccc;
        position: relative;
    }
    .wrapper > img {
        position: absolute;
        left: 50%;
        top: 50%;
        tansform: translate(-50%, -50%)
    }
    登入後複製

    注:使用該方法只適用於行內元素(a、img、label、br、select等)(寬度隨元素的內容變化而變化),用於塊級元素(獨佔一行)會有問題,left/top 的50%是基於圖片最左側的邊來移動的,tanslate會將多移動的圖片自身的半個長寬移動回去,就實現了水平垂直居中的效果

  • display: table-cell

    <div class="wrapper">
      <p>absghjdgalsjdbhaksldjba</p>
    </div>
    
    .wrapper {
        width: 900px;
        height: 300px;
        border: 1px solid #ccc;
        display: table-cell;
        vertical-align: middle;
        text-align: center;
    }
    登入後複製

瀏覽器和網路篇

瀏覽器和網路是八股中最典型的案例了,無論你是幾年經驗,只要是前端,總會有問到你的瀏覽器和網路協定。

最好的學習文章是李兵老師的《瀏覽器工作原理與實踐》

image.png

跨頁面通訊的方法?

這裡分了同源頁面和不同源頁面的通訊。

不同源頁面可以通過 iframe 作為一個橋樑,因為 iframe 可以指定 origin 來忽略同源限制,所以可以在每個頁面都嵌入同一個 iframe 然後監聽 iframe 中傳遞的 message 就可以了。

同源頁面的通訊大致分為了三類:廣播模式、共用儲存模式和口口相傳模式

第一種廣播模式,就是可以通過 BroadCast Channel、Service Worker 或者 localStorage 作為廣播,然後去監聽廣播事件中訊息的變化,達到頁面通訊的效果。

第二種是共用儲存模式,我們可以通過Shared Worker 或者 IndexedDB,建立全域性共用的資料儲存。然後再通過輪詢去定時獲取這些被儲存的資料是否有變更,達到一個的通訊效果。像常見cookie 也可以作為實現共用儲存達到頁面通訊的一種方式

最後一種是口口相傳模式,這個主要是在使用 window.open 的時候,會返回被開啟頁面的 window 的參照,而在被開啟的頁面可以通過 window.opener 獲取開啟它的頁面的 window 點參照,這樣,多個頁面之間的 window 是能夠相互獲取到的,傳遞訊息的話通過 postMessage 去傳遞再做一個事件監聽就可以了

詳細說說 HTTP 快取

在瀏覽器第一次發起請求服務的過程中,會根據響應報文中的快取標識決定是否快取結果,是否將快取標識和請求結果存入到瀏覽器快取中。

HTTP 快取分為強制快取和協商快取兩類。

強制快取就是請求的時候瀏覽器向快取查詢這次請求的結果,這裡分了三種情況,沒查詢到直接發起請求(和第一次請求一致);查詢到了並且快取結果還沒有失效就直接使用快取結果;查詢到但是快取結果失效了就會使用協商快取。

強制快取有 Expires 和 Cache-control 兩個快取標識,Expires 是http/1.0 的欄位,是用來指定過期的具體的一個時間(如 Fri, 02 Sep 2022 08:03:35 GMT),當伺服器時間和瀏覽器時間不一致的話,就會出現問題。所以在 http1.1 新增了 cache-control 這個欄位,它的值規定了快取的範圍(public/private/no-cache/no-store),也可以規定快取在xxx時間內失效(max-age=xxx)是個相對值,就能避免了 expires帶來的問題。

協商快取就是強制快取的快取結果失效了,瀏覽器攜帶快取標識向伺服器發起請求,有伺服器通過快取標識決定是否使用快取的過程

控制協商快取的欄位有 last-modified / if-modified-since 和 Etag / if-none-match,後者優先順序更高。

大致過程就是通過請求報文傳遞 last-modified 或 Etag 的值給伺服器與伺服器中對應值作對比,若和響應報文中的 if-modified-since 或 if-none-match 結果一致,則協商快取有效,使用快取結果,返回304;否則失效,重新請求結果,返回200

輸入 URL 到頁面展現的全過程

使用者輸入一段內容後,瀏覽器會先去判斷這段內容是搜尋內容還是 URL ,是搜尋內容的話就會接合預設的搜尋引擎生成 URL,比如 google 瀏覽器是goole.com/search?xxxx,如果是 URL 會拼接協定,比如 http/https。當頁面沒有監聽 beforeupload 時間或者同意了繼續執行流程,瀏覽器圖示欄會進入載入中的狀態。

接下來瀏覽器程序會通過 IPC 程序間通訊將 URL 請求傳送給網路程序,網路程序會先去快取中查詢該資源,如果有則攔截請求並直接200返回,沒有的話會進入網路請求流程。

網路請求流程是網路程序請求 DNS 伺服器返回域名對應的IP和埠號(如果這些之前有快取也是直接返回快取結果),如果沒有埠號,http預設為80,https預設為443,如果是https還需要建立 TLS 安全連線建立加密的資料通道。

接著就是 TCP 三次握手建立瀏覽器和伺服器連線,然後進行資料傳輸,資料傳輸完成四次揮手斷開連線,如果設定了connection: keep-alive就可以一直保持連線。

網路程序將通過TCP獲取的封包進行解析,首先是根據響應頭的content-type來判斷資料型別,如果是位元組流或者檔案型別的話,會交給下載管理器進行下載,這時候導航流程就結束了。如果是 text/html 型別,就會通知到瀏覽器程序獲取檔案進行渲染。

瀏覽器程序獲取到渲染的通知,會根據當前頁面和新輸入的頁面判斷是否是同一個站點,是的話就複用之前網頁建立的渲染程序,否則的話會新建立一個單獨的渲染程序。

瀏覽器程序將「提交檔案」的訊息給渲染程序,渲染程序接收到訊息就會和網路程序建立傳輸資料的通道,資料傳輸完成後就返回「確認提交」的資訊給瀏覽器程序。

瀏覽器接收到渲染程序的「確認提交「的訊息後,就會更新瀏覽器的頁面狀態:安全狀態、位址列 URL、前進後退的歷史訊息,並更新 web頁面,此時頁面是空白頁面(白屏)。

頁面渲染過程(重點記憶)

最後是渲染程序對檔案進行頁面解析和子資源載入,渲染程序會將 HTML 轉換成 DOM 樹結構,將 css 轉換成 styleSeets ( CSSOM)。然後複製 DOM 樹過濾掉不顯示的元素建立基本的渲染樹,接著計算每個 DOM 節點的樣式和計算每個節點的位置佈局資訊構建成佈局樹。

具有層疊上下文或者需要要裁剪的地方會獨立建立圖層,這就是分層,最終會形成一個分層樹,渲染程序會給每個圖層生成繪製列表並提交給合成執行緒,合成執行緒將圖層分成圖塊(避免一次性繪製圖層所有內容,可以根據圖塊優先需渲染視口部分),並在光柵化執行緒池中將圖塊轉換成點陣圖。

轉換完畢後合成執行緒傳送繪製圖塊命令 DrawQuard 給瀏覽器程序,瀏覽器根據 DrawQuard 訊息生成頁面,並顯示到瀏覽器上。

速記:

瀏覽器的渲染程序將 html 解析成 dom樹,將 css 解析成 cssom 樹,然後會先複製一份 DOM 樹過濾掉不顯示的元素(比如 display: none),再和 cssom 結合進行計算每個 dom 節點的佈局資訊構建成一個佈局樹。

佈局樹生成完畢就會根據圖層的層疊上下文或者裁剪部分進行分層,形成一個分層樹。

渲染程序再將每個圖層生成繪製列表並提交給合成執行緒,合成執行緒為了避免一次性渲染,就是分塊渲染,會將圖層分成圖塊,並通過光柵化執行緒池將圖塊轉換成點陣圖。

轉換完畢後,合成執行緒將繪製圖塊的命令傳送給瀏覽器進行顯示

TCP 和 UDP 的區別

UDP 是使用者封包協定(User Dataprogram Protocol),IP 通過 IP 地址資訊把封包傳送給指定電腦後,UDP 可以通過埠號把封包分發給正確的程式。UDP 可以校驗資料是否正確,但沒有重發的機制,只會丟棄錯誤的封包,同時 UDP 在傳送之後無法確認是否到達目的地。UDP 不能保證資料的可靠性,但是傳輸的速度非常快,通常運用於線上視訊、互動遊戲這些不那麼嚴格保證資料完整性的領域。

TCP 是為了解決 UDP 的資料容易丟失,且無法正確組裝封包二引入的傳輸控制協定(Transmission Control Protocol),是一種面向連線的,可靠的,基於位元組流的傳輸層通訊協定。TCP 在處理封包丟失的情況,提供了重傳機制;並且 TCP 引入了封包排序機制,可以將亂序的封包組合成完整的檔案。

TCP 頭除了包含目標埠和本機埠號外,還提供了用於排序的序列號,以便接收端通過序號來重排封包。

TCP 和 UDP 的區別

一個 TCP 連線的生命週期會經歷連結階段,資料傳輸和斷開連線階段三個階段。

連線階段

用來建立使用者端和伺服器之間的連結,通過三次握手用來確認使用者端、伺服器端相互之間的封包收發能力。

1、使用者端先傳送 SYN 報文用來確認伺服器端能夠發資料,並進入 SYN_SENT 狀態等待伺服器端確認

2、伺服器端收到 SYN 報文,會向用戶端傳送一個 ACK 確認報文,同時伺服器端也會向用戶端傳送 SYN 報文用來確認使用者端是否能夠傳送資料,此時伺服器端進入 SYN_RCVD 狀態

3、使用者端接收到 ACK + SYN 的報文,就會向伺服器端傳送封包並進入 ESTABLISHED 狀態(建立連線);伺服器端接收到使用者端傳送的 ACK 包也會進入 ESTABLISHED 狀態,完成三次握手

傳輸資料階段

該階段,接收端需要對每個包進行確認操作;

所以當傳送端傳送了一個封包之後,在規定時間內沒有接收到接收端反饋的確認訊息,就會判斷為包丟失,從而觸發重發機制;

一個大檔案在傳輸過程中會分為很多個小封包,封包到達接收端後會根據 TCP 頭中的序號為其排序,保證資料的完整。

斷開連線階段

通過四次揮手,來保證雙方的建立的連線能夠斷開

1、使用者端向伺服器發起 FIN 包,並進入 FIN_WAIT_1 狀態

2、伺服器端收到 FIN 包,發出確認包 ACK,並帶上自己的序號,伺服器端進入 CLOSE_WAIT 狀態。這時候使用者端已經沒有資料要發給伺服器端了,但是伺服器端如果有資料要發給使用者端,使用者端還是需要接收。使用者端收到 ACK 後進入 FIN_WAIT_2 狀態

3、伺服器端資料傳送完畢後,向用戶端傳送 FIN 包,此時伺服器進入 LAST_ACK 狀態

4、使用者端收到 FIN 包發出確認包 ACK ,此時使用者端進入 TIME_WAIT 狀態,等待 2 MSL 後進入 CLOSED 狀態;伺服器端接收到使用者端的 ACK 後就進入 CLOSED 狀態了。

對於四次揮手,因為 TCP 是全雙工通訊,在主動關閉方傳送 FIN 包後,接收端可能還要傳送資料,不能立即關閉伺服器端到使用者端的資料通道,所以也就不能將伺服器端的 FIN 包與對使用者端的 ACK 包合併行送,只能先確認 ACK,然後伺服器待無需傳送資料時再傳送 FIN 包,所以四次揮手時必須是四次封包的互動

Content-length 瞭解嗎?

Content-length 是 http 訊息長度,用十進位制數位表示的位元組的數目。

如果 content-length > 實際長度,伺服器端/使用者端讀取到訊息隊尾時會繼續等待下一個位元組,會出現無響應超時的情況

如果 content-length < 實際長度,首次請求的訊息會被擷取,然後會導致後續的資料解析混亂。

當不確定content-length的值應該使用Transfer-Encoding: chunked,能夠將需要返回的資料分成多個資料塊,直到返回長度為 0 的終止塊

跨域常用方案

什麼是跨域?

協定 + 域名 + 埠號均相同時則為同域,任意一個不同則為跨域

解決方案

1、 傳統的jsonp:利用<script>標籤沒有跨域限制的特點,僅支援 get介面,應該沒有人用這個了

2、 一般使用 cors(跨域資源共用)來解決跨域問題,瀏覽器在請求頭中傳送origin欄位指明請求發起的地址,伺服器端返回Access-control-allow-orign,如果一致的話就可以進行跨域存取

3、 Iframe 解決主域名相同,子域名不同的跨域請求

4、 瀏覽器關閉跨域限制的功能

5、 http-proxy-middleware 代理

預檢

補充:http會在跨域的時候發起一次預檢請求,「需預檢的請求」要求必須首先使用OPTIONS方法發起一個預檢請求到伺服器,以獲知伺服器是否允許該實際請求。「預檢請求」的使用,可以避免跨域請求對伺服器的使用者資料產生未預期的影響。

withCredentials為 true不會產生預請求;content-type為application/json會產生預請求;設定了使用者自定義請求頭會產生預檢請求;delete方法會產生預檢請求;

XSS 和 CSRF

xss基本概念

Xss (Cross site scripting)跨站指令碼攻擊,為了和 css 區別開來所以叫 xss

Xss 指駭客向 html 或 dom 中注入惡意指令碼,從而在使用者瀏覽頁面的時候利用注入指令碼對使用者實施攻擊的手段

惡意指令碼可以做到:竊取 cookie 資訊、監聽使用者行為(比如表單的輸入)、修改DOM(比如偽造登入介面騙使用者輸入賬號密碼)、在頁面生成浮窗廣告等

惡意指令碼注入方式:

  • 儲存型 xss

    駭客利用站點漏洞將惡意 js 程式碼提交到站點伺服器,使用者存取頁面就會導致惡意指令碼獲取使用者的cookie等資訊。

  • 反射性 xss

    使用者將一段惡意程式碼請求提交給 web 伺服器,web 伺服器接收到請求後將惡意程式碼反射到瀏覽器端

  • 基於 DOM 的 xss 攻擊

    通過網路劫持在頁面傳輸過程中更改 HTML 內容

前兩種屬於伺服器端漏洞,最後一種屬於前端漏洞

防止xss攻擊的策略

1、伺服器對輸入指令碼進行過濾或者轉碼,比如將code:<script>alert('你被xss攻擊了')</script>轉換成code:&lt;script&gt;alert(&#39;你被xss攻擊了&#39;)&lt;/script&gt;

2、充分利用內容安全策略 CSP(content-security-policy),可以通過 http 頭資訊的 content-security-policy 欄位控制可以載入和執行的外部資源;或者通過html的meta 標籤<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">

3、cookie設定為 http-only, cookie 就無法通過 document.cookie 來讀取

csrf基本概念

Csrf(cross site request forgery)跨站請求偽造,指駭客引導使用者存取駭客的網站。

CSRF 是指駭客引誘使用者開啟駭客的網站,在駭客的網站中,利用使用者的登入狀態發起的跨站請求。簡單來講,CSRF 攻擊就是駭客利用了使用者的登入狀態,並通過第三方的站點來做一些壞事。

Csrf 攻擊場景

  • 自動發起 get 請求

    比如駭客網站有個圖片:

    <img src="https://time.geekbang.org/sendcoin?user=hacker&number=100">
    登入後複製

    駭客將轉賬的請求介面隱藏在 img 標籤內,欺騙瀏覽器這是一張圖片資源。當該頁面被載入時,瀏覽器會自動發起 img 的資源請求,如果伺服器沒有對該請求做判斷的話,那麼伺服器就會認為該請求是一個轉賬請求,於是使用者賬戶上的 100 極客幣就被轉移到駭客的賬戶上去了。

  • 自動發起 post 請求

    駭客在頁面中構建一個隱藏的表單,當使用者點開連結後,表單自動提交

  • 引誘使用者點選連結

    比如頁面上放了一張美女圖片,下面放了圖片下載地址,而這個下載地址實際上是駭客用來轉賬的介面,一旦使用者點選了這個連結,那麼他的極客幣就被轉到駭客賬戶上了

防止csrf方法

1、設定 cookie 時帶上SameSite: strict/Lax選項

2、驗證請求的來源站點,通過 origin 和 refere 判斷來源站點資訊

3、csrf token,瀏覽器發起請求伺服器生成csrf token,發起請求前會驗證 csrf token是否合法。第三方網站肯定是拿不到這個token。我們的 csrf token 是前後端約定好後寫死的。

websocket

websocket是一種支援雙向通訊的協定,就是伺服器可以主動向使用者端發訊息,使用者端也可以主動向伺服器發訊息。

它是基於 HTTP 協定來建立連線的的,與http協定的相容性很好,所以能通過 HTTP 代理伺服器;沒有同源限制。

WebSocket 是一種事件驅動的協定,這意味著可以將其用於真正的實時通訊。與 HTTP 不同(必須不斷地請求更新),而使用 websockets,更新在可用時就會立即傳送

當連線終止時,WebSockets 不會自動恢復,這是應用開發中需要自己實現的機制,也是存在許多使用者端開源庫的原因之一。

像webpack和vite的devServer就使用了websocket實現熱更新

Post 和 Get 區別

  • 應用場景: (GET 請求是一個冪等的請求)一般 Get 請求用於對伺服器資源不會產生影響的場景,比如說請求一個網頁的資源。(而 Post 不是一個冪等的請求)一般用於對伺服器資源會產生影響的情景,比如註冊使用者這一類的操作。(冪等是指一個請求方法執行多次和僅執行一次的效果完全相同
  • 是否快取: 因為兩者應用場景不同,瀏覽器一般會對 Get 請求快取,但很少對 Post 請求快取。
  • 傳參方式不同: Get 通過查詢字串傳參,Post 通過請求體傳參。
  • 安全性: Get 請求可以將請求的引數放入 url 中向伺服器傳送,這樣的做法相對於 Post 請求來說是不太安全的,因為請求的 url 會被保留在歷史記錄中。
  • 請求長度: 瀏覽器由於對 url 長度的限制,所以會影響 get 請求傳送資料時的長度。這個限制是瀏覽器規定的,並不是 RFC 規定的。
  • 引數型別: get引數只允許ASCII字元,post 的引數傳遞支援更多的資料型別(如檔案、圖片)。

效能優化篇

效能優化是中大型公司非常注重的點,因為和前端人的KPI息息相關,所以自然會作為面試題的常客。

image.png

效能優化有哪些手段

  • 從快取的角度

    • 將一些不常變的巨量資料通過localstorage/sessionStorage/indexdDB 進行讀取
    • 活用 http 快取(強快取和協商快取),將內容儲存在記憶體或者硬碟中,減少對伺服器端的請求
  • 網路方面比較常用的是靜態資源使用 CDN

  • 打包方面

    • 路由的按需載入
    • 優化打包後資源的大小
    • 開啟 gzip 壓縮資源
    • 按需載入三方庫
  • 程式碼層面

    • 減少不必要的請求,刪除不必要的程式碼
    • 避免耗時過長的js處理阻塞主執行緒(耗時且無關 DOM 可以丟到web worker去處理或者拆分成小的任務)
    • 圖片可以使用懶載入的方式,長列表使用虛擬捲動
  • 首屏速度提升

    • 程式碼壓縮,減少打包的靜態資源體積(Terser plugin/MiniCssExtratplugin)
    • 路由懶載入,首屏就只會請求第一個路由的相關資源
    • 使用 cdn加速第三方庫,我們是toB的產品,會需要部署在內網,所以一般不用,toC用的多
    • ssr 伺服器端渲染,由伺服器直接返回拼接好的html頁面
  • vue 常見的效能優化方式

    • 圖片懶載入: vue-lazyLoad
    • 虛擬捲動
    • 函數式元件
    • v-show/ keep-alive 複用 dom
    • deffer延時渲染元件(requestIdleCallback)
    • 時間切片 time slicing

前端監控 SDK 技術要點

  • 可以通過window.performance獲取各項效能指標資料

  • 完整的前端監控平臺包括:資料採集和上報、資料整理和儲存、資料展示

  • 網頁效能指標:

    • FP(first-paint)從頁面載入到第一個畫素繪製到螢幕上的時間
    • FCP(first-contentful-paint),從頁面載入開始到頁面內容的任何部分在螢幕上完成渲染的時間
    • LCP(largest-contentful-paint),從頁面載入到最大文字或影象元素在螢幕上完成渲染的時間
  • 以上指標可以通過獲取

  • 首屏渲染時間計算:通過MutationObserver監聽document物件的屬性變化

如何減少迴流、重繪,充分利用 GPU 加速渲染?

首先應該避免直接使用 DOM API 操作 DOM,像 vue react 虛擬 DOM 讓對 DOM 的多次操作合併成了一次。

  • 樣式集中改變,好的方式是使用動態 class

  • 讀寫操作分離,避免讀後寫,寫後又讀

    // bad 強制重新整理 觸發四次重排+重繪
    div.style.left = div.offsetLeft + 1 + 'px';
    div.style.top = div.offsetTop + 1 + 'px';
    div.style.right = div.offsetRight + 1 + 'px';
    div.style.bottom = div.offsetBottom + 1 + 'px';
    
    
    // good 快取佈局資訊 相當於讀寫分離 觸發一次重排+重繪
    var curLeft = div.offsetLeft;
    var curTop = div.offsetTop;
    var curRight = div.offsetRight;
    var curBottom = div.offsetBottom;
    
    div.style.left = curLeft + 1 + 'px';
    div.style.top = curTop + 1 + 'px';
    div.style.right = curRight + 1 + 'px';
    div.style.bottom = curBottom + 1 + 'px';
    登入後複製

    原來的操作會導致四次重排,讀寫分離之後實際上只觸發了一次重排,這都得益於瀏覽器的渲染佇列機制:

    當我們修改了元素的幾何屬性,導致瀏覽器觸發重排或重繪時。它會把該操作放進渲染佇列,等到佇列中的操作到了一定的數量或者到了一定的時間間隔時,瀏覽器就會批次執行這些操作。

  • 使用display: none後元素不會存在渲染樹中,這時對它進行各種操作,然後更改 display 顯示即可(範例:向2000個div中插入一個div)

  • 通過 documentFragment 建立 dom 片段,在它上面批次操作 dom ,操作完後再新增到檔案中,這樣只有一次重排(範例:一次性插入2000個div)

  • 複製節點在副本上操作然後替換它

  • 使用 BFC 脫離檔案流,重排開銷小

Css 中的transformopacityfilterwill-change能觸發硬體加速

大圖片優化的方案

  • 優化請求數

    • 雪碧圖,將所有圖示合併成一個獨立的圖片檔案,再通過 background-urlbackgroun-position來顯示圖示
    • 懶載入,儘量只載入使用者正則瀏覽器或者即將瀏覽的圖片。最簡單使用監聽頁面捲動判斷圖片是否進入視野;使用 intersection Observer API;使用已知工具庫;使用css的background-url來懶載入
    • base64,小圖示或骨架圖可以使用內聯 base64因為 base64相比普通圖片體積大。注意首屏不需要懶載入,設定合理的佔點陣圖避免抖動。
  • 減小圖片大小

    • 使用合適的格式比如WebP、svg、video替代 GIF 、漸進式 JPEG
    • 削減圖片品質
    • 使用合適的大小和解析度
    • 刪除冗餘的圖片資訊
    • Svg 壓縮
  • 快取

程式碼優化

  • 非響應式變數可以定義在created勾點中使用 this.xxx 賦值

  • 存取區域性變數比全域性變數塊,因為不需要切換作用域

  • 儘可能使用 const宣告變數,注意陣列和物件

  • 使用 v8 引擎時,執行期間,V8 會將建立的物件與隱藏類關聯起來,以追蹤它們的屬性特徵。能夠共用相同隱藏類的物件效能會更好,v8 會針對這種情況去優化。所以為了貼合」共用隱藏類「,我們要避免」先建立再補充「式的動態屬性複製以及動態刪除屬性(使用delete關鍵字)。即儘量在建構函式/物件中一次性宣告所有屬性。屬性刪除時可以設定為 null,這樣可以保持隱藏類不變和繼續共用。

  • 避免記憶體洩露的方式

    • 儘可能少建立全域性變數
    • 手動清除定時器
    • 少用閉包
    • 清除 DOM 參照
    • 弱參照
  • 避免強制同步,在修改 DOM 之前查詢相關值

  • 避免佈局抖動(一次 JS 執行過程中多次執行強制佈局和抖動操作),儘量不要在修改 DOM 結構時再去查詢一些相關值

  • 合理利用 css 合成動畫,如果能用 css 處理就交給 css。因為合成動畫會由合成執行緒執行,不會佔用主執行緒

  • 避免頻繁的垃圾回收,優化儲存結構,避免小顆粒物件的產生

感興趣的可以看看我之前的一篇效能優化技巧整理的文章

前端工程化篇

前端工程化是前端er成長路上最必不可少的技能點,一切能夠提高前端開發效率的功能都可以當做前端工程化的一部分。剛入門的可以從零搭建腳手架開始

對於面試而言,考察的最多的還是對打包工具的掌握程度。

image.png

webpack的執行流程和生命週期

webpack 是為現代 JS 應用提供靜態資源打包功能的 bundle。

核心流程有三個階段: 初始化階段、構建階段和生成階段

1、初始化階段,會從組態檔、設定物件和Shell引數中讀取初始化的引數並與預設設定結合成最終的引數,之以及建立 compiler 編譯器物件和初始化它的執行環境

2、構建階段,編譯器會執行它的 run()方法開始編譯的過程,其中會先確認 entry 入口檔案,從入口檔案開始搜尋和入口檔案有直接或者簡介關聯的所有檔案建立依賴物件,之後再根據依賴物件建立 module 物件,這時候會使用 loader 將模組轉換標準的 js 內容,再呼叫 js 的直譯器將內容轉換成 AST 物件,再從 AST 中找到該模組依賴的模組,遞迴本步驟知道所有入口依賴檔案都經過了本步驟的處理。最後完成模組編譯,得到了每個模組被翻譯的內容和他們之間的關係依賴圖(dependency graph),這個依賴圖就是專案所有用到的模組的對映關係。

3、生成階段,將編譯後的 module 組合成 chunk ,再把每個 chunk 轉換成一個單獨的檔案輸出到檔案列表,確定好輸出內容後,根據設定確定輸出路徑和檔名,就把檔案內容寫入檔案系統

webpack的plugin 和loader

loader

webpack只能理解 JS 和 JSON 檔案,loader 本質上就是個轉換器,能將其他型別的檔案轉換成 webpack 識別的東西

loader 會在 webpack 的構建階段將依賴物件建立的 module 轉換成標準的 js 內容的東西。比如 vue-loader 將vue檔案轉換成 js 模組,圖片字型通過 url-loader 轉換成 data URL,這些 webpack 能夠識別的東西。

可以在 module.rules 中設定不同的 loader 解析不同的檔案

plugin

外掛本質是一個帶有 apply 函數的類class myPlugin { apply(compiler) {} },這個apply 函數有個引數 compiler 是webpack 初始化階段生成的編譯器物件,可以呼叫編譯器物件中的 hooks 註冊各種勾點的回撥這些 hooks 是貫穿整個編譯的生命週期。所以開發者可以通過勾點回撥在裡面插入特定的程式碼,實現特定的功能。

比如stylelint plugin可以指定 stylelint 的需要檢查檔案型別和檔案範圍;HtmlWebpackPlugin 用來生成打包後的模板檔案;MiniCssExtactPlugin會將所有的css提取成獨立的chunks,stylelintplugin可以在開發階段提供樣式的檢查功能。

webpack的hash策略

MiniCssExtractPlugin 對於瀏覽器來說,一方面期望每次請求頁面資源時,獲得的都是最新的資源;一方面期望在資源沒有發生變化時,能夠複用快取物件。這個時候,使用檔名+檔案雜湊值的方式,就可以實現只要通過檔名,就可以區分資源是否有更新。而webpack就內建了hash計算方法,對生成檔案的可以在輸出檔案中新增hash欄位。

Webpack 內建 hash 有三種

  • hash: 專案每次構建都會生成一個hash,和整個專案有關,專案任意地方有改變就會改變

    hash會更據每次工程的內容進行計算,很容易造成不必要的hash變更,不利於版本管理。 一般來說,沒有什麼機會直接使用hash。

  • content hash: 和單個檔案的內容相關。指定檔案的內容發生改變,就會改變hash,內容不變hash 值不變

    對於css檔案來說,一般會使用MiniCssExtractPlugin將其抽取為一個單獨的css檔案。

    此時可以使用contenthash進行標記,確保css檔案內容變化時,可以更新hash。

  • chunk hash:和webpack打包生成的chunk相關。每一個entry,都會有不同的hash。

    一般來說,針對於輸出檔案,我們使用chunkhash。

    因為webpack打包後,最終每個entry檔案及其依賴會生成單獨的一個js檔案。

    此時使用chunkhash,能夠保證整個打包內容的更新準確性。

擴充套件:file-loader 的 hash 可能有同學會表示有以下疑問。

明明經常看到在處理一些圖片,字型的file-loader的打包時,使用的是[name]_[hash:8].[ext]

但是如果改了其他工程檔案,比如index.js,生成的圖片hash並沒有變化。

這裡需要注意的是,file-loader的hash欄位,這個loader自己定義的預留位置,和webpack的內建hash欄位並不一致。

這裡的hash是使用md4等hash演演算法,對檔案內容進行hash。

所以只要檔案內容不變,hash還是會保持一致。

vite原理

Vite 主要由兩個部分組成

  • 開發環境

    Vite 利用瀏覽器去解析 imports,在伺服器端按需編譯返回,完全跳過了打包這個概念,伺服器隨起隨用(就相當於把我們在開發的檔案轉換成 ESM 格式直接傳送給瀏覽器)

    當瀏覽器解析 import HelloWorld from './components/HelloWorld.vue' 時,會向當前域名傳送一個請求獲取對應的資源(ESM支援解析相對路徑),瀏覽器直接下載對應的檔案然後解析成模組記錄(開啟 network 面板可以看到響應資料都是 ESM 型別的 js)。然後範例化為模組分配記憶體,按照匯入匯出語句建立模組和記憶體的對映關係。最後執行程式碼。

    vite 會啟動一個 koa 伺服器攔截瀏覽器對 ESM 的請求,通過請求路徑找到目錄下對應的檔案並處理成 ESM 格式返回給使用者端

    vite的熱載入是在使用者端和伺服器端之間建立了 websocket 連線,程式碼修改後伺服器端傳送訊息通知使用者端去請求修改模組的程式碼,完成熱更新,就是改了哪個 view 檔案就重新請求那個檔案,這樣保證了熱更新速度不受專案大小影響。

    開發環境會使用 esbuild 對依賴進行個預構建快取,第一次啟動會慢一點,後面的啟動會直接讀取快取

  • 生產環境

    使用 rollup 來構建程式碼,提供指令可以用來優化構建過程。缺點就是開發環境和生產環境可能不一致;

webpack 和 vite 對比

Webpack 的熱更新原理簡單來說就是,一旦發生某個依賴(比如 a.js )改變,就將這個依賴所處的 module 的更新,並將新的 module 傳送給瀏覽器重新執行。每次熱更新都會重新生成 bundle。試想如果依賴越來越多,就算只修改一個檔案,理論上熱更新的速度也會越來越慢

Vite 利用瀏覽器去解析 imports,在伺服器端按需編譯返回,完全跳過了打包這個概念,伺服器隨起隨用,熱更新是在使用者端和伺服器端之間建立了 websocket 連線,程式碼修改後伺服器端傳送訊息通知使用者端去請求修改模組的程式碼,完成熱更新,就是改了哪個檔案就重新請求那個檔案,這樣保證了熱更新速度不受專案大小影響。

所以vite目前的最大亮點在於開發體驗上,服務啟動快、熱更新快,明顯地優化了開發者體驗,生產環境因為底層是 rollup ,rollup更適合小的程式碼庫,從擴充套件和功能上都是不如 webpack 的,可以使用vite作為一個開發伺服器dev server使用,生產打包用webpack這樣的模式。

做過哪些 wewbpack 的優化

0、升級 webpack 版本,3升4,實測是提升了幾十秒的打包速度

1、splitChunksPlugin 抽離共用模組輸出單獨的chunks ,像一些第三方的依賴庫,可以單獨拆分出來,避免單個chunks過大。

2、DllPlugin 作用同上,這個依賴庫相當於從業務程式碼中剝離出來,只有依賴庫自身版本變化才會重新打包,提升打包速度

3、loaders的執行是同步的,同各模組會執行全部的loaders

  • 可以使用oneOf,只要匹配上對應的loader就不會繼續執行loader
  • 使用 happyPack 將loader的同步執行轉換成並行比如(style-loader,css-loader,less-loader合併起來執行)

4、exclude/include 指定匹配範圍;alias指定路徑別名;

5、cache-loader持久化儲存;

6、ESM專案開啟 useExport標記,使用 tree-shaking

7、exclude/include 指定匹配範圍;alias指定路徑別名;cache-loader持久化儲存;

8、terserPlugin 可以提供程式碼壓縮,去除註釋、去除空格的功能;MiniCssExtractPlugin 壓縮 css

npm run 執行過程

0、在 package.json 檔案中可以定義 script 設定項,裡面可以定義執行指令碼的鍵和值

1、在 npm install 的時候,npm 會讀取設定將執行指令碼軟連結node_modules/.bin目錄下,同時將./bin加入當環境變數$PATH,所以如果在全域性直接執行該命令會去全域性目錄裡找,可能會找不到該命令就會報錯。比如 npm run start,他是執行的webpack-dev-server帶上引數

2、還有一種情況,就是單純的執行指令碼命令,比如 npm run build,實際執行的是 node build.js,即使用 node 執行 build.js 這個檔案

ESM和CJS 的區別

ES6

  • ES6模組是參照,重新賦值會編譯報錯,不能修改其變數的指標指向,但可以改變內部屬性的值;
  • ES6模組中的值屬於動態唯讀參照。
  • 對於唯讀來說,即不允許修改引入變數的值,import的變數是唯讀的,不論是基本資料型別還是複雜資料型別。當模組遇到import命令時,就會生成一個唯讀參照。等到指令碼真正執行時,再根據這個唯讀參照,到被載入的那個模組裡面去取值。
  • 對於動態來說,原始值發生變化,import 載入的值也會發生變化。不論是基本資料型別還是複雜資料型別。
  • 迴圈載入時,ES6模組是動態參照。只要兩個模組之間存在某個參照,程式碼就能夠執行。

CommonJS

  • CommonJS模組是拷貝(淺拷貝),可以重新賦值,可以修改指標指向;
  • 對於基本資料型別,屬於複製。即會被模組快取。同時,在另一個模組可以對該模組輸出的變數重新賦值。
  • 對於複雜資料型別,屬於淺拷貝。由於兩個模組參照的物件指向同一個記憶體空間,因此對該模組的值做修改時會影響另一個模組。 當使用require命令載入某個模組時,就會執行整個模組的程式碼。
  • 當使用require命令載入同一個模組時,不會再執行該模組,而是取到快取之中的值。也就是說,CommonJS模組無論載入多少次,都只會在第一次載入時執行一次,以後再載入,就返回第一次執行的結果,除非手動清除系統快取。
  • 當迴圈載入時,指令碼程式碼在require的時候,就會全部執行。一旦出現某個模組被"迴圈載入",就只輸出已經執行的部分,還未執行的部分不會輸出。

設計模式篇

代理模式

代理模式:為物件提供一個代用品或預留位置,以便控制對它的存取

例如實現圖片懶載入的功能,先通過一張loading圖佔位,然後通過非同步的方式載入圖片,等圖片載入好了再把完成的圖片載入到img標籤裡面

裝飾者模式

裝飾者模式的定義:在不改變物件自身的基礎上,在程式執行期間給物件動態地新增方法

通常運用在原有方法維持不變,在原有方法上再掛載其他方法來滿足現有需求。

像 typescript 的裝飾器就是一個典型的裝飾者模式,還有 vue 當中的 mixin

單例模式

保證一個類僅有一個範例,並提供一個存取它的全域性存取點。實現的方法為先判斷範例存在與否,如果存在則直接返回,如果不存在就建立了再返回,這就確保了一個類只有一個範例物件

比如 ice stark 的子應用,一次只保證渲染一個子應用

觀察者模式和釋出訂閱模式

1、雖然兩種模式都存在訂閱者和釋出者(具體觀察者可認為是訂閱者、具體目標可認為是釋出者),但是觀察者模式是由具體目標排程的,而釋出/訂閱模式是統一由排程中心調的,所以觀察者模式的訂閱者與釋出者之間是存在依賴的,而釋出/訂閱模式則不會。

2、兩種模式都可以用於鬆散耦合,改程序式碼管理和潛在的複用。

3、在觀察者模式中,觀察者是知道Subject的,Subject一直保持對觀察者進行記錄。然而,在釋出訂閱模式中,釋出者和訂閱者不知道對方的存在。它們只有通過訊息代理進行通訊

4、觀察者模式大多數時候是同步的,比如當事件觸發,Subject就會去呼叫觀察者的方法。而釋出-訂閱模式大多數時候是非同步的(使用訊息佇列)

其他

正規表示式

正規表示式是什麼資料型別?

是物件,let re = /ab+c/等價於let re = new RegExp('ab+c')

正則貪婪模式和非貪婪模式?

量詞

*:0或多次; ?:0或1次; +:1到多次; {m}:出現m次; {m,}:出現至少m次; {m, n}:出現m到n次

貪婪模式

正則中,表示次數的量詞預設是貪婪的,會盡可能匹配最大長度,比如a*會從第一個匹配到a的時候向後匹配儘可能多的a,直到沒有a為止。

非貪婪模式

在量詞後面加?就能變成非貪婪模式,非貪婪即找出長度最小且滿足條件的

貪婪& 非貪婪特點

貪婪模式和非貪婪模式,都需要發生回溯才能完成相應的功能

獨佔模式

獨佔模式和貪婪模式很像,獨佔模式會盡可能多地去匹配,如果匹配失敗就結束,不會進行回溯,這樣的話就比較節省時間。

寫法:量詞後面使用+

優缺點:獨佔模式效能好,可以減少匹配的時間和 cpu 資源;但是某些情況下匹配不到,比如:


正則文字結果
貪婪模式a{1,3}abaaab匹配
非貪婪模式a{1,3}?abaaab匹配
獨佔模式a{1,3}+abaaab不匹配

a{1,3}+ab 去匹配 aaab 字串,a{1,3}+ 會把前面三個 a 都用掉,並且不會回溯

常見正則匹配

操作符說明範例

.表示任何單個字元


[ ]字元集,對單個字元給出範圍[abc]表示 a、b、c,[a-z]表示 a-z 的單個字元

[^ ]非字元集,對單個字元給出排除範圍[^abc]表示非a或b或c的單個字元

_前一個字元零次或無限次擴充套件abc_表示 ab、abc、abcc、abccc 等

``左右表示式的任意一個`abcdef`表示 abc、def
$匹配字串結尾abc$表示 abc 且在一個字串結尾

( )分組標記內部只能使用(abc)表示 abc,`(abcdef)`表示 abc、def
\D非數位


\d數位,等價於0-9


\S可見字元


\s空白字元(空格、換行、製表符等等)


\W非單詞字元


\w單詞字元,等價於[a-z0-9A-Z_]


匹配字串開頭^abc表示 abc 且在一個字串的開頭

{m,n}擴充套件前一個字元 m 到 n 次ab{1,2}c表示 abc、abbc

{m}擴充套件前一個字元 m 次ab{2}c表示 abbc

{m,}匹配前一個字元至少m 次


前一個字元 0 次或 1 次擴充套件abc? 表示 ab、abc

單元測試

概念:前端自動化測試領域的, 用來驗證獨立的程式碼片段能否正常工作

1、可以直接用 Node 中自帶的 assert 模組做斷言:如果當前程式的某種狀態符合 assert 的期望此程式才能正常執行,否則直接退出應用。

function multiple(a, b) {
    let result = 0;
    for (let i = 0; i < b; ++i)
        result += a;
    return result;
}

const assert = require('assert');
assert.equal(multiple(1, 2), 3));
登入後複製

2、常見單測工具:Jest,使用範例

const sum = require('./sum');

describe('sum function test', () => {
  it('sum(1, 2) === 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
  
  // 這裡 test 和 it 沒有明顯區別,it 是指: it should xxx, test 是指 test xxx
  test('sum(1, 2) === 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
})
登入後複製

babel原理和用途

babel 用途

  • 跳脫 esnext、typescript 到目標環境支援 js (高階語言到到低階語言叫編譯,高階語言到高階語言叫轉譯)
  • 程式碼轉換(taro)
  • 程式碼分析(模組分析、tree-shaking、linter 等)

bebel 如何轉換的?

對原始碼字串 parse 生成 AST,然後對 AST 進行增刪改,然後輸出目的碼字串

轉換過程

  • parse 階段:首先使用 @babel/parser將原始碼轉換成 AST

  • transform 階段:接著使用@babel/traverse遍歷 AST,並呼叫 visitor 函數修改 AST,修改過程中通過@babel/types來建立、判斷 AST 節點;使用@babel/template來批次建立 AST

  • generate 階段:使用@babel/generate將 AST 列印為目的碼字串,期間遇到程式碼錯誤位置時會用到@babel/code-frame

技術面.png

好的,以上就是我對三年前端經驗通過面試題做的一個總結了,祝大家早日找到心儀的工作~

【推薦學習:、】