大厂面试-字节跳动

字节跳动

框架

你用过dva吗?请简述一下dva的实现原理

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;
}

使用过redux吗?请简述一下redux的实现原理

// 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)));
    }

看你主要技术栈是vue,请简述一下vuex和redux在实现上有哪些区别?

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中由哪些生命周期?

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(销毁后) 在实例销毁以后调用。调用后,全部的事件监听器会被移除,全部的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

vue中如何获取更新后的dom节点?

- $refs
- this.$nextTick

你都用过vue中的哪些指令?

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 只会阻止对元素自身的点击。

v-if和v-for谁的优先级高?如何同时使用?不要多套一层

放在计算属性遍历
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}

v-for中key的做用

> 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改变时会引发新组件的建立和原有组件的删除,此时组件的生命周期钩子就会被触发。

vue全家桶你都用过哪些?

- vuex
- vue-Router
- axios
- element-ui
....

你提到了Vue Router,那么它有几种模式?是如何实现的?(提到了abstract模式使用场景)你提到了hash模式和history模式,那么如何获取路由跳转后的参数?如何实现的?

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,路由会自动强制进入这个模式.

采用history模式,在node服务器端须要如何配置?

  1. 新建server-proxy.js文件(dev和pro公用的proxy转发文件)
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;
  1. 新建 server-after-package.js ,打包后在当前目录就能够启动history模式,并作了proxy的转发。
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')
})

你都用过哪些es6语法?

- 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)

你提到了箭头函数的this,请指出下列选项中的this分别表明谁?

都是objcss

const obj={
        a:()=>console.log(this)
    }

    ① obj.a()
    ② const test=obj.a;
       test();
    ③  const test2={}
        obj.a.call(test2)

你提到了vue3中用到了proxy,简述一下你对proxy的理解,vue3和vue2有哪些不一样?

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 优化,提供了更多的内置功能。

原生js

你都知道哪些跨域方式?

- 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,就能够实现父页面调用子页面的函数

你提到了jsonp,请简述一下jsonp原理

一、一个众所周知的问题,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提供公开服务。

你提到了cors,请简述一下它是如何实现跨域的?

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

sessionStorage和localStorage的区别是什么?请详述

- 相同点:都存储在客户端
- 不一样点:
    - 存储大小:
        - cookie数据大小不能超过4k
        - sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大的多,能够达到5M或更大,就是为了解决cookie存储空间不足而诞生的
    - 有限时间:
        - localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
        - sessionStorage数据在当前浏览器窗口关闭后自动删除
        - cookie设置的cookie过时时间以前一直有效,即便窗口或浏览器关闭
    - 数据域服务器之间的交互方式
        - cookie的数据会自动的传递到服务器,服务器端也能够写cookie到客户端
        - sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存

你提到了cookie,请简述一下cookie的原理。全部cookie大小限制都是5kb吗?哪些不是?

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

如下状况可否获取到localStorage?

都不能访问,有同源策略前端

在toutiao.com/a/b的localStorage中有一个a=1
    ① toutiao/a/d 
    ② toutiao/d/c
    ③ dev.toutiao/a/b

css

你都用过哪些css预处理器?

- sass或scss
- less
- stylus

你提到了sass,请简述一下sass的使用方法

- 定义变量$
- @mixin
- @include

你知道css module吗?请详述

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 in js吗?请详述

将一些经常使用的 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有哪些值?它们的区别是什么?

通过定位的元素,其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 相同。

请看下面的页面,当div2的position分别为relative,relative且top=0,relative且top=20px时div2的位置,上边的position所有换成absolute又是什么效果?

- position:relative
    - 无:div2位置不变
    - 'top=0':div2位置不变
    - 'top=20px':距离div1为20px

- position:absolute
    - 无:div2不显示
    - 'top=0':div2不显示
    - 'top=20px':div2不显示
相关文章
相关标签/搜索