精华内容
下载资源
问答
  • vue状态管理、Vuex使用详解

    千次阅读 多人点赞 2019-03-02 14:09:47
    state :全局访问的state对象,存放要设置的初始状态名及值(必须要有) mutations :里面可以存放改变 state 的初始值的方法 ( 同步操作--必须要有 ) getters :实时监听state值的变化可对...

    1.NPM安装

        npm install vuex --save

    2.配置vuex

    Vuex 实例对象属性 主要有下面5个核心属性

        state :全局访问的state对象,存放要设置的初始状态名及值(必须要有)

        mutations :里面可以存放改变 state 的初始值的方法 ( 同步操作--必须要有 )

        getters :实时监听state值的变化可对状态进行处理,返回一个新的状态,相当于store的计算属性(不是必须的)

        actions :里面可以存放用来异步触发 mutations 里面的方法的方法 ( 异步操作--不是必须的 )

        modules :存放模块化的数据(不是必须的)

    全局配置Vuex:

        在 src 目录下创建 store 文件夹,并在里面创建一个index.js文件,然后index.js中配置如下:

    第一步:引入Vue、和Vuex(固定写法)
    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex); 
    
    第二步:声明Vuex 的五个属性,其中state,mutations 是一定要定义的,其他的三个属性对象根据实际需要。
    const state = {  // 初始化状态值--一定要有该属性对象         
        ...
    }
    const mutations = {  // 自定义改变state初始值的方法--一定要有该属性对象
        ...
    }
    const getters = {  // 状态计算属性--该属性对象不是必须的            
        ...
    }
    const actions = { // 异步操作状态--该属性对象不是必须的
        ...
    }
    const modules = {  // 状态模块--该属性对象不是必须的
        ...
    }
    
    第三步:创建一个 store 实例,将声明的五个变量赋值赋值给 store 实例,如下:
    const store = new Vuex.Store({
       state,
       mutations,
        //下面三个非必须
       getters,
       actions,
       modules
    })
    
    第四步:导出 store 实例,供外部访问
    export default store

    在项目的main.js中将Vuex注册到全局实例中

    ...
    import store from './store'
    ...
    
    new Vue({
      el: '#app',
      router,
      store,         //注入,组件中可以使用 this.$store 获取
      components: { App },
    })

    上面只是讲了Vuex的全局配置,给我们打好了骨架,那么接下来认识一下,Vuex中五个核心属性的具体用法以及什么时候会用到 非必须的三个属性对象  getters,actions,modules

    3.vuex使用

    • state属性

           在配置文件store/index.js中,比如初始化设置两个状态 StudNumStudScore

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex); 
    
    const state = {      
        StudNum:3, // 初始化一个状态,存放学生人数
        StudScore:[ // 初始化一个状态,存放学生的分数信息
            {name:'小敏',score:80},
            {name:'小花',score:90},
            {name:'小红',score:98}
        ]
    }
    
    const store = new Vuex.Store({
       state
    })
    export default store

    在组件中获取两个状态的值:this.$store.state.xxx

    <template>
    <div>
        <h4>直接使用状态值</h4>
        <p>学生人数:{{$store.state.StudNum}}</p>
        <p v-for="(item,index) in $store.state.StudScore" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>通过计算属性获取</h4>
        <p>学生人数:{{StudNum}}</p>
        <p v-for="(item,index) in StudScore" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    </div>
    </template>
    <script>
    export default{
        computed:{ // 计算属性
            StudNum(){
                 return this.$store.state.StudNum
            },
            StudScore(){
                 return this.$store.state.StudScore
            },
        }
    }
    </script>

    页面效果如下:

    state使用
    state使用

    注:两种方式都可以获取状态值。效果是一样的:直接使用状态,或者在vue计算属性中使用。  接下来的获取状态值,都是用vue计算属性,方便后面讲解辅助函数

    mapState 辅助函数

            当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键。上面计算属性获取状态值用辅助函数可写法如下:

    <template>
    <div>
        <h4>通过计算属性获取</h4>
        <p>学生人数:{{StudNum}}</p>
        <p v-for="(item,index) in StudScore" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    </div>
    </template>
    <script>
    // 在单独构建的版本中辅助函数为 Vuex.mapState
    import { mapState } from 'vuex'
    export default {
        data(){
            return{
                  localCount:12
            }
        },
        computed: mapState({
            StudNum: state => state.StudNum,  // 箭头函数可使代码更简练
            StudScore: 'StudScore',           // 可也传字符串参数 'StudScore' 等同于 `state =>state.StudScore
            nweNum(state) {  // 为了能够使用 `this` 获取局部状态,必须使用常规函数纠正this指向
                return state.count + this.localCount
            }
        })
    }
    </script>
    

             当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组,上例中的计算属性StudNumStudScore跟state中的子节点状态名相同,因此可简写成如下写法:

    computed: mapState([
      'StudNum',   // 映射 this.StudNum为 this.$store.state.StudNum
      'StudScore'  // 映射 this.StudScore为 this.$store.state.StudScore
    ])
    

    对象展开运算符

       mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符(现处于 ECMAScript 提案 stage-4 阶段),我们可以极大地简化写法:后面讲解案例用到辅助函数时都会用这种方法,也推荐使用这种写法来使用辅助函数。当然使用辅助函数也不是必须的,对于单个组件中用到的状态比较多时,使用辅助函数是个很好的选择,能极大的简化代码。具体写法如下:

    computed: {
      localComputed () { // 组件中的其他计算属性
           return 23
      },
      ...mapState([  // 使用对象展开运算符将此对象混入到外部对象中
          'StudNum',   // 映射 this.StudNum为 this.$store.state.StudNum
          'StudScore'  // 映射 this.StudScore为 this.$store.state.StudScore
      ])
    }

            vuex中的辅助函数有四种,mapState, mapGetters,mapMutations, mapActions,他们的用法一模一样,掌握了mapState的用法,其他的三个用法也就掌握了。


    • getters属性

        前面说了,getters不是必须的,那么什么时候会用到呢?有时候我们需要从 store 中的 state 中派生出一些状态(对state进行计算过滤等操作),例如上例,我们在getters中从state的子节点StudScore里派生出两个状态:90分以上的学生以及其数量:

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex); 
    
    const state = {      
        StudNum:2, // 初始化一个状态,代表学生人数
        StudScore:[ // 初始化一个状态,存放学生的分数信息
            {name:'小敏',score:80},
            {name:'小花',score:90},
            {name:'小红',score:98}
        ]
    }
    const getters = {
        // 获取分数为90分以上的学生
        perfect: state => { // 过滤分数,获取90分及以上的学生
            return state.StudScore.filter(Stud => Stud.score>=90)
        },
        // 获取分数为90分以上的学生数量
        perfectNum: (state,getters) => { // getters 也可以接受其他 getters 作为第二个参数
            return getters.perfect.length
        }
    };
    
    const store = new Vuex.Store({
       state,
       getters
    })
    export default store
    
    

    在组件中获取getters派生的两个状态的值:this.$store.getters.xxx

    <template>
    <div>
        <h4>state原始状态</h4>
        <p>学生人数:{{$store.state.StudNum}}</p>
        <p v-for="(item,index) in $store.state.StudScore" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>getters 派生的状态 通过属性访问</h4>
        <p>优秀学生人数:{{$store.getters.perfectNum}}</p>
        <p v-for="(item,index) in $store.getters.perfect" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    </div>
    </template>
    <script>
    import { mapState } from 'vuex'
    export default{
        computed:{
            ...mapState([  // 使用辅助函数 mapState 
                'StudNum',   // 映射 this.StudNum为 this.$store.state.StudNum
                'StudScore'  // 映射 this.StudScore为 this.$store.state.StudScore
            ])
        }
    }
    </script>

    页面效果如下: 

    getters使用 属性访问

     

          从上面例子可以看出,使用getters我们从state子节点StudScore中派生了两个新的状态,这两个派生出的新状态不用在state中进行初始化,这是getters以属性的形式返回的情况,getters也可以返回一个函数,通过函数来访问getters,我们紧接着上面的例子,在getters中再派生出一个状态:checkScore 通过分数找出学生信息,它返回的是一个函数,如下:

    ...
    const getters = {
        ....
        // 用分数查询学生信息
        checkScore:state=>n=>{ // 返回一个方法函数
            return state.StudScore.find(Stud=> Stud.score=== n)
        }
    }
    ...

    在组件中通过方法访问getters派生的状态:this.$store.getters.xxx( val ) 

    <template>
    <div>
        <h4>state原始状态</h4>
        <p>学生人数:{{StudNum}}</p>
        <p v-for="(item,index) in StudScore" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>getters 派生的状态 通过属性访问</h4>
        <p>优秀学生人数:{{$store.getters.perfectNum}}</p>
        <p v-for="(item,index) in $store.getters.perfect" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>getters 派生的状态 通过方法访问</h4>
        <p>有没有人得98分</p>
        <p>{{$store.getters.checkScore(98)}}</p>
        </div>
    </div>
    </template>
    <script>
    import { mapState } from 'vuex'
    export default{
        computed:{
            ...mapState([  // 使用辅助函数 mapState 
                'StudNum',   // 映射 this.StudNum为 this.$store.state.StudNum
                'StudScore'  // 映射 this.StudScore为 this.$store.state.StudScore
            ])
        }
    }
    </script>

    页面效果如下:

    getters使用  方法访问

     

        注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的,即只要对应的state状态不发生改变,不管执行多少次getters,都会从缓存中获取getters的状态值,不会重新计算,一旦对应的state发生改变,getters就会重新计算,并缓存起来。

                   getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。即不管对应的状态有没有发生改变,访问一次getters,就会执行一次getters返回的函数,并且不会被缓存。

        通过上面getters的使用,我们可以看到对状态进行计算操作,我们不一定非使用getters不可,我们也可以在组件中获取状态值,再对得到的值进行过滤计算等操作也是可以的,所以说getters不是必须的。

    mapGetters 辅助函数

        mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

    import { mapState, mapGetters } from 'vuex'
    
    export default {
      computed: {
            ...mapState([    // 使用辅助函数 mapState 
                'StudNum',   // 映射 this.StudNum为 this.$store.state.StudNum
                'StudScore'  // 映射 this.StudScore为 this.$store.state.StudScore
            ]),
            ...mapGetters([  // 使用辅助函数 mapGetters
                'perfect',   // 映射 this.perfect为 this.$store.getters.perfect
                'perfectNum',// 映射 this.perfectNum为 this.$store.getters.perfectNum
                'checkScore' // 映射 this.checkScore为 this.$store.getters.checkScore()
            ])
      }
    }
    

    如果你想将一个 getter 属性另取一个名字,使用对象形式:

    computed: {
        ...mapGetters({
            P:'perfect',    // 把 `this.P` 映射为 `this.$store.getters.perfect`
            N:'perfectNum', // 把 `this.N` 映射为 `this.$store.getters.perfectNum`
            S:'checkScore'  // 把 `this.S()` 映射为 `this.$store.getters.checkScore()`
        })
    }

    • mutations

         更改 Vuex 的 store 中的状态的唯一方法是提交 mutation ,所以他是必须的。现在来看一下,怎么更改store中的状态,我们再次继续上面的学生分数信息的例子,我们在mutations里面,初始化一个方法:ChangeStudScore ,在此方法里面根据外部传参,我们重新改变state中StudNum STudScore 的值,如下:

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex); 
    
    const state = {      
        StudNum:2, // 初始化一个状态,代表学生人数
        StudScore:[ // 初始化一个状态,存放学生的分数信息
            {name:'小敏',score:80},
            {name:'小花',score:90},
            {name:'小红',score:98}
        ]
    }
    const getters = {
        // 获取分数为90分以上的学生
        perfect: state => { // 过滤分数,获取90分及以上的学生
            return state.StudScore.filter(Stud => Stud.score>=90)
        },
        // 获取分数为90分以上的学生数量
        perfectNum: (state,getters) => { // getters 也可以接受其他 getters 作为第二个参数
            return getters.perfect.length
        },
        // 用分数查询学生信息
        checkScore:state=>n=>{ // 返回一个方法函数
            return state.StudScore.find(Stud=> Stud.score=== n)
        }
    };
    const mutations={
        ChangeStudScore(state,obj) {   //自定义改变state初始值的方法,这里面的参数除了state之外还可以再传额外的参数(变量或对象);
            state.StudNum = obj.length; // 更改状态StudNum 的值
            state.StudScore = obj; // 更改状态StudScore 的值
        }
    }
    
    const store = new Vuex.Store({
       state,
       getters,
       mutations
    })
    export default store

    在组件中使用mutations中的方法来改变状态的值(顺便把辅助函数一起讲解): this.$store.commit( "xxx" )

    <template>
    <div>
    <button @click="add">点击改变--></button>
        <h4>state原始状态</h4>
        <p>学生人数:{{StudNum}}</p>
        <p v-for="(item,index) in StudScore" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>getters 派生的状态 通过属性访问</h4>
        <p>优秀学生人数:{{perfectNum}}</p>
        <p v-for="(item,index) in perfect" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>getters 派生的状态 通过方法访问</h4>
        <p>有没有人得98分</p>
        <p>{{checkScore(98)}}</p>
        </div>
    </div>
    </template>
    <script>
    import { mapState, mapGetters, mapMutations } from 'vuex'
    export default{
        computed:{
      computed: {
            ...mapState([    // 使用辅助函数 mapState 
                'StudNum',   // 映射 this.StudNum为 this.$store.state.StudNum
                'StudScore'  // 映射 this.StudScore为 this.$store.state.StudScore
            ]),
            ...mapGetters([  // 使用辅助函数 mapGetters
                'perfect',   // 映射 this.perfect为 this.$store.getters.perfect
                'perfectNum',// 映射 this.perfectNum为 this.$store.getters.perfectNum
                'checkScore' // 映射 this.checkScore为 this.$store.getters.checkScore
            ])
      }
        },
        methods: {
            ...mapMutations([// 使用辅助函数 mapMutations
                "ChangeStudScore" // 映射 this.ChangeStudScore(obj)为 this.$store.commit("ChangeStudScore", obj)
            ]),
            add() { // 点击按钮,设置状态的值
                let obj = [
                    {name: '张三',score: 93},
                    {name: '李四',score: 90},
                    {name: '王五',score: 98},
                    {name: '赵六',score: 70},
                ]
                //this.$store.commit("ChangeStudScore", obj) // 不使用辅助函数时的写法
                this.ChangeStudScore(obj)// 使用辅助函数时的写法
            }
        },
    }
    </script>
    		

    页面展示效果: 

    mutations使用

    使用常量替代 Mutation 事件类型( 直接复制官网,实际开发中没用过 )

         使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

    // mutation-types.js
    export const SOME_MUTATION = 'SOME_MUTATION'
    
    // store.js
    import Vuex from 'vuex'
    import { SOME_MUTATION } from './mutation-types'
    
    const store = new Vuex.Store({
      state: { ... },
      mutations: {
        // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
        [SOME_MUTATION] (state) {
          // mutate state
        }
      }
    })
    

    用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。

    Mutations 必须是同步函数:

           一条重要的原则就是要记住 mutations 必须是同步函数.

    const mutations= {
      someMutation (state) {
        api.callAsyncMethod(() => {      //当这个 callAsyncMethod 请求函数执行完毕时 让 state.count++
          state.count++
        })
      }
    }

           现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

           关于Mutation 必须是同步函数,上面的例子和说明是官方的原话,但是我自己测试时,在Mutation 里面使用异步操作是可以改变state状态值的。如下:

    const mutations= {
      someMutation (state) {
        setTimeout(() => {      //模拟异步函数,延迟5s后执行 state.count++
          state.count++
        },5000)
      }
    }

          在组件中触发this.$store.commit("someMutation "), 5s后状态更新了,并没有什么问题,所以官网说Mutation 必须是同步函数,暂时无法理解是什么意思。既然官网都说了,异步操作不要在mutations 里面使用,那只好遵循官网的规定,并且关于异步操作,Vuex有专门的方法,使用 actions。

    Mutation 需遵守 Vue 的响应规则

          既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

    1. 最好提前在你的 store 中初始化好所有所需属性。

    2. 当需要在对象上添加新属性时,你应该

    • actions

    Action 类似于 mutation,不同在于:

    1. Action 提交的是 mutation,而不是直接变更状态。
    2. Action 可以包含任意异步操作。

        在上面讲解mutations时,官方说了不要在mutations里面使用异步操作,这里我们来看一下,专门用来处理异步操作的actions方法

    我们继续上面学生考分的例子,我们在actions里面初始化一个方法

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex); 
    
    const state = {      
        StudNum:2, // 初始化一个状态,代表学生人数
        StudScore:[ // 初始化一个状态,存放学生的分数信息
            {name:'小敏',score:80},
            {name:'小花',score:90},
            {name:'小红',score:98}
        ]
    }
    const getters = {
        // 获取分数为90分以上的学生
        perfect: state => { // 过滤分数,获取90分及以上的学生
            return state.StudScore.filter(Stud => Stud.score>=90)
        },
        // 获取分数为90分以上的学生数量
        perfectNum: (state,getters) => { // getters 也可以接受其他 getters 作为第二个参数
            return getters.perfect.length
        },
        // 用分数查询学生信息
        checkScore:state=>n=>{ // 返回一个方法函数
            return state.StudScore.find(Stud=> Stud.score=== n)
        }
    }
    const mutations={
        ChangeStudScore(state,obj) {   //自定义改变state初始值的方法,这里面的参数除了state之外还可以再传额外的参数(变量或对象);
            state.StudNum = obj.length; // 更改状态StudNum 的值
            state.StudScore = obj; // 更改状态StudScore 的值
        }
    }
    const actions = {
        AsyncChangeStudScore(context) {
          // 模拟异步请求,5秒后获取导数据,然后触发mutations中的方法ChangeStudScore,并传值
          setTimeout(()=>{
              let obj = {
                    {name: '张三',score: 93},
                    {name: '李四',score: 90},
                    {name: '王五',score: 98},
                    {name: '赵六',score: 70},
              }
              context.commit('ChangeStudScore',obj)
          },5000)
    
        }
    }
    const store = new Vuex.Store({
       state,
       getters,
       mutations
    })
    export default store

         在组件中调用actions方法( 辅助函数一起讲解 ):this.$store.dispatch( "xxx", obj ) 

    <template>
    <div>
    <button @click="add">点击改变--></button>
        <h4>state原始状态</h4>
        <p>学生人数:{{StudNum}}</p>
        <p v-for="(item,index) in StudScore" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>getters 派生的状态 通过属性访问</h4>
        <p>优秀学生人数:{{perfectNum}}</p>
        <p v-for="(item,index) in perfect" :key="index">
            姓名:{{item.name}} | 分数:{{item.score}}
        </p>
    <!------------------------->
        <h4>getters 派生的状态 通过方法访问</h4>
        <p>有没有人得98分</p>
        <p>{{checkScore(98)}}</p>
        </div>
    </div>
    </template>
    <script>
    import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
    export default{
        computed:{
      computed: {
            
            ...mapState([    // 使用辅助函数 mapState 
               'StudNum',   // 映射 this.StudNum为 this.$store.state.StudNum
               'StudScore'  // 映射 this.StudScore为 this.$store.state.StudScore
            ]),
            ...mapGetters([  // 使用辅助函数 mapGetters
                'perfect',   // 映射 this.perfect为 this.$store.getters.perfect
                'perfectNum',// 映射 this.perfectNum为 this.$store.getters.perfectNum
                'checkScore' // 映射 this.checkScore为 this.$store.getters.checkScore
            ])
      }
        },
        methods: {
    /*   不直接使用mutations改变状态,使用下面的异步请求到的数据来更新状态
            ...mapMutations([ // 使用辅助函数 mapMutations
                "ChangeStudScore" // 映射 this.ChangeStudScore(obj)为 this.$store.commit("ChangeStudScore", obj)
            ]),
            add() { // 点击按钮,设置状态的值
                let obj = [
                    {name: '张三',score: 93},
                    {name: '李四',score: 90},
                    {name: '王五',score: 98},
                    {name: '赵六',score: 70},
                ]
                //this.$store.commit("ChangeStudScore", obj) // 不使用辅助函数时的写法
                this.ChangeStudScore(obj)// 使用辅助函数时的写法
            }
    */
            ...mapActions([  // 使用辅助函数 mapMutations
                'AsyncChangeStudScore'
            ]),
            add() { // 点击按钮,设置状态的值
                //this.$store.dispatch("AsyncChangeStudScore") // 不使用辅助函数时的写法
                this.AsyncChangeStudScore(obj)// 使用辅助函数时的写法
            }
        },
    }
    </script>

        页面中最终展示的效果,跟讲解mutations 时显示的信息是一模一样的,只不过是这里采用了异步获取数据来更新状态,即点击按钮后,先进行异步请求操作,5秒后将异步获取成功的新的数据更新到状态里面,此时视图页面也跟着更新。

    当然actions含有其他许多用法,下面来看一下actions的其他介绍(官网复制而来)

           让我们来注册一个简单的 action

    const store = new Vuex.Store({
      state: {
        count: 0
      },
      mutations: {
        increment (state) {
          state.count++
        }
      },
      actions: {
        increment (context) {
          context.commit('increment')
        }
      }
    })
    

           Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

    实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit 很多次的时候):

    actions: {
      increment ({ commit }) {
        commit('increment')
      }
    }
    

    分发 Action

           Action 通过 store.dispatch 方法触发:

    store.dispatch('increment')
    

          乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

    actions: {
      incrementAsync ({ commit }) {
        setTimeout(() => {
          commit('increment')
        }, 1000)
      }
    }
    

    Actions 支持同样的载荷方式和对象方式进行分发:

    // 以载荷形式分发
    store.dispatch('incrementAsync', {
      amount: 10
    })
    
    // 以对象形式分发
    store.dispatch({
      type: 'incrementAsync',
      amount: 10
    })
    

    来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation

    actions: {
      checkout ({ commit, state }, products) {
        // 把当前购物车的物品备份起来
        const savedCartItems = [...state.cart.added]
        // 发出结账请求,然后乐观地清空购物车
        commit(types.CHECKOUT_REQUEST)
        // 购物 API 接受一个成功回调和一个失败回调
        shop.buyProducts(
          products,
          // 成功操作
          () => commit(types.CHECKOUT_SUCCESS),
          // 失败操作
          () => commit(types.CHECKOUT_FAILURE, savedCartItems)
        )
      }
    }
    

    注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。

    在组件中分发 Action

        你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

    import { mapActions } from 'vuex'
    
    export default {
      // ...
      methods: {
        ...mapActions([
          'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
    
          // `mapActions` 也支持载荷:
          'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
        ]),
        ...mapActions({
          add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
        })
      }
    }
    

    组合 Action

              Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

    首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

    actions: {
      actionA ({ commit }) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            commit('someMutation')
            resolve()
          }, 1000)
        })
      }
    }
    

    现在你可以:

    store.dispatch('actionA').then(() => {
      // ...
    })
    

    在另外一个 action 中也可以:

    actions: {
      actionB ({ dispatch, commit }) {
        return dispatch('actionA').then(() => {
          commit('someOtherMutation')
        })
      }
    }
    

    最后,如果我们利用 async / await,我们可以如下组合 action:

    // 假设 getData() 和 getOtherData() 返回的是 Promise
    
    actions: {
      async actionA ({ commit }) {
        commit('gotData', await getData())
      },
      async actionB ({ dispatch, commit }) {
        await dispatch('actionA') // 等待 actionA 完成
        commit('gotOtherData', await getOtherData())
      }
    }
    

    一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

    • modules

         由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

           为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

    const moduleA = {
      state: { ... },
      mutations: { ... },
      actions: { ... },
      getters: { ... }
    }
    
    const moduleB = {
      state: { ... },
      mutations: { ... },
      actions: { ... }
    }
    
    const store = new Vuex.Store({
      modules: {
        a: moduleA,
        b: moduleB
      }
    })
    
    store.state.a // -> moduleA 的状态
    store.state.b // -> moduleB 的状态
    

    模块的局部状态

    对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象

    const moduleA = {
      state: { count: 0 },
      mutations: {
        increment (state) {
          // 这里的 `state` 对象是模块的局部状态
          state.count++
        }
      },
    
      getters: {
        doubleCount (state) {
          return state.count * 2
        }
      }
    }
    

    同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

    const moduleA = {
      // ...
      actions: {
        incrementIfOddOnRootSum ({ state, commit, rootState }) {
          if ((state.count + rootState.count) % 2 === 1) {
            commit('increment')
          }
        }
      }
    }
    

    对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

    const moduleA = {
      // ...
      getters: {
        sumWithRootCount (state, getters, rootState) {
          return state.count + rootState.count
        }
      }
    }
    

    命名空间

         默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

    如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

    const store = new Vuex.Store({
        modules: {
            account: {
                namespaced: true,
                // 模块内容(module assets)
                state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
                getters: {
                    isAdmin () { ... } // -> getters['account/isAdmin']
                },
                actions: {
                    login () { ... } // -> dispatch('account/login')
                },
                mutations: {
                    login () { ... } // -> commit('account/login')
                },
                
                // 嵌套模块
                modules: {
                    // 继承父模块的命名空间
                    myPage: {
                        state: { ... },
                        getters: {
                            profile () { ... } // -> getters['account/profile']
                        }
                    },
                    // 进一步嵌套命名空间
                    posts: {
                        namespaced: true,
                        state: { ... },
                        getters: {
                            popular () { ... } // -> getters['account/posts/popular']
                        }
                    }
                }
    
            }
        }
    })
    

    启用了命名空间的 getter 和 action 会收到局部化的 getterdispatch 和 commit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

    #在带命名空间的模块内访问全局内容(Global Assets)

    如果你希望使用全局 state 和 getter,rootState 和 rootGetter 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

    若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。

    modules: {
      foo: {
        namespaced: true,
        getters: {
          // 在这个模块的 getter 中,`getters` 被局部化了
          // 你可以使用 getter 的第四个参数来调用 `rootGetters`
          someGetter (state, getters, rootState, rootGetters) {
            getters.someOtherGetter // -> 'foo/someOtherGetter'
            rootGetters.someOtherGetter // -> 'someOtherGetter'
          },
          someOtherGetter: state => { ... }
        },
    
        actions: {
          // 在这个模块中, dispatch 和 commit 也被局部化了
          // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
          someAction ({ dispatch, commit, getters, rootGetters }) {
            getters.someGetter // -> 'foo/someGetter'
            rootGetters.someGetter // -> 'someGetter'
    
            dispatch('someOtherAction') // -> 'foo/someOtherAction'
            dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
    
            commit('someMutation') // -> 'foo/someMutation'
            commit('someMutation', null, { root: true }) // -> 'someMutation'
          },
          someOtherAction (ctx, payload) { ... }
        }
      }
    }
    

    在带命名空间的模块注册全局 action

          若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

    {
      actions: {
        someOtherAction ({dispatch}) {
          dispatch('someAction')
        }
      },
      modules: {
        foo: {
          namespaced: true,
          actions: {
            someAction: {
              root: true,
              handler (namespacedContext, payload) { ... } // -> 'someAction'
            }
          }
        }
      }
    }
    

    带命名空间的绑定函数

            当使用 mapStatemapGettersmapActions 和 mapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

    computed: {
      ...mapState({
        a: state => state.some.nested.module.a,
        b: state => state.some.nested.module.b
      })
    },
    methods: {
      ...mapActions([
        'some/nested/module/foo', // -> this['some/nested/module/foo']()
        'some/nested/module/bar' // -> this['some/nested/module/bar']()
      ])
    }
    

            对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

    computed: {
      ...mapState('some/nested/module', {
        a: state => state.a,
        b: state => state.b
      })
    },
    methods: {
      ...mapActions('some/nested/module', [
        'foo', // -> this.foo()
        'bar' // -> this.bar()
      ])
    }
    

            而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

    import { createNamespacedHelpers } from 'vuex'
    const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
    
    export default {
      computed: {
        // 在 `some/nested/module` 中查找
        ...mapState({
          a: state => state.a,
          b: state => state.b
        })
      },
      methods: {
        // 在 `some/nested/module` 中查找
        ...mapActions([
          'foo',
          'bar'
        ])
      }
    }
    

    给插件开发者的注意事项

            如果你开发的插件(Plugin)提供了模块并允许用户将其添加到 Vuex store,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称:

    // 通过插件的参数对象得到空间名称
    // 然后返回 Vuex 插件函数
    export function createPlugin (options = {}) {
      return function (store) {
        // 把空间名字添加到插件模块的类型(type)中去
        const namespace = options.namespace || ''
        store.dispatch(namespace + 'pluginAction')
      }
    }
    

    模块动态注册

            在 store 创建之后,你可以使用 store.registerModule 方法注册模块:

    // 注册模块 `myModule`
    store.registerModule('myModule', {
      // ...
    })
    // 注册嵌套模块 `nested/myModule`
    store.registerModule(['nested', 'myModule'], {
      // ...
    })
    

    之后就可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态。

    模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync 插件就是通过动态注册模块将 vue-router 和 vuex 结合在一起,实现应用的路由状态管理。

    你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。

    保留 state

            在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState 选项将其归档:store.registerModule('a', module, { preserveState: true })

    当你设置 preserveState: true 时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写。

    模块重用

    有时我们可能需要创建一个模块的多个实例,例如:

    如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。

    实际上这和 Vue 组件内的 data 是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):

    const MyReusableModule = {
      state () {
        return {
          foo: 'bar'
        }
      },
      // mutation, action 和 getter 等等...
    }

    4.配置使用总结

           在 src 目录下创建 store 文件夹,并在里面创建一些文件,结如

    src
     ├── ....
     ├── main.js
     └── store
           ├── modules  // 模块化
           │      ├── page1.js 
           │      └── page2.js 
           └── index.js 

    a) store/index.js 中的内容: 

          开始配置主文件store / index.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex); 
    import page1 from './modules/page1' // 将page1.js文件导入
    import page2 from './modules/page2' // 将page1.js文件导入
    const state = {          
      showFooter: true,
      obj:{ b:2 }   
    }
    const mutations = {  
      showFooter(state,val) {
        state.showFooter = val;
      },
      obj(state,val){ 
        state.obj = val;
      }
    }
    const getters = { 
      showFooter(state) {  
        return state.showFooter 
      },
      obj(){                       
        return state.obj 
      }
    }
    const actions = {
      showFooter(context,val) { 
        setTimeout(()=>{
            context.commit('showFooter',val); 
        },3000)      
      },
      obj(context,val){                 
        context.commit('obj',val)  
      }
    }
    const modules = {
      page1,
      page2
    }
    const store = new Vuex.Store({
       state,
       getters,
       mutations,
       actions,
       modules
    })
    export default store

    b).store / page1.js 中的内容:( page1.js使用的是普通语法,数据结构和 page2.js一样)

          模块js中的 5个属性的定义,与index.js 中的一样

    export default {
      namespaced:true,       //用于在全局引用此文件里的方法时标识这一个的文件名
      state:{
        arr:["a"],           //初始化一个 arr 数组
      },
      getters:{
        getArr:function(state){              
          return state.arr
        }
      },
      mutations:{
        pushArr:function(state,items){      
          state.arr.push(items)
        }
      },
      actions:{
        pushArrRun:function(context,item){   
          context.commit('pushArr',item)
        }
      }
    }

    c). store / page2.js 中的内容:( page2.js使用的是ES6语法。page1.js,page2.两个写法一样,看个人喜好,推荐使用ES6语法)

    const state={         
      sum:1   //初始化一个 sum
    }
    const getters = {      
      getSum: () => state.sum
    }
    const mutations = {    
      add: (state,m) => state.sum += m 
    }
    const actions = {      
      addTo:(context,m) => context.commit('add',m)
    }
    export default {
      namespaced: true,    
      state,
      getters,
      mutations,
      actions
    }

    d). 最后一步   在 main.js 中将 配置好的Vuex 挂载(注入)到vue 实例中

    //.......
    import store from './store'
    
    new Vue({
      el: '#app',
      router,
      store,         //注入,组件中可以使用 this.$store 获取
      components: { App },
    })

    到此,Vuex 就配置完成了,简单大致梳理一下配置流程:

        1.声明 state 对象,存放要初始状态的数据对象

        2.声明 getters 对象,存放 派生state中数据的方法(store计算属性)

        3.声明 mutations 对象,存放 设置 state中数据的方法(同步操作)

        4.声明 actions 对象,存放 异步操作 mutations中的方法的方法(异步操作)

        5.声明 modules 对象, 存放 外部模板 文件 (如果没有创建外部 js 文件,次步可去除)

        6.创建 Vuex 实例,注入上面声明的对象,并用export default导出,然后再在main.js中将 Vuex 挂在到Vue实例中 

    再简单看一下基本使用的语法:

        1.获取 某个状态的值:  index.js   中的 obj 的值:this.$store.state.obj

                                            index.js   中的 obj 的值:this.$store.getters.obj

                                            page1.js  中的 arr 的值:this.$store.state[ "page1/arr" ]

                                            page1.js  中的 arr 的值:this.$store.getters[ "page1/arr" ]

        2.设置 某个状态的值:  index.js   中的 obj 的值:this.$store.commit( "obj" , val )

                                            index.js 中的 obj 的值:this.$store.dispatch( "obj" , val )

                                            page1.js 中的 arr 的值:this.$store.commit( "page1/arr" , val )

                                            page1.js 中的 arr 的值:this.$store.dispatch( "page1/arr" , val )

    1.基本语法使用

    
    <script>
    export default {
      data () {
        return { }
      },
      computed:{
        objData(){ // 获取 store/index.js 中的 obj
          return this.$store.state.obj  
    或
          return this.$store.getters.obj                  
        },
        arrData(){ // 获取 store/module/page1.js 中的 arr 
          return this.$store.state['page1/getArr']    
    或 
          return this.$store.getters['page1/getArr']  
        },
        sumData(){ // 获取 store/module/page2.js 中的 sum
          return this.$store.state['page2/getSum']
    或
          return this.$store.getters['page2/getSum']        
        }
      },
      methods:{  
        getDatas(s){ 
          // 获取 store/index.js 中对象 ctions的obj方法改变状态的值
          this.$store.dispatch('obj', s)
        },
        pushArr(s){  
          // 获取 store/module/page1.js 中对象actions的pushArrRun方法改变状态的值
          this.$store.dispatch('page1/pushArrRun', s)    
        },
        add(s){      
          // 获取 store/module/page2.js 中对象actions的addTo 方法改变状态的值
          this.$store.dispatch('page2/addTo', s)         
        },
        push:function(e){
          this.pushArr(e.target.innerHTML)
          this.add(1)
          this.getDatas({
            total: this.sumData,
            list: this.arrData
          }) 
        }
      }
    }
    </script>

    2.辅助函数 mapStatemapGettersmapActions 和 mapMutations

    <template>
      <div class="test">
        <button @click="push">拖鞋</button>
        <button @click="push">上衣</button>
        <button @click="push">帽子</button>
        <p>所有物品:{{ arrData }}</p>
        <p>总件数:{{ sumData }}</p>
        <p>总信息:{{ objData }}</p>
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters, mapActions } from 'vuex'  // 引入三个方法,个人觉得mapState多余,几乎用不到。
    export default {
      data () {
        return {
        }
      },
      computed:{
        ...mapState({                   
          objData:state=>state.obj,        
          arrData:state=>state.page1.arr,  
          sumData:state=>state.page2.sum 
        }),
        ...mapGetters({              
          objData:'obj',                     
          arrData:'page1/getArr',          
          sumData:'page2/getSum'           
        })
      },
      methods:{ 
        ...mapMutations({             
          newObjRun:'obj',                 
          pushArrRun:'page1/pushArrRun',   
          addTo:'page2/addTo'              
        }),
        ...mapActions({             
          newObjRun:'obj',                 
          pushArrRun:'page1/pushArrRun',   
          addTo:'page2/addTo'              
        })
      }
    }
    </script>

     

    传送门:

    vue 完整项目配置1(下载/安装)

    vue项目配置2-项目目录结构,vue打包白屏,图片加载不出等问题

    vue项目配置3-router路由配置,元信息meta的使用-登录拦截验证

    vue项目配置5-axios请求 配置

    展开全文
  • 管理应用程序的状态有多种不同的方法,在大中型单页面应用项目中,Vue引入了Vuex完成对多个组件之间需要共享数据或状态,完成集中式数据和状态管理, 使得一处修改,多处使用。 VUEX的安装 Vuex是一个专为Vue.js应用...

    前言

    随着WEB单页面(SPA)技术的兴起,越来越多的后端工作转移到了前端,不同组件之间数据共享成为亟需解决的问题。一个组件改变了数据值,其他使用到该数据的组件应该做出积极的响应与变化,这是同步问题就变得非常的麻烦。应用状态管理就是为完成数据同步问题的解决方案。管理应用程序的状态有多种不同的方法,在大中型单页面应用项目中,Vue引入了Vuex完成对多个组件之间需要共享数据或状态,完成集中式数据和状态管理, 使得一处修改,多处使用。

    VUEX的安装

    Vuex是一个专为Vue.js应用程序开发的状态管理插件,该插件并非是VUE管理应用程序的状态唯一的解决方案,使用前必须安装该插件。安装步骤如下

    1. 查看该组件是否已经安装。打开工程目录下的文件package.json,查看dependencies项,如下图所示,如果有"vuex"项,代表已经安装;如果没有安装需要通过npm进行安装。
      在这里插入图片描述
    2. 打开cmd窗口,进入项目所在目录,输入命令:cnpm install vuex –save或npm install vuex –save,完成vuex的安装
    3. 安装完成后,再次打开package.json文件,如上图所示如有"vuex"项代表已经成功安装

    vuex五种基本对象

    Vuex是一个专为Vue.js应用程序开发的状态管理模式。采用集中式存储管理应用所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。这些规则集中表现在Vuex五种默认的基本对象的使用上。

    state对象:state为单一状态树,用来存储各种变量的值,变量可以是数组、对象、字符串等数据类型。只有在state中定义这些状态,在组件中才能获取、修改和读取的这个对象的状态。存储状态(变量或属性),它是一个简单的键值对对象。如

    const state = {  
    	  loginState: false  
    	  ...  
    	  } 
    

    在这里定义一个state对象,state对象包含了一个属性(变量)loginState,它的数据类型是布尔型,默认值是false。
    依赖这个数据的组件state值的获取通过this.$store.state.【key】,如loginState的获取为:

    var loginState = this.$store.state.loginState
    

    值得注意的是使用data接收state值,state中的状态改变的时候,需要将state值的获取放置到使用组件的computed中,才能监听到值的变化,数据和视图是同步的。

    getters对象:从基本数据(state)派生的数据,相当于state的计算属性。在项目中使用Vuex的store实例时,如果需要对store的state对象状态值进行简单运算后再使用,这时就要用到store实例的计算属性getter。如:在state中记录营业额,营业额计量单位是元,统计模块要使用到营业额,但使用到计量单位是万元,此时就需要使用到getters,可以理解为state的计算属性。如:

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      state: {
        loginState: false,
        atm: 10020 //
      },
      getters: {     
        convertAtm: function(state) {
          return state.atm / 10000
        }
      }
    })
    
    export default store
    

    在getters定义了一个营业额转换函数,在统计组件中可以直接使用,使用时再统计组件computed,通过 this.$store.getters.convertAtm获取所需的值。

    Mutations对象:通过state对象或者Getters对象,组件可以非常方便的使用store实例中的状态。如果组件中需要更改store实例中的状态,则需要使用mutation提供的方法提交修改。以下通过一个示例说明Mutations对象的使用。
    在state中有一个属性value记录当前值,在一个组件中获取到该值,在组件中包含两个按钮,分别用于增加或减少value值。store中代码如下

    import Vuex from 'vuex'  
    Vue.use(Vuex)  
    const state = {    //定义state
      value: 1  
    }   
    const mutations = {  //定义mutations
      increment (state, v) {  
        state.value = parseInt(state.value) + parseInt(v)  
      },  
      decrement (state, v) {  
        state.value = parseInt(state.value) - parseInt(v)  
      }  
    }  
    export default new Vuex.Store({  
      state,  
      getters,  
      mutations  //注册mutations  
    }) 
    

    以上代码定义了一个state的值为1,在mutations中定义了increment和decrement两个方法,分别用于完成状态值的增加与减少。调用组件中代码如下:

    import Vue from 'vue'  
    <template>  
      <div class="hello">  
        <h1>使用computed接收:{{ computedmsg }}</h1> 
        <input type="number" value="1" v-model="num" />  
        <button @click="increment">+增加</button>  
        <button @click="decrement">-减少</button>  
      </div>  
    </template>  
    <script>  
    export default {  
      name: 'hello',  
      data () {  
        return {  
          num: 1
        }  
      },  
      computed: {  
        computedmsg () {  
          return this.$store.state.value 
        }  
      },  
      methods: {  
        increment () {  
          this.$store.commit('increment', this.num)  //第一参数为store定义的函数名,第二个为参数
        },  
        decrement () {  
          this.$store.commit('decrement', this.num)  
        }  
      }  
    }  
    </script>  
    <style>  
    </style> 
    

    当一个组件更改状态值时,通过this.$store.commit()方法进行修改,其中第一参数是mutations定义的函数名,第二个参数为要传入的参数。通过这种机制,将状态的变化规则封装到mutations对象中,而传入的参数为state变化的一个因素(参数)。
    Actions对象:vuex是通过Mutations对象完成状态值的修改,一旦执行commit则state中的属性值就发生了变化,如果想在执行commit前或commit后完成一些操作是不可能。这时就必须借助Actions对象来完成。Actions类似于Mutations的代理对象,实际的状态修改提交工作是Mutations的commit完成,使用Actions可以动态的增强相应的功能。Actions对象使用时,在Actions中自定义的函数接收一个context参数和要变化的形参,context与store实例具有相同的方法和属性,所以它可以执行context.commit(’ ') 调用mutations里面的方法。示例代码如下:
    【例】state中有一个count的属性,点击更改时,count的值更改为加上传入的参数,并且需要在更改此状态前,在控制台上输出“要不要更改状态?算了还是更改吧”。

    import Vue from 'vue'  
    import Vuex from 'vuex'  
      
    //使用vuex模块  
    Vue.use(Vuex);  
      
    //声明静态常量为4  
    const state = {  
      count: 4  
    };  
    const mutations = {  //这里使用已经能够完成state的增加了
      add(state, n) {  
        state.count += n.a;  
      }  
    };  
    //这里的actionAdd是组件中和所触发的事件相对应的方法  
    const actions = {  
      increment(context,num) { //context官方给出的指定对象,可以理解为store对象,num代表参数  
        console.log(“要不要更改状态?算了还是更改吧”)
        context.commit('add', {//这里的add’是mutations中的方法  
          a: num.res //res为传递参数  
        })  
      },   
    };  
      
    //导出一个模块  
    export default new Vuex.Store({  
      state,  
      mutations,  
      actions  
    })  
    

    这里increment为自定义函数,里面有context,num两个参数,/context官方给出的指定对象,可以理解为store对象,num代表参数,参数可以是单个值,也可以是对象、数组,这里的num是一个对象。
    使用组件代码为:

    <template>  
      <div class="hello">  
        <p>组件内部count{{count}}</p>  
        <button @click="addNum">+增加</button>  
      </div>  
    </template>  
      
    <script>  
      export default {  
        name: 'storeAction',  
        data() {  
          return {}  
        },  
        computed: {  
          count() {  
            return this.$store.state.count  
          }  
        },  
        methods: {  
          addNum() {  
            this.$store.dispatch('increment', {  
              res: 15  
            })  
          }
        }  
      }  
    </script> 
    

    在组件中可以通过this.$store.dispatch分发action,increment是action定义的参数名,{ res: 15 }是要传入的参数。

    modules对象:Vuex使用单一状态树,所有状态会集中到一个对象中。当项目非常复杂时,store对象可能变得相当臃肿,不易维护。为了解决这个问题,Vuex允许将store分割成若干个模块(module)进行管理,每个模块拥有各自的 state、mutation、action、getter。
    】创建modules模块,将系统store分割为app和user两个模块。
    首先在项目store文件下创建modules文件夹用来存放系统状态分割的模块,并在此目录下创建app.js和user.js两个文件,用于项目级状态值的管理,app.js完整代码如下

    const state = {  
      appName: '项目名称'  
    }  
    const getters = {  
      getName (state) {  
        return state.appName  
      }  
    }  
    const mutations = {  
      Set_Name (state, name) {  
        state.appName = name  
      }  
    }  
    const actions = {  
      setAppName (context, name) {  
        console.log(name)  
        context.commit('Set_Name', name)  
      }  
    }  
    export default {  
      namespaced: true, // 用于在全局引用方法时标识这一个的文件名  
      state,  
      getters,  
      mutations,  
      actions  
    }  
    

    user.js代码如下:

    const state = {  
      userName: 'Felix'  
    }  
    const getters = {  
      getName (state) {  
        return state.userName  
      }  
    }  
    const mutations = {  
      Set_Name (state, name) {  
        state.userName = name  
      }  
    }  
    const actions = {  
      setUserName (context, name) {  
        context.commit('Set_Name', name)  
      }  
    }  
    export default {  
      namespaced: true, // 用于在全局引用方法时标识这一个的文件名 如:user/setUserName  
      state,  
      getters,  
      mutations,  
      actions  
    } 
    

    其次完成这两个模块载入,修改 store/index.js代码如下

    import Vue from 'vue'  
    import Vuex from 'vuex'  
    import app from './modules/app'  
    import user from './modules/user'  
    Vue.use(Vuex)  
    export default new Vuex.Store({  
       modules: {  
        app, 
        user  
      }  
    })  
    

    这样比把所有的状态写到一起,更好的完成了维护。使用时和上面的使用方法一致,仅需增加模块的名称而已。代码如下

    <template>  
      <div class="hello">  
        <h1>项目名称:{{ getAppName }}</h1>  
        <h1>项目名称:{{ mapAppName }}</h1>  
        <h1>用户名称:{{ getUserName }}</h1>  
      </div>  
    </template>  
    <script>  
    export default {  
      name: 'hello',   
      computed: {  
        getAppName () {  
          return this.$store.state.app.appName  
        },  
        getUserName () {  
          return this.$store.state.user.userName  
        },  
      }
    }  
    </script>
    

    示例

    示例界面如下图,特别丑陋,仅为了说明问题
    在这里插入图片描述
    写三个组件,分别是使用data接收state和getters数据(DataShow.vue)、使用computed接收state和getters数据(ComputeShow.vue),通过输入框输入的数据增加(减少)state内容(CountChange.vue)。通过该示例体会state、getters、mutations和Actions对象的使用。
    组件DataShow代码

    <template>
      <div align="center" style="background-color:aliceblue;">
        <p>以下是使用Data直接获取stores中的状态数据,状态数据发生变化时,该部分数据不同步刷新</p>
        <h1>使用data接收  state:{{ dataState }}</h1>
        <h1>使用data接收Getters:{{ dataGetters }}</h1>
      </div>
    </template>
    
    <script>
      export default {
        name: 'DataShow',
        data() {
          return {
            dataState: this.$store.state.count,
            dataGetters: this.$store.getters.getChangeValue
          }
        }
      }
    </script>
    
    <style>
    </style>
    

    使用Data直接获取state和getters中的状态数据,状态数据发生变化时,该部分数据不同步刷新。
    组件ComputeShow代码

    <template>
      <div align="center" style="background-color: bisque;">
        <p>以下是使用computed直接获取stores中的状态数据,状态数据发生变化时,同步刷新</p>
        <h1>使用computed接收 state:{{ computedState }}</h1>
        <h1>使用computed接收Getters:{{ computedGetters }}</h1>
      </div>
    </template>
    
    <script>
      export default {
        name: 'ComputeShow',
        computed: {
          computedState() {
            return this.$store.state.count; //通过$store.getters获取相应结果
          },
          computedGetters() {
            return this.$store.getters.getChangeValue; //通过$store.getters获取相应结果
          },
        }
      }
    </script>
    
    <style>
    </style>
    

    使用computed获取state和getters中的状态数据,状态数据发生变化时,view同步刷新。

    CountChange代码如下

    <template>
      <div align="center">
        <input type="number" v-model="paramValue" />
        <button @click="addNum">+增加</button>
        <button @click="subNum">-减少</button>
      </div>
    </template>
    
    <script>
     export default {
        name: 'CountChange',
        data() {
          return {
            paramValue: 1,
          }
        },
        methods: {
          addNum() {
            this.$store.dispatch('increment', {
              res: this.paramValue
            })
          },
          subNum() {
            this.$store.commit('decrement', this.num)
          },
        }
      }
    </script>
    
    <style>
    </style>
    

    变更状态值时,addNum使用了action对象,subNum直接使用mutations提交变化。
    stores代码:在src创建stores目录,在该目录下创建index.js文件,内容如下:

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    //使用vuex模块
    Vue.use(Vuex);
    
    //声明静态常量为4
    const state = {
      count: 4
    };
    
    const getters = {
      getChangeValue(state) {
        return state.count * 100
      }
    };
    
    const mutations = {
      add(state, n) {
        console.log(n)
        state.count += n.paramvalue;
      },
      sub(state) {
        state.count--;
      }
    };
    //这里的actionAdd是组件中和所触发的事件相对应的方法
    const actions = {
      increment(context, num) { //context官方给出的指定对象,可以理解为store对象,num代表参数
        context.commit('add', { //这里的add’是mutations中的方法
          paramvalue: num.res //res为传递参数
        })
      },
      decrement(context) {
        context.commit('sub');
      }
    };
    
    //导出一个模块
    export default new Vuex.Store({
      state,
      getters,
      mutations,
      actions
    })
    

    这里定义了一个count,默认值是4,getter给count乘100,mutations完成count值的减少和增加,通过actions代理mutations完成数据的更新。

    在main.js中,将stores挂载到根目录下,全局使用,代码如下

    import Vue from 'vue'
    import App from './App'
    import store from './stores'  //导入store
    
    Vue.config.productionTip = false
    
    /* eslint-disable no-new */
    new Vue({
      el: '#app',
      store,                //挂载在全局下
      components: { App },
      template: '<App/>'
    })
    

    编写页面组件(StoreView.vue),将上述三个组件引入,进行组合,代码如下

    <template>
      <div>
        <v-data-show></v-data-show>
        <v-compute-show></v-compute-show>
        <v-count-change></v-count-change>
      </div>
    </template>
    
    <script>
      import dataShow from '@/view/DataShow.vue'
      import computeShow from '@/view/ComputeShow.vue'
      import countChange from '@/view/CountChange.vue'
      export default {
        name: 'StoreView',
        components: {
          'v-data-show': dataShow,
          'v-compute-show': computeShow,
          'v-count-change': countChange
        }
      }
    </script>
    
    <style>
    </style>
    

    更改app.vue,将页面组件挂载app下。

    <template>
      <div id="app">
        <img src="./assets/logo.png">
        <div align="left">
          <v-home></v-home>
        </div>
      </div>
    </template>
    
    <script>
      import home from '@/view/StroeView.vue'
      export default {
        name: 'App',
        components: {
          'v-home': home
        }
      }
    </script>
    

    小结

    vuex状态管理器是一种单一状态树,state用于定义和存储状态值,通过this.store.key可以获取到状态值,为了能够做到实时响应,获取state值应该computes中;有时取有变化的状态值时,需要使用到getters对象;状态值的更新不能直接更新,需要使用mutations对象;如果需要异步更新时,使用Actions对象作为mutations的代理对象完成对状态更新。modules对象很简单将store切割若干模块,更易于维护。经过上述内容学习,已经完全会使用vuex进行组件间的通讯与同步,除此之外,vuex还提供了了四个辅助函数,用于简化代码,方便组件使用。

    展开全文
  • Vue源码探究-状态初始化 本篇代码位于vue/src/core/instance/state.js 继续随着核心类的初始化展开探索其他的模块,这一篇来研究一下Vue状态初始化。这里的状态初始化指的就是在创建实例的时候,在配置对象里定义...

    Vue源码探究-状态初始化

    本篇代码位于vue/src/core/instance/state.js

    继续随着核心类的初始化展开探索其他的模块,这一篇来研究一下Vue的状态初始化。这里的状态初始化指的就是在创建实例的时候,在配置对象里定义的属性、数据变量、方法等是如何进行初始处理的。由于随后的数据更新变动都交给观察系统来负责,所以在事先弄明白了数据绑定的原理之后,就只需要将目光集中在这一部分。

    来仔细看看在核心类中首先执行的关于 state 部分的源码:

    initState

    
    // 定义并导出initState函数,接收参数vm
    export function initState (vm: Component) {
      // 初始化实例的私有属性_watchers
      // 这就是在观察系统里会使用到的存储所有显式监视器的对象
      vm._watchers = []
      // 获取实例的配置对象
      const opts = vm.$options
      // 如果定义了props,则初始化props
      if (opts.props) initProps(vm, opts.props)
      // 如果定义了methods,则初始化methods
      if (opts.methods) initMethods(vm, opts.methods)
      // 如果定义了data,则初始化data
      if (opts.data) {
        initData(vm)
      } else {
        // 否则初始化实例的私有属性_data为空对象,并开启观察
        observe(vm._data = {}, true /* asRootData */)
      }
      // 如果定义了computed,则初始化计算属性
      if (opts.computed) initComputed(vm, opts.computed)
      // 如果定义了watch并且不是nativeWatch,则初始化watch
      // nativeWatch是火狐浏览器下定义的对象的原型方法
      if (opts.watch &amp;&amp; opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    

    这段代码非常直白,主要用来执行配置对象里定义的了状态的初始化。这里分别有 propsdatamethodscomputedwatch 五个配置对象,分别有各自的初始化方法。在仔细研究它们的具体实现之前,先来看一段将在各个初始化函数里用到的辅助函数。

    
    // 定义共享属性定义描述符对象sharedPropertyDefinition
    // 描述符对象的枚举和可配置属性都设置为true
    // get、set方法设置为空函数
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    
    // 定义并导出proxy函数,该函数用来为在目标对象上定义并代理属性
    // 接收目标对象target,路径键名sourceKey,属性键名三个参数
    export function proxy (target: Object, sourceKey: string, key: string) {
      // 设置属性描述符对象的get方法
      sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
      }
      // 设置属性描述性对象的set犯法
      sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val
      }
      // 在目标对象上定义属性
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    

    proxy 函数的定义非常重要,在下面要探究的各个初始化函数中它,它会将我们在配置对象中设置的属性全部定义到实例对象中,但是我们对这些属性的操作是通过各部分相应的代理属性上来执行的。getset 方法的实现非常明白的表示出这一过程,然后再将属性定义到实例中。由这个函数作为基础,继续来看看其他五个状态的初始化函数的内容。

    initProps

    
    // 定义initProps函数,接收vm,propsOptions两个参数
    function initProps (vm: Component, propsOptions: Object) {
      // 赋值propsData,propsData是全局扩展传入的赋值对象
      // 在使用extend的时候会用到,实际开发里运用较少
      const propsData = vm.$options.propsData || {}
      // 定义实例的_props私有属性,并赋值给props
      const props = vm._props = {}
      // 缓存prop键,以便将来props更新可以使用Array而不是动态对象键枚举进行迭代。
      // cache prop keys so that future props updates can iterate using Array
      // instead of dynamic object key enumeration.
      const keys = vm.$options._propKeys = []
      // 是否是根实例
      const isRoot = !vm.$parent
      // 对于非根实例,关闭观察标识
      // root instance props should be converted
      if (!isRoot) {
        toggleObserving(false)
      }
      // 遍历props配置对象
      for (const key in propsOptions) {
        // 向缓存键值数组中添加键名
        keys.push(key)
        // 验证prop的值,validateProp执行对初始化定义的props的类型检查和默认赋值
        // 如果有定义类型检查,布尔值没有默认值时会被赋予false,字符串默认undefined
        // 对propsOptions的比较也是在使用extend扩展时才有意义
        // 具体实现可以参考 src/core/util/props.js,没有难点这里不详细解释
        const value = validateProp(key, propsOptions, propsData, vm)
    
        // 非生产环境下进行检查和提示
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          // 进行键名的转换,将驼峰式转换成连字符式的键名
          const hyphenatedKey = hyphenate(key)
          // 对与保留变量名冲突的键名给予提示
          if (isReservedAttribute(hyphenatedKey) ||
              config.isReservedAttr(hyphenatedKey)) {
            warn(
              `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
              vm
            )
          }
          // 对属性建立观察,并在直接使用属性时给予警告
          defineReactive(props, key, value, () =&gt; {
            if (vm.$parent &amp;&amp; !isUpdatingChildComponent) {
              warn(
                `Avoid mutating a prop directly since the value will be ` +
                `overwritten whenever the parent component re-renders. ` +
                `Instead, use a data or computed property based on the prop's ` +
                `value. Prop being mutated: "${key}"`,
                vm
              )
            }
          })
        } else {
          // 非生产环境下直接对属性进行存取器包装,建立依赖观察
          defineReactive(props, key, value)
        }
        // 使用Vue.extend()方法扩展属性时,已经对静态属性进行了代理
        // 这里只需要针对实例化时的属性执行代理操作
        // static props are already proxied on the component's prototype
        // during Vue.extend(). We only need to proxy props defined at
        // instantiation here.
        // 当实例上没有同名属性时,对属性进行代理操作
        // 将对键名的引用指向vm._props对象中
        if (!(key in vm)) {
          proxy(vm, `_props`, key)
        }
      }
      // 开启观察状态标识
      toggleObserving(true)
    }
    

    initProps 函数的最主要内容有两点,一是对定义的数据建立观察,二是对数据进行代理,这就是私有变量 _props 的作用,之后获取和设置的变量都是作为 _props 的属性被操作。

    另外初始化 props 的过程中有针对 extend 方法会使用到的 propsData 属性的初始化。具体使用是在扩展对象时定义一些 props,然后在创建实例的过程中传入 propsData 配置对象,扩展对象里相应的props属性会接收 propsData 传入的值。与在父组件传入 props 的值类似,只是这里要显式的通过 propsData 配置对象来传入值。

    initData

    
    // 定义initData函数
    function initData (vm: Component) {
      // 获取配置对象的data属性
      let data = vm.$options.data
      // 判断data是否是函数
      // 若是函数则将getData函数的返回赋值给data和实例私有属性_data
      // 否则直接将data赋值给实例_data属性,并在无data时赋值空对象
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      // 如果data不是对象则将data赋值为空对象
      // 进一步保证data是对象类型
      if (!isPlainObject(data)) {
        data = {}
        // 在非生产环境下给出警告提示
        process.env.NODE_ENV !== 'production' &amp;&amp; warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // 实例对象代理data
      // proxy data on instance
      // 获取所有data键值
      const keys = Object.keys(data)
      // 获取配置对象的props
      const props = vm.$options.props
      // 获取配置对象的methods
      const methods = vm.$options.methods
      // 遍历keys
      let i = keys.length
      while (i--) {
        const key = keys[i]
        // 非生产环境给出与methods定义的方法名冲突的警告
        if (process.env.NODE_ENV !== 'production') {
          if (methods &amp;&amp; hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        // 检测是否与props冲突
        if (props &amp;&amp; hasOwn(props, key)) {
          // 非生产环境给出冲突警告
          process.env.NODE_ENV !== 'production' &amp;&amp; warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        // 没有与props冲突并且非保留字时,代理键名到实例的_data对象上
        } else if (!isReserved(key)) {
          proxy(vm, `_data`, key)
        }
      }
      // 观察数据
      // observe data
      observe(data, true /* asRootData */)
    }
    
    // 定义并导出getData函数,接受函数类型的data对象,和Vue实例对象
    export function getData (data: Function, vm: Component): any {
      // pushTarget和popTarget是为了解决Vue依赖性检测的缺陷可能导致冗余依赖性的问题
      // 具体可参阅 https://github.com/vuejs/vue/issues/7573
      // 此操作会设置Dep.target为undefined,在初始化option时调用dep.depend()也不会建立依赖
      // #7573 调用数据getter时禁用dep集合
      // #7573 disable dep collection when invoking data getters
      pushTarget()
      // 尝试在vm上调用data函数并返回执行结果
      try {
        return data.call(vm, vm)
      } catch (e) {
        // 如果捕获到错误则处理错误,并返回空对象
        handleError(e, vm, `data()`)
        return {}
      } finally {
        popTarget()
      }
    }
    

    与 props 的处理类似,initData 函数的作用也是为了对数据建立观察的依赖关系,并且代理数据到私有变量 _data 上,另外包括了对 data 与其他配置对象属性的键名冲突的检测。

    initComputed

    
    // 设置computedWatcherOptions对象
    const computedWatcherOptions = { computed: true }
    
    // 定义initComputed函数,接受实例vm,和computed对象
    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      // 定义watchers和实例_computedWatchers属性,初始赋值空对象
      const watchers = vm._computedWatchers = Object.create(null)
      // 是否是服务器渲染,computed属性在服务器渲染期间只能是getter
      // computed properties are just getters during SSR
      const isSSR = isServerRendering()
    
      // 遍历computed
      for (const key in computed) {
        // 获取用户定义的值
        const userDef = computed[key]
        // 如果用户定义的是函数则赋值给getter否则j将userDef.get方法赋值给getter
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        // 非生产环境抛出缺少计算属性错误警告
        if (process.env.NODE_ENV !== 'production' &amp;&amp; getter == null) {
          warn(
            `Getter is missing for computed property "${key}".`,
            vm
          )
        }
    
        // 非服务器渲染下
        if (!isSSR) {
          // 为计算属性创建内部监视器
          // create internal watcher for the computed property.
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions
          )
        }
    
        // 组件定义的内部计算属性已经在组件的原型上定义好了
        // 所以这里只要关注实例初始化时用户定义的计算属性
        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        // 键名非实例根属性时,定义计算属性,具体参照defineComputed函数
        if (!(key in vm)) {
          defineComputed(vm, key, userDef)
        // 非生产环境下,检测与data属性名的冲突并给出警告
        } else if (process.env.NODE_ENV !== 'production') {
          if (key in vm.$data) {
            warn(`The computed property "${key}" is already defined in data.`, vm)
          } else if (vm.$options.props &amp;&amp; key in vm.$options.props) {
            warn(`The computed property "${key}" is already defined as a prop.`, vm)
          }
        }
      }
    }
    
    // 定义并导出defineComputed哈数
    // 接收实例target,计算属性键名key,计算属性值userDef参数
    export function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      // 在非服务器渲染下设置缓存
      const shouldCache = !isServerRendering()
      // 计算属性值是函数时
      if (typeof userDef === 'function') {
        // 设置计算属性的getter,setter为空函数
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : userDef
        sharedPropertyDefinition.set = noop
      } else {
        // 当计算属性是对象时,设置计算属性的getter和setter
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache &amp;&amp; userDef.cache !== false
            ? createComputedGetter(key)
            : userDef.get
          : noop
        sharedPropertyDefinition.set = userDef.set
          ? userDef.set
          : noop
      }
      // 非生产环境下,如果没哟定义计算属性的setter
      // 想设置计算属性时给出警告
      if (process.env.NODE_ENV !== 'production' &amp;&amp;
          sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function () {
          warn(
            `Computed property "${key}" was assigned to but it has no setter.`,
            this
          )
        }
      }
      // 以重新设置的属性描述符为基础在实例对象上定义计算属性
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    // 定义createComputedGetter,创建计算属性getter
    // 目的是在非服务器渲染情况下建立计算属性的观察依赖,
    // 并根据其依赖属性返回计算后的值
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers &amp;&amp; this._computedWatchers[key]
        if (watcher) {
          watcher.depend()
          return watcher.evaluate()
        }
      }
    }
    

    计算属性的初始化相对复杂一些,首先要对计算属性建立观察,然后再在实例上重新定义计算属性,并且执行属性代理。由于加入了服务器渲染的功能,在定义计算属性的时候对使用环境做判断,是非服务器渲染会影响到计算属性的定义,这是由于服务器渲染下使用框架时,计算属性是不提供 setter 的;另外也要根据用户定义的值是函数或者对象来对计算属性重新定义 getter 和 setter。从这段代码里可以看出一个非常重要的程序,即在获取计算属性的时候才去计算它的值,这正是懒加载的实现。

    initMethods

    
    // 定义initMethods方法,接受实例vm,配置属性methods
    function initMethods (vm: Component, methods: Object) {
      // 获取实例的props
      const props = vm.$options.props
      // 遍历methods对象
      for (const key in methods) {
        // 非生产环境下给出警告
        if (process.env.NODE_ENV !== 'production') {
          // 未赋值方法警告
          if (methods[key] == null) {
            warn(
              `Method "${key}" has an undefined value in the component definition. ` +
              `Did you reference the function correctly?`,
              vm
            )
          }
          // 与props属性名冲突警告
          if (props &amp;&amp; hasOwn(props, key)) {
            warn(
              `Method "${key}" has already been defined as a prop.`,
              vm
            )
          }
          // 与保留字冲突警告
          if ((key in vm) &amp;&amp; isReserved(key)) {
            warn(
              `Method "${key}" conflicts with an existing Vue instance method. ` +
              `Avoid defining component methods that start with _ or $.`
            )
          }
        }
        // 在实例上定义方法,赋值为用户未定义函数或空函数
        vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
      }
    }
    

    initMethods 函数非常简单,除了一大段在非生产环境里报告检查冲突的代码,唯一的内容就是在实例上定义相应的方法并且把上下文绑定到实例对象上,这样即便不是使用箭头函数,在方法内也默认用 this 指代了实例对象。

    initWatch

    
    // 定义initWatch函数,接受实例vm和配置属性watch
    function initWatch (vm: Component, watch: Object) {
      // 遍历watch
      for (const key in watch) {
        // 暂存属性的值
        const handler = watch[key]
        // 如果handler是数组
        if (Array.isArray(handler)) {
          // 遍历数组为每一个元素创建相应watcher
          for (let i = 0; i &lt; handler.length; i++) {
            createWatcher(vm, key, handler[i])
          }
        } else {
          // 窦否则handler应该是函数,直接为key创建watcher
          createWatcher(vm, key, handler)
        }
      }
    }
    
    // 定义createWatcher函数
    // 接受实例vm、表达式或函数expOrFn,处理器handler,可选的options
    function createWatcher (
      vm: Component,
      expOrFn: string | Function,
      handler: any,
      options?: Object
    ) {
      // 如果handler是对象
      if (isPlainObject(handler)) {
        // 将handler赋值给options.
        options = handler
        // 重新赋值handler
        handler = handler.handler
      }
      // 如果handler是字符串,在实例上寻找handler并赋值给handler
      if (typeof handler === 'string') {
        handler = vm[handler]
      }
      // 创建观察并返回
      return vm.$watch(expOrFn, handler, options)
    }
    

    initWatcher 为传入的观察对象创建监视器,比较简单。值得注意的是参数的传入类型,观察对象 expOrFn 可以有两种方式,一种是字符串,一种是函数,在 Watcher 类中对此参数进行了检测,而在初始化的函数里不对它做任何处理。handler 对象也可以接受对象或字符串类型,在代码中对这两种传入方式做判断,最终找到handler引用的函数传入 $watch

    stateMixin

    探索完了 initState 函数之后,继续来看看 state 混入的方法 stateMixin,在这个函数里会提供上面还未曾提到的 $watch 方法的具体实现:

    
    // 定义并导出stateMixin函数,接收参数Vue
    export function stateMixin (Vue: Class&lt;Component&gt;) {
      // 使用 Object.defineProperty 方法直接声明定义对象时,flow会发生问题
      // 所以必须在此程序化定义对象
      // flow somehow has problems with directly declared definition object
      // when using Object.defineProperty, so we have to procedurally build up
      // the object here.
      // 定义dataDef对象
      const dataDef = {}
      // 定义dataDef的get方法,返回Vue实例私有属性_data
      dataDef.get = function () { return this._data }
      // 定义propsDef对象
      const propsDef = {}
      // 定义propsDef的get方法,返回Vue实例私有属性_props
      propsDef.get = function () { return this._props }
      // 非生产环境下,定义dataDef和propsDef的set方法
      if (process.env.NODE_ENV !== 'production') {
        // dataDef的set方法接收Object类型的newData形参
        dataDef.set = function (newData: Object) {
          // 提示避免传入对象覆盖属性$data
          // 推荐使用嵌套的数据属性代替
          warn(
            'Avoid replacing instance root $data. ' +
            'Use nested data properties instead.',
            this
          )
        }
        // 设置propsDef的set方法为只读
        propsDef.set = function () {
          warn(`$props is readonly.`, this)
        }
      }
      // 定义Vue原型对象公共属性$data,并赋值为dataDef
      Object.defineProperty(Vue.prototype, '$data', dataDef)
      // 定义Vue原型对象公共属性$props,并赋值为propsDef
      Object.defineProperty(Vue.prototype, '$props', propsDef)
    
      // 定义Vue原型对象的$set方法,并赋值为从观察者导入的set函数
      Vue.prototype.$set = set
      // 定义Vue原型对象的$delete方法,并赋值为从观察者导入的del函数
      Vue.prototype.$delete = del
    
      // 定义Vue原型对象的$watch方法
      // 接收字符串或函数类型的expOrFn,从命名中可看出希望为表达式或函数
      // 接收任何类型的cb,这里希望为回调函数或者是一个对象
      // 接收对象类型的options
      // 要求返回函数类型
      Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        // 把实例赋值给vm变量,类型需为Component
        const vm: Component = this
        // 如果cb是纯粹的对象类型
        if (isPlainObject(cb)) {
          // 返回createWatcher函数
          return createWatcher(vm, expOrFn, cb, options)
        }
        // 定义观察目标的options,大多数情况下为undefined
        options = options || {}
        // 定义options的user属性值为true,标识为用户定义
        options.user = true
        // 创建watcher实例
        const watcher = new Watcher(vm, expOrFn, cb, options)
        // 如果options的immediate为真
        if (options.immediate) {
          // 在vm上调用cb回调函数,并传入watcher.value作为参数
          cb.call(vm, watcher.value)
        }
        // 返回unwatchFn函数
        return function unwatchFn () {
          // 执行watcher.teardown()方法清除观察
          watcher.teardown()
        }
      }
    }
    

    stateMixin执行的是关于状态观察的一系列方法的混入,主要是三个方面:

    • 定义实例 $data 和 $props 属性的存取器
    • 定义实例的 $set、$delete 方法,具体实在定义在观察者模块中
    • 定义实例的 $watch 方法

    到这里,关于状态初始化的部分就探索完毕了,接下来要继续研究另一个与开发过程紧密关联的部分——虚拟节点和模板渲染。


    状态初始化是与我们在开发的时候最息息相关的部分,在创建实例对象的配置对象中,我们设置了这些属性和方法,实例初始化的过程中对这些传入的配置进行了很多预先的处理,这就是状态初始化背后的逻辑。在探索到这一部分的时候才真正的感到,终于与平时的开发关联起来了。

    原文地址:https://segmentfault.com/a/1190000017006161

    展开全文
  • vue

    2021-01-07 19:18:56
    Vue.js 简介 1.Vue (读音 /vjuː/,类似于 view)的简单认识 (1)Vue是一个渐进式的框架,什么是渐进式的呢? 渐进式意味着你可以将Vue作为你应用的一部分嵌入其中,带来更丰富的交互体验。...状态管理 虚拟DOM

    Vue.js

    简介

    1.Vue (读音 /vjuː/,类似于 view)的简单认识

    (1)Vue是一个渐进式的框架,什么是渐进式的呢?

    • 渐进式意味着你可以将Vue作为你应用的一部分嵌入其中,带来更丰富的交互体验。
    • 或者如果你希望将更多的业务逻辑使用Vue实现,那么Vue的核心库以及其生态系统。
    • 比如Core+Vue-router+Vuex,也可以满足你各种各样的需求。

    (2)Vue有很多特点和Web开发中常见的高级功能

    • 解耦视图和数据

    • 可复用的组件

    • 前端路由技术

    • 状态管理

    • 虚拟DOM

    • Vue.js 是目前最火的一个前端框架,React是最流行的一个前端框架(React除了开发网站,还可以开发手机App, Vue语法也是可以用于进行手机App开发的,需要借助于Weex)

    • Vue.js 是前端的主流框架之一,和Angular.js、React.js 一起,并成为前端三大主流框架!

    • Vue.js 是一套构建用户界面的框架,只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。(Vue有配套的第三方类库,可以整合起来做大型项目的开发)

    • 前端的主要工作?主要负责MVC中的V这一层;主要工作就是和界面打交道,来制作前端页面效果;

    提高开发效率的发展历程

    原生JS -> Jquery之类的类库 -> 前端模板引擎 -> Angular.js / Vue.js/React.js

    1. jquery解决了兼容问题,但不能很好的操纵dom元素,比如表格,列表的生成需要不断地拼接

    2. 所以后来出现了模板引擎很方便的生成dom元素,但该操作是整体性的,并不能单个操作,如果少量数据出错又要重新渲染,消耗性能

    3. 所以出现了angular和vue框架,减少不必要的dom操作,提高渲染效率,

    4. vue还提出了双向数据绑定的概念:通过框架提供的指令,我们只需要关心数据的业务逻辑,不再关心DOM是如何渲染;

    框架和库的区别

    框架:是一套完整的解决方案,对项目的侵入性很大,项目如果需要更换框架,则需要重新架构整个项目;

    (插件):提供某一个小功能,对项目的侵入性小,如果某些库无法完成某些功能,可以很容易切换到其他库实现需求;

    基本使用

    使用一个框架,我们第一步要做什么呢?安装下载它

    安装Vue的方式有很多:

    方式一:直接CDN引入
    你可以选择引入开发环境版本还是生产环境版本

    <!-- 开发环境版本,包含了有帮助的命令行警告 --> 
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <!-- 生产环境版本,优化了尺寸和速度 -->
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    1234
    

    方式二:下载和引入

    开发环境 https://vuejs.org/js/vue.js

    生产环境 https://vuejs.org/js/vue.min.js

    方式三:NPM安装

    后续通过webpack和CLI的使用,我们使用该方式。

    MVVM

    注:本文多数内容属于Vue2.6之前的内容,只有较为重要的地方才会补充2.6版本之后的内容,望周知。

    1、Vue中的MVVM

    (1)什么是MVVM呢?

    通常我们学习一个概念,最好的方式是去看维基百科(对,千万别看成了百度百科)
    https://zh.wikipedia.org/wiki/MVVM
    维基百科的官方解释,我们这里不再赘述。

    (2)Vue的MVVM

    在这里插入图片描述

    View层:

    • 视图层
    • 在我们前端开发中,通常就是DOM层。
    • 主要的作用是给用户展示各种信息。

    Model层:

    • 数据层
    • 数据可能是我们固定的死数据,更多的是来自我们服务器,从网络上请求下来的数据。
    • 在我们计数器的案例中,就是后面抽取出来的obj,当然,里面的数据可能没有这么简单。

    VueModel层:(Vue的实例(监听DOM和数据绑定操作))

    • 视图模型层
    • 视图模型层是View和Model沟通的桥梁。
    • 一方面它实现了Data Binding,也就是数据绑定,将Model的改变实时的反应到View中
    • 另一方面它实现了DOM Listener,也就是DOM监听,当DOM发生一些事件(点击、滚动、touch等)时,可以监听到,并在需要的情况下改变对应的Data。

    如果数据变化了,视图就会改变,改变视图,数据也会变化,就通过Vue来连接这种关系,也是一种方式,也是一种模式,MVVM模式

    三种引入方式

    1.通过CDN在线引入vue的方式来实现Vue的相关操作

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y4bZQl9J-1610018237175)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915184852495.png)]

    2.github搜索Vue,然后找到源码,找到里面的dist目录,下载里面的vue.js文件,通过本地引入

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NSQdTDPz-1610018237179)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915184942870.png)]

    3.通过脚手架的方式进行Vue的开发操作,暂时不讲,Vue课程第二天再使用并进行讲解

    名词解释

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4QsjuWco-1610018237185)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200909200304053.png)]

    vue指令

    v-bind 强制数据绑定

    强制数据绑定:标签上的一些属性的值能够动态的进行操作,但是需要Vue中的指令来实现

    强制数据绑定的指令,可以用来为某个属性动态的绑定数据

    <input type="text" v-bind:value="val" />
    <p v-bind:text="tt">这是一个p标签</p>
    <!--强制数据绑定的这个指令的简写方式:     :属性名字="表达式"-->
    <p :text="tt">这是一个p标签</p>
    插图片
     <img :src="carousel.imgUrl" alt />
    

    为什么要有强制数据绑定? Vue搭建界面,离不开操作html标签,标签就有属性,属性中的值如果是动态的,那么操作起来会非常的方便,但是有很多的属性就是普通的标签的普通的属性,希望普通的属性中的数据也可以是动态的

    v-on 绑定事件监听

    绑定事件监听,v-on:事件名字=“回调函数”

    绑定事件监听的简写方式: @事件名字=“回调函数”

    <!--绑定事件监听,v-on:事件名字="回调函数"-->
    <button v-on:click="showMsg1">华哥说:</button>
    <button v-on:click="showMsg2">静哥说:</button>
    <button v-on:click="showMsg3">超哥说:</button>
    <hr>
    <!--绑定事件监听的简写方式:  @事件名字="回调函数"  -->
    <button @click="showMsg4">一个小故事</button>
    

    有时候后面不一定是回调函数,也有可能是表达式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40ZRamCf-1610018237188)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915190047501.png)]

    v-model 双向数据绑定

    双向数据绑定指令,v-model=“表达式”,一般用在表单标签中, 相当于:value和input事件的配合

    需求: 文本框中输入内容后,p标签中的内容会随时自动的变化

    <!--双向数据绑定指令,v-model="表达式",一般用在表单标签中-->
    <input type="text" v-model="msg" />
    <p>{{msg}}</p>
    

    v-model 是v-bind和v-on 的结合

    v-if

    v-if和v-else通常都是配合使用,

    v-if可以单独使用

    使用了v-if指令或者v-else指令的标签,如果为true,则该标签在DOM树存在,如果表达式的值是false,当前的这个标签在DOM树是不存在的

    <p v-if="isSeen">我喜欢你</p>
    <p v-else>你喜欢我</p>
    

    v-if指令可以配置v-else-if指令使用

    <button @click="score='C'">设置级别,显示对应的分数段</button>
    <p v-if="score==='A'">90100分之间</p>
    <p v-else-if="score==='B'">8090分之间</p>
    <p v-else-if="score==='C'">7080分之间</p>
    <p v-else-if="score==='D'">6070分之间</p>
    <p v-else>不及格</p>
    

    v-show

    v-show指令,可以设置标签的显示和隐藏,几乎和v-if类似,但是,区别在于使用v-show指令的标签,无论是显示还是隐藏,在DOM树中始终存在,v-if(v-else,v-else-if)指令的标签,有可能在DOM树中不存在

    另外使用v-show指令的标签主要是通过style属性中的display属性来控制其显示或者隐藏

    <!--方式2:使用v-show指令实现-->
    <button @click="isShow=!isShow">切换显示效果</button>
    <p v-show="isShow">我想你啊</p>
    

    v-if和v-show

    v-if和v-show都可以决定一个元素是否渲染,那么开发中我们如何选择呢?

    • v-if当条件为false时,压根不会有对应的元素在DOM中。
    • v-show当条件为false时,仅仅是将元素的display属性设置为none而已。

    开发中如何选择呢?

    • 当需要在显示与隐藏之间切片很频繁时,使用v-show
    • 当只有一次切换时,通过使用v-if

    v-for:遍历数据的指令

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AlmVAJb4-1610018237190)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915194853750.png)]

     v-for指令:
          语法1:
           v-for="(表达式1,表达式2) in 数组"
           表达式1---->数组中每一个项--->数组元素
           表达式2---->数组中的索引
          语法2:
           v-for="表达式 in 数组"
           表达式---->数组中每一个项----数组元素
    
          语法3:
           v-for="(表达式1,表达式2,表达式3) in 对象"
           表达式1--->值
           表达式2--->键
           表达式3--->索引
    

    :key=“表达式” 该表达式一般都是唯一的值,主要是用来标识该标签的唯一性

    ​ 涉及到渲染虚拟DOM的时候效率的问题

    ​ 虚拟DOM:—Vue的源码分析就清楚了

    ​ 渲染:把虚拟DOM展示在界面中,变成了真实的DOM

    ​ :key=""值最好是使用唯一的标识值,如果仅仅是遍历数组展示数据信息,此时使用索引是可以的(一旦修改数组中的数据,或者排序操作,或者删除数据,那么此时不推荐使用索引,还是推荐使用唯一标识)

    v-text和v-html

    <p v-text="content">{{content}}</p>
    <a v-html="content">百度</a>
    

    参照innerHTML 和innerText

    v-bind绑定class

    绑定class有两种方式:

    • 对象语法
    • 数组语法

    (1)绑定方式:对象语法

    • 对象语法的含义是:class后面跟的是一个对象。

    对象语法有下面这些用法:

    一般意为当前的h2标签的class是否要应用active这个类样式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tXnN4Yjg-1610018237192)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915193618852.png)]

    绑定方式:数组语法

    • 数组语法的含义是:class后面跟的是一个数组。

    数组语法有下面这些用法:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EhbsMxKS-1610018237195)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915194118562.png)]

    1. 在Vue中如何去修改或者操作DOM标签的样式
    2. 操作class属性来设置或者修改标签样式
    • 可以直接使用表达式的方式: 如: :class=“表达式” —>:class=“myClass”
    • 使用对象的方式: 如: :class="{类样式名字:表达式}"—>:class="{cls:isFlag}"
    • isFlag是布尔类型,为false,表示该标签不应用该样式,为true,表示该标签应用该样式
    • 使用数组的方式: 如: :class="[表达式1,表达式2,表达式3]"—>:class="[clsA,clsB,clsC]"
    • 使用静态和动态结合的方式 如: class=“cls1” :class=“clsA”
    • 使用静态绑定 :class="[‘cls1’,‘cls2’,‘cls3’]"----知道有这么个写法

    ​ 第一种和第二种和第四种用法比较多

    v-bind绑定style

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tFNXnhg-1610018237197)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915194439830.png)]

    Vue中style属性操作标签的样式的方式:

    • 键值对的方式:----> :style="{color:fontColor,fontSize:fontSize}"
    • 数组的方式:-----> :style="[fontColor1,fontSize2]"

    v-slot 插槽

    什么是插槽?
    插槽(Slot)是Vue提出来的一个概念,正如名字一样,插槽用于决定将所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性
    插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制 如下

    这样两个插槽全都会使用父组件传的

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEWk2ame-1610018237199)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201105172148867.png)]

    具名插槽

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VcpRobnj-1610018237201)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201105175750030.png)]

    从 vue@2.6.x 开始,Vue 为具名和范围插槽引入了一个全新的语法,即我们今天要讲的主角:v-slot 指令。目的就是想统一 slot 和 slot-scope 语法,使代码更加规范和清晰。既然有新的语法上位,很明显,slot 和 scope-slot 也将会在 vue@3.0.x 中彻底的跟我们说拜拜了。而从 vue@2.6.0 开始,官方推荐我们使用 v-slot 来替代后两者。

    跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header:

    v-slot的出现是为了代替原有的slot和slot-scope
    简化了一些复杂的语法。
    一句话概括就是v-slot :后边是插槽名称,=后边是组件内部绑定作用域值的映射。

    作用域插槽

    父组件向子组件传递一个数组,子组件把遍历后的数组内容通过插槽slot(自定义属性方式)再传给了父组件

    子组件遍历自己组件的数据再通过插槽给父组件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rBcsRG1-1610018237212)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201105183130299.png)]

    vue自定义指令

    Vue提供了自定义指令的方法

    ​ 1.注册全局指令

    ​ Vue.directive(‘指令名字’,function(el,binding){})

    ​ 2.注册局部指令

    ​ 在Vue的配置中

    directives:{
    	'指令名字'(el,binding){
    		‘指令要干的事’
    	}
    }
    

    ​ * el:element—指令属性所爱的标签对象

    ​ * binding:包含指令相关数据的对象容器,里面有value值就是标签中的值

    ​ * 区别:全局的指令作用范围更大,局部的指令只能在自己的包裹的标签中使用

    指令名字定义的时候不用V-,使用时要加V-

    全局指令都能用,局部指令在哪个vue实例里面写的,只有这个实例对应的容器内能用

    computed 计算属性

    计算属性无非就是某个属性的值发生了变化,相关联的属性的数据值也会自动的发生变化

    计算属性的setter和getter

    每个计算属性都包含一个getter和一个setter

    • getter(return)读取数据。
    • 在某些情况下,你也可以提供一个setter方法(设置数据)。
    • 如果没有set的话默认是get

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BdzYMjNh-1610018237213)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915192009991.png)]

    我们可能会考虑这样的一个问题:

    • methods和computed看起来都可以实现我们的功能,
    • 那么为什么还要多一个计算属性这个东西呢?
    • 原因:计算属性会进行缓存,如果多次使用时,计算属性只会调用一次。

    computed区别于method的核心

    在官方文档中,强调了computed区别于method最重要的两点

    1. computed是属性调用,而methods是函数调用
    2. computed带有缓存功能,而methods不是
    3. computed定义的方法我们是以属性访问的形式调用的,{{computedTest}}
    4. 但是methods定义的方法,我们必须要加上()来调用,如{{methodTest()}}否则,视图会出现test1的情况,见下图img
    5. 我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 text 还没有发生改变,多次访问 getText 计算属性会立即返回之前的计算结果,而不必再次执行函数。而方法只要页面中的属性发生改变就会重新执行
    6. 对于任何复杂逻辑,你都应当使用计算属性
    7. computed依赖于data中的数据,只有在它的相关依赖数据发生改变时才会重新求值

    一般来说需要依赖别的属性来动态获得值的时候可以使用 computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch。

    Vue-watch 监视

    watch: {}用来监听数据的改变,每当被监听的数据改变时,就会执行对应的方法。一般用来监听路由路径的改变。

    注意

    watch中监视的变量发生改变时,执行的方法没有返回值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dfwlvJ3-1610018237214)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915192719138.png)]

    deep

    如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true

    watch: {
     a: {
       deep: true,
       handler(newVal) {
         console.log(newVal)
       }
     }
    }
    

    侦听开始回调立刻调用一次,不用等数据变化

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P0Anznd6-1610018237216)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200928184817861.png)]

    vue事件参数对象的获取

    <div id="app">
      <h2>1.事件监听</h2>
      <button @click="show1('哈哈')">按钮1</button>
      <button @click="show2('嘎嘎',$event)">按钮2</button>
      <button @click="show3($event)">按钮3</button>
    </div>
    <script type="text/javascript">
    
      const vm = new Vue({
        el: '#app',
        methods: {
          show1(txt) {
            console.log(txt)
          },
          show2(txt, event) {
            console.log(txt, event)
          },
          show3(event) {
            console.log(event)
          }
        }
      })
    

    @click="showMessage2( e v e n t ) " 括 号 中 直 接 可 以 传 入 事 件 参 数 对 象 , event)" 括号中直接可以传入事件参数对象, event)",event是固定写法,传入的事件参数的位置,一定是在后面

    如果想要在Vue实例对象的某些相关方法中使用到事件参数对象(前提是在html模版中已经传了其他的参数了,或者没有传,但是还要想使用事件参数对象)就需要在html模版中的某个事件的回调函数中传入$event

    此时在方法中进行接收该参数,可以直接使用该事件参数对象

    v-on指令高级使用-事件修饰符

    • .stop - 调用 event.stopPropagation()
    • .prevent - 调用 event.preventDefault()
    • .capture - 添加事件侦听器时使用 capture 模式。
    • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
    • .native - 监听组件根元素的原生事件。
    • .once - 只触发一次回调。
    • .left - (2.2.0) 只当点击鼠标左键时触发。
    • .right - (2.2.0) 只当点击鼠标右键时触发。
    • .middle - (2.2.0) 只当点击鼠标中键时触发。
    • .passive - (2.3.0) 以 { passive: true } 模式添加侦听器
    <!-- 方法处理器 -->
    <button v-on:click="doThis"></button>
    
    <!-- 动态事件 (2.6.0+) -->
    <button v-on:[event]="doThis"></button>
    
    <!-- 内联语句 -->
    <button v-on:click="doThat('hello', $event)"></button>
    
    <!-- 缩写 -->
    <button @click="doThis"></button>
    
    <!-- 动态事件缩写 (2.6.0+) -->
    <button @[event]="doThis"></button>
    
    <!-- 停止冒泡 -->
    <button @click.stop="doThis"></button>
    
    <!-- 阻止默认行为 -->
    <button @click.prevent="doThis"></button>
    
    <!-- 阻止默认行为,没有表达式 -->
    <form @submit.prevent></form>
    
    <!--  串联修饰符 -->
    <button @click.stop.prevent="doThis"></button>
    
    <!-- 键修饰符,键别名 -->
    <input @keyup.="onEnter">
    
    <!-- 键修饰符,键代码 -->
    <input @keyup.13="onEnter">
    
    <!-- 点击回调只会触发一次 -->
    <button v-on:click.once="doThis"></button>
    
    <!-- 对象语法 (2.4.0+) -->
    <button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
    

    .stop 阻止冒泡

    <div class="box" @click='div1'>
        <!-- 点击按钮是会触发事件冒泡 -->
        <!-- 冒泡机制,先触发按钮的点击事件,再触发div的点击事件 -->
        <!-- <input type="button" value="按钮" @click='btn'> -->
        <!-- .stop 阻止事件冒泡 -->
        <input type="button" value="按钮" @click.stop='btn'>
    </div>
    

    .self 事件在该元素本身时触发回调

    <!-- 点击按钮时会触发div的点击事件,所以div的点击事件是被动触发的 -->
    <!-- .self 只有事件在该元素本身时才会触发回调 -->
    <!-- 即只有点击div时才会触发div的点击事件,此时点击按钮时事件冒泡不触发 -->
    <div class="box" @click.self='div1'>
        <input type="button" value="按钮" @click='btn'>
    </div>
    

    .stop和.self的区别

    <!-- .self只会阻止自己身上冒泡行为的触发,并不会真正阻止冒泡行为 -->
    <!-- 当点击按钮时依然会触发事件冒泡,但是会跳过div1,执行div2的点击事件 -->
    <div class="box2" @click='div2'>
        <div class="box" @click.self='div1'>
            <input type="button" value="按钮" @click='btn'>
        </div>
    </div>
    

    .stop是阻止事件冒泡,

    .self只会阻止自己身上冒泡行为的触发,并不会真正阻止冒泡行为

    .prevent 阻止事件默认行为

    <a href="http://www.baidu.com" @click.prevent='linkClick'>{{msg3}}</a>
    

    .capture 事件捕获模式

    <!-- .capture 添加事件侦听器时使用事件捕获模式 -->
    <!-- 事件捕获 先触发div的点击事件,再触发按钮的点击事件 -->
    <div class="box" @click.capture='div1'>
        <input type="button" value="按钮" @click='btn'>
    </div>
    

    .once 只触发一次事件处理函数

    1. v-once添加的元素,内部的胡子语法,只会解析一次,后续数据改变了不会触发更新
    2. 某些元素只需要解析一次,后续不需要更新,可以使用这个指令提升性能
    <!-- .once 只触发一次事件处理函数 -->
    <!-- 即阻止默认行为只会触发一次,当点击第二次时,.prevent不起作用,页面会跳转 -->
    <!-- 并且点击事件也只能触发一次,只会打印一次{{msg3}} -->
    <a href="http://www.baidu.com" @click.prevent.once='linkClick'>{{msg3}}</a>
    

    vue的生命周期/钩子函数

    可以理解vue生命周期就是指vue实例从创建到销毁的过程,在vue中分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/后。

    一、创建(实例)

    1、beforeCreate( 数据初始化之前):这个阶段实例已经初始化,只是数据观察与事件机制尚未形成,不能获取DOM节点(没有data,没有el)
    使用场景:因为此时data和methods都拿不到,所以通常在实例以外使用
    2、created( 数据初始化之后)实例已经创建,仍然不能获取DOM节点(有data,没有el)
    使用场景:模板渲染成html前调用,此时可以获取data和methods,so 可以初始化某些属性值,然后再渲染成视图,异步操作可以放在这里

    二、载入(数据)

    1、beforeMount(界面显示之前):是个过渡阶段,此时依然获取不到具体的DOM节点,但是vue挂载的根节点已经创建(有data,有el)
    2、mounted(界面渲染后)数据和DOM都已经被渲染出来了
    使用场景:模板渲染成html后调用,通常是初始化页面完成后再对数据和DOM做一些操作,需要操作DOM的方法可以放在这里

    三、更新

    1、beforeUpdate(界面更新前):检测到数据更新时,但在DOM更新前执行
    2、updated(界面更新后):更新结束后执行
    使用场景:需要对数据更新做统一处理的;如果需要区分不同的数据更新操作可以使用$nextTick

    四、销毁

    1、beforeDestroy(组件销毁前):当要销毁vue实例时,在销毁前执行
    2、destroyed(组件销毁后):销毁vue实例时执行

    面试题

    1、什么是 vue 生命周期?有什么作用?

    答:每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做 生命周期钩子 的函数,这给了用户在不同阶段添加自己的代码的机会。(ps:生命周期钩子就是生命周期函数)例如,如果要通过某些插件操作DOM节点,如想在页面渲染完后弹出广告窗, 那我们最早可在mounted 中进行。

    2、created和mounted的区别

    • created一般是在html渲染前的操作,此时el还是undefined,data已经存在。这里不能对dom进行操作。
    • mounted一般是在html渲染完成后的操作,此时el,data都已经加载完成,一般对dom的操作都写在mounted中,例如获取innerHTML,初始化echarts的时候。

    3、第一次页面加载会触发哪几个钩子?

    答:beforeCreate, created, beforeMount, mounted

    4、简述每个周期具体适合哪些场景

    答:

    • beforeCreate:在new一个vue实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没创建。在beforeCreate生命周期执行的时候,data和methods中的数据都还没有初始化。不能在这个阶段使用data中的数据和methods中的方法
    • create:data 和 methods都已经被初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作
    • beforeMount:执行到这个钩子的时候,在内存中已经编译好了模板了,但是还没有挂载到页面中,此时,页面还是旧的
    • mounted:执行到这个钩子的时候,就表示Vue实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。 如果我们想要通过插件操作页面上的DOM节点,最早可以在和这个阶段中进行
    • beforeUpdate: 当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的, 页面还没有和最新的数据保持同步
    • updated:页面显示的数据和data中的数据已经保持同步了,都是最新的
    • beforeDestory:Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于可用状态。还没有真正被销毁
    • destroyed: 这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于不可用状态。组件已经被销毁了。

    5、vue获取数据在哪个周期函数

    答:一般 created/beforeMount/mounted 皆可。
    比如如果你要操作 DOM , 那肯定 mounted 时候才能操作。

    vue transition 过渡

    过渡效果无非就是从隐藏到显示,从显示到隐藏,有一些淡入淡出的效果,通过css来实现

    从隐藏到显示,三个阶段
    		.fade-enter  开始阶段
    		.fade-enter-active 过渡阶段
    		.fade-enter-to  结束阶段
    
    		从显示到隐藏,三个阶段
    		.fade-leave  开始阶段
    		.fade-leave-active 过渡阶段
    		.fade-leave-to 结束阶段
    		//从隐藏到显示开始和从显示到隐藏的结束,样式一样可以合写
          .fade-enter,
          .fade-leave-to {
              opacity: 0;
          }
    
          .fade-enter-active,
          .fade-leave-active {
              transition: all 5s;
          }
    

    自带类过渡动画<transition></transition>,如果是列表即用v-for则用<transitiongroup></transitiongroup>

    两个大阶段:v-enter-activev-leave-active
    进入阶段分为v-enter进入前,v-enter-to进入后。
    离开阶段分为v-leave离开前,v-leave-to离开后。

    如果是自定义过渡动画格式,在<transition name ="my">,style中fade-就变成了my-

    vue过滤器filter

     <div id="app">
            <h2>显示格式化的日期时间</h2>
            <h2>{{time}}</h2>
            <h2>{{time|filter1}}</h2>
            <h2>{{time|filter2('YYYY-MM-DD')}}</h2>
            <h2>{{time|filter3}}</h2>
     </div>
    <script src="https://cdn.bootcdn.net/ajax/libs/moment.js/2.24.0/moment.js"></script>
    <script>
        //value 就是time作为参数传进去
        Vue.filter("filter1", function (value) {
            return moment(value).format('YYYY-MM-DD hh:mm:ss')
        })
        //可以自己书写形式传进去
        Vue.filter("filter2", function (value, filterString) {
            return moment(value).format(filterString)
        })
        //防止没传,自己设置个默认的形式
        Vue.filter("filter3", function (value, filterString = 'YY-MM-DD') {
            return moment(value).format(filterString)
        })
        const vm = new Vue({
            el: '#app',
            data: {
                time: Date.now()
            }
        })
    </script>
    

    vue组件通信

    组件之间的数据传递

    事件总线实现任意组件通信

    一、简介:

    先说一下什么是事件总线,其实就是订阅发布者模式;

    比如有一个bus对象,这个对象上有两个方法,一个是on(监听,也就是订阅),一个是emit(触发,也就是发布),我们通过on方法去监听某个事件,再用emit去触发这个事件,同时调用on中的回调函数,这样就完成了一次事件触发;

    在vue被实例化之后,他就具备了充当事件总线对象的能力,在他上面挂了两个方法,是 e m i t 和 emit和 emiton;

    而vue文档说的很明白,$emit会触发当前实例上的事件,附加参数都会传给监听器回调;

    二、实现全局事件总线对象

    bus可以实现兄弟传值,跨级

    理解

    前提:所有的组件全都直接或则简介继承自Vue.

    bus是个vue的实例且在原型上,(带 几 乎 都 是 实 例 的 方 法 ) , 所 以 b u s 可 以 用 实 例 的 方 法 ‘ 几乎都是实例的方法),所以bus可以用实例的方法` ),buson$emit`

    **又因为bus在原型上,组件继承自Vue,所以所有的组件都可以.bus**

    //在mian.js中
    Vue.prototype.bus = new Vue()  //这样我们就实现了全局的事件总线对象,bus是个vue的实例,且在原型上
     
    //组件A中,监听事件(带$都是实例的方法几乎)
    this.bus.$on('updata', function(data) {//回调里可以调用方法,也可以把回调本身设置成方法
        console.log(data)  //data就是触发updata事件带过来的数据
    })
     
    //组件B中,触发事件
    this.bus.$emit('updata', data)  //data就是触发updata事件要带走的数据
    
    
    
    // 也可以写在生命周期函数里(main.js)
    new Vue({
        beforeCreate() {
            // 事件总线的方式
            Vue.prototype.$bus = new Vue()
        },
        // 渲染App组件
        render: h => h(App),
        // 注册路由
        router,
        store // 注册vuex仓库
    }).$mount('#app') // 相当于el:'#app'
    

    每个组件在销毁时连同事件也要销毁,不然它会在你看不到的地方继续执行而难以被发现

    父子组件通信prop

    通过props向子组件传递数据

    1. 在父组件的模板中将数据用单项数据绑定的形式,绑定在子组件身上

      <Son :money = "money"/>
      
    2. 在子组件的配置项中可以使用一个props配置项来接收这个数据,接收时,props的取值可以是一个数组

      Vue.component('Son',{
        template: '#son',
        //方法一
        props: ['money']
        //方法二
        props: {
        	todo:Array,
        	show:Function,
        	todos:Object
      	}
        //方法三
        props: {
          addTodo: {
          type: Function, // 类型
          required: true, // 必须的
          },
        },
        
      })
      
    3. 在子组件模板中,接收到的属性可以像全局变量一样直接使用
      <p> 父亲给了我 {{ money }} 钱 </p>

    $emit子—父通信(自定义事件)

    通过事件向父组件发送消息

    流程

    在子组件中通过$emit()来触发事件

    // $emit()用来分发事件的
    // 参数1:事件类型(事件名字)
    // 参数2:事件的回调函数所需的参数
    this.$emit('addTodo',todo)
    

    在父组件中,通过v-on来监听子组件事件

     <Header @addTodo="addTodo" />
     
    methods: {
      // 添加数据的方法
      addTodo(todo) {
        this.todos.unshift(todo)
      },
     }
    

    pubsub.js(消息订阅与发布)

    也可以实现兄弟传值,跨级

    方法原理和$bus类似

    使用1,npm install pubsub-js (下载)

    ​ 2,组件里引入 import PubSub from ‘pubsub-js’

    // 消息订阅(接收数据)
    // 参数1:消息名字
    // 参数2:回调函数,msg---消息名字,data---->该消息发布的时候传入的参数
    // 返回值是该消息的标识
    this.token = PubSub.subscribe('toggleTodo', (msg, data) => {  //类似于bus.$on
      // 回调里执行需要执行的方法
      this.toggleTodo(data)
    })
    
    // 取消订阅
    PubSub.unsubscribe(this.token)  //根据标识取消
    PubSub.unsubscribe() //全部取消
    PubSub.unsubscribe('toggleTodo')//根据消息名字取消
    
    //消息发布(传递数据)
    PubSub.publish('toggleTodo', this.todo);//这个类似与bus.$emit 参数1事件名字,参数2需要的参数  异步发布
    PubSub.publishSync('toggleTodo', this.todo)  //同步发布
    

    总结

    props: 父子组件通信

    * 自定义事件:父子组件通信

    * 事件总线($bus):任意组件通信

    * 消息订阅(PubSub):任意组件通信,PubSub属于一个单独的插件(别人封装好的js库),不属于Vue

    * 插槽:相当于占位(挖坑)----填坑

    * Vuex(Vue中非常重点的)

    localStorage的存储,读取,删除

    localStorage存储
    我们通过以下方式将数据储存到localStorage中

    window.localStorage.setItem('key',value)
    

    但有时value为一个对象Object,以上面的方式写入,会出现读取的返回值为
    {object Object}的情况,但这并不是我们想要的,此时我们需要使用新的方式
    传入Object

    window.localStorage.setItem('param',JSON.stringify(Object))
    

    通过JSON.stringify(Object)方法将对象转化为一个json格式的字符串进行存储

    localStorage读取
    我们通过以下方式来读取localStorage中的值

    window.localStorage.getItem("key")
    

    相对的在读取json格式字符串只有我们也无法直接使用,需要将它转换为josn对象之后才是我们想要的结果,所以我们需要调用 JSON.parse()方法来进行转化,
    之后在继续使用

    JSON.parse(window.localStorage.getItem("key"))
    JSON.parse(window.localStorage.getItem("key")||{})
    

    localStorage删除
    我们通过以下方法来删除对应key以及key中的内容

    window.localStorage.removeItem('key')
    
    

    localStorage清空所有的key
    清空localStorage中所有的key;
    注意:请谨慎使用,它会清空所有的本地存储数据

    window.localStorage.clear()
    

    Vue-cli (脚手架2)安装

    cmd命令窗口中
     * node -v 版本
     * npm -v 版本
     * https://github.com/vuejs/vue-cli/tree/v2#vue-cli--
     * 安装脚手架的命令工具
     * npm install -g vue-cli
     * vue -V 版本
     * 
     * 以上 都是在cmd中执行的
     * 找到一个合适的目录中,打开命令窗口,进行下载操作
     * 下载脚手架对应的vue的项目(模版)
     * vue init webpack 项目名字
     * 
     * npm run dev  运行项目
     * 
     * npm run build 打包项目---将来在公司中开发完毕项目后,需要上线的时候,你的同事找你要的打包文件
     * 
     * serve dist 运行打包文件
    

    Vue-cli (脚手架2—脚手架4的过渡)

    /*
    1. 全局卸载电脑中脚手架2工具
    npm uninstall vue-cli -g
    
    2. 全局安装脚手架4
    npm install -g @vue/cli
    或者
    yarn global add @vue/cli
    
    3. 查看当前vue的版本
    vue -V
    
    4. 通过脚手架4创建新的项目模版
    vue create gshop_client
    
    5. 此时电脑中不能再使用脚手架2的命令来下载项目啦,肿么办,安装桥接工具
    npm install -g @vue/cli-init
    
    6. 此时电脑中脚手架2/4的命令都可以下载项目了
    
    7. npm run serve 运行项目
       npm run build 打包
       serve dist 运行打包
    
    
    脚手架2和脚手架4 的区别
    1) 目录及文件个数不同
    2) 脚手架4的 index.html 在public目录中
    3) 项目能够在浏览器中自动的打开, 在package.json中后面加--open
    "serve": "vue-cli-service serve --open"
    4) main.js中的创建Vue实例对象中的代码不同
    
    // 脚手架4中的代码
    new Vue({
      render: h => h(App),
    }).$mount('#app')
    
    // 脚手架2的项目中的代码
    new Vue({
      el:'#app',
      components:{App},
      template:'<App/>'
    })
    

    插件vue-resource

    1、体积小:vue-resource非常小巧,在压缩以后只有大约12KB,服务端启用gzip压缩后只有4.5KB大小,这远比jQuery的体积要小得多。

    2、支持主流浏览器:和Vue.js一样,vue-resource除了不支持IE 9以下的浏览器,其他主流的浏览器都支持

    3、支持Promise API和URI Templates:Promise是ES6的特性,Promise的中文含义为“先知”,Promise对象用于异步计算。 URI Templates表示URI模板,有些类似于ASP.NET MVC的路由模板

    4、支持拦截器:拦截器是全局的,拦截器可以在请求发送前和发送请求后做一些处理。 拦截器在一些场景下会非常有用,比如请求发送前在headers中设置access_token,或者在请求失败时,提供共通的处理方式。

    二、安装与引用

    NPM:npm install vue-resource --save-dev

    main.js中

    /*引入Vue框架*/
    import Vue from 'vue'
    // 引入App组件
    import App from './App.vue'
    /*引入资源请求插件*/
    import VueResource from 'vue-resource'
    
    /*使用VueResource插件*/
    Vue.use(VueResource)
    

    三、语法

    引入vue-resource后,可以基于全局的Vue对象使用http,也可以基于某个Vue实例使用http

    // 基于全局Vue对象使用http
    Vue.http.get('/someUrl', [options]).then(successCallback, errorCallback);
    Vue.http.post('/someUrl', [body], [options]).then(successCallback, errorCallback);
    
    // 在一个Vue实例内使用$http
    this.$http.get('/someUrl', [options]).then(successCallback, errorCallback);
    this.$http.post('/someUrl', [body], [options]).then(successCallback, errorCallback);
    

    在发送请求后,使用then方法来处理响应结果,then方法有两个参数,第一个参数是响应成功时的回调函数,第二个参数是响应失败时的回调函数。

    vue-router

    * vue-router 路由器:路由的管理工具

    * 路由:指的是一种映射关系,地址和组件的关系

    * 组件:具有特定功能效果的集合(html+css+js)

    * 组件:普通的组件和路由组件--------

    * 路由组件:普通组件通过注册和某个地址发生了关系,此时该组件就是路由组件

    * 地址:路由地址---->路由链接地址

    * 要想使用路由,必须要先注册路由,然后通过声明式路由或者编程式路由实现单页面应用

    * 声明式路由:路由链接(地址)和路由视图(展示某个组件内容的)组成

    * 编程式路由:通过js代码的方式来实现地址和组件的效果展示

    *

    * 普通的组件一般放在components目录中

    * 如果当前的组件是路由组件,一般推荐放在pages目录中(上班后看老大)

    是用来渲染通过路由映射过来的组件,当路径更改时, 中的内容也会发生更改,可以对应多个组件

    主要应用于单页面中,与router-link配合,渲染router-link 映射过来的组件。

    运作过程:就是几个跳转链接跳到对应的子页面(路由组件),程序运行的时候,会将子页面(路由组件)标签里面的内容都注入到App.vue页面中的router-view标签中,从而实现无刷新的路由跳转

    路由视图可以传递数据

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hvljoRB6-1610018237218)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919082722799.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9hOcptgZ-1610018237220)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919082820362.png)]

    router和route

    router里有路由跳转的push和replace方法

    route里则可以获取path(路由路径),params和query传递的参数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y1nE3SdF-1610018237222)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919082505519.png)]

    路由的使用

    * 路由的使用步骤:

    * 1. 安装路由器插件

    * npm install vue-router

    * 2. 引入路由器对象并实例化,而且需要在实例化的Vue中进行路由器的注册

    * 一般情况路由器的实例化和暴露会放在一个单独的目录中router目录,内部有一个index.js文件(名字可改)

    注册多个路由组件(新建单独文件一起注册然后暴露出去)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQpHhxy2-1610018237224)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200913200327467.png)]

    引入多个路由组件对象并注册(routes)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y3xXpMRp-1610018237226)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200913200503854.png)]

    把index引入到main.js里,router注册路由器(组件在main.js里注册了都会有 r o u t e 这 个 对 象 ( 里 面 有 p a r a m s 传 递 的 参 数 ) , 还 有 route这个对象(里面有params传递的参数),还有 route(params)router里有push,replace等方法)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jOKjZxfK-1610018237227)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200913200237953.png)]

    声明式路由跳转和编程式路由跳转

    声明式路由跳转就是router-link

    编程式就是绑定事件回调函数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XOzR3y6-1610018237229)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915211401221.png)]

    路由组件里面编程式路由跳转点击多次报错的解决

    编程式路由跳转点击多次会报错
    1.传入成功的回调
    this.$router.push('/search', () => {})
    2.传入成功和失败的回调
    this.$router.push(
      '/search',
      () => {},
      () => {}
    )
    3.传入失败的回调,成功的那个可以写null和undefined
    this.$router.push('/search', undefined, () => {})
    4.then和catch一起使用
    this.$router
      .push('/search')
      .then(() => {})
      .catch(() => {})
    5.只使用catch也可以解决问题
    this.$router.push('/search').catch(()=>{})
    
    

    ///治标不治本,如果有多个跳转的话每个都写太麻烦,所以我们可以重写router的push和replace方法

    在router文件夹下面的index.js里面,声明使用路由器插件前重写

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PPGPHYxT-1610018237232)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919002918320.png)]

    路由的params和query的传参

    params是路由的一部分,必须要在路由后面添加参数名。query是拼接在url后面的参数,没有也没关系

    params普通方式(单个)

    // params的方式
    // this.$router.push(`/search/${this.keyword}`)
    //  <router-link :to="'/detail/'+item.productId">
    

    params对象方式(多个)

    // params的对象方式传参(keyword与接收的地方要一致)
    // this.$router.push({ name: 'search', params: { keyword: this.keyword } })
    // <router-link :to="{name:'detail',params:{skuId:this.$route.query.skuId}}">查看商品详情</router-link>
    //一般会判断
    // 判断文本框中是否输入内容
    if (this.keyword) {
      this.$router.push({
        name: 'search', //跳转的地址,注册组件那边写一个对应的name属性
        params: {
          keyword: this.keyword,
        },
      })
    } else {
      this.$router.push({ name: 'search' })
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gs7o2vRs-1610018237233)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919003833660.png)]

    query普通方式(单个)

    // query的方式(?后面keyword可以随便写,内容是最后显示在地址栏上的)
    // this.$router.push(`/search?keyword=${this.keyword}`)
    

    query对象方式(多个)

    //query的对象方式传参(keyword可任意写,内容是最后显示在地址栏上的)
    // this.$router.push({ name: 'search', query: { keyword: this.keyword } })
    // this.$router.push({ path: '/search', query: { keyword: this.keyword } })
    
    //this.$router.push({ path: '/search', query: { keyword: this.keyword },params:{} }) 也可以
    

    query和params以对象的形式做路由跳转并传参的时候,query的方式可以使用path或者name,但是params的方式不能用path,可以用name

    还有一个meta对象也可以传

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VTdQVAaR-1610018237235)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919004530168.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XnbZbnXT-1610018237235)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200928184448024.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3zr759Nv-1610018237236)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919083035124.png)]

    props也可以路由传参

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TcydC8M8-1610018237239)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200928184402078.png)]

    需要组件里用props接收

    vue异步请求解决跨域

    脚手架项目模板下新建一个vue.config.js文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sxMtaOwk-1610018237241)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919004916084.png)]

    官方文档

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6i8Toor-1610018237242)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200919005031339.png)]

    如何区分是不是一个二级路由?

    嵌套的二级子路由应该显示在一级路由视图里面(一级路由视图里有route-view显示二级路由视图),应该具有相同的部分,如果是全新的页面,则不是二级路由,并不是在一个路由里面点击跳转到另一个页面,那么这个页面就是二级路由,这是不对的

    VueX

    Vuex:集中式的管理状态数据,是一种管理状态数据的模式,也是一种工具,也是一个对象

    Vuex的状态管理模式:通过actions来改变数据的状态,从而使界面发生变化

    Vuex的使用步骤

    1. npm install vuex 安装vuex

    2. 在src目录中新建一个目录: vuex目录/store.js文件(store目录/index.js),在公司开发名字看老大

    3. 在store.js文件中引入vue,引入vuex,声明使用vuex的插件,实例化Vuex的对象,并暴露出去,main.js中引入store对象,并注册store仓库对象

    4. 在实例化Vuex的对象的时候,需要初始化内部的配置对象,里面有state,mutations,actions,getters,(modules暂时可不写)

    五个核心概念

    state:包含了多个状态数据的对象(data里的数据)

    mutations:包含了多个直接修改状态数据的方法的对象,(mutation接受 state 作为第一个参数)

    • ​ mutations对象中的每个方法都可以叫mutation,
    • ​ 都是同步的代码

    actions:包含了多个间接修改状态数据的方法的对象

    • ​ actions对象中的每个方法都可以叫action
    • ​ 同步或者异步的代码
    • ​ Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个mutation(commit之前可以修改返回的数据)
    • 可以将commit解构赋值出来

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3NIaIpH-1610018237244)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915212921852.png)]

    getters:包含了多个状态数据的计算属性的GET方法的对象(Getter 接受 state 作为其第一个参数)

    modules:当项目庞大,状态非常多时,可以采用模块化管理模式。Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A4yTH6Y5-1610018237246)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200920212558593.png)]

    辅助函数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wLim3Uo9-1610018237247)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200920212329424.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q51PL2xS-1610018237248)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200920212400629.png)]

    一般用法:

    在src下建立store文件夹,文件夹里建一个index.js文件(引入vue vuex 声明使用vuex ,然后实例化Vuex并且暴露出去)

    将五个核心概念对象分别写在五个JS文件中,通过export default {},将对象暴露出来

    index.js里面引入各个暴露对象的JS文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F646SY75-1610018237250)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915231237892.png)]

    组件里面通过commit---->mutations里的方法,dispatch—>actions里的方法

    dispatch第二个参数可以传参

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cbtdSyjb-1610018237251)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200915232020617.png)]

    流程:

    一般组件里通过触发dispatch—>vuex里的actions,actions—>mutations里的方法,mutation改变了数据状态反映到state里,然后state再渲染到页面。

    (有时候也可以直接commit找mutations)

    组件里可以通过 s t o r e . s t a t e . x x x , store.state.xxx, store.state.xxx,store.getters.xxx,拿到state对象和getters对象里的数据

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sj5wph0V-1610018237253)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200914181452324.png)]

    图片懒加载

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e58MLoeH-1610018237256)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200929184353131.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XTAlg9tT-1610018237258)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20200929184503054.png)]

    v-lazy指令使用

    Vue组件通信(高级)

    Vue中组件通信的方式(普通的):

    \1. props: 父子组件之间通信,可以传递动态的属性或者是回调函数

    \2. 自定义事件:父子组件之间通信,事件

    \3. 事件总线:任意组件之间进行通信,本质:原型

    \4. PubSub消息订阅:不属于Vue,单独的一个插件(React中同样可以使用),任意组件通信

    \5. 插槽(普通插槽:没有名字,具名插槽:有名字,作用域插槽):父子组件通信

    \6. vuex:任意组件通信

    Vue中组件通信的方式(高级的):

    自定义事件

    自定义事件和原生事件的区分

    • 原生事件和自定义事件,主要是针对组件而言的,组件中可以使用原生事件,也可以使用自定义事件
    • 原生事件:系统自带的,事件绑定了回调函数后,事件一旦触发,对应的回调函数中的代码就会自动的执行
    • 自定义事件:自己定义的事件
    • 组件中原生事件:使用了系统自带的事件,并且使用了.native进行修饰
    • 组件中自定义事件:自己定义的或者使用了系统自带的事件,但是没有使用.native进行修饰
    • 组件中的原生事件,最终事件给了子级组件中的最外层的html标签了(使用了事件委托的方式实现的)
    • 组件中的自定义事件:是子级组件内部手动的分发父级组件传递过来的自定义事件,才能够正常的使用

    组件中使用了自定义事件,但是又使用了.native进行了修饰,此时到底是原生事件还是自定义事件?

    自定义事件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OR0JheW4-1610018237261)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009211139994.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q0HSw7xv-1610018237262)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009211249581.png)]

    组件中传递自定义事件的方式:可以实现父子组件,子父组件进行通信

    v-model指令实现组件通信(本质:value属性和input事件)

    当一个组件中使用了v-model指令,也就意味着,向这个子级组件传递了value属性和input事件,子级组件中可以接收并使用value属性的数据,同时也可以分发input自定义事件,最终可以实现父子,子父组件的通信

    子级组件需要props接收value,并且分发input事件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XOyhvW7L-1610018237264)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009211651903.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ggFJUEZ-1610018237267)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009211758748.png)]

    .sync修饰符实现父子组件通信

    .sync的本质:向子级组件内部动态传递数据和自定义的updata事件,子级内部分发父级组件传递的自定义的update事件,从而实现数据更新,其实是:父子组件通信

    父向子传递动态属性数据,子需要直接修改该数据并传给父组件,此时用.sync

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cXd2bza-1610018237269)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009212042907.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgeDme8i-1610018237270)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009212234133.png)]

    .sync可以用来实现 子组件修改父组件的数据

    在父组件中通过 :属性名.sync = 数据

    然后在子组件中通过 $emit(update:属性名,修改后的数据)来实现子组件修改 父组件的数据

    a t t r s 和 attrs和 attrslisteners实现组件通信

    a t t r s : 父 级 组 件 向 子 级 组 件 中 传 递 的 所 有 的 属 性 都 在 attrs: 父级组件向子级组件中传递的所有的属性都在 attrs:attrs中,(class和stlye和props接收的属性)都不会在$attrs中存在

    $listeners:父级组件向子级组件中传递的所有的事件,(.native修饰符的原生事件)除外,

    v-bind:可以绑定对象的形式

    v-on:可以绑定对象的形式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BCS5yDbX-1610018237272)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009212547299.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2tucjx7n-1610018237274)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009212616970.png)]

    $children 和 $parent 实现组件通信

    $children:可以获取当前的父级组件中的直接子级组件(间接的父子关系的组件是不行的)

    $parent:可以获取当前组件的直接父级组件,要有才可以

    父组件定义事件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m7OYSzdF-1610018237276)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009221628615.png)]

    遍历所有子组件,调用子组件内的borrowMoney方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uq2zVHNM-1610018237277)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009221817267.png)]

    子组件定义点击事件改变父组件数据

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xavolGTd-1610018237279)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009222019350.png)]

    获取父级组件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y25KQV7s-1610018237283)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009222131193.png)]

    作用域插槽:可以实现父子,子父组件传递数据,组件通信

    父组件向子组件传递一个数组,子组件把遍历后的数组内容通过插槽slot(自定义属性方式)再传给了父组件

    父组件通过slot-scope="scope"与v-slot:default="scope"接收子组件传过来的数据对象

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PQUDMXeC-1610018237284)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009221226095.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fry1mGoU-1610018237285)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201009213222272.png)]

    slot-scope="scope"与v-slot:default="scope"是一样的

    this.$nextTick

    this.$nextTick()将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

    由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了 Vue.nextTick()方法。

    简单的理解是:当数据更新了,在dom中渲染后,自动执行该函数,

    应用场景:在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

    通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它

    1.在Vue生命周期的created()钩子函数进行DOM操作一定要放到Vue.nextTick()的回调函数中。

    2.在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

    this.$nextTick(()=>{
    
    })
    

    路由的懒加载

    // 引入Home组件
    // import Home from '@/pages/Home'
    //路由的懒加载
    const Home = () => import('@/pages/Home')
    

    正则

    const xxx= 正则
    if(xxx.test(value)) value需要验证的
    

    拿到vueX里面的数据遍历

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ErSYeKqh-1610018237287)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201117014402789.png)]

    控制单选多选,

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3QaLkqI-1610018237289)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201117014617697.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TeOPWL1U-1610018237291)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201117014709707.png)]

    methods

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-smXi4QIE-1610018237293)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201117014939533.png)]

    Getters

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bYCXo660-1610018237295)(C:\Users\宋文现\AppData\Roaming\Typora\typora-user-images\image-20201117015114727.png)]

    mockjs

    mock数据
    模拟数据,拦截Ajax请求,然后模拟数据返回
    npm i mockjs
    新建mock文件夹,里面index.js
    1 先引入Mock
    2.引入模板数据 (自己写的json数据)json文件存数据 {} Key为字符串,到时候放入data作为数据返回
    Mock.mock(要拦截的Ajax地址,{code:200,message:‘成功’,data:数据 })
    有几个请求写几个Mock.mock

    地址 ’/mock/xxx‘ ,将来mock拦截的是以mock开头的地址

    3.main.js引入 import "./mock "

    然后接口请求地址就是你写的地址后面的xxx,ajax封装的baseURL:’/mock’(根路径)

    展开全文
  • 一丶首先在用户登录前后分别给出一个状态标识此用户是否登录(建议用vuex);import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex);var state = { token:0, //初始时候给一个 token=0 表示用户未登录};...
  • Vue

    2020-11-17 18:41:05
    Vue Vue是一个渐进式的框架。Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。...状态管理 虚拟DOM 一. 安装Vue.js cdn引用方
  • VUE

    2021-03-16 14:02:10
    VueCLI3--运行单个*.vue文件 1.安装全局扩展 npminstall-g@vue/cli-service-global 2.运行.vue文件 vueserve文件夹名 注意: 如果已经安装过可以直接运行vue serve 文件夹运行时, 会自动生成一个node_modules...
  •  一丶首先在用户登录前后分别给出一个状态标识此用户是否登录(建议用vuex);  简单用vuex表示一下,不会可以自己去官网多看看; import Vue from ‘vue‘ import Vuex from ‘vuex‘ Vue.use(Vuex); ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,793
精华内容 4,717
关键字:

vue状态标识

vue 订阅