• 高性能和可扩展的React-Redux


    注意:文章很长,只想了解逻辑而不深入的,可以直接跳到总结部分

    初识

    首先,从它暴露对外的API开始

    
    ReactReduxContext
    /*
    提供了 React.createContext(null)
    */
    
    Provider  
    /*
    一个储存数据的组件,渲染了ContextProvider,内部调用redux中store.subscribe
    订阅数据,每当redux中的数据变动,比较新值与旧值,判断是否重新渲染
    */ 
    
    connect
    /* 
    一个高阶组件,第一阶传入对数据处理方法,第二阶传入要渲染的组件
    内部处理了:
    1. 对参数的检查
    2. 对传入的数据处理方法进行处理
    (没传怎么处理,传了提供什么参数,传的类型不同怎么处理,结果如何比较等等)
    3. 静态方法转移
    4. 对渲染组件的传递(传递给connectAdvanced)
    */
    
    connectAdvanced
    /*
    保存每一次执行的数据,执行connect定义的方案和逻辑,新旧数据对比(全等对比),渲染组件
    这里作为公开API,如果我们去使用,那么connect里面的逻辑就需要我们自定义了。
    */
    

    现在对它的大概工作范围有了解后,我们可以开始沿着执行顺序分析。


    抽丝

    Provider

    我们使用时,当写完了redux的reducer, action, bindActionCreators, combineReducers, createStore这一系列内容后,
    我们得到了一个store

    会先使用<Provider store={store}包裹住根组件。

    这时,Provider组件开始工作

    
    componentDidMount() {
      this._isMounted = true
      this.subscribe()
    }
    

    第一次加载,需要执行subscribe

    subscribe是什么呢,就是对reduxstore执行subscribe一个自定义函数,
    这样,每当数据变动,这个函数便会执行

    
    subscribe() {
      const { store } = this.props
      // redux 的 store 订阅
      // 订阅后,每当state改变 则自动执行这个函数
      this.unsubscribe = store.subscribe(() =&gt; {
        // store.getState() 获取最新的 state
        const newStoreState = store.getState()
        // 组件未加载,取消
        if (!this._isMounted) {
          return
        }
        // 比较state是否相等,全等的不更新
        this.setState(providerState =&gt; {
          if (providerState.storeState === newStoreState) {
            return null
          }
          return { storeState: newStoreState }
        })
      })
      /* ... */
    }
    

    看到吗,这个自定义函数非常简单,每次收到数据,进行全等比较,不等则更新数据。

    这个组件的另2个生命周期函数:

    
    componentWillUnmount() {
      if (this.unsubscribe) this.unsubscribe()
      this._isMounted = false
    }
    
    componentDidUpdate(prevProps) {
      // 比较store是否相等,如果相等则跳过
      if (this.props.store !== prevProps.store) {
        // 取消订阅之前的,再订阅现在的(因为数据(store)不同了)
        if (this.unsubscribe) this.unsubscribe()
        this.subscribe()
      }
    }
    

    这2段的意思就是,每当数据变了,就取消上一次数据的订阅,在订阅本次的数据,
    当要销毁组件,取消订阅。

    一段题外话(可跳过):

    这个逻辑用HooksuseEffect简直完美匹配!

    
    useEffect(()=&gt;{
      subscribe()
      return ()=&gt;{
        unSubscribe()
      }
    },props.data)
    

    这段的意思就是,当props.data发生改变,执行unSubscribe(),再执行subscribe()

    逻辑完全一致有没有!

    最后的render

    这里Context就是React.createContext(null)

    
    &lt;Context.Provider value={this.state}&gt;
      {this.props.children}
    &lt;/Context.Provider&gt;
    

    到这里我称为react-redux的第一阶段。

    一个小总结,第一阶段就做了1件事:

    定义了Provider组件,内部订阅了store


    connect

    到主菜了,先看它的export

    export default createConnect()

    一看,我们应该有个猜测,这货createConnect是个高阶函数。

    看看它的参数吧。

    
    export function createConnect({
      connectHOC = connectAdvanced,
      mapStateToPropsFactories = defaultMapStateToPropsFactories,
      mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
      mergePropsFactories = defaultMergePropsFactories,
      selectorFactory = defaultSelectorFactory
    } = {}) {
      /* ... */
    }
    

    题外话:一个编写默认对象内部含有默认值的方法

    
    function a({x=1,y=2}={}){}
    
    a()      // x:1,y:2
    a({})    // x:1,y:2
    a({x:2,z:5}) //x:2,y:2
    

    这里先说明一下它的参数,后面读起来会很顺。

    
    connectHOC: 一个重要组件,用于执行已确定的逻辑,渲染最终组件,后面会详细说。
    mapStateToPropsFactories: 对 mapStateToProps 这个传入的参数的类型选择一个合适的方法。
    mapDispatchToPropsFactories: 对 mapDispatchToProps 这个传入的参数的类型选择一个合适的方法。
    mergePropsFactories: 对 mergeProps 这个传入的参数的类型选择一个合适的方法。 
    selectorFactory: 以上3个只是简单的返回另一个合适的处理方法,它则执行这些处理方法,并且对结果定义了如何比较的逻辑。
    

    可能有点绕,但react-redux就是这么一个个高阶函数组成的,selectorFactory后面会详细说。

    首先我们再次确定这3个名字很长,实际很简单的函数(源码这里不放了)

    mapStateToPropsFactories

    mapDispatchToPropsFactories

    mergePropsFactories

    它们只是判断了参数是否存在,是什么类型,并且返回一个合适的处理方法,它们并没有任何处理逻辑。

    • 举个例子:

      const MyComponent=connect((state)=>state.articles})

      这里我只定义了mapStateToProps,并且是个function,那么mapStateToPropsFactories就会返回一个
      处理function的方法。

      我没有定义mapDispatchToProps,那么mapDispatchToPropsFactories检测不到参数,
      则会提供一个默认值dispatch => ({ dispatch }),返回一个处理非function(object)的方法。

    那么处理逻辑是谁定义呢?

    wrapMapToProps

    wrapMapToProps.js这个文件内部做了以下事情:

    1. 定义了一个处理object的方法(简单的返回即可,因为最终目的就是要object)。
    2. 定义了一个处理函数高阶函数(执行2次)的方法,这个方法比上面的复杂在于它需要检测参数是否订阅了ownProps

    检测方法很简单,就是检查参数的length(这里dependsOnOwnProps是上一次检查的结果,如果存在则不需要再次检查)

    
    export function getDependsOnOwnProps(mapToProps) {
      return mapToProps.dependsOnOwnProps !== null &amp;&amp;
        mapToProps.dependsOnOwnProps !== undefined
        ? Boolean(mapToProps.dependsOnOwnProps)
        : mapToProps.length !== 1
    }
    

    回到connect,继续往下看

    
    export function createConnect({
      /* 上面所讲的参数 */
    } = {}) {
      return function connect(
        mapStateToProps,
        mapDispatchToProps,
        mergeProps,
        {
          pure = true,
          areStatesEqual = strictEqual,
          areOwnPropsEqual = shallowEqual,
          areStatePropsEqual = shallowEqual,
          areMergedPropsEqual = shallowEqual,
          ...extraOptions
        } = {}
      ) {
      /* ... */
      }
    }
    

    已经到了我们传递参数的地方,前3个参数意思就不解释了,最后的参数options

    
    areStatesEqual = strictEqual,           // ===比较
    areOwnPropsEqual = shallowEqual,        // 浅比较
    areStatePropsEqual = shallowEqual,      // 浅比较
    areMergedPropsEqual = shallowEqual,     // 浅比较
    

    它们用在selectorFactory这个比较数据结果的方法内部。

    继续往下看

    
    export function createConnect({
      /* 上面已讲 */
    } = {}) {
      return function connect(
        /* 上面已讲 */
      ) {
        const initMapStateToProps = match(
          mapStateToProps,
          mapStateToPropsFactories,
          'mapStateToProps'
        )
        const initMapDispatchToProps = match(
          mapDispatchToProps,
          mapDispatchToPropsFactories,
          'mapDispatchToProps'
        )
        const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
    

    这里定义了3个变量(函数),match的作用是什么?

    mapStateToProps举例来说,

    因为上面也说了,mapStateToPropsFactories里面有多个方法,需要找到一个适合mapStateToProps的,
    match就是干这事了。

    match方法内部遍历mapStateToPropsFactories所有的处理方法,任何一个方法能够匹配参数mapStateToProps,便被match捕获返回,
    如果一个都找不到则报错提示参数配置错误。

    现在这3个变量定义明确了,都是对应的参数的合适的处理方法。

    至此,我们已经完成了第二阶段,

    做个小总结,第二阶段做了哪些事:

    1. connect接收了对参数处理方案(3个...Factories)。
    2. connect接收了参数的结果比较方案(selectFactory)
    3. connect接收了参数(mapStateToProps,mapDispatchToProps,mergeProps,options)。
    4. 定义了比较方案(4个are...Equal,其实就是全等比较浅比较)。

    前2个阶段都是定义阶段,接下来需要我们传入自定义组件,也就是最后一个阶段

    connect(...)(Component)


    接着看connect源码

    
    export function createConnect({
      /* 上面已讲 */
    } = {}) {
      return function connect(
        /* 上面已讲 */
      ) {
        /* 上面已讲 */
        return connectHOC(selectorFactory, {
          // 方法名称,用在错误提示信息
          methodName: 'connect',
          // 最终渲染的组件名称
          getDisplayName: name =&gt; `Connect(${name})`,
          shouldHandleStateChanges: Boolean(mapStateToProps),
          // 以下是传递给 selectFactory
          initMapStateToProps,
          initMapDispatchToProps,
          initMergeProps,
          pure,
          areStatesEqual,
          areOwnPropsEqual,
          areStatePropsEqual,
          areMergedPropsEqual,
    
          // any extra options args can override defaults of connect or connectAdvanced
          ...extraOptions
        })
      }
    }
    

    这里执行了connectHOC(),传递了上面已经讲过的参数,而connectHOC = connectAdvanced

    因此我们进入最后一个对外APIconnectAdvanced

    connectAdvanced

    connectAdvanced函数,之前也提过,就是一个执行、组件渲染和组件更新的地方。

    它里面没有什么新概念,都是将我们上面讲到的参数进行调用,最后根据结果进行渲染新组件。

    还是从源码开始

    
    export default function connectAdvanced(
      selectorFactory,
      {
        // 执行后作用于connect这个HOC组件名称
        getDisplayName = name =&gt; `ConnectAdvanced(${name})`,
        // 用于错误提示
        methodName = 'connectAdvanced',
        // 有REMOVED标志,这里不关注
        renderCountProp = undefined,
        // 确定connect这个HOC是否订阅state变动,好像已经没有用到了
        shouldHandleStateChanges = true,
        // 有REMOVED标志,这里不关注
        storeKey = 'store',
        // 有REMOVED标志,这里不关注
        withRef = false,
        // 是否通过 forwardRef 暴露出传入的Component的DOM
        forwardRef = false,
        // React的createContext
        context = ReactReduxContext,
    
        // 其余的(比较方法,参数处理方法等)将会传递给上面的 selectFactory
        ...connectOptions
      } = {}
    ) {
      /* ... */
    }
    

    参数也没什么特别的,有一个forwardRef作用就是能获取到我们传入的Component的DOM。
    这里也不深入。

    接着看

    
    export default function connectAdvanced(
      /* 上面已讲 */
    ) {
      /* ...对参数的一些验证和提示哪些参数已经作废... */
      
      // 定义Context
      const Context = context
    
      return function wrapWithConnect(WrappedComponent) {
        /* ...检查 WrappedComponent 是否符合要求... */
       
        /* ...获取传入的WrappedComponent的名称... */
       
        /* ...通过WrappedComponent的名称计算出当前HOC的名称... */
    
        /* ...获取一些上面的参数(没有新的参数,都是之前见过的)... */
    
        // Component就是React.Component
        let OuterBaseComponent = Component
        let FinalWrappedComponent = WrappedComponent
    
        // 是否纯组件
        if (pure) {
          OuterBaseComponent = PureComponent
        }
    
        /* 定义 makeDerivedPropsSelector 方法,作用后面讲 */
    
        /* 定义 makeChildElementSelector 方法,作用后面讲 */
    
        /* 定义 Connect 组件,作用后面讲 */
    
        Connect.WrappedComponent = WrappedComponent
        Connect.displayName = displayName
    
        /* ...如果是forWardRef 为true的情况,此处不深入... */
        
        // 静态方法转换
        return hoistStatics(Connect, WrappedComponent)
      }
    }
    

    这一段特别长,因此我将不太重要的直接用注释说明了它们在做什么,具体代码就不放了(不重要)。

    并且定义了3个新东西,makeDerivedPropsSelectormakeChildElementSelector,Connect

    先看最后一句hoistStatics就是hoist-non-react-statics,它的作用是将组件WrappedComponent的所有非React
    静态方法传递到Connect内部。

    那么最终它还是返回了一个Connect组件。

    Connect组件

    这个组件已经是我们写了完整connect(...)(Component)的返回值了,所以能确定,只要调用<Connect />,就能渲染出一个新的组件出来。

    因此它的功能就是确定是否重复更新组件和确定到底更新什么?

    看一个组件,从constructor看起

    
    class Connect extends OuterBaseComponent {
      constructor(props) {
        super(props)
       
        /* ...提示一些无用的参数...*/
        
        this.selectDerivedProps = makeDerivedPropsSelector()
        this.selectChildElement = makeChildElementSelector()
        this.renderWrappedComponent = this.renderWrappedComponent.bind(this)
      }
      /* ... */
    }
    

    绑定了一个方法,看名字是render的意思,先不管它。

    执行了2个函数。

    Connect组件还没完,这里先放着,我们先看makeDerivedPropsSelectormakeChildElementSelector

    makeDerivedPropsSelector

    
    function makeDerivedPropsSelector() {
      // 闭包储存上一次的执行结果
      let lastProps
      let lastState
      let lastDerivedProps
      let lastStore
      let sourceSelector
    
      return function selectDerivedProps(state, props, store) {
        // props和state都和之前相等 直接返回上一次的结果
        if (pure &amp;&amp; lastProps === props &amp;&amp; lastState === state) {
          return lastDerivedProps
        }
    
        // 当前store和lastStore不等,更新lastStore
        if (store !== lastStore) {
          lastStore = store
          
          // 终于调用 selectorFactory 了
          sourceSelector = selectorFactory(
            store.dispatch,
            selectorFactoryOptions
          )
        }
    
        // 更新数据
        lastProps = props
        lastState = state
    
        // 返回的就是最终的包含所有相应的 state 和 props 的结果
        const nextProps = sourceSelector(state, props)
    
        // 最终的比较
        if (lastDerivedProps === nextProps) {
          return lastDerivedProps
        }
        lastDerivedProps = nextProps
        return lastDerivedProps
      }
    }
    

    大概的说,makeDerivedPropsSelector的执行,先判断了当前传入的props(组件的props)state(redux传入的state)
    跟以前的是否全等,如果全等就不需要更新了;

    如果不等,则调用了高阶函数selectFactory,并且获得最终数据,最后再判断最终数据和之前的最终数据是否全等。

    为什么第一次判断了,还要判断第二次,而且都是===判断?

    因为第一次获取的stateredux传入的,是整个APP的所有数据,它们不等说明有组件更新了,但不确定是否是当前组件;

    第二次比较的是当前组件的最新数据和以前数据对比。

    现在,我们知道selectFactory的作用是获取当前组件的的最新数据,深入源码看看。

    selectFactory

    
    export default function finalPropsSelectorFactory(
      // redux store的store.dispatch
      dispatch,
      // 3种已经确定了的处理方法
      { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
    ) {
      // 返回一个针对用户传入的类型的解析函数
      // 例如 mapStateToProps 如果是function,那么就返回proxy,proxy可以判断是否需要ownProps,并且对高阶函数的 mapStateToProps 进行2次处理,
      // 最终确保返回一个plainObject,否则报错
      const mapStateToProps = initMapStateToProps(dispatch, options)
      const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
      const mergeProps = initMergeProps(dispatch, options)
    
      if (process.env.NODE_ENV !== 'production') {
        verifySubselectors(
          mapStateToProps,
          mapDispatchToProps,
          mergeProps,
          options.displayName
        )
      }
    
      const selectorFactory = options.pure
        ? pureFinalPropsSelectorFactory
        : impureFinalPropsSelectorFactory
    
      // 默认pure问题true,因此执行 pureFinalPropsSelectorFactory(...)
      return selectorFactory(
        mapStateToProps,
        mapDispatchToProps,
        mergeProps,
        dispatch,
        options
      )
    }
    

    参数就不说了,看注释。

    以下3个,到底返回了什么,源码在wrapMapToProps.js上面也说过这个文件内部做了什么事情。

    
    const mapStateToProps = initMapStateToProps(dispatch, options)
    const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
    const mergeProps = initMergeProps(dispatch, options)
    

    这3个调用返回的一个函数,名字叫proxy,这个proxy一旦调用,
    就能返回经过mapStateToProps, mapDispatchToProps, mergeProps这3个参数处理过后的数据(plainObject)。

    接下来:

    
    const selectorFactory = options.pure
        ? pureFinalPropsSelectorFactory
        : impureFinalPropsSelectorFactory
    
      // 默认pure问题true,因此执行 pureFinalPropsSelectorFactory(...)
      return selectorFactory(
        mapStateToProps,
        mapDispatchToProps,
        mergeProps,
        dispatch,
        options
      )
    

    返回了selectorFactory的调用值,也就是pureFinalPropsSelectorFactory(pure默认为true)。

    pureFinalPropsSelectorFactory,它的代码不少,但逻辑很明了,大方向就是对比数据。

    这里关键的如何比较不列代码,只用注释讲明白它的逻辑。

    
    export function pureFinalPropsSelectorFactory(
      // 接受3个proxy方法
      mapStateToProps,
      mapDispatchToProps,
      mergeProps,
      dispatch,
      // 接受3个比较方法
      { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
    ) {
      
      /* ...定义变量保存之前的数据(闭包)... */
    
      function handleFirstCall(firstState, firstOwnProps) {
        /* ...定义第一次执行数据比较的方法,也就是简单的赋值给上面定义的闭包变量... */
      }
    
      function handleNewPropsAndNewState() {
        /* 当state和props都有变动时的处理方法 */
      }
    
      function handleNewProps() {
        /* 当state无变动,props有变动时的处理方法 */
      }
    
      function handleNewState() {
        /* 当state有变动,props无变动时的处理方法 */
      }
    
      // 后续数据比较的方法
      function handleSubsequentCalls(nextState, nextOwnProps) {
        // 浅比较
        const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
        // 全等比较
        const stateChanged = !areStatesEqual(nextState, state)
        // 更新数据
        state = nextState
        ownProps = nextOwnProps
        // 当发生不相等的3种情况(关键)
        if (propsChanged &amp;&amp; stateChanged) return handleNewPropsAndNewState()
        if (propsChanged) return handleNewProps()
        if (stateChanged) return handleNewState()
        // 比较都相等,直接返回旧值
        return mergedProps
      }
      return function pureFinalPropsSelector(nextState, nextOwnProps) {
        return hasRunAtLeastOnce
          ? handleSubsequentCalls(nextState, nextOwnProps)
          : handleFirstCall(nextState, nextOwnProps)
      }
    }
    

    上面的闭包变量储存了上一次的数据,关键点就是当和这一次的数据比较后,如果处理更新。

    react-redux将它分为3种情况

    • stateprops都相等。
    • state相等,props不等。
    • state不等,props相等。

    • 第一种:stateprops都相等

      • mapStateToProps(proxy):

        不管是否订阅ownProps,执行mapStateToProps, 因为state有变动。

      • mapDispatchToProps(proxy):

        只有订阅了ownProps,才会执行mapDispatchToProps,因为state变动与mapDispatchToProps无影响。

      • mergedProps(proxy):

        必定执行,将所有结果合并。

    • 第二种:state相等,props不等

      • mapStateToProps(proxy):

        只有订阅了ownProps,才会执行mapStateToProps, 因为state无变动。

      • mapDispatchToProps(proxy):

        只有订阅了ownProps,才会执行mapDispatchToProps,因为state变动与mapDispatchToProps无影响。

      • mergedProps(proxy):

        必定执行,将所有结果合并。

    • 第三种:state不等,props相等

      • mapStateToProps(proxy):

        不管是否订阅ownProps,执行mapStateToProps, 因为state有变动。

        注意,这里结果需要浅比较判断

        因为如果没有浅比较检查,而两者刚好浅比较相等
        那么最后也会认为返回一个新的props,也就是相当于重复更新了。

        之所以第一个stateprops都有变动的不需要浅比较检查,
        是因为如果props变了,则必须要更新组件。

      • mapDispatchToProps(proxy):

        不会执行,因为它只关注props

      • mergedProps(proxy):

        只有上面浅比较不等,才会执行。

    makeDerivedPropsSelector的总结:

    通过闭包管理数据,并且通过浅比较和全等比较判断是否需要更新组件数据。

    makeChildElementSelector

    makeChildElementSelector也是一个高阶函数,储存了之前的数据组件,并且判断与当前的判断。

    这里是最终渲染组件的地方,因为需要判断一下刚才最终给出的数据是否需要去更新组件。

    2个逻辑:

    1. 数据与之前不等(===),更新组件。
    2. forWardRef属性值与之前不等,更新组件。

    否则,返回旧组件(不更新)。

    继续回到Connect组件。

    之后就是render

    
    render() {
      // React的createContext
      const ContextToUse = this.props.context || Context
    
      return (
        &lt;ContextToUse.Consumer&gt;
          {this.renderWrappedComponent}
        &lt;/ContextToUse.Consumer&gt;
      )
    }
    

    Context.Consumer内部必须是一个函数,这个函数的参数就是Context.Providervalue,也就是reduxstore

    renderWrappedComponent

    最后一个函数:renderWrappedComponent

    
    renderWrappedComponent(value) {
      /* ...验证参数有效性... */
      
      // 这里 storeState=store.getState()
      const { storeState, store } = value
    
      // 传入自定义组件的props
      let wrapperProps = this.props
      
      let forwardedRef
      if (forwardRef) {
        wrapperProps = this.props.wrapperProps
        forwardedRef = this.props.forwardedRef
      }
    
      // 上面已经讲了,返回最终数据
      let derivedProps = this.selectDerivedProps(
        storeState,
        wrapperProps,
        store
      )
    
      // 返回最终渲染的自定义组件
      return this.selectChildElement(derivedProps, forwardedRef)
    }
    

    总算结束了,可能有点混乱,做个总结吧。


    总结

    我把react-redux的执行流程分为3个阶段,分别对应我们的代码编写(搭配导图阅读)


    一张导图:


    第一阶段:

    对应的用户代码:

    
    &lt;Provider store={store}&gt;
      &lt;App /&gt;
    &lt;/Provider&gt;
    

    执行内容有:

    1. 定义了Provider组件,这个组件内部订阅了reduxstore,保证当store发生变动,会立刻执行更新。

    第二阶段:

    对应的用户代码:

    
    connect(mapStateToProps,mapDispatchToProps,mergeProps,options)
    

    执行内容有:

    1. connect接收了参数(mapStateToProps,mapDispatchToProps,mergeProps,options)。
    2. connect接收了对参数如何处理方案(3个...Factories)。
    3. connect接收了参数的结果比较方案(selectFactory)
    4. 定义了比较方案(4个are...Equal,其实就是全等比较浅比较)。

    第三阶段:

    对应的用户代码:

    
    let newComponent=connect(...)(Component)
    
    &lt;newComponent /&gt;
    

    执行内容有:

    1. 接受自定义组件(Component)。
    2. 创建一个Connect组件。
    3. Component的非React静态方法转移到Connect
    4. 获取Provider传入的数据(redux的整个数据),利用闭包保存数据,用于和未来数据做比较。
    5. 当比较(===)有变动,执行上一阶段传入的参数,获取当前组件真正的数据。
    6. 利用闭包保存当前组件真正的数据,用于和未来作比较。
    7. 通过全等和浅比较,处理state变动和props变动的逻辑,判断返回新数据还是旧数据。
    8. 利用闭包保存渲染的组件,通过上面返回的最终数据,判断需要返回新组件还是就组件。

    逻辑理顺了,还是很好理解的。

    其中第三阶段就是对外APIconnectAdvanced的执行内容。


    此处查看更多前端源码阅读内容。

    或许哪一天,我们需要设计一个专用的数据管理系统,那么就利用好connectAdvanced
    我们要做的就是编写一个自定义第二阶段的逻辑体系。

    感谢阅读!

    来源:https://segmentfault.com/a/1190000017062631

  • 相关阅读:
    arcgis python 把多个MXD批量导出一个PDF
    arcgis python pdf合并
    arcgis python 列出一个表所有字段
    arcgis python 随机取部分数据
    arcgis python 删除一个数据库所有数据
    python将py文件转换为pyc
    从 10.x 到 ArcGIS Pro 的 Python 迁移
    Entity Framework 6.X实现记录执行的SQL功能
    Entity Framework 5.0系列之EF概览-三种编程方式
    Entity Framework 全面教程详解(转)
  • 原文地址:https://www.cnblogs.com/datiangou/p/10156283.html
Copyright © 2020-2023  润新知