Vue学习笔记

2022/1/1 vue

# 1、data

因为“数据驱动”所以开篇就讲它!

  • 类型:Object | Function
  • 限制:组件的定义只接受 function。

# 定义

Vue 实例的数据对象。Vue 会递归地把 data 的 property 转换为 getter/setter, 从而让 data 的 property 能够响应数据变化。 对象必须是纯粹的对象 (含有零个或多个的 key/value 对):浏览器 API 创建的原生对象,原型上的 property 会被忽略。 大概来说,data 应该只能是数据 - 不推荐观察拥有状态行为的对象。

详细说明

  • https://cn.vuejs.org/v2/guide/reactivity.html
  • https://cn.vuejs.org/v2/api/#data

# 重点关注

  • 一旦观察过,你就无法在根数据对象上添加响应式 property。因此推荐在创建实例之前,就声明所有的根级响应式 property
var vm = new Vue({
    data: {
        // 如果使用message则必须创建前声明,即便是一个空值
        message: ''
    },
    template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'
1
2
3
4
5
6
7
8
9
  • 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化所以处理对象和数组方法如下: 对于对象
// 使用$set添加属性,如data.someObject属性添加一个b字段
this.$set(this.someObject, 'b', 2)

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`直接重新赋值
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
1
2
3
4
5

对于数组是一样的,vue不能监听直接对某个数组下标元素赋值或者手动修改length属性

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

// 以上两类的替代方法
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
vm.items.splice(newLength)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 异步更新队列 (opens new window)

data变化后组件会在下一个事件循环“tick”中更新,为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

等价于下面的

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}
1
2
3
4
5
6
7
8

# 组件中使用data

当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。 如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象! 通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

var Component = Vue.extend({
  data: function () {
    return { a: 1 }
  }
})
1
2
3
4
5

# 2、computed

也可以看做有getter/setter的属性,不同于data的是,他更像是一个函数,通过对data数据等的计算返回新的值。

  • 类型:{ [key: string]: Function | { get: Function, set: Function } } 看定义知道他的语法如下:
var vm = new Vue({
    data: { a: 1 },
    computed: {
        // 仅读取,只有key 和 Function
        aDouble: function () {
            return this.a * 2
        },
        // 读取和设置,有key和getter/setter
        aPlus: {
            get: function () {
                return this.a + 1
            },
            set: function (v) {
                this.a = v - 1
            }
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3、method

  • 类型:{ [key: string]: Function }
  • demo:
var vm = new Vue({
  data: { a: 1 },
  methods: {
    plus: function () {
      this.a++
    }
  }
})
1
2
3
4
5
6
7
8

# 4、watch

  • 类型:{ [key: string]: string | Function | Object | Array }

  • 详细:

    一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property。

  • demo:

var vm = new Vue({
    data: {
        a: 1,
        b: 2,
        c: 3,
        d: 4,
        e: {
            f: {
                g: 5
            }
        }
    },
    watch: {
        // 最常用用法
        a: function (val, oldVal) {
            console.log('new: %s, old: %s', val, oldVal)
        },
        // 方法名
        b: 'someMethod',
        // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
        c: {
            handler: function (val, oldVal) { /* ... */
            },
            deep: true
        },
        // 该回调将会在侦听开始之后被立即调用
        d: {
            handler: 'someMethod',
            immediate: true
        },
        // 你可以传入回调数组,它们会被逐一调用
        e: [
            'handle1',
            function handle2(val, oldVal) { /* ... */
            },
            {
                handler: function handle3(val, oldVal) { /* ... */
                },
                /* ... */
            }
        ],
        // watch vm.e.f's value: {g: 5}
        'e.f': function (val, oldVal) { /* ... */
        }
    }
})
vm.a = 2 // => new: 2, old: 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  • 选项:deep
    为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。 -选项:immediate
    在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调。

# 5、created

在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。

# 6、mounted

实例被挂载后调用,这时 el 被新创建的 vm.el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.el 也在文档内。

注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick:

mounted: function () {
  this.$nextTick(function () {
    // 仅在整个视图都被渲染之后才会运行的代码
  })
}
1
2
3
4
5

# 7、Prop

  • 类型:Array<string> | Object
  • 说明:

props (opens new window) 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值。

  • 应用场景:

一个界面由多个组件组成,收集多个组件的数据来组成表单提交,可以使用props来传递数据,这样的好处是子组件、父组件、兄弟组件之间可以共用数据

# 8、vm.$root

以下所有的vm都代表当前vue实例对象,代码中使用this即可(箭头函数需要注意!)

  • 定义:当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
  • 作用:访问根组件实例
  • demo:
// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。

// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()
1
2
3
4
5
6
7
8
9
10
11

# 9、vm.$parent

  • 访问父级组件实例

和 root 类似,parent property 可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

# 10、vm.$refs

  • 一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
  • 用来访问子组件实例或子元素
  • demo:
// 子组件
<base-input ref="usernameInput"></base-input>

// 访问子组件
this.$refs.usernameInput
1
2
3
4
5

实际用例,触发子组件的表单校验

// 子组件定义
<template>
  <div>
    <el-form ref="form" :model="form" :rules="rules" label-width="80px" enctype="multipart/form-data">
      ....
    </el-form>
  </div>
</template>
<script>
export default {
  name: "Kid",
  data() {
    return {
      form: {
        // ,,,,
      },
      // 表单校验
      rules: {
        // ,,,,
      }
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

父组件中多次使用子组件,并要触发子组件的表单验证

// 子组件定义
<template>
  <div>
    <el-form ref="form" :model="form" :rules="rules" label-width="80px" enctype="multipart/form-data">
      ....
      <!--循环多次出现子组件-->
      <div v-for="(d,index) in kidList" :key="index">
        <Kid :ref="'kid' + index"></Kid>
      </div>
    </el-form>
  </div>
</template>
<script>
import Kid from "@/components/Kid";
export default {
  name: "Parent",
  data() {
    return {
      form: {
        // ,,,,
      },
      // 表单校验
      rules: {
        // ,,,,
      },
      kidList: []
    }
  },
  methods: {
    submit () {
      let kidValid = true
      for (let i=0; i< this.kidList.length; i++){
        // 通过$refs获得子组件,并触发子组件的表单验证
        this.$refs['kid' + i][0].$refs["form"].validate(r=>{
          kidValid = kidValid && r
        })
      }

      this.$refs["form"].validate(valid => {
        if (valid && kidValid) {
          // 父组件和子组件都通过表单验证
        }
      })
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 11、vm.$set

  • 语法:vm.$set( target, propertyName/index, value )
  • 参数:
  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value
  • 返回值: 设置的值

# 12、vm.$delete

  • 语法:vm.$delete( target, propertyName/index )
  • 参数:
  • {Object | Array} target
  • {string | number} propertyName/index

# 13、vm.$emit

  • 语法:vm.$emit( eventName, […args] )
  • 参数:
    • {string} eventName
    • [...args] 触发当前实例上的事件。附加参数都会传给监听器回调。 最常用的场景就是父组件使用子组件时,子组件触发父组件中的事件

# 14、v-slot/slot/slot-scope

插槽:新版本中使用v-slot,老版本中使用slotslot-scope
假设有<base-layout>组件模板内容如下:

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11

# 简单模板定义

使用插槽后组件模板可以设计成如下:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11

# 场景一

使用v-slot:<slot-name>指定具名插槽对应名称,没有名称的都会匹配到匿名插槽中

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
1
2
3
4
5
6
7
8
9
10
11
12

或者

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

老版本还可以如下直接把 slot attribute 用在一个普通元素上:

<base-layout>
  <h1 slot="header">Here might be a page title</h1>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <p slot="footer">Here's some contact info</p>
</base-layout>
1
2
3
4
5
6
7
8

都会渲染成:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11
12

注意点:v-slot 只能添加在 <template>

# 场景二:作用域插槽

  • 老版本使用slot-scope绑定,如:slot-scope="slotProps" 在 <template> 上使用特殊的 slot-scope attribute,可以接收传递给插槽的 prop
<slot-example>
  <template slot="default" slot-scope="slotProps">
    {{ slotProps.msg }}
  </template>
</slot-example>
1
2
3
4
5
  • 新版本使用v-slot,如:v-slot:default="slotProps"
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>
1
2
3
4
5

# 解构插槽 Prop

作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里:

function (slotProps) {
  // 插槽内容
}
1
2
3

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。 所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>
1
2
3

这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person:

<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>
1
2
3

你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>
1
2
3

# 具名插槽的缩写

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

更新日期: 2022/4/27 下午2:27:30