github上的博客系统 github上的博客会读取docs文件夹中的markdown和html文件
npm run build的时候是带包到dist文件夹下的,把dist文件夹下的内容拷贝到了docs下
基本数据类型
undefined、number、string、boolean
引用数据类型
null、Object、Number、String、Boolean、Function、Array、Date、RegExp、Error、Arguments
typeof操作符可能返回下面几种字符串
undefined boolean string number 都是基本的数据类型
function 和 object是引用类型,变量指向的是对象的地址,
对于引用类型的变量,typeof只可以区分出function,其他类型的统一识别成object。
boolean string 和number这三种基本的数据类型,都有对应的引用包装类型
Boolean String 和Number。
对于这些包装类型的变量,typeof统一识别成object
var a = new String('hello');
typeof a // object
var b = 'hello';
a === b // false
a ==b //true
a 实际变成了一个String类型的引用变量
所以a === b 是false,但是用== 比较的时候 b隐式调用了toString的方法 所以是true
多说一句,其实我们在调用基本类型的方法的时候,都是隐式的转为包装对象以后才能调用。
instanceof 应用于引用类型的判断,所以对于string number boolean 这三类基本类型没有什么意义。
instanceof 支持继承 因为所有的引用类型都继承自Object,所以所有引用变量都是Object的实例
var a = new String('hello');
a instanceof String //true
a instanceof Object //true
var b = 'hello';
b instanceof String // false
我开始以为instanceof是通过判断a的__proto__
上的constructor
属性来判断构造函数的类型,但是改变a.__proto__.constructor = Number
之后
a instanceof String
仍然为true
var a = new String('hello');
a.__proto__.constructor = Number;
a instanceof String //true
a instanceof Number //false
这个是通过调用Object原型上的toString方法来判断变量的类型
这个方法不会区分是基本类型还是包装的引用类型,其实大多数情况下我们真不不需要区分。
var a = new String('hello')
var b = 'hello';
Object.prototype.toString.call(a) //"[object String]"
Object.prototype.toString.call(b) //"[object String]"
该方法还能够区分null和undefined
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
所以判断数据类型最靠谱的方法就是这个了。
//代码中的toString 方法 就是Object.prototype.toString
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
_.each(['Arguments','Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
});
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};
虽然这个方法很好但是没有办法区分基本类型和引用类型,采用typeof可以判断:
// 判断是否是引用类型
_.isObject = function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
};
//通过instanceof Object 应该也可以判断 是不是引用类型 并且不是null和undefined
// 判断是否为数组
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) === '[object Array]';
};
//nativeIsArray 是ES5原生的Array.isArray
//判断是否是NaN,利用NaN是唯一一个不等于自己的Number类型
_.isNaN = function(obj) {
return _.isNumber(obj) && obj !== +obj;
};
_.isUndefined = function(obj) {
return obj === void 0;
};
_.isNull = function(obj) {
return obj === null;
};
_.isFinite = function(obj) {
return isFinite(obj) && !isNaN(parseFloat(obj));
};
//在 IE < 9 下对 arguments 调用 Object.prototype.toString.call,结果是 [object Object],所以利用他的callee属性来判断
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return _.has(obj, 'callee');
};
}
第一种ES5写法,React.createClass
React.createClass({
getDefaultProps() {
return {
key1:value1
}
},
getInitialState() {
return {
state1:state1
}
}
});
第二种是ES6写法,继承React.Component类
export default class Test1 extends React.Component {
constructor (props) {
super(props);
this.state = {
state1: state1
}
}
static defaultProps = {
data: 2,
};
static propTypes = {
optionsData: React.PropTypes.array,
onSelect: React.PropTypes.func,
selectedOption: React.PropTypes.object,
topStyle: React.PropTypes.any,
placeholder: React.PropTypes.string
}
}
getDefaultProps、getInitialState是在createClass时调用的方法,而在继承component类时,getDefaultProps方法对应给类添加静态属性 defaultProps ,getInitialState 对应在类的构造函数中设置 state属性
组件首次装载(first-Mount):
卸载组件时(Unmount):componentWillUnmount()
当重新装载组件时(Re-Mount):
当再次渲染组件时(Re-Render),此时按顺序执行
单独调用setState的重新渲染
1、在单页应用中,用react-router的history做页面跳转时,是将当前route的所有组件卸载,再跳转回来时是重新装载组件,而不是首次装载。
2、在使用react-native的Navigator时,每次push新页面的时候是首次加载,旧页面没有卸载,在pop新页面的时候,新页面会卸载 调用Unmount,旧页面是重新渲染
3、组件在内存中装载过一次之后,组件的defaultProps就初始化了,之后装载就不会重新设置。
4、父组件的render都会引起子组件的重新渲染。
5、 不能在componentWillUpdate ,render和componentDidUpdate 中调用setState
3.1 getDefaultProps方法 或者 defaultProps 对象
3.2 getInitialState方法或constructor的state属性
项目中会把组件用到的state初始化在这里
constructor (props) {
super(props);
this.state = {
poi: null,
activeTabId: null,
cartCount: Shopcart.size(),
domSize:{
headerHeight: 100,
bannerHeight: 200,
categoryTabHeight: 100,
},
hiddenBanner: false //是否隐藏banner
};
}
3.3 componentWillMount()
组件初次render之前调用,如果在此方法中调用setState,render将感知到更新后的state,并且只执行这一次render,可以在此方法中fetch数据,不需要dom操作的数据获取。
3.4 render()
组件渲染的方法,是组件唯一的必须实现的方法,在该方法中,我们可以通过props和state渲染不同的组件。返回null或者false代表不渲染任何东西。
render () {
return (
<header className="header-wrapper">
<div className="header header-normal">
{this.renderLeftComponent()}
<span>{this.props.title || '美团商超'}</span>
{this.renderRightComponent()}
</div>
</header>
);
}
3.5 componentDidMount()
组件装载后调用的方法,因为该方法调用的时候,组件已经装载,并且该方法不在render的循环当中,一般在该方法中做一些fetch数据或者改变state的方法。
还可以通过ReactDOM.findDOMNode(_this.refs.wrapper) 来获取DOM节点 进行操作。
componentDidMount() {
this.mounted = true;
if(this.props.poi){
this.fetchCategoryTabs(this.props.poi.poiId);
}
if(!this.isCalculate) {
this.calculateWidth();
}
}
3.6 componentWillReceiveProps(nextProps)
在组件接收到新的 props 的时候调用。在初始化渲染的时候,该方法不会调用。可以在该方法中判断,当props变化时,是否再去重新fetch数据,setState。
componentWillReceiveProps (nextProps) {
if(nextProps.poi &&(nextProps.poi != this.props.poi)) {
this.fetchBannerList(nextProps.poi.poiId);
}
}
3.7 shouldComponentUpdate(nextProps, nextState)
在接收到新的props或者state变化时,被调用,该方法在初始化渲染和forceUpdate的时候不会被调用。
默认返回true,如果返回false,则render不会执行。可以在这个方法中来阻止不必要的render,因为有时是因为父组件的render引起的子组件不必要的render。
shouldComponentUpdate(nextProps, nextState) {
const isStateChanged = Object.keys(nextState).some(key=> {
return nextState[key] !== this.state[key]
});
const isPropsChanged = Object.keys(nextProps).some(key=> {
return nextProps[key] !== this.props[key]
});
return isStateChanged || isPropsChanged
}
3.8 componentWillUpdate(nextProps, nextState)
在接收到新的 props 或者 state 之前立刻调用。在初始化渲染的时候该方法不会被调用。使用该方法做一些更新之前的准备工作。你不能在刚方法中使用 this.setState()。如果需要更新 state 来响应某个 prop 的改变,请使用 componentWillReceiveProps。 项目中应用不多。
3.9 componentDidUpdate
在组件的更新已经同步到 DOM 中之后立刻被调用。该方法不会在初始化渲染的时候调用。使用该方法可以在组件更新之后操作 DOM 元素。
有些操作可能需要操作DOM元素,并且在第一次componentDidMount时还没有达到条件,所以需要在componentDidUpdate时再做操作,但是componentDidUpdate在render的循环函数中,
所以需要设置变量做控制。
下面例子中 this.isCalculate 就是判断是否计算过的变量。
componentDidMount() {
this.mounted = true;
if(this.props.poi){
this.fetchCategoryTabs(this.props.poi.poiId);
}
if(!this.isCalculate) {
this.calculateWidth();
}
}
componentDidUpdate () {
if(!this.isCalculate) {
this.calculateWidth();
}
}
calculateWidth () {
if(this.isCalculate) {
return;
}
let tablist = this.state.categoryTabs;
if(tablist.length == 0) {
return;
}
let tabsDOM = this.refs.tablist,
childrensDOM = tabsDOM.childNodes,
container = this.refs.tabcontainer,
wrapper = this.refs.wrapper,
// 横向滚动宽度
scrollwidth = 5;
for(let i=0; i<childrensDOM.length; i++){
let childDOM = childrensDOM[i];
scrollwidth += childDOM.clientWidth + parseInt(childDOM.style.marginRight);
}
scrollwidth = Math.max(tabsDOM.clientWidth,scrollwidth);
this.setState({tabsWidth: scrollwidth + 'px'});
this.props.setCategoryTabHeight(wrapper.offsetHeight);
this.isCalculate = true;
}
3.10 componentWillUnmount
在组件从 DOM 中移除的时候立刻被调用。在该方法中执行任何必要的清理,比如无效的定时器,或者清除在 componentDidMount 中创建的 DOM 元素。
可以记录组件的mount状态,在 componentDidMount 中设置this.mounted = true 。 在componentWillUnmount 中设置 this.mounted = false。
由于手机屏幕高度不定,做表单页面时,外层通常加上ScrollView组件,使其能够适应屏幕进行滚动。业务需要里面放置多个TextInput组件。
出现的问题是,首次点击focus TextInput,键盘弹出,然后想要点击连续focus TextInput时,结果没有focus,而是键盘收起,需要再次点击TextInput 进行focus。
结果就是每次切换输入框都需要点击两次。
这个问题使得app的体验差到了极点,饱受用户吐槽。
保持键盘一直展开,然后点击的时候,能够判断点击的组件是否为TextInput,如果是则键盘不收起,否则键盘收起。
阅读ScrollView的源码可以发现,它有一个属性keyboardShouldPersistTaps,默认值为false,表示只要点击当前focus组件外的地方,都会引起键盘的收起。
这个是不能连续focus TextInput的原因,引起键盘收起后,还捕获了点击事件,使得focus失效。 所以我们应该把ScrollView的这个属性设置为 true,让键盘一直保持弹出状态。
它还有一个属性keyboardDismissMode,是枚举类型: 代表键盘在drag的时候是否收起
'none' 是默认值,代表键盘一直展开
'on-drag' 代表 ScrollView拖动的时候 键盘收起 在android下无效
'interactive' 翻译过来是互动的时候键盘收起
但是发现在android和ios下也没什么效果
尝试着用这个属性去收起键盘,但是效果不好,并且不兼容android。
最终我们对ScrollView的操作是加上属性 keyboardShouldPersistTaps = true
如何判断点击到的组件是TextInput ?
阅读View的原文我们可以发现属性 onStartShouldSetResponderCapture,这个属性接收一个回调函数,函数原型是 function(evt): bool,在触摸事件开始(touchDown)的时候,RN 容器组件会回调此函数,询问组件是否要劫持事件响应者设置,自己接收事件处理,如果返回 true,表示需要劫持,如果返回false,表示不劫持。
传给回调函数的event里,包含一个触摸事件参数nativeEvent。nativeEvent 的详细内容如下:
identifier:触摸的 ID,一般对应手指,在多点触控的时候,用来区分是哪个手指的触摸事件;
locationX 和 locationY:触摸点相对组件的位置;
pageX 和 pageY:触摸点相对于屏幕的位置;
timestamp:当前触摸的事件的时间戳,可以用来进行滑动计算;
target:接收当前触摸事件的组件 ID;
changedTouches:evt数组,从上次回调上报的触摸事件,到这次上报之间的所有事件数组。因为用户触摸过程中,会产生大量事件,有时候可能没有及时上报,系统用这种方式批量上报;
touches:evt 数组,多点触摸的时候,包含当前所有触摸点的事件。
这里我们用到的是target属性,它就代表点击的组件ID。
React-Native官网的0.28版文档中,View和TextInput组件中都有属性onLayout,这个属性接收一个回调函数,函数原型是 function(evt),在mount组件和layout的组件的时候触发该事件,传给回调函数的event里,参数nativeEvent,其中的target属性为该组件的ID。
所以可以在TextInput组件layout时,将他们的ID存放在数组中,然后判断触发事件的组件ID是否在数组中,来确定该组件时候为TextInput。
React-Native官网的0.28版文档中,View和TextInput组件中都有属性onLayout,这个属性接收一个回调函数,函数原型是 function(evt),在mount组件和layout的组件的时候触发该事件,传给回调函数的event里,参数nativeEvent,其中的target属性为该组件的ID。
所以可以在TextInput组件layout时,将他们的ID存放在数组中,然后判断触发事件的组件ID是否在数组。
0.28版本之前 TextInput组件没有onLayout属性,所以在0.28版本中TextInput没有onLayout属性,hack的方法是用View包裹TextInput,然后给View加onLayout属性,
获取到的是View的组件ID,调试发现,View里面的TextInput的ID是外层View的ID+1,通过这种方法获取的TextInput的组件ID,在0.29以后的版本中,可以直接给TextInput加onLayout属性了。
react-native 中有dismissKeyboard模块,用于设置键盘的收起,直接调用该模块方法,即可收起键盘。
const dismissKeyboard = require('dismissKeyboard');
const inputComponents = [];
_onStartShouldSetResponderCapture (event) {
let target = event.nativeEvent.target;
if(!inputComponents.includes(target)) {
dismissKeyboard();
}
return false;
}
_inputOnLayout(event){
inputComponents.push(event.nativeEvent.target);
}
<ScrollView ref="scrollView" keyboardShouldPersistTaps = {true} >
<View onStartShouldSetResponderCapture={this._onStartShouldSetResponderCapture.bind(this)}>
<TextInput onLayout={this._inputOnLayout.bind(this)} style={{flex: 1}}/>
</View>
</ScrollView>
在项目开发的过程中,随着应用功能复杂度的增加和组件层次划分的需求,组件之间的通信越来越多,
我大致认为组件之间的通信分为3种:父-子组件通信、子-父组件通信和同级组件之间的通信。
这是最常见的通信方式,父组件只需要将子组件需要的props传给子组件,子组件直接通过this.props来使用。
更多要提的是如何合理的设置子组件的props,要想将子组件设计成一个复用性强的通用组件,需要将能够复用的部分抽象出来,
抽象出来的props有两种形成,一种是简单的变量,另一种是抽象出来处理某种逻辑的函数。
抽象出来三个props,分别是中间的title,渲染组件左边的renderLeftComponent,渲染组件右边的renderRightComponent
Header的 部分实现
renderLeftComponent () {
let leftDOM = {};
if(this.props.renderLeftComponent) {
return this.props.renderLeftComponent();
}
if (this.props.showBack) {
let backFunc = this.props.onBack || this.goBack;
leftDOM = (<a onClick={backFunc.bind(this)}><i className="icon left-icon icon-left-arrow"></i></a>);
}
return leftDOM;
}
renderRightComponent () {
if(this.props.renderRightComponent) {
return this.props.renderRightComponent();
}
}
render () {
return (
<header className="header-wrapper">
<div className="header header-normal">
{this.renderLeftComponent()}
<span>{this.props.title || '维C果蔬'}</span>
{this.renderRightComponent()}
</div>
</header>
);
}
1.1中Header组件 props的通信动机 是子组件需要这样的属性来完成自己的展示。还有一种动机可以统称向子组件传递监听事件,
前一种是子组件的需求,后一种更多的是父组件的需求,例如Listview的onEndReached这种属性,触发源是在子组件中,当子组件
的事件被触发或者达到某种状态的时候,调用父组件从属性中传过来的方法。
父-子组件通信的手段是通过子组件的props是子组件用父组件的东西,子-父组件通信,是父组件用子组件的东西,应该将传递的内容直接写在子组件上,然后给子组件设置ref,父组件直接通过ref操作子组件的属性。
子组件的属性
父组件想要调用子组件的属性
同级组件之间的通信,是构建复杂界面的粘合剂,哎呦喂,我好会比喻啊
以维C果蔬的首页为例:
通信1: Listview需要offsetHeight属性,Listview
Height的计算公式:window.innerHeight-Banner的Height-Menu的Height,
而Banner和Menu的Height都是需要在其Mount的时候才能计算。
通信2: ListView需要Menu的MenuId,才能够根据MenuId获取sku数据。
同级组件之间的通信还是需要通过父组件作为中介,利用多次父-子组件通信,项目中将需要传递的数据放在了父组件的state中,变动时可以自动的同步传递。
将 bannerHeight,menuHeight,MenuId放在state中。
父组件代码示意:
this.state {
bannerHeight: 0,
menuHeight: 0,
MenuId: 0
}
setBannerHeight(height) {
this.setState({bannerHeight:height});
}
setMenuHeight(height) {
this.setState({menuHeight:height});
}
onMenuClick(menuId) {
this.setState({menuId:menuId});
}
<Banner setBannerHeight={this.setBannerHeight.bind(this)} />
<Menu setMenuHeight={the.setMenuHeight.bind(this)} onMenuClick={this.onMenuClick.bind(this)} />
<SkuList offsetHeight={this.state.bannerHeight + this.state.menuHeight} menuId={this.state.menuId} />
当组件需要的props,不能直接从父组件中获取时,需要父组件作为中介,再与其他的组件进行通信获取。
管理Web应用全局状态的框架。
单页面应用,顾名思义,和传统项目的最明显区别就是项目只有一个页面,页面有一个根元素,我们写的每一个页面是一个大的组件,前端接管路由来渲染不同的页面组件。
随着应用的复杂,前端需要存储更多的state,包括缓存的全局数据,本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
如果是单纯的数据缓存 也没什么需要考虑的东西,放到内存就可以了, 重点是还要让state和view有绑定的关系。state的变化能够触发view的变化。
在我们的项目中,没有用redux之前,我们都是页面组件管理state,出现以下需求的时候,写起来就比较复杂
(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)所有的状态,保存在一个对象里面。
执行的动作,包括动作所需要的数据,改变store数据的唯一来源,一般是通过store.dispatch() 将 action 传到 store。
Action 本质上是 JavaScript 普通对象。
flux-standard-action FSA标准
###3.2 Reducer
根据action 做更新state的操作。
Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。
Store就是保存全局state的容器,保存三个常用的api
用户通过View,dispatch 相应的action,store调用reducer获取最新的state,并触发通过subscribe订阅的监听函数,监听函数中我们通过store的getState方法获取最新的state,更新view。
工作流程图
)
simpleredux/index.js
import {createStore} from 'redux';
export function createAction(type, payload) {
return {
type,
payload
}
}
const initialState = {
time: new Date().getTime()
}
function reducer(state = initialState, action) {
switch (action.type) {
case 'NOW_TIME':
return {
...state,
time: action.payload
}
default:
return state;
}
}
let store;
export function getStore() {
if(store) return store;
return store = createStore(reducer);
}
TestRedux.js
'use strict';
import React, { Component } from 'react';
import {
StyleSheet,
View,
Text
} from 'react-native';
import MtButton from '@scfe/react-native-button';
import {getStore, createAction} from '../../simpleredux/index';
const store = getStore();
class TestRedux extends Component {
constructor(props) {
super(props);
let state = store.getState();
this.state = {
time: state.time
};
store.subscribe(()=>{
let state = store.getState();
this.setState({
time: state.time
});
});
}
_sendAction() {
let action = createAction('NOW_TIME', new Date().getTime());
store.dispatch(action);
}
render() {
return (
<View style={styles.container}>
<Text>{this.state.time}
</Text>
<MtButton text="发出action" onPress={this._sendAction.bind(this)} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 40
}
});
export default TestRedux;
1.android下设置TextInput的height很小以后,大概是小于40,里面的文本显示不全
出现原因:
问题是因为android下的TextInput上下有最小的padding,height很小以后,paddingBottom + fontSize > height 所以显示不全。
解决办法:
给TextInput的paddingBottom 设置的小一点。让font能够在height里面垂直居中。
2.android下View设置的zIndex属性有显示bug
使用场景:
写的Select组件,想让option显示在上层,所以用的zIndex属性,但是在android下时出现显示的问题。
解决办法:
首先将Select组件放在布局的最后面,然后使用position: absolute 绝对定位,定位到需要的位置,因为Select组件放在的是布局的最后面,所以他也会在最上层显示
1、开发者在写测试脚本的时候,能够更好的理解代码的的功能,返回值等等。
2、能够实现准确直接的测试,并立即看到测试结果,进行调整。
3、面对复杂的项目,对代码的修改有可能会牵一发动全身,代码的改动可能会影响到其他部分的功能,自动化测试能帮我们整体检查一遍。
4、测试的结果能够当做一个代码质量的依据。
在segmentfault上搜索“探知js测试”,可以得到三篇系列文章对js测试进行讲解,第一篇的地址:https://segmentfault.com/a/1190000004428902
需要用到的知识包括:BDD的测试模式、Mocha测试框架、chai断言库,更倾向使用expect/should、istanbul 测试覆盖率工具,
这里有篇简单介绍 http://www.ruanyifeng.com/blog/2015/06/istanbul.html,需要学一下makefile的使用,supertest 测试api接口的工具
测试的项目:https://github.com/yylgit/test-demo
项目接入travis平台
建立.travis.yml文件,文件内容
language: node_js
node_js:
- "5"
- "4"
travis 执行的是package中的scripts的test命令
接入后在travis平台上可以看到每当仓库有变动时重新执行测试,https://travis-ci.org/yylgit/test-demo
travsi每次都是在新的环境中进行测试
接入 coveralls平台,https://coveralls.io/github/yylgit/test-demo
node项目利用 node-coveralls +istanbul
redux-action的createAction方法就是给我们提供了灵活的创建符合FSA标准的action的方法。
exports.default = createAction;
/**
返回创建action的函数
*/
function createAction(type, payloadCreator, metaCreator) {
var finalPayloadCreator = typeof payloadCreator === 'function' ? payloadCreator : _identity2.default;
/**
返回的函数
*/
var actionHandler = function actionHandler() {
var hasError = (arguments.length <= 0 ? undefined : arguments[0]) instanceof Error;
/**
返回的action
*/
var action = {
type: type
};
//根据传入的参数,执行payloadCreator获取payload
var payload = hasError ? arguments.length <= 0 ? undefined : arguments[0] : finalPayloadCreator.apply(undefined, arguments);
if (!(payload === null || payload === undefined)) {
action.payload = payload;
}
if (hasError) {
// Handle FSA errors where the payload is an Error object. Set error.
action.error = true;
}
//根据传入的参数,执行metaCreator获取payload
if (typeof metaCreator === 'function') {
action.meta = metaCreator.apply(undefined, arguments);
}
//可以看到 payloadCreator和metaCreator的参数都是用的传给actionHandler的参数
return action;
};
actionHandler.toString = function () {
return type.toString();
};
return actionHandler;
}
redux的combineReducer方法 用于将多个reducer,合并成一个大的reducer函数,传给store。
用于分解state树,每一个reducer对应state的一个key对应的子state。比如poi的reducer对应的就是state[poi]。这样在将state传递给props时利于分解。
reducer以对象的形式传入,finalReducers 存放最终的reducer,finalReducerKeys存放reducer的key
最终返回 combination函数 reducer类型的函数,接受state和action 返回state
state的形式是一个大对象下面每一个reducer对应一个子state。
触发一个action,会遍历所有的reducer,
将该reducer的旧state和action传入,然后根据返回的新的state对象是否改变,来决定
最终的返回的state是否改变。
这里需要注意:由于state都是引用类型,这里比较是值比较
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
所以如果我们想要改变全局的state,需要在reducer中返回新的对象,而不是原来的state对象,
如果返回原来的对象,即使对象里的值改变了,也不会引起全局state的改变。
*/
export default function combineReducers(reducers) {
var reducerKeys = Object.keys(reducers)
var finalReducers = {}
for (var i = 0; i < reducerKeys.length; i++) {
var key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
var finalReducerKeys = Object.keys(finalReducers)
return function combination(state = {}, action) {
/**
校验语法错误,reducer返回的state不能是undefined
*/
if (sanityError) {
throw sanityError
}
var hasChanged = false
var nextState = {}
for (var i = 0; i < finalReducerKeys.length; i++) {
var key = finalReducerKeys[i]
var reducer = finalReducers[key]
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
var errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
/**
所以如果我们想要改变全局的state,需要在reducer中返回新的对象,而不是原来的state对象,
如果返回原来的对象,即使对象里的值改变了,也不会引起全局state的改变。
*/
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
应用中间件的目的是包装dispatch,在action传递给dispatch执行之前,需要经过中间件的层层处理,进行一些业务上的处理,决定action的走向。
源码实现了这种将函数数组,通过reducerRight的方法,实现层层嵌套的执行,达到中间件的实现。
/**
显示执行中间件,得到中间件的返回函数数组chain,然后利用compose方法,生成嵌套的执行chain
方法的包装dispatch函数,
中间件的形式是
(getState, dispatch)=> next => action => {
next(action);
}
*/
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
/**
store.dispatch 就是第一个next 是last ware的next
(...args) => {
return ware0(ware1(ware2(last(...args))))
}
dispatch = ware0(ware1(ware2(last(...args))))
所以中间件中next传入后返回的函数就是我们需要的函数形式,
例如dispatch 需要的函数形式是 传一个action
*/
return {
...store,
dispatch
}
}
}
/**
reduceRight是数组的从右至左执行,
初始的参数是最后一个函数接受dispatch,
的到的一个action=>{
dispatch(action);
}
形式的函数,作为参数composed
f的形式是
next=>action=>{
}
最终形成的就是
(...args) => {
return funcs0(funcs1(funcs2(last(...args))))
}
*/
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
中间件执行过程模拟
中间件原理
*/
function func1 (next) {
console.log('func1 return');
return function (action) {
console.log('func1start');
next(action);
console.log('func1end');
}
}
function func2 (next) {
console.log('func2 return');
return function (action) {
console.log('func2start');
next(action);
console.log('func2end');
}
}
function func3 (next) {
console.log('func3 return');
return function (action) {
console.log('func3start');
next(action);
console.log('func3end');
}
}
function dispatch(action) {
console.log(action);
}
function afterCompose (args) {
return func1(func2(func3(args)));
}
var newdispatch = afterCompose(dispatch);
newdispatch('action');
/**
执行顺序
func3 return
func2 return
func1 return
func1start
func2start
func3start
action
func3end
func2end
func1end
*/
应用场景参见中间件的应用代码与applyMiddleware源码,是redux提供创建store的方法。
import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'
export var ActionTypes = {
INIT: '@@redux/INIT'
}
export default function createStore(reducer, preloadedState, enhancer) {
var currentReducer = reducer
var currentState = preloadedState
var currentListeners = []
var nextListeners = currentListeners
var isDispatching = false
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function getState() {
return currentState
}
/**
订阅监听
*/
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
var isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners()
var index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
/**
执行reducer,获取state,执行listener
*/
function dispatch(action) {
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
var listeners = currentListeners = nextListeners
for (var i = 0; i < listeners.length; i++) {
listeners[i]()
}
return action
}
/**
替换reducer
*/
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.INIT })
}
/**
store创建的时候,获取初始的sate树
*/
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer
}
}
redux和react的结合,Provider作为根组件,将store的state放在context中供子组件使用。
import { Component, PropTypes, Children } from 'react'
import storeShape from '../utils/storeShape'
import warning from '../utils/warning'
export default class Provider extends Component {
//把 store 放在context里面,给子元素用
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
render() {
const { children } = this.props
//渲染唯一的子元素
return Children.only(children)
}
}
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired
}
Provider.childContextTypes = {
store: storeShape.isRequired
}
connect方法,将React的组件进行包装,包装的目的如下:
const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars
const defaultMapDispatchToProps = dispatch => ({ dispatch })
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
//返回包装组件的函数
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
shouldComponentUpdate() {
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
}
constructor(props, context) {
super(props, context)
this.version = version
this.store = props.store || context.store
const storeState = this.store.getState()
this.state = { storeState }
this.clearCache()
}
isSubscribed() {
return typeof this.unsubscribe === 'function'
}
trySubscribe() {
if (shouldSubscribe && !this.unsubscribe) {
//订阅store的state变化
this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
this.handleChange()
}
}
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
}
}
componentDidMount() {
//订阅store的state变化
this.trySubscribe()
}
componentWillReceiveProps(nextProps) {
if (!pure || !shallowEqual(nextProps, this.props)) {
this.haveOwnPropsChanged = true
}
}
componentWillUnmount() {
this.tryUnsubscribe()
this.clearCache()
}
//订阅变化
handleChange() {
if (!this.unsubscribe) {
return
}
const storeState = this.store.getState()
const prevStoreState = this.state.storeState
if (pure && prevStoreState === storeState) {
return
}
if (pure && !this.doStatePropsDependOnOwnProps) {
const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
if (!haveStatePropsChanged) {
return
}
if (haveStatePropsChanged === errorObject) {
this.statePropsPrecalculationError = errorObject.value
}
this.haveStatePropsBeenPrecalculated = true
}
this.hasStoreStateChanged = true
//如果有变化 setState,触发render
this.setState({ storeState })
}
render() {
const {
haveOwnPropsChanged,
hasStoreStateChanged,
haveStatePropsBeenPrecalculated,
statePropsPrecalculationError,
renderedElement
} = this
//最终渲染组件,将合并属性传递给WrappedComponent
if (withRef) {
this.renderedElement = createElement(WrappedComponent, {
...this.mergedProps,
ref: 'wrappedInstance'
})
} else {
this.renderedElement = createElement(WrappedComponent,
this.mergedProps
)
}
return this.renderedElement
}
}
Connect.displayName = connectDisplayName
Connect.WrappedComponent = WrappedComponent
Connect.contextTypes = {
store: storeShape
}
Connect.propTypes = {
store: storeShape
}
//把WrappedComponent的非静态react属性 复制到Connect,最终返回Connect
return hoistStatics(Connect, WrappedComponent)
}
}
使用实例参见 react-redux connect 的使用实例
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
将actionCreators绑定上dispatch,key还是actionCreators的key,但是多做了一层dispatch
*/
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
)
}
var keys = Object.keys(actionCreators)
var boundActionCreators = {}
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
var actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.