Code Monkey home page Code Monkey logo

comments's People

Contributors

ichengbo avatar

Watchers

 avatar  avatar

comments's Issues

🤔 普通函数与箭头函数的区别是什么? | 解柒工作室

🤔 普通函数与箭头函数的区别 | 解柒工作室

前言

在 JavaScript 中,我们可以有多种方式定义函数,如:函数声明、函数表达式和箭头函数

// 函数声明
function normalFn() {
    return 'normalFn';
}
// 函数表达式
const normalFn = function() {
    return 'normalFn';
}
// 箭头函数
const arrowFn = () => {
    return 'arrowFn';
}

其中,箭头函数是在 ES2015(ES6) 标准中新增的,其语法与 ES6 之前的函数声明及函数表达式两种定义方式不同。本文中,将函数声明和函数表达式两种定义方式归为普通函数。

那么,普通函数和箭头函数有什么区别呢?🤔️

1. this 指向

在 JavaScript 中,this 的指向是个基础且重要的知识点。

1.1 普通函数

在普通函数中,this 的指向(执行上下文)是动态的,其值取决于函数是如何被调用的,通常有以下 4 种调用方式:

  • 1)直接调用时,其指向为全局对象(严格模式下为 undefined)
function fnc() {
    console.log(this);
}

fnc(); // 全局对象(global 或 window)
  • 2)方法调用时,其指向为调用该方法的对象
var obj = {
    fnc1: function(){
        console.log(this === obj);
    }
}
obj.fnc2 = function() {
    console.log(this === obj);
}
function fnc3() {
    console.log(this === obj);
}
obj.fnc3 = fnc3;
obj.fnc1(); // true
obj.fnc2(); // true
obj.fnc3(); // true
  • 3)new 调用时,其指向为新创建的实例对象
function fnc() {
  console.log(this);
}

new fnc(); // fnc 的实例 fnc {}
  • 4)call、apply、bind 调用时,其指向为三种方法的第一个参数
function fnc() {
  console.log(this);
}

const ctx = { value: 'a' };

fnc.call(ctx);      // { value: 'a' }
fnc.apply(ctx);     // { value: 'a' }
fnc.bind(ctx)();    // { value: 'a' }

在旧版的 JavaScript 中,经常使用 bind 显式的设置 this 的指向,这种模式通常可以在 ES6 出现之前的某些早期版本的框架(如 React)中找到。而箭头函数的出现则提供了一种更便捷的方式解决此问题。

1.2 箭头函数

无论如何执行或在何处执行,箭头函数内部的 this 值始终等于外部函数的值,即箭头函数不会改变 this 的指向,

const obj = {
  fnc(arr) {
    console.log(this); // obj
    const cb = () => {
      console.log(this); // obj
    };
    arr.forEach(cb);
  }
};

obj.fnc([1, 2, 3]); 

注意:由于箭头函数没有自己的 this 指针,通过 call() 、 apply() 和 bind() 方法调用时,只能传递参数,而不能绑定 this,他们的第一个参数会被忽略。如下:(例子来源于 MDN

var adder = {
  base : 1,

  add : function(a) {
    var f = v => v + this.base;
    return f(a);
  },

  addThruCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base : 2
    };

    return f.call(b, a);
  }
};

console.log(adder.add(1));         // 输出 2
console.log(adder.addThruCall(1)); // 仍然输出 2

2. 构造函数

在 JavaScript 中,函数和类的继承是通过 prototype 属性实现的,且 prototype 拥有属性 constructor 指向构造函数,如下:

function fnc() {}
console.lof(fnc.prototype) // {constructor: ƒ}

而采用箭头函数定义函数时,其是没有 prototype 属性的,也就无法指向构造函数。

const arrowFnc = () => {}

console.log(arrowFnc.prototype) // undefined

针对普通函数与箭头函数在构造函数上的区别,可以引出一个问题 -- 箭头函数可以通过 new 进行实例化吗?

const arrowFnc = () => {}
const arrowIns = new arrowFnc() // Uncaught TypeError: arrowFnc is not a constructor

答案是【不可以】,那么为什么呢?🤔️

  • 没有自己的 this,也就意味着无法调用 apply、call 等
  • 没有 prototype 属性,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 _proto_
function newOperator(Con, ...args) {
  let obj = {};
  Object.setPrototypeOf(obj, Con.prototype); // 相当于 obj.__proto__ = Con.prototype
  let result = Con.apply(obj, args);
  return result instanceof Object ? result : obj;
}

更具体的原因是,JavaScript函数两个内部方法: [[Call]] 和 [[Construct]],当函数被直接调用时执行的是 [[Call]] 方法,即直接执行函数体,而 new 调用时是执行的 [[Construct]]方法。箭头函数没有 [[Construct]]方法,因此不能被用作构造函数进行实例化。

3. 作为方法属性

'use strict';
var obj = {
  i: 10,
  b: () => console.log(this.i, this),
  c: function() {
    console.log(this.i, this)
  }
}
obj.b(); // undefined, Window{...}
obj.c(); // 10, Object {...}

可以看到,箭头函数是没有 this 绑定的,其指向始终与上一级保持一致。

上文中提到,当构造函数或类被 new 调用时,其 this 指向为新创建的实例对象,需要注意的是,这里的 this 是 constructor 中的 this,而不是该函数或类的任意地方。如下所示:

class Person {
  constructor(name) {
    this.name = name;
  }

  getName() {
    console.log(this.name, this);
  }
}

const p = new Person('Tom');

p.getName();                // Tom
setTimeout(p.getName, 100); // undefined, Window{...}

为了避免这种错误,我们通常需要在 constructor 中绑定 this,如下所示:

class Person {
  constructor(name) {
    this.name = name;
    this.getName = this.getName.bind(this);
  }

  getName() {
    console.log(this.name);
  }
}
const p = new Person('Tom');
setTimeout(p.getName, 100); // Tom

这种写法,很容易在 React 中发现,其实也是为了填补 JavaScript 的坑 😂。当然,也可以使用箭头函数来避免这种错误,并简化写法,如下:

class Person {
  constructor(name) {
    this.name = name;
  }

  getName = () => {
    console.log(this.name);
  }
}
const p = new Person('Tom');
setTimeout(p.getName, 100); // Tom

在使用箭头函数时,this 是具有词法约束的,也就是说箭头函数会自动将 this 绑定到定义它的上下文。

4. 参数

普通函数与箭头函数在参数上区别主要在于,箭头函数不绑定 arguments 对象。

const fn = () => arguments[0];
fn(1); // Uncaught ReferenceError: arguments is not defined

当我们需要使用参数时,可以考虑使用剩余参数,如下:

const fn = (...args) => args[0];
fn(1, 2); // 1

另外,当函数参数个数为 1 时,箭头函数可以省略括号,进行缩写,如下所示:

const fn = x => x * x;

5. 返回值

在处理函数的返回值时,相比于普通函数,箭头函数可以隐式返回。

const sum = (a, b) => {
  return a + b
}
const sum = (a, b) => (a + b);

隐式返回通常会创建一个单行操作用于 map、filter 等操作,注意:如果不能将函数主题编写为单行代码的话,则必须使用普通的函数体语法,即不能省略花括号和 return。

[1,2,3].map(i => i * 2); // [2,4,6]
[1,2,3].filter(i => i != 2); // [1,3]

总结

本文主要介绍了普通函数与箭头函数的区别,相对于普通函数来说,ES6 箭头函数的主要区别如下:

  • 箭头函数不绑定 arguments,可以使用 ...args 代替;
  • 箭头函数可以进行隐式返回;
  • 箭头函数内的 this 是词法绑定的,与外层函数保持一致;
  • 箭头函数没有 prototype 属性,不能进行 new 实例化,亦不能通过 call、apply 等绑定 this;
  • 在定义类的方法时,箭头函数不需要在 constructor 中绑定 this

参考文章

React-Native 全方位异常监控 | 解柒工作室

React-Native 全方位异常监控 | 解柒工作室

header-img.png

最近在做 RN 应用线上错误监控的需求,在此记录下常用方案。

0. 开始

React Native 在架构上整体可分为三块:Native、JavaScript 和 Bridge。其中,Native 管理 UI 更新及交互,JavaScript 调用 Native 能力实现业务功能,Bridge 负责在二者之间传递消息。

rn-architecture.png

最上层提供类 React 支持,运行在 JavaScriptCore 提供的 JavaScript 运行时环境中,Bridge 层将 JavaScript 与 Native 世界连接起来

本文从以下三个角度,分别介绍如何捕获 RN 应用中未被处理的异常:

  • Native 异常捕获;
  • JS 异常捕获;
  • React 异常捕获;

1. Native 异常捕获

Native 有较多成熟的方案,如友盟、Bugly、网易云捕和 crashlytics 等,这些平台不仅提供异常捕获能力,还相应的有上报、统计、预警等能力。本文不对以上平台异常捕获实现方式进行分析,而是通过分析 react-native-exception-handler 了解 Native 端异常捕获的实现原理。 react-native-exception-handler 实现了 setNativeExceptionHandle 用于设置 Native 监测到异常时的回调函数,如下所示:

export const setNativeExceptionHandler = (customErrorHandler = noop, forceApplicationToQuit = true, executeDefaultHandler = false) => {
  if (typeof customErrorHandler !== "function" || typeof forceApplicationToQuit !== "boolean") {
    console.log("setNativeExceptionHandler is called with wrong argument types.. first argument should be callback function and second argument is optional should be a boolean");
    console.log("Not setting the native handler .. please fix setNativeExceptionHandler call");
    return;
  }
  if (Platform.OS === "ios") {
    ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, customErrorHandler);
  } else {
    ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, forceApplicationToQuit, customErrorHandler);
  }
};

1.1 Android 异常捕获

Android 提供了一个异常捕获接口 Thread.UncaughtExceptionHandler 用于捕获未被处理的异常。react-native-exception-handler 亦是基于此实现对 Android 端异常捕获的,其主要代码及分析如下所示:

@ReactMethod
public void setHandlerforNativeException(
        final boolean executeOriginalUncaughtExceptionHandler,
        final boolean forceToQuit,
        Callback customHandler) {

    callbackHolder = customHandler;
    // 获取原有的异常处理器
    originalHandler = Thread.getDefaultUncaughtExceptionHandler();
    // 实例化异常处理器后,利用 setDefaultUncaughtExceptionHandler 重置异常处理器
    Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {

        // 重写 uncaughtException 方法,当程序中有未捕获的异常时,会调用该方法
        @Override
        public void uncaughtException(Thread thread, Throwable throwable) {

          String stackTraceString = Log.getStackTraceString(throwable);
          // 执行传入 JS 处理函数
          callbackHolder.invoke(stackTraceString);

          // 用于兼容自定义 Native Exception handler 的情况,即通过 MainApplication.java 中 实例化 NativeExceptionHandlerIfc 并重写其 handleNativeException 方法。
          if (nativeExceptionHandler != null) {
              nativeExceptionHandler.handleNativeException(thread, throwable, originalHandler);
          } else {
              // 获取 activity 并展示错误信息(一个弹窗,并提供重启和退出按钮)
              activity = getCurrentActivity();

              Intent i = new Intent();
              i.setClass(activity, errorIntentTargetClass);
              i.putExtra("stack_trace_string",stackTraceString);
              i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

              activity.startActivity(i);
              activity.finish();

              // 允许执行且已存在异常处理函数时,执行原异常处理函数
              if (executeOriginalUncaughtExceptionHandler && originalHandler != null) {
                  originalHandler.uncaughtException(thread, throwable);
              }

              // 设置出现异常情况直接退出
              if (forceToQuit) {
                  System.exit(0);
              }
          }
        }
    });
}

可以看到,主要做了四件事:

  • 实例化 new Thread.UncaughtExceptionHandler(),并重写其 uncaughtException 方法;
  • uncaughtException 方法中执行 JS 回调函数;
  • 兼容自定义 Native Exception handler 的情况;
  • 调用 Thread.setDefaultUncaughtExceptionHandler 重置异常处理器;

1.2 iOS 异常捕获

iOS 通常利用 NSSetUncaughtExceptionHandler 设置全部的异常处理器,当异常情况发生时,会执行其设置的异常处理器。react-native-exception-handler 也是基于此实现对 iOS 端异常的捕获,如下所示:

// ====================================
// REACT NATIVE MODULE EXPOSED METHODS
// ====================================

RCT_EXPORT_MODULE();

// METHOD TO INITIALIZE THE EXCEPTION HANDLER AND SET THE JS CALLBACK BLOCK
RCT_EXPORT_METHOD(setHandlerforNativeException:(BOOL)callPreviouslyDefinedHandler withCallback: (RCTResponseSenderBlock)callback)
{
    // 1.设置异常处理函数用于执行 JS 回调;
    jsErrorCallbackBlock = ^(NSException *exception, NSString *readeableException){
        callback(@[readeableException]);
    };

    // 2.获取已存在的 native 异常处理器;
    previousNativeErrorCallbackBlock = NSGetUncaughtExceptionHandler();
    callPreviousNativeErrorCallbackBlock = callPreviouslyDefinedHandler;
    
    // 3. 利用 NSSetUncaughtExceptionHandler 自定义异常处理器 HandleException;
    NSSetUncaughtExceptionHandler(&HandleException);
    signal(SIGABRT, SignalHandler);
    signal(SIGILL, SignalHandler);
    signal(SIGSEGV, SignalHandler);
    signal(SIGFPE, SignalHandler);
    signal(SIGBUS, SignalHandler);
    //signal(SIGPIPE, SignalHandler);
    //Removing SIGPIPE as per https://github.com/master-atul/react-native-exception-handler/issues/32
    NSLog(@"REGISTERED RN EXCEPTION HANDLER");
}

上述代码主要做了三件事:

  • 设置异常处理函数用于执行 JS 回调;
  • 获取已存在的 native 异常处理器;
  • 利用 NSSetUncaughtExceptionHandler 自定义异常处理器 HandleException;

接下来,看下具体的 handleException 又做了些什么呢?

// ================================================================
// ACTUAL CUSTOM HANDLER called by the EXCEPTION AND SIGNAL HANDLER
// WHICH KEEPS THE APP RUNNING ON EXCEPTION
// ================================================================

- (void)handleException:(NSException *)exception
{
    NSString * readeableError = [NSString stringWithFormat:NSLocalizedString(@"%@\n%@", nil),
                                 [exception reason],
                                 [[exception userInfo] objectForKey:RNUncaughtExceptionHandlerAddressesKey]];
    dismissApp = false;

    // 1.允许执行且已存在异常处理函数时,执行原异常处理函数
    if (callPreviousNativeErrorCallbackBlock && previousNativeErrorCallbackBlock) {
        previousNativeErrorCallbackBlock(exception);
    }

    // 2. 用于兼容自定义 Native Exception handler 的情况,可通过调用 replaceNativeExceptionHandlerBlock 实现
    if(nativeErrorCallbackBlock != nil){
        nativeErrorCallbackBlock(exception,readeableError);
    }else{
        defaultNativeErrorCallbackBlock(exception,readeableError);
    }

    // 3. 执行 js 异常处理函数
    jsErrorCallbackBlock(exception,readeableError);
    
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
    while (!dismissApp)
    {
        long count = CFArrayGetCount(allModes);
        long i = 0;
        while(i < count){
            NSString *mode = CFArrayGetValueAtIndex(allModes, i);
            if(![mode isEqualToString:@"kCFRunLoopCommonModes"]){
                CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
            }
            i++;
        }
    }
    
    CFRelease(allModes);
    
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
    
    kill(getpid(), [[[exception userInfo] objectForKey:RNUncaughtExceptionHandlerSignalKey] intValue]);
    
}

1.3 小结

通过对 react-native-exception-handler 源码的解读,可以知道,Android 和 iOS 分别利用 Thread.UncaughtExceptionHandlerNSSetUncaughtExceptionHandler 实现对应用程序的异常捕获。需要注意一点的是,当我们重置异常处理器时,需要考虑到其已存在的异常处理逻辑,避免将其直接覆盖,导致其他监测处理程序失效。

2. React 异常捕获

为了解决部分 UI 的 JavaScript 错误导致整个应用白屏或者崩溃的问题,React 16 引入了新的概念 —— Error Boundaries(错误边界)。

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

借用 static getDerivedStateFromError()componentDidCatch() 两个生命周期实现错误边界,当抛出错误后,使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

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

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <Text>Something went wrong.</Text>;
    }

    return this.props.children; 
  }
}

错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。错误边界无法捕获以下场景中产生的错误:

  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

3. JS 异常捕获

上文中提到,Error Boundaries 能捕获子组件生命周期函数中的异常,包括构造函数(constructor)和 render 函数。而无法捕获以下异常:

  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

对于这些错误边界无法捕获的异常,在 web 中可以通过 window.onerror() 加载一个全局的error事件处理函数用于自动收集错误报告。

那么 React Native 中是如何处理的呢?

3.1 BatchedBridge

React Native 是通过 JS Bridge 处理 JS 与 Native 的所有通信的,而 JS Bridge (BatchedBridge.js)是 MessageQueue.js 的实例。

'use strict';

const MessageQueue = require('MessageQueue');
const BatchedBridge = new MessageQueue();

Object.defineProperty(global, '__fbBatchedBridge', {
  configurable: true,
  value: BatchedBridge,
});

module.exports = BatchedBridge;

BatchedBridge 创建一个 MessageQueue 实例,并将它定义到全局变量中,以便给 JSCExecutor.cpp 中获取到。

3.2 MessageQueue

rn-messagequeue.png

MessageQueue 是 JS Context 和 Native Context 之间的唯一连接,如图,网络请求/响应、布局计算、渲染请求、用户交互、动画序列指令、Native 模块的调用和 I/O 的操作等,都要经过 MessageQueue 进行处理。开发中,可以通过调用 MessageQueue.spy 查看 JS <-> Native 之间的具体通信过程:

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue';
MessageQueue.spy(true);
// -or- 
// MessageQueue.spy((info) => console.log("I'm spying!", info));

MessageQueue_log.png

MessageQueue.js 有三个作用:

  • 注册所有的 JavaScriptModule
  • 提供方法供 c++ 端调用
  • 分发 js 端 NativeModule 所有异步方法的调用(同步方法会直接调用c++端代码)

查看 MessageQueue.js 构造函数:

constructor() {
    this._lazyCallableModules = {};
    this._queue = [[], [], [], 0];
    this._successCallbacks = [];
    this._failureCallbacks = [];
    this._callID = 0;
    this._lastFlush = 0;
    this._eventLoopStartTime = new Date().getTime();

    if (__DEV__) {
      this._debugInfo = {};
      this._remoteModuleTable = {};
      this._remoteMethodTable = {};
    }

    (this: any).callFunctionReturnFlushedQueue = this.callFunctionReturnFlushedQueue.bind(
      this,
    );
    (this: any).callFunctionReturnResultAndFlushedQueue = this.callFunctionReturnResultAndFlushedQueue.bind(
      this,
    );
    (this: any).flushedQueue = this.flushedQueue.bind(this);
    (this: any).invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind(
      this,
    );
  }

从 MessageQueue 源码中,可以看到,其定义了多个变量以及四个函数:

  • callFunctionReturnFlushedQueue
  • callFunctionReturnResultAndFlushedQueue
  • flushedQueue
  • invokeCallbackAndReturnFlushedQueue

而以上四个函数的调用时机则是交给 c++ 端 NativeToJsBridge.cpp,具体的通信机制可参考文章

继续阅读上述四个函数的实现,可以看到都调用了 MessageQueue 的私有方法 __guard:

__guard(fn: () => void) {
  if (this.__shouldPauseOnThrow()) {
    fn();
  } else {
    try {
      fn();
    } catch (error) {
      ErrorUtils.reportFatalError(error);
    }
  }
}

代码很简单,可以看到 __guard 会 根据 ___shouldPauseOnThrow 的返回值决定是否对 fn 进行 try catch 处理,当 __shouldPauseOnThrow 返回 false 时,且 fn 有异常时,则会执行 ErrorUtils.reportFatalError(error) 将错误上报。

// MessageQueue installs a global handler to catch all exceptions where JS users can register their own behavior
// This handler makes all exceptions to be propagated from inside MessageQueue rather than by the VM at their origin
// This makes stacktraces to be placed at MessageQueue rather than at where they were launched
// The parameter DebuggerInternal.shouldPauseOnThrow is used to check before catching all exceptions and
// can be configured by the VM or any Inspector
__shouldPauseOnThrow(): boolean {
  return (
    // $FlowFixMe
    typeof DebuggerInternal !== 'undefined' &&
    DebuggerInternal.shouldPauseOnThrow === true // eslint-disable-line no-undef
  );
}

注释写的也很清晰,MessageQueue 设置了一个用于处理所有 JS 侧异常行为的处理器,并且可以通过设置 DebuggerInternal.shouldPauseOnThrow 来决定是否对异常进行捕获。

3.3 ErrorUtils

/**
 * This is the error handler that is called when we encounter an exception
 * when loading a module. This will report any errors encountered before
 * ExceptionsManager is configured.
 */
let _globalHandler: ErrorHandler = function onError(
  e: mixed,
  isFatal: boolean,
) {
  throw e;
};
/**
 * The particular require runtime that we are using looks for a global
 * `ErrorUtils` object and if it exists, then it requires modules with the
 * error handler specified via ErrorUtils.setGlobalHandler by calling the
 * require function with applyWithGuard. Since the require module is loaded
 * before any of the modules, this ErrorUtils must be defined (and the handler
 * set) globally before requiring anything.
 */
const ErrorUtils = {
  setGlobalHandler(fun: ErrorHandler): void {
    _globalHandler = fun;
  },
  getGlobalHandler(): ErrorHandler {
    return _globalHandler;
  },
  reportError(error: mixed): void {
    _globalHandler && _globalHandler(error, false);
  },
  reportFatalError(error: mixed): void {
    // NOTE: This has an untyped call site in Metro.
    _globalHandler && _globalHandler(error, true);
  },
  ...
}

当调用 ErrorUtils.reportFatalError(error) 时,若存在 __globalHandler 则执行 _globalHandler,并将错误信息作为参数传入。同时,ErrorUtils 提供了函数 setGlobalHandler 用于重置 _globalHandler。

global.ErrorUtils.setGlobalHandler(function (err) {
    consolo.log('global error: ', err);
});

3.4 demo

那么 JS 的异常错误会被 MessageQueue 处理吗?我们可以开启 MessageQueue 看下其日志。

import React from 'react';
import {
  View,
  Text,
} from 'react-native';

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue';
MessageQueue.spy(true); // -or- MessageQueue.spy((info) => console.log("I'm spying!", info));

global.ErrorUtils.setGlobalHandler(function (err) {
    consolo.log('global error: ', err);
});

const App = () => {
  const onPressButton = () => {
    throw new Error('i am error');
  };
  return (
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text onPress={onPressButton}>按钮</Text>
    </View>
  );
};

当点击屏幕按钮时,可在控制台上看到如下信息:

MessageQueue_demo.png

可以看到,当 JS 抛出异常时,会被 ErrorUtils 捕获到,并执行通过 global.ErrorUtils.setGlobalHandler 设置的处理函数。

注意:0.64 版本开始,react-native pollfills 相关(包含 ErrorUtils 实现)已由 react-native/Libraries/polyfills 抽离为 @react-native/polyfills

4 Promise 异常捕获

除了上述提到的几种导致 APP crash 或者崩溃的异常处理之外,当我们使用 Promise 时,若抛出异常时未被 catch 捕获或在 catch 阶段再次抛出异常,此时会导致后续逻辑无法正常执行。

在 web 端,浏览器会自动追踪内存使用情况,通过垃圾回收机制处理这个 rejected Promise,并且提供unhandledrejection事件进行监听。

window.addEventListener('unhandledrejection', event => ···);

那么,那么在 React Native 中是如何处理此类 Promise 异常的呢?

在 RN 中,当遇到未处理的 Promise 异常时,控制台输出黄色警告⚠️

rn_yellow_log.png

而设备则表现为弹出黄屏:

查看源码 react-native/Libraries/Promise.js 可知,RN 默认在开发环境下,通过promise/setimmediate/rejection-tracking去追踪 rejected 状态的Promise,并提供了onUnhandled回调函数处理未进行处理的 rejected Promise:

if (__DEV__) {
    require('promise/setimmediate/rejection-tracking').enable({
        allRejections: true,
        onUnhandled: (id, error) => {
            const {message, stack} = error;
            const warning =
                `Possible Unhandled Promise Rejection (id: ${id}):\n` +
                (message == null ? '' : `${message}\n`) +
                (stack == null ? '' : stack);
            console.warn(warning);
        },
        onHandled: (id) => {
            const warning =
                `Promise Rejection Handled (id: ${id})\n` +
                'This means you can ignore any previous messages of the form ' +
                `"Possible Unhandled Promise Rejection (id: ${id}):"`;
            console.warn(warning);
        },
    });
}

其执行时机可以在rejection-tracking.js中源码中找到:

//...
timeout: setTimeout(
    onUnhandled.bind(null, promise._51),
    // For reference errors and type errors, this almost always
    // means the programmer made a mistake, so log them after just
    // 100ms
    // otherwise, wait 2 seconds to see if they get handled
    matchWhitelist(err, DEFAULT_WHITELIST)
      ? 100
      : 2000
  ),
//...

那么,我们是否可以仿照 RN 的处理,自定义 Promise 的异常处理逻辑呢?答案当然可以了,直接从源码中 copy 并将其中的 onUnhandled 替换为自己的异常处理逻辑即可,具体代码也可参考🔗

总结

本文从 React Native 应用异常监控出发,基于 react-native-exception-handler 分析了 Native 侧异常捕获的常用方案,然后介绍了 React 利用错误边界处理组件渲染异常的方式,接着通过分析 React Native 中 MessageQueue.js 的源码引出调用 global.ErrorUtils.setGlobalHandler 捕获并处理 JS 侧的全局未捕获异常,最后提供了捕获 Promise Rejection 的方法。

文章的最后,提下本人实现的 react-native-error-helper,与 react-native-exception-handler 相比,去除了 Native 异常处理捕获,在 JS 异常捕获 的基础上,添加了用于捕获 React 异常 的 错误边界组件 ErrorBoundary 和高阶组件 withErrorBoundary(hook useErrorBoundary 计划中),期待您的 star⭐️

推荐阅读

参考文章

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.