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.html、https://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)只能执行一次,否则崩溃。