最新整理的25道前端面試真題(含答案與解析)

Hi~ 很久不見。javascript

上一次整理的100道前端面試題在掘金火了以後,不少小夥伴反饋看答案不方便,其實主要是由於我整理的答案和解析內容很是全面,致使每一道題的篇幅都很長,閱讀體驗不太好,因此纔給你們把答案放到github上。css

最近解鎖了掘金的新功能——摺疊內容,我將在這篇文章中儘量多的的放置答案和解析。html

1.如何獲取 html 元素實際的樣式值?

公司:京東前端

分類:JavaScriptvue

查看解析

代碼實現

實際的樣式值 能夠理解爲 瀏覽器的計算樣式java

style 對象中包含支持 style 屬性的元素爲這個屬性設置的樣式信息,但不包含從其餘樣式表層疊繼承的一樣影響該元素的樣式信息。node

DOM2 Style 在 document.defaultView 上增長了 getComputedStyle() 方法。這個方法接收兩個參數:要取得計算樣式的元素和僞元素字符串(如":after")。若是不須要查詢僞元素,則第二個參數能夠傳 null。getComputedStyle()方法返回一個 CSSStyleDeclaration 對象(與 style 屬性的類型同樣),包含元素的計算樣式。react

<!DOCTYPE html>
<html>
  <head>
    <title>Computed Styles Example</title>
    <style type="text/css"> #myDiv { background-color: blue; width: 100px; height: 200px; } </style>
  </head>
  <body>
    <div id="myDiv" style="background-color: red; border: 1px solid black" ></div>
  </body>
  <script>
    let myDiv = document.getElementById("myDiv");
    let computedStyle = document.defaultView.getComputedStyle(myDiv, null);

    console.log(computedStyle.backgroundColor); // "red"
    console.log(computedStyle.width); // "100px"
    console.log(computedStyle.height); // "200px"
    console.log(computedStyle.border); // "1px solid black"(在某些瀏覽器中)

    /* 兼容寫法 */
    function getStyleByAttr(obj, name) {
       return window.getComputedStyle
         ? window.getComputedStyle(obj, null)[name]
         : obj.currentStyle[name];
     }
     let node = document.getElementById("myDiv");
     console.log(getStyleByAttr(node, "backgroundColor"));
     console.log(getStyleByAttr(node, "width"));
     console.log(getStyleByAttr(node, "height"));
     console.log(getStyleByAttr(node, "border
  </script>
</html>
複製代碼

2.說一下對 React Hook 的理解,它的實現原理,和生命週期有哪些區別?

公司:高德、頭條nginx

分類:Reactgit

查看解析

1、React Hook

1.1 什麼是 React Hook

Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。

從官網的這句話中,咱們能夠明確的知道,Hook 增長了函數式組件中state的使用,在以前函數式組件是沒法擁有本身的狀態,只能經過props以及context來渲染本身的UI,而在業務邏輯中,有些場景必需要使用到state,那麼咱們就只能將函數式組件定義爲class組件。而如今經過Hook,咱們能夠輕鬆的在函數式組件中維護咱們的狀態,不須要更改成class組件。

React16.8 加入 hooks,讓 React 函數式組件更加靈活

hooks 以前,React 存在不少問題

  1. 在組件間複用狀態邏輯很難
  2. 複雜組件變得難以理解,高階組件和函數組件的嵌套過深。
  3. class 組件的 this 指向問題
  4. 難以記憶的生命週期

hooks 很好的解決了上述問題,hooks 提供了不少方法

  1. useState 返回有狀態值,以及更新這個狀態值的函數
  2. useEffect 接受包含命令式,可能有反作用代碼的函數。
  3. useContext 接受上下文對象(從 React.createContext 返回的值)並返回當前上下文值,
  4. useReducer useState 的替代方案。接受類型爲(state,action) => newState 的 reducer,並返回與 dispatch 方法配對的當前狀態。
  5. useCallback 返回一個回憶的 memoized 版本,該版本僅在其中一個輸入發生更改時纔會更改。純函數的輸入輸出肯定性
  6. useMemo 純的一個記憶函數
  7. useRef 返回一個可變的 ref 對象,其.current 屬性被初始化爲傳遞的參數
  8. useImperativeMethods 自定義使用 ref 時公開給父組件的實例值
  9. useMutationEffect 更新兄弟組件以前,它在 React 執行其 DOM 改變的同一階段同步觸發
  10. useLayoutEffect DOM 改變後同步觸發。使用它來從 DOM 讀取佈局並同步從新渲染

1.2.React Hook 要解決什麼問題

React Hooks要解決的問題是狀態共享,這裏的狀態共享是指只共享狀態邏輯複用,並非指數據之間的共享。咱們知道在React Hooks以前,解決狀態邏輯複用問題,咱們一般使用higher-order componentsrender-props

既然已經有了這兩種解決方案,爲何React開發者還要引入React Hook?對於higher-order componentsrender-propsReact Hook的優點在哪?

PS:Hook 最大的優點其實仍是對於狀態邏輯的複用便捷,還有代碼的簡潔,以及幫助函數組件加強功能,

咱們先來看一下React官方給出的React Hookdemo

import { useState } from "React";

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
  );
}
複製代碼

咱們再來看看不用React Hook的話,如何實現

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  render() {
    return (
      <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
    );
  }
}
複製代碼

能夠看到,在React Hook中,class Example組件變成了函數式組件,可是這個函數式組件卻擁有的本身的狀態,同時還能夠更新自身的狀態。這一切都得益於useState這個HookuseState 會返回一對值:當前狀態和一個讓你更新它的函數,你能夠在事件處理函數中或其餘一些地方調用這個函數。它相似 class 組件的 this.setState,可是它不會把新的 state 和舊的 state 進行合併

1.3 實現原理

Hooks 的基本類型:

type Hooks = {
  memoizedState: any, // 指向當前渲染節點 Fiber
  baseState: any, // 初始化 initialState, 已經每次 dispatch 以後 newState
  baseUpdate: Update<any> | null, // 當前須要更新的 Update ,每次更新完以後,會賦值上一個 update,方便 react 在渲染錯誤的邊緣,數據回溯
  queue: UpdateQueue<any> | null, // UpdateQueue 經過
  next: Hook | null, // link 到下一個 hooks,經過 next 串聯每一 hooks
};

type Effect = {
  tag: HookEffectTag, // effectTag 標記當前 hook 做用在 life-cycles 的哪個階段
  create: () => mixed, // 初始化 callback
  destroy: (() => mixed) | null, // 卸載 callback
  deps: Array<mixed> | null,
  next: Effect, // 同上
};
複製代碼

React Hooks 全局維護了一個 workInProgressHook 變量,每一次調取 Hooks API 都會首先調取 createWorkInProgressHooks 函數。

function createWorkInProgressHook() {
  if (workInProgressHook === null) {
    // This is the first hook in the list
    if (firstWorkInProgressHook === null) {
      currentHook = firstCurrentHook;
      if (currentHook === null) {
        // This is a newly mounted hook
        workInProgressHook = createHook();
      } else {
        // Clone the current hook.
        workInProgressHook = cloneHook(currentHook);
      }
      firstWorkInProgressHook = workInProgressHook;
    } else {
      // There's already a work-in-progress. Reuse it.
      currentHook = firstCurrentHook;
      workInProgressHook = firstWorkInProgressHook;
    }
  } else {
    if (workInProgressHook.next === null) {
      let hook;
      if (currentHook === null) {
        // This is a newly mounted hook
        hook = createHook();
      } else {
        currentHook = currentHook.next;
        if (currentHook === null) {
          // This is a newly mounted hook
          hook = createHook();
        } else {
          // Clone the current hook.
          hook = cloneHook(currentHook);
        }
      }
      // Append to the end of the list
      workInProgressHook = workInProgressHook.next = hook;
    } else {
      // There's already a work-in-progress. Reuse it.
      workInProgressHook = workInProgressHook.next;
      currentHook = currentHook !== null ? currentHook.next : null;
    }
  }
  return workInProgressHook;
}
複製代碼

假設咱們須要執行如下 hooks 代碼:

function FunctionComponet() {

  const [ state0, setState0 ] = useState(0);
  const [ state1, setState1 ] = useState(1);
  useEffect(() => {
  	document.addEventListener('mousemove', handlerMouseMove, false);
    ...
    ...
    ...
    return () => {
      ...
      ...
      ...
    	document.removeEventListener('mousemove', handlerMouseMove, false);
    }
  })

  const [ satte3, setState3 ] = useState(3);
  return [state0, state1, state3];
}
複製代碼

當咱們瞭解 React Hooks 的簡單原理,獲得 Hooks 的串聯不是一個數組,可是是一個鏈式的數據結構,從根節點 workInProgressHook 向下經過 next 進行串聯。這也就是爲何 Hooks 不能嵌套使用,不能在條件判斷中使用,不能在循環中使用。不然會破壞鏈式結構。

2、和生命週期的區別

函數組件 的本質是函數,沒有 state 的概念的,所以不存在生命週期一說,僅僅是一個 render 函數而已。

可是引入 Hooks 以後就變得不一樣了,它能讓組件在不使用 class 的狀況下擁有 state,因此就有了生命週期的概念,所謂的生命週期其實就是 useState、 useEffect() 和 useLayoutEffect() 。

即:Hooks 組件(使用了 Hooks 的函數組件)有生命週期,而函數組件(未使用 Hooks 的函數組件)是沒有生命週期的。

下面,是具體的 class 與 Hooks 的生命週期對應關係:

hook & lifcycle

3.移動端適配方案具體實現以及對比

公司:頭條

分類:Css

查看解析

常見的移動端適配方案

  • media queries
  • flex 佈局
  • rem + viewport
  • vh vw
  • 百分比

1、Meida Queries

meida queries 的方式能夠說是我早期採用的佈局方式,它主要是經過查詢設備的寬度來執行不一樣的 css 代碼,最終達到界面的配置。

核心語法:

@media only screen and (max-width: 374px) {
  /* iphone5 或者更小的尺寸,以 iphone5 的寬度(320px)比例設置樣式*/
}
@media only screen and (min-width: 375px) and (max-width: 413px) {
  /* iphone6/7/8 和 iphone x */
}
@media only screen and (min-width: 414px) {
  /* iphone6p 或者更大的尺寸,以 iphone6p 的寬度(414px)比例設置樣式 */
}
複製代碼

優勢:

  • media query 能夠作到設備像素比的判斷,方法簡單,成本低,特別是針對移動端和 PC 端維護同一套代碼的時候。目前像 Bootstrap 等框架使用這種方式佈局
  • 圖片便於修改,只需修改 css 文件
  • 調整屏幕寬度的時候不用刷新頁面便可響應式展現

缺點:

  • 代碼量比較大,維護不方便
  • 爲了兼顧大屏幕或高清設備,會形成其餘設備資源浪費,特別是加載圖片資源
  • 爲了兼顧移動端和 PC 端各自響應式的展現效果,不免會損失各自特有的交互方式

2、Flex 彈性佈局

以天貓的實現方式進行說明:

它的 viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

高度定死,寬度自適應,元素都採用 px 作單位。

隨着屏幕寬度變化,頁面也會跟着變化,效果就和 PC 頁面的流體佈局差很少,在哪一個寬度須要調整的時候使用響應式佈局調調就行(好比網易新聞),這樣就實現了『適配』。

3、rem+viewport 縮放

實現原理:

根據 rem 將頁面放大 dpr 倍, 而後 viewport 設置爲 1/dpr.

  • 如 iphone6 plus 的 dpr 爲 3, 則頁面總體放大 3 倍, 1px(css 單位)在 plus 下默認爲 3px(物理像素)
  • 而後 viewport 設置爲 1/3, 這樣頁面總體縮回原始大小. 從而實現高清。

這樣整個網頁在設備內顯示時的頁面寬度就會等於設備邏輯像素大小,也就是 device-width。這個 device-width 的計算公式爲:

設備的物理分辨率/(devicePixelRatio * scale),在 scale 爲 1 的狀況下,device-width = 設備的物理分辨率/devicePixelRatio

4、rem 實現

rem是相對長度單位,rem方案中的樣式設計爲相對於根元素font-size計算值的倍數。根據屏幕寬度設置html標籤的font-size,在佈局時使用 rem 單位佈局,達到自適應的目的。

viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

經過如下代碼來控制 rem 基準值(設計稿以 720px 寬度量取實際尺寸)

!(function (d) {
  var c = d.document;
  var a = c.documentElement;
  var b = d.devicePixelRatio;
  var f;
  function e() {
    var h = a.getBoundingClientRect().width,
      g;
    if (b === 1) {
      h = 720;
    }
    if (h > 720) h = 720; //設置基準值的極限值
    g = h / 7.2;
    a.style.fontSize = g + "px";
  }
  if (b > 2) {
    b = 3;
  } else {
    if (b > 1) {
      b = 2;
    } else {
      b = 1;
    }
  }
  a.setAttribute("data-dpr", b);
  d.addEventListener(
    "resize",
    function () {
      clearTimeout(f);
      f = setTimeout(e, 200);
    },
    false
  );
  e();
})(window);
複製代碼

css 經過 sass 預編譯,設置量取的 px 值轉化 rem 的變量$px: (1/100)+rem;

優勢:

  • 兼容性好,頁面不會由於伸縮發生變形,自適應效果更佳。

缺點:

  • 不是純 css 移動適配方案,須要在頭部內嵌一段 js腳本監聽分辨率的變化來動態改變根元素的字體大小,css樣式和 js 代碼有必定耦合性,而且必須將改變font-size的代碼放在 css 樣式以前。
  • 小數像素問題,瀏覽器渲染最小的單位是像素,元素根據屏幕寬度自適應,經過 rem 計算後可能會出現小數像素,瀏覽器會對這部分小數四捨五入,按照整數渲染,有可能沒那麼準確。

5、純 vw 方案

視口是瀏覽器中用於呈現網頁的區域。

  • vw : 1vw 等於 視口寬度1%
  • vh : 1vh 等於 視口高度 的 **1% **
  • vmin : 選取 vwvh最小 的那個
  • vmax : 選取 vwvh最大 的那個

雖然 vw 能更優雅的適配,可是仍是有點小問題,就是寬,高無法限制。

$base_vw = 375;
@function vw ($px) {
    return ($px/$base_vw) * 100vw
};
複製代碼

優勢:

  • css 移動端適配方案,不存在腳本依賴問題。
  • 相對於 rem 以根元素字體大小的倍數定義元素大小,邏輯清晰簡單。

缺點:

  • 存在一些兼容性問題,有些瀏覽器不支持

6、vw + rem 方案

// scss 語法
// 設置html根元素的大小 750px->75 640px->64
// 將屏幕分紅10份,每份做爲根元素的大小。
$vw_fontsize: 75
@function rem($px) {
    // 例如:一個div的寬度爲100px,那麼它對應的rem單位就是(100/根元素的大小)* 1rem
    @return ($px / $vw_fontsize) * 1rem;
}
$base_design: 750
html {
    // rem與vw相關聯
    font-size: ($vw_fontsize / ($base_design / 2)) * 100vw;
    // 同時,經過Media Queries 限制根元素最大最小值
    @media screen and (max-width: 320px) {
        font-size: 64px;
    }
    @media screen and (min-width: 540px) {
        font-size: 108px;
    }
}

// body 也增長最大最小寬度限制,避免默認100%寬度的 block 元素跟隨 body 而過大太小
body {
    max-width: 540px;
    min-width: 320px;
}

複製代碼

7、百分比

使用百分比%定義寬度,高度用px固定,根據可視區域實時尺寸進行調整,儘量適應各類分辨率,一般使用max-width/min-width控制尺寸範圍過大或者太小。

優勢:

  • 原理簡單,不存在兼容性問題

缺點:

  • 若是屏幕尺度跨度太大,相對設計稿過大或者太小的屏幕不能正常顯示,在大屏手機或橫豎屏切換場景下可能會致使頁面元素被拉伸變形,字體大小沒法隨屏幕大小發生變化。
  • 設置盒模型的不一樣屬性時,其百分比設置的參考元素不惟一,容易使佈局問題變得複雜。

4.var arr =[[‘A’,’B’],[‘a’,’b’],[1,2]] 求二維數組的全排列組合 結果:Aa1,Aa2,Ab1,Ab2,Ba1,Ba2,Bb1,Bb2

公司:美團

分類:算法

查看解析

參考代碼實現

  • 實現方式一
function foo(arr) {
  // 用於記錄初始數組長度, 用於將數組前兩組已經獲取到全排列的數組進行截取標識
  var len = arr.length;
  // 當遞歸操做後, 數組長度爲1時, 直接返回arr[0], 只有大於1繼續處理
  if (len >= 2) {
    // 每次只作傳入數組的前面兩個數組進行全排列組合, 即arr[0]和arr[1]的全排列組合
    var len1 = arr[0].length;
    var len2 = arr[1].length;
    var items = new Array(len1 * len2); // 建立全排列組合有可能次數的數組
    var index = 0; // 記錄每次全排列組合後的數組下標
    for (var i = 0; i < len1; i++) {
      for (var j = 0; j < len2; j++) {
        if (Array.isArray(arr[0])) {
          // 當第二次進來後, 數組第一個元素一定是數組包着數組
          items[index] = arr[0][i].concat(arr[1][j]); // 對於已是第二次遞歸進來的全排列直接追加便可
        } else {
          items[index] = [arr[0][i]].concat(arr[1][j]); // 這裏由於只須要去arr[0]和arr[1]的全排列, 因此這裏就直接使用concat便可
        }
        index++; // 更新全排列組合的下標
      }
    }
    // 若是數組大於2, 這裏新的newArr作一個遞歸操做
    var newArr = new Array(len - 1); // 遞歸的數組比傳進來的數組長度少一, 由於上面已經將傳進來的數組的arr[0]和arr[1]進行全排列組合, 因此這裏的newArr[0]就是上面已經全排列好的數組item
    for (var i = 2; i < arr.length; i++) {
      // 這裏的for循環是爲了截取下標1項後的數組進行賦值給newArr
      newArr[i - 1] = arr[i];
    }
    newArr[0] = items; // 由於上面已經將傳進來的數組的arr[0]和arr[1]進行全排列組合, 因此這裏的newArr[0]就是上面已經全排列好的數組item
    // 從新組合後的數組進行遞歸操做
    return foo(newArr);
  } else {
    // 當遞歸操做後, 數組長度爲1時, 直接返回arr[0],
    return arr[0];
  }
}
var arr = [
  ["A", "B"],
  ["a", "b"],
  [1, 2],
];
console.log(foo(arr));
複製代碼
  • 實現方式二
const getResult = (arr1, arr2) => {
  if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
    return;
  }
  if (!arr1.length) {
    return arr2;
  }
  if (!arr2.length) {
    return arr1;
  }
  let result = [];
  for (let i = 0; i < arr1.length; i++) {
    for (let j = 0; j < arr2.length; j++) {
      result.push(String(arr1[i]) + String(arr2[j]));
    }
  }
  return result;
};

const findAll = (arr) =>
  arr.reduce((total, current) => {
    return getResult(total, current);
  }, []);
var arr = [
  ["A", "B"],
  ["a", "b"],
  [1, 2],
];
console.log(findAll(arr));
複製代碼
  • 實現方式三
var arr = [
  ["A", "B"],
  ["a", "b"],
  [1, 2],
];
let res = [],
  lengthArr = arr.map((d) => d.length);
let indexArr = new Array(arr.length).fill(0);
function addIndexArr() {
  indexArr[0] = indexArr[0] + 1;
  let i = 0;
  let overflow = false;
  while (i <= indexArr.length - 1) {
    if (indexArr[i] >= lengthArr[i]) {
      if (i < indexArr.length - 1) {
        indexArr[i] = 0;
        indexArr[i + 1] = indexArr[i + 1] + 1;
      } else {
        overflow = true;
      }
    }
    i++;
  }
  return overflow;
}
function getAll(arr, indexArr) {
  let str = "";
  arr.forEach((item, index) => {
    str += item[indexArr[index]];
  });
  res.push(str);
  let overflow = addIndexArr();
  if (overflow) {
    return;
  } else {
    return getAll(arr, indexArr);
  }
}
getAll(arr, indexArr);
console.log(res);
複製代碼

5.說下工做流程(開發流程、代碼規範、打包流程等)

公司:騰訊微視

分類:工程化

查看解析

1、拿到原型圖,先自我解析需求,畫出思惟導圖,流程圖

  • 在未拿到 UI 給定的 PSD 時,能夠先理清咱們的需求
    • 依賴的外部資源
      • 後端提供的接口
      • UI 出圖的大概佈局
      • 後期頻繁改動的地方
  • 須要實現的效果
    • 下拉刷新
    • 動畫效果
    • 吸頂效果
    • 懶加載、預加載、防抖、節流

2、產品召集項目相關人員,開需求討論會,產品講解原型

  1. 理解產品的需求,提出質疑:這是什麼功能,怎麼作,爲啥這麼作
  2. 評估實現難度和實現成本,是否有潛在技術問題/風險
  3. 對比本身整理的需求圖,若是有和本身想的不符合的,提出疑問
  4. 理解 PM 提出這次需求的目的,明白哪些內容是重點,哪些次要,能夠適當取捨
  5. 若是產品要求提供時間,簡單項目能夠預估,複雜項目不可立刻給出時間,須要仔細評估,評估時間包含開發、自測、測試人員測試、修復 bug、上線準備

3、會後進一步整理需求

  1. 細化細節,整理有疑問的地方,與產品、設計等其餘人進行確認
  2. 評估項目完成時間--影響因素:須要的人力、 中間插入的需求、 開發、 自測、 測試人員測試、 修復 bug、 上線準備、 其餘風險(如技術選型錯誤等)
  3. 初步制定排期表

4、需求二次確認(開發中遇到不肯定的,依舊須要找相關人員進行需求確認,杜絕作無用功)

  1. IM 工具溝通確認
  2. 郵件確認
  3. 小型需求/項目相關討論會
  4. 肯定最終排期表

5、開發

  1. 技術選型
  2. 搭建開發環境:工具鏈
  3. 搭建項目架構
  4. 業務模塊劃分
    • 優先級排序
    • 新項目介入,須要當前項目和介入項目的相關負責人 Pk 優先級,隨後調整項目排期
    • 開發過程當中發現工做量與預期有嚴重出入,須要儘早向其餘項目人員反饋,方便其修改時間安排
  5. 定製開發規範
    • 開發規範
      • commit 提交格式:[改動文件類型]:[改動說明]
      • 單分支開發或者多分支開發
        1. 小項目、並行開發少,則只在 master 主分支開發
        2. 中大項目,需求複雜,並行功能多,則須要分爲 master、developer、開發者分支;須要開發者自創一個分支開發,合併到 developer,確認無問題後,發佈到 master,最後上線
    • 代碼規範
      1. jsconfig.json
      2. .postcssrc.js
      3. .babelrc
      4. .prettierrc(vscode 插件 prettier-code fomatter)— 注意與 eslint 要保持一致
      5. .editorconfig
      6. .eslintrc.js(強制開啓驗證模式)
    • 源碼管理
    • 版本管理
    • 安全管理

6、自測

  1. 手動測試
  2. 單元測試
  3. 集成測試

7、提測---測試人員測試

  1. 開發人員修復 bug
  2. 期間不可接手耗時大的需求
  3. 有不肯定優先級高低的需求,須要各個需求方互相 pk 優先級,再肯定作與不作,不能所以拖延項目完成點
  4. 測試修復 bug 時間可能比開發時間還長,所以開發者預估開發時間不能樂觀

8、上線

  1. 上線準備 a. 域名申請 b. 備案申請 c. 服務器申請 d. 部署

  2. 測試線上環境

    • 有 bug 回到修復 bug 環節
  3. 日誌監控

    1. 調用棧
    2. sourcemap
    3. 本地日誌
    4. 用戶環境、IP
    5. 低成本接入
    6. 統計功能
    7. 報警功能

9、維護

  1. 技術創新(對現有的技術領域以及具體項目實現方法進行優化)
    1. 提升效率:例如 jenkins 構建部署
    2. 減小成本
    3. 提高穩定性
    4. 安全性

6.Vue 中父組件能夠監聽到子組件的生命週期嗎?

公司:水滴籌

分類:Vue

查看解析

實現方式

1、使用 on 和 emit

// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
  this.$emit("mounted");
}
複製代碼

2、使用 hook 鉤子函數

// Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父組件監聽到 mounted 鉤子函數 ...');
},
// Child.vue
mounted(){
   console.log('子組件觸發 mounted 鉤子函數 ...');
},
// 以上輸出順序爲:
// 子組件觸發 mounted 鉤子函數 ...
// 父組件監聽到 mounted 鉤子函數 ...
複製代碼

7.怎麼給 Vue 定義全局方法

分類:Vue

查看解析

實現方式

1、將方法掛載到 Vue.prototype 上面

缺點:調用這個方法的時候沒有提示

// global.js
const RandomString = (encode = 36, number = -8) => {
  return Math.random() // 生成隨機數字, eg: 0.123456
    .toString(encode) // 轉化成36進制 : "0.4fzyo82mvyr"
    .slice(number);
},
export default {
	RandomString,
  ...
}
複製代碼
// 在項目入口的main.js裏配置
import Vue from "vue";
import global from "@/global";
Object.keys(global).forEach((key) => {
  Vue.prototype["$global" + key] = global[key];
});
複製代碼
// 掛載以後,在須要引用全局變量的模塊處(App.vue),不需再導入全局變量模塊,而是直接用this就能夠引用了,以下:
export default {
  mounted() {
    this.$globalRandomString();
  },
};
複製代碼

2、利用全局混入mixin

優勢:由於mixin裏面的methods會和建立的每一個單文件組件合併。這樣作的優勢是調用這個方法的時候有提示

// mixin.js
import moment from 'moment'
const mixin = {
  methods: {
    minRandomString(encode = 36, number = -8) {
      return Math.random() // 生成隨機數字, eg: 0.123456
        .toString(encode) // 轉化成36進制 : "0.4fzyo82mvyr"
        .slice(number);
    },
    ...
  }
}
export default mixin
複製代碼
// 在項目入口的main.js裏配置
import Vue from 'vue'
import mixin from '@/mixin'
Vue.mixin(mixin)
複製代碼
export default {
 mounted() {
   this.minRandomString()
 }
}
複製代碼

3、使用Plugin方式

Vue.use的實現沒有掛載的功能,只是觸發了插件的install方法,本質仍是使用了Vue.prototype。

// plugin.js
function randomString(encode = 36, number = -8) {
  return Math.random() // 生成隨機數字, eg: 0.123456
    .toString(encode) // 轉化成36進制 : "0.4fzyo82mvyr"
    .slice(number);
}
const plugin = {
  // install 是默認的方法。
  // 當外界在 use 這個組件或函數的時候,就會調用自己的 install 方法,同時傳一個 Vue 這個類的參數。
  install: function(Vue){
    Vue.prototype.$pluginRandomString = randomString
    ...
  },
}
export default plugin
複製代碼
// 在項目入口的main.js裏配置
import Vue from 'vue'
import plugin from '@/plugin'
Vue.use(plugin)
複製代碼
export default {
 mounted() {
   this.$pluginRandomString()
 }
}
複製代碼

4、任意 vue 文件中寫全局函數

// 建立全局方法
this.$root.$on("test", function () {
  console.log("test");
});
// 銷燬全局方法
this.$root.$off("test");
// 調用全局方法
this.$root.$emit("test");
複製代碼

8.說一下 vm.$set 原理

公司:極光推送

分類:Vue

查看解析

vm.$set()解決了什麼問題

在 Vue.js 裏面只有 data 中已經存在的屬性纔會被 Observe 爲響應式數據,若是你是新增的屬性是不會成爲響應式數據,所以 Vue 提供了一個 api(vm.$set)來解決這個問題。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue Demo</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  </head>
  <body>
    <div id="app">
      {{user.name}} {{user.age}}
      <button @click="addUserAgeField">增長一個年紀字段</button>
    </div>
    <script> const app = new Vue({ el: "#app", data: { user: { name: "test", }, }, mounted() {}, methods: { addUserAgeField() { // this.user.age = 20 這樣是不起做用, 不會被Observer this.$set(this.user, "age", 20); // 應該使用 }, }, }); </script>
  </body>
</html>
複製代碼

原理

vm.$set()在 new Vue()時候就被注入到 Vue 的原型上。

源碼位置: vue/src/core/instance/index.js

import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";

function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

initMixin(Vue);
// 給原型綁定代理屬性$props, $data
// 給Vue原型綁定三個實例方法: vm.$watch,vm.$set,vm.$delete
stateMixin(Vue);
// 給Vue原型綁定事件相關的實例方法: vm.$on, vm.$once ,vm.$off , vm.$emit
eventsMixin(Vue);
// 給Vue原型綁定生命週期相關的實例方法: vm.$forceUpdate, vm.destroy, 以及私有方法_update
lifecycleMixin(Vue);
// 給Vue原型綁定生命週期相關的實例方法: vm.$nextTick, 以及私有方法_render, 以及一堆工具方法
renderMixin(Vue);

export default Vue;
複製代碼
  • stateMixin()
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
複製代碼
  • set()

源碼位置: vue/src/core/observer/index.js

export function set(target: Array<any> | Object, key: any, val: any): any {
  // 1.類型判斷
  // 若是 set 函數的第一個參數是 undefined 或 null 或者是原始類型值,那麼在非生產環境下會打印警告信息
  // 這個api原本就是給對象與數組使用的
  if (
    process.env.NODE_ENV !== "production" &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(
      `Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
    );
  }
  // 2.數組處理
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 相似$vm.set(vm.$data.arr, 0, 3)
    // 修改數組的長度, 避免索引>數組長度致使splcie()執行有誤
    //若是不設置length,splice時,超過本來數量的index則不會添加空白項
    target.length = Math.max(target.length, key);
    // 利用數組的splice變異方法觸發響應式, 這個前面講過
    target.splice(key, 1, val);
    return val;
  }
  //3.對象,且key不是原型上的屬性處理
  // target爲對象, key在target或者target.prototype上。
  // 同時必須不能在 Object.prototype 上
  // 直接修改便可, 有興趣能夠看issue: https://github.com/vuejs/vue/issues/6845
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 以上都不成立, 即開始給target建立一個全新的屬性
  // 獲取Observer實例
  const ob = (target: any).__ob__;
  // Vue 實例對象擁有 _isVue 屬性, 即不容許給Vue 實例對象添加屬性
  // 也不容許Vue.set/$set 函數爲根數據對象(vm.$data)添加屬性
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid adding reactive properties to a Vue instance or its root $data " +
          "at runtime - declare it upfront in the data option."
      );
    return val;
  }
  //5.target是非響應式數據時
  // target自己就不是響應式數據, 直接賦值
  if (!ob) {
    target[key] = val;
    return val;
  }
  //6.target對象是響應式數據時
  //定義響應式對象
  defineReactive(ob.value, key, val);
  //watcher執行
  ob.dep.notify();
  return val;
}
複製代碼
  • 工具函數
// 判斷給定變量是不是未定義,當變量值爲 null時,也會認爲其是未定義
export function isUndef(v: any): boolean %checks {
  return v === undefined || v === null;
}

// 判斷給定變量是不是原始類型值
export function isPrimitive(value: any): boolean %checks {
  return (
    typeof value === "string" ||
    typeof value === "number" ||
    // $flow-disable-line
    typeof value === "symbol" ||
    typeof value === "boolean"
  );
}

// 判斷給定變量的值是不是有效的數組索引
export function isValidArrayIndex(val: any): boolean {
  const n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val);
}
複製代碼
  • 關於(ob && ob.vmCount)
export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 省略...
  if (asRootData && ob) {
    // vue已經被Observer了,而且是根數據對象, vmCount纔會++
    ob.vmCount++;
  }
  return ob;
}
複製代碼
  • 在初始化 Vue 的過程當中有
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    //opts.data爲對象屬性
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
複製代碼
  • initData(vm)
function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};

  // 省略...

  // observe data
  observe(data, true /* asRootData */);
}
複製代碼

從源碼能夠看出 set 主要邏輯以下:

  1. 類型判斷
  2. target 爲數組:調用 splice 方法
  3. target 爲對象,且 key 不是原型上的屬性處理:直接修改
  4. target 不能是 Vue 實例,或者 Vue 實例的根數據對象,不然報錯
  5. target 是非響應式數據時,咱們就按照普通對象添加屬性的方式來處理
  6. target 爲響應數據,且key 爲新增屬性,咱們 key 設置爲響應式,並手動觸發其屬性值的更新

總結

vm.$set(target、key、value)

  • 當 target 爲數組時,直接調用數組方法 splice 實現;
  • 若是目標是對象,會先判讀屬性是否存在、對象是不是響應式
  • 最終若是要對屬性進行響應式處理,則是經過調用 defineReactive 方法進行響應式處理
    • defineReactive 方法就是 Vue 在初始化對象時,給對象屬性採用 Object.defineProperty 動態添加 getter 和 setter 的功能所調用的方法

9.深拷貝如何解決循環引用?

公司:極光推送

分類:JavaScript

查看解析

循環引用問題

看個例子

function deepCopy(obj){
    const res = Array.isArray(obj) ? [] : {};
    for(let key in obj){
        if(typeof obj[key] === 'object'){
            res[key] = deepCopy(obj[key]);
        }else{
            res[key] = obj[key];
        }
    }
    return res
}
var obj = {
    a:1,
    b:2,
    c:[1,2,3],
    d:{aa:1,bb:2},
};
obj.e = obj;
console.log('obj',obj); // 不會報錯

const objCopy = deepCopy(obj);
console.log(objCopy); //Uncaught RangeError: Maximum call stack size exceeded
複製代碼

從例子能夠看到,當存在循環引用的時候,deepCopy會報錯,棧溢出。

  • obj對象存在循環引用時,打印它時是不會棧溢出
  • 深拷貝obj時,纔會致使棧溢出

循環應用問題解決

  • 即:目標對象存在循環應用時報錯處理

你們都知道,對象的key是不能是對象的。

{{a:1}:2}
// Uncaught SyntaxError: Unexpected token ':'
複製代碼

參考解決方式一:使用weekmap:

解決循環引用問題,咱們能夠額外開闢一個存儲空間,來存儲當前對象和拷貝對象的對應關係

這個存儲空間,須要能夠存儲key-value形式的數據,且key能夠是一個引用類型,

咱們能夠選擇 WeakMap  這種數據結構:

  • 檢查 WeakMap  中有無克隆過的對象
  • 有,直接返回
  • 沒有,將當前對象做爲key,克隆對象做爲value進行存儲
  • 繼續克隆
function isObject(obj) {
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function cloneDeep(source, hash = new WeakMap()) {
  if (!isObject(source)) return source;
  if (hash.has(source)) return hash.get(source); // 新增代碼,查哈希表

  var target = Array.isArray(source) ? [] : {};
  hash.set(source, target); // 新增代碼,哈希表設值

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (isObject(source[key])) {
        target[key] = cloneDeep(source[key], hash); // 新增代碼,傳入哈希表
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}
複製代碼

參考解決方式二:

能夠用 Set,發現相同的對象直接賦值,也可用 Map

const o = { a: 1, b: 2 };
o.c = o;

function isPrimitive(val) {
    return Object(val) !== val;
}
const set = new Set();
function clone(obj) {
    const copied = {};
    for (const [key, value] of Object.entries(obj)) {
        if (isPrimitive(value)) {
            copied[key] = value;
        } else {
            if (set.has(value)) {
                copied[key] = { ...value };
            } else {
                set.add(value);
                copied[key] = clone(value);
            }
        }
    }
    return copied;
}
複製代碼

10.單元測試如何測試?代碼覆蓋率如何?

公司:編程貓

分類:工程化

查看解析

1、爲何要單元測試?

有單元測試加持能夠保障交付代碼質量,加強本身和他人的信心。咱們選擇第三方庫的時候不也是會優先選擇有測試保障的嗎?將來對代碼進行改動時也能夠節省迴歸測試的時間。

2、怎麼測?

在作單元測試時儘可能以集成測試爲主,對少許難以被集成測試覆蓋或須要分發的代碼作單元測試,同時也能夠有少許的端到端測試輔助。

儘可能不測試代碼實現,測試代碼實現可能會讓測試用例很快就失效。好比斷言變量,當變量名發生變動時會致使測試不經過,可是可能功能並無發生改變。

2.1 要寫些什麼樣的測試?

以用戶視角測試程序的功能,而非上帝視角。

對一個組件,傳入不一樣參數渲染 dom ,對用戶而言可能能夠看到某些特定文字或能夠作某些操做。此時能夠斷言 dom 是否出現了某些字,作動做(如點擊、輸入 、提交表單等)是否有正確的響應。

2.2 不要寫什麼樣的測試?

不要測試實現細節。好比以上帝視角檢查  redux store  上的數據、state  的數據等,而這些在最終用戶眼裏是不存在的,用戶能感知的只是所表現的功能。

3、測試框架和周邊配套

Jest  是 facebook 出品的測試框架。開箱即用,自帶斷言庫、mock、覆蓋率報告等功能。

因爲前端要測試 UI,須要在模擬瀏覽器環境中渲染出 dom,因此須要一個這樣的庫。存在不少這樣的庫,經常使用的有  Enzyme@testing-library/react

4、測試覆蓋/效率報告

Jest  自帶測試報告,可是衆多的項目分散在 gitlab 中給查看報告帶來了麻煩。須要考慮有一個集中的地方查看測試報告。這裏結合了  sonar  和  reportportal  歸集測試報告,能夠經過一個集中的地方查看全部項目的測試報告。

其中結合  sonar  的代碼掃描功能能夠查看測試覆蓋率等報告信息。reportportal  能夠查看測試執行率,另外官方宣稱自帶 AI 分析報告,能夠得出多維度的統計信息。

11.說說 React 狀態邏輯複用問題

公司:編程貓

分類:React

查看解析

React 狀態邏輯複用

1、Mixins

雖然 React 自己有些函數式味道,但爲了迎合用戶習慣,早期只提供了 React.createClass() API 來定義組件: 天然而然地,(類)繼承就成了一種直覺性的嘗試。而在 JavaScript 基於原型的擴展模式下,相似於繼承的 Mixin 方案就成了首選:

// 定義Mixin
var Mixin1 = {
  getMessage: function () {
    return "hello world";
  },
};
var Mixin2 = {
  componentDidMount: function () {
    console.log("Mixin2.componentDidMount()");
  },
};
// 用Mixin來加強現有組件
var MyComponent = React.createClass({
  mixins: [Mixin1, Mixin2],
  render: function () {
    return <div>{this.getMessage()}</div>;
  },
});
複製代碼

但存在諸多缺陷

組件與 Mixin 之間存在隱式依賴(Mixin 常常依賴組件的特定方法,但在定義組件時並不知道這種依賴關係)多個 Mixin 之間可能產生衝突(好比定義了相同的 state 字段)Mixin 傾向於增長更多狀態,這下降了應用的可預測性(The more state in your application, the harder it is to reason about it.),致使複雜度劇增。

隱式依賴致使依賴關係不透明,維護成本和理解成本迅速攀升:難以快速理解組件行爲,須要全盤瞭解全部依賴 Mixin 的擴展行爲,及其之間的相互影響。組件自身的方法和 state 字段不敢輕易刪改,由於難以肯定有沒有 Mixin 依賴它 Mixin 也難以維護,由於 Mixin 邏輯最後會被打平合併到一塊兒,很難搞清楚一個 Mixin 的輸入輸出。

毫無疑問,這些問題是致命的 因此,React v0.13.0 放棄了 Mixin(繼承),轉而走向 HOC(組合)。

2、Higher - Order Components

// 定義高階組件
var Enhance = (ComposedComponent) =>
  class extends Component {
    constructor() {
      this.state = { data: null };
    }
    componentDidMount() {
      this.setState({ data: "Hello" });
    }
    render() {
      return <ComposedComponent {...this.props} data={this.state.data} />;
    }
  };
class MyComponent {
  render() {
    if (!this.data) return <div>Waiting...</div>;
    return <div>{this.data}</div>;
  }
}
// 用高階組件來加強普通組件,進而實現邏輯複用
export default Enhance(MyComponent);
複製代碼

理論上,只要接受組件類型參數並返回一個組件的函數都是高階組件((Component, ...args) => Component),但爲了方便組合,推薦Component => Component形式的 HOC,經過偏函數應用來傳入其它參數,例如:React Redux's connect const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

優勢:

  • 組件樹結構 下降耦合和複雜度;
  • 代碼複用,邏輯抽象化
  • 渲染劫持,屬性代理,劫持組件的 props 和 state
  • 裝飾器,能夠做爲裝飾器來使用;
  • 函數柯里化

缺點:HOC 雖然沒有那麼多致命問題,但也存在一些小缺陷:

  • 擴展性限制
  • 不要在 render 中使用,每次 render 會從新建立一個高階組件,致使組件和子組件狀態丟失,影響性能;
  • 靜態方法會丟失,新組件沒有靜態方法,須要手動處理;
  • refs 不會往下傳遞,須要使用 forwardRef
  • 屢次嵌套,增長複雜度和理解成本;
  • 未使用命名空間的話,可能出現命名衝突,覆蓋舊屬性;
  • 不可見性,不知道外面包了啥,黑盒;

3、Render Props

「render prop」 是指⼀種在 React 組件之間使⽤⼀個值爲函數的 prop 共享代碼的簡單技術;

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }
  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  }
  render() {
    return (
      <div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}> {/* Instead of providing a static representation of what <Mouse> renders, use the `render` prop to dynamically determine what to render. */} {this.props.render(this.state)} </div> ); } } 複製代碼

優勢:數據共享、代碼復⽤,將組件內的 state 做爲 props 傳遞給調⽤者,將渲染邏輯交給調⽤者

缺點:⽆法在 return 語句外訪問數據、嵌套寫法不夠優雅;

4、Hooks

function MyResponsiveComponent() {
  const width = useWindowWidth();
  // Our custom Hook
  return <p>Window width is {width}</p>;
}
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  });
  return width;
}
複製代碼

比起上面提到的其它方案,Hooks 讓組件內邏輯複用再也不與組件複用捆綁在一塊兒,是真正在從下層去嘗試解決(組件間)細粒度邏輯的複用問題。

此外,這種聲明式邏輯複用方案將組件間的顯式數據流與組合思想進一步延伸到了組件內,契合 React 理念。

優勢以下:

  • 解決嵌套問題,簡潔,代碼量更少: React Hooks 解決了 HOC 和 Render Props 的嵌套問題,更加簡潔
  • 解耦: React Hooks 能夠更方便地把 UI 和狀態分離,作到更完全的解耦
  • 組合: Hooks 中能夠引用另外的 Hooks 造成新的 Hooks,組合變化萬千
  • 解決類組件的 3 個問題: React Hooks 爲函數組件而生,從而解決了類組件的幾大問題:
    • this 指向容易錯誤
    • 業務邏輯被分割在不一樣聲明週期中,使得代碼難以理解和維護
    • 代碼複用成本高(高階組件容易使代碼量劇增)

Hooks 也並不是完美,只是就目前而言,其缺點以下:

  • 還有兩個類組件的生命週期函數不能用 hooks 替代,getSnapshotBeforeUpdate 和 componentDidCatch
  • 額外的學習成本(Functional Component 與 Class Component 之間的困惑)
  • 寫法上有限制(不能在條件、循環、嵌套函數中使用),只能在函數頂層使用,增長了重構舊代碼的成本;由於 react 須要利用調用順序來更新狀態和調用鉤子函數;放到循環或條件分支中,可能致使調用順序不一致,致使奇怪的 bug;
  • 破壞了 PureComponent、React.memo 淺比較的性能優化效果(爲了取最新的 props 和 state,每次 render()都要從新建立事件處函數)
  • 在閉包場景可能會引用到舊的 state、props 值內部實現上不直觀(依賴一份可變的全局狀態,再也不那麼「純」)
  • React.memo 並不能徹底替代 shouldComponentUpdate(由於拿不到 state change,只針對 props change)
  • useState API 設計上不太完美
  • 使用 useState 時,數組對象,使用 push、pop、splice 直接更新,無效;好比 let [nums, setNums] = useState([0,1,2]); nums.push(1) 無效,必須使用 nums=[...nums, 1],再 setNums(nums);類組件中直接 push 是沒問題的
  • 不能使用裝飾器
  • 函數組件 ref 須要 forwardRef

12.組件庫設計有什麼原則?

公司:編程貓

分類:JavaScript

查看解析

組件庫設計原則

1.1 標準性

任何一個組件都應該遵照一套標準,能夠使得不一樣區域的開發人員據此標準開發出一套標準統一的組件

1.2 獨立性

  • 描述了組件的細粒度,遵循單一職責原則,保持組件的純粹性
  • 屬性配置等API對外開放,組件內部狀態對外封閉,儘量的少與業務耦合

1.3 複用與易用

  • UI差別,消化在組件內部(注意並非寫一堆if/else)
  • 輸入輸出友好,易用

1.4 適用SPOT法則

Single Point Of Truth,就是儘可能不要重複代碼,出自《The Art of Unix Programming》

1.5 避免暴露組件內部實現

1.6 避免直接操做DOM,避免使用ref

使用父組件的 state 控制子組件的狀態而不是直接經過 ref 操做子組件

1.7 入口處檢查參數的有效性,出口處檢查返回的正確性

1.8 無環依賴原則(ADP)

1.9 穩定抽象原則(SAP)

  • 組件的抽象程度與其穩定程度成正比,
  • 一個穩定的組件應該是抽象的(邏輯無關的)
  • 一個不穩定的組件應該是具體的(邏輯相關的)
  • 爲下降組件之間的耦合度,咱們要針對抽象組件編程,而不是針對業務實現編程

1.10 避免冗餘狀態

  • 若是一個數據能夠由另外一個 state 變換獲得,那麼這個數據就不是一個 state,只須要寫一個變換的處理函數,在 Vue 中能夠使用計算屬性
  • 若是一個數據是固定的,不會變化的常量,那麼這個數據就如同 HTML 固定的站點標題同樣,寫死或做爲全局配置屬性等,不屬於 state
  • 若是兄弟組件擁有相同的 state,那麼這個state 應該放到更高的層級,使用 props 傳遞到兩個組件中

1.11合理的依賴關係

父組件不依賴子組件,刪除某個子組件不會形成功能異常

1.12 扁平化參數

除了數據,避免複雜的對象,儘可能只接收原始類型的值

1.13 良好的接口設計

  • 把組件內部能夠完成的工做作到極致,雖然提倡擁抱變化,但接口不是越多越好
  • 若是常量變爲 props 能應對更多的場景,那麼就能夠做爲 props,原有的常量可做爲默認值。
  • 若是須要爲了某一調用者編寫大量特定需求的代碼,那麼能夠考慮經過擴展等方式構建一個新的組件。
  • 保證組件的屬性和事件足夠的給大多數的組件使用。

1.14 API儘可能和已知概念保持一致

13.寫出代碼輸出值,並說明緣由

function F() {
  this.a = 1;
}
var obj = new F();
console.log(obj.prototype);
複製代碼

公司:富途

分類:JavaScript

查看解析

答案

undefined
複製代碼

參考分析

構造函數實例通常沒有prototype屬性。除了Function構造函數

只有函數纔有prototype屬性,這個屬性值爲一個object對象 實例對象時沒有這個屬性

實例對象經過__proto__這個內部屬性([[prototype]])來串起一個原型鏈的,經過這個原型鏈能夠查找屬性,

方法 經過new操做符初始化一個函數對象的時候就會構建出一個實例對象,

函數對象的prototype屬性指向的對象就是這個實例對象的原型對象,也就是__proto__指向的對象

經典與原型鏈圖

prototype

14.把 10 萬次 for 循環的代碼插到 html 中間,會有什麼現象?出現卡頓現象怎麼解決?添加 defer 屬性以後腳本會在何時執行?採用 defer 以後,用戶點擊頁面會怎麼樣?若是禁用 WebWoker,還有其餘方法嗎?

公司:富途

分類:JavaScript

查看解析

1、十萬次循環代碼插入 body 中,頁面會出現卡頓

十萬次循環代碼插入 body 中,頁面會出現卡頓,代碼後的 DOM 節點加載不出來

2、解決

設置 script 標籤 defer 屬性,瀏覽器其它線程將下載腳本,待到文檔解析完成腳本纔會執行。

3、採用 defer 以後,用戶點擊問題

  • 若 button 中的點擊事件在 defer 腳本前定義,則在 defer 腳本加載完後,響應點擊事件。

  • 若 button 中的點擊事件在 defer 腳本後定義,則用戶點擊 button 無反應,待腳本加載完後,再次點擊有響應。

  • 代碼示例

<!-- test.html -->
<!DOCTYPE html>
<html>
  <head>
    <title></title>
  </head>

  <body>
    <div class="test1">test1</div>
    <div id="hello"></div>
    <script> // 待defer腳本下載完成後響應 function alertMsg() { alert("123"); } </script>
    <input type="button" id="button1" onclick="alertMsg()" />
    <script src="./test.js" defer></script>
    <div class="test2">test2</div>
  </body>
  <style> .test1 { color: red; font-size: 50px; } .test2 { color: yellow; font-size: 50px; } </style>
</html>
複製代碼
// test.js

for (let i = 0; i < 100000; i++) {
  console.log(i);
}
document.getElementById("hello").innerHTML = "hello world";
複製代碼

4、若是禁用 WebWoker,還有其餘方法嗎?

4.1 使用 Concurrent.Thread.js

  • Concurrent.Thread.js 用來模擬多線程,進行多線程開發。
Concurrent.Thread.create(function () {
  $("#test").click(function () {
    alert(1);
  });
  for (var i = 0; i < 100000; i++) {
    console.log(i);
  }
});
複製代碼

4.2 使用虛擬列表

若該情形是渲染十萬條數據的狀況下,則能夠使用虛擬列表。虛擬列表即只渲染可視區域的數據,使得在數據量龐大的狀況下,減小 DOM 的渲染,使得列表流暢地無限滾動。

實現方案:

基於虛擬列表是渲染可視區域的特性,咱們須要作到如下三點

  1. 需計算頂部和底部不可視區域留白的高度,撐起整個列表高度,使其高度與沒有截斷數據時同樣,這兩個高度分別命名爲 topHeight、bottomHeight
  2. 計算截斷開始位置 start 和結束位置 end,則可視區域的數據爲 list.slice(start,end)
  3. 滾動過程當中需不斷更新 topHeight、bottomHeight、start、end,從而更新可視區域視圖。固然咱們須要對比老舊 start、end 來判斷是否須要更新。

topHeight 的計算比較簡單,就是滾動了多少高度,topHeight=scrollTop。

start 的計算依賴於 topHeight 和每項元素的高度 itemHeight,假設咱們向上移動了兩個列表項,則 start 爲 2,如此,咱們有 start = Math.floor(topHeight / itemHeight)

end 的計算依賴於屏幕的高度能顯示多少個列表項,咱們稱之爲 visibleCount,則有 visibleCount = Math.ceil(clientHeight / itemHeight),向上取整是爲了不計算偏小致使屏幕沒有顯示足夠的內容,則 end = start + visibleCount。 bottomHeight 須要咱們知道整個列表沒有被截斷前的高度,減去其頂部的高度,計算頂部的高度有了 end 就很簡單了,假設咱們的整個列表項的數量爲 totalItem,則 bottomHeight = (totalItem - end - 1) \* itemHeight

會出現的問題:

可是當這樣實現的時候,會發現有兩個問題:

  1. 滾動的時候可視區域的頂部或者底部會出現留白。
  2. 每次滾動到須要把空白處替換成實際的列表項的時候,頁面會出現抖動,這個緣由是每一個列表項高度不一致,要替換的時候,替換的列表項比 itemHeight 大或者小,而且是在可見區域內替換的,瀏覽器就會抖動下,這個解決辦法能夠經過把替換的時機提早,即在咱們不可見的頂部進行替換。

來分析下,對於第一個問題,會出現留白的狀況,那麼咱們能夠在頂部或者底部預留必定的位置,而第二個問題,也是能夠經過在頂部和底部預留必定的空間,因此解決這個問題只要一個方案就能夠解決了,那就是頂部和底部都預留必定的位置。

假設 reserveTop 爲頂部預留的位置數,reserveBottom 爲底部預留的位置數,那麼咱們上面的數據的計算就要從新定義了,具體如何計算,請看下圖。

avatar

reserveTop 和 reserveBottom 儘可能大點(固然也不要太大),或者知道列表項的最高高度爲多少,就按這個最高高度來。當你發現你滾動的時候頂部有留白,就調大 reserveTop 的數值,當你發現滾動的時候底部有留白,那就調大 reserveBottom 的數值。

15.有 100 瓶水,其中有一瓶有毒,小白鼠只要嘗一點帶毒的水 3 天后就會死亡,至少要多少隻小白鼠才能在 3 天內鑑別出哪瓶水有毒?

公司:富途

分類:其它

查看解析

答案

7複製代碼

分析

每一個老鼠只有死或活 2 種狀態,所以每一個老鼠能夠看做一個 bit,取 0 或 1N 個老鼠能夠看做 N 個 bit,能夠表達 2^N 種狀態(其中第 n 個狀態表明第 n 個瓶子有毒)所以全部老鼠能表示的狀態數能大於等於 100 便可。

代碼實現

let n = 1;
while (Math.pow(2, n) < 100) {
  n++;
}
console.log(n);
複製代碼

通俗點的理解:

給 100 個瓶分別標上以下標籤(7 位長度): 0000001 (第 1 瓶) 0000010 (第 2 瓶) 0000011 (第 3 瓶) ...... 1100100 (第 100 瓶)

從編號最後 1 位是 1 的全部的瓶子裏面取出 1 滴混在一塊兒(好比從第一瓶,第三瓶,。。。裏分別取出一滴混在一塊兒)並標上記號爲 1。以此類推,從編號第一位是 1 的全部的瓶子裏面取出 1 滴混在一塊兒並標上記號爲 7。如今獲得有 7 個編號的混合液,小白鼠排排站,分別標上 7,6,。。。1 號,並分別給它們灌上對應號碼的混合液。三天過去了,過來驗屍吧:

從左到右,死了的小白鼠貼上標籤 1,沒死的貼上 0,最後獲得一個序號,把這個序號換成 10 進制的數字,就是有毒的那瓶水的編號。

檢驗一下:假如第一瓶有毒,按照 0000001 (第 1 瓶),說明第 1 號混合液有毒,所以小白鼠的生死符爲 0000001(編號爲 1 的小白鼠掛了),0000001 二進制標籤轉換成十進制=1 號瓶有毒;假如第三瓶有毒,0000011 (第 3 瓶),第 1 號和第 2 號混合液有毒,所以小白鼠的生死符爲 0000011(編號爲 1,2 的鼠兄弟掛了),0000011 二進制標籤轉換成十進制=3 號瓶有毒。

因此結果就是 2^7 = 128 >= 100,至少須要 7 只小白鼠。

16.Promise.allSettled 瞭解嗎?手寫 Promise.allSettled

公司:快手

分類:JavaScript

查看解析

Promise.allSettled(iterable)概念

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {
  console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
複製代碼
  1. Promise.allSettled()  方法接受一組 Promise  實例做爲參數,返回一個新的 Promise 實例。
  2. 只有等到全部這些參數實例都返回結果,無論是 fulfilled  仍是 rejected ,包裝實例纔會結束。
  3. 返回的新 Promise  實例,一旦結束,狀態老是 fulfilled ,不會變成 rejected 。
  4. 新的 Promise  實例給監聽函數傳遞一個數組 results 。該數組的每一個成員都是一個對象,對應傳入 Promise.allSettled的 Promise 實例。每一個對象都有 status 屬性,對應着 fulfilled  和 rejected 。 fulfilled  時,對象有 value  屬性, rejected  時有 reason  屬性,對應兩種狀態的返回值。
  5. 有時候咱們不關心異步操做的結果,只關心這些操做有沒有結束時,這個方法會比較有用。

手寫實現

const formatSettledResult = (success, value) =>
  success
    ? { status: "fulfilled", value }
    : { status: "rejected", reason: value };

Promise.all_settled = function (iterators) {
  const promises = Array.from(iterators);
  const num = promises.length;
  const resultList = new Array(num);
  let resultNum = 0;

  return new Promise((resolve) => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          resultList[index] = formatSettledResult(true, value);
          if (++resultNum === num) {
            resolve(resultList);
          }
        })
        .catch((error) => {
          resultList[index] = formatSettledResult(false, error);
          if (++resultNum === num) {
            resolve(resultList);
          }
        });
    });
  });
};

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
Promise.all_settled([resolved, rejected]).then((results) => {
  console.log(results);
});
複製代碼

17.瀏覽器爲何要阻止跨域請求?如何解決跨域?每次跨域請求都須要到達服務端嗎?

公司:快手

分類:JavaScript

查看解析

1、什麼是跨域

跨域是針對瀏覽器的「同源策略」提出的說法。之因此有「同源策略」這種模式是基於網絡安全方面的考慮。所謂的同源策略關注三點:

  1. 協議 (http:www.baidu.com & https.www.baidu.com http 協議不一樣,跨域)
  2. 域名 (https://www.aliyun.com & https://developer.aliyun.com 域名不一樣,跨域)
  3. 端口 (http://localhost:8080 & http://localhost:8000 端口號不一樣,跨域)

2、哪些網絡資源涉及到跨域

「同源策略」對於跨域網絡資源的設定很是的清晰。

這些場景涉及到跨域禁止操做:

  1. 沒法獲取非同源網頁的 cookie、localstorage 和 indexedDB。
  2. 沒法訪問非同源網頁的 DOM (iframe)。
  3. 沒法向非同源地址發送 AJAX 請求 或 fetch 請求(能夠發送,但瀏覽器拒絕接受響應)。

爲何要阻止跨域呢?上文咱們說過是基於安全策略:好比一個惡意網站的頁面經過 iframe 嵌入了銀行的登陸頁面(兩者不一樣源),若是沒有同源限制,惡意網頁上的 javascript 腳本就能夠在用戶登陸銀行的時候獲取用戶名和密碼。

3、如何解決跨域

針對跨越問題咱們該如何解決,主流的方案有如下:

一、 經過 jsonp 跨域 二、 document.domain + iframe 跨域 三、 location.hash + iframe 四、 window.name + iframe 跨域 五、 postMessage 跨域 六、 跨域資源共享(CORS) 七、 nginx 代理跨域 八、 nodejs 中間件代理跨域 九、 WebSocket 協議跨域

4、關於跨域須要明確的問題

跨域並不是瀏覽器限制了發起跨站請求,而是跨站請求能夠正常發起,可是返回結果被瀏覽器攔截了。

每次需求都會發出,服務器端也會作出響應,只是瀏覽器端在接受響應的時候會基於同源策略進行攔截。

注意:有些瀏覽器不容許從 HTTPS 的域跨域訪問 HTTP,好比 Chrome 和 Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。

18.瀏覽器緩存瞭解嗎?強緩存通常存放在哪裏?計算整個文件獲得 etag 會耗費性能,怎麼解決?若是我不想要使用緩存了,每次都請求最新的,怎麼作?no-store 和 no-cache 的區別是什麼?

公司:快手

分類:網絡&安全

查看解析

1、瀏覽器緩存

瀏覽器緩存主要分爲四個階段:

  1. 強制緩存階段:先在本地查找該資源,若是有發現該資源,並且該資源尚未過時,就使用這一個資源,徹底不會發送 http 請求到服務器。
  2. 協商緩存階段:若是在本地緩存找到對應的資源,可是不知道該資源是否過時或者已通過期,則發一個 http 請求到服務器,而後服務器判斷這個請求,若是請求的資源在服務器上沒有改動過,則返回 304,讓瀏覽器使用本地找到的那個資源。
  3. 啓發式緩存階段:當緩存過時時間的字段一個都沒有的時候,瀏覽器下次並不會直接進入協商階段,而是先進入啓發式緩存階段,它根據響應頭中 2 個時間字段 Date 和 Last-Modified 之間的時間差值,取其值的 10%做爲緩存時間週期。也就是說,當存有 Last-Modified 字段的時候,即便是斷網,且強緩存都失效後,也有必定時間是直接讀取緩存文件的。etag 是沒有這個階段的。
  4. 緩存失敗階段:當服務器發現請求的資源已經修改過,或者這是一個新的請求(再原本沒有找到資源),服務器則返回該資源的數據,而且返回 200, 固然這個是指找到資源的狀況下,若是服務器上沒有這個資源,則返回 404。

2、強緩存通常放在哪裏

強緩存通常存放於 Memory Cache 或者 Disk Cache。

3、計算整個文件獲得 etag 會耗費性能,怎麼解決

etag 能夠經過文件的 Last-Modified 和 content-length 計算。

Nginx官方默認的ETag計算方式是爲"文件最後修改時間16進制-文件長度16進制"。例:ETag: 「59e72c84-2404」

注意:

無論怎麼樣的算法,在服務器端都要進行計算,計算就有開銷,會帶來性能損失。所以爲了榨乾這一點點性能,很多網站徹底把Etag禁用了(好比Yahoo!),這其實不符合HTTP/1.1的規定,由於HTTP/1.1老是鼓勵服務器儘量的開啓Etag。

4、不使用緩存的方式,讓每次請求都是最新的

不使用緩存常見的方法是經過 url 拼接 random 的方式或者設置 Cache-Control 設置 no-cache。

5、no-stroe & no-cache

  • no-store 禁止瀏覽器和中間服務器緩存。每次都從服務器獲取。
    • 注意,no-store 纔是真正的完徹底全的禁止本地緩存。
  • no-cache 每次請求都會驗證該緩存是否過時。能夠在本地緩存,能夠在代理服務器緩存,可是這個緩存要服務器驗證才能夠使用
    • 注意,no-cache 不是不緩存的意思。

19.說一下平時項目是怎麼優化的?優化以後是怎麼度量的?首屏時間是怎麼計算的?

公司:快手

分類:其它

查看解析

針對每一個過程進行優化

網頁從加載到呈現會經歷一系列過程,針對每一個過程進行優化

  • 網絡鏈接
  • 請求優化
  • 響應優化
  • 瀏覽器渲染

image.png

經過 performance.timing API,能夠獲取各個階段的執行時間:

{
  navigationStart: 1578537857229; //上一個文檔卸載(unload)結束時的時間戳
  unloadEventStart: 1578537857497; //表徵了unload事件拋出時的時間戳
  unloadEventEnd: 1578537857497; //表徵了unload事件處理完成時的時間戳
  redirectStart: 0; // 重定向開始時的時間戳
  redirectEnd: 0; //重定向完成時的時間戳
  fetchStart: 1578537857232; //準備好HTTP請求來獲取(fetch)文檔的時間戳
  domainLookupStart: 1578537857232; //域名查詢開始的時間戳
  domainLookupEnd: 1578537857232; //域名查詢結束的時間戳
  connectStart: 1578537857232; //HTTP請求開始向服務器發送時的時間戳
  connectEnd: 1578537857232; //瀏覽器與服務器之間的鏈接創建時的時間戳
  secureConnectionStart: 0; //安全連接的握手時的U時間戳
  requestStart: 1578537857253; //HTTP請求(或從本地緩存讀取)時的時間戳
  responseStart: 1578537857491; //服務器收到(或從本地緩存讀取)第一個字節時的時間戳。
  responseEnd: 1578537857493; //響應結束
  domLoading: 1578537857504; //DOM結構開始解析時的時間戳
  domInteractive: 1578537858118; //DOM結構結束解析、開始加載內嵌資源時的時間戳
  domContentLoadedEventStart: 1578537858118; //DOMContentLoaded 事件開始時間戳
  domContentLoadedEventEnd: 1578537858118; //當全部須要當即執行的腳本已經被執行(不論執行順序)時的時間戳
  domComplete: 1578537858492; //當前文檔解析完成的時間戳
  loadEventStart: 1578537858492; //load事件被髮送時的時間戳
  loadEventEnd: 1578537858494; //當load事件結束時的時間戳
}
複製代碼

1.1 網絡鏈接方面優化

主要是針對重定向、DNS、TCP 鏈接進行優化

  • 避免重定向
  • DNS 查找優化:頁面採用預解析 dns-prefetch ,同時將同類型的資源放到一塊兒,減小 domain  數量也是能夠減小 DNS 查找
  • 使用 CDN(內容分發網絡)
  • HTTP/1.1 版本,客戶端能夠經過 Keep-Alive 選項和服務器創建長鏈接,讓多個資源經過一個 TCP 鏈接傳輸。

1.2 請求方面優化

減小瀏覽器向瀏覽器發送的請求數目以及請求資源的大小是請求優化的核心思想

  • 合理使用文件的壓縮和合並
    • 合理運用瀏覽器對於資源並行加載的特性,在資源的加載的數量和資源的大小之間作一個合理的平衡
    • 在移動端頁面中,將首屏的請求資源控制在 5 個之內,每一個資源在 Gzip 以後的大小控制在 28.5KB 以內,能夠顯著的提高首屏時間。
  • 壓縮圖片,使用雪碧圖,小圖片使用 Base64 內聯
  • 組件延遲加載
  • 給 Cookie 瘦身
    • 靜態資源使用 CDN 等方式放在和當前域不一樣的域上,以免請求靜態資源時攜帶 Cookie
  • 善用 CDN 提高瀏覽器資源加載能力
    • 資源分散到多個不一樣的 CDN 中,最大化的利用瀏覽器的並行加載能力
  • 合理運用緩存策略緩存靜態資源,Ajax 響應等
    • 利用 Manifest + 本地存儲作持久化緩存
    • 將對訪問實時性要求不高的其餘資源,如圖片、廣告腳本等內容存放在 IndexDB 或 WebSQL 中,IndexDB 後 WebSQL 的存儲容量比 LocalStorage 大得多,能夠用來存放這些資源。
    • 使用 localForage 操做持久化緩存
    • 庫文件放入 CDN 或者開啓強緩

1.3 響應優化

  • 優化服務端處理流程,如使用緩存、優化數據庫查詢、減小查詢次數
  • 優化響應資源的大小,如對響應的資源開啓 Gzip 壓縮等。

1.3.1 頁面加載的核心指標

  • TTFB
    • 首個字節
  • FP
    • 首次繪製,只有 div 跟節點,對應 vue 生命週期的 created
  • FCP
    • 首次有內容的繪製,頁面的基本框架,可是沒有數據內容,對應 vue 生命週期的 mounted
  • FMP
    • 首次有意義的繪製,包含全部元素和數據內容,對應 vue 生命週期的 updated
  • TTI
    • 首次能交互時間
  • Long Task
    • >=50ms 的任務
  • SSR&CSR
    • 服務端渲染和客戶端渲染
  • Isomorphic javascript
    • 同構化

1.4 瀏覽器首屏渲染優化

1.4.1 首屏時間:

指用戶打開網站開始,到瀏覽器首屏內容渲染完成的時間。對於用戶體驗來講,首屏時間是用戶對一個網站的重要體驗因素。一般一個網站,若是首屏時間在 5 秒之內是比較優秀的,10 秒之內是能夠接受的,10 秒以上就不可容忍了。超過 10 秒的首屏時間用戶會選擇刷新頁面或馬上離開。

1.4.2首屏時間計算:

  • 首屏模塊標籤標記法

一般適用於首屏內容不須要經過拉取數據才能生存以及頁面不考慮圖片等資源加載的狀況,咱們會在 HTML 文檔中對應首屏內容的標籤結束位置,使用內聯的 JavaScript 代碼記錄當前時間戳。以下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>首屏</title>
    <script type="text/javascript"> window.pageStartTime = Date.now(); </script>
    <link rel="stylesheet" href="common.css" />
    <link rel="stylesheet" href="page.css" />
  </head>
  <body>
    <!-- 首屏可見模塊1 -->
    <div class="module-1"></div>
    <!-- 首屏可見模塊2 -->
    <div class="module-2"></div>
    <script type="text/javascript"> window.firstScreen = Date.now(); </script>
    <!-- 首屏不可見模塊3 -->
    <div class="module-3"></div>
    <!-- 首屏不可見模塊4 -->
    <div class="module-4"></div>
  </body>
</html>
複製代碼

此時首屏時間等於  firstScreen - performance.timing.navigationStart

事實上首屏模塊標籤標記法   在業務中的狀況比較少,大多數頁面都須要經過接口拉取數據才能完整展現

  • 統計首屏內加載最慢的圖片的時間:

一般咱們首屏內容加載最慢的就是圖片資源,所以咱們會把首屏內加載最慢的圖片的時間當作首屏的時間。

DOM 樹構建完成後將會去遍歷首屏內的全部圖片標籤,而且監聽全部圖片標籤 onload 事件,最終遍歷圖片標籤的加載時間的最大值,並用這個最大值減去 navigationStart 便可得到近似的首屏時間。

此時首屏時間等於 加載最慢的圖片的時間點 - performance.timing.navigationStart

  • 自定義首屏內容計算法

因爲統計首屏內圖片完成加載的時間比較複雜。所以咱們在業務中一般會經過自定義模塊內容,來簡化計算首屏時間。以下面的作法:

  • 忽略圖片等資源加載狀況,只考慮頁面主要 DOM
  • 只考慮首屏的主要模塊,而不是嚴格意義首屏線以上的全部內容

1.4.3首屏優化方案:

  • 頁面直出:骨架屏或者 SSR
  • 首幀渲染優化
  • 資源動態加載
  • 瀏覽器緩存
  • 優化 JavaScript 腳本執行時間
  • 減小重排重繪
  • 硬件加速提高動畫性能等頁面渲染方面的優化方案

1.5瀏覽器渲染優化

  • 優化 JavaScript 腳本執行時間
  • 減小重排重繪
  • 硬件加速提高動畫性能等

20.怎麼計算組件在視口內出現了幾回?IntersectionObserver 怎麼使用的?怎麼知道一個 DOM 節點出如今視口內?

公司:快手

分類:JavaScript

查看解析

1、監聽 Scroll

要了解某個元素是否進入了"視口"(viewport),即用戶能不能看到它,傳統的實現方法是,監聽到scroll事件後,調用目標元素的getBoundingClientRect()方法,獲得它對應於視口左上角的座標,再判斷是否在視口以內。而後聲明一個全局變量,每出現一次就加一,就能夠得出在視口出現了幾回。這種方法的缺點是,因爲scroll事件密集發生,計算量很大,容易形成性能問題。

因而便有了 IntersectionObserver API

2、IntersectionObserver

2.1 API

var io = new IntersectionObserver(callback, option);
複製代碼

上面代碼中,IntersectionObserver是瀏覽器原生提供的構造函數,接受兩個參數:callback是可見性變化時的回調函數,option是配置對象(該參數可選)。

構造函數的返回值是一個觀察器實例。實例的observe方法能夠指定觀察哪一個 DOM 節點。

// 開始觀察
io.observe(document.getElementById("example"));

// 中止觀察
io.unobserve(element);

// 關閉觀察器
io.disconnect();
複製代碼

上面代碼中,observe的參數是一個 DOM 節點對象。若是要觀察多個節點,就要屢次調用這個方法。

io.observe(elementA);
io.observe(elementB);
複製代碼

2.2 Callback 參數

目標元素的可見性變化時,就會調用觀察器的回調函數callback

callback通常會觸發兩次。一次是目標元素剛剛進入視口(開始可見),另外一次是徹底離開視口(開始不可見)。

var io = new IntersectionObserver((entries) => {
  console.log(entries);
});
複製代碼

callback函數的參數(entries)是一個數組,每一個成員都是一個IntersectionObserverEntry對象。若是同時有兩個被觀察的對象的可見性發生變化,entries數組就會有兩個成員。

IntersectionObserverEntry對象提供目標元素的信息,一共有六個屬性。

{
  time: 3893.92,
  rootBounds: ClientRect {
    bottom: 920,
    height: 1024,
    left: 0,
    right: 1024,
    top: 0,
    width: 920
  },
  boundingClientRect: ClientRect {
     // ...
  },
  intersectionRect: ClientRect {
    // ...
  },
  intersectionRatio: 0.54,
  target: element
}
複製代碼

每一個屬性的含義以下。

  • time:可見性發生變化的時間,是一個高精度時間戳,單位爲毫秒
  • target:被觀察的目標元素,是一個 DOM 節點對象
  • rootBounds:根元素的矩形區域的信息,getBoundingClientRect()方法的返回值,若是沒有根元素(即直接相對於視口滾動),則返回null
  • boundingClientRect:目標元素的矩形區域的信息
  • intersectionRect:目標元素與視口(或根元素)的交叉區域的信息
  • intersectionRatio:目標元素的可見比例,即intersectionRectboundingClientRect的比例,徹底可見時爲1,徹底不可見時小於等於0

2.3 Option 對象

IntersectionObserver構造函數的第二個參數是一個配置對象。它能夠設置如下屬性。

2.3.1 threshold 屬性:

threshold屬性決定了何時觸發回調函數。它是一個數組,每一個成員都是一個門檻值,默認爲[0],即交叉比例(intersectionRatio)達到0時觸發回調函數。

new IntersectionObserver(
  (entries) => {
    /* ... */
  },
  {
    threshold: [0, 0.25, 0.5, 0.75, 1],
  }
);
複製代碼

用戶能夠自定義這個數組。好比,[0, 0.25, 0.5, 0.75, 1]就表示當目標元素 0%、25%、50%、75%、100% 可見時,會觸發回調函數。

2.3.2 root 屬性、rootMargin 屬性:

不少時候,目標元素不只會隨着窗口滾動,還會在容器裏面滾動(好比在iframe窗口裏滾動)。容器內滾動也會影響目標元素的可見性。

IntersectionObserver API 支持容器內滾動。root屬性指定目標元素所在的容器節點(即根元素)。注意,容器元素必須是目標元素的祖先節點。

var opts = {
  root: document.querySelector(".container"),
  rootMargin: "500px 0px",
};

var observer = new IntersectionObserver(callback, opts);
複製代碼

上面代碼中,除了root屬性,還有rootMargin屬性。後者定義根元素的margin,用來擴展或縮小rootBounds這個矩形的大小,從而影響intersectionRect交叉區域的大小。它使用 CSS 的定義方法,好比10px 20px 30px 40px,表示 top、right、bottom 和 left 四個方向的值。

這樣設置之後,無論是窗口滾動或者容器內滾動,只要目標元素可見性變化,都會觸發觀察器。

---------------------------------- 一條講武德的分割線 ------------------------------

因掘金髮文的字數限制,剩下的答案你們掃碼便可查看。

21.versions 是一個項目的版本號列表,因多人維護,不規則,動手實現一個版本號處理函數

var versions = ["1.45.0", "1.5", "6", "3.3.3.3.3.3.3"];
// 要求從小到大排序,注意'1.45'比'1.5'大
function sortVersion(versions) {
  // TODO
}
// => ['1.5','1.45.0','3.3.3.3.3.3','6']
複製代碼

公司:頭條

分類:JavaScript

22.什麼是微服務,微服務跟單體應用的區別是啥,用微服務有啥好處?

分類:Node

23.動手實現一個 repeat 方法

function repeat(func, times, wait) {
  // TODO
}
const repeatFunc = repeat(alert, 4, 3000);
// 調用這個 repeatFunc ("hellworld"),會alert4次 helloworld, 每次間隔3秒
複製代碼

公司:頭條

分類:JavaScript

24.請列出目前主流的 JavaScript 模塊化實現的技術有哪些?說出它們的區別?

公司:玄武科技

分類:JavaScript

25.請描述下 JavaScript 中 Scope、Closure、Prototype 概念,並說明 JavaScript 封裝、繼承實現原理。

公司:玄武科技

分類:JavaScrip

相關文章
相關標籤/搜索