美文网首页程序员
react-native几个问题追查原因

react-native几个问题追查原因

作者: 请叫我啊亮 | 来源:发表于2017-08-29 10:55 被阅读317次

    1、原生端导出模块方法到js,怎么没有区分静态方法和对象方法?怎么导出单例对象?
    追查:原生端导出的模块会添加到一个配置表中间。通过断点,发现程序启动时就会遍历该模块,将必须的模块以同步的方式在主线程依次初始化完毕,也就是说程序启动时就将所有导出模块的对象创建好了,并且只有一个对象,所有的导出模块都是单例对象。

    以下是程序启动时,模块初始化方法的调用顺序图

    第一张图 第二张图

    2、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对象等

    3、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) {
            let target = event.nativeEvent.target;
            if (!this.events.includes(target)) {
                Keyboard.dismiss()
            }
            return false;
        }
    
          render() {
            return (
                <ScrollView  keyboardShouldPersistTaps='always' >
                    <View
                        style={{alignItems:'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能唯一的标识该组件。

    4、redux中的reducer为何要返回Object.assign
    redux-devtools中,我们可以查看到redux下所有通过reducer更新state的记录,每一个记录都对应着内存中某一个具体的state,让用户可以追溯到每一次历史操作产生与执行时,当时的具体状态,这也是使用redux管理状态的重要优势之一.
    若不创建副本,redux的所有操作都将指向内存中的同一个state,我们将无从获取每一次操作前后,state的具体状态与改变,若没有副本,redux-devtools列表里所有的state都将被最后一次操作的结果所取代.我们将无法追溯state变更的历史记录.
    创建副本也是为了保证向下传入的this.props与nextProps能得到正确的值,以便我们能够利用前后props的改变情况以决定如何render组件(摘自http://www.jianshu.com/p/8287a1dd8ae9

    5、使用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;
            }
        }
    

    6、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

    7、为何需要render的组件被保存到数组中需要设置key?
    追查:跟虚拟DOM和Diff算法有关。(http://www.jianshu.com/p/616999666920)
    一次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,效率高。

    8、Diffing算法相关
    在任何一个单点时刻 render() 函数的作用是创建 React 元素树。在下一个 state 或props 更新时,render() 函数将会返回一个不同的 React 元素树。 React 通过Diffing算法找出两颗元素树的差异,更新必须的部分,其假定规则是:
    a、DOM 节点跨层级的移动操作特别少,可以忽略不计。
    b、拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
    c、对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
    具体的比较如下:
    a、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 将会改变每一个子节点而没有意识到需要保留 <li>1</li> 和 <li>2</li> 两个子树。这很低效。为了解决这个问题,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 ,如果存在重新排序时,性能将会很差,应该避免这种情况。(http://www.css88.com/react/docs/reconciliation.htmlhttps://zhuanlan.zhihu.com/p/20346379?refer=purerender)

    9、高阶组件(HOC)
    高阶组件是重用组件逻辑的一项高级技术。高阶组件并不是React API的一部分。高阶组件源自于React生态。具体来说,高阶组件是一个函数,能够接受一个组件并返回一个新的组件,例如Redux的connect函数。
    HOC存在的问题:
    1、组件的静态方法不会被传递,需要自行传递处理
    2、refs不会被传递,应该避免此,或者用自定义属性传递
    (http://www.css88.com/react/docs/higher-order-components.html)

    6、react-native-fetch-blob的POST请求不成功。
    7、js传到原生端的函数(ios中叫block)只能执行一次,否则崩溃。

    相关文章

      网友评论

        本文标题:react-native几个问题追查原因

        本文链接:https://www.haomeiwen.com/subject/bjqodxtx.html