ReactNative中的reducer函数中的浅拷贝和深拷贝

我们都知道reducer函数必须是纯函数,不能修改传入的state参数。先看一段示例代码

export function updateData(state=[], action) {
    switch(action.type){
        case Actions.ADD:
            return state.concat(action.data);
        case Actions.UPDATE:
            state[action.index] = action.data
            return state
        default:
            return state;
    }
}

这里state是一个简单的数组,ADD操作返回的是一个新的state,因为concat函数会创建并返回一个新的array。而UPDATE操作是修改了原state后返回原state。在demo中的表现就是,当reducer收到ADD时,会触发render刷新界面。而当收到update时,不会触发render,但如果再次收到ADD触发render刷新界面时,能看到UPDATE操作的数据已经被更新了。

结论就是

  • 只要返回的是原state,就不会触发render。这也正是我们default需要返回state本身的原因。即使state数据发生了变化,也不会触发render,但数据的变化被存储起来了。
  • 只要返回的是新state,就会触发render,即使数据完全不变

如果希望上面的UPDATE生效的话也很简单,把return state改成return state.slice(0)就可以了。但这种操作是不对的,虽然通过返回一个新的state来让render触发了,但它修改了state,它会导致的问题就是在component的componentWillReceiveProps函数里,nextProps和原来的props完全一致。reducer的原则是每个action对应着一个state,如果在action操作前后state相同,那就失去了这个特性了。

我们一般使用的Object.assign来构造一个新的state,Object.assign执行的就是浅拷贝而不是深拷贝,所以如果我们操作的state是一个比较复杂的结构,那么应该想办法手动执行深拷贝,否则使用浅拷贝的话,对应的内容就是同一份。

export function updateDeep(state={sth:[]}, action) {
    let sth = state.sth;
    switch(action.type){
        case Actions.ADD:
            sth.push({data:action.data});
            return Object.assign({}, {sth:sth})
        case Actions.DELETE:
            sth.splice(action.index, 1);
            return Object.assign({}, {sth:sth})
        case Actions.UPDATE:
            sth[action.index].data = action.data;
            return Object.assign({}, {sth:sth})
        default:
            return state;
    }
}

这个reducer函数直接修改了state.sth,虽然使用Object.assign返回了一个新的state,触发了render进行了刷新,但如果在componentWillReceiveProps函数里观察,就会发现this.props里的sth和nextprops里的sth是一模一样的,也就是执行action操作前后的state没有区分开来。

要解决上面的问题,可以有两种方案,一个是把reducer函数细分,确保在操作state时不会执行对对象进行浅拷贝。比如第二个例子的state改为[],用combineReducer来合并细分后的reducer函数,但如果数组成员是Object,而不是简单数据类型,就仍然有浅拷贝的问题,所以可以用第二种方案,先把state深拷贝一份,然后修改这个拷贝后的state并返回,如果state是个很复杂的数据结构,深拷贝一次代价会比较大。所以实际中应该这两种方案结合起来。

一个比较好的git分支管理方案

假如当前处于开发分支develop上,接到一个任务时,使用git的话,应该优先考虑开一个分支来做这件事情,假设我们开了一个分支b1,这个任务未必是可以一次开发完,可能中间去做了别的事情,导致写了一半的代码暂时提交上去,这样到这个任务完成时,这个分支有了commit1, commit2…..一系列的提交。但假设这是一件完整的单独的功能,我们需要让人review代码并作为一次完整的提交的时候,我们可以这么做:

在b1分支上先git reset –soft回到开分支的节点去,这样我们在这个分支上所有的改动,都变成了待commit的状态,然后我们找人review并提交。之后回到develop分支上,用git cherry-pick把刚才那个提交搬到develop分支上来,在develop分支上就只会看到一个干干净净的提交节点了。然后我们删除本地分支b1就可以了。

相比其它做法,比如在develop分支上使用merge来合并,它的缺点是查看develop分支的log时,会看到b1分支跟它交错,如果我们有很多个分支b1,b2….那develop分支的log看起来就会像蜘蛛网一样乱了。

当然我们可以用git merge –no-ff或者git rebase来保持develop分支的干净,但这样也有缺陷,我们在b1分支上有很多临时提交节点,也全部在develop分支上显示了,明明只是一个单独的任务,却有很多个commit节点,在查看log的时候,也会比较麻烦。

有的人不想临时提交,所以要做别的事情时,使用stash来暂存工作区,再次回到分支来时,使用stash pop恢复工作区,这是非常不可取的,一旦分支多了,在某个分支上stash,另一个分支上pop,就容易乱套了。所以还是尽量commit,但如果又不想让commit节点太多太乱的话,就可以用reset的办法来合并成一次提交,就干净了。

ReactNative中的listView使用介绍

今天下午稍微把listView的js代码看了一遍,大致总结一下它的接口和使用,官方文档上的介绍太过简单。listView的js源代码位于node_modules\react-native\Libraries文件夹内。

一个最简单的listView,代码如下

<ListView
      dataSource={this.state.dataSource}
      renderRow={(rowData) => <Text>{rowData}</Text>}
 />

dataSource

dataSource顾名思义是为了给listView提供数据源,这个类的源代码为ListViewDataSource.js。这个类除了存储数据外,还提供了4个接口给listView调用,这4个接口我们都可以自定义实现,其中2个有默认实现,2个没有。这段源代码就是

this._rowHasChanged = params.rowHasChanged;
this._getRowData = params.getRowData || defaultGetRowData;
this._sectionHeaderHasChanged = params.sectionHeaderHasChanged;
this._getSectionHeaderData =
  params.getSectionHeaderData || defaultGetSectionHeaderData;

rowHasChanged是一定要实现的,用来区分两个row是否相同,如果没有特殊需求的话,使用下面的就可以了

rowHasChanged: (r1, r2) => r1 !== r2

sectionHeaderHasChanged是如果listView有分节,则必须要的,没有特殊需求的话可以直接

sectionHeaderHasChanged: (s1, s2) => s1 !== s2

getRowData这个源代码有提供默认的实现,它用来获取每行的需要显示的数据,没有特殊需求的话不用实现
getSectionHeaderData源代码也有默认实现,顾名思义它用来获取每节的数据。

dataSource要拿来使用,除了实现必须的接口之外,还需要给它提供数据,这就用到了cloneWithRows和cloneWithRowsAndSections这两个方法,前者是后者的简化版,所以我们就拿最复杂的来举例说明。cloneWithRowsAndSections的函数声明为

 cloneWithRowsAndSections(
  dataBlob: any,
  sectionIdentities: ?Array<string>,
  rowIdentities: ?Array<Array<string>>): ListViewDataSource

它接受三个参数,并返回一个ListViewDataSource的实例对象。第一个参数dataBlob是传入的数据,它应该是一个Object,第二个参数是一个数组,它指定了一些key,也就是说dataBlob这个Object里,sectionIdentities里的keys对应的value才是需要显示的。rowIdentities则是一个二维数组,它指定了每节的数据中,哪些key对应的数据需要显示。前面我们说过,需要显示的数据是由getRowData和getSectionHeaderData来获取的,所以上面所说的是系统默认的实现,我们当然也可以自己去实现这两个接口,来自定义数据的获取行为。 以下是一段示例代码

let data=[["1","2", "3"],["4","5", "6"],["7","8", "9"]];
const ds = new ListView.DataSource({
    rowHasChanged: (r1, r2) => r1 !== r2, 
    sectionHeaderHasChanged: (s1, s2) => s1 !== s2
});
<ListView
    dataSource={ds.cloneWithRowsAndSections(data, ["0", "2"], [[0,1], [1,2]])}
/>

这段代码的行为是显示两个section,第一个section显示1和2,第二个section会显示8和9。这个例子里dataBlob是一个数组,它只是比较特殊的Object,key是0123…,如果是普通的key-value也是一样处理。cloneWithRows同理但更为简单,它只有两个参数,第一个参数dataBlob是传入的数据,第二个参数是一个数组,指定需要显示的rowIdentities。

对于这样一段代码

let data = ["111", "222", "333"]
<ListView
    dataSource={ds.cloneWithRowsAndSections(data)}
/>

显示效果会是有三个section,每个section有3个row,这是由于系统默认的getRowData和getSectionHeaderData实现方式决定的,思考一下就能明白了。

ListView

把dataSource弄明白之后,接下来看listView。从源代码的propTypes可以查看它能接收的props。

  • dataSource就是我们上面说到的,给它传递一个dataSource实例即可

  • renderSeparator函数声明为(sectionID, rowID, adjacentRowHighlighted) => renderable
    这个函数可以不用实现,系统会有默认实现,它的作用是绘制listView里每节中各个行的分割线。adjacentRowHighlighted为bool值,它的值由renderRow函数指定。

  • renderRow函数声明为(rowData, sectionID, rowID, highlightRow) => renderable,这是必须实现的,作用是绘制每个单元行,rowData就是从dataSource里获取来的数据,sectionID为所在节id,rowID为所在行的id。这三个数据都是dataSource传递过来的,所以像上面的那个dataSource例子,sectionID分别是”0”, “1”, “2”, rowID是0, 1, 2。最后的highlightRow参数是一个function,它可以在renderRow函数里适当的时候调用(比如按钮被点击),调用highlightRow(sectionID, rowID)可以让这个单元在上面的renderSeparator函数里接受到的adjacentRowHighlighted变为true。不要直接在renderRow函数里调用highlightRow函数,它会导致死循环然后调用栈溢出

  • renderSectionHeader函数声明为(sectionData, sectionID) => renderable,如果listView有分节,则实现此函数来绘制每节的头部,返回null或者undefined则不会渲染。

  • initialListSize用来指定起始时渲染多少个单元行,如果不指定的话,系统默认是10个。但首次渲染的单元行数量不完全取决于此,还取决于一个属性值DEFAULT_SCROLL_RENDER_AHEAD = 1000,这代表整个listView最多渲染多少个逻辑像素高,首次渲染的单元行数量取这两者中较大的那个。这是用来做性能优化的,如果确实碰到性能瓶颈时,需要将源代码完全看明白才能着手,所以建议在没完全看明白源代码之前没必要去碰这些参数。

  • scrollRenderAheadDistance这是一个数字,前面说过了,它参与限制首次渲染的单元行数量,默认值为1000,我们写一个demo,设置数据量比较大,然后让这个值为不同的值,可以看到首次渲染的单元行数量会不一样

  • onEndReached在整个listView滚动到最底部时会被调用的回调

  • onEndReachedThreshold是一个数字,指定当滑动了多少距离时会触发onEndReached事件

  • pageSize指定了每次事件循环时渲染的单元格数量

  • renderFooter和renderHeader函数声明为() => renderable,它们渲染整个listView的头部和底部,头部和底部会随着listView滑动。

  • renderScrollComponent函数声明为(props) => renderable,用来渲染装listView内容的容器,默认实现是直接返回了一个ScrollView

  • onChangeVisibleRows函数声明为(visibleRows, changedRows) => void,它在当前正在显示哪些单元行发生变化时被调用,visibleRows是一个字典,形式为{ sectionID: { rowID: true }},表示所有当前可见的单元行,changedRows也是一个字典,形式为{ sectionID: { rowID: true | false }},表示visible发生了变化的单元行

  • removeClippedSubviews 这是一个bool值,默认是true,用来优化数据量很大时的显示性能

  • stickyHeaderIndices,这是一个数字数组,用来指定哪些单元行在listview滑动时固定在屏幕顶端,只在ios平台而且是竖直方向的listView上才生效

  • enableEmptySections,这是一个Bool值,用来指定当一个没有数据的节是否需要展示,例如dataSource数据为[[1,2],[],[3,4]]时,中间的那个节(包括sectionHead和rows)是否会展示,如果不指定的话这个值为undefined,就不会显示。

以上就是listView的所有props了,也就是说明白了上面的内容,使用listView来完成功能肯定没问题了,但如果希望了解更多的细节,还是需要查看源代码,包括js端以及native端的源码。react-native的最新版本提供了flatList组件,它是listView的升级版,应该优先考虑使用flatList。

ps:当我们修改listView的数据源时,即使只是修改数组内的一项,也会导致整个listView都重新刷新,如果短时间内频繁更新数据源的话,可能导致性能瓶颈,应该优化为缓存数据后集中更新一次。

ReactNative之Image控件从js到java的追踪流程

我们以image控件为例,简单的介绍下一个系统控件的实现,方便进行自定义,以及了解它的内部实现。

我们要使用Image的话,第一步就是

import {Image} from "react-native"

我们需要找到源文件export出Image的地方,它位于node_modules/react-native/Libraries/Image目录下。基本上RN的js源代码都在这个Libraries目录下。

在Image.android.js里,文件结尾是

module.exports = Image;

证明我们使用的Image就是这里导出的。在这个文件的render函数里,可以看出来它使用了RKImage控件。然后查找RKImage的来源

var RKImage = requireNativeComponent('RCTImageView', Image, cfg);

可以看出来,RKImage是native的实现,所以我们到node_modules/react-native/ReactAndroid/src/main/java目录下搜索关键字RCTImageView,找到在哪里注册的。

观察一下搜索结果,可以看到有两个类RCTImageViewManager和ReactImageManager,他们都是导出到js层的类,且导出名字都是RCTImageView。

我们找一下这两个类是在哪里注册的,如果对流程比较熟悉的话,看到它们继承自ViewManager也已经知道了。在node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\shell\MainReactPackage.java的createViewManagers方法里可以看到

if(useFlatUi) {
    viewManagers.add(new RCTImageViewManager());
}else{
    viewManagers.add(new ReactImageManager());
}

所以根据useFlatUi的值,Image控件的native实现,可能是node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\views\image\ReactImageView.java或者node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\flat\RCTImageView.java

其余控件如果想追踪native实现,也可以按这个流程走就行了。

创建一个redux saga项目的简要流程

我们先新建一个RN项目

react-native init testrn

然后安装redux

cd testrn
npm install --save redux

在动手之前确保自己已经理解了redux的概念 redux官方中文文档

现在开始写代码了,第一步我们需要设计好state的结构,我们写一个很简单的demo,页面有两个text和两个Button,点第一个button修改第一个text的文字内容,点第二个button修改第二个text的文字内容。我们将state设计为

{
    "subState1":{"text":""},
    "subState2":{"text":""}
}

实际项目中的state肯定不会这么简单,可能会非常庞大,这就要求结构不能太草率,因为是树状结构,所以要掌握好分级的粒度,层级太少则可能单个子state过于复杂,因为一个reducer处理一个子state,则会导致reducer函数过于繁冗。层级太多则不易梳理结构,显得混乱。

然后我们给这两个修改text的动作设定两个action

const CHANGE1 = "change1";
const CHANGE2 = "change2";

然后根据这个结构来写reducer

function reducer1(state={"text":""}, action) {
    if(action.type === CHANGE1) {
        return Object.assign({}, state, {"text":"hello" + action.data});
    }
    return state;
}
function reducer2(state={"text":""}, action) {
    if(action.type === CHANGE2) {
        return Object.assign({}, state, {"text":"world" + action.data});
    }
    return state;
}
let reducer = combineReducers({"subState1":reducer1, "subState2":reducer2});

有的人在使用combineReducers时,习惯使用ES5的对象简写,例如

let reducer = combineReducers({reducer1, reducer2})

这样不是很好,这样写的话,state的实际内容就是

{
    "reducer1":{"text":""},
    "reducer2":{"text":""}
}

我们在使用state数据时,以reducer1为key,而reducer1本身又是reducer函数的名称,一不小心就容易搞糊涂。所以我们提倡在设计state时就想好子state的key,然后在combine时用key:reducer的形式。

为了方便使用redux,我们还需要react-redux库

npm install --save react-redux

我们使用connect这个API,来简化触发和监听action的操作。我们开始把这个页面写出来

import {Provider, connect} from "react-redux"

class TestRN extends Component{
    render(){
        return (<View>
            <Text>{this.props.subState1.text}</Text>
            <Text>{this.props.subState2.text}</Text>
            <Button title="btn1" onPress={()=>{
                this.props.createSagaAction1();
            }}/>
            <Button title="btn2" onPress={()=>{
                this.props.createSagaAction2();
            }}/>
        </View>)
    }
}

const mapStateToProps = (state, ownProps) =>{
    return state;
}
const mapDispatchToProps = (dispatch, ownProps)=>{
    return bindActionCreators({createSagaAction1, createSagaAction2}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(TestRN)

这段代码里,我们点击按钮后,并不是发出了CHANGE1和CHANGE2事件,而是createSagaAction1和createSagaAction2,这是因为我打算在接下来使用redux saga,让它收到而是createSagaAction1和createSagaAction2后,模拟一次异步操作,再由saga来发出CHANGE1和CHANGE2事件

redux saga是用来处理异步操作的,所以我们先写个模拟异步的功能函数

function delay(cb, time) {
    return new Promise((resolve)=>{
        setTimeout(()=>{
            cb(parseInt(Math.random() * 100));
            resolve();
        }, time);
    })
}
async function getNum(){
    let a;
    await delay((num)=>{
        a = num;
    }, 5000);
    return a;
}

然后开始redux saga相关,首先是安装saga

npm install --save redux-saga

然后开始代码,我们把点击按钮要发出的两个action实现出来然后监听并处理

import regeneratorRuntime from "regenerator-runtime";

const SAGA_ACTION1 = "sagaAction1";
const SAGA_ACTION2 = "sagaAction2";
function createSagaAction1(){
    return {"type": SAGA_ACTION1};
}
function createSagaAction2(){
    return {"type": SAGA_ACTION2};
}
function* sagaFunc1(){
    while(true) {
        yield take(SAGA_ACTION1);
        let a = yield call(getNum);
        yield put({type:CHANGE1, data:a});
    }
}
function* sagaFunc2(){
    while(true) {
        yield take(SAGA_ACTION2);
        let a = yield call(getNum);
        yield put({type:CHANGE2, data:a});
    }
}
function* mySaga(){
    yield fork(sagaFunc1);
    yield fork(sagaFunc2);
}

这里需要注意的就是saga函数必须是generators函数,不能用async, await。 sagaFunc1通过使用三个effect来进行操作,首先使用take来监听SAGA_ACTION1,然后使用call来阻塞调用异步函数,最后使用put来发出CHANGE1这个action,这时我们前面写的reducer收到CHANGE1这个action,就会修改state,从而让页面发生变化。

然后就是不要忘了import regeneratorRuntime,否则项目是跑不起来的,会出现红屏错误,提示cannot read property ‘mark’ of undefined。这是babel处理generators函数所需要的。

最后是创建store和使用provider了

let middle = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(middle));
middle.run(mySaga);

export default class TestRNContainer extends Component{
    render(){
        return (
            <Provider store = {store}>
            <TestRN />
            </Provider>
        )
    }
}

到此为止,一个新建项目使用redux saga就全部完成了

ReactNative之ref赋值应使用成员函数

在线上项目收集的js error里,发现有一个问题,有的ref成员变量可能变为null,然后就出了问题。先看下面的代码。

class Test extends Component{
    constructor(props) {
        super(props);
        this._ref = null;
    }

    render(){
        return (<View>
        <MyComponent ref={component=>this._ref=component}/>
        <Button onPress={this._onPress.bind(this)}/>
        </View>	
        )
    }

    _onPress(){
        this.setState({});
        this._ref.test();
    }
}

当_onPress被调用时,this._ref就可能变为null。

在facebook的官方文档里,最后一段是一个警告信息

If the ref callback is defined as an inline function, it will get called twice during updates, first with null and then again with the DOM element. This is because a new instance of the function is created with each render, so React needs to clear the old ref and set up the new one. You can avoid this by defining the ref callback as a bound method on the class, but note that it shouldn’t matter in most cases.

可以很清楚的看到,如果ref函数每次都是一个新函数,就可能导致当render函数被执行时,ref被赋值两次,第一次被赋值为null,第二次才被赋值为需要指定的component。上面例子里我们在_onPress函数里调用setState导致页面被重新渲染,然后_ref被重新赋值,有比较小的几率_ref会变为null,如果此时我们使用了_ref变量,就出现问题了。

解决的办法很简单,我们不应该让ref赋值函数每次都是新的函数,而不管是箭头函数,还是bind方法,每次都是生成一个新函数。所以我们在constructor函数里先绑定好一个成员函数来,然后使用它就可以了。这里就不写代码了,相信对js熟练的同学肯定很快就能写出来。

ReactNative中的pixelRatio

关于pixelRatio,官方文档有相关内容。通过

PixelRatio.get()

接口可以获取到设备的像素密度,但有的同学不清楚这个像素密度做什么用的,比如我发现在项目里,给图片设置尺寸时,都乘以了这个像素密度值,实际上这是不对的。

手机屏幕像素是大家都知道的,比如1080*1920,他们指的是物理像素。除此之外还有一个参数叫做设备独立像素,在这里可以看到部分移动设备的相关参数。这个设备独立像素,我们用Dimensions API可以获取到

import {Dimensions} from "react-native"
let size = Dimensions.get("window");
console.log("width: " size.width + " height: " + size.height);

而像素密度比pixelRatio,就是物理像素除以独立像素的值。比如我手里的坚果pro,它的物理像素是1080*1920,设备独立像素是360*640,所以它的像素密度是3。 在上面界面里也可以看到,例如ipad, GalaxyTab等平板一般像素密度是1,中断设备像素密度是2,高端些的设备像素密度就是3,当然也有一些设备像素密度是1.5或者2.5等。

首先我们要明确一点,我们在给图片设置尺寸时,是不需要写单位的,实际上单位就是独立像素。

假设有两个设备,它们的独立像素相同,都是360*640,那么我有一张图片,设置宽度为180,在这两个设备上都是占了一半屏幕宽。而如果有一个pad它的独立像素是768*1024,我们仍然希望它占一半屏幕的话,就需要把宽度设置为768*0.5=384了。例如对于背景图片,显然需要在任何设备上都占满屏幕,所以给它设置尺寸直接就是设备独立像素的值,也就是Dimension.get(“window”)得到的值。有的时候我们不希望随着屏幕变,例如那张图片仍然设置宽度为180,那么它在pad上宽度只占了23.4%。所以我们可以得知,给图片设置尺寸时,只决定于它在不同的独立像素屏幕上,需要显示成什么效果。最简单的方案是两种,要么设为固定值,要么跟屏幕独立像素成比例。否则就根据不同的屏幕来细分了。

那么这个像素密度拿来做什么用呢?

仍然说那两个设备,它们独立像素都是360*640,A设备像素密度为1,B设备像素密度为3,很容易得知A设备物理像素为360*640,而B设备物理像素为1080*1920。我有一张图片,美术给的尺寸是360*360,现在我给它设的尺寸宽度为180*180,根据上面所说,在这两个设备上图片都占屏幕一半宽,但A设备屏幕一半的物理像素是180,对于图片物理像素宽度360来说,缩小了一半。B设备屏幕一半的物理像素是540,对于图片物理像素宽度360来说,放大了1.5倍。不论放大还是缩小,都会导致图片显示模糊。如果要达到最好的显示效果,就应该给A设备提供180*180尺寸的图片,给B设备提供540*540尺寸的图片,这就是像素密度的作用。

是否要根据不同屏幕提供多套素材资源,取决于项目要求。我们在实际项目中可以使用pixelRation接口来实现物理像素和独立像素之间数值的转换,只要记住物理像素=独立像素*像素密度这个公式即可。

ReactNative拆分bundle实践

实践了一下最简单的ReactNative bundle拆分方案。将项目bundle拆分成RN源代码和业务代码,这样在热更新时不用每次都更新整个bundle。

首先新建一个项目testrn,将index.android.js内代码注释掉,只保留两行import

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

然后使用bundle命令打包成common.bundle,这就是RN源代码部分

react-native bundle --entry-file ./index.android.js --platform android --dev false --bundle-output ./output/common.bundle

这里dev可以选择true或者false,如果为了试验建议先设成true,正式使用时使用false

然后我们将index.android.js内注释代码打开,再使用bundle命令打包成total.bundle

我们在total.bundle内搜索项目名testrn,很容易找到这么一段代码:(或者使用软件例如compareBeyond对比)

__d(/* testrn/index.android.js */function(global, require, module, exports) {Object.defineProperty(exports, "__esModule", {
    value: true});
    var _jsxFileName = 'F:\\test\\testrn\\index.android.js';

    //中间代码省略...

    _reactNative.AppRegistry.registerComponent('testrn', function () {
    return testrn;
    });
}, 0, null, "testrn/index.android.js");

其实这是整个的一句,从当前的__d到下个__d

我们把这一段放到一个新建文件叫bussiness.bundle内,同时把common.bundle的最后两行剪切到bussiness.bundle的最后

;require(120);
;require(0);

然后我们修改一下C++代码,加载bundle时改成读取common.bundle和bussiness.bundle并连接起来,就可以了。我的修改代码如下,已经测试运行正常:

//node_modules\react-native\ReactAndroid\src\main\jni\xreact\jni\CatalystInstanceImpl.cpp
void CatalystInstanceImpl::jniLoadScriptFromAssets(
    jni::alias_ref<JAssetManager::javaobject> assetManager, const std::string& assetURL) {
    const int kAssetsLength = 9;  // strlen("assets://");
    auto sourceURL = assetURL.substr(kAssetsLength);

    auto manager = extractAssetManager(assetManager);
    // auto script = loadScriptFromAssets(manager, "index.android.bundle");
    auto script1 = loadScriptFromAssets(manager, "common.bundle");
    auto script2 = loadScriptFromAssets(manager, "diff.js");

    auto script = folly::make_unique<JSBigBufferString>(script1->size()+script2->size());
    memcpy(script->data(), script1->c_str(), script1->size());
    memcpy(script->data() + script1->size(), script2->c_str(), script2->size());

    if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) {instance_->loadUnbundle(
        folly::make_unique<JniJSModulesUnbundle>(manager, sourceURL),
        std::move(script),
        sourceURL);
        return;
    } else {
        instance_->loadScriptFromString(std::move(script), sourceURL);
    }
}

Redux中的Provider和connect

redux里有不少概念,一时半会看不明白,这里讲下我理解的provider和connect。我们知道使用Reudx,数据作为state被存储在一个单独的store中。我们在渲染时从state获取数据,需要修改数据时,dispatch一个action即可。provider和connect的作用,是为了更方便的存取state数据。

如果不使用provider和connect,也是完全没问题的,这样的话我们需要把store传入到Component中以便使用,例如需要这么写:

index.android.js:	//此代码缺少action和reducer的实现,并不能直接运行,只是用以描述

let store = createStore(reducer);
export default class testrn extends Component{
    render(){
        <NoConnect store={store}/>
    }
}

NoConnect.js:

export default class NoConnect extends Component{
    constructor(props){
        super(props);
        this.state = this.props.store.getState();
    }
    render(){
        return (<View>
            <Text>this.state.prop1</Text>
            <Button title="click" onPress={()=>{
                this.props.store.dispatch(action);
                this.setState(this.props.store.getState());
            }}>
        </View>)
    }
}

从上面可以看出来,我们主要是通过把store传入Component,然后利用它的getState和dispatch接口进行存取数据,数据需要保存为自身的state,在dispatch后需要通过setState来刷新界面。

如果使用provider和connect,就可以大大简化代码了。例如

index.android.js:

let store = createStore(reducer);
export default class testrn extends Component{
    render(){
        <Provider store={store}>
        <TestContainer  />
        </Provider>
    }
}

testContainer.js:
import { connect } from 'react-redux';
class testContainer extends Component{
    render(){
        return (<View>
            <Text>this.props.text1</Text>
            <Button title="click" onPress={()=>{
                this.props.change1("123");
            }}>
        </View>)
    }
}

const mapStateToProps = (state, ownProps)=>{
    return {
        text1:state.prop1
    }
}

const mapDispatchToProps = {
    change1:Actions.createAction1
}

export default connect(mapStateToProps, mapDispatchToProps)(testContainer)

从上面代码可以看出来,在Component里不再需要使用store变量,代码简化了。只要被Provider包含着的组件及其子组件,都可以使用connect方法,就不需要到处传递store变量了。

mapStateToProps的作用,就是把state内的值转换成Component的props的值,这样在Component内使用时,不需要通过store.getState来获取并存为自己的state了。每当state发生变化时,这个方法就会被调用,这样Component的props就被修改了,于是就不用再通过setState来通知页面刷新。

mapStateToProps是一个方法,它的第一个参数就是state,第二个参数ownProps是传递给Component的props。在上述代码中就是<TestContainer />处传递的props

mapDispatchToProps的作用,就是把创建action的方法,绑定在Component的props上,这样就不需要通过store.dispatch来更改state。

上述代码中mapDispatchToProps是一个Object,它的values就是创建action的方法,keys绑定为Component的props属性,这样在testContainer中调用this.props.change1就创建并发布了一个action。

mapDispatchToProps也可以是一个方法,它的第一个参数是store的dispatch方法,第二个参数是ownProps。用方法实现比起用Object实现,可以做更多的注入操作,代码如下

const mapDispatchToProps = (dispatch, ownProps) {
    return {
        change1:(data)=>{
            console.log("dispatch action1.....");
            dispatch(Actions.createAction1(data));
        }
    }
}

可以看到如果用方法来实现,在change1里就可以做很灵活的操作了。同时Redux提供了一个很简便的接口bindActionCreators(Actions, dispatch),使用它相当于使用Object实现,并且key和创建action的方法名一致。

上面代码里用到的Actions,是创建action的接口

除了上面提到的mapStateToProps和mapDispatchToProps外,connect方法后面还可以再带两个参数

mergeprops

options:

ES6中合并import

最近打算优化一下ReactNative项目的代码,项目中有个公共模块文件夹utils,里面有不少文件,使用的也很频繁,这就导致了在很多js文件里都有这么一大段代码

import * as A from "./utils/a" 
import * as B from "./utils/b" 
...
import * as G from "./utils/g" 
import * as H from "./utils/h" 

于是我就打算把整个utils内的文件做个汇总,具体实现方案是,在utils文件夹内添加一个index.js,其内容为

import * as A from "./utils/a" 
import * as B from "./utils/b" 
...
import * as G from "./utils/g" 
import * as H from "./utils/h"
export {A, B, ... G, H}

然后原来每个import utils内文件的地方,就改成了

import {A, B, ... G, H} from "./utils/index"

我总结一下这样改的优劣。

好处是

  1. 暴露一个唯一的接口,可以保持一致。原来在不同的文件里import同一个接口,可能取不同的名字,在这个文件里是import * as A from './utils/a',另一个文件里就可能是import * as B from './utils/a'了,文件多了容易导致混乱,但我们统一成一个接口,除非特意使用as重命名,否则每个文件里都是一致的
  2. 可以看出来用新的方法减少了一定的代码量,看起来舒服一些。RN本身也是这种风格,例如import {Text, Button} from 'react-native'。这样汇总尤其适合用于对外暴露接口。

当然也有一些不好的地方

  1. 多维护了一个文件,当utils内新加文件时,需要到这个index.js内添加。
  2. 被汇总的文件,要么只export default一个接口,要么以import *的方式被汇总。否则如果从这个文件里import若干个,再从另一个文件里import几个进来,然后汇总出去,就显得比较乱了,会分不清哪个接口是哪个文件的。

2017/12/15新加:

最近重构项目,把utils内一些文件挪到别的文件夹,这时才感受到汇总import再export最大的好处了,如果使用汇总的方案,那只需要改index.js一个文件,其余所有的都不用动。 但如果不进行汇总,那有多少个地方import了就需要改多少个地方,漏一个都报错。这应该是使用汇总方案最大的好处吧。