正式学习React(四) 前序篇
预热
柯里化函数(curry
)
通俗的来讲,可以用一句话概括柯里化函数:返回函数的函数
// example
const funcA = (a) => {
return const funcB = (b) => {
return a + b
}
};
上述的funcA
函数接收一个参数,并返回同样接收一个参数的funcB
函数。
柯里化函数有什么好处呢?
避免了给一个函数传入大量的参数--我们可以通过柯里化来构建类似上例的函数嵌套,将参数的代入分离开,更有利于调试
降低耦合度和代码冗余,便于复用
举个栗子:
// 已知listA, listB两个Array,都由int组成,需要筛选出两个Array的交集
const listA = [1, 2, 3, 4, 5];
const listB = [2, 3, 4];
const checkIfDataExist = (list) => {
return (target) => {
return list.some(value => value === target)
};
};
// 调用一次checkIfDataExist函数,并将listA作为参数传入,来构建一个新的函数。
// 而新函数的作用则是:检查传入的参数是否存在于listA里
const ifDataExist = checkIfDataExist(listA);
// 使用新函数来对listB里的每一个元素进行筛选
const intersectionList = listB.filter(value => ifDataExist(value));
console.log(intersectionList); // [2, 3, 4]
代码组合(compose
)
代码组合就像是数学中的结合律:
const compose = (f, g) => {
return (x) => {
return f(g(x));
};
};
// 还可以再简洁点
const compose = (f, g) => (x) => f(g(x));
通过这样函数之间的组合,可以大大增加可读性,效果远大于嵌套一大堆的函数调用,并且我们可以随意更改函数的调用顺序
Redux
combineReducers
// 回顾一下combineReducers的使用格式
// 两个reducer
const todos = (state = INIT.todos, action) => {
// ....
};
const filterStatus = (state = INIT.filterStatus, action) => {
// ...
};
const appReducer = combineReducers({
todos,
filterStatus
});
还记得
combineReducers
的黑魔法吗?即:
传入的Object参数中,对象的
key
与value
所代表的reducer function
同名各个
reducer function
的名称和需要传入该reducer的state
参数同名
源码标注解读(省略部分):
export default function combineReducers(reducers) {
// 第一次筛选,参数reducers为Object
// 筛选掉reducers中不是function的键值对
var reducerKeys = Object.keys(reducers);
var finalReducers = {}
for (var i = 0; i < reducerKeys.length; i++) {
var key = reducerKeys[i];
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
var finalReducerKeys = Object.keys(finalReducers)
// 二次筛选,判断reducer中传入的值是否合法(!== undefined)
// 获取筛选完之后的所有key
var sanityError
try {
// assertReducerSanity函数用于遍历finalReducers中的reducer,检查传入reducer的state是否合法
assertReducerSanity(finalReducers)
} catch (e) {
sanityError = e
}
// 返回一个function。该方法接收state和action作为参数
return function combination(state = {}, action) {
// 如果之前的判断reducers中有不法值,则抛出错误
if (sanityError) {
throw sanityError
}
// 如果不是production环境则抛出warning
if (process.env.NODE_ENV !== 'production') {
var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action)
if (warningMessage) {
warning(warningMessage)
}
}
var hasChanged = false
var nextState = {}
// 遍历所有的key和reducer,分别将reducer对应的key所代表的state,代入到reducer中进行函数调用
for (var i = 0; i < finalReducerKeys.length; i++) {
var key = finalReducerKeys[i]
var reducer = finalReducers[key]
// 这也就是为什么说combineReducers黑魔法--要求传入的Object参数中,reducer function的名称和要和state同名的原因
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
// 如果reducer返回undefined则抛出错误
if (typeof nextStateForKey === 'undefined') {
var errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
// 将reducer返回的值填入nextState
nextState[key] = nextStateForKey
// 如果任一state有更新则hasChanged为true
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
// 检查传入reducer的state是否合法
function assertReducerSanity(reducers) {
Object.keys(reducers).forEach(key => {
var reducer = reducers[key]
// 遍历全部reducer,并给它传入(undefined, action)
// 当第一个参数传入undefined时,则为各个reducer定义的默认参数
var initialState = reducer(undefined, { type: ActionTypes.INIT })
// ActionTypes.INIT几乎不会被定义,所以会通过switch的default返回reducer的默认参数。如果没有指定默认参数,则返回undefined,抛出错误
if (typeof initialState === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined during initialization. ` +
`If the state passed to the reducer is undefined, you must ` +
`explicitly return the initial state. The initial state may ` +
`not be undefined.`
)
}
var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.')
if (typeof reducer(undefined, { type }) === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined when probed with a random type. ` +
`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
`namespace. They are considered private. Instead, you must return the ` +
`current state for any unknown actions, unless it is undefined, ` +
`in which case you must return the initial state, regardless of the ` +
`action type. The initial state may not be undefined.`
)
}
})
}
createStore
// 回顾下使用方法
const store = createStore(reducers, state, enhance);
源码标注解读(省略部分):
// 对于未知的action.type,reducer必须返回默认的参数state。这个ActionTypes.INIT就可以用来监测当reducer传入未知type的action时,返回的state是否合法
export var ActionTypes = {
INIT: '@@redux/INIT'
}
export default function createStore(reducer, initialState, enhancer) {
// 检查你的state和enhance参数有没有传反
if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
enhancer = initialState
initialState = undefined
}
// 如果有传入合法的enhance,则通过enhancer再调用一次createStore
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, initialState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
var currentReducer = reducer
var currentState = initialState
var currentListeners = []
var nextListeners = currentListeners
var isDispatching = false // 是否正在分发事件
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 我们在action middleware中经常使用的getState()方法,返回当前state
function getState() {
return currentState
}
// 注册listener,同时返回一个取消事件注册的方法。当调用store.dispatch的时候调用listener
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
// 从nextListeners中去除掉当前listener
ensureCanMutateNextListeners()
var index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// dispatch方法接收的action是个对象,而不是方法。
// 这个对象实际上就是我们自定义action的返回值,因为dispatch的时候,已经调用过我们的自定义action了,比如 dispatch(addTodo())
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 调用dispatch的时候只能一个个调用,通过dispatch判断调用的状态
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 遍历调用各个linster
var listeners = currentListeners = nextListeners
for (var i = 0; i < listeners.length; i++) {
listeners[i]()
}
return action
}
// Replaces the reducer currently used by the store to calculate the state.
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.INIT })
}
// 当create store的时候,reducer会接受一个type为ActionTypes.INIT的action,使reducer返回他们默认的state,这样可以快速的形成默认的state的结构
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer
}
}
thunkMiddleware
源码及其简单简直给跪...
// 返回以 dispatch 和 getState 作为参数的action
export default function thunkMiddleware({ dispatch, getState }) {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
applyMiddleware
先复习下用法:
// usage
import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
const store = createStore(
reducers,
state,
applyMiddleware(thunkMiddleware)
);
applyMiddleware
首先接收thunkMiddleware
作为参数,两者组合成为一个新的函数(enhance
),之后在createStore
内部,因为enhance
的存在,将会变成返回enhancer(createStore)(reducer, initialState)
源码标注解读(省略部分):
// 定义一个代码组合的方法
// 传入一些function作为参数,返回其链式调用的形态。例如,
// compose(f, g, h) 最终返回 (...args) => f(g(h(...args)))
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
} else {
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
}
export default function applyMiddleware(...middlewares) {
// 最终返回一个以createStore为参数的匿名函数
// 这个函数返回另一个以reducer, initialState, enhancer为参数的匿名函数
return (createStore) => (reducer, initialState, enhancer) => {
var store = createStore(reducer, initialState, enhancer)
var dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 每个 middleware 都以 middlewareAPI 作为参数进行注入,返回一个新的链。此时的返回值相当于调用 thunkMiddleware 返回的函数: (next) => (action) => {} ,接收一个next作为其参数
chain = middlewares.map(middleware => middleware(middlewareAPI))
// 并将链代入进 compose 组成一个函数的调用链
// compose(...chain) 返回形如(...args) => f(g(h(...args))),f/g/h都是chain中的函数对象。
// 在目前只有 thunkMiddleware 作为 middlewares 参数的情况下,将返回 (next) => (action) => {}
// 之后以 store.dispatch 作为参数进行注入
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
一脸懵逼?没关系,来结合实际使用总结一下:
当我们搭配redux-thunk
这个库的时候,在redux
配合components
时,通常这么写
import thunkMiddleware from 'redux-thunk';
import { createStore, applyMiddleware, combineReducer } from 'redux';
import * as reducers from './reducers.js';
const appReducer = combineReducer(reducers);
const store = createStore(appReducer, initialState, applyMiddleware(thunkMiddleware));
还记得当createStore
收到的参数中有enhance
时会怎么做吗?
// createStore.js
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, initialState)
}
也就是说,会变成下面的情况
applyMiddleware(thunkMiddleware)(createStore)(reducer, initialState)
applyMiddleware(thunkMiddleware)
applyMiddleware
接收thunkMiddleware
作为参数,返回形如(createStore) => (reducer, initialState, enhancer) => {}
的函数。
applyMiddleware(thunkMiddleware)(createStore)
以 createStore 作为参数,调用上一步返回的函数(reducer, initialState, enhancer) => {}
applyMiddleware(thunkMiddleware)(createStore)(reducer, initialState)
以(reducer, initialState)为参数进行调用。
在这个函数内部,thunkMiddleware
被调用,其作用是监测type
是function
的action
因此,如果dispatch
的action
返回的是一个function
,则证明是中间件,则将(dispatch, getState)
作为参数代入其中,进行action
内部下一步的操作。否则的话,认为只是一个普通的action
,将通过next
(也就是dispatch
)进一步分发。
也就是说,applyMiddleware(thunkMiddleware)
作为enhance
,最终起了这样的作用:
对dispatch
调用的action
(例如,dispatch(addNewTodo(todo)))
进行检查,如果action
在第一次调用之后返回的是function
,则将(dispatch, getState)
作为参数注入到action
返回的方法中,否则就正常对action
进行分发,这样一来我们的中间件就完成喽~
因此,当action
内部需要获取state
,或者需要进行异步操作,在操作完成之后进行事件调用分发的话,我们就可以让action
返回一个以(dispatch, getState)
为参数的function
而不是通常的Object
,enhance
就会对其进行检测以便正确的处理。
bindActionCreator
这个方法感觉比较少见,我个人也很少用到
在传统写法下,当我们要把 state 和 action 注入到子组件中时,一般会这么做:
import { connect } from 'react-redux';
import {addTodo, deleteTodo} from './action.js';
class TodoComponect extends Component {
render() {
return (
<ChildComponent
deleteTodo={this.props.deleteTodo}
addTodo={this.props.addTodo}
/>
)
}
}
function mapStateToProps(state) {
return {
state
}
}
function mapDispatchToProps(dispatch) {
return {
deleteTodo: (id) => {
dispatch(deleteTodo(id));
},
addTodo: (todo) => {
dispatch(addTodo(todo));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoComponect);
使用bindActionCreators
可以把 action 转为同名 key 的对象,但使用 dispatch 把每个 action 包围起来调用
惟一使用 bindActionCreators 的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 Redux store 或 dispatch 传给它。
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {addTodo, deleteTodo} as TodoActions from './action.js';
class TodoComponect extends React.Component {
// 在本组件内的应用
addTodo(todo) {
let action = TodoActions.addTodo(todo);
this.props.dispatch(action);
}
deleteTodo(id) {
let action = TodoActions.deleteTodo(id);
this.props.dispatch(action);
}
render() {
let dispatch = this.props.dispatch;
// 传递给子组件
let boundActionCreators = bindActionCreators(TodoActions, dispatch);
return (
<ChildComponent
{...boundActionCreators}
/>
)
}
}
function mapStateToProps(state) {
return {
state
}
}
export default connect(mapStateToProps)(TodoComponect)
bindActionCreator
源码解析
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
// bindActionCreators期待一个Object作为actionCreators传入,里面是 key: action
export default function bindActionCreators(actionCreators, dispatch) {
// 如果只是传入一个action,则通过bindActionCreator返回被绑定到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"?`
)
}
// 遍历并通过bindActionCreator分发绑定至dispatch
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
}
react-redux
Provider
export default class Provider extends Component {
getChildContext() {
// 将其声明为 context 的属性之一
return { store: this.store }
}
constructor(props, context) {
super(props, context)
// 接收 redux 的 store 作为 props
this.store = props.store
}
render() {
return Children.only(this.props.children)
}
}
if (process.env.NODE_ENV !== 'production') {
Provider.prototype.componentWillReceiveProps = function (nextProps) {
const { store } = this
const { store: nextStore } = nextProps
if (store !== nextStore) {
warnAboutReceivingStore()
}
}
}
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired
}
Provider.childContextTypes = {
store: storeShape.isRequired
}
connect
传入mapStateToProps
,mapDispatchToProps
,mergeProps
,options
。
首先获取传入的参数,如果没有则以默认值代替
const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars
const defaultMapDispatchToProps = dispatch => ({ dispatch })
const { pure = true, withRef = false } = options
之后,通过
const finalMergeProps = mergeProps || defaultMergeProps
选择合并stateProps
,dispatchProps
,parentProps
的方式,默认的合并方式 defaultMergeProps
为:
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
返回一个以 Component 作为参数的函数。在这个函数内部,生成了一个叫做Connect
的 Component
// ...
return function wrapWithConnect(WrappedComponent) {
const connectDisplayName = `Connect(${getDisplayName(WrappedComponent)})`
// 检查参数合法性
function checkStateShape(props, methodName) {}
// 合并props
function computeMergedProps(stateProps, dispatchProps, parentProps) {
const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
if (process.env.NODE_ENV !== 'production') {
checkStateShape(mergedProps, 'mergeProps')
}
return mergedProps
}
// start of Connect
class Connect extends Component {
constructor(props, context) {
super(props, context);
this.store = props.store || context.store
const storeState = this.store.getState()
this.state = { storeState }
this.clearCache()
}
computeStateProps(store, props) {
// 调用configureFinalMapState,使用传入的mapStateToProps方法(或默认方法),将state map进props
}
configureFinalMapState(store, props) {}
computeDispatchProps(store, props) {
// 调用configureFinalMapDispatch,使用传入的mapDispatchToProps方法(或默认方法),将action使用dispatch封装map进props
}
configureFinalMapDispatch(store, props) {}
// 判断是否更新props
updateStatePropsIfNeeded() {}
updateDispatchPropsIfNeeded() {}
updateMergedPropsIfNeeded() {}
componentDidMount() {
// 内部调用this.store.subscribe(this.handleChange.bind(this))
this.trySubscribe()
}
handleChange() {
const storeState = this.store.getState()
const prevStoreState = this.state.storeState
// 对数据进行监听,发送改变时调用
this.setState({ storeState })
}
// 取消监听,清除缓存
componentWillUnmount() {
this.tryUnsubscribe()
this.clearCache()
}
render() {
this.renderedElement = createElement(WrappedComponent,
this.mergedProps
)
return this.renderedElement
}
}
// end of Connect
Connect.displayName = connectDisplayName
Connect.WrappedComponent = WrappedComponent
Connect.contextTypes = {
store: storeShape
}
Connect.propTypes = {
store: storeShape
}
return hoistStatics(Connect, WrappedComponent)
}
// ...
我们看见,在connect的最后,返回了使用hoistStatics
包装的Connect
和WrappedComponent
hoistStatics是什么鬼?为什么使用它?
Copies non-react specific statics from a child component to a parent component. Similar to Object.assign, but with React static keywords blacklisted from being overridden.
也就是说,它类似于Object.assign
,作用是将子组件中的 static 方法复制进父组件,但不会覆盖组件中的关键字方法(如 componentDidMount)
import hoistNonReactStatic from 'hoist-non-react-statics';
hoistNonReactStatic(targetComponent, sourceComponent);
正式学习React(四) 前序篇的更多相关文章
- 正式学习React(四) ----Redux源码分析
今天看了下Redux的源码,竟然出奇的简单,好吧.简单翻译做下笔记: 喜欢的同学自己可以去github上看:点这里 createStore.js import isPlainObject from ' ...
- 正式学习React (六) 项目篇
https://github.com/huenchao/yingshili 或者点这里 注意事项看ReadME.md 会持续更新,反正就是把之前分析的redux react-redux都用一下,然后会 ...
- 正式学习React(五) react-redux源码分析
磨刀不误砍柴工,咱先把react-redux里的工具函数分析一下: 源码点这里 shallowEqual.js export default function shallowEqual(objA, ...
- 正式学习React(一) 开始学习之前必读
为什么要加这个必读!因为webpack本身是基于node环境的, 里面会涉及很多路径问题,我们可能对paths怎么写!webpack又是怎么找到这些paths的很迷惑. 本文是我已经写完正式学习Rea ...
- 正式学习React (七) react-router 源码分析
学习react已经有10来天了,对于react redux react-redux 的使用流程和原理,也已经有一定的了解,在我上一篇的实战项目里,我用到了react-route,其实对它还只是 停留在 ...
- 正式学习 react(三)
有了基础的webpack基础,我们要对react的基本语法进行学习. 我这个教程全部用es6 实现.可能会忽略一些最基本的语法讲解,这些你在官网上或者其他别的地方都比我讲的全. 今天我要讲一下reac ...
- 正式学习 React(三)番外篇 reactjs性能优化之shouldComponentUpdate
性能优化 每当开发者选择将React用在真实项目中时都会先问一个问题:使用react是否会让项目速度更快,更灵活,更容易维护.此外每次状态数据发生改变时都会进行重新渲染界面的处理做法会不会造成性能瓶颈 ...
- 正式学习react(二)
今天把上一篇还没学习完的 webpack部分学习完: 之前有说过关于css的webpack使用.我们讲了 ExtractTextPlugin 来单独管理css讲了module.loaders下关于 c ...
- 正式学习React( 三)
最基本的jsx语法什么的,我就不介绍了,唯一觉得有用点的,就是声明周期了. 下面的内容是转来的,自己也可以网上去搜,我觉得别人归纳的挺不错的,不过写法可能不是es6的,不影响学习. 在组件的整个生命周 ...
随机推荐
- yii2中的url美化
在yii2中,如果想实现类似于post/1,post/update/1之类的效果,官方文档已经有明确的说明 但是如果想把所有的controller都实现,这里采用yii1的方法 'rules' =&g ...
- 基于方法的LINQ语句
LINQ中的查询方法有两站,一种是使用类似于SQL语句的方式,另一种则是基于方法的语句.基于方法的查询方法使用的是C#中面向对象概念的,主要的方法有: 投影: Select | SelectMany ...
- BZOJ 1086 王室联邦
http://www.lydsy.com/JudgeOnline/problem.php?id=1086 思路:贪心,每次当储存的儿子大于等于B时,分出一个块,这样每次每个块至多为2B,这样剩下的没有 ...
- 开源C/C++网络库比较
在开源的C/C++网络库中, 常用的就那么几个, 在业界知名度最高的, 应该是ACE了, 不过是个重量级的大家伙, 轻量级的有libevent, libev, 还有 Boost的ASIO. ACE是一 ...
- 其中 (%{WORD:x_forword}|-) |表示或的意思
121.40.205.143 [30/Aug/2016:14:03:08 +0800] "GET /resources/images/favicon.ico HTTP/1.1" - ...
- UVA 10047-The Monocycle(队列bfs+保存4种状态)
题意:给你一张地图,S代表起点,T代表终点,有一个轮盘,轮盘平均分成5份,每往前走一格恰好转1/5,轮盘只能往前进,但可以向左右转90°,每走一步或是向左向右转90° 要花费1单位的时间,问最少的时间 ...
- python刷取CSDN博文访问量之一
python刷取CSDN博文访问量之一 作者:vpoet 注:这个系列我只贴代码,代码不注释.有兴趣的自己读读就懂了,纯属娱乐,望管理员抬手 若有转载一定不要注明来源 #coding=utf-8 ...
- rsync常用参数详解
rsync常用参数详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 在linux中,一切皆是文件,包括你的终端,硬件设备信息,目录,内核文件等等.所以工作中我们难免会遇到拷贝文件 ...
- ffmpeg API录制rtsp视频流
原文出自http://blog.csdn.net/zxwangyun/article/details/8190638#reply 作者 Sloan 这里在录制时,并没有进行转码,只是相当于把rts ...
- MapReduce程序依赖的jar包
难得想写个mapreduce程序.发现已经不记得须要加入那些jar包了,网上找了一会也没发现准确的答案.幸好对hadoop体系结构略知一二.迅速试出了写mapreduce程序须要的五个jar包. 不多 ...