React 数据流 - 笔记
为降低难度,使用 unistore 作为切入口进行源码分析。
源码
import { h, Component } from 'preact';
/** Creates a new store, which is a tiny evented state container.
* @name createStore
* @param {Object} [state={}] Optional initial state
* @returns {store}
* @example
* let store = createStore();
* store.subscribe( state => console.log(state) );
* store.setState({ a: 'b' }); // logs { a: 'b' }
* store.setState({ c: 'd' }); // logs { a: 'b', c: 'd' }
*/
export function createStore(state) {
let listeners = [];
state = state || {};
function unsubscribe(listener) {
let out = [];
for (let i=0; i<listeners.length; i++) {
if (listeners[i]===listener) {
listener = null;
}
else {
out.push(listeners[i]);
}
}
listeners = out;
}
function setState(update, overwrite) {
state = overwrite ? update : assign(assign({}, state), update);
let currentListeners = listeners;
for (let i=0; i<currentListeners.length; i++) currentListeners[i](state);
}
/** An observable state container, returned from {@link createStore}
* @name store
*/
return /** @lends store */ {
/** Create a bound copy of the given action function.
* The bound returned function invokes action() and persists the result back to the store.
* If the return value of `action` is a Promise, the resolved value will be used as state.
* @param {Function} action An action of the form `action(state, ...args) -> stateUpdate`
* @returns {Function} boundAction()
*/
action(action) {
// Note: perf tests verifying this implementation: https://esbench.com/bench/5a295e6299634800a0349500
return function() {
let args = [state];
for (let i=0; i<arguments.length; i++) args.push(arguments[i]);
let ret = action.apply(this, args);
if (ret!=null) {
if (ret.then) ret.then(setState);
else setState(ret);
}
};
},
/** Apply a partial state object to the current state, invoking registered listeners.
* @param {Object} update An object with properties to be merged into state
* @param {Boolean} [overwrite=false] If `true`, update will replace state instead of being merged into it
*/
setState,
/** Register a listener function to be called whenever state is changed. Returns an `unsubscribe()` function.
* @param {Function} listener A function to call when state changes. Gets passed the new state.
* @returns {Function} unsubscribe()
*/
subscribe(listener) {
listeners.push(listener);
return () => { unsubscribe(listener); };
},
/** Remove a previously-registered listener function.
* @param {Function} listener The callback previously passed to `subscribe()` that should be removed.
* @function
*/
unsubscribe,
/** Retrieve the current state object.
* @returns {Object} state
*/
getState() {
return state;
}
};
}
/** Wire a component up to the store. Passes state as props, re-renders on change.
* @param {Function|Array|String} mapStateToProps A function mapping of store state to prop values, or an array/CSV of properties to map.
* @param {Function|Object} [actions] Action functions (pure state mappings), or a factory returning them. Every action function gets current state as the first parameter and any other params next
* @returns {Component} ConnectedComponent
* @example
* const Foo = connect('foo,bar')( ({ foo, bar }) => <div /> )
* @example
* const actions = { someAction }
* const Foo = connect('foo,bar', actions)( ({ foo, bar, someAction }) => <div /> )
* @example
* @connect( state => ({ foo: state.foo, bar: state.bar }) )
* export class Foo { render({ foo, bar }) { } }
*/
export function connect(mapStateToProps, actions) {
if (typeof mapStateToProps!=='function') {
mapStateToProps = select(mapStateToProps || []);
}
return Child => {
function Wrapper(props, { store }) {
// 过滤筛选选择的 props
let state = mapStateToProps(store ? store.getState() : {}, props);
// 进行 action 的 store 绑定
let boundActions = actions ? mapActions(actions, store) : { store };
let update = () => {
let mapped = mapStateToProps(store ? store.getState() : {}, this.props);
for (let i in mapped) if (mapped[i]!==state[i]) {
state = mapped;
return this.setState(null);
}
for (let i in state) if (!(i in mapped)) {
state = mapped;
return this.setState(null);
}
};
this.componentDidMount = () => {
store.subscribe(update);
};
this.componentWillUnmount = () => {
store.unsubscribe(update);
};
// 将 actions , props, state 合并,传入
this.render = props => h(Child, assign(assign(assign({}, boundActions), props), state));
}
return (Wrapper.prototype = new Component()).constructor = Wrapper;
};
}
// 提供一个 Provider 元素,并通过 props 制定 store,并设置到到页面上下文中
export function Provider(props) {
this.getChildContext = () => ({ store: props.store });
}
Provider.prototype.render = props => props.children[0];
// Bind an object/factory of actions to the store and wrap them.
function mapActions(actions, store) {
if (typeof actions==='function') actions = actions(store);
let mapped = {};
for (let i in actions) {
mapped[i] = store.action(actions[i]);
}
return mapped;
}
// select('foo,bar') creates a function of the form:
// state => ({ foo, bar })
function select(properties) {
if (typeof properties==='string') properties = properties.split(',');
return state => {
let selected = {};
for (let i=0; i<properties.length; i++) {
selected[properties[i]] = state[properties[i]];
}
return selected;
};
}
// Lighter Object.assign stand-in
function assign(obj, props) {
for (let i in props) obj[i] = props[i];
return obj;
}
样例
import { Provider, createStore, connect } from 'unistore'
let store = createStore({ count: 0 })
// If actions is a function, it gets passed the store:
let actions = store => ({
// Actions can just return a state update:
increment(state) {
return { count: state.count+1 }
},
// The above example as an Arrow Function:
increment2: ({ count }) => ({ count: count+1 }),
//Actions receive current state as first parameter and any other params next
//check this function as <button onClick={incrementAndLog}>
incrementAndLog: ({ count }, event) => {
console.info(event)
return { count: count+1 }
},
// Async actions can be pure async/promise functions:
async getStuff(state) {
let res = await fetch('/foo.json')
return { stuff: await res.json() }
},
// ... or just actions that call store.setState() later:
incrementAsync(state) {
setTimeout( () => {
store.setState({ count: state.count+1 })
}, 100)
}
})
const App = connect('count', actions)(
({ count, increment }) => (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
)
)
export default () => (
<Provider store={store}>
<App />
</Provider>
)
Comments
Leave a comment