目录 §

  • 1. 进阶属性概览 §
  • 2. 复习响应式原理 §
  • 3. Computed 计算属性(数据) §
  • 4. Watch 监听属性(数据) §
  • 5. Directive 指令(资源) §
  • 6. Mixin 混入(组合) §
  • 7. Extends 继承、扩展(组合) §
  • 8. ProvideInject(组合) §
  • 9. Vue3.x响应式系统 API watchEffect §
  • 10. $3 §
  • 11. $3 §

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
    • 还可以预先定义其他构造选项
    • 继承就是为了减少重复的构造选项
    • 不用ES6extends的原因
  • provide/inject
    • 祖孙组件通信
    • 后代组件通信
    • 全局变量,局部的全局变量

2. 复习响应式原理 §

  • 当传入options.dataVue之后
    • 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

 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

  • 根据usergender筛选结果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做了特殊处理
  • 示例代码3Vue源码
 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时解除事件监听
  • 简化了在createdbeforeDestroy时原本的DOM操作

缩写

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

指令的作用

用于DOM操作

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

减少重复

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

6. Mixin 混入 §

Mixin减少重复

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

就是复制

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

场景描述 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>
  • 假设需要在多个组件上添加nametime
  • 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技巧

选项智能合并

  • 文档 选项合并
  • 当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”
  • 可以添加相应操作,同名覆盖
    • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先
    • 同名钩子函数将合并为一个数组,因此都将被调用
    • 混入对象的钩子将在组件自身钩子之前调用
    • 值为对象的选项,例如methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对
 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.xMixin 合并行为变更
  • Vue3.x 中,冲突合并的结果是组件中键值对完全替代mixins的,浅层次执行合并,并不深入比对每一项
  • 对于依赖对象声明的用户,建议
    • 将共享数据提取到外部对象并将其用作 data 中的每个 property
    • 重写对共享数据的引用以指向新的共享对象
    • 对于依赖 mixin 的深度合并行为的用户,建议重构代码以完全避免这种依赖,因为 mixin 的深度合并非常隐式,让代码逻辑更难理解和调试
    • 或使用Composition代替

7. Extend 继承、扩展 §

Extend减少Mixin的重复

  • 不每次都写一个mixins
  • 使用Vue.extendoptions.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. ProvideInject §

提供和注入,需求描述

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

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

 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
<template>
<div>
  <button @click="changeColor">
    当前主题色: {{ displayColor }} 换肤 换成{{ showThemeName }}
  </button>
  <button @click="changeFontSize('big')">大字</button>
  <button @click="changeFontSize('small')">小字</button>
  <button @click="changeFontSize('normal')">正常字</button>
</div>
</template>

<script>
export default {
  inject: ["themeName", "changeTheme", "changeFontSize"],
  data() {
    return {
      currentColor: this.themeName,
    };
  },
  computed: {
    displayColor() {
      return this.currentColor;
    },
    showThemeName() {
      return this.currentColor === "blue" ? "red" : "blue";
    },
  },
  methods: {
    changeColor() {
      const currentColor = this.changeTheme();
      console.log(currentColor);
      this.currentColor = currentColor;
    },
  },
};
</script>
  • .app.fontSize-normal 是同一个选择器,同时满足两种类
  • ["normal", "big", "small"].indexOf(name) >= 0代替短路判断
  • fontSize默认不继承

ProvideInject小结

  • 在提供数据的地方写ProvideProvide为函数形式,返回提供的数据的拷贝(比如字符串)
    • 如果在注入的地方修改,就需要再提供一个修改的函数到Provide,让注入拿到函数的引用
    • 或者用复杂类型的引用
  • 在需要数据的地方写InjectInject为数组['InjectData1', 'InjectData2', ...]
  • ProvideInject作用是大范围的 数据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,只是形式不同
  • provideinject(组合)
    • 祖先组件不需要知道哪些后代组件使用它提供的 property
    • 后代组件不需要知道被注入的 property 来自哪里
    • 祖代提供东西,后代注入使用
    • 作用是祖代提供,隔N代共享信息,反过来不行


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名