一次令人窒息的百度面試
作者:let_code
https://juejin.cn/post/7178783712363708475
最近接到了百度的面試,個人覺得基礎知識準備的比較充分,就去網上找了一些百度的面經,冥冥之中我在衆多的面試題中打開了下邊兩個面試題:2021 百度前端社招面經 [1],百度前端面試題分享,帶答案 [2]
看完之後我直呼 “哇哦~”,全部在我的射程範圍之內。我該不會如此幸運到問的全會吧。
是的,答案就是不會,我就是沒有幸運到問的全會。
話不多說,接下來就回顧下面試的問題。
看簡歷
上來首先是萬年不變的自我介紹,介紹完之後面試官就開始逐行看我的簡歷,並針對簡歷上的項目經歷進行詢問。詢問的十分詳細。
如何實現新手指引
問這個問題的原因是我簡歷上寫到了使用 driver.js 庫實現了新手指引。
使用 js 實現如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta >
<title>新手指引功能</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
box-sizing: border-box;
}
.stepBlock {
background-color: burlywood;
margin-right: 20px;
}
.positionStyle{
position: absolute;
z-index: 200;
}
/* 蒙層樣式 */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .5);
z-index: 100;
}
</style>
</head>
<body>
<section id="mask">
<section class="positionStyle" id="tip"></section>
<section class="positionStyle" id="curStepMask"></section>
</section>
<section style="margin:200px;">
<span id="first" class="stepBlock">
第一步
</span>
<span id="second" class="stepBlock">
第二步
</span>
<span id="third" class="stepBlock">
第三步
</span>
</section>
<section style="margin-top:30px">
<button onclick="setMask()">開始指引</button>
</section>
<script>
const tipDict = [
{ id: 'first', content: '這裏是第1步哦' },
{ id: 'second', content: '這裏是第2步哦' },
{ id: 'third', content: '這裏是最後一步哦,點擊完成按鈕結束新手指引' },
]
let flag = 0;
function setMask() {
// 添加蒙層
let mask = document.getElementById('mask')
mask.setAttribute('class', 'overlay')
setTip()
}
function removeMask() {
// 移除蒙層
let mask = document.getElementById('mask')
mask.setAttribute('class', '')
// 移除tip提示的子元素
removeTip()
removeStepMask()
}
function setTip() {
if (flag < tipDict.length) {
// 獲取當前步驟的元素,以及元素的位置信息,供後續定位提示信息和覆蓋信息使用
const curStepEle = document.getElementById(tipDict[flag].id)
const bound = curStepEle.getBoundingClientRect()
// 找到id爲tip的元素
let ele = document.getElementById("tip")
// 如果存在子元素,先移除
removeTip()
removeStepMask()
// 創建提示信息和下一步的統一父元素,方便後續移除元素
let node = document.createElement('div')
// 創建提示信息
let tipText = document.createTextNode(tipDict[flag].content)
// 將提示信息插入到父元素
node.appendChild(tipText)
// 創建“下一步”按鈕
let nextBtn = document.createElement('button')
nextBtn.innerHTML = flag === tipDict.length - 1 ? '完成' : '下一步';
nextBtn.onclick = setTip;
// 將按鈕插入到父元素
node.appendChild(nextBtn)
// 設置統一父元素的位置
ele.style.left = bound.x + 'px'
ele.style.top = bound.y + 20 + 'px'
// 將統一的父元素插入到id爲tip的元素
ele.appendChild(node)
// 將當前步驟高亮顯示
let tag = flag - 1
if (tag >= 0) {
document.getElementById(tipDict[tag].id).style = ''
}
// const curStepEle = document.getElementById(tipDict[flag].id)
// const bound = curStepEle.getBoundingClientRect()
const curStepMask = document.getElementById('curStepMask')
curStepMask.style.left = bound.x + 'px'
curStepMask.style.top = bound.y + 'px'
const curStepEleClone = curStepEle.cloneNode(true)
curStepMask.appendChild(curStepEleClone)
flag++
} else {
flag = 0;
removeMask()
}
}
function removeStepMask() {
let ele = document.getElementById('curStepMask')
let child = ele.lastElementChild
if (child) {
ele.removeChild(child)
}
}
function removeTip() {
let ele = document.getElementById("tip")
let child = ele.lastElementChild
if (child) {
ele.removeChild(child)
}
}
</script>
</body>
</html>
注意元素中包含如下結構:
<section id="mask">
<section class="positionStyle" id="tip"></section>
<section class="positionStyle" id="curStepMask"></section>
</section>
實現思路是:
-
點擊 “開始指引”:找到 id 爲 mask 的元素,爲該元素添加蒙層樣式(setMask)
-
添加提示信息:找到 id 爲 tip 的元素,將提示信息添加爲該元素的子元素 (setTip)
-
高亮當前步驟元素:找到當前目標元素,克隆目標元素,然後將克隆後的目標元素添加爲 curStepMask 的子元素 (setTip)
-
定位 tip 和 curStepMask 的元素:curStepMask 元素在當前目標元素的正上方,tip 元素根據情況而定
-
每次添加當前提示信息時要移除上一次添加的提示信息和覆蓋元素(removeTip,removeStepMask)
getBoundingClientRect
返回值是一個 DOMRect[3] 對象,是包含整個元素的最小矩形(包括
padding
和border-width
)。該對象使用left
、top
、right
、bottom
、x
、y
、width
和height
這幾個以像素爲單位的只讀屬性描述整個矩形的位置和大小。除了width
和height
以外的屬性是相對於視圖窗口的左上角來計算的。 --MDN
圖片上有一個人臉,除了臉部以外加上蒙層
方案一:
添加遮罩層,在圖片上方添加一張只有人臉的圖片:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta >
<title>圖片添加蒙層</title>
<style type="text/css">
img {
position: absolute;
top: 50%;
left: 50%;
width: 300px;
}
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, .5);
z-index: 100;
}
</style>
</head>
<body>
<div class="overlay">
<img src="../images/mask.png" style="width:200px" />
</div>
<img src="../images/cat.png" />
</body>
</html>
最終實現效果:(沒有用一模一樣的圖片,只是模擬了類似的效果)
上述是在整個頁面添加蒙層,若想只在圖片部分添加蒙層:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta >
<title>圖片添加蒙層</title>
<style type="text/css">
img {
width: 300px;
}
.overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, .5);
z-index: 100;
}
</style>
</head>
<body>
<div style="position: relative;width: 300px;">
<div class="overlay">
<img src="../images/kid.png" style="width:200px" />
</div>
<img src="../images/cat.png" />
</div>
</body>
</html>
效果:
echarts 動畫實現原理
對 Echarts 目前處於使用的水平,原理後續學習的話再補上吧~~~
瞭解 canvas 嗎
HTML5 <canvas>
元素用於圖形的繪製,通過腳本 (通常是 JavaScript) 來完成。
<canvas>
標籤只是圖形容器,必須使用腳本來繪製圖形。
可以通過多種方法使用 canvas 繪製路徑, 盒、圓、字符以及添加圖像。
canvas 教程傳送門 [4]
如何實現組件滑動切換效果
不使用 react-transition-group
對語義化的理解
語義化就是正確的標籤做正確的事。語義化的好處在於:
-
對於開發團隊而言,代碼更加容易維護
-
在 css 沒有加載出來的情況下也能很好的展示結構
-
有利於 SEO 優化
-
更好地支持各種終端,例如無障礙閱讀和有聲小說等
HTML5 有哪些語義化標籤
常用的語義化標籤有:
-
header:定義頁眉信息
-
nav:導航欄
-
section:頁面的組成部分
-
footer:腳註信息
-
aside:側邊欄信息,比如菜單或者廣告等
less 多處用到 px 轉換爲 vw 如何實現
sass 中可以定義函數,接收參數並且返回計算值:
/*比如:在父元素字體大小爲 12px 的容器內繪製圖形交互*/
@function pxToEm ($px) {
@return ($px/12) + em;
}
# Sass
.box {
width: pxToEm(36);
}
# CSS
.box {
width: 3em;
}
複製代碼
less 中函數是內置的不能夠自定義,所以可以使用混入:
/*
將寬度爲 375px 的 UI 設計稿轉換成使用單位 vw 來適配移動端的網頁。
避免編譯:~' 值 '
*/
.pxToVW (@px, @attr: width) {
@vw: (@px / 375) * 100;
@{attr}: ~"@{vw}vw";
}
# Less
.box {
.pxToVW(75);
.pxToVW(150, height);
}
# CSS
.box {
width: 20vw;
height: 40vw;
}
less 傳送門 [5]
vue-router 中 router 和 route 的區別
router 是路由實例對象,包含一些路由跳轉方法,比如 push。
route 是路由信息對象,包含和路由相關的一些信息,比如 params,location 等。
vue 單頁面應用無刷新更新組件怎麼實現的
我理解面試官詢問的點在於 vue-router 兩種模式下如何實現的 url 到組件的映射。
hash 模式
hash 模式是 vue-router 的默認模式。hash 指的是 url 描點,當描點發生變化的時候,瀏覽器只會修改訪問歷史記錄,不會訪問服務器重新獲取頁面。因此可以監聽描點值的變化,根據描點值渲染指定 dom。
- 改變描點
可以通過location.hash = "/hashpath"
的方式修改瀏覽器的 hash 值。
- 監聽描點變化
可以通過監聽 hashchange 事件監聽 hash 值的變化。
window.addEventListener('hashchange', () => {
const hash = window.location.hash.substr(1)
// 根據hash值渲染不同的dom
})
history 模式
通過 mode 選項開啓 history 模式,history 模式和 hash 模式的區別在於:
-
history 模式中不帶有 “#”,更加美觀
-
history 模式當用戶刷新或直接輸入地址時會向服務器發送一個請求,所以 history 模式需要服務端同學進行支持,將路由都重定向到根路由
- 改變 url
H5 的 history 對象提供了 pushState 和 replaceState 兩個方法,當調用這兩個方法的時候,url 會發生變化,瀏覽器訪問歷史也會發生變化,但是瀏覽器不會向後臺發送請求。
// 第一個參數:data對象,在監聽變化的事件中能夠獲取到
// 第二個參數:title標題
// 第三個參數:跳轉地址
history.pushState({}, "", '/a')
- 監聽 url 變化
可以通過監聽 popstate 事件監聽 history 變化,也就是點擊瀏覽器的前進或者後退功能時觸發。
window.addEventListener("popstate", () => {
const path = window.location.pathname
// 根據path不同可渲染不同的dom
})
從某種程度來說,調用
pushState()
和window.location = "#foo"
基本上一樣,他們都會在當前的 document 中創建和激活一個新的歷史記錄。但是pushState()
有以下優勢:
新的 URL 可以是任何和當前 URL 同源的 URL。但是設置 window.location[6] 只會在你只設置錨的時候纔會使當前的 URL。
非強制修改 URL。相反,設置
window.location = "#foo";
僅僅會在錨的值不是 #foo 情況下創建一條新的歷史記錄。可以在新的歷史記錄中關聯任何數據。
window.location = "#foo"
形式的操作,你只可以將所需數據寫入錨的字符串中。注意:
pushState()
不會造成 hashchange[7] 事件調用,即使新的 URL 和之前的 URL 只是錨的數據不同。 ----MDN
vue 在頁面中如何監聽回到上一步的操作
掛載完成後,判斷瀏覽器是否支持 popstate
mounted(){
if (window.history && window.history.pushState) {
history.pushState(null, null, document.URL);
window.addEventListener('popstate', this.goBack, false);
}
},
頁面銷燬時,取消監聽。否則其他 vue 路由頁面也會被監聽
destroyed(){
window.removeEventListener('popstate', this.goBack, false);
},
頁面跳轉函數
methods:{
goBack(){
this.$router.replace({path: '/'});
//replace替換原路由,作用是避免回退死循環
}
}
代碼題:迴文字符串
function checkStr(str) {
return str === str.split('').reverse().join('')
}
場景提:一個公告欄,每一天都可以展示,當用戶點擊關閉後今天不顯示,明天(過了今天零點)還會顯示
我給出的方案就是在 localStorage 中存儲用戶關閉公告欄的時間戳,等再次進入頁面的時候判斷是不是存在 localStorage:
-
若不存在則證明從來沒有關閉過公告欄,那就顯示;
-
若存在,就判斷時間戳和當前時間是否是同一天,不是同一天就顯示
代碼題:命名方式中劃線改小駝峯
方案一:
function transName(arr) {
let res = arr.map(e => {
let items = e.split('-').map((item, index) => {
if (index) {
let first = item.substring(0,1)
let rest = item.substring(1)
return first.toUpperCase()+rest
}else{
return item.toLowerCase()
}
})
return items.join('')
})
return res
}
console.log(transName(['A-b-cee', 'ca-de-ea', 'e-fe-eaa','f-g','mn']))
方案二:
function turnName(str){
return str.replace(/-[a-zA-Z]/g,match=>match.replace('-','').toUpperCase())
}
代碼題:命名方式小駝峯改中劃線
let s1 = 'aBBcdE';
let t = s1.replace( /[A-Z]/g, match=>'-'+match.toLowerCase());
console.log(t);
git commit 之後修改上一次 commit 的信息
剛 commit 還沒有 push
git commit --amend
會進入 vim 編輯器,點擊 i,修改 commit 信息後,點擊 esc,輸入 ZZ 退出。
git log 可以看見最近 commit 信息
剛 push,修改最近一次 commit
git commit --amend
會進入 vim 編輯器,點擊 i,修改 commit 信息後,點擊 esc,輸入 ZZ 退出。
git log 可以看見最近 commit 信息,pull 後再 push 到遠程 (但是每次 pull 後再 push 會導致覆蓋原來的更改,後來直接強制推送成功了:git push origin HEAD:master --force)
修改歷史 push 的 commit 信息
git rebase -i HEAD~3
表示要修改當前版本的倒數第三次狀態.
這個命令出來之後,會出來三行東東:
pick:*******
pick:*******
pick:*******
如果你要修改哪個,就把那行的 pick 改成 edit,然後保存退出 (點擊 esc,輸入 ZZ 退出)
這時通過 git log 你可以發現,git 的最後一次提交已經變成你選的那個了,這時再使用:
git commit --amend 來對 commit 進行修改。
修改完成後使用 git rebase --continue
然後將變化 push 到遠程:git push origin HEAD:master --force
webpack 優化構建速度
前端性能優化
頁面渲染優化
Webkit 渲染引擎流程:
-
處理 HTML 並構建 DOM 樹
-
處理 CSS 構建 CSS 規則樹 (CSSOM)
-
DOM Tree 和 CSSOM Tree 合成一棵渲染樹 Render Tree。
-
根據渲染樹來佈局,計算每個節點的位置
-
調用 GPU 繪製,合成圖層,顯示在屏幕上
-
避免 css 阻塞:css 影響 renderTree 的構建,會阻塞頁面的渲染,因此應該儘早(將 CSS 放在 head 標籤裏)和儘快(啓用 CDN 實現靜態資源加載速度的優化) 的將 css 資源加載
-
避免 js 阻塞:js 可以修改 CSSOM 和 DOM,因此 js 會阻塞頁面的解析和渲染,並且會等待 css 資源的加載。也就是說 js 會搶走渲染引擎的控制權。所以我們需要給 js 資源添加 defer 或者 async,延遲 js 腳本的執行。
-
使用字體圖標 iconfont 代替圖片圖標:
-
圖片會增加網絡請求次數,從而拖慢頁面加載時間
-
iconfont 可以很好的縮放並且不會添加額外的請求
- 降低 css 選擇器的複雜度:瀏覽器讀取選擇器,遵循的原則是從選擇器的右邊到左邊讀取。
-
減少嵌套:最多不要超過三層,並且後代選擇器的開銷較高,慎重使用
-
避免使用通配符,對用到的元素進行匹配即可
-
利用繼承,避免重複匹配和定義
-
正確使用類選擇器和 id 選擇器
-
減少重繪和迴流:
CSS
JavaScript
-
避免頻繁操作樣式,最好一次性重寫 style 屬性,或者將樣式列表定義爲 class 並一次性更改 class 屬性。
-
避免頻繁操作 DOM,創建一個 documentFragment,在它上面應用所有 DOM 操作,最後再把它添加到文檔中。
-
爲元素設置 display: none,操作結束後再把它顯示出來。因爲在 display 屬性爲 none 的元素上進行的 DOM 操作不會引發迴流和重繪。用一次迴流替代多次迴流
-
避免頻繁讀取會引發迴流 / 重繪的屬性,如果確實需要多次使用,就用一個變量緩存起來。
-
對具有複雜動畫的元素生成一個新圖層
-
避免使用 table 佈局。
-
儘可能在 DOM 樹的最末端改變 class。
-
避免設置多層內聯樣式。
-
將動畫效果應用到 position 屬性爲 absolute 或 fixed 的元素上。
-
避免使用 CSS 表達式(例如:calc())。
JS 中的性能優化
-
使用事件委託
-
防抖和節流
-
儘量不要使用 JS 動畫 [8],css3 動畫 [9] 和 canvas 動畫 [10] 都比 JS 動畫性能好
圖片的優化
-
雪碧圖:藉助減少 http 請求次數來進行優化
-
圖片懶加載:在圖片即將進入可視區域的時候進行加載(後邊有判斷即將進入可視區域的方法)
-
使用 CSS3 代替圖片:有很多圖片使用 CSS 效果(漸變、陰影等)就能畫出來,這種情況選擇 CSS3 效果更好
webpack 優化
-
代碼壓縮:html,css,js 文件壓縮
-
Tree shaking 去除死代碼
-
babel-plugin-transform-runtime
減少 ES6 轉化 ES5 的冗餘 -
提升打包速度
vue
-
路由懶加載
-
合理使用 computed 和 watch
-
v-for 添加 key
-
v-for 的同時避免使用 v-if
-
destory 時銷燬事件:比如 addEventListener 添加的事件、setTimeout、setInterval、bus.$on 綁定的監聽事件等
-
第三方插件按需引入
react
-
map 循環展示添加 key
-
路由懶加載
-
使用 scu,memo 或者 pureComponent 避免不必要的渲染
前端的緩存機制
分爲強緩存和協商緩存。
強緩存不需要客戶端向服務端發送請求,有兩種響應頭實現方案:
-
Expires:值是一個絕時間,在這個時間前緩存有效,但是如果本地時間被修改,會導致緩存失效
-
Cache-control:值是一個相對時間,單位爲秒,資源在這個時間內有效
強緩存過期之後會使用協商緩存,協商緩存需要客戶端向服務端發送請求,資源未過期則服務端返回 304 否則返回新的資源。 協商緩存也有兩種實現方案:
-
Last-Modified 和 If-Modified-Since:
Last-Modified
表示本地文件最後修改日期,If-Modified-Since
會將Last-Modified
的值發送給服務器,詢問服務器在該日期後資源是否有更新,有更新的話就會將新的資源發送回來。但是如果本地文件被打開,會導致Last-Modified
被修改。 -
ETag 和 If-None-Match:
ETag
類似於文件指紋,If-None-Match
會將當前ETag
發送給服務器,詢問該資源ETag
是否變動,有變動的話就將新的資源發送回來。並且ETag
優先級比Last-Modified
高。
如何判斷圖片即將進入可視區域
方案 1:clientHeight+scroolTop>offsetTop
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta >
<title>圖片加載優化</title>
</head>
<body>
<div style="background-color: green;width:100vw;height:8000px">
</div>
<div id="yellow" style="background-color: yellow;width:100vw;height:800px">
</div>
<script>
document.addEventListener('scroll', () => {
const clientH = document.documentElement.clientHeight//獲取屏幕可視區域的高度
const scrollT = document.documentElement.scrollTop//獲取瀏覽器窗口頂部與文檔頂部之間的距離,也就是滾動條滾動的距離
const offsetTop = document.getElementById('yellow').offsetTop//獲取元素相對於文檔頂部的高度
if (clientH + scrollT > offsetTop) {
console.log('進入可視區域啦!!')
}
})
</script>
</body>
</html>
方案 2:下滑過程中 bound.top 會越來越小
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta >
<title>圖片加載優化</title>
</head>
<body>
<div style="background-color: green;width:100vw;height:8000px">
</div>
<div id="yellow" style="background-color: yellow;width:100vw;height:800px">
</div>
<script>
document.addEventListener('scroll', () => {
var bound = document.getElementById('yellow').getBoundingClientRect(); ////獲取元素的大小及位置
var clientHeight = window.innerHeight;
if (bound.top <= clientHeight) {
console.log('進入可視區域啦')
}
})
</script>
</body>
</html>
參考資料
[1]
2021 百度前端社招面經: https://juejin.cn/post/7002233276861513758
[2]
百度前端面試題分享,帶答案: https://juejin.cn/post/6970867290480853006
[3]
DOMRect: https://developer.mozilla.org/zh-CN/docs/Web/API/DOMRect
[4]
canvas 教程傳送門: https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial
[5]
less 傳送門: https://juejin.cn/post/6844903520441729037
[6]
window.location: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/location
[7]
hashchange: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/hashchange_event
[8]
JS 動畫: https://zh.javascript.info/js-animation
[9]
css3 動畫: https://www.runoob.com/css3/css3-animations.html
[10]
canvas 動畫: https://juejin.cn/post/7008811592733655077
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/m-wBQ7Ef_oy4SRluFx5O8A