目录 § ⇧


1. 进阶属性概览 §

  • computed 计算属性
    • 不需要加括号
    • 会根据依赖是否变化来缓存
  • watch 监听
    • 一旦data变化,就执行的函数
    • options.watch用法
    • this.$watch用法
    • deep, immediate含义
  • directives 指令
    • 内置指令 v-if / v-for / v-bind /v-on
    • 自定义指令,如v-focus
    • 指令是为了减少重复的DOM操作
  • mixin 混入
    • 重复三次之后的出路
    • 混入 V.S. 全局混入
    • 选项自动合并
    • 混入就是为了减少重复的构造选项
  • extends 继承
    • Vue.extend
    • 觉得用了mixin还是重复,自己写了View,继承Vue
    • 还可以预先定义其他构造选项
    • 继承就是为了减少重复的构造选项
    • 不用ES6的extends的原因
  • provide/inject
    • 祖孙组件通信
    • 后代组件通信
    • 全局变量,局部的全局变量

2. 复习响应式原理 § ⇧

  • 当传入options.data给Vue之后
    • data会被Vue监听,对象被Vue篡改setter getter
    • 会被Vue实例vm代理
    • 每次对data的读写会被Vue监控
    • Vue会在data变化时更新UI

data变化时除了更新UI外,其他的操作

3. Computed 计算属性(数据) § ⇧

用途 ⇧

main.js

 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
new Vue({
  data() {
    return {
      user: {
        email: "FrankFang",
        nickname: "方",
        phone: "13812312312"
      }
    }
  },
  computed: {
    displayName() {
      const user = this.user
      return user.nickname || user.email || user.phone
    },
  },
  // DRY 原则
  // 用 computed 来计算 displayName 代替模板中的 user.nickname || user.email || user.phone
  template: `
    <div>
      {{displayName}}
      <div>
        {{displayName}}
      </div>
      {{displayName}}
    </div>
  `,
}).$mount('#app')
  • 用户可能没有填昵称,优先展示昵称,没有昵称其次展示邮箱,再次展示手机
  • 页面中多处展示昵称、邮箱和手机
  • DRY原则,灵活面对需求变化:先展示手机,最后是邮箱
  • 将所有的数据的可能变化,写在一个函数中(displayName() {return ...} 必须有返回值),按需调用(当做属性,不用写括号){{displayName}}
  • 需要多处展示,多处写入计算属性即可
  • 好处就是可以让一些事根据其他属性计算而来的属性变成一个属性,形式是一个方法,但必须当做属性来用,默认去读取方法的返回值

可读可写main.js

 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
new Vue({
  data() {
    return {
      user: {
        email: "FrankFang",
        nickname: "方",
        phone: "13812312312"
      },
    }
  },
  computed: {
    displayName: {
      get() {
        const user = this.user
        return user.nickname || user.email || user.phone
      },
      set(value) {
        console.log(value)
        this.user.nickname = value
      }
    }
  },
  // DRY 原则
  // 用 computed 来计算 displayName 代替模板中的 user.nickname || user.email || user.phone
  template: `
    <div>
      {{displayName}}
      <div>
        {{displayName}}
      </div>
      {{displayName}}
    <button @click="set">set</button>
    </div>
  `,
  methods: {
    set() {
      console.log('set')
      this.displayName = '圆圆'
    }
  }
}).$mount('#app')
  • 示例代码2:列表展示
  • 需求 点击性别展示符合的内容;点击全部,展示全部

如何给三个按钮添加事件处理函数

  • 思路一:点击后改userList,不可改变原始数据
  • 复制一份数据用来展示displayUsers: []
  • 思路二:使用computed

思路一:点击后改userList ⇧

  • 不用 computed 筛选男女

     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
    48
    49
    50
    51
    52
    53
    54
    55
    
    let id = 0
    const createUser = (name, gender) => {
    id += 1
    return {id: id, name: name, gender: gender}
    }
    new Vue({
    data() {
    return {
      userList: [
        createUser("方方", "男"),
        createUser("圆圆", "女"),
        createUser("小新", "女"),
        createUser("小葵", "女"),
      ],
      // DRY 重复代码 使用computed重构displayUsers
      displayUsers: [],
    }
    },
    // created 实例出现在内存中,而未出现在页面中
    // 复制一份原始数据用来展示
    created() {
    this.displayUsers = this.userList
    },
    template: `
      <div>
        <button @click="showAll">全部</button>
        <button @click="showMale">男</button>
        <button @click="showFemale">女</button>
        <ul>
    <!--      <li v-for="u in userList" :key="u.id">   -->
          <li v-for="u in displayUsers" :key="u.id">
            {{u.name}} - {{u.gender}}
          </li>
        </ul>
      </div>
    </div>
    `,
    methods: {
    set() {
      console.log('set')
      this.displayName = '圆圆'
    },
    // DRY 重复代码 使用computed重构
    showMale() {
      this.displayUsers = this.userList.filter(user => user.gender === '男')
    },
    showFemale() {
      this.displayUsers = this.userList.filter(user => user.gender === '女')
    
    },
    showAll() {
      this.displayUsers = this.userList
    },
    }
    }).$mount('#app')
    

思路二:使用computed ⇧

  • 根据user和gender筛选结果result
  • 封装函数result = f(users, gender){...},通过计算得到结果
  • 在methods的方法中判断this.gender
  • 在template点击事件中,直接赋值gender,自动计算显示内容
  • 用 computed 筛选男女

      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
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    
    let id = 0
    const createUser = (name, gender) => {
    id += 1
    return {id: id, name: name, gender: gender}
    }
    new Vue({
    data() {
    return {
      userList: [
        createUser("方方", "男"),
        createUser("圆圆", "女"),
        createUser("小新", "女"),
        createUser("小葵", "女"),
      ],
    
      // DRY 重复代码 使用computed重构displayUsers
      /* displayUsers: [],*/
    
      // 初始化gender,根据gender筛选结果result
      gender: '',
    }
    },
    // created 实例出现在内存中,而未出现在页面中
    // 创建时 复制一份原始数据用来展示
    // DRY 重复代码 使用computed重构displayUsers时不需要
    /*
    created() {
    this.displayUsers = this.userList
    },
    */
    computed: {
      displayUsers() {
        // 哈希映射
        const hash = {
          male: '男',
          female: '女'
        }
        const {userList, gender} = this
        if(gender === '') {
          return userList
        }
        /*
        else if(gender === 'male') {
          return userList.filter(user => user.gender === '男')
        }else if(gender === 'female') {
          return userList.filter(user => user.gender === '女')
        }
        */
        /*
        else if(gender === 'male' || gender === 'female') {
          return userList.filter(user => user.gender === hash[gender])
        }
        */
        else if(typeof gender === 'string') {
          return userList.filter(user => user.gender === hash[gender])
        }else{
          throw new Error('gender的值是意外的值')
        }
      },
    },
    template: `
      <div>
        <!--
        <button @click="showAll">全部</button>
        <button @click="showMale">男</button>
        <button @click="showFemale">女</button>
        -->
        <!--  不需要加this 可省略methods中的三个方法 直接改变this.gender    -->
        <!--
        <button @click="gender =''">全部</button>
        <button @click="gender ='male'">男</button>
        <button @click="gender ='female'">女</button>
        -->
        <button @click="setGender('')">全部</button>
        <button @click="setGender('male')">男</button>
        <button @click="setGender('female')">女</button>
        <ul>
    <!--      <li v-for="u in userList" :key="u.id">   -->
          <li v-for="(u, index) in displayUsers" :key="index">
            {{u.name}} - {{u.gender}}
          </li>
        </ul>
      </div>
    </div>
    `,
    methods: {
    // 在template中改变属性的选择条件,自动计算 改变内容
    /*
    // DRY 重复代码 使用computed重构
    showMale() {
      // 用 computed 计算属性 根据不同的gender显示userList内容 来代替
      // this.displayUsers = this.userList.filter(user => user.gender === '男')
      this.gender = 'male'
    },
    showFemale() {
      // 用 computed 计算属性 根据不同的gender显示userList内容 来代替
      // this.displayUsers = this.userList.filter(user => user.gender === '女')
      this.gender = 'female'
    },
    showAll() {
      // 用 computed 计算属性 根据不同的gender显示userList内容 来代替
      // this.displayUsers = this.userList
      this.gender = ''
    },
    */
    // setGender 代替以上三个函数
    setGender(string) {
      this.gender = string
    }
    }
    }).$mount('#app')
    
  • 根据gender返回userList的不同部分

  • computed使得button的逻辑更加清晰,按钮直接改变gender,而不用通过绑定函数,计算对应的值,全部在computed中计算


Computed 缓存原理 ⇧

  • 如果依赖的属性没有变化,计算属性就不会重新计算
  • getter/setter默认不会做缓存,Vue做了特殊处理
  • 示例代码3 非Vue源码

     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
    48
    49
    50
    51
    
    // Computed 缓存原理
    let obj1 = {
    姓: "高",
    名: "圆圆",
    get 姓名() {
    console.log('计算了一次,将姓与名相加')
    return this.姓 + this.名;
    },
    set 姓名(xxx){
    this.姓 = xxx[0]
    this.名 = xxx.slice(1)
    },
    age: 18
    };
    
    console.log(obj1.姓名)
    console.log(obj1.姓名)
    console.log(obj1.姓名)
    
    console.log('----------------') // 精髓
    
    // 杠精说:为什么每次都要重新计算?如果计算很复杂,不就很浪费时间吗?
    // 那就缓存一下吧
    // 缓存是啥?就是哈希表
    const cache = {} // {'高':{'圆圆': '高圆圆'}}
    let obj2 = {
    姓: "高",
    名: "圆圆",
    get 姓名() {
    // 由于对象不支持 ['高','圆圆'] 数组作为 key,只能变通一下
    if(this.姓 in cache && this.名 in cache[this.姓]){
      console.log('有缓存,不再计算')
      return cache[this.姓][this.名]
    }
    // 如果 cache[this.姓] 不存在,就赋值为 {}
    // 如果 cache[this.姓] 存在,就赋值为它自己(相当于什么都不做)
    cache[this.姓] = cache[this.姓] || {} // 保底值
    cache[this.姓][this.名] = this.姓 + this.名
    console.log('计算了一次,将姓与名相加')
    return cache[this.姓][this.名];
    },
    set 姓名(xxx){
    this.姓 = xxx[0]
    this.名 = xxx.slice(1)
    },
    age: 18
    };
    
    console.log(obj2.姓名)
    console.log(obj2.姓名)
    console.log(obj2.姓名)
    
  • 勿重复在data中声明数据,否则报错The computed property "xxx" is already defined in data


4. Watch 监听属性(数据)§ ⇧

用途 ⇧

  • 当数据变化时,执行一个函数
  • 又称“侦听器”
  • 当需要在数据变化时执行异步或开销较大的操作时,来响应数据的变化

示例代码4:撤销

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
new Vue({
  template: `
    <div>
      <hr/>
      {{n}}
      <hr/>
      <button @click="add1">+1</button>
      <button @click="add2">+2</button>
      <button @click="minus1">-1</button>
      <button @click="minus2">-2</button>
      <hr/>
      <button @click="undo">撤销</button>
      <hr/>
      {{history}}
    </div>
    `,
  name: 'watch',
  data() {
    return {
      n: 0,
      history: [],
      inUndoMode: false, // 撤销模式
    }
  },
  watch: {
    n(newValue, oldValue) {
      console.log('now in undo mode ' + this.inUndoMode)
      console.log(`在不在撤销模式:${this.inUndoMode?'在':'不在'}`)
      if (!this.inUndoMode) {
        return this.history.push({ from: oldValue, to: newValue})
      }
    }
  },
  methods: {
    add1() {
      this.n += 1
    },
    add2() {
      this.n += 2
    },
    minus1() {
      this.n -= 1
    },
    minus2() {
      this.n -= 2
    },
    undo() {
      const last = this.history.pop()
      // 开启撤销模式
      this.inUndoMode = true
      console.log('now in undo mode ' + this.inUndoMode)
      console.log(`在不在撤销模式:${this.inUndoMode?'在':'不在'}`)
      const old = last.from
      // watch 的函数是异步的 this.n = old 也会被监听到
      // 等到同级的同步代码执行完毕再去执行 watch的代码
      this.n = old
      this.$nextTick(() => {
        this.inUndoMode = false
      }, 0)
    }
  }
}
})
  • 官方描述:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把 “接触”过的数据 property 记录为依赖
  • 之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染
  • 即所有methods中操作数据的改变都会被watch监听到

异步

示例代码5:模拟computed 不建议使用,优先使用computed

 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
new Vue({
  template: `
      <div>
        <hr>
        {{displayName}}
        <button @click="user.nickname=undefined">remove nickname</button>
        <button @click="user.email=undefined">remove email</button>
      </div>
  `,
  name: "WatchMockComputed.vue",
  data() {
    return {
      user: {
        email: 'fangfang@qq.com',
        nickname: "rectangle",
        phone: "13812345678",
      },
      displayName: null,
    }
  },
  watch: {
    'user.email': {
        handler: 'changed',
        immediate: true, // 第一次渲染也触发 watch
    },
    'user.nickname': {
        handler: 'changed',
        immediate: true, // 第一次渲染也触发 watch
    },
    'user.phone': {
      handler: 'changed',
      immediate: true, // 第一次渲染也触发 watch
    },
  },
  methods: {
    changed(item) {
      console.log(`item: ${item}`)
      console.log(item === this.displayName)
      console.log(`已知this.displayName: ${this.displayName}`)
      const {user: {email, nickname, phone}} = this
      this.displayName = nickname || email || phone
      },
    },
  }
})
  • immediate:是否在第一次渲染时运行watch函数监听数据初始值
  • 注意watch监听data第二层数据user.email的写法
  • watch一般不监听初始的值:从无到有,即第一次数据是空值
  • 除非设置watch内部对象的属性immediate: true 使得第一次渲染也触发 watch
  • 都是在数据变化的时候执行一个函数
  • computed着重依赖之间的变化和缓存
  • watch着重变化后执行函数,而不是得到的结果

数据变化 ⇧

  • 示例代码6
  • obj原本是{a: 'a'},变为obj = {a: 'a'},地址变了,而obj.a的值没变
  • 简单类型看值,复杂类型(对象看地址)
  • 对象的属性变了,但对象的地址没变,Vue认为对象没变
  • 和===的规则相同

watch 类型语法

  • watch: { [key: string]: string | Function | Object | Array }
  • 常用watch: {fn(new, old) {...}}

语法1 文档

1
2
3
4
5
6
7
8
9
watch: {
    // o0: () => {} // 箭头函数this是全局对象
    o1: function(value, oldValue) {...},
    o2(value, oldValue) {...},
    o3: [f1, f2],
    o4: 'methodName',
    o5: {handler: fn, deep: true, immediate: true},
    'object.a': function() {}
}

语法2 实例方法 / 数据 - 使用命令式的 vm.$watch

  • Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property

    1
    2
    3
    
    vm.$watch('xxx', fn, {deep: ..., immediate: ...})
    // 'xxx'可以改为一个返回字符串的函数
    // 在组件中可写到钩子函数里
    

语法3 钩子函数中写 this.$watch(...)

1
2
3
4
5
6
...,
  created() {
    this.$watch('xxx', fn, {deep: ..., immediate: ...})
    // 'xxx'可以改为一个返回字符串的函数
  }
...

深入监听deep: true ⇧

  • 如果object.a变了,deep: true控制object变了
  • 如果object.a变了,deep: false控制object不变
  • deep即监听object深入监听
  • 不仅比较地址,任何一层的值变了,都判定数据变化,都受到监听
  • 即不需要再监听对象里面的属性变化,只要监听外层,同时监听了内部数据变化
  • deep默认值是false

     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
    48
    49
    50
    51
    52
    53
    
    const vmWatchDeep = new Vue({
    template: `
      <div>
          <hr>
          <button @click="n += 1">n + 1</button>
          <button @click="m += 1">m + 1</button>
          <button @click="obj.a += 'hi'">obj.a + 'hi'</button>
          <button @click="obj1.a += 'ho'">obj1.a + 'ho'</button>
          <button @click="obj = {a: 'a'}">obj = 新对象</button>
      </div>
    `,
    name: 'WatchDeep',
    data() {
    return {
      n: 0,
      obj: {
        a: "a"
      },
      obj1: {
        a: "a"
      },
    }
    },
    created(){
    this.$watch('m', function() {
      console.log("m 也变了")
    },{immediate: true})
    },
    watch: {
    n() {
      console.log("n 变了")
    },
    obj() {
      console.log("obj 变了")
    },
    "obj.a": function() {
      console.log("obj.a 变了")
    },
    obj1: {
      handler() {
        console.log('obj1变了')
      },
      deep: true,
    },
    "obj1.a": function() {
      console.log("obj1.a 变了")
    },
    }
    }).$mount('#watchDeep')
    
    vmWatchDeep.$watch('n', function() {
    console.log("n 也变222了")
    },{immediate: true})
    
  • 写在外面太丑,可写在生命周期的钩子中

  • 和DOM无关,放在created中,创建完立即监听数据


简述 computed 和 watch 的区别 ⇧

  • 翻译成中文
  • 各自描述(可用代码例子)

标准答案

  • 把 computed 翻译为计算属性
  • 把 watch 翻译为监听/观察/侦听
  • 描述 computed 的功能:用于计算一个新的属性,并提及「依赖」和「缓存」
  • 描述了 watch 的功能:在数据变化时执行一个函数
  • 其他正确的描述:
    • computed计算出的值不需要加括号,在template中,直接当做属性来用
    • computed根据「依赖」会自动「缓存」,依赖不变,不会重新计算值
    • watch监听某个属性变化了,就执行一个函数
    • watch中的immediate表示是否在第一次渲染中监听属性
    • watch中的deep是否监听对象内部属性的变化

详细描述 计算属性computed:

  • 用来计算一个值,这个值使用时不需要加函数执行的括号,直接当属性来用;值会根据所依赖的数据动态显示新的计算结果,并自动缓存,即依赖不变,不会重新计算
  • 支持缓存,只有依赖数据发生改变,才会重新进行计算
  • 不支持异步,当computed内有异步操作时无效,无法监听数据的变化
  • 可用Vue提供的异步函数nextTick
  • computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的
  • 即基于data中声明过的数据或者父组件传递的props中的数据通过计算得到的值
  • 如果一个属性是由其他属性计算而来的,即这个属性(数据)依赖其他属性(数据),是多对一或者一对一,可用computed
  • 如果computed属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。

详细描述 监听属性watch:

  • 对data的监听回调,即当依赖的data数据变化,执行回调函数
  • 监听某个属性变化了,就执行一个函数
  • 不支持缓存,数据被监,数据不变就不触发回调;
  • watch支持异步;
  • 监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
  • 当一个属性发生变化时,需要执行对应的操作;一对多;
  • 监听数据必须是data中声明过的数据或者父组件传递过来的props中的数据,当数据变化时,触发其他操作
  • immediate 组件加载立即触发回调函数执行,即第一次渲染时就会执行这个函数
  • deep 监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器
  • 监听某个数据的变化时用watch
  • 不可使用箭头函数来定义 watcher 回调函数,箭头函数绑定了父级作用域的上下文,this 将不会按照期望指向 Vue 实例

小结 ⇧

区别 computed watch watchEffect
作用简述 动态显示计算值 监听数据变化,执行回调函数 —
初始数据 属性值 immediate属性可监听初始值,否则为空 —
缓存 支持 不支持 —
异步 不支持 支持 —
特点1 不需要加括号,可以当一个属性来用,依赖于其他数据的变化 仅当数据变化才执行回调函数 —
特点2 多对一、一对一 一对多 —
目的 动态显示计算值 响应数据的变化,执行相应操作 —

5. Directive 指令 § ⇧

通过学习自定义指令来掌握Vue的指令,再使用内置指令

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令

两种写法

  1. 全局声明一个指令Vue.directive('directiveName', directiveOptions)
  2. 局部声明多个指令,在options中声明directives: {'directiveName': directiveOptions}对象

main.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 全局指令
Vue.directive('xyz', {/* directiveOptions */})
// 可在任意组件的标签中使用 v-xyz

// 局部指令
new Vue({
    ...,
    directives: {
        'x': {/* directiveOptions */},
        'y': {/* directiveOptions */},
        'z': {/* directiveOptions */},
    }
})

目标:造出v-y,点击即出现一个y

main.js

 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
// 全局指令
Vue.directive('xyz', {
  inserted: function (el) {
    // 点击即出现一个y
    el.addEventListener('click', () => {
      console.log('y')
    })
  },
})

// 局部指令 在组件中声明 在组件内 template 使用
new Vue({
    template: `
        <div v-x v-xyz>
            hi
        </div>
    `,
    directives: {
        'x': {
            inserted: function (el) {
            el.addEventListener('click', () => {
              console.log('x');
        })
      }
    },
  },
})
  • 声明的局部指令只能用在该实例中

directiveOptions ⇧

directiveOptions的五个属性(钩子)Vue2.x

  • bind(el, info, vnode, oldVnode)类似created
  • inserted(el, info, vnode, oldVnode)类似mounted
  • updated(el, info, vnode, oldVnode)类似updated
  • componentUpdated(el, info, vnode, oldVnode),文档
  • unbind(el, info, vnode, oldVnode)类似destroyed

自定义指令的生命周期钩子更名了directiveOptions的六个属性(钩子)Vue3.x


仿v-on指令 ⇧

  • v-on2(省略事件委托)

main.js

 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
new Vue({
  template:`
  <div>
    <button v-on2:click="hi">点</button>
  </div>
  `,
  directives: {
    'on2': {
      // inserted 可以改为 bind
      inserted(el, info) {
        console.log(`el: ${el}`)
        console.log(el)
        console.log(`info: ${info}`);
        console.log(info)
        console.log(`info.arg: ${info.arg}`);
        console.log(info.value)
        el.addEventListener(info.arg, info.value("hi"))
      },
      unbind(el, info) {
        el.removeEventListener(info.arg, info.value)
      }
    },
  },
  methods: {
    hi(words) {
      console.log(`${words}`)
    }
  },
})
  • inserted时添加事件监听
  • unbind时解除事件监听
  • 简化了在created和beforeDestroy时原本的DOM操作

缩写

  • directiveOptions在某些条件下可以缩写为函数,文档

指令的作用 ⇧

用于DOM操作

  • Vue的实例/组件用于数据绑定、事件监听、DOM更新
  • Vue的指令主要目的就是原生DOM操作

减少重复

  • 封装为自定义指令,复用DOM操作
  • 封装为自定义指令,简化DOM操作

6. Mixin 混入 § ⇧

Mixin减少重复

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能

就是复制

  • Directive 指令的作用是减少DOM操作的重复
  • Mixin 混入的作用是减少data、methods、钩子的重复

场景描述 Mixin.vue

 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
<template>
<div id="mixins">
  <Child1 v-if="child1Visible" />
  <button @click="child1Visible = false">x</button>
  <Child2 v-if="child2Visible" />
  <button @click="child2Visible = false">x</button>
  <Child3 v-if="child3Visible" />
  <button @click="child3Visible = false">x</button>
</div>
</template>
<script>
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
import Child3 from './Child3.vue'
export default {
  name: "Mixins",
  components: {
    Child1,
    Child2,
    Child3,
  },
  data() {
    return {
      name: "Mixins",
      child1Visible: true,
      child2Visible: true,
      child3Visible: true,
    }
  },
}
</script>
  • 假设需要在多个组件上添加name和time
  • 在created、destroyed时,打出提示,并报出存活时间
  • 一共有五个组件
    • 给每个组件添加data和钩子,共五次?
    • 使用mixin减少重复
  • 示例链接

./mixins/log.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
export default {
  data() {
    return {
      name: undefined,
      time: undefined
    }
  },
  created() {
    if (!this.name) {
      throw new Error('need name')
    }
    this.time = new Date();
    console.log(`${this.name}出生了`);
  },
  beforeDestroy() {
    const now = new Date()
    console.log(`${this.name}死亡了, 共生存了${now - this.time}ms`);
  },
}

Child1.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
<div>
  Child1
</div>
</template>
<script>
import log from '../mixins/log.js'
export default {
  data() {
    return {
      name: "Child1"
    }
  },
  mixins: [log],
}
</script>

mixins用法

  • mixins 选项接收一个混入对象的数组 mixins: [obj1, ...]
  • 这些混入对象可以像正常的实例对象一样包含实例选项options
  • 当组件使用混入对象时,这些选项将会被合并到该组件本的选项形成身最终的选项,使用的是和 Vue.extend() 一样的选项合并逻辑
  • 比如混入包含一个 created 钩子,而创建组件本身也有一个,那么两个函数都会被调用
  • Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用

     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
    
    const mixin = {
    created: function () { console.log(1) }
    }
    const vm = new Vue({
    created: function () { console.log(2) },
    mixins: [mixin]
    })
    // => 1
    // => 2
    
    // 定义一个混入对象
    const myMixin = {
    created: function () {
    this.hello()
    },
    methods: {
    hello() {
      console.log('hello from mixin!')
    }
    }
    }
    
    // 定义一个使用混入对象的组件
    const Component = Vue.extend({
    mixins: [myMixin]
    })
    
    const component = new Component() // => "hello from mixin!"
    

mixin技巧

选项智能合并

  • 文档 选项合并
  • 当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”
  • 可以添加相应操作,同名覆盖

    • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先
    • 同名钩子函数将合并为一个数组,因此都将被调用
    • 混入对象的钩子将在组件自身钩子之前调用
    • 值为对象的选项,例如methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对

       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
      
      /* 混入对象 */
      const mixin = {
      created: function () {
      console.log('混入对象的钩子被调用')
      }
      }
      
      /* 组件 */
      new Vue({
      mixins: [mixin],
      created: function () {
      console.log('组件钩子被调用')
      }
      })
      
      // => "混入对象的钩子被调用"
      // => "组件钩子被调用"
      
      /* 同名覆盖 */
      var mixin = {
      methods: {
      conflicting: function () {
      console.log('from mixin')
      }
      }
      }
      
      var vm = new Vue({
      mixins: [mixin],
      methods: {
      conflicting: function () {
      console.log('from self')
      }
      }
      })
      
      vm.conflicting() // => "from self"
      
  • Vue.extend() 也使用同样的策略进行合并

全局混入Vue.mixin

  • 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例
  • 文档 不推荐使用
  • 注意所有组件的data中需要相应的数据是否声明完整
  • 范围过大,插件作者可以使用混入,向组件注入自定义的行为,以避免重复应用混入。不推荐在应用代码中使用

Vue3.x混入(mixin) 将不再作为推荐使用, Composition API可以实现更灵活且无副作用的复用代码

  • Vue3.x 中Mixin 合并行为变更
  • Vue3.x 中,冲突合并的结果是组件中键值对完全替代mixins的,浅层次执行合并,并不深入比对每一项
  • 对于依赖对象声明的用户,建议
    • 将共享数据提取到外部对象并将其用作 data 中的每个 property
    • 重写对共享数据的引用以指向新的共享对象
    • 对于依赖 mixin 的深度合并行为的用户,建议重构代码以完全避免这种依赖,因为 mixin 的深度合并非常隐式,让代码逻辑更难理解和调试
    • 或使用Composition代替

7. Extend 继承、扩展 § ⇧

Extend减少Mixin的重复

  • 不每次都写一个mixins
  • 使用Vue.extend或options.extends

MyVue.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Vue from 'vue'
const MyVue = Vue.extend({
  data() {
    return {
      name: undefined,
      time: undefined
    }
  },
  created() {
    if (!this.name) {
      throw new Error('need name')
    }
    this.time = new Date();
    console.log(`${this.name}出生了`);

  },
  beforeDestroy() {
    const now = new Date()
    console.log(`${this.name}死亡了, 共生存了${now - this.time}ms`);
  },
})
export default MyVue

在组件Child1中使用或者new MyVue(options)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
<div>Child1</div>
</template>

<script>
import MyVue from "../MyVue.js";
export default {
  name: "use extends",
  data() {
    return {
      name: "Child1",
    };
  },
  extends: MyVue, // 只有一项 不可用数组 单继承
};
</script>

extend 是比 mixin 更抽象的封装

  • 写一次extend代替写多次mixin,较少用到

8. Provide 和 Inject § ⇧

提供和注入,需求描述

  • 一键换肤功能:默认蓝色,切换为红色
  • 文字大小:默认正常,切换大小
  • 示例链接

Provide-Inject.vue - Provide

  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
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
<template>
<div id="provideInject" :class="`app theme-${themeName} fontSize-${fontSizeName}`">
  <!-- :class="`app theme-${themeName}`" 双引号中的是值 反引号中的是JS字符串 -->
  <ChangeThemeButton />
  <change-theme-button />
  <Child1 />
  <button>x</button>
  <Child2 />
  <button>x</button>
</div>
</template>

<script>
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";
import ChangeThemeButton from "./ChangeThemeButton.vue";
export default {
  name: "ProvideInject",
  data() {
    return {
      themeName: "blue", // "red"
      fontSizeName: "normal", // "big" | "small"
    };
  },
  provide() {
    return {
      themeName: this.themeName,
      changeTheme: this.changeTheme,
      changeFontSize: this.changeFontSize,
    };
  },
  components: {
    Child1,
    Child2,
    ChangeThemeButton,
  },
  methods: {
    changeTheme() {
      this.themeName === "blue" ?
        (this.themeName = "red") :
        (this.themeName = "blue");
      return this.themeName;
    },
    changeFontSize(name) {
      /*
      if (name === "normal" || name === "big" || name === "small") {
        this.fontSizeName = name;
      }
       */

      if (["normal", "big", "small"].indexOf(name) >= 0) {
        this.fontSizeName = name;
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.app {
  font-family: "Avenir", Helvetica, sans-serif;
  text-align: center;
  color: #2c3ef0;
  margin-top: 60px;

  // .app.theme-blue (注意没有空格)表示同时满足 .app 和 .theme-blue 两个类
  // 但我用SCSS

  &.theme-blue {
    color: #2c3ef0;

    button {
      background-color: blue;
      color: white;
    }
  }

  &.theme-red {
    color: darkgoldenrod;

    button {
      background-color: red;
      color: white;
    }
  }

  & button {
    font-size: inherit;
  }

  &.fontSize-normal {
    font-size: 16px;
  }

  &.fontSize-big {
    font-size: 26px;
  }

  &.fontSize-small {
    font-size: 12px;
  }
}
</style>

Child1.vue

 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>
  Child1
  <!-- <change-theme-button />
  <ChangeThemeButton /> -->
</div>
</template>

<script>
import ChangeThemeButton from "./ChangeThemeButton.vue";
export default {
  data() {
    return {
      name: "Child1",
    };
  },
  components: {
    ChangeThemeButton,
  },
};
</script>
````

> `ChangeThemeButton.vue` - Inject

HTML

```

  • .app.fontSize-normal 是同一个选择器,同时满足两种类
  • ["normal", "big", "small"].indexOf(name) >= 0代替短路判断
  • fontSize默认不继承

Provide 和 Inject小结

  • 在提供数据的地方写Provide,Provide为函数形式,返回提供的数据的拷贝(比如字符串)
    • 如果在注入的地方修改,就需要再提供一个修改的函数到Provide,让注入拿到函数的引用
    • 或者用复杂类型的引用
  • 在需要数据的地方写Inject,Inject为数组['InjectData1', 'InjectData2', ...]
  • Provide 和 Inject作用是大范围的 数据data 和 方法methods 共用,Provide把东西提供给所有人去引用,让Inject去调用
  • 注意点:示例中不可只传themeName而不传changeTheme,因为themeName的值是被复制给provide
  • 不推荐传引用,变量容易失控,示例

总结 ⇧

  • computed 计算属性(数据)
    • 全局用Vue.
    • 局部用options.
    • 作用是``
  • watch 监听属性(数据)
    • 全局用Vue.
    • 局部用$watch()
    • 作用是``
  • directive 指令(资源)
    • 全局用Vue.directive({...})
    • 局部用options.directives
    • 作用是减少DOM操作相关的代码重复
  • mixin 混入(组合)
    • 全局用Vue.mixin({...})
    • 局部用options.mixins: [mixin1, mixin2]
    • 作用是减少options里的代码重复
  • extend 继承、扩展(组合)
    • 全局用Vue.extend({...})
    • 局部用options.extends: {...}
    • 作用是类似mixin,只是形式不同
  • provide 和 inject(组合)
    • 祖先组件不需要知道哪些后代组件使用它提供的 property
    • 后代组件不需要知道被注入的 property 来自哪里
    • 祖代提供东西,后代注入使用
    • 作用是祖代提供,隔N代共享信息,反过来不行

·未完待续·

参考文章

相关文章

  • 无