React 中的 JS 報錯及容錯方案

前言

導致白屏的原因大概有兩種,一爲資源的加載,二爲 JS 執行出錯

本文就 JS 中執行的報錯,會比較容易造成"白屏"場景,和能解決這些問題的一些方法,作出一個彙總

常見的錯誤

SyntaxError

SyntaxError(語法錯誤)對象代表嘗試解析不符合語法的代碼的錯誤。當 Javascript 引擎解析代碼時,遇到了不符合語法規範的標記(token)或標記順序,則會拋出 SyntaxError

這裏陳列下 SyntaxError 的常見錯誤

保留字錯誤

SyntaxError: "x" is a reserved identifier (Firefox)
SyntaxError: Unexpected reserved word (Chrome)

如在控制檯執行下方代碼,則會上述錯誤出現

const enum = 1

enum嚴格模式和非嚴格模式下都是保留字。

而以下標記符只會在嚴格模式下才作爲保留字:

  • implements

  • interface

  • let

  • package

  • private

  • protected

  • public

  • static

例如:


const implements = 1 // ✅


"use strict";
const implements = 1; // caught SyntaxError: Unexpected strict mode reserved word

命名錯誤

一個 JavaScript 標識符必須以字母開頭,下劃線(_)或美元符號($)。他們不能以數字開頭。只有後續字符可以是數字(0-9)。

var 1life = 'foo';
// SyntaxError: identifier starts immediately after numeric literal

var foo = 1life;
// SyntaxError: identifier starts immediately after numeric literal

錯誤的標點

在代碼中有非法的或者不期望出現的標記符號出現在不該出現的位置。

“This looks like a string”;
// SyntaxError: illegal character

42 – 13;
// SyntaxError: illegal character

代碼裏使用了中文的引號和橫槓,造成了解析錯誤,這裏就體現了編輯器的重要性

JSON 解析

JSON.parse('[1, 2, 3, 4, ]');
JSON.parse('{"foo" : 1, }');
// SyntaxError JSON.parse: unexpected character
// at line 1 column 14 of the JSON data

json 解析失敗的類型有很多,這裏就不贅述了,我們在進行 json 解析的時候,一定要加上 try...catch 語句來避免錯誤

分號問題

通常情況下,這個錯誤只是另一個錯誤一個導致的,如不正確轉義字符串,使用 var 的錯誤

const foo = 'Tom's bar';
// SyntaxError: missing ; before statement

通過其他方案聲明:

var foo = "Tom's bar";
var foo = 'Tom\'s bar';
var foo = `Tom's bar`; // 推薦這種方案

使用 var 錯誤

var array = [];
var array[0] = "there"; // SyntaxError missing ; before 

類似當前錯誤的還有很多,比如:

  • SyntaxError: missing ) after argument list
  • SyntaxError: missing ) after condition
  • SyntaxError: missing } after function body
  • SyntaxError: missing } after property list

這些都是語法的錯誤,在編輯器/IDE使用時期都能解析,但是在某些比較古老的框架下,
編輯器可能並不能識別出來他的語法,這便是此錯誤經常出現的場景

小結

SyntaxError 屬於運行時代碼錯誤,通常也是新手開發者容易犯得的錯誤 ,在 dev 時期就可以發現,不然無法通過編譯,是屬於比較容易發現的問題

TypeError

TypeError(類型錯誤)對象通常(但並不只是)用來表示值的類型非預期類型時發生的錯誤。

以下情況會拋出 TypeError

  • 傳遞給運算符的操作數或傳遞給函數的參數與預期的類型不兼容;
  • 嘗試修改無法更改的值;
  • 嘗試以不適當的方法使用一個值。

不可迭代屬性

當使用 for...of ,右側的值不是一個可迭代值時,或者作爲數組解構賦值時,會報此問題

例如:

const myobj = { arrayOrObjProp1: {}, arrayOrObjProp2: [42] };

const {
  arrayOrObjProp1: [value1],
  arrayOrObjProp2: [value2],
} = myobj; // TypeError: object is not iterable


const obj = { France: "Paris", England: "London" };
for (const p of obj) {
  // …
} // TypeError: obj is not iterable

JS 中有內置的可迭代對象,如: StringArrayTypedArrayMapSet 以及 Intl.Segments (en-US),因爲它們的每個 prototype 對象都實現了 @@iterator 方法。

Object 是不可迭代的,除非它們實現了迭代協議。

簡單來說,對象中缺少一個可迭代屬性: next 函數

將上述 obj 改造:

const obj = {
  France: "Paris", England: "London",
  [Symbol.iterator]() {
    // 用原生的空數組迭代器來兼容
    return [][Symbol.iterator]();
  },
};
for (const p of obj) {
  // …
}

如此可不報錯,但是也不會進入循環中

點此查看什麼是迭代協議

空值問題

null.foo;
// 錯誤類型:null 沒有這個屬性

undefined.bar;
// 錯誤類型:undefined 沒有這個屬性

const foo = undefined;
foo.substring(1); // TypeError: foo is undefined

雖然看起來簡單,但是他是出現白屏最爲頻繁的報錯原因之一

在以前我們通常這樣解決問題:

var value = null;

value && value.foo;

現在我們可以使用 可選鏈 Optional chaining 來解決這個問題

var value = null;

value?.foo;

// 但是他也不能用來賦值:
value?.foo = 1

可選鏈語法:

obj.val?.prop
obj.val?.[expr]
obj.func?.(args)

錯誤的函數執行

錯誤的函數名稱:

var x = document.getElementByID("foo");
// TypeError: document.getElementByID is not a function

var x = document.getElementById("foo"); // 正確的函數

不存在的函數:

var obj = { a: 13, b: 37, c: 42 };

obj.map(function(num) {
  return num * 2;
});
// TypeError: obj.map is not a function

in 的錯誤場景

在判斷一個對象中是否存在某個值時,比較常用的是一種方法是使用 in 來判斷:

var foo = { baz: "bar" };

if('baz' in foo){
  // operation 
}

因爲不能確定 foo['baz'] 的具體值,所以這種方案也是不錯的,但是當 foo 的類型也不能確認的時候就會容易出現報錯了

var foo = null;
"bar" in foo;
// TypeError: invalid 'in' operand "foo"

"Hello" in "Hello World";
// TypeError: invalid 'in' operand "Hello World"

字符串和空值不適合使用此語法

另外需要注意的是,在數組中需要小心使用

const number = [2, 3, 4, 5];

3 in number // 返回 true.
2 in number // 返回 true.

5 in number // 返回 false,因爲 5 不是數組上現有的索引,而是一個值;

小結

因爲錯誤是跟隨着不同的值類型,而數據的接收/轉變我們並不能做到 100% 的把控。
它是我們平時線上報錯最頻繁的一種類型,也是最容易造成頁面白屏的。需要保持 120% 的小心。

RangeError

RangeError 對象表示一個特定值不在所允許的範圍或者集合中的錯誤。

在以下的情況中,可能會遇到這個問題:

這裏舉幾個例子:

String.fromCodePoint("_"); // RangeError

new Array(-1); // RangeError

new Date("2014-25-23").toISOString(); // RangeError

(2.34).toFixed(-100); // RangeError

(42).toString(1);

const b = BigInt(NaN);
// RangeError: NaN cannot be converted to a BigInt because it is not an integer

總的來說 RangeError 都是因爲傳入了不正確的值而導致的,這種情況發生的概率較小,部分數字都是自己可以手動控制或者寫死在代碼裏的
除非是定製化很高的情況,比如低代碼,讓用戶隨意輸入的時候,在使用的時候,最好先做出判斷,或者加上 try...catch

ReferenceError

ReferenceError(引用錯誤)對象代表當一個不存在(或尚未初始化)的變量被引用時發生的錯誤。

這種報錯的場景大多處於嚴格模式下,在正常情況下 "變量未定義" 這種報錯出現的情況較多

foo.substring(1); // ReferenceError: foo is not defined

如上,foo 未定義即直接使用,則就會出現報錯

還有一類報錯是賦值的問題,比如上方講過的可選鏈功能,他是不能賦值的:

foo?.bar = 123

這一類在編碼因爲容易分析,一般在編輯器中就能容易發現,所以並不會帶來很多困擾。

其他

InternalError 對象表示出現在 JavaScript 引擎內部的錯誤。尚未成爲任何規範的一部分,所以我們可以忽略。


EvalError 代表了一個關於 eval() 全局函數的錯誤。

他不在當前的 ECMAScript 規範中使用,因此不會被運行時拋出。但是對象本身仍然與規範的早期版本向後兼容。


URIError 對象用來表示以一種錯誤的方式使用全局 URI 處理函數而產生的錯誤。

例如:

decodeURIComponent('%')
// caught URIError: URI malformed

decodeURI("%")     
// Uncaught URIError: URI malformed at decodeURI

所以使用 decodeURIComponent 函數時,需要加上 try...catch 來保持正確性

另類錯誤

unhandledrejection

Promise 被 reject 且沒有 reject 處理器的時候,會觸發 unhandledrejection 事件;
這個時候,就會報一個錯誤:unhanled rejection;沒有堆棧信息,只能依靠行爲軌跡來定位錯誤發生的時機。

window.addEventListener('unhandledrejection', event =>
{
    console.log('unhandledrejection: ', event.reason); // 打印
});

let p = Promise.reject("oops");

// 打印 unhandledrejection:  oops
// caught (in promise) oops

手動拋出錯誤

我們在書第三方庫的時候,可以手動拋出錯誤。但是throw error會阻斷程序運行,請謹慎使用。

throw new Error("出錯了!"); // caught Error: 出錯了!
throw new RangeError("出錯了,變量超出有效範圍!");
throw new TypeError("出錯了,變量類型無效!");

同樣的,此種方案我們可以使用在 Promisethen 中:

// 模擬一個接口的返回
Promise.resolve({code: 3000, message: '這是一個報錯!'}).then(res => {
  if (res.code !== 200) {
    throw new Error(`code 3000: ${res.message}`)
  }
  console.log(res); // 這裏可以看做是執行正常操作, 拋出錯誤時, 此處就不會執行了
}).catch(err => {
  alert(err.message)
});

catch 中我們可以通過 name 來判斷不同的 Error:

try {
  throw new TypeError(`This is an Error`)
} catch (e) {
  console.log(e.name); // TypeError
}

再加上自定義的 Error,我們就可以製作更加自由的報錯信息:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

try {
  throw new ValidationError(`This is an Error`)
} catch (e) {
  console.log(e.name);
  // 'ValidationError'
  if (e instanceof ValidationError) {
    alert("Invalid data: " + e.message); // Invalid data: This is an Error
  }
}

Error 的基礎上我們還可以做更深入的繼承,來製作更多的自定義 Error

報錯在 react 中的影響

react 報錯按照位置,我將他分成兩類,一類是渲染報錯,另一類是執行報錯;
渲染即 render 函數中的視圖渲染報錯,另一個則是執行函數報錯;

函數的執行報錯,是不會影響視圖的渲染的,即白屏,但是他會有一些不良影響,如

  • 代碼執行暫停,部分邏輯未執行,未能閉環整體邏輯,如點擊按鈕一直卡在 loading
  • 數據的渲染出現異常,兩邊數據對不上

在視圖渲染中(包括函數的 return) ,觸發 JS 錯誤,都會渲染問題

那爲什麼整個頁面都會白屏呢 ?

原因是自 React 16 起,任何未被錯誤邊界捕獲的錯誤將會導致整個 React 組件樹被卸載。

錯誤邊界

在 react 中存在此生命週期 componentDidCatch,他會在一個子組件拋出錯誤後被調用。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  // 最新的官方推薦, 通過此 api 獲取是否觸發錯誤
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  // 舊方案是在此處 setState
  componentDidCatch(error, info) {
    // Example "componentStack":
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logComponentStackToMyService(info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}
<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <Profile />
</ErrorBoundary>

這是來自官網的一個簡單例子,可以覆蓋子組件出錯的情況,避免本身組件或兄弟組件收到波及,而錯誤邊界組件的粒度需要開發者本身來界定

降級和熔斷

在官方的文檔中他更加推薦此組件 react-error-boundary,它有着更加豐富的使用:

他可以簡單的顯示錯誤:

function Fallback({ error }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{ color: "red" }}>{error.message}</pre>
    </div>
  );
}

<ErrorBoundary
  FallbackComponent={Fallback}
>
  <ExampleApplication />
</ErrorBoundary>;

image

也可以使用重置方案:

function Fallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre style={{ color: "red" }}>{error.message}</pre>
      <button onclick={resetErrorBoundary}></button>
    </div>
  );
}

image

通過此方法重置組件,避免了刷新頁面,對於用戶來說更加友好

更多使用,可以查看此處博客

總結

JS中有很多報錯,但是編輯器/編譯,已經幫助我們過濾了一大部分的錯誤,但是仍然會有部分報錯會在特殊條件下出現
所以一方面需要充分的測試,如最大值/最小值/特殊值等等,另一方面就是需要積累經驗,一些寫法就是容易出現問題,可以通過 codeReview 來預防部分問題
但最終要堅守軟件開發的 不信任原則,保持 overly pessimistic (過於悲觀),把和程序有關的一切請求、服務、接口、返回值、機器、框架、中間件等等都當做不可信的,步步爲營、處處設防。

引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章