React、React Native面试题

1.React Native相对于原生的ios和Android有哪些优势。

react native一套代码可以开发出跨平台app, 减少了人力、节省了时间、避免了 iOS 与 Android 版本发布的时间差,开发新功能可以更迅速。等等

2.React Native的优点和缺点在哪里。

缺点:内存、转化为原生的

3.父传子,子传父数据传递方式。

props state refs 方面回答

4.如何实现底部TabBar的高度不一样呢。(类似新浪微博底部加号)

主要考察flex布局绝对定位问题

5.请您简单介绍一下redux、mobx。

redux ==> action/reducer/store

mobx ==>数据双向绑定

6.当你调用setState的时候,发生了什么事。

当调用 setState 时,React会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。 这将启动一个称为和解(reconciliation)的过程。 和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新UI。 为此,React将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。 一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较( diff )。 通过这样做, React 将会知道发生的确切变化,并且通过了解发生什么变化,只需在绝对必要的情况下进行更新即可最小化 UI 的占用空间。

7.React中Element 和 Component 有何区别。

简单地说,一个 React element 描述了你想在屏幕上看到什么。 换个说法就是,一个 React element 是一些 UI 的对象表示。 一个 React Component 是一个函数或一个类, 它可以接受输入并返回一个 React element (通常是通过 JSX ,它被转化成一个 createElement 调用)。

8.shouldComponentUpdate 应该做什么

其实这个问题也是跟reconciliation有关系。 “和解( reconciliation )的最终目标是以最有效的方式,根据新的状态更新用户界面”。 如果我们知道我们的用户界面(UI)的某一部分不会改变, 那么没有理由让 React 很麻烦地试图去弄清楚它是否应该渲染。 通过从 shouldComponentUpdate 返回 false, React 将假定当前组件及其所有子组件将保持与当前组件相同

9.描述事件在React中的处理方式

为了解决跨浏览器兼容性问题, 您的 React 中的事件处理程序将传递 SyntheticEvent 的实例, 它是 React 的浏览器本机事件的跨浏览器包装器。 这些 SyntheticEvent 与您习惯的原生事件具有相同的接口,除了它们在所有浏览器中都兼容。 有趣的是,React 实际上并没有将事件附加到子节点本身。 React 将使用单个事件监听器监听顶层的所有事件。 这对于性能是有好处的,这也意味着在更新DOM时,React 不需要担心跟踪事件监听器

10.reactJS的props.children.map函数来遍历会收到异常提示,为什么。应该如何遍历。

this.props.children 的值有三种可能

  1. 当前组件没有子节点,它就是 undefined;
  2. 有一个子节点,数据类型是 object;
  3. 有多个子节点,数据类型就是 array.

系统提供React.Children.map()方法安全的遍历子节点对象

11.XSS与CSRF介绍

XSS是一种跨站脚本攻击,是属于代码注入的一种,攻击者通过将代码注入网页中,其他用户看到会受到影响(代码内容有请求外部服务器);

CSRF是一种跨站请求伪造,冒充用户发起请求,完成一些违背用户请求的行为(删帖,改密码,发邮件,发帖等)

12.在使用redux过程中,如何防止定义的action-type的常量重复。

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。 Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象 Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

13 PureComponent 为什么比Component高效?

追查:这两个组件的实现在ReactBaseClasses.js中间,除掉注释后如下

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactComponent.prototype.isReactComponent = {};

ReactComponent.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

ReactComponent.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};


function ReactPureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

function ComponentDummy() {}
ComponentDummy.prototype = ReactComponent.prototype;

ReactPureComponent.prototype = new ComponentDummy();
ReactPureComponent.prototype.constructor = ReactPureComponent;
_assign(ReactPureComponent.prototype, ReactComponent.prototype);
ReactPureComponent.prototype.isPureReactComponent = true;

module.exports = {
  Component: ReactComponent,
  PureComponent: ReactPureComponent
};

发现Component就只实现了构造方法,定义了setState方法就完了。而ReactPureComponent更狠,就只是用js原型模拟继承的方法继承了Component,然后定义属性isPureReactComponent为true。全局搜索isPureReactComponent属性,发现在ReactCompositeComponent.js中有使用,这个类就是管理组件的更新、加载等的。关键代码在updateComponent方法中,如下

var shouldUpdate = true;    // 这个变量决定是否需要重新渲染组件
  if (!this._pendingForceUpdate) {
    var prevState = inst.state;
    shouldUpdate = willReceive || nextState !== prevState;
    // inst代表组件实例,这个判断条件就是组件是否自己实现了shouldComponentUpdate方法
    if (inst.shouldComponentUpdate) {
      if (__DEV__) {
        shouldUpdate = measureLifeCyclePerf(
          () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
          this._debugID,
          'shouldComponentUpdate',
        );
      } else {
        shouldUpdate = inst.shouldComponentUpdate(
          nextProps,
          nextState,
          nextContext,
        );
      }
    } else {// 组件没有实现shouldComponentUpdate方法,且是PureComponent,采用shallowEqual浅比较
      if (this._compositeType === ReactCompositeComponentTypes.PureClass) {
        shouldUpdate = !shallowEqual(prevProps, nextProps) ||
          !shallowEqual(inst.state, nextState);
      }
    }
  }

shallowEqual的实现在shallowEqual.js中,大意就是作浅比较,也就是对象数组等只比较对象所处的地址是否相等,而不比较具体的内容,因为深层次递归比较对象内容是否一致很耗费性能。

结论

PureComponent是Component的子类,当PureComponent手动实现了shouldComponentUpdate方法时两个组件没有区别,但若没有手动实现该方法,则PureComponent采用默认的shallowEqual比较对象是否相等性能更佳。由此可能引发的页面不刷新现象可以采用别的办法解决,如重新生成新的对象、采用immutable.js对象等

14 TextInput首次focus时键盘出现缓慢,卡顿

追查:原因就是键盘是懒加载模式,初次出现时需要先初始化键盘视图耗费时间,要想缩减首次耗时间隔,可以事先就让键盘初始化完毕。js端没想到如何做,但是原生端可以在didFinishLaunchingWithOptions方法中写:

  UITextField *textField = [[UITextField alloc] init];
  [self.window addSubview:textField];
  [textField becomeFirstResponder];
  [textField resignFirstResponder];
  [textField removeFromSuperview];

键盘消失与否

TextInput聚焦时弹出了键盘,点击非TextInput空白处键盘是不会消失的,若想实现该功能只需要让TextInput嵌入在ScrollView中即可。

那么问题又来了,这样做之后,除了TextInput外屏幕上任意地方点击键盘都会先消失,导致例如页面上有个按钮A,点击A时会先退下键盘,再次点击才能触发A的事件,很扯淡。解决方法大体如下:

_addEvent = (event) => {
  this.events.push(event.nativeEvent.target);
};

_onStartShouldSetResponder(event) {
  const target = event.nativeEvent.target;
  if (!this.events.includes(target)) {
    Keyboard.dismiss();
  }
  return false;
}

render() {
  return (
    <ScrollView keyboardShouldPersistTaps="always">
      <View
        center', flex: 1, height: SCREEN_HEIGHT }}
        onStartShouldSetResponder={(event) => this._onStartShouldSetResponder(event)}
      >
        <Button
          text="登陆"
          onLayout={(event) => this._addEvent(event)}
        />
      </View>
    </ScrollView>
  );
}

ScrollView的keyboardShouldPersistTaps属性设置为always,则键盘不再拦截点击事件,点击空白处键盘不会自动消失。

onStartShouldSetResponderCapture是点击事件发生时调用,询问该视图是否要拦截事件,自定义处理,当点击屏幕除了指定位置外都退下键盘。指定位置A(比如登录按钮)点击时,键盘不退下。

A的onLayout在视图布局完成回调,event.nativeEvent.target能唯一的标识该组件。

15 redux中的reducer为何要返回Object.assign

在redux-devtools中,我们可以查看到redux下所有通过reducer更新state的记录,每一个记录都对应着内存中某一个具体的state,让用户可以追溯到每一次历史操作产生与执行时,当时的具体状态,这也是使用redux管理状态的重要优势之一.

  • 若不创建副本,redux的所有操作都将指向内存中的同一个state,我们将无从获取每一次操作前后,state的具体状态与改变,
  • 若没有副本,redux-devtools列表里所有的state都将被最后一次操作的结果所取代.我们将无法追溯state变更的历史记录.

    创建副本也是为了保证向下传入的this.props与nextProps能得到正确的值,以便我们能够利用前后props的改变情况以决定如何render组件

16 使用redux时,不能在componentWillReceiveProps方法中使用action。

原因:有时候一个action改变数据后,我们希望拿到改变后的数据做另外一个action,比如初始化action读取硬盘中的数据到内存,然后用该参数进行请求网络数据action。此时我们可以在componentWillReceiveProps方法中拿到参数,若此时发出再发出action,则数据返回后改变reducer会再次进入componentWillReceiveProps方法,又继续发出action,陷入死循环。可以如下解决

componentWillReceiveProps(nextProp) {
    if(nextProp.app.user && nextProp.app.sessionId && !this.isFirstLoad){
        this.props.action(nextProp.app);   // action操作
        this.isFirstLoad = true;
    }
}

17 navigation导航栏下方那根黑线是什么?

追查:发现在iphone7plus模拟器中黑线看不到,但是iphone6模拟器能看见。查看源代码,在navigation组件中的Header.js第300行找到了黑线样式定义,

let platformContainerStyles;
if (Platform.OS === 'ios') {
    platformContainerStyles = {
        borderBottomWidth: StyleSheet.hairlineWidth,  // hairlineWidth为当前分辨率下能显示的最小宽度,模拟器下可能看不见
        borderBottomColor: 'rgba(0, 0, 0, .3)',
    };
} else {
    platformContainerStyles = {
        shadowColor: 'black',
        shadowOpacity: 0.1,
        shadowRadius: StyleSheet.hairlineWidth,
        shadowOffset: {
            height: StyleSheet.hairlineWidth,
        },
        elevation: 4,
    };
}

可见在ios中下方黑线使用边框的形式实现,而安卓则是设置图层阴影。若想隐藏该线,ios中设置headerStyle的borderBottomWidth为0,安卓中设置elevation/shadowOpacity为0.

同上,可在TabBarBottom.js中180行找到tabbar上方那跟线的默认设置,更改则可在TabNavigator中的tabBarOptions的style中设置borderTopWidth和borderTopColor

18 为何需要render的组件被保存到数组中需要设置key?

追查:跟虚拟DOM和Diff算法有关。

一次DOM操作流程包括,拿到页面所有DOM节点,拿到css样式表,生成render树,布局计算节点位置,渲染等操作。 传统应用,一个操作如果需要改变10个DOM节点,则会相应的进行10次DOM操作,很多重复浪费性能。

虚拟DOM就是刚开始就将所有的DOM节点转换成js的相关代码保存到内存中,一个操作改变10次DOM节点全部在内存中完成,再将内存中的js转换为实际的DOM节点渲染,性能高。

虚拟DOM一个操作中10次改变DOM节点,每次只是改变了必要的那一个节点,不需要全部改变,为了减少时间复杂度,引入Diff算法,只比较节点改变了的那一点,进行增删改操作等。比如现在的render树是A、B、C节点,想再A节点后面插入D节点,若没有key,React无法区分各个节点,只能根据渲染树的排列依次卸载B、装载D、卸载C、装载B、装载C,效率低下。如果ABC节点都有key,则React就能根据key找出对应的节点,直接渲染A、D、B、C,效率高。

19 Diffing算法相关

在任何一个单点时刻 render() 函数的作用是创建 React 元素树。在下一个 state 或props 更新时,render() 函数将会返回一个不同的 React 元素树。 React 通过Diffing算法找出两颗元素树的差异,更新必须的部分,其假定规则是:

a、DOM 节点跨层级的移动操作特别少,可以忽略不计。

b、拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。

c、对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

具体的比较如下:

1、tree diff,DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。若有节点跨层级的移动,性能会受到影响

2、component diff,如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

3、element diff,当节点处于同一层级时,默认情况下,当递归一个 DOM 节点的子节点时,React 只需同时遍历所有的孩子节点并更改不同点,如在列表组件追加几个item时,性能不错。但是当如下

<ul>
  <li>1</li>
  <li>2</li>
</ul>
<ul>
  <li>3</li>
  <li>1</li>
  <li>2</li>
</ul>

React 将会改变每一个子节点而没有意识到需要保留

  • 1
  • 2
  • 两个子树。这很低效。为了解决这个问题,React 支持一个 key 属性(attributes)。当子节点有了 key ,React 使用这个 key 去比较原来的树的子节点和之后树的子节点。例如,添加一个 key 到我们上面那个低效的例子中可以使树的转换变高效
    <ul>
      <li key="2015">1</li>
      <li key="2016">2</li>
    </ul>
    <ul>
      <li key="2014">3</li>
      <li key="2015">1</li>
      <li key="2016">2</li>
    </ul>
    

    现在 React 知道有'2014' key 的元素是新的, key为'2015' 和'2016'的两个元素仅仅只是被移动而已,效率变高很多。要注意key必须具备唯一性。若将数组中的索引作为 key ,如果存在重新排序时,性能将会很差,应该避免这种情况。

    高阶组件(HOC)

    高阶组件是重用组件逻辑的一项高级技术。高阶组件并不是React API的一部分。高阶组件源自于React生态。具体来说,高阶组件是一个函数,能够接受一个组件并返回一个新的组件,例如Redux的connect函数。

    HOC存在的问题:

    1、组件的静态方法不会被传递,需要自行传递处理

    2、refs不会被传递,应该避免此,或者用自定义属性传递

    ()

    3、react-native-fetch-blob的POST请求不成功。

    4、js传到原生端的函数(ios中叫block)只能执行一次,否则崩溃。

    转: