dva 首先是一個基於 redux 和 redux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,因此也能夠理解爲一個輕量級的應用框架。
import React from 'react'; import ReactDOM from 'react-dom'; import {createStore,combineReducers,applyMiddleware} from 'redux'; import {connect,Provider} from 'react-redux'; import createSagaMiddleware from 'redux-saga'; import * as effects from 'redux-saga/effects'; import createRootSaga from './createRootSaga'; import createReducers from './createReducers'; import {createHashHistory} from 'history'; import {routerMiddleware,connectRouter,ConnectedRouter} from 'connected-react-router'; import {prefix} from './utils'; export {connect} export default function(opts){ const app = { _models:[],//存放着全部的模型 model,//添加模型的方法 _router:null,//此處存放着路由定義 router,//定義路由的方法 start } function model(model){ app._models.push(model); } function router(routerConfig){ app._router = routerConfig; } function start(selector){ let history = opts.history||createHashHistory(); let rootReducer = createReducers(app._models,history,opts.extraReducers);//router let finalReducer = function(state,action){ let newRootReducer = opts.onReducer(rootReducer); let newState = newRootReducer(state,action); if(opts.onStateChange){ opts.onStateChange(newState); } return newState; } let sagaMiddleware = createSagaMiddleware(); let rootSaga = createRootSaga(app._models,opts.onError||function(){},opts.onEffect); if(opts.onAction){ if(!Array.isArray(opts.onAction)){ opts.onAction=[opts.onAction]; } }else { opts.onAction=[] } let newCreateStore = createStore; if(opts.extraEnhancers){ newCreateStore = opts.extraEnhancers(createStore); } let store = newCreateStore(finalReducer,opts.initialState||undefined, applyMiddleware(routerMiddleware(history),sagaMiddleware,...opts.onAction)); sagaMiddleware.run(rootSaga);//開始啓動rootSaga執行 let App = app._router({history}); ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> {App} </ConnectedRouter> </Provider> ,document.querySelector(selector)); } return app; }
// bindActionCreator export default function bindActionCreator(actions,dispatch){ let newActions={}; for(let key in actions){ newActions[key]=()=>dispatch(actions[key].apply(null,arguments)); } return newActions; } // combineReducers export default combineReducers=reducers=>(state={},action)=>Object.keys(reducers).reduce((currentState,key)=>{ currentState[key]=reducers[key](state[key],action); return currentState; },{}); // createStore export default function createStore(reducer,enhancer){ if(typeof enhancer !== 'undefined'){ return enhancer(createStore)(reducer); } let state=null; const listeners=[]; const subscribe=(listener)=>{ listeners.push(listener); } const getState=()=>state; const dispatch=(action)=>{ state=reducer(state,action); listeners.forEach((listener)=>listener()) }; dispatch({}); return {getState,dispatch,subscribe} } // applyMiddleware export default function applyMiddleware(...middlewares){ return (createStore)=>(reducer)=>{ const store=createStore(reducer); let dispatch=store.dispatch; let chain=[]; const middlewareAPI={ getState:store.getState, dispatch:(action)=>dispatch(action) } chain=middlewares.map(middleware=>middleware(middlewareAPI)); dispatch=compose(...chain)(store.dispatch); return { ...store, dispatch } } } // compose export default function compose(...funcs){ return funcs.reduce((a,b)=>(...args)=>a(b(...args))); }
https://segmentfault.com/a/11...javascript
一 全局狀態管理框架的產生 Redux和Vuex都是全局狀態管理框架,我以爲他們的出現有四個緣由: 第一,是SPA,若是不是SPA,多頁面之間也就不存在複雜的交互,每打開一個頁面表明新的生命週期,頁面之間的交互能夠利用window對象和URL參數,固然也存在一個頁面的組件之間有複雜的交互,可是Redux和Vuex確實是更適用於SPA的場景,頁面做爲組件,頁面之間能夠共同管理數據狀態,更像是一個應用,Redux和Vuex管理着這個應用的全部狀態。 第二,是組件化,我認爲組件化有兩種,第一種是與業務邏輯無關的,能夠複用的組件,第二種是業務邏輯的組件,提取出來是爲了代碼的可維護性,可讀性。組件化以後,一個頁面可能有幾個甚至十幾個組件組成,並且還可能出現幾層嵌套的狀況,這就產生了組件之間共同管理狀態的場景。 第三,是複雜的交互邏輯,有了SPA有了組件化,再出現複雜的交互邏輯,須要多頁面多組件之間的數據共享。若是是同一頁面內,在沒有管理框架的時候咱們都是把共享狀態放在根組件,而後將利用屬性下傳狀態和管理方法,或者利用全局事件的方式。全局事件多了管理混亂,傳屬性的話,可能出現代碼重複,而且也是管理不規範。 第四,是數據與視圖的綁定,數據驅動視圖的更新的方式,使得數據的管理相當重要,數據決定了視圖的展現,因此對於數據的管理須要一套規範。 二 Redux源碼分析 具體的使用和概念參見Redux中文文檔 http://cn.redux.js.org//index...。本文從源碼的角度進行分析。 2.1 redux源碼的結構介紹 index.js:源碼的入口文件,集合導出其他5個文件的功能。 createStore.js: 建立store對象的方法,接受reducer和initState。 applyMiddle.js: 應用中間件的方法,該方法接受中間件數組,起到包裝store的dispatch方法的做用,在action到達reducer以前能夠作一些操做。 combineReducers.js: 組合reducer的方法,將多個reducer組合成一個reducer,redux中對於state的劃分就是利用reducer的劃分, combineReducers方法將多個reducer合成一個reducer方法,也將多個reducer的state合成一個全局的state,每個reducer只能操做自身的state。 bindActionCreators.js: 提供了一個幫助方法,對actionCreator方法利用dispatch再進行一次包裝,包裝成的方法能夠直接觸發dispatch。 2.2 createStore.js文件 createStore.js文件提供了建立store的方法,下面只顯示一些加了註釋的關鍵代碼部分。 currentState: 內部的state對象 currentReducer: 接受action的reducer對象 currentListener: 存放監聽函數的數組 getState: 閉包方法獲取內部的state subscribe: 提供訂閱監聽的方法 dispatch: 接受action,將action傳遞給reducer,將返回值付給state,而且觸發監聽函數的方法 replaceReducer: 替換reducer的方法。 export default function createStore(reducer, preloadedState, enhancer) { var currentReducer = reducer var currentState = preloadedState var currentListeners = [] var nextListeners = currentListeners var isDispatching = false //獲取state的方法 function getState() { return currentState } function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } //提供訂閱監聽的方法 function subscribe(listener) { 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) } } //將action和currentState傳入currentReducer,並將返回值賦值給currentState 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 } //總體替換reduer function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } currentReducer = nextReducer dispatch({ type: ActionTypes.INIT }) } // When a store is created, an "INIT" action is dispatched so that every // reducer returns their initial state. This effectively populates // the initial state tree. dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } } 2.3 combineReducers.js文件 提供了組合reducer的方法,將多個reducer組合成一個reducer,redux中對於state的劃分就是利用reducer的劃分, combineReducers方法將多個reducer合成一個reducer方法,也將多個reducer的state合成一個全局的state,每個reducer只能操做自身的state。 finalReducers: 最終的reducers對象 finalReducerKeys: 最終的reducers的key值數組 combination: 最終返回的組合的reducer方法 關鍵的combination代碼中,能夠獲得幾點心得: 1 每個reducer只能拿到本身的子state 2 全局的state是由子state組成的,若是初始的state是空的話,那麼只有在reducer被第一次調用的時候纔會賦值 3 若是想改變state,由於是值比較,因此在reducer中須要返回新的state對象,同時若是全局的state變化,也會返回新的對象 //接受reduers對象, 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 (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } var finalReducerKeys = Object.keys(finalReducers) //最後返回的組合reducer函數,接受初始的state和action return function combination(state = {}, action) { var hasChanged = false //新的全局state var nextState = {} //遍歷每個reducer for (var i = 0; i < finalReducerKeys.length; i++) { var key = finalReducerKeys[i] var reducer = finalReducers[key] //該reducer上的子state,若是建立store的時候沒有傳state,則是空的 var previousStateForKey = state[key] //真正調用reducer函數返回state的地方 //能夠看到reducer中的state只是本身的state,不是全局的state var nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } //將返回的新state放入新的全局nextState nextState[key] = nextStateForKey //是否改變比較的是state的值,因此咱們在寫reducer的時候,若是須要改變state //應該返回一個新的對象,若是沒有改變的話,應該返回傳給reducer的舊state對象 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } //若是有一個子state變化,那麼就返回新的state對象,這裏也是返回的新對象nextState,而不是 //在原來的state上進行修改 return hasChanged ? nextState : state } } 2.4 applyMiddleware.js文件 提供能夠插入中間件的方法,應用中間件的目的是包裝dispatch,在action傳遞給dispatch執行以前,須要通過中間件的層層處理,進行一些業務上的處理,決定action的走向。 import compose from './compose' /** 中間件的格式 ({dispatch, getState}) =>{ return next =>{ return action =>{ } } } */ export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { //拿到store var store = createStore(reducer, preloadedState, enhancer) var dispatch = store.dispatch var chain = [] var middlewareAPI = { getState: store.getState, //這裏包一下dispatch的緣由是,讓傳給中間件的dispatch是包裝了中間件以後的dispatch,而不是原始的dispatch //若是寫成dispatch:diapatch,那麼當dispatch變化時,這裏的dispatch仍是原始的dispatch dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) /* chain鏈中的元素格式 next =>{ return action =>{ } } */ //利用compose函數拿到包裝了中間件的dispatch dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } 2.5 compose.js文件,提供了組合中間件的方法 /** * compose函數最終返回的結果 * (...args) => middle1(middle2(middle3(...args))). * 其中middle的格式 * next =>{ return action =>{ } } */ 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)) } 2.6 bindActionCreators.js文件,提供了綁定actoinCreator的方法 //添加dispatch的方法 function bindActionCreator(actionCreator, dispatch) { //返回的函數,接受參數,傳遞給actionCreator調用,actionCreator返回標準的action,而後返回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 } 以上是Redux中6個文件的分析,下面就寫一個簡單的例子,看一下redux最基本的使用,理解下redux。 2.7 redux簡單demo store.js import {createStore} from 'redux'; //建立action的方法 export function createAction(type, payload) { return { type, payload } } //初始的state const initialState = { time: new Date().getTime() } //reducer函數 function reducer(state = initialState, action) { switch (action.type) { case 'NOW_TIME': return { ...state, time: action.payload } default: return state; } } let store; //獲取store的方法 export function getStore() { if(store) return store; return store = createStore(reducer); } testRedux.js react-native的一段代碼 '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 './store'; //獲取到store const store = getStore(); class TestRedux extends Component { constructor(props) { super(props); let state = store.getState(); this.state = { time: state.time }; //這裏訂閱state的變化,state變化以後拿到新的state,而後從新setState,更新視圖 store.subscribe(()=>{ let state = store.getState(); this.setState({ time: state.time }); }); } //調用dispatch的方法 _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; 固然咱們在實際生產中確定不會這樣用,還須要依賴react-redux、create-action等必要的模塊,下面就繼續看一下相關的模塊。 三 Redux相關庫源碼分析 3.1 react-actions react-actions提供了一種靈活的建立符合FSA標準action的方法,其中的createAction.js是咱們生產中經常使用的文件,關鍵代碼以下: 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = createAction; var _identity = require('lodash/identity'); var _identity2 = _interopRequireDefault(_identity); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * * @param type action中的type,String類型 * @param payloadCreator 建立action中的payload的函數 * @param metaCreator 建立action中的payload的函數 */ function createAction(type, payloadCreator, metaCreator) { // _identity2.default 函數返回第一個參數 var finalPayloadCreator = typeof payloadCreator === 'function' ? payloadCreator : _identity2.default; //調用createAction返回一個actionHandler函數,再調用actionHandler才返回action對象 var actionHandler = function actionHandler() { var hasError = (arguments.length <= 0 ? undefined : arguments[0]) instanceof Error; //最終返回的action對象 var action = { type: type }; //若是在createAction中傳payloadCreator和metaCreator函數,那麼在actionHandler中傳的參數將傳遞給 //payloadCreator和metaCreator函數,而且將payloadCreator的返回結果當作action的payload,將metaCreator的返回結果當作action的meta 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的返回結果當作action的meta if (typeof metaCreator === 'function') { action.meta = metaCreator.apply(undefined, arguments); } //返回action return action; }; actionHandler.toString = function () { return type.toString(); }; return actionHandler; } 3.2 createAction方法使用實例 types.js 一般咱們把action的type統一放在一塊兒 export const GET_POI_INFO = 'GET_POI_INFO' export const CHANGE_POI_STATUS = 'CHANGE_POI_STATUS' actions.js actions.js用於調用createAction產生actionHandler。 import {createAction} from 'redux-actions'; import * as types from './actioins'; /** * 須要利用payloadCreator接收參數產生payload的action,這裏payload返回的是一個Promise, * 接下來會講到redux-promise中間件用於處理payload是promise的狀況, * 實現了在createAction的時候可以處理異步產生action的狀況 * */ export const getPoiInfo = createAction(types.GET_POI_INFO, async(poiId)=> { return await poiService.getPoiInfo(poiId) .then(data=> { if (data == null) throw 'poi info is null'; return data; }); }); //不須要利用payloadCreator產生payload的action export const changePoiStatus = createAction(types.CHANGE_POI_STATUS); 3.3 redux-promise redux-promise中間件是用於解決異步的action。 'use strict'; exports.__esModule = true; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; exports['default'] = promiseMiddleware; var _fluxStandardAction = require('flux-standard-action'); function isPromise(val) { return val && typeof val.then === 'function'; } //_ref接收的參數對象有兩個 {getState,dispatch} function promiseMiddleware(_ref) { var dispatch = _ref.dispatch; return function (next) { return function (action) { //若是不是標準的action if (!_fluxStandardAction.isFSA(action)) { //若是是action是promise,直接dispatch這個promise的結果,若是不是promise交給下一個中間件 return isPromise(action) ? action.then(dispatch) : next(action); } //若是payload是promise,則把promise的結果做爲這個action的payload,而後dispatch這個action //不然交給下一個中間件 return isPromise(action.payload) ? action.payload.then(function (result) { return dispatch(_extends({}, action, { payload: result })); }, function (error) { return dispatch(_extends({}, action, { payload: error, error: true })); }) : next(action); }; }; } module.exports = exports['default']; dispatch(new Promise(){ resolve(action) }) 有了reduex-promise中間件之後咱們就可讓action的payload是promise,而後promise的返回值是paylaod或者直接action就是一個promise,直接dispatch promise的返回值 3.4 react-redux 以上都是redux自己相關的庫,react-redux是把redux更好的結合應用於react的庫。 index.js 入口文件,向外提供了Provider和connect兩個對象。 Provider 是一個react的組件,接受store做爲屬性,而後放在context中,提供給子組件。咱們把它做爲根組件使用。 Provider.js import { Component, PropTypes, Children } from 'react' import storeShape from '../utils/storeShape' import warning from '../utils/warning' //繼承react的組件 export default class Provider extends Component { //利用context傳遞store,子組件在構造函數中能夠經過context.store拿到store getChildContext() { return { store: this.store } } constructor(props, context) { super(props, context) this.store = props.store } //渲染惟一的子組件 render() { const { children } = this.props return Children.only(children) } } if (process.env.NODE_ENV !== 'production') { Provider.prototype.componentWillReceiveProps = function (nextProps) { const { store } = this const { store: nextStore } = nextProps //store變化時給出警告 if (store !== nextStore) { warnAboutReceivingStore() } } } connect 方法實現將自定義將store中的state映射到組件的props上,把createAction方法包裝成dispatch的方法掛在組件的props上,而且監聽store中state的變化,更新組件的props。 connect.js 調用connect最終會返回包裝的組件,在組件mounted的時候調用trySubscribe,訂閱store中state的變化,在handleChange方法中經過this.setState({ storeState })觸發從新渲染組件,在render中,調用 updateStatePropsIfNeeded updateDispatchPropsIfNeeded updateMergedPropsIfNeeded 三個方法將更新最終的this.mergeProps import { Component, createElement } from 'react' import storeShape from '../utils/storeShape' import shallowEqual from '../utils/shallowEqual' import wrapActionCreators from '../utils/wrapActionCreators' import warning from '../utils/warning' import isPlainObject from 'lodash/isPlainObject' import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars const defaultMapDispatchToProps = dispatch => ({ dispatch }) //更新屬性 4 const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({ ...parentProps, ...stateProps, ...dispatchProps }) export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { const shouldSubscribe = Boolean(mapStateToProps) const mapState = mapStateToProps || defaultMapStateToProps let mapDispatch if (typeof mapDispatchToProps === 'function') { mapDispatch = mapDispatchToProps } else if (!mapDispatchToProps) { mapDispatch = defaultMapDispatchToProps } else { mapDispatch = wrapActionCreators(mapDispatchToProps) } //更新屬性 3 const finalMergeProps = mergeProps || defaultMergeProps const { pure = true, withRef = false } = options const checkMergedEquals = pure && finalMergeProps !== defaultMergeProps // Helps track hot reloading. const version = nextVersion++ //connect函數返回的包裝組件的方法 return function wrapWithConnect(WrappedComponent) { const connectDisplayName = `Connect(${getDisplayName(WrappedComponent)})` function checkStateShape(props, methodName) { if (!isPlainObject(props)) { warning( `${methodName}() in ${connectDisplayName} must return a plain object. ` + `Instead received ${props}.` ) } } //計算mergeProps function computeMergedProps(stateProps, dispatchProps, parentProps) { const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps) if (process.env.NODE_ENV !== 'production') { checkStateShape(mergedProps, 'mergeProps') } return mergedProps } class Connect extends Component { shouldComponentUpdate() { return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged } constructor(props, context) { super(props, context) this.version = version //獲取store this.store = props.store || context.store invariant(this.store, `Could not find "store" in either the context or ` + `props of "${connectDisplayName}". ` + `Either wrap the root component in a <Provider>, ` + `or explicitly pass "store" as a prop to "${connectDisplayName}".` ) const storeState = this.store.getState() this.state = { storeState } this.clearCache() } //計算stateProps computeStateProps(store, props) { if (!this.finalMapStateToProps) { return this.configureFinalMapState(store, props) } const state = store.getState() //獲取state中的內容爲props const stateProps = this.doStatePropsDependOnOwnProps ? this.finalMapStateToProps(state, props) : this.finalMapStateToProps(state) if (process.env.NODE_ENV !== 'production') { checkStateShape(stateProps, 'mapStateToProps') } return stateProps } configureFinalMapState(store, props) { const mappedState = mapState(store.getState(), props) const isFactory = typeof mappedState === 'function' this.finalMapStateToProps = isFactory ? mappedState : mapState this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1 if (isFactory) { return this.computeStateProps(store, props) } if (process.env.NODE_ENV !== 'production') { checkStateShape(mappedState, 'mapStateToProps') } return mappedState } //計算dispatchProps computeDispatchProps(store, props) { if (!this.finalMapDispatchToProps) { return this.configureFinalMapDispatch(store, props) } const { dispatch } = store const dispatchProps = this.doDispatchPropsDependOnOwnProps ? this.finalMapDispatchToProps(dispatch, props) : this.finalMapDispatchToProps(dispatch) if (process.env.NODE_ENV !== 'production') { checkStateShape(dispatchProps, 'mapDispatchToProps') } return dispatchProps } configureFinalMapDispatch(store, props) { const mappedDispatch = mapDispatch(store.dispatch, props) const isFactory = typeof mappedDispatch === 'function' this.finalMapDispatchToProps = isFactory ? mappedDispatch : mapDispatch this.doDispatchPropsDependOnOwnProps = this.finalMapDispatchToProps.length !== 1 if (isFactory) { return this.computeDispatchProps(store, props) } if (process.env.NODE_ENV !== 'production') { checkStateShape(mappedDispatch, 'mapDispatchToProps') } return mappedDispatch } //更新stateProps的地方 updateStatePropsIfNeeded() { const nextStateProps = this.computeStateProps(this.store, this.props) if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) { return false } this.stateProps = nextStateProps return true } //更新dispatchProps的地方 updateDispatchPropsIfNeeded() { const nextDispatchProps = this.computeDispatchProps(this.store, this.props) if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) { return false } this.dispatchProps = nextDispatchProps return true } //更新mergeProps的地方 updateMergedPropsIfNeeded() { //mergeProps由 this.stateProps, this.dispatchProps, this.props 組成 const nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props) if (this.mergedProps && checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) { return false } this.mergedProps = nextMergedProps return true } isSubscribed() { return typeof this.unsubscribe === 'function' } //訂閱store中state的變化 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() } //store變化調用的方法 handleChange() { if (!this.unsubscribe) { return } //reducer每次返回的state也是新的對象 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 //設置state從新渲染組件 this.setState({ storeState }) } render() { if (withRef) { // this.mergedProps 是最終給組件的屬性 this.renderedElement = createElement(WrappedComponent, { ...this.mergedProps, ref: 'wrappedInstance' }) } else { // this.mergedProps 是最終給組件的屬性 this.renderedElement = createElement(WrappedComponent, this.mergedProps ) } return this.renderedElement } } return hoistStatics(Connect, WrappedComponent) } } 四 Vuex源碼分析 vuex是專門爲Vue提供的全局狀態管理框架,具體的概念和使用參見文檔:https://vuex.vuejs.org/zh-cn/。本文從源碼的角度進行分析。 完整Vuex註釋參見倉庫:https://github.com/yylgit/vuex index.js是入口文件,導出了6個關鍵方法。 import { Store, install } from './store' import { mapState, mapMutations, mapGetters, mapActions } from './helpers' export default { Store, //建立store對象的方法 install, //安裝vuex插件的方法 version: '__VERSION__', mapState, //將store中的state映射到vue組件computed的方法 mapMutations, //將store中的mutation映射到vue組件methods的方法 mapGetters, //將store中的state映射到vue組件computed的方法 mapActions //將store中的action映射到vue組件methods的方法 } 4.1 Vuex的裝載 首先講到install方法,咱們安裝vuex使用Vue.use方法,會將Vue傳遞給vuex的install方法並執行 import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) 可是若是在引入vuex以前已經將Vue掛在了window對象上的話,則不須要再調用Vue.use方法,相關源碼以下: store.js export function install (_Vue) { if (Vue) { if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } Vue = _Vue applyMixin(Vue) } // auto install in dist mode //若是window上有Vue則自動安裝 if (typeof window !== 'undefined' && window.Vue) { install(window.Vue) } install方法中調用了applyMixin方法,該方法在mixin.js文件中,其中若是Vue的版本大於等於2時,將vuexInit 函數mixin到init或者beforeCreate生命週期函數中,1.x版本時,經過重寫Vue.prototype._init方法,將vuexInit函數放在_init的options中,_init方法在Vue的構造函數中會調用。因此在每個vue實例建立的時候都會調用vuexInit方法。 mixin.js export default function (Vue) { const version = Number(Vue.version.split('.')[0]) /** * 若是是2.x.x以上版本,能夠使用 hook 的形式進行注入,或使用封裝並替換Vue對象原型的_init方法,實現注入。 */ //Vue2 經過Vue組件的init方法或者beforeCreate方法 if (version >= 2) { const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1 Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit }) } else { // override init and inject vuex init procedure // for 1.x backwards compatibility. //由於Vue的構造函數會調用_init方法 const _init = Vue.prototype._init Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit _init.call(this, options) } } 再看一下vuexInit方法,將store對象掛在每個vue實例的$store屬性上。 /** * Vuex init hook, injected into each instances init hooks list. * 初始化Vue根組件時傳入的store設置到this.$store屬性上, * 子組件從其父組件引用$store屬性,層層嵌套進行設置。 * 在任意組件中執行 this.$store 都能找到裝載的那個store對象 */ function vuexInit () { const options = this.$options // store injection if (options.store) { //根組件 this.$store = options.store //子組件 } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store } } 4.2 Store構造函數 安裝了Vuex以後,咱們將利用Store方法建立store對象,示例以下: const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) 那麼下面就看看Store方法的構造函數中都作了什麼事情。 聲明變量 this._committing 標識是否利用commit改變state this._actions 存放全部模塊的action,其中key值已經加上命名空間 this._mutations 存放全部模塊的mutation,其中key值已經加上命名空間 this._wrappedGetters 存放全部模塊的getter,其中key值已經加上命名空間 this._modules 存放模塊樹 this._modulesNamespaceMap 存放有命名空間的模塊與命名空間之間的map this._subscribers 存放訂閱state變化的函數 this._watcherVM 提供一個VM用於監聽state和getter的變化 this.dispatch 綁定this爲store的dispatch this.commit 綁定this爲store的dispatch state 用於存放全部模塊state樹的rootState installModule 安裝模塊的方法 resetStoreVM 設置store._vm plugins.forEach安裝插件 具體代碼以下: constructor (options = {}) { //已經執行安裝函數進行裝載; //支持Promise語法 //必須用new操做符 if (process.env.NODE_ENV !== 'production') { assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`) assert(this instanceof Store, `Store must be called with the new operator.`) } const { plugins = [], strict = false } = options let { state = {} } = options if (typeof state === 'function') { state = state() } // store internal state //是否正在commit this._committing = false this._actions = Object.create(null) this._mutations = Object.create(null) this._wrappedGetters = Object.create(null) this._modules = new ModuleCollection(options) // Vuex支持store分模塊傳入,存儲分析後的modules this._modulesNamespaceMap = Object.create(null) // 命名空間與對應模塊的map this._subscribers = [] // 訂閱函數集合,Vuex提供了subscribe功能 this._watcherVM = new Vue() // Vue組件用於watch監視變化 // bind commit and dispatch to self const store = this const { dispatch, commit } = this //封裝替換原型中的dispatch和commit方法,將this指向當前store對象,當該方法不是使用store調用時,this仍然指向store this.dispatch = function boundDispatch (type, payload) { return dispatch.call(store, type, payload) } this.commit = function boundCommit (type, payload, options) { return commit.call(store, type, payload, options) } // strict mode this.strict = strict // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters installModule(this, state, [], this._modules.root) // initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) // state對象,通過installModule以後已經成了rootState resetStoreVM(this, state) // apply plugins plugins.concat(devtoolPlugin).forEach(plugin => plugin(this)) } 4.3 Store的模塊樹和命名空間 在構造函數中咱們能夠看到,開始處理options參數的是這行代碼 this._modules = new ModuleCollection(options) 它就是根據咱們傳入的store參數去構造store模塊樹。 關於store的模塊化和命名空間參見文檔:https://vuex.vuejs.org/zh-cn/... 歸納起來包括如下幾點: Vuex的store能夠分模塊,模塊能夠添加命名空間,添加了命名空間的模塊有局部的上下文。 傳給mutation的是局部的state。 傳給action的是局部和全局的state和getter。 傳給getter的是局部和全局的state和getter。 默認的commit和dispatch都是分發局部的mutation和action。 若須要在全局命名空間內分發 action 或提交 mutation,將 { root: true } 做爲第三參數傳給 dispatch 或 commit 便可 對於模塊樹的構造,咱們首先須要看一下模塊節點的構造函數module/module.js this._rawModule 存放原始的模塊對象 this.state 指向this._rawModule.state或者是this._rawModule.state() this._children 存放子模塊 addChild,removeChild,getChild 添加,刪除和獲取子模塊 forEachChild,forEachGetter,forEachAction,forEachMutation分別提供 遍歷這幾種元素的方法 update 提供更新rawModule的方法 namespaced 判斷模塊是否有命名空間 import { forEachValue } from '../util' export default class Module { constructor (rawModule, runtime) { this.runtime = runtime //存放子模塊 this._children = Object.create(null) //存放原始的模塊對象 this._rawModule = rawModule const rawState = rawModule.state this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } //判斷模塊是否有命名空間 get namespaced () { return !!this._rawModule.namespaced } addChild (key, module) { this._children[key] = module } removeChild (key) { delete this._children[key] } getChild (key) { return this._children[key] } update (rawModule) { this._rawModule.namespaced = rawModule.namespaced if (rawModule.actions) { this._rawModule.actions = rawModule.actions } if (rawModule.mutations) { this._rawModule.mutations = rawModule.mutations } if (rawModule.getters) { this._rawModule.getters = rawModule.getters } } forEachChild (fn) { forEachValue(this._children, fn) } forEachGetter (fn) { if (this._rawModule.getters) { forEachValue(this._rawModule.getters, fn) } } forEachAction (fn) { if (this._rawModule.actions) { forEachValue(this._rawModule.actions, fn) } } forEachMutation (fn) { if (this._rawModule.mutations) { forEachValue(this._rawModule.mutations, fn) } } } 再看一下模塊樹的構造函數module/module-collection.js, this.root,指向模塊樹的根模塊 register,根據path和參數,構造模塊,而且根據path掛載到root指向的模塊樹上,而後遍歷參數的modules對象,遞歸調用register。 unregister,根據path從模塊樹上卸載模塊 update,遞歸更新整個模塊樹 get 根據path從模塊樹上獲取module,例如path爲['a'],獲取到a的module getNamespace 獲取模塊樹上某一個模塊的命名空間 注意: 模塊的命名空間只與模塊設置的namespaced屬性有關。沒有設置namespaced屬性的模塊它的命名空間仍是全局的。 import Module from './module' import { assert, forEachValue } from '../util' export default class ModuleCollection { constructor (rawRootModule) { // register root module (Vuex.Store options) this.register([], rawRootModule, false) } //根據path從模塊樹上獲取module,例如path爲['a'],獲取到a的module get (path) { return path.reduce((module, key) => { return module.getChild(key) }, this.root) } //獲取模塊樹上某一個模塊的命名空間,若是全部模塊的namespaced都爲true,那麼獲得的命名空間就和path相同 getNamespace (path) { let module = this.root return path.reduce((namespace, key) => { module = module.getChild(key) return namespace + (module.namespaced ? key + '/' : '') }, '') } //遞歸更新整個模塊樹 update (rawRootModule) { update([], this.root, rawRootModule) } /** *根據path和參數,構造模塊,而且根據path掛載到root *指向的模塊樹上,而後遍參數的modules對象,遞歸調用register。 */ register (path, rawModule, runtime = true) { if (process.env.NODE_ENV !== 'production') { assertRawModule(path, rawModule) } const newModule = new Module(rawModule, runtime) //根module if (path.length === 0) { this.root = newModule } else { //掛到父級module const parent = this.get(path.slice(0, -1)) parent.addChild(path[path.length - 1], newModule) } // register nested modules //子module if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime) }) } } //根據path從模塊樹上卸載模塊 unregister (path) { const parent = this.get(path.slice(0, -1)) const key = path[path.length - 1] if (!parent.getChild(key).runtime) return parent.removeChild(key) } } function update (path, targetModule, newModule) { if (process.env.NODE_ENV !== 'production') { assertRawModule(path, newModule) } // update target module targetModule.update(newModule) // update nested modules if (newModule.modules) { for (const key in newModule.modules) { if (!targetModule.getChild(key)) { if (process.env.NODE_ENV !== 'production') { console.warn( `[vuex] trying to add a new module '${key}' on hot reloading, ` + 'manual reload is needed' ) } return } update( path.concat(key), targetModule.getChild(key), newModule.modules[key] ) } } } function assertRawModule (path, rawModule) { ['getters', 'actions', 'mutations'].forEach(key => { if (!rawModule[key]) return forEachValue(rawModule[key], (value, type) => { assert( typeof value === 'function', makeAssertionMessage(path, key, type, value) ) }) }) } function makeAssertionMessage (path, key, type, value) { let buf = `${key} should be function but "${key}.${type}"` if (path.length > 0) { buf += ` in module "${path.join('.')}"` } buf += ` is ${JSON.stringify(value)}.` return buf } 4.4 模塊的安裝 上一節中,講到了將構造的模塊樹存到了this._modules中,接下來開始遍歷模塊樹進行安裝 installModule(this, state, [], this._modules.root) installModule方法作了以下幾件事情: 若是模塊有命名空間,將對應關係存入store._modulesNamespaceMap中 調用store._withCommit設置模塊的state到state樹上 建立模塊的局部上下文 local 循環註冊模塊的mutation、action和getter到 store._mutations、store._actions和store._wrappedGetters中 遍歷模塊的子模塊遞歸安裝 具體代碼以下: //安裝模塊 function installModule (store, rootState, path, module, hot) { const isRoot = !path.length const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { //若是這個模塊是有命名空間的,則將命名空間與模塊之間的關係存入_modulesNamespaceMap store._modulesNamespaceMap[namespace] = module } //非根組件而且非熱更新,熱更新是用新的模塊替換原來的模塊 if (!isRoot && !hot) { //根據path獲取上一級state對象 const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] //把模塊的state設置在rootState樹上 store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) } //建立命名空間下的context對象,包括state,getter,dispatch,commit const local = module.context = makeLocalContext(store, namespace, path) //註冊模塊的mutation,action和getter到store中 module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) module.forEachAction((action, key) => { const namespacedType = namespace + key registerAction(store, namespacedType, action, local) }) module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) //遞歸安裝子模塊 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) } 模塊的命名空間體如今了模塊註冊的的各個部分,首先是局部上下文的建立 const local = module.context = makeLocalContext(store, namespace, path) 上下文包括了四個部分 dispatch方法,若是命名空間是空字符串,則直接返回store.dispatch,若是有命名空間,而且調用dispath的時候第三個參數options.root!=true的狀況下,就會在調用store.dispatch的時候type加上命名空間,這樣就只調用命名空間下的action。 commit方法,與dispatch方法同理 getters對象,從從store.getters中篩選命名空間下的getters state對象,根據path從store.state中找模塊對應的state 若是沒有命名空間的話,那麼全局的上下文就是store中的這四個元素。 具體makeLocalContext方法以下: /** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */ //根據命名空間來生成局部的上下文,包括type加上namespace的dispatch,commit,還有根據namespace獲取的局部state和getter function makeLocalContext (store, namespace, path) { const noNamespace = namespace === '' const local = { dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args //在!options.root的狀況下type添加命名空間 if (!options || !options.root) { //在type前面加上namespace,只觸發該namespace的actions type = namespace + type if (process.env.NODE_ENV !== 'production' && !store._actions[type]) { console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`) return } } return store.dispatch(type, payload) }, commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args //在!options.root的狀況下type添加命名空間 if (!options || !options.root) { //在type前面加上namespace,只觸發該namespace的mutation type = namespace + type if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) { console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`) return } } store.commit(type, payload, options) } } // getters and state object must be gotten lazily // because they will be changed by vm update Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, //<2>local的state仍是從store中取的state state: { get: () => getNestedState(store.state, path) } }) return local } 其中還用到了以下方法: makeLocalGetters方法 //生成命名空間下的getter,從store的getter中篩選前綴是namespace的屬性 function makeLocalGetters (store, namespace) { const gettersProxy = {} const splitPos = namespace.length Object.keys(store.getters).forEach(type => { // skip if the target getter is not match this namespace if (type.slice(0, splitPos) !== namespace) return // extract local getter type const localType = type.slice(splitPos) // Add a port to the getters proxy. // Define as getter property because // we do not want to evaluate the getters in this time. Object.defineProperty(gettersProxy, localType, { get: () => store.getters[type], enumerable: true }) }) return gettersProxy } unifyObjectStyle方法用於dispatch和commit方法參數的適配處理 //參數的適配處理 //能夠只傳一個對象參數,對象中有type,對象自己是payload,第二個參數是options function unifyObjectStyle (type, payload, options) { if (isObject(type) && type.type) { options = payload payload = type type = type.type } if (process.env.NODE_ENV !== 'production') { assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`) } return { type, payload, options } } getNestedState方法 //根據path獲取state狀態,註冊state樹的時候用到 function getNestedState (state, path) { return path.length ? path.reduce((state, key) => state[key], state) : state } 建立了上下文之後,就開始註冊mutation、action、和getter。 註冊mutaion的方法,能夠看到 store._mutations[type]爲數組,也就是說能夠有多個key值相同的mutation 只傳給mutation的是local.state,即不建議利用mutation操做命名空間以外的state 咱們是直接在咱們寫的mutation中改變state,而不須要像redux中寫reducer那樣要返回一個新的對象,纔可以觸發訂閱state變化的事件 store.state是get,不能直接修改,而local.state是從state對象上找的指針,因此能夠向直接操做Vue中定義的data同樣直接操做改變,而能觸發響應。 //註冊mutation,將mutation存入store._mutations,傳入的type爲模塊namespace+mutation的key值 //store._mutations[type]爲數組,也就是說能夠有多個key值相同的mutation function registerMutation (store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []) entry.push(function wrappedMutationHandler (payload) { //這裏的handler是咱們本身寫的mutation函數, //最終調用mutation的時候傳入的是局部的state。 //這是最終改變state的地方 //我猜測沒有傳入全局state的緣由是不想讓咱們利用局部的mutation改變全局的state //而把全局的state傳入了action,這樣就能夠在action中拿到全局的state做爲payload //傳入mutation handler(local.state, payload) }) } 註冊action的方法 能夠看到將上下文中的四項都傳給了它,並且還傳了store的getters和state,因此在action中能夠調用store中的任何state和getters來觸發該命名空間下和全局的action和mutation,複雜的組合邏輯均可以寫到action函數中。 還能夠看到store._actions中的函數返回的確定都是Promise //註冊action到store._actions,傳入的type爲模塊的namespace+action的key值 //store._actions[type]爲數組,也就是說能夠有多個key值相同的action function registerAction (store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []) entry.push(function wrappedActionHandler (payload, cb) { //這裏的handler是咱們本身寫的action函數 //能夠看到傳入了局部的dispatch,commit,getter,state,還有全局的getter和state let res = handler({ dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb) //若是action返回的結果不是Promise,也會包裝成Promise,因此最後action返回的結果是Promsie if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) } 註冊getter的方法 註冊getter的時候若是重名之前面的爲準 getter也均可以利用全局的state和getter來組合 //註冊getter,一樣type是模塊的namespace+getter的key值 function registerGetter (store, type, rawGetter, local) { //getter不是數組,是惟一的函數,action和mutation是數組 //若是已經有了則return,說明註冊getter的時候若是重名之前面的爲準 if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate getter key: ${type}`) } return } store._wrappedGetters[type] = function wrappedGetter (store) { //傳給getter的四個參數 return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) } } 至此,模塊的安裝就告一段落,經歷了installModule以後,Store的_actions,_mutations,_wrappedGetters,還有內部的state就都擁有了內容。 4.5 設置Store的VM對象 在構造函數中調用以下: resetStoreVM(this, state) resetStoreVM方法 設置store._vm爲VM,其中將內部變量state做爲data,將store._wrappedGetters做爲計算屬性,利用了VM的雙向綁定和計算屬性的緩存 設置store.getters,指向store._vm的計算屬性,利用它的緩存 清空舊VM的數據並銷燬 function resetStoreVM (store, state, hot) { const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism //將getter存到computed對象中,而後給_vm做爲計算屬性,利用了算屬性的緩存機制 computed[key] = () => fn(store) //設置store的getters,從_vm中取,也能夠直接get: () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins const silent = Vue.config.silent Vue.config.silent = true //利用install module以後獲得的rootState和store._wrappedGetters獲得的計算屬性 //建立Vue對象做爲store._vm store._vm = new Vue({ data: { $$state: state //this.state }, computed }) Vue.config.silent = silent // enable strict mode for new vm if (store.strict) { enableStrictMode(store) } if (oldVm) { if (hot) { // dispatch changes in all subscribed watchers // to force getter re-evaluation for hot reloading. store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) } } 最終對外的store.state是經過getter來訪問store._vm._data.$$state,實現了只讀的效果。 //取得this._vm._data.$$state get state () { return this._vm._data.$$state } //不能直接給state賦值 set state (v) { if (process.env.NODE_ENV !== 'production') { assert(false, `Use store.replaceState() to explicit replace store state.`) } } 4.6 commit and dispatch方法 commit方法是store對象對外提供的執行mutation的方法 根據type從this._mutations中找到mutation並依次執行 遍歷執行this._subscribers觸發訂閱state變化的函數 //對外提供的觸發mutation的方法 commit (_type, _payload, _options) { // check object-style commit const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } const entry = this._mutations[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown mutation type: ${type}`) } return } //執行註冊的mutation, this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) //觸發訂閱者 this._subscribers.forEach(sub => sub(mutation, this.state)) if ( process.env.NODE_ENV !== 'production' && options && options.silent ) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } } 在執行mutation改變state的時候調用了_withCommit方法,它的做用是執行改變state的時候,保證store._committing === true。在resetStoreVM時,若是是設置了嚴格模式store.strict == true,則調用enableStrictMode方法,利用store._vm 的watch方法,監聽state的變化,若是變化,則判斷store._committing === true,若是不是則發出警告不要利用mutation以外的方法改變state。 /** * * 保存執行時的committing狀態將當前狀態設置爲true後進行本次提交操做,待操做完畢後,將committing狀態還原爲以前的狀態 */ _withCommit (fn) { // 保存以前的提交狀態 const committing = this._committing // 進行本次提交,若不設置爲true,直接修改state,strict模式下,Vuex將會產生非法修改state的警告 this._committing = true // 執行state的修改操做 fn() // 修改完成,還本來次修改以前的狀態 this._committing = committing } } function enableStrictMode (store) { store._vm.$watch(function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`) } }, { deep: true, sync: true }) } dispatch方法是store對象對外提供的執行action的方法,返回值是promise dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload) const entry = this._actions[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } //逐個執行action,返回promise return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) } 4.7 訂閱state變化的方法 store提供了兩個訂閱state變化的方法,一個是subscribe,一個是watch。 subscribe方法將訂閱函數放在store._subscribers中,用於監聽state的變化,實際上是監聽commit方法的執行,在上一節的commit代碼中能夠看到,只要執行commit方法就觸發store._subscribers中函數的執行。 //訂閱state變化的方法 subscribe (fn) { const subs = this._subscribers if (subs.indexOf(fn) < 0) { subs.push(fn) } return () => { const i = subs.indexOf(fn) if (i > -1) { subs.splice(i, 1) } } } watch方法用來訂閱咱們對於store.state和store.getters自定義屬性的變化,利用了store._watcherVM.$watch方法 watch (getter, cb, options) { if (process.env.NODE_ENV !== 'production') { assert(typeof getter === 'function', `store.watch only accepts a function.`) } return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options) } 4.8 更新store的方法 replaceState替換store.state的方法 //替換全局的state replaceState (state) { this._withCommit(() => { this._vm._data.$$state = state }) } registerModule在store中註冊新模塊的方法,適用於多業務線之間的配合,每個業務線將自身的模塊註冊到sore中。 registerModule (path, rawModule) { if (typeof path === 'string') path = [path] if (process.env.NODE_ENV !== 'production') { assert(Array.isArray(path), `module path must be a string or an Array.`) assert(path.length > 0, 'cannot register the root module by using registerModule.') } //在模塊樹上進行註冊 this._modules.register(path, rawModule) //安裝該模塊 installModule(this, this.state, path, this._modules.get(path)) // reset store to update getters... //更新_vm,主要是更新VM的計算屬性和store.getters resetStoreVM(this, this.state) } resetStore重置store的方法,除了state不變之外,從新安裝模塊和重設store._vm //重置store function resetStore (store, hot) { store._actions = Object.create(null) store._mutations = Object.create(null) store._wrappedGetters = Object.create(null) store._modulesNamespaceMap = Object.create(null) const state = store.state // init all modules //state沒有變,installModule的第五個參數爲true,不會重置state installModule(store, state, [], store._modules.root, true) // reset vm resetStoreVM(store, state, hot) } hotUpdate熱更新模塊的方法,調用模塊樹的update方法,更新模塊樹,而後resetStore,不會更新state。 //熱更新store hotUpdate (newOptions) { this._modules.update(newOptions) resetStore(this, true) } 4.9 helpers.js Vuex的入口文件除了導出Store和install方法以外,還導出了四個幫助咱們在Vue中使用Vuex的方法mapState,mapMutations,mapGetters,mapActions,他們都放在helpers.js文件中,因此咱們要分析下這個文件。 getModuleByNamespace利用存儲在store._modulesNamespaceMap中namespace與module的關係,來獲取module。 //根據命名空間獲取模塊 function getModuleByNamespace (store, helper, namespace) { const module = store._modulesNamespaceMap[namespace] if (process.env.NODE_ENV !== 'production' && !module) { console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`) } return module } normalizeNamespace 是內部幫助函數,規範命名空間的函數,兼容不傳namespace的狀況,若是不傳則是空字符串 //兼容沒有命名空間的函數 function normalizeNamespace (fn) { return (namespace, map) => { if (typeof namespace !== 'string') { map = namespace namespace = '' } else if (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/' } return fn(namespace, map) } } normalizeMap是內部幫助函數,規範map的函數,能夠接受數組或者對象,最後返回的對象是{key,val}形式 function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) } mapState函數,先調用normalizeNamespace函數規範命名空間參數,而後規範化傳入的states對象,若是val是字符串則直接返回state[val],若是val是函數,則傳入state和getter返回函數執行結果。 export const mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { res[key] = function mappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { const module = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } state = module.context.state getters = module.context.getters } return typeof val === 'function' ? val.call(this, state, getters) : state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res }) mapGetters函數,同理是先規範化,而後把key值添加上namespace從this.$store.getters中取。 export const mapGetters = normalizeNamespace((namespace, getters) => { const res = {} normalizeMap(getters).forEach(({ key, val }) => { val = namespace + val res[key] = function mappedGetter () { if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) { return } if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) { console.error(`[vuex] unknown getter: ${val}`) return } return this.$store.getters[val] } // mark vuex getter for devtools res[key].vuex = true }) return res }) mapMutations函數,同理先規範化,key值添加namespace,而後調用commit函數,觸發mutation。 export const mapMutations = normalizeNamespace((namespace, mutations) => { const res = {} normalizeMap(mutations).forEach(({ key, val }) => { val = namespace + val res[key] = function mappedMutation (...args) { if (namespace && !getModuleByNamespace(this.$store, 'mapMutations', namespace)) { return } //調用commit方法執行mutation return this.$store.commit.apply(this.$store, [val].concat(args)) } }) return res }) mapActions函數,同理先規範化,key值添加namespace,而後調用dispatch函數,觸發action。 export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} normalizeMap(actions).forEach(({ key, val }) => { val = namespace + val res[key] = function mappedAction (...args) { if (namespace && !getModuleByNamespace(this.$store, 'mapActions', namespace)) { return } //調用dispatch方法執行action return this.$store.dispatch.apply(this.$store, [val].concat(args)) } }) return res }) 5 Redux與Vuex的對比 1 Redux改變state的方法是在reducer中返回新的state對象, Vuex是在mutation中直接改變state 2 state變化觸發視圖的更新,在Redux中是須要本身去監聽,而後從新setState, 在Vuex中是直接利用了VM的數據響應 3 Vuex將改變state的方法進行了細分,分紅mutation和action,更利於寫複雜的邏輯,還直接處理了異步的狀況,而Redux中須要在中間件中處理
Vue 實例從建立到銷燬的過程,就是生命週期。從開始建立、初始化數據、編譯模板、掛載Dom→渲染、更新→渲染、銷燬等一系列過程,稱之爲 Vue 的生命週期。 生命週期中有多個事件鉤子,以下: - beforeCreate(建立前) 在數據觀測和初始化事件還未開始 - created(建立後) 完成數據觀測,屬性和方法的運算,初始化事件,$el屬性尚未顯示出來 - beforeMount(載入前) 在掛載開始以前被調用,相關的render函數首次被調用。實例已完成如下的配置:編譯模板,把data裏面的數據和模板生成html。注意此時尚未掛載html到頁面上。 - mounted(載入後) 在el 被新建立的 vm.$el 替換,並掛載到實例上去以後調用。實例已完成如下的配置:用上面編譯好的html內容替換el屬性指向的DOM對象。完成模板中的html渲染到html頁面中。此過程當中進行ajax交互。 - beforeUpdate(更新前) 在數據更新以前調用,發生在虛擬DOM從新渲染和打補丁以前。能夠在該鉤子中進一步地更改狀態,不會觸發附加的重渲染過程。 - updated(更新後) 在因爲數據更改致使的虛擬DOM從新渲染和打補丁以後調用。調用時,組件DOM已經更新,因此能夠執行依賴於DOM的操做。然而在大多數狀況下,應該避免在此期間更改狀態,由於這可能會致使更新無限循環。該鉤子在服務器端渲染期間不被調用。 - beforeDestroy(銷燬前) 在實例銷燬以前調用。實例仍然徹底可用。 - destroyed(銷燬後) 在實例銷燬以後調用。調用後,全部的事件監聽器會被移除,全部的子實例也會被銷燬。該鉤子在服務器端渲染期間不被調用。
- $refs - this.$nextTick
1. v-text v-text主要用來更新textContent,能夠等同於JS的text屬性。 <span v-text="msg"></span> 這二者等價: <span>{{msg}}</span> 2. v-html 雙大括號的方式會將數據解釋爲純文本,而非HTML。爲了輸出真正的HTML,能夠用v-html指令。它等同於JS的innerHtml屬性。 <div v-html="rawHtml"></div> 這個div的內容將會替換成屬性值rawHtml,直接做爲HTML進行渲染。 3. v-pre v-pre主要用來跳過這個元素和它的子元素編譯過程。能夠用來顯示原始的Mustache標籤。跳過大量沒有指令的節點加快編譯。 <div id="app"> <span v-pre>{{message}}</span> //這條語句不進行編譯 <span>{{message}}</span> </div> 最終僅顯示第二個span的內容 4. v-cloak 這個指令是用來保持在元素上直到關聯實例結束時進行編譯。 <div id="app" v-cloak> <div> {{message}} </div> </div> <script type="text/javascript"> new Vue({ el:'#app', data:{ message:'hello world' } }) </script> 在頁面加載時會閃爍,先顯示: <div> {{message}} </div> 而後纔會編譯爲: <div> hello world! </div> 5. v-once v-once關聯的實例,只會渲染一次。以後的從新渲染,實例極其全部的子節點將被視爲靜態內容跳過,這能夠用於優化更新性能。 <span v-once>This will never change:{{msg}}</span> //單個元素 <div v-once>//有子元素 <h1>comment</h1> <p>{{msg}}</p> </div> <my-component v-once:comment="msg"></my-component> //組件 <ul> <li v-for="i in list">{{i}}</li> </ul> 上面的例子中,msg,list即便產生改變,也不會從新渲染。 6. v-if v-if能夠實現條件渲染,Vue會根據表達式的值的真假條件來渲染元素。 <a v-if="ok">yes</a> 若是屬性值ok爲true,則顯示。不然,不會渲染這個元素。 7. v-else v-else是搭配v-if使用的,它必須緊跟在v-if或者v-else-if後面,不然不起做用。 <a v-if="ok">yes</a> <a v-else>No</a> 8. v-else-if v-else-if充當v-if的else-if塊,能夠鏈式的使用屢次。能夠更加方便的實現switch語句。 <div v-if="type==='A'"> A </div> <div v-else-if="type==='B'"> B </div> <div v-else-if="type==='C'"> C </div> <div v-else> Not A,B,C </div> 9. v-show <h1 v-show="ok">hello world</h1> 也是用於根據條件展現元素。和v-if不一樣的是,若是v-if的值是false,則這個元素被銷燬,不在dom中。可是v-show的元素會始終被渲染並保存在dom中,它只是簡單的切換css的dispaly屬性。 注意:v-if有更高的切換開銷 v-show有更高的初始渲染開銷。 所以,若是要很是頻繁的切換,則使用v-show較好;若是在運行時條件不太可能改變,則v-if較好 10. v-for 用v-for指令根據遍歷數組來進行渲染 有下面兩種遍歷形式 <div v-for="(item,index) in items"></div> //使用in,index是一個可選參數,表示當前項的索引 <div v-for="item of items"></div> //使用of 下面是一個例子,而且在v-for中,擁有對父做用域屬性的徹底訪問權限。 <ul id="app"> <li v-for="item in items"> {{parent}}-{{item.text}} </li> </ul> <script type="text/javascript"> var example = new Vue({ el:'#app', data:{ parent:'父做用域' items:[ {text:'文本1'}, {text:'文本2'} ] } }) </script> 會被渲染爲: <ul id="app"> <li>父做用域-文本1</li> <li>父做用域-文本2</li> </ul> 注意:當v-for和v-if同處於一個節點時,v-for的優先級比v-if更高。這意味着v-if將運行在每一個v-for循環中 11. v-bind v-bind用來動態的綁定一個或者多個特性。沒有參數時,能夠綁定到一個包含鍵值對的對象。經常使用於動態綁定class和style。以及href等。 簡寫爲一個冒號【 :】 <1>對象語法: //進行類切換的例子 <div id="app"> <!--當data裏面定義的isActive等於true時,is-active這個類纔會被添加起做用--> <!--當data裏面定義的hasError等於true時,text-danger這個類纔會被添加起做用--> <div :class="{'is-active':isActive, 'text-danger':hasError}"></div> </div> <script> var app = new Vue({ el: '#app', data: { isActive: true, hasError: false } }) </script> 渲染結果: <!--由於hasError: false,因此text-danger不被渲染--> <div class = "is-active"></div> <2>數組語法 <div id="app"> <!--數組語法:errorClass在data對應的類必定會添加--> <!--is-active是對象語法,根據activeClass對應的取值決定是否添加--> <p :class="[{'is-active':activeClass},errorClass]">12345</p> </div> <script> var app = new Vue({ el: '#app', data: { activeClass: false, errorClass: 'text-danger' } }) </script> 渲染結果: <!--由於activeClass: false,因此is-active不被渲染--> <p class = "text-danger"></p> <3>直接綁定數據對象 <div id="app"> <!--在vue實例的data中定義了classObject對象,這個對象裏面是全部類名及其真值--> <!--當裏面的類的值是true時會被渲染--> <div :class="classObject">12345</div> </div> <script> var app = new Vue({ el: '#app', data: { classObject:{ 'is-active': false, 'text-danger':true } } }) </script> 渲染結果: <!--由於'is-active': false,因此is-active不被渲染--> <div class = "text-danger"></div> 12. v-model 這個指令用於在表單上建立雙向數據綁定。 v-model會忽略全部表單元素的value、checked、selected特性的初始值。由於它選擇Vue實例數據作爲具體的值。 <div id="app"> <input v-model="somebody"> <p>hello {{somebody}}</p> </div> <script> var app = new Vue({ el: '#app', data: { somebody:'小明' } }) </script> 這個例子中直接在瀏覽器input中輸入別的名字,下面的p的內容會直接跟着變。這就是雙向數據綁定。 v-model修飾符 <1> .lazy 默認狀況下,v-model同步輸入框的值和數據。能夠經過這個修飾符,轉變爲在change事件再同步。 <input v-model.lazy="msg"> <2> .number 自動將用戶的輸入值轉化爲數值類型 <input v-model.number="msg"> <3> .trim 自動過濾用戶輸入的首尾空格 <input v-model.trim="msg"> 13. v-on v-on主要用來監聽dom事件,以便執行一些代碼塊。表達式能夠是一個方法名。 簡寫爲:【 @ 】 <div id="app"> <button @click="consoleLog"></button> </div> <script> var app = new Vue({ el: '#app', methods:{ consoleLog:function (event) { console.log(1) } } }) </script> 事件修飾符 .stop 阻止事件繼續傳播 .prevent 事件再也不重載頁面 .capture 使用事件捕獲模式,即元素自身觸發的事件先在此到處理,而後才交由內部元素進行處理 .self 只當在 event.target 是當前元素自身時觸發處理函數 .once 事件將只會觸發一次 .passive 告訴瀏覽器你不想阻止事件的默認行爲 <!-- 阻止單擊事件繼續傳播 --> <a v-on:click.stop="doThis"></a> <!-- 提交事件再也不重載頁面 --> <form v-on:submit.prevent="onSubmit"></form> <!-- 修飾符能夠串聯 --> <a v-on:click.stop.prevent="doThat"></a> <!-- 只有修飾符 --> <form v-on:submit.prevent></form> <!-- 添加事件監聽器時使用事件捕獲模式 --> <!-- 即元素自身觸發的事件先在此到處理,而後才交由內部元素進行處理 --> <div v-on:click.capture="doThis">...</div> <!-- 只當在 event.target 是當前元素自身時觸發處理函數 --> <!-- 即事件不是從內部元素觸發的 --> <div v-on:click.self="doThat">...</div> <!-- 點擊事件將只會觸發一次 --> <a v-on:click.once="doThis"></a> <!-- 滾動事件的默認行爲 (即滾動行爲) 將會當即觸發 --> <!-- 而不會等待 `onScroll` 完成 --> <!-- 這其中包含 `event.preventDefault()` 的狀況 --> <div v-on:scroll.passive="onScroll">...</div> 使用修飾符時,順序很重要;相應的代碼會以一樣的順序產生。所以,用v-on:click.prevent.self會阻止全部的點擊,而 v-on:click.self.prevent 只會阻止對元素自身的點擊。
放在計算屬性遍歷 computed: { activeUsers: function () { return this.users.filter(function (user) { return user.isActive }) } }
> key 是爲 Vue 中 vnode 的惟一標記,經過這個 key,咱們的 diff 操做能夠更準確、更快速。Vue 的 diff 過程能夠歸納爲:oldCh 和 newCh 各有兩個頭尾的變量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它們會新節點和舊節點會進行兩兩對比,即一共有4種比較方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,若是以上 4 種比較都沒匹配,若是設置了key,就會用 key 再進行比較,在比較的過程當中,遍歷會往中間靠,一旦 StartIdx > EndIdx 代表 oldCh 和 newCh 至少有一個已經遍歷完了,就會結束比較。 因此 Vue 中 key 的做用是:key 是爲 Vue 中 vnode 的惟一標記,經過這個 key,咱們的 diff 操做能夠更準確、更快速 更準確:由於帶 key 就不是就地複用了,在 sameNode 函數 a.key === b.key 對比中能夠避免就地複用的狀況。因此會更加準確。 更快速:利用 key 的惟一性生成 map 對象來獲取對應節點,比遍歷方式更快,源碼以下: function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map } 強制替換元素,從而能夠觸發組件的生命週期鉤子或者觸發過渡。由於當key改變時,Vue認爲一個新的元素產生了,從而會新插入一個元素來替換掉原有的元素。 <transition> <span :key="text">{{text}}</span> </transition>、 --這裏若是text發生改變,整個<span>元素會發生更新,由於當text改變時,這個元素的key屬性就發生了改變,在渲染更新時,Vue會認爲這裏新產生了一個元素,而老的元素因爲key不存在了,因此會被刪除,從而觸發了過渡。 同理,key屬性被用在組件上時,當key改變時會引發新組件的建立和原有組件的刪除,此時組件的生命週期鉤子就會被觸發。
- vuex - vue-Router - axios - element-ui ....
vue-router 有 3 種路由模式:hash、history、abstract,對應的源碼以下所示: switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } - hash模式:在瀏覽器中符號「#」,#以及#後面的字符稱之爲hash,用window.location.hash讀取;特色:hash雖然在URL中,但不被包括在HTTP請求中;用來指導瀏覽器動做,對服務端安全無用,hash不會重加載頁面。 早期的前端路由的實現就是基於 location.hash 來實現的。其實現原理很簡單,location.hash 的值就是 URL 中 # 後面的內容。好比下面這個網站,它的 location.hash 的值爲 '#search': hash 路由模式的實現主要是基於下面幾個特性: URL 中 hash 值只是客戶端的一種狀態,也就是說當向服務器端發出請求時,hash 部分不會被髮送; hash 值的改變,都會在瀏覽器的訪問歷史中增長一個記錄。所以咱們能經過瀏覽器的回退、前進按鈕控制hash 的切換; 能夠經過 a 標籤,並設置 href 屬性,當用戶點擊這個標籤後,URL 的 hash 值會發生改變;或者使用 JavaScript 來對 loaction.hash 進行賦值,改變 URL 的 hash 值; 咱們能夠使用 hashchange 事件來監聽 hash 值的變化,從而對頁面進行跳轉(渲染)。 - history模式:history採用HTML5的新特性;且提供了兩個新方法:pushState(),replaceState()能夠對瀏覽器歷史記錄棧進行修改,以及popState事件的監聽到狀態變動。 HTML5 提供了 History API 來實現 URL 的變化。其中作最主要的 API 有如下兩個:history.pushState() 和 history.repalceState()。這兩個 API 能夠在不進行刷新的狀況下,操做瀏覽器的歷史紀錄。惟一不一樣的是,前者是新增一個歷史記錄,後者是直接替換當前的歷史記錄,以下所示: window.history.pushState(null, null, path); window.history.replaceState(null, null, path); history 路由模式的實現主要基於存在下面幾個特性: pushState 和 repalceState 兩個 API 來操做實現 URL 的變化 ; 咱們能夠使用 popstate 事件來監聽 url 的變化,從而對頁面進行跳轉(渲染); history.pushState() 或 history.replaceState() 不會觸發 popstate 事件,這時咱們須要手動觸發頁面跳轉(渲染)。 - abstract : 支持全部 JavaScript 運行環境,如 Node.js 服務器端。若是發現沒有瀏覽器的 API,路由會自動強制進入這個模式.
var proxy = { local:"http://localhost:9999",//mock環境 } var config = { dev: { historyApiFallback: true, stats: { colors: true }, hot: true, inline: true, progress: true, disableHostCheck:true, //contentBase:"./app/index", proxy: { '/api/mock': { target: proxy.local, //pathRewrite: {'^/column' : '/column'}, secure: false, changeOrigin: true } } }, portPro: '10086' } module.exports = config;
console.time('start server-after-package need time') const http = require('http') const fs = require('fs') var proxyhttp = require('express-http-proxy') var express = require('express') var app = express() var proxy = require('./server-proxy') app.set('roots', __dirname+'/dist') app.use('/', express.static(app.get('roots'))) app.engine('html', require('ejs').renderFile) for (var i in proxy.dev.proxy) { if (proxy.dev.proxy.hasOwnProperty(i)) { console.log(i, proxy.dev.proxy[i].target) app.use(i + '/*', proxyhttp(proxy.dev.proxy[i].target, { proxyReqPathResolver: function (req, res) { console.log(req.originalUrl) return req.originalUrl } })) } } app.use('*', function (req, res, next) { fs.readFile(app.get('roots')+'/index.html', 'utf-8', (err, content) => { if (err) { console.log('We cannot open "index.htm" file.') } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) res.end(content) }) }); var server = app.listen(proxy.portPro, function () { var host = server.address().address var port = server.address().port console.log('app listening at ' + require("os").hostname() + ' http://localhost:' + port) console.timeEnd('start server-after-package need time') })
- let 和 const - Set 和 Map數據結構 - Class - 模板字符串 - 箭頭函數 - Itertor 和 for of 遍歷索引數組和類數組對象 - ... 參數加強和打散數組 - 解構 數組/對象/參數 - Promise - Symbol 基本類型 - Reflect - Proxy - Decorator 裝飾器 - es6 module es6模塊
主要區別在this指向問題 - 普通函數的this 指向調用它的那個對象,例如 obj.func ,那麼func中的this就是obj - 箭頭函數不能做爲構造函數,不能使用new,沒有this,arguments箭頭函數,箭頭函數的this永遠指向其上下文的 this ,任何方法都改變不了其指向,如 call() , bind() , apply()(或者說箭頭函數中的this指向的是定義時的this,而不是執行時的this)
都是objcss
const obj={ a:()=>console.log(this) } ① obj.a() ② const test=obj.a; test(); ③ const test2={} obj.a.call(test2)
Vue 3.0 正走在發佈的路上,Vue 3.0 的目標是讓 Vue 核心變得更小、更快、更強大,所以 Vue 3.0 增長如下這些新特性: (1)監測機制的改變 3.0 將帶來基於代理 Proxy 的 observer 實現,提供全語言覆蓋的反應性跟蹤。這消除了 Vue 2 當中基於 Object.defineProperty 的實現所存在的不少限制: 只能監測屬性,不能監測對象 檢測屬性的添加和刪除; 檢測數組索引和長度的變動; 支持 Map、Set、WeakMap 和 WeakSet。 新的 observer 還提供瞭如下特性: 用於建立 observable 的公開 API。這爲中小規模場景提供了簡單輕量級的跨組件狀態管理解決方案。 默認採用惰性觀察。在 2.x 中,無論反應式數據有多大,都會在啓動時被觀察到。若是你的數據集很大,這可能會在應用啓動時帶來明顯的開銷。在 3.x 中,只觀察用於渲染應用程序最初可見部分的數據。 更精確的變動通知。在 2.x 中,經過 Vue.set 強制添加新屬性將致使依賴於該對象的 watcher 收到變動通知。在 3.x 中,只有依賴於特定屬性的 watcher 纔會收到通知。 不可變的 observable:咱們能夠建立值的「不可變」版本(即便是嵌套屬性),除非系統在內部暫時將其「解禁」。這個機制可用於凍結 prop 傳遞或 Vuex 狀態樹之外的變化。 更好的調試功能:咱們能夠使用新的 renderTracked 和 renderTriggered 鉤子精確地跟蹤組件在何時以及爲何從新渲染。 (2)模板 模板方面沒有大的變動,只改了做用域插槽,2.x 的機制致使做用域插槽變了,父組件會從新渲染,而 3.0 把做用域插槽改爲了函數的方式,這樣只會影響子組件的從新渲染,提高了渲染的性能。 同時,對於 render 函數的方面,vue3.0 也會進行一系列更改來方便習慣直接使用 api 來生成 vdom 。 (3)對象式的組件聲明方式 vue2.x 中的組件是經過聲明的方式傳入一系列 option,和 TypeScript 的結合須要經過一些裝飾器的方式來作,雖然能實現功能,可是比較麻煩。3.0 修改了組件的聲明方式,改爲了類式的寫法,這樣使得和 TypeScript 的結合變得很容易。 此外,vue 的源碼也改用了 TypeScript 來寫。其實當代碼的功能複雜以後,必須有一個靜態類型系統來作一些輔助管理。如今 vue3.0 也全面改用 TypeScript 來重寫了,更是使得對外暴露的 api 更容易結合 TypeScript。靜態類型系統對於複雜代碼的維護確實頗有必要。 (4)其它方面的更改 vue3.0 的改變是全面的,上面只涉及到主要的 3 個方面,還有一些其餘的更改: 支持自定義渲染器,從而使得 weex 能夠經過自定義渲染器的方式來擴展,而不是直接 fork 源碼來改的方式。 支持 Fragment(多個根節點)和 Protal(在 dom 其餘部分渲染組建內容)組件,針對一些特殊的場景作了處理。 基於 treeshaking 優化,提供了更多的內置功能。
- cors 服務器端對於CORS的支持,主要就是經過設置Access-Control-Allow-Origin來進行的。若是瀏覽器檢測到相應的設置,就能夠容許Ajax進行跨域的訪問。 - jsonp var script = document.createElement('script'); script.src = "http://aa.xx.com/js/*.js"; document.body.appendChild(script); - postMessage window對象新增了一個window.postMessage方法,容許跨窗口通訊,不論這兩個窗口是否同源。目前IE8+、FireFox、Chrome、Opera等瀏覽器都已經支持window.postMessage方法。 - window.name - location.hash - http-proxy - nginx - websocket - iframe 基於iframe實現的跨域要求兩個域具備aa.xx.com,bb.xx.com這種特色,也就是兩個頁面必須屬於同一個頂級基礎域(例如都是xxx.com,或是xxx.com.cn),使用同一協議(例如都是 http)和同一端口(例如都是80),這樣在兩個頁面中同時添加document.domain,就能夠實現父頁面調用子頁面的函數
一、一個衆所周知的問題,Ajax直接請求普通文件存在跨域無權限訪問的問題,甭管你是靜態頁面、動態網頁、web服務、WCF,只要是跨域請求,一概不許; 二、不過咱們又發現,Web頁面上調用js文件時則不受是否跨域的影響(不只如此,咱們還發現凡是擁有"src"這個屬性的標籤都擁有跨域的能力,好比<script>、<img>、<iframe>); 三、因而能夠判斷,當前階段若是想經過純web端(ActiveX控件、服務端代理、屬於將來的HTML5之Websocket等方式不算)跨域訪問數據就只有一種可能,那就是在遠程服務器上設法把數據裝進js格式的文件裏,供客戶端調用和進一步處理; 四、恰巧咱們已經知道有一種叫作JSON的純字符數據格式能夠簡潔的描述複雜數據,更妙的是JSON還被js原生支持,因此在客戶端幾乎能夠爲所欲爲的處理這種格式的數據; 五、這樣子解決方案就呼之欲出了,web客戶端經過與調用腳本如出一轍的方式,來調用跨域服務器上動態生成的js格式文件(通常以JSON爲後綴),顯而易見,服務器之因此要動態生成JSON文件,目的就在於把客戶端須要的數據裝入進去。 六、客戶端在對JSON文件調用成功以後,也就得到了本身所需的數據,剩下的就是按照本身需求進行處理和展示了,這種獲取遠程數據的方式看起來很是像AJAX,但其實並不同。 七、爲了便於客戶端使用數據,逐漸造成了一種非正式傳輸協議,人們把它稱做JSONP,該協議的一個要點就是容許用戶傳遞一個callback參數給服務端,而後服務端返回數據時會將這個callback參數做爲函數名來包裹住JSON數據,這樣客戶端就能夠隨意定製本身的函數來自動處理返回數據了。 八、ajax 的核心是經過 XmlHttpRequest 獲取非本頁內容,而 jsonp 的核心則是動態添加 <script> 標籤來調用服務器提供的 js 腳本。 九、jsonp是一種方式或者說非強制性協議,如同ajax同樣,它也不必定非要用json格式來傳遞數據,若是你願意,字符串都行,只不過這樣不利於用jsonp提供公開服務。
1、簡介 CORS須要瀏覽器和服務器同時支持。目前,全部瀏覽器都支持該功能,IE瀏覽器不能低於IE10。 整個CORS通訊過程,都是瀏覽器自動完成,不須要用戶參與。對於開發者來講,CORS通訊與同源的AJAX通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。 所以,實現CORS通訊的關鍵是服務器。只要服務器實現了CORS接口,就能夠跨源通訊。 2、兩種請求 瀏覽器將CORS請求分紅兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。 只要同時知足如下兩大條件,就屬於簡單請求。 (1) 請求方法是如下三種方法之一: HEAD GET POST (2)HTTP的頭信息不超出如下幾種字段: Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain 凡是不一樣時知足上面兩個條件,就屬於非簡單請求。 瀏覽器對這兩種請求的處理,是不同的。 3、簡單請求 3.1 基本流程 對於簡單請求,瀏覽器直接發出CORS請求。具體來講,就是在頭信息之中,增長一個Origin字段。 下面是一個例子,瀏覽器發現此次跨源AJAX請求是簡單請求,就自動在頭信息之中,添加一個Origin字段。 GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0... 上面的頭信息中,Origin字段用來講明,本次請求來自哪一個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否贊成此次請求。 若是Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。注意,這種錯誤沒法經過狀態碼識別,由於HTTP迴應的狀態碼有多是200。 若是Origin指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段。 Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8 上面的頭信息之中,有三個與CORS請求相關的字段,都以Access-Control-開頭。 (1)Access-Control-Allow-Origin 該字段是必須的。它的值要麼是請求時Origin字段的值,要麼是一個*,表示接受任意域名的請求。 (2)Access-Control-Allow-Credentials 該字段可選。它的值是一個布爾值,表示是否容許發送Cookie。默認狀況下,Cookie不包括在CORS請求之中。設爲true,即表示服務器明確許可,Cookie能夠包含在請求中,一塊兒發給服務器。這個值也只能設爲true,若是服務器不要瀏覽器發送Cookie,刪除該字段便可。 (3)Access-Control-Expose-Headers 該字段可選。CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其餘字段,就必須在Access-Control-Expose-Headers裏面指定。上面的例子指定,getResponseHeader('FooBar')能夠返回FooBar字段的值。 3.2 withCredentials 屬性 上面說到,CORS請求默認不發送Cookie和HTTP認證信息。若是要把Cookie發到服務器,一方面要服務器贊成,指定Access-Control-Allow-Credentials字段。 Access-Control-Allow-Credentials: true 另外一方面,開發者必須在AJAX請求中打開withCredentials屬性。 var xhr = new XMLHttpRequest(); xhr.withCredentials = true; 不然,即便服務器贊成發送Cookie,瀏覽器也不會發送。或者,服務器要求設置Cookie,瀏覽器也不會處理。 可是,若是省略withCredentials設置,有的瀏覽器仍是會一塊兒發送Cookie。這時,能夠顯式關閉withCredentials。 xhr.withCredentials = false; 須要注意的是,若是要發送Cookie,Access-Control-Allow-Origin就不能設爲星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie纔會上傳,其餘域名的Cookie並不會上傳,且(跨源)原網頁代碼中的document.cookie也沒法讀取服務器域名下的Cookie。 4、非簡單請求 4.1 預檢請求 非簡單請求是那種對服務器有特殊要求的請求,好比請求方法是PUT或DELETE,或者Content-Type字段的類型是application/json。 非簡單請求的CORS請求,會在正式通訊以前,增長一次HTTP查詢請求,稱爲"預檢"請求(preflight)。 瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及能夠使用哪些HTTP動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,不然就報錯。 下面是一段瀏覽器的JavaScript腳本。 var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send(); 上面代碼中,HTTP請求的方法是PUT,而且發送一個自定義頭信息X-Custom-Header。 瀏覽器發現,這是一個非簡單請求,就自動發出一個"預檢"請求,要求服務器確承認以這樣請求。下面是這個"預檢"請求的HTTP頭信息。 OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0... "預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息裏面,關鍵字段是Origin,表示請求來自哪一個源。 除了Origin字段,"預檢"請求的頭信息包括兩個特殊字段。 (1)Access-Control-Request-Method 該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是PUT。 (2)Access-Control-Request-Headers 該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段,上例是X-Custom-Header。 4.2 預檢請求的迴應 服務器收到"預檢"請求之後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段之後,確認容許跨源請求,就能夠作出迴應。 HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain 上面的HTTP迴應中,關鍵的是Access-Control-Allow-Origin字段,表示http://api.bob.com能夠請求數據。該字段也能夠設爲星號,表示贊成任意跨源請求。 Access-Control-Allow-Origin: * 若是瀏覽器否認了"預檢"請求,會返回一個正常的HTTP迴應,可是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不一樣意預檢請求,所以觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲。控制檯會打印出以下的報錯信息。 XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin. 服務器迴應的其餘CORS相關字段以下。 Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000 (1)Access-Control-Allow-Methods 該字段必需,它的值是逗號分隔的一個字符串,代表服務器支持的全部跨域請求的方法。注意,返回的是全部支持的方法,而不單是瀏覽器請求的那個方法。這是爲了不屢次"預檢"請求。 (2)Access-Control-Allow-Headers 若是瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,代表服務器支持的全部頭信息字段,不限於瀏覽器在"預檢"中請求的字段。 (3)Access-Control-Allow-Credentials 該字段與簡單請求時的含義相同。 (4)Access-Control-Max-Age 該字段可選,用來指定本次預檢請求的有效期,單位爲秒。上面結果中,有效期是20天(1728000秒),即容許緩存該條迴應1728000秒(即20天),在此期間,不用發出另外一條預檢請求。 4.3 瀏覽器的正常請求和迴應 一旦服務器經過了"預檢"請求,之後每次瀏覽器正常的CORS請求,就都跟簡單請求同樣,會有一個Origin頭信息字段。服務器的迴應,也都會有一個Access-Control-Allow-Origin頭信息字段。 下面是"預檢"請求以後,瀏覽器的正常CORS請求。 PUT /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com X-Custom-Header: value Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0... 上面頭信息的Origin字段是瀏覽器自動添加的。 下面是服務器正常的迴應。 Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8 上面頭信息中,Access-Control-Allow-Origin字段是每次迴應都一定包含的。 5、與JSONP的比較 CORS與JSONP的使用目的相同,可是比JSONP更強大。 JSONP只支持GET請求,CORS支持全部類型的HTTP請求。JSONP的優點在於支持老式瀏覽器,以及能夠向不支持CORS的網站請求數據。
- DNS緩存 主要就是在瀏覽器本地把對應的 IP 和域名關聯起來,這樣在進行DNS解析的時候就很快。 - MemoryCache 是指存在內存中的緩存。從優先級上來講,它是瀏覽器最早嘗試去命中的一種緩存。從效率上來講,它是響應速度最快的一種緩存。內存緩存是快的,也是「短命」的。它和渲染進程「生死相依」,當進程結束後,也就是 tab 關閉之後,內存裏的數據也將不復存在。 - 瀏覽器緩存 瀏覽器緩存,也稱Http緩存,分爲強緩存和協商緩存。優先級較高的是強緩存,在命中強緩存失敗的狀況下,纔會走協商緩存。 - 強緩存:強緩存是利用 http 頭中的 Expires 和 Cache-Control 兩個字段來控制的。強緩存中,當請求再次發出時,瀏覽器會根據其中的 expires 和 cache-control 判斷目標資源是否「命中」強緩存,若命中則直接從緩存中獲取資源,不會再與服務端發生通訊。 實現強緩存,過去咱們一直用expires。當服務器返回響應時,在 Response Headers 中將過時時間寫入 expires 字段。像這樣 expires: Wed, 12 Sep 2019 06:12:18 GMT 能夠看到,expires 是一個時間戳,接下來若是咱們試圖再次向服務器請求資源,瀏覽器就會先對比本地時間和 expires 的時間戳,若是本地時間小於 expires 設定的過時時間,那麼就直接去緩存中取這個資源。 從這樣的描述中你們也不難猜想,expires 是有問題的,它最大的問題在於對「本地時間」的依賴。若是服務端和客戶端的時間設置可能不一樣,或者我直接手動去把客戶端的時間改掉,那麼 expires 將沒法達到咱們的預期。 考慮到 expires 的侷限性,HTTP1.1 新增了Cache-Control字段來完成 expires 的任務。expires 能作的事情,Cache-Control 都能作;expires 完成不了的事情,Cache-Control 也能作。所以,Cache-Control 能夠視做是 expires 的徹底替代方案。在當下的前端實踐裏,咱們繼續使用 expires 的惟一目的就是向下兼容。 cache-control: max-age=31536000 在 Cache-Control 中,咱們經過max-age來控制資源的有效期。max-age 不是一個時間戳,而是一個時間長度。在本例中,max-age 是 31536000 秒,它意味着該資源在 31536000 秒之內都是有效的,完美地規避了時間戳帶來的潛在問題。 Cache-Control 相對於 expires 更加準確,它的優先級也更高。當 Cache-Control 與 expires 同時出現時,咱們以 Cache-Control 爲準。 - 協商緩存:協商緩存依賴於服務端與瀏覽器之間的通訊。協商緩存機制下,瀏覽器須要向服務器去詢問緩存的相關信息,進而判斷是從新發起請求、下載完整的響應,仍是從本地獲取緩存的資源。若是服務端提示緩存資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種狀況下網絡請求對應的狀態碼是 304。 協商緩存的實現,從 Last-Modified 到 Etag,Last-Modified 是一個時間戳,若是咱們啓用了協商緩存,它會在首次請求時隨着 Response Headers 返回: Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT 隨後咱們每次請求時,會帶上一個叫 If-Modified-Since 的時間戳字段,它的值正是上一次 response 返回給它的 last-modified 值: If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT 服務器接收到這個時間戳後,會比對該時間戳和資源在服務器上的最後修改時間是否一致,從而判斷資源是否發生了變化。若是發生了變化,就會返回一個完整的響應內容,並在 Response Headers 中添加新的 Last-Modified 值;不然,返回如上圖的 304 響應,Response Headers 不會再添加 Last-Modified 字段。 使用 Last-Modified 存在一些弊端,這其中最多見的就是這樣兩個場景: 咱們編輯了文件,但文件的內容沒有改變。服務端並不清楚咱們是否真正改變了文件,它仍然經過最後編輯時間進行判斷。所以這個資源在再次被請求時,會被當作新資源,進而引起一次完整的響應——不應從新請求的時候,也會從新請求。 當咱們修改文件的速度過快時(好比花了 100ms 完成了改動),因爲 If-Modified-Since 只能檢查到以秒爲最小計量單位的時間差,因此它是感知不到這個改動的——該從新請求的時候,反而沒有從新請求了。 這兩個場景其實指向了同一個 bug——服務器並無正確感知文件的變化。爲了解決這樣的問題,Etag 做爲 Last-Modified 的補充出現了。 Etag 是由服務器爲每一個資源生成的惟一的標識字符串,這個標識字符串能夠是基於文件內容編碼的,只要文件內容不一樣,它們對應的 Etag 就是不一樣的,反之亦然。所以 Etag 可以精準地感知文件的變化。 Etag 的生成過程須要服務器額外付出開銷,會影響服務端的性能,這是它的弊端。所以啓用 Etag 須要咱們審時度勢。正如咱們剛剛所提到的——Etag 並不能替代 Last-Modified,它只能做爲 Last-Modified 的補充和強化存在。 Etag 在感知文件變化上比 Last-Modified 更加準確,優先級也更高。當 Etag 和 Last-Modified 同時存在時,以 Etag 爲準。 - Service Worker Cache Service Worker 是一種獨立於主線程以外的 Javascript 線程。它脫離於瀏覽器窗體,所以沒法直接訪問 DOM。這樣獨立的個性使得 Service Worker 的「我的行爲」沒法干擾頁面的性能,這個「幕後工做者」能夠幫咱們實現離線緩存、消息推送和網絡代理等功能。咱們藉助 Service worker 實現的離線緩存就稱爲 Service Worker Cache。 Service Worker 的生命週期包括 install、active、working 三個階段。一旦 Service Worker 被 install,它將始終存在,只會在 active 與 working 之間切換,除非咱們主動終止它。這是它能夠用來實現離線存儲的重要先決條件. - Push Cache Push Cache 是指 HTTP2 在 server push 階段存在的緩存。這塊的知識比較新,應用也還處於萌芽階段,應用範圍有限不表明不重要——HTTP2 是趨勢、是將來。在它還未被推而廣之的此時此刻,我仍但願你們能對 Push Cache 的關鍵特性有所瞭解: - Push Cache 是緩存的最後一道防線。瀏覽器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的狀況下才會去詢問 Push Cache。 - Push Cache 是一種存在於會話階段的緩存,當 session 終止時,緩存也隨之釋放。 - 不一樣的頁面只要共享了同一個 HTTP2 鏈接,那麼它們就能夠共享同一個 Push Cache。
(1)Local Storage (2)Session Storage (3)IndexedDB (4)Web SQL (5)Cookie
- 相同點:都存儲在客戶端 - 不一樣點: - 存儲大小: - cookie數據大小不能超過4k - sessionStorage和localStorage雖然也有存儲大小的限制,但比cookie大的多,能夠達到5M或更大,就是爲了解決cookie存儲空間不足而誕生的 - 有限時間: - localStorage存儲持久數據,瀏覽器關閉後數據不丟失除非主動刪除數據 - sessionStorage數據在當前瀏覽器窗口關閉後自動刪除 - cookie設置的cookie過時時間以前一直有效,即便窗口或瀏覽器關閉 - 數據域服務器之間的交互方式 - cookie的數據會自動的傳遞到服務器,服務器端也能夠寫cookie到客戶端 - sessionStorage和localStorage不會自動把數據發給服務器,僅在本地保存
1、瀏覽器容許每一個域名所包含的cookie數: Microsoft指出InternetExplorer8增長cookie限制爲每一個域名50個,但IE7彷佛也容許每一個域名50個cookie。 Firefox每一個域名cookie限制爲50個。 Opera每一個域名cookie限制爲30個。 Safari/WebKit貌似沒有cookie限制。可是若是cookie不少,則會使header大小超過服務器的處理的限制,會致使錯誤發生。 注:「每一個域名cookie限制爲20個」將再也不正確! 2、當不少的cookie被設置,瀏覽器如何去響應。 除Safari(能夠設置所有cookie,無論數量多少),有兩個方法: 最少最近使用(leastrecentlyused(LRU))的方法:當Cookie已達到限額,自動踢除最老的Cookie,以使給最新的Cookie一些空間。InternetExplorer和Opera使用此方法。 Firefox很獨特:雖然最後的設置的Cookie始終保留,但彷佛隨機決定哪些cookie被保留。彷佛沒有任何計劃(建議:在Firefox中不要超過Cookie限制)。 3、不一樣瀏覽器間cookie總大小也不一樣: Firefox和Safari容許cookie多達4097個字節,包括名(name)、值(value)和等號。 Opera容許cookie多達4096個字節,包括:名(name)、值(value)和等號。 InternetExplorer容許cookie多達4095個字節,包括:名(name)、值(value)和等號。 注:多字節字符計算爲兩個字節。在全部瀏覽器中,任何cookie大小超過限制都被忽略,且永遠不會被設置。
||IE6.0|IE7.0/8.0|Opera|FF|Safari|Chrome|
|cookie個數|每一個域爲20個|每一個域爲50個|每一個域爲30個|每一個域爲50個|沒有個數限制|每一個域爲53個|
|cookie總大小|4096個字節|4096個字節|4096個字節|4096個字節|4096個字節|4096個字節|html
都不能訪問,有同源策略前端
在toutiao.com/a/b的localStorage中有一個a=1 ① toutiao/a/d ② toutiao/d/c ③ dev.toutiao/a/b
- sass或scss - less - stylus
- 定義變量$ - @mixin - @include
CSS Modules 能最大化地結合現有 CSS 生態和 JS 模塊化能力,API 簡潔到幾乎零學習成本。發佈時依舊編譯出單獨的 JS 和 CSS。它並不依賴於 React,只要你使用 Webpack,能夠在 Vue/Angular/jQuery 中使用。 CSS Modules 內部經過 ICSS 來解決樣式導入和導出這兩個問題。分別對應 :import 和 :export 兩個新增的僞類。 :import("path/to/dep.css") { localAlias: keyFromDep; /* ... */ } :export { exportedKey: exportedValue; /* ... */ } 但直接使用這兩個關鍵字編程太麻煩,實際項目中不多會直接使用它們,咱們須要的是用 JS 來管理 CSS 的能力。結合 Webpack 的 css-loader 後,就能夠在 CSS 中定義樣式,在 JS 中導入。 啓用 CSS Modules // webpack.config.js css?modules&localIdentName=[name]__[local]-[hash:base64:5] 加上 modules 即爲啓用,localIdentName 是設置生成樣式的命名規則。 也能夠這樣設置: test: /\.less$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]-[hash:base64:5]', }, }, ], }, 樣式文件Button.css: .normal { /* normal 相關的全部樣式 */ } .disabled { /* disabled 相關的全部樣式 */ } /* components/Button.js */ import styles from './Button.css'; console.log(styles); buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>` 生成的 HTML 是 <button class="button--normal-abc53">Submit</button> 注意到 button--normal-abc53 是 CSS Modules 按照 localIdentName 自動生成的 class 名。其中的 abc53 是按照給定算法生成的序列碼。通過這樣混淆處理後,class 名基本就是惟一的,大大下降了項目中樣式覆蓋的概率。同時在生產環境下修改規則,生成更短的class名,能夠提升CSS的壓縮率。 上例中 console 打印的結果是: Object { normal: 'button--normal-abc53', disabled: 'button--disabled-def884', } CSS Modules 對 CSS 中的 class 名都作了處理,使用對象來保存原 class 和混淆後 class 的對應關係。 經過這些簡單的處理,CSS Modules 實現瞭如下幾點: 全部樣式都是 local 的,解決了命名衝突和全局污染問題 class 名生成規則配置靈活,能夠此來壓縮 class 名 只需引用組件的 JS 就能搞定組件全部的 JS 和 CSS 依然是 CSS,幾乎 0 學習成本 樣式默認局部 使用了 CSS Modules 後,就至關於給每一個 class 名外加加了一個 :local,以此來實現樣式的局部化,若是你想切換到全局模式,使用對應的 :global。 .normal { color: green; } /* 以上與下面等價 */ :local(.normal) { color: green; } /* 定義全局樣式 */ :global(.btn) { color: red; } /* 定義多個全局樣式 */ :global { .link { color: green; } .box { color: yellow; } } Compose 來組合樣式 對於樣式複用,CSS Modules 只提供了惟一的方式來處理:composes 組合 /* components/Button.css */ .base { /* 全部通用的樣式 */ } .normal { composes: base; /* normal 其它樣式 */ } .disabled { composes: base; /* disabled 其它樣式 */ } import styles from './Button.css'; buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>` 生成的 HTML 變爲 <button class="button--base-daf62 button--normal-abc53">Submit</button> 因爲在 .normal 中 composes 了 .base,編譯後會 normal 會變成兩個 class。 composes 還能夠組合外部文件中的樣式。 /* settings.css */ .primary-color { color: #f40; } /* components/Button.css */ .base { /* 全部通用的樣式 */ } .primary { composes: base; composes: primary-color from './settings.css'; /* primary 其它樣式 */ } 對於大多數項目,有了 composes 後已經再也不須要 Sass/Less/PostCSS。但若是你想用的話,因爲 composes 不是標準的 CSS 語法,編譯時會報錯。就只能使用預處理器本身的語法來作樣式複用了。 class 命名技巧 CSS Modules 的命名規範是從 BEM 擴展而來。 clipboard.png BEM 把樣式名分爲 3 個級別,分別是: Block:對應模塊名,如 Dialog Element:對應模塊中的節點名 Confirm Button Modifier:對應節點相關的狀態,如 disabled、highlight 綜上,BEM 最終獲得的 class 名爲 dialog__confirm-button--highlight。使用雙符號 __ 和 -- 是爲了和區塊內單詞間的分隔符區分開來。雖然看起來有點奇怪,但 BEM 被很是多的大型項目和團隊採用。咱們實踐下來也很承認這種命名方法。 CSS Modules 中 CSS 文件名剛好對應 Block 名,只須要再考慮 Element 和 Modifier。BEM 對應到 CSS Modules 的作法是: /* .dialog.css */ .ConfirmButton--disabled { } 你也能夠不遵循完整的命名規範,使用 camelCase 的寫法把 Block 和 Modifier 放到一塊兒: /* .dialog.css */ .disabledConfirmButton { } 模塊化命名實踐:MBC 【僅供參考參考】 M:module 模塊(組件)名 B:block 元素塊的功能說明 C:custom 自定義內容 如何實現CSS,JS變量共享 注:CSS Modules 中沒有變量的概念,這裏的 CSS 變量指的是 Sass 中的變量。 上面提到的 :export 關鍵字能夠把 CSS 中的 變量輸出到 JS 中。下面演示如何在 JS 中讀取 Sass 變量: /* config.scss */ $primary-color: #f40; :export { primaryColor: $primary-color; } /* app.js */ import style from 'config.scss'; // 會輸出 #F40 console.log(style.primaryColor); CSS Modules 使用技巧 CSS Modules 是對現有的 CSS 作減法。爲了追求簡單可控,做者建議遵循以下原則: 不使用選擇器,只使用 class 名來定義樣式 不層疊多個 class,只使用一個 class 把全部樣式定義好 全部樣式經過 composes 組合來實現複用 不嵌套 上面兩條原則至關於削弱了樣式中最靈活的部分,初使用者很難接受。第一條實踐起來難度不大,但第二條若是模塊狀態過多時,class 數量將成倍上升。 必定要知道,上面之因此稱爲建議,是由於 CSS Modules 並不強制你必定要這麼作。聽起來有些矛盾,因爲多數 CSS 項目存在深厚的歷史遺留問題,過多的限制就意味着增長遷移成本和與外部合做的成本。初期使用中確定須要一些折衷。幸運的是,CSS Modules 這點作的很好: 若是我對一個元素使用多個 class 呢? 沒問題,樣式照樣生效。 如何我在一個 style 文件中使用同名 class 呢? 沒問題,這些同名 class 編譯後雖然多是隨機碼,但還是同名的。 若是我在 style 文件中使用僞類,標籤選擇器等呢? 沒問題,全部這些選擇器將不被轉換,原封不動的出如今編譯後的 css 中。也就是說 CSS Modules 只會轉換 class 名和 id 選擇器名相關的樣式。 但注意,上面 3 個「若是」儘可能不要發生。 CSS Modules 結合 React 實踐 首先,在 CSS loader中開啓CSS Module: { loader: 'css-loader', options: { modules: true, localIdentName: '[local]', }, }, 在 className 處直接使用 css 中 class 名便可。 /* dialog.css */ .root {} .confirm {} .disabledConfirm {} import classNames from 'classnames'; import styles from './dialog.css'; export default class Dialog extends React.Component { render() { const cx = classNames({ [styles.confirm]: !this.state.disabled, [styles.disabledConfirm]: this.state.disabled }); return <div className={styles.root}> <a className={cx}>Confirm</a> ... </div> } } 注意,通常把組件最外層節點對應的 class 名稱爲 root。這裏使用了 classnames 庫來操做 class 名。 若是你不想頻繁的輸入 styles.xx,能夠試一下 react-css-modules,它經過高階函數的形式來避免重複輸入 styles.xx。 React 中使用CSS Module,能夠設置 5.幾點注意 1.多個class的狀況 // 能夠使用字符串拼接 className = {style.oneclass+' '+style.twoclass} //能夠使用es6的字符串模板 className = {`${style['calculator']} ${style['calculator']}`} 2.若是class使用的是連字符能夠使用數組方式style['box-text'] 3.一個class是父組件傳下來的 若是一個class是父組件傳下來的,在父組件已經使用style轉變過了,在子組件中就不須要再style轉變一次,例子以下: //父組件render中 <CalculatorKey className={style['key-0']} onPress={() => this.inputDigit(0)}>0</CalculatorKey> //CalculatorKey 組件render中 const { onPress, className, ...props } = this.props; return ( <PointTarget onPoint={onPress}> <button className={`${style['calculator-key']} ${className}`} {...props}/> </PointTarget> ) 子組件CalculatorKey接收到的className已經在父組件中style轉變過了 4.顯示undefined 若是一個class你沒有在樣式文件中定義,那麼最後顯示的就是undefined,而不是一個style事後的沒有任何樣式的class, 這點很奇怪。 6.從Vue 來看CSS 管理方案的發展 /* HTML */ <template> <h1 @click="clickHandler">{{ msg }}</h1> </template> /* script */ <script> module.exports = { data: function() { return { msg: 'Hello, world!' } }, methods:{ clickHandler(){ alert('hi'); } } } </script> /* scoped CSS */ <style scoped> h1 { color: red; font-size: 46px; } </style> 在Vue 元件檔,透過上面這樣的方式提供了一個template (HTML)、script 以及帶有scoped 的style 樣式,也仍然能夠保有過去HTML、CSS 與JS 分離的開發體驗。但本質上還是all-in-JS 的變種語法糖。 值得一提的是,當style標籤加上scoped屬性,那麼在Vue元件檔通過編譯後,會自動在元件上打上一個隨機的屬性 data-v-hash,再透過CSS Attribute Selector的特性來作到CSS scope的切分,使得即使是在不一樣元件檔裏的h1也能有CSS樣式不互相干擾的效果。固然開發起來,比起JSX、或是inline-style等的方式,這種折衷的做法更漂亮。
將一些經常使用的 CSS 屬性封裝成函數,用起來很是方便,充分體現使用 JavaScript 語言寫 CSS 的優點。 我以爲這個庫很值得推薦,這篇文章的主要目的,就是想從這個庫來看怎麼使用 CSS in JS。 首先,加載 polished.js。 const polished = require('polished'); 若是是瀏覽器,插入下面的腳本。 <script src="https://unpkg.com/polished@1.0.0/dist/polished.min.js"> </script> polished.js目前有50多個方法,好比clearfix方法用來清理浮動。 const styles = { ...polished.clearFix(), }; 上面代碼中,clearFix就是一個普通的 JavaScript 函數,返回一個對象。 polished.clearFix() // { // &::after: { // clear: "both", // content: "", // display: "table" // } // } "展開運算符"(...)將clearFix返回的對象展開,便於與其餘 CSS 屬性混合。而後,將樣式對象賦給 React 組件的style屬性,這個組件就能清理浮動了。 ReactDOM.render( <h1 style={style}>Hello, React!</h1>, document.getElementById('example') ); 從這個例子,你們應該可以體會polished.js的用法。 下面再看幾個頗有用的函數。 ellipsis將超過指定長度的文本,使用省略號替代(查看完整代碼)。 const styles = { ...polished.ellipsis('200px') } // 返回值 // { // 'display': 'inline-block', // 'max-width': '250px', // 'overflow': 'hidden', // 'text-overflow': 'ellipsis', // 'white-space': 'nowrap', // 'word-wrap': 'normal' // } hideText用於隱藏文本,顯示圖片。 const styles = { 'background-image': 'url(logo.png)', ...polished.hideText(), }; // 返回值 // { 'background-image': 'url(logo.png)', 'text-indent': '101%', 'overflow': 'hidden', 'white-space': 'nowrap', } hiDPI指定高分屏樣式。 const styles = { [polished.hiDPI(1.5)]: { width: '200px', } }; // 返回值 //'@media only screen and (-webkit-min-device-pixel-ratio: 1.5), // only screen and (min--moz-device-pixel-ratio: 1.5), // only screen and (-o-min-device-pixel-ratio: 1.5/1), // only screen and (min-resolution: 144dpi), // only screen and (min-resolution: 1.5dppx)': { // 'width': '200px', //} retinaImage爲高分屏和低分屏設置不一樣的背景圖。 const styles = { ...polished.retinaImage('my-img') }; // 返回值 // backgroundImage: 'url(my-img.png)', // '@media only screen and (-webkit-min-device-pixel-ratio: 1.3), // only screen and (min--moz-device-pixel-ratio: 1.3), // only screen and (-o-min-device-pixel-ratio: 1.3/1), // only screen and (min-resolution: 144dpi), // only screen and (min-resolution: 1.5dppx)': { // backgroundImage: 'url(my-img_2x.png)', // } polished.js提供的其餘方法以下,詳細用法請參考文檔。 normalize():樣式表初始化 placeholder():對 placeholder 僞元素設置樣式 selection():對 selection 僞元素設置樣式 darken():調節顏色深淺 lighten():調節顏色深淺 desaturate():下降顏色的飽和度 saturate():增長顏色的飽和度 opacify():調節透明度 complement():返回互補色 grayscale():將一個顏色轉爲灰度 rgb():指定紅、綠、藍三個值,返回一個顏色 rgba():指定紅、綠、藍和透明度四個值,返回一個顏色 hsl():指定色調、飽和度和亮度三個值,返回一個顏色 hsla():指定色調、飽和度、亮度和透明度三個值,返回一個顏色 mix():混合兩種顏色 em():將像素轉爲 em rem():將像素轉爲 rem 目前,polished.js只是1.0版,之後應該會有愈來愈多的方法。 polished.js還有一個特點:全部函數默認都是柯里化的,所以能夠進行函數組合運算,定製出本身想要的函數。 import { compose } from 'ramda'; import { lighten, desaturate } from 'polished'; const tone = compose(lighten(10), desaturate(10))
通過定位的元素,其position屬性值必然是relative、absolute、fixed或sticky。 - static:默認定位屬性值。該關鍵字指定元素使用正常的佈局行爲,即元素在文檔常規流中當前的佈局位置。此時 top, right, bottom, left 和 z-index 屬性無效。 - relative:該關鍵字下,元素先放置在未添加定位時的位置,再在不改變頁面佈局的前提下調整元素位置(所以會在此元素未添加定位時所在位置留下空白)。 - absolute:不爲元素預留空間,經過指定元素相對於最近的非 static 定位祖先元素的偏移,來肯定元素位置。絕對定位的元素能夠設置外邊距(margins),且不會與其餘邊距合併。 - fixed:不爲元素預留空間,而是經過指定元素相對於屏幕視口(viewport)的位置來指定元素位置。元素的位置在屏幕滾動時不會改變。打印時,元素會出如今的每頁的固定位置。fixed 屬性會建立新的層疊上下文。當元素祖先的 transform 屬性非 none 時,容器由視口改成該祖先。 - sticky:盒位置根據正常流計算(這稱爲正常流動中的位置),而後相對於該元素在流中的 flow root(BFC)和 containing block(最近的塊級祖先元素)定位。在全部狀況下(即使被定位元素爲 table 時),該元素定位均不對後續元素形成影響。當元素 B 被粘性定位時,後續元素的位置仍按照 B 未定位時的位置來肯定。position: sticky 對 table 元素的效果與 position: relative 相同。
- position:relative - 無:div2位置不變 - 'top=0':div2位置不變 - 'top=20px':距離div1爲20px - position:absolute - 無:div2不顯示 - 'top=0':div2不顯示 - 'top=20px':div2不顯示