前端乾貨預警🚨立刻金三銀四,精選10道最新面試題帶你起飛🛫️

精選10道面試題

1.Vue v-model 是如何實現的,語法糖實際是什麼

公司:脈脈css

分類:Vuehtml

答案&解析前端

1、語法糖

指計算機語言中添加的某種語法,這種語法對語言的功能並無影響,可是更方便程序員使用。一般來講使用語法糖可以增長程序的可讀性,從而減小程序代碼出錯的機會。糖在不改變其所在位置的語法結構的前提下,實現了運行時的等價。能夠簡單理解爲,加糖後的代碼編譯後跟加糖前同樣,代碼更簡潔流暢,代碼更語義天然.vue

2、實現原理

1.做用在普通表單元素上

動態綁定了 input 的 value 指向了 messgae 變量,而且在觸發 input 事件的時候去動態把 message 設置爲目標值node

<input v-model="sth" />
// 等同於
<input v-bind:value="message" v-on:input="message=$event.target.value" > //$event 指代當前觸發的事件對象; //$event.target 指代當前觸發的事件對象的dom; //$event.target.value 就是當前dom的value值; //在@input方法中,value => sth; //在:value中,sth => value; 複製代碼

2.做用在組件上

在自定義組件中,v-model 默認會利用名爲 value 的 prop 和名爲 input 的事件react

本質是一個父子組件通訊的語法糖,經過prop和$.emit實現jquery

所以父組件v-model語法糖本質上能夠修改成 '<child :value="message" @input="function(e){message = e}"></child>'webpack

在組件的實現中,咱們是能夠經過 v-model屬性 來配置子組件接收的prop名稱,以及派發的事件名稱。ios

例子git

// 父組件
<aa-input v-model="aa"></aa-input>
// 等價於
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>

// 子組件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>

props:{value:aa,}
methods:{
    onmessage(e){
        $emit('input',e.target.value)
    }
}
複製代碼

默認狀況下,一個組件上的 v-model 會把 value 用做 prop 且把 input 用做 event

可是一些輸入類型好比單選框和複選框按鈕可能想使用 value prop 來達到不一樣的目的。使用 model 選項能夠迴避這些狀況產生的衝突。

js 監聽input 輸入框輸入數據改變,用oninput ,數據改變之後就會馬上出發這個事件。

經過input事件把數據$emit 出去,在父組件接受。

父組件設置v-model的值爲input$emit過來的值。


2.React 中 setState 後發生了什麼?setState 爲何默認是異步?setState 何時是同步?

公司:微醫

分類:React

答案&解析

1、React中setState後發生了什麼

在代碼中調用setState函數以後,React 會將傳入的參數對象與組件當前的狀態合併,而後觸發所謂的調和過程(Reconciliation)。

通過調和過程,React 會以相對高效的方式根據新的狀態構建 React 元素樹而且着手從新渲染整個UI界面。

在 React 獲得元素樹以後,React 會自動計算出新的樹與老樹的節點差別,而後根據差別對界面進行最小化重渲染。

在差別計算算法中,React 可以相對精確地知道哪些位置發生了改變以及應該如何改變,這就保證了按需更新,而不是所有從新渲染。

2、setState 爲何默認是異步

假如全部setState是同步的,意味着每執行一次setState時(有可能一個同步代碼中,屢次setState),都從新vnode diff + dom修改,這對性能來講是極爲很差的。若是是異步,則能夠把一個同步代碼中的多個setState合併成一次組件更新。

3、setState 何時是同步

在setTimeout或者原生事件中,setState是同步的。


3.多個 tab 只對應一個內容框,點擊每一個 tab 都會請求接口並渲染到內容框,怎麼確保頻繁點擊 tab 但可以確保數據正常顯示?

公司:愛範兒

分類:JavaScript

答案&解析

1、分析

由於每一個請求處理時長不一致,可能會致使先發送的請求後響應,即請求響應順序和請求發送順序不一致,從而致使數據顯示不正確。

便可以理解爲連續觸發多個請求,如何保證請求響應順序和請求發送順序一致。對於問題所在場景,用戶只關心最後數據是否顯示正確,便可以簡化爲:連續觸發多個請求,如何保證最後響應的結果是最後發送的請求(不關注以前的請求是否發送或者響應成功)

相似場景:input輸入框即時搜索,表格快速切換頁碼

2、解決方案

防抖(過濾掉一些非必要的請求) + 取消上次未完成的請求(保證最後一次請求的響應順序)

取消請求方法:

  • XMLHttpRequest 使用 abort api 取消請求
  • axios 使用 cancel token 取消請求

僞代碼(以 setTimeout 模擬請求,clearTimeout 取消請求)

/** * 函數防抖,必定時間內連續觸發事件只執行一次 * @param {*} func 須要防抖的函數 * @param {*} delay 防抖延遲 * @param {*} immediate 是否當即執行,爲true表示連續觸發時當即執行,即執行第一次,爲false表示連續觸發後delay ms後執行一次 */
let debounce = function(func, delay = 100, immediate = false) {
  let timeoutId, last, context, args, result

  function later() {
    const interval = Date.now() - last
    if (interval < delay && interval >= 0) {
      timeoutId = setTimeout(later, delay - interval)
    } else {
      timeoutId = null
      if (!immediate) {
        result = func.apply(context, args)
        context = args = null
      }
    }
  }

  return function() {
    context = this
    args = arguments
    last = Date.now()

    if (immediate && !timeoutId) {
      result = func.apply(context, args)
      context = args = null // 解除引用
    }
    
    if (!timeoutId) {
      timeoutId = setTimeout(later, delay)
    }

    return result
  }
}


let flag = false   // 標誌位,表示當前是否正在請求數據
let xhr = null

let request = (i) => {
    if (flag) {
        clearTimeout(xhr)
        console.log(`取消第${i - 1}次請求`)
    }
    flag = true
    console.log(`開始第${i}次請求`)
    xhr = setTimeout(() => {
        console.log(`請求${i}響應成功`)
        flag = false
    }, Math.random() * 200)
}

let fetchData = debounce(request, 50)  // 防抖

// 模擬連續觸發的請求
let count = 1 
let getData = () => {
  setTimeout(() => {
    fetchData(count)
    count++
    if (count < 11) {
        getData()
    }
  }, Math.random() * 200)
}
getData()

/* 某次測試輸出: 開始第2次請求 請求2響應成功 開始第3次請求 取消第3次請求 開始第4次請求 請求4響應成功 開始第5次請求 請求5響應成功 開始第8次請求 取消第8次請求 開始第9次請求 請求9響應成功 開始第10次請求 請求10響應成功 */
複製代碼

前方高能預警🚨 🚨 🚨

因文章篇幅過長,影響閱讀體驗,接下來的7道題目答案將被摺疊,點擊查看解析,便可查看所有答案,選擇你感興趣的題查看答案吧~


4.對虛擬 DOM 的理解?虛擬 DOM 主要作了什麼?虛擬 DOM 自己是什麼?

公司:有贊、微醫、58

分類:React、Vue

答案&解析

查看解析

1、什麼是虛擬Dom

從本質上來講,Virtual Dom是一個JavaScript對象,經過對象的方式來表示DOM結構。將頁面的狀態抽象爲JS對象的形式,配合不一樣的渲染工具,使跨平臺渲染成爲可能。經過事務處理機制,將屢次DOM修改的結果一次性的更新到頁面上,從而有效的減小頁面渲染的次數,減小修改DOM的重繪重排次數,提升渲染性能

虛擬dom是對DOM的抽象,這個對象是更加輕量級的對DOM的描述。它設計的最初目的,就是更好的跨平臺,好比Node.js就沒有DOM,若是想實現SSR,那麼一個方式就是藉助虛擬dom, 由於虛擬dom自己是js對象。

在代碼渲染到頁面以前,vue或者react會把代碼轉換成一個對象(虛擬DOM)。以對象的形式來描述真實dom結構,最終渲染到頁面。在每次數據發生變化前,虛擬dom都會緩存一份,變化之時,如今的虛擬dom會與緩存的虛擬dom進行比較。

在vue或者react內部封裝了diff算法,經過這個算法來進行比較,渲染時修改改變的變化,原先沒有發生改變的經過原先的數據進行渲染。

另外現代前端框架的一個基本要求就是無須手動操做DOM,一方面是由於手動操做DOM沒法保證程序性能,多人協做的項目中若是review不嚴格,可能會有開發者寫出性能較低的代碼,另外一方面更重要的是省略手動DOM操做能夠大大提升開發效率。

2、爲何要用 Virtual DOM

1.保證性能下限,在不進行手動優化的狀況下,提供過得去的性能

看一下頁面渲染的一個流程:

  • 解析HTNL ☞ 生成DOM🌲 ☞ 生成 CSSOM ☞ Layout ☞ Paint ☞ Compiler

下面對比一下修改DOM時真實DOM操做和Virtual DOM的過程,來看一下它們重排重繪的性能消耗:

  • 真實DOM: 生成HTML字符串 + 重建全部的DOM元素
  • Virtual DOM: 生成vNode + DOMDiff + 必要的dom更新

Virtual DOM的更新DOM的準備工做耗費更多的時間,也就是JS層面,相比於更多的DOM操做它的消費是極其便宜的。尤雨溪在社區論壇中說道: 框架給你的保證是,你不須要手動優化的狀況下,我依然能夠給你提供過得去的性能。

2.跨平臺

Virtual DOM本質上是JavaScript的對象,它能夠很方便的跨平臺操做,好比服務端渲染、uniapp等。

3、Virtual DOM真的比真實DOM性能好嗎

  1. 首次渲染大量DOM時,因爲多了一層虛擬DOM的計算,會比innerHTML插入慢。
  2. 正如它能保證性能下限,在真實DOM操做的時候進行鍼對性的優化時,仍是更快的。

5.Webpack 爲何慢,如何進行優化

分類:工程化

答案&解析

查看解析

1、webpack 爲何慢

webpack是所謂的模塊捆綁器,內部有循環引用來分析模塊間之間的依賴,把文件解析成AST,經過一系類不一樣loader的加工,最後所有打包到一個js文件裏。

webpack4之前在打包速度上沒有作過多的優化手段,編譯慢的大部分時間是花費在不一樣loader編譯過程,webpack4之後,吸取借鑑了不少優秀工具的思路,

如支持0配置,多線程等功能,速度也大幅提高,但依然有一些優化手段。如合理的代碼拆分,公共代碼的提取,css資源的抽離

2、優化 Webpack 的構建速度

  • 使用高版本的 Webpack (使用webpack4)
  • 多線程/多實例構建:HappyPack(不維護了)、thread-loader
  • 縮小打包做用域:
    • exclude/include (肯定 loader 規則範圍)
    • resolve.modules 指明第三方模塊的絕對路徑 (減小沒必要要的查找)
    • resolve.extensions 儘量減小後綴嘗試的可能性
    • noParse 對徹底不須要解析的庫進行忽略 (不去解析但仍會打包到 bundle 中,注意被忽略掉的文件裏不該該包含 import、require、define 等模塊化語句)
    • IgnorePlugin (徹底排除模塊)
    • 合理使用alias
  • 充分利用緩存提高二次構建速度:
    • babel-loader 開啓緩存
    • terser-webpack-plugin 開啓緩存
    • 使用 cache-loader 或者 hard-source-webpack-plugin

注意:thread-loader 和 cache-loader 兩個要一塊兒使用的話,請先放 cache-loader 接著是 thread-loader 最後纔是 heavy-loader

  • DLL
    • 使用 DllPlugin 進行分包,使用 DllReferencePlugin(索引連接) 對 manifest.json 引用,讓一些基本不會改動的代碼先打包成靜態資源,避免反覆編譯浪費時間。

3、使用Webpack4帶來的優化

  • V8帶來的優化(for of替代forEach、Map和Set替代Object、includes替代indexOf)
  • 默認使用更快的md4 hash算法
  • webpack AST能夠直接從loader傳遞給AST,減小解析時間
  • 使用字符串方法替代正則表達式

來看下具體使用

1.noParse

  • 不去解析某個庫內部的依賴關係
  • 好比jquery 這個庫是獨立的, 則不去解析這個庫內部依賴的其餘的東西
  • 在獨立庫的時候可使用
module.exports = {
  module: {
    noParse: /jquery/,
    rules:[]
  }
}
複製代碼

2.IgnorePlugin

  • 忽略掉某些內容 不去解析依賴庫內部引用的某些內容
  • 從moment中引用 ./local 則忽略掉
  • 若是要用local的話 則必須在項目中必須手動引入 import 'moment/locale/zh-cn'
module.exports = {
  plugins: [
    new Webpack.IgnorePlugin(/\.\/local/, /moment/),
  ]
}
複製代碼

3.dillPlugin

  • 不會屢次打包, 優化打包時間
  • 先把依賴的不變的庫打包
  • 生成 manifest.json文件
  • 而後在webpack.config中引入
  • webpack.DllPluginWebpack.DllReferencePlugin

4.happypack -> thread-loader

  • 大項目的時候開啓多線程打包
  • 影響前端發佈速度的有兩個方面,一個是 構建 ,一個就是 壓縮 ,把這兩個東西優化起來,能夠減小不少發佈的時間。

5.thread-loader

thread-loader 會將您的 loader 放置在一個 worker 池裏面運行,以達到多線程構建。

把這個 loader 放置在其餘 loader 以前,放置在這個 loader 以後的 loader 就會在一個單獨的 worker 池(worker pool)中運行。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          // 你的高開銷的loader放置在此 (e.g babel-loader)
        ]
      }
    ]
  }
}
複製代碼

每一個 worker 都是一個單獨的有 600ms 限制的 node.js 進程。同時跨進程的數據交換也會被限制。請在高開銷的loader中使用,不然效果不佳

6.壓縮加速——開啓多線程壓縮

不推薦使用 webpack-paralle-uglify-plugin,項目基本處於沒人維護的階段,issue 沒人處理,pr沒人合併。

Webpack 4.0之前:uglifyjs-webpack-plugin,parallel參數

module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        parallel: true,
      }),
    ],
  },};
複製代碼

推薦使用 terser-webpack-plugin

module.exports = {
  optimization: {
    minimizer: [new TerserPlugin(
      parallel: true   // 多線程
    )],
  },
};
複製代碼

6.客戶端緩存有幾種方式?瀏覽器出現 from disk、from memory 的策略是啥

分類:網絡&安全

答案&解析

查看解析

1、客戶端緩存

瀏覽器緩存策略:

瀏覽器每次發起請求時,先在本地緩存中查找結果以及緩存標識,根據緩存標識來判斷是否使用本地緩存。若是緩存有效,則使用本地緩存;不然,則向服務器發起請求並攜帶緩存標識。根據是否需向服務器發起HTTP請求,將緩存過程劃分爲兩個部分:強制緩存和協商緩存,強緩優先於協商緩存

HTTP緩存都是從第二次請求開始的

  • 第一次請求資源時,服務器返回資源,並在response header中回傳資源的緩存策略;
  • 第二次請求時,瀏覽器判斷這些請求參數,擊中強緩存就直接200,不然就把請求參數加到request header頭中傳給服務器,看是否擊中協商緩存,擊中則返回304,不然服務器會返回新的資源。這是緩存運做的一個總體流程圖:

img

1.強緩存

服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求,直接用緩存,不在時間內,執行比較緩存策略。

強緩存命中則直接讀取瀏覽器本地的資源,在network中顯示的是from memory或者from disk

控制強制緩存的字段有:Cache-Control(http1.1)和Expires(http1.0)

  • Cache-control是一個相對時間,用以表達自上次請求正確的資源以後的多少秒的時間段內緩存有效。
  • Expires是一個絕對時間。用以表達在這個時間點以前發起請求能夠直接從瀏覽器中讀取數據,而無需發起請求
  • Cache-Control的優先級比Expires的優先級高。前者的出現是爲了解決Expires在瀏覽器時間被手動更改致使緩存判斷錯誤的問題。
  • 若是同時存在則使用Cache-control。

1)強緩存-expires

該字段是服務器響應消息頭字段,告訴瀏覽器在過時時間以前能夠直接從瀏覽器緩存中存取數據。

Expires 是 HTTP 1.0 的字段,表示緩存到期時間,是一個絕對的時間 (當前時間+緩存時間)。在響應消息頭中,設置這個字段以後,就能夠告訴瀏覽器,在未過時以前不須要再次請求。

因爲是絕對時間,用戶可能會將客戶端本地的時間進行修改,而致使瀏覽器判斷緩存失效,從新請求該資源。此外,即便不考慮修改,時差或者偏差等因素也可能形成客戶端與服務端的時間不一致,導致緩存失效。

優點特色:

  • HTTP 1.0 產物,能夠在HTTP 1.0和1.1中使用,簡單易用。
  • 以時刻標識失效時間。

劣勢問題:

  • 時間是由服務器發送的(UTC),若是服務器時間和客戶端時間存在不一致,可能會出現問題。
  • 存在版本問題,到期以前的修改客戶端是不可知的。

2)強緩存-cache-control

已知Expires的缺點以後,在HTTP/1.1中,增長了一個字段Cache-control,該字段表示資源緩存的最大有效時間,在該時間內,客戶端不須要向服務器發送請求。

這二者的區別就是前者是絕對時間,然後者是相對時間。下面列舉一些 Cache-control 字段經常使用的值:(完整的列表能夠查看MDN)

  • max-age:即最大有效時間。
  • must-revalidate:若是超過了 max-age 的時間,瀏覽器必須向服務器發送請求,驗證資源是否還有效。
  • no-cache:不使用強緩存,須要與服務器驗證緩存是否新鮮。
  • no-store: 真正意義上的「不要緩存」。全部內容都不走緩存,包括強制和對比。
  • public:全部的內容均可以被緩存 (包括客戶端和代理服務器, 如 CDN)
  • private:全部的內容只有客戶端才能夠緩存,代理服務器不能緩存。默認值。

Cache-control 的優先級高於 Expires,爲了兼容 HTTP/1.0 和 HTTP/1.1,實際項目中兩個字段均可以設置。

該字段能夠在請求頭或者響應頭設置,可組合使用多種指令:

  • 可緩存性
    • public:default,瀏覽器和緩存服務器均可以緩存頁面信息
    • private:代理服務器不可緩存,只能被單個用戶緩存
    • no-cache:瀏覽器器和服務器都不該該緩存頁面信息,但仍可緩存,只是在緩存前須要向服務器確認資源是否被更改。可配合private, 過時時間設置爲過去時間。
    • only-if-cache:客戶端只接受已緩存的響應
  • 到期
    • max-age=<seconds>:緩存存儲的最大週期,超過這個週期被認爲過時。
    • s-maxage=<seconds>:設置共享緩存,好比can。會覆蓋max-age和expires。
    • max-stale[=<seconds>]:客戶端願意接收一個已通過期的資源
    • min-fresh=<seconds>:客戶端但願在指定的時間內獲取最新的響應
    • stale-while-revalidate=<seconds>:客戶端願意接收陳舊的響應,而且在後臺一部檢查新的響應。時間表明客戶端願意接收陳舊響應 的時間長度。
    • stale-if-error=<seconds>:如新的檢測失敗,客戶端則願意接收陳舊的響應,時間表明等待時間。
  • 從新驗證和從新加載
    • must-revalidate:如頁面過時,則去服務器進行獲取。
    • proxy-revalidate:用於共享緩存。
    • immutable:響應正文不隨時間改變。
  • 其餘
    • no-store:絕對禁止緩存
    • no-transform:不得對資源進行轉換和轉變。例如,不得對圖像格式進行轉換。

優點特色:

  • HTTP 1.1 產物,以時間間隔標識失效時間,解決了Expires服務器和客戶端相對時間的問題。
  • 比Expires多了不少選項設置。

劣勢問題:

  • 存在版本問題,到期以前的修改客戶端是不可知的。

2.協商緩存

讓客戶端與服務器之間能實現緩存文件是否更新的驗證、提高緩存的複用率,將緩存信息中的Etag和Last-Modified經過請求發送給服務器,由服務器校驗,返回304狀態碼時,瀏覽器直接使用緩存。

  • 協商緩存的狀態碼由服務器決策返回200或者304
  • 當瀏覽器的強緩存失效的時候或者請求頭中設置了不走強緩存,而且在請求頭中設置了If-Modified-Since 或者 If-None-Match 的時候,會將這兩個屬性值到服務端去驗證是否命中協商緩存,若是命中了協商緩存,會返回 304 狀態,加載瀏覽器緩存,而且響應頭會設置 Last-Modified 或者 ETag 屬性。
  • 對比緩存在請求數上和沒有緩存是一致的,但若是是 304 的話,返回的僅僅是一個狀態碼而已,並無實際的文件內容,所以 在響應體體積上的節省是它的優化點。
  • 協商緩存有 2 組字段(不是兩個),控制協商緩存的字段有:Last-Modified/If-Modified-since(http1.0)和Etag/If-None-match(http1.1)
  • Last-Modified/If-Modified-since表示的是服務器的資源最後一次修改的時間;Etag/If-None-match表示的是服務器資源的惟一標識,只要資源變化,Etag就會從新生成。
  • Etag/If-None-match的優先級比Last-Modified/If-Modified-since高。

1)協商緩存-協商緩存-Last-Modified/If-Modified-since

  1. 服務器經過 Last-Modified 字段告知客戶端,資源最後一次被修改的時間,例如 Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  2. 瀏覽器將這個值和內容一塊兒記錄在緩存數據庫中。
  3. 下一次請求相同資源時時,瀏覽器從本身的緩存中找出「不肯定是否過時的」緩存。所以在請求頭中將上次的 Last-Modified 的值寫入到請求頭的 If-Modified-Since 字段
  4. 服務器會將 If-Modified-Since 的值與 Last-Modified 字段進行對比。若是相等,則表示未修改,響應 304;反之,則表示修改了,響應 200 狀態碼,並返回數據。

優點特色:

  • 不存在版本問題,每次請求都會去服務器進行校驗。服務器對比最後修改時間若是相同則返回304,不一樣返回200以及資源內容。

劣勢問題:

  1. 只要資源修改,不管內容是否發生實質性的變化,都會將該資源返回客戶端。例如週期性重寫,這種狀況下該資源包含的數據實際上同樣的。
  2. 以時刻做爲標識,沒法識別一秒內進行屢次修改的狀況。 若是資源更新的速度是秒如下單位,那麼該緩存是不能被使用的,由於它的時間單位最低是秒。
  3. 某些服務器不能精確的獲得文件的最後修改時間。
  4. 若是文件是經過服務器動態生成的,那麼該方法的更新時間永遠是生成的時間,儘管文件可能沒有變化,因此起不到緩存的做用。

2)協商緩存-Etag/If-None-match

  • 爲了解決上述問題,出現了一組新的字段 EtagIf-None-Match
  • Etag 存儲的是文件的特殊標識(通常都是 hash 生成的),服務器存儲着文件的 Etag 字段。以後的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新時間改變成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 變成了 If-None-Match。服務器一樣進行比較,命中返回 304, 不命中返回新資源和 200。
  • 瀏覽器在發起請求時,服務器返回在Response header中返回請求資源的惟一標識。在下一次請求時,會將上一次返回的Etag值賦值給If-No-Matched並添加在Request Header中。服務器將瀏覽器傳來的if-no-matched跟本身的本地的資源的ETag作對比,若是匹配,則返回304通知瀏覽器讀取本地緩存,不然返回200和更新後的資源。
  • Etag 的優先級高於 Last-Modified

優點特色:

  • 能夠更加精確的判斷資源是否被修改,能夠識別一秒內屢次修改的狀況。
  • 不存在版本問題,每次請求都回去服務器進行校驗。

劣勢問題:

  • 計算ETag值須要性能損耗。
  • 分佈式服務器存儲的狀況下,計算ETag的算法若是不同,會致使瀏覽器從一臺服務器上得到頁面內容後到另一臺服務器上進行驗證時現ETag不匹配的狀況。

2、瀏覽器出現 from disk、from memory 的策略

強緩存:服務器通知瀏覽器一個緩存時間,在緩存時間內,下次請求,直接用緩存,不在時間內,執行其餘緩存策略

  1. 瀏覽器發現緩存無數據,因而發送請求,向服務器獲取資源
  2. 服務器響應請求,返回資源,同時標記資源的有效期Cache-Contrl: max-age=3000
  3. 瀏覽器緩存資源,等待下次重用

7.商城的列表頁跳轉到商品的詳情頁,詳情頁數據接口很慢,前端能夠怎麼優化用戶體驗?

公司:愛範兒

分類:JavaScript

答案&解析

查看解析

1、優化簡要版

1)懶加載:獲取首屏數據,後邊的數據進行滑動加載請求

  1. 首先,不要將圖片地址放到src屬性中,而是放到其它屬性(data-original)中。
  2. 頁面加載完成後,根據scrollTop判斷圖片是否在用戶的視野內,若是在,則將data-original屬性中的值取出存放到src屬性中。
  3. 在滾動事件中重複判斷圖片是否進入視野,若是進入,則將data-original屬性中的值取出存放到src屬性中

2)利用骨架屏提高用戶體驗

3)PreloadJS預加載

使用PreloadJS庫,PreloadJS提供了一種預加載內容的一致方式,以便在HTML應用程序中使用。預加載可使用HTML標籤以及XHR來完成。默認狀況下,PreloadJS會嘗試使用XHR加載內容,由於它提供了對進度和完成事件的更好支持,可是因爲跨域問題,使用基於標記的加載可能更好。

4)除了添加前端loading和超時404頁面外,接口部分能夠添加接口緩存和接口的預加載

  1. 使用workbox對數據進行緩存 緩存優先
  2. 使用orm對本地離線數據進行緩存 優先請求本地。
  3. 採用預加載 再進入到詳情頁階段使用quicklink預加載詳情頁
  4. 使用nodejs做爲中間層將詳情頁數據緩存至redis等

上面的方法,能夠根據業務需求選擇組合使用。

2、優化詳細版

1.打開谷歌搜索爲例

load和DOMContentLoad.png

  • 藍色的分界線左邊表明瀏覽器的 DOMContentLoaded,當初始 HTML 文檔已徹底加載和解析而無需等待樣式表,圖像和子幀完成加載時的標識;
  • 紅色分界線表明 load, 當整個頁面及全部依賴資源如樣式表和圖片都已完成加載時

因此咱們能夠大體分爲在

  • TTFB 以前的優化
  • 瀏覽器上面渲染的優化

2.當網絡過慢時在獲取數據前的處理

首先先上一張經典到不能再經典的圖

timing-overview.png

其中cnd在dns階段, dom渲染在processing onload階段

上圖從 promot for unload 到 onload 的過程這麼多步驟, 在用戶體驗來講, 一個頁面從加載到展現超過 4 秒, 就會有一種很是直觀的卡頓現象, 其中 load 對應的位置是 onLoad 事件結束後, 纔開始構建 dom 樹, 可是用戶不必定是關心當前頁面是不是完成了資源的下載; 每每是一個頁面開始出現可見元素開始FCP 首次內容繪製或者是FC 首次繪製 此時用戶視覺體驗開始, 到TTI(可交互時間) , 可交互元素的出現, 意味着,用戶交互體驗開始, 這時候用戶就能夠愉快的瀏覽使用咱們的頁面啦;

因此這個問題的主要痛點是須要縮短到達 TTIFCP 的時間

可是這裏已知進入咱們詳情頁面時, 接口數據返回速度是很慢的, FCPFC , 以及加快到達 TTI , 就須要咱們頁面預處理了

3.頁面數據緩存處理(緩存大法好)

第一次 進入詳情頁面, 可使用骨架圖進行模擬 FC 展現, 而且骨架圖, 可以使用背景圖且行內樣式的方式對首次進入詳情頁面進行展現, 對於請求過慢的詳情接口使用 worker 進程, 對詳情的接口請求丟到另一個工做線程進行請求, 頁面渲染其餘已返回數據的元素; 當很慢的數據回來後, 須要對頁面根據商品 id 簽名爲 key 進行 webp 或者是縮略圖商品圖的 cnd 路徑 localStorage 的緩存, 商品 id 的簽名由放在 cookie 並設置成 httpOnly

非第一次 進入詳情頁時, 前端可經過特定的接口請求回來對應的商品 id 簽名的 cookieid, 讀取 localStorage 的商品圖片的緩存數據, 這樣對於第一次骨架圖的展現時間就能夠縮短, 快速到達 TTI 與用戶交互的時間, 再經過 worker 數據, 進行高清圖片的切換

4.過時緩存數據的處理(後端控制爲主, LRU 爲輔)

對於緩存圖片地址的處理, 雖然說緩存圖片是放在 localStorage 中, 不會用大小限制, 可是太多也是很差的, 這裏使用 LRU 算法對圖片以及其餘 localStorage 進行清除處理, 對於超過 7 天的數據進行清理 localStorage 詳情頁的數據, 數據結構以下:

"讀取後端的cookieID": {
  "path": "對應cdn圖片的地址",
  "time": "緩存時間戳",
  "size": "大小"
}
複製代碼

5.數據緩存和過時緩存數據的處理主體流程

進入商品詳情頁,接口數據很慢時,對頁面的優化

6.對於大請求量的請求(如詳情頁面中的猜你喜歡, 推薦商品等一些大數據量的靜態資源)

  1. 因爲這些不屬於用戶進入詳情想第一時間獲取的信息, 即不屬於當前頁面的目標主體, 因此這些可使用 Intersection Observer API 進行主體元素的觀察, 噹噹前主體元素被加載出來後, 在進行非主體元素的網絡資源分配, 即網絡空閒時再請求猜你喜歡, 推薦商品等資源, 處理請求優先級的問題
  2. 須要保證當前詳情頁的請求列表的請求數 不超過當前瀏覽器的請求一個 tcp 最大 http 請求數

7.當 worker 數據回來後, 出現 大量圖片 替換對應元素的的 webp 或者縮略圖出現的問題(靜態資源過多)

這裏有兩種情景

  1. 移動端, 對於移動端, 通常不會出現大量圖片, 通常一個商品詳情頁, 不會超過 100 張圖片資源; 這時候, 選擇懶加載方案; 根據 GitHub 現有的不少方案, 當前滑動到可被觀察的元素後才加載當前可視區域的圖片資源, 一樣使用的是 Intersection Observer API ; 好比 vue 的一個庫 vue-lazy , 這個庫就是對 Intersection_Observer_API 進行封裝, 對可視區域的 img 便籤進行 data-src 和 src 屬性替換

  2. 第二個狀況, pc 端, 可能會出現大量的 img 標籤, 可能多達 300~400 張, 這時候, 使用懶加載, 用戶體驗就不太好了; 好比說: 當用戶在查看商品說明介紹時, 這些商品說明和介紹有可能只是一張張圖片, 當用戶很快速的滑動時, 頁面還沒懶加載完, 用戶就有可能看不到想看的信息; 鑑於會出現這種狀況, 這裏給出一個方案就是, img 出現一張 load 一張; 實現以下:

// 這裏針對非第一次進入詳情頁,
//當前localStorage已經有了當前詳情頁商品圖片的縮略圖
for(let i = 0; i < worker.img.length; i++) {
  // nodeList是對應img標籤,
  // 注意, 這裏對應的nodeList必定要使用內聯style把位置大小設置好, 避免大量的重繪重排
  const img = nodeList[i]
  img.src = worker.img['path'];
  img.onerror = () => {
    // 將替換失敗或者加載失敗的圖片降級到縮略圖, 
    // 即緩存到localStorage的縮略圖或者webp圖
    // 兼容客戶端處理webp失敗的狀況
  }
}
複製代碼

8.頁面重繪重排處理

頁面渲染流程

觸發重排的操做主要是幾何因素:

  1. 頁面首次進入的渲染。
  2. 瀏覽器 resize
  3. 元素位置和尺寸發生改變的時候
  4. 可見元素的增刪
  5. 內容發生改變
  6. 字體的 font 的改變。
  7. css 僞類激活。 .....

儘可能減小上面這些產生重繪重排的操做

好比說:

這裏產生很大的重繪重排主要發生在 worker 回來的數據替換頁面中的圖片 src 這一步

// 該節點爲img標籤的父節點
const imgParent = docucment.getElementById('imgParent'); 
// 克隆當前須要替換img標籤的父元素下全部的標籤
const newImgParent = imgParent.cloneNode(true); 
const imgParentParent = docucment.getElementById('imgParentParent');
for(let i = 0; i < newImgParent.children.length; i++) { 
// 批量獲取完全部img標籤後, 再進行重繪
  newImgParent.children[i].src = worker.img[i].path;
}
// 經過img父節點的父節點, 來替換整個img父節點
// 包括對應的全部子節點, 只進行一次重繪操做
imgParentParent.replaceChild(newImgParent, imgParent); 
複製代碼

9.css代碼處理

注意被阻塞的css資源

衆所周知, css的加載會阻塞瀏覽器其餘資源的加載, 直至CSSOM CSS OBJECT MODEL 構建完成, 而後再掛在DOM樹上, 瀏覽器依次使用渲染樹來佈局和繪製網頁。

不少人都下意識的知道, 將css文件一概放到head標籤中是比較好的, 可是爲何將css放在head標籤是最後了呢?

咱們用淘寶作例子

沒有加載css的淘寶頁面 好比這種沒有css樣式的頁面稱之爲FOUC(內容樣式短暫失效), 可是這種狀況通常出如今ie系列以及前期的瀏覽器身上; 就是當cssom在domtree生成後, 依然還沒完成加載出來, 先展現純html代碼的頁面一會再出現正確的帶css樣式的頁面;

減小不一樣頁面的css代碼加載

對於電商頁面, 有些在頭部的css代碼有些是首頁展現的有些是特定狀況才展現的, 好比當咱們須要減小一些css文件大小可是當前網站又須要多屏展現, 這時候, 不少人都會想到是媒體查詢, 沒錯方向是對的, 可是怎樣的媒體查詢纔對css文件保持足夠的小呢, 可使用link標籤媒體查詢,看下邊的的例子:

<link href="base.css" rel="stylesheet">
<link href="other.css" rel="stylesheet" media="(min-width: 750px)">
複製代碼

第一個css資源表示全部頁面都會加載, 第二個css資源, 寬度在750px纔會加載, 默認media="all"

在一些需求寫css媒體查詢的網站, 不要在css代碼裏面寫, 最好寫兩套css代碼, 經過link媒體查詢去動態加載, 這樣就能很好的減輕網站加載css文件的壓力

10.靜態js代碼處理

這種js代碼, 是那些關於埋點, 本地日記, 以及動態修改css代碼, 讀取頁面成型後的信息的一些js代碼, 這種一概放在同域下的localStorage上面, 什麼是同域下的localStorage

這裏仍是以天貓爲例

11.容錯處理

  1. 頁面在獲取到 worker 回來的數據後, 經過拷貝整個html片斷, 再將worker的img路徑在替換對應的 img 資源後再進行追加到對應的dom節點
  2. 緩存 css 文件和 js 文件到 localStorage 中, 若當前沒有對應的 css 文件或者 js 文件, 或者被惡意修改過的 css 文件或者 js 文件(可以使用簽名進行判斷), 刪除再獲取對應文件的更新

12.推薦方案理由

  1. 使用了 worker 線程請求詳情數據, 不佔用瀏覽器主線程; 進而減小主進程消耗在網絡的時間
  2. 使用 localStorage 的緩存機制, 由於當 worker 回來的數據後, 讀取 localStorage 是同步讀取的, 基本不會有太大的等待時間, 而且讀取 localStorage 時, 使用的是後端返回來的 cookieID 進行讀取, 且本地的 cookID 是 httpOnly 避免了第三方獲取到 cookieID 進行讀取商品信息
  3. 使用 LRU 清除過多的緩存數據
  4. 首次進入頁面時, 保證已知頁面佈局狀況下的快速渲染以及配置骨架圖, 加快到達 FCP 和 FP 的時間
  5. 就算 img 靜態資源過大, 在第二次進入該頁面的時候, 也能夠作到低次數重繪重排, 加快到底 TTI 的時間

13.方案不足

  1. 在網絡依然很慢的狀況下, 首次進入詳情頁面, 若是長時間的骨架圖和已知佈局下, 用戶的體驗依然是很差的, 這裏能夠考慮 PWA 方案, 對最近一次成功請求的內容進行劫持, 並在無網狀況下, 作出相應的提示和展現處理
  2. 須要 UI 那邊提供三套靜態 img 資源

8.說一下單點登陸實現原理

公司:CVTE

分類:JavaScript

答案&解析

查看解析

1、什麼是單點登陸

單點登陸SSO(Single Sign On),是一個多系統共存的環境下,用戶在一處登陸後,就不用在其餘系統中登陸,也就是用戶的一次登陸獲得其餘全部系統的信任

好比現有業務系統A、B、C以及SSO系統,第一次訪問A系統時,發現沒有登陸,引導用戶到SSO系統登陸,根據用戶的登陸信息,生成惟一的一個憑據token,返回給用戶。後期用戶訪問B、C系統的時候,攜帶上對應的憑證到SSO系統去校驗,校驗經過後,就能夠單點登陸;

單點登陸在大型網站中使用的很是頻繁,例如,阿里旗下有淘寶、天貓、支付寶等網站,其背後的成百上千的子系統,用戶操做一次或者交易可能涉及到不少子系統,每一個子系統都須要驗證,因此提出,用戶登陸一次就能夠訪問相互信任的應用系統

單點登陸有一個獨立的認證中心,只有認證中心才能接受用戶的用戶名和密碼等信息進行認證,其餘系統不提供登陸入口,只接受認證中心的間接受權。間接受權經過令牌實現,當用戶提供的用戶名和密碼經過認證中心認證後,認證中心會建立受權令牌,在接下來的跳轉過程當中,受權令牌做爲參數發送給各個子系統,子系統拿到令牌即獲得了受權,而後建立局部會話。

2、單點登陸原理

單點登陸有同域和跨域兩種場景

1)同域

適用場景:都是企業本身的系統,全部系統都使用同一個一級域名經過不一樣的二級域名來區分。

舉個例子:公司有一個一級域名爲 zlt.com ,咱們有三個系統分別是:門戶系統(sso.zlt.com)、應用1(app1.zlt.com)和應用2(app2.zlt.com),須要實現系統之間的單點登陸,實現架構以下

核心原理:

  1. 門戶系統設置的cookie的domain爲一級域名也是zlt.com,這樣就能夠共享門戶的cookie給全部的使用該域名xxx.alt.com的系統
  2. 使用Spring Session等技術讓全部系統共享Session
  3. 這樣只要門戶系統登陸以後不管跳轉應用1或者應用2,都能經過門戶Cookie中的sessionId讀取到Session中的登陸信息實現單點登陸

2)跨域

單點登陸之間的系統域名不同,例如第三方系統。因爲域名不同不能共享Cookie了,須要的一個獨立的受權系統,即一個獨立的認證中心(passport),子系統的登陸都可以經過passport,子系統自己將不參與登陸操做,當一個系統登陸成功後,passprot將會頒發一個令牌給子系統,子系統能夠拿着令牌去獲取各自的保護資源,爲了減小頻繁認證,各個子系統在被passport受權之後,會創建一個局部會話,在必定時間內無需再次向passport發起認證

基本原理

  1. 用戶第一次訪問應用系統的時候,由於沒有登陸,會被引導到認證系統中進行登陸;
  2. 根據用戶提供的登陸信息,認證系統進行身份校驗,若是經過,返回給用戶一個認證憑據-令牌
  3. 用戶再次訪問別的應用的時候,帶上令牌做爲認證憑證
  4. 應用系統接收到請求後會把令牌送到認證服務器進行校驗,若是經過,用戶就能夠在不用登陸的狀況下訪問其餘信任的業務服務器。

登陸流程

登陸流程

  1. 用戶訪問系統1的受保護資源,系統1發現用戶沒有登陸,跳轉到sso認證中心,並將本身的地址做爲參數
  2. sso認證中心發現用戶未登陸,將用戶引導到登陸頁面
  3. 用戶提交用戶名、密碼進行登陸
  4. sso認證中心校驗用戶信息,建立用戶與sso認證中心之間的會話,稱之爲全局會話,同時建立受權令牌
  5. sso 帶着令牌跳轉回最初的請求的地址(系統1)
  6. 系統1拿着令牌,去sso認證中心校驗令牌是否有效
  7. sso認證中心校驗令牌,返回有效,註冊系統1(也就是返回一個cookie)
  8. 系統一使用該令牌建立與用戶的會話,成爲局部會話,返回受保護的資源
  9. 用戶訪問系統2受保護的資源
  10. 系統2發現用戶未登陸,跳轉至sso認證中心,並將本身的地址做爲參數
  11. sso認證中心發現用戶已登陸,跳轉回系統2的地址,而且附上令牌
  12. 系統2拿到令牌,去sso中心驗證令牌是否有效,返回有效,註冊系統2
  13. 系統2使用該令牌建立與用戶的局部會話,返回受保護資源
  14. 用戶登陸成功以後,會與sso認證中心以及各個子系統創建會話,用戶與sso認證中心創建的會話稱之爲全局會話,用戶與各個子系統創建的會話稱之爲局部會話,局部會話創建以後,用戶訪問子系統受保護資源將再也不經過sso認證中心

註銷流程

註銷流程

  1. 用戶向系統提交註銷操做
  2. 系統根據用戶與系統1創建的會話,拿到令牌,向sso認證中心提交註銷操做
  3. sso認證中心校驗令牌有效,銷燬全局會話,同時取出全部用此令牌註冊的系統地址
  4. sso認證中心向全部註冊系統發起註銷請求,各註冊系統銷燬局部會話
  5. sso認證中心引導用戶到登陸頁面

9.說一下減小 dom 數量的辦法?一次性給你大量的 dom 怎麼優化?

公司:58

分類:Html

答案&解析

查看解析

1、減小DOM數量的方法

  1. 可使用僞元素,陰影實現的內容儘可能不使用DOM實現,如清除浮動、樣式實現等;
  2. 按需加載,減小沒必要要的渲染;
  3. 結構合理,語義化標籤;

2、大量DOM時的優化

當對Dom元素進行一系列操做時,對Dom進行訪問和修改Dom引發的重繪和重排都比較消耗性能,因此關於操做Dom,應該從如下幾點出發:

1.緩存Dom對象

首先無論在什麼場景下。操做Dom通常首先會去訪問Dom,尤爲是像循環遍歷這種時間複雜度可能會比較高的操做。那麼能夠在循環以前就將主節點,沒必要循環的Dom節點先獲取到,那麼在循環裏就能夠直接引用,而沒必要去從新查詢。

let rootElem = document.querySelector('#app');
let childList = rootElem.child; // 假設全是dom節點
for(let i = 0;i<childList.len;j++){
    /** * 根據條件對應操做 */
}
複製代碼

2.文檔片斷

利用document.createDocumentFragment()方法建立文檔碎片節點,建立的是一個虛擬的節點對象。向這個節點添加dom節點,修改dom節點並不會影響到真實的dom結構。

咱們能夠利用這一點先將咱們須要修改的dom一併修改完,保存至文檔碎片中,而後用文檔碎片一次性的替換真是的dom節點。與虛擬dom相似,一樣達到了不頻繁修改dom而致使的重排跟重繪的過程。

let fragment = document.createDocumentFragment();
const operationDomHandle = (fragment) =>{
    // 操做 
}
operationDomHandle(fragment);
// 而後最後再替換 
rootElem.replaceChild(fragment,oldDom);
複製代碼

這樣就只會觸發一次迴流,效率會獲得很大的提高。若是須要對元素進行復雜的操做(刪減、添加子節點),那麼咱們應當先將元素從頁面中移除,而後再對其進行操做,或者將其複製一個(cloneNode()),在內存中進行操做後再替換原來的節點。

var clone=old.cloneNode(true);
operationDomHandle(clone);
rootElem.replaceChild(clone,oldDom)
複製代碼

3.用innerHtml 代替高頻的appendChild

4.最優的layout方案

批量讀,一次性寫。先對一個不在render tree上的節點進行操做,再把這個節點添加回render tree。這樣只會觸發一次DOM操做。 使用requestAnimationFrame(),把任何致使重繪的操做放入requestAnimationFrame

5.虛擬Dom

js模擬DOM樹並對DOM樹操做的一種技術。virtual DOM是一個純js對象(字符串對象),因此對他操做會高效。

利用virtual dom,將dom抽象爲虛擬dom,在dom發生變化的時候先對虛擬dom進行操做,經過dom diff算法將虛擬dom和原虛擬dom的結構作對比,最終批量的去修改真實的dom結構,儘量的避免了頻繁修改dom而致使的頻繁的重排和重繪。


10.按要求實現代碼

/* 根據傳入參數n(數字)對一維數組(純數字)按照距離n最近的順序排序。 (距離即數字與n的差值的絕對值) */ 
var arr = [7, 28, -1, 0, 7, 33]; 
function sort(n) { 
  //your code
}
複製代碼

公司:高思教育

分類:算法

答案&解析

查看解析

代碼實現

  • 實現一:排序方法的靈活應用
var arr = [7, 28, -28, 0, 7, 33];
function sort(n) {
    arr.sort((a, b) => {
        return Math.abs(a - n) - Math.abs(b - n);
    })
    console.log(arr);
}
sort(28);
複製代碼
  • 實現二:基於差距進行歸併排序O(nlogn)
var nearbySort = function (n, arr) {
  var splitedArr = [];
  for (var i = 0; i < arr.length; i++) {
    splitedArr.push([arr[i]]);
  }
  while(splitedArr.length > 1) { // 兩兩一組,依次歸併 ~ 遞歸就不用了~
    var half = Math.ceil(splitedArr.length / 2);
    for(var j = 0; j < half; j++) {
      splitedArr[j] = mergeArr(n, splitedArr[j], splitedArr[j + half]);
    }
    console.log(half, splitedArr);
    splitedArr.length = half;
  }
  return (splitedArr.length === 1) ? splitedArr[0] : [];
}
var getDistance = function (n, m){ // 獲取n 與 m數值差別(絕對值)
  return n > m ? (n - m) : (m - n);
}
var mergeArr = function(n, left, right){ // left right爲已排序數組
  if (!left) {
    return right;
  }
  if (!right) {
    return left;
  }
  var sortedArr = [], leftIndex = 0, rightIndex = 0, leftLen = left.length, rightLen = right.length;
  while(leftIndex < leftLen || rightIndex < rightLen){
    var leftNum = left[leftIndex], rightNum = right[rightIndex];
    if (leftNum === undefined) {
      sortedArr.push(rightNum);
      rightIndex++;
    } else if (rightNum === undefined) {
      sortedArr.push(leftNum);
      leftIndex++;
    } else {
      var leftDistance = getDistance(n, leftNum);
      var rightDistanc = getDistance(n, rightNum);
      if (leftDistance <= rightDistanc) {
        sortedArr.push(leftNum);
        leftIndex++;
      } else {
        sortedArr.push(rightNum);
        rightIndex++;
      }
    }
  }
  return sortedArr;
}
複製代碼
  • 實現三:差距必然只會是固定排序的正整數, 能夠建立散列表{0: [], 1: [], ....n: []}進行記錄進行最終合併: 複雜度O(n);
var getDistance = function (n, m){ // 獲取n 與 m數值差別(絕對值)
  return n > m ? (n - m) : (m - n);
}
var nearbySort = function (n, arr) {
  var distanceObj = {}, len = arr.length, distance, maxDistance = 0, result = [];
  for(var i = 0; i < len; i++){
    distance = getDistance(n, arr[i]);
    if(distance > maxDistance){
      maxDistance = distance;
    }
    if (distanceObj[distance]) {
      distanceObj[distance].push(arr[i]);
    } else {
      distanceObj[distance] = [arr[i]];
    }
  }
  for(var j = 0; j <= maxDistance; j++) { // 兩層for, 但內層循環數固定, 總循環固定O(n);
    if (distanceObj[j]) {
      var eachDistance = distanceObj[j];
      var eachLength = eachDistance.length;
      for(var m = 0; m < eachLength; m++) {
        result.push(eachDistance[m])
      }
    }
  }
  return result;
}
複製代碼

還沒看過癮?打開個人刷題小程序:前端面試星球,1000+道前端面試題(包含詳細解析&答案)等你解鎖。

相關文章
相關標籤/搜索