Vue 语法 | 原理


# Vue 语法 | 原理

# 1. 面试题环节

# 1.1 请说下 MVVM 的理解

MVC 和 MVVM

在传统的 mvc 中除了 model 和 view 以外的逻辑都放在了 controller 中, 导致 controller 逻辑复杂难以维护,在 mvvm 中 view 和 model 没有直接的关系, 全部通过 viewModel 进行交互,

# 👉1.1.1 Vue 具有的特点

特点

  • Vue 具有的特点
    • Vue 是响应式变化, 如果数据更新了, 默认会刷新视图
    • Vue 使用对象的时候, 必须先声明属性, 这个属性才是响应式的
    • Vue 中如果数组里放的是对象, 对象是支持响应式变化的,常量则没有效果
    • Vue 中修改数组索引和长度, 是不会导致视图更新的
    • Vue 中如果数组中新增的数据是对象类型, 也会支持响应式变化
    • Vue 不会再本轮代码执行的时候去渲染 dom, 而是在下一个事件环中执行(不这样做的话,就会频繁刷新 dom) (下一个事件环有, promise.then, mutationobserver, setimmediate settimeout)

以下代码演示


  • Vue 中绑定什么样的数据可以具有响应式变化?
  • Vue 增加不存在的属性, 不能更新视图
  • Vue 默认会递归所有数据, 增加 getter 和 setter

Object.defineProperty 用法



































 


 











function observer(obj) {
  // 判断是不是对象,如果不是对象则直接 返回这个数据
  if (typeof obj !== 'object' || typeof obj == null) {
    return obj
  }
  //否则就是对象, 接着遍历这个对象
  for (let key in obj) {
    // 3个参数,分别是- 原对象, 键 , 值
    defineReactive(obj, key, obj[key])
  }
}

function defineReactive(obj, key, value) {
  //   Object.defineProperty() 方法
  //   会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象

  // 如果深层次监听数据, 需要递归的监听value 的值还是不是个对象
  observer(value)
  Object.defineProperty(obj, key, {
    get() {
      return value
    },
    set(newValue) {
      // 给某个key 设置值的时候, 可能也是个对象
      observer(newValue)
      console.log(newValue)
      if (value != newValue) {
        value = newValue
        console.log('视图更新')
      }
    }
  })
}
let data = {
  name: 'zd'
}
observer(data)

// 更改数据视图更新
data.name = 'ls'

// 增加一个不存在的属性, 视图没有更新
data.a = '15'

// 给属性赋值为对象
data.name = {
  id: 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
48

# 1.2 Vue 实现数据双向绑定的原理

双向绑定

  • 使用 Object.defineProperty()方法
  • 递归的监听对象的每个属性,给每个属性都加上 setter 和 getter
  • 对于数组的监听, 进行重写数组原型的原有方法,但不能改变原有方法

# 1.3 Vue 常用的指令

指令

  • v-once,值渲染一次
  • v-html, 将 html 渲染到页面中, 不要将用户输入的 html 展示在这个里边, 会导致 xss 攻计
  • v-bind,绑定属性, 简写 :
  • v-if/v-else,判断
  • v-show, 隐藏,显示
  • v-for, 遍历数据

# 1.4 v-model 的原理

原理: value 属性和 input 事件的组合使用

  • 用在子组件改变父组件的值

只要是能改的,都可以进行双向绑定, 组件也可以双向绑定

<!-- v-model原理 就是下边这个 -->
<input type="text" :value="value" @input="e => e.target.value = value" />
<!-- 使用v-model -->
<input type="text" v-model="value" />
1
2
3
4

# 1.5 v-if 和 v-show 的区别

区别

  • v-if 是“真正”的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
  • v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块
  • v-show 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换

比较:v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销

因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好

# 1.6 Vue 中 key 值的作用

  • 1.区分元素

注意

尽量不要使用 index 作为 key 值,举例来说,假如有个数组,让它反续排列,就会所有的数据进行重新比较,然后,重写创建 只是静态展示的话, 可以使用 index

# 1.7 computed 和 watch 有什么区别?

区别

  • computedwatch 用起来的时候他俩没有啥关系,只不过有时候可以实现相同的功能
    • computed: 会根据其它的值来计算,多次取值是有缓存效果的,如果依赖的值变化, 才会重新执行
      • 内部还使用了 defineProperty, 所以它还有 set 和 get 方法
    • watch: 监控某个值的变化, 每个值的变化, 都能执行相应的回调
    • 这俩方法都是基于 vm.$watch 来实现的

注意: 内部原理实现参考 👉 computed 和 watch

# 1.8 Vue 的声明周期, 每个声明周期具体适合哪些场景?

参考 👉 Vue 声明周期

# 1.9 Vue 的 ref 是什么?

参考 👉 ref 组件传值

# 1.10 Vue 动画的生命周期?

参考 👉Vue 中的动画

# 1.11 Vue 如何编写自定义指令?

参考 👉Vue 自定义指令


# 2. Vue 数据响应式原理

# 👉 2.1 响应原理(只针对对象)

  • vue 数据观察者原理, 给每个对象的 key 值,都添加 set get 方法, 缺点就是递归数据,消耗性能
// 监听函数
function observer(obj) {
  // 判断是不是复杂数据类型
  if (typeof obj !== 'object') {
    return
  }
  // 循环所有的key 值
  for (let key in obj) {
    defineReactive(obj, key, obj[key])
  }
}

function defineReactive(obj, key, value) {
  // 如果value 还是个对象, 继续递归
  observer(value)
  // 监听数据
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      return value
    },
    set(newVal) {
      // 判断如果这个值还可能是个对象
      if (typeof newVal === 'object') {
        observer(newVal)
      }
      console.log('更新:' + newVal)
      value = newVal
    }
  })
}
// 对象
let a = {
  id: 10,
  name: {
    str: 'li四'
  }
}

observer(a)

a.id = 20
a.name.str = '阳光'

console.log(a) // { id: [Getter/Setter], name: [Getter/Setter] }
console.log(a.id) // 20
console.log(a.name) // { str: [Getter/Setter] }
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

# 👉 2.2 响应原理(添加对数组的支持)

  • 处理数组原理就是拷贝数组原型的原有方法, 然后进行重写数组的所有方法
let arrayProto = Array.prototype // 数组原型上的方法
let proto = Object.create(arrayProto)

;['push', 'unshift', 'splice', 'reverse', 'sort', 'shift', 'pop'].forEach(
  method => {
    proto[method] = function(...args) {
      // 这个数组应该也被监控
      // Array.prototype.push.call([1,2,3],4,5,6);
      let inserted // 默认没有插入新的数据
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice': // 数组的splice 只有传递三个参数 才有追加效果
          inserted = args.slice(2)
        default:
          break
      }
      console.log('视图更新')
      ArrayObserver(inserted)
      arrayProto[method].call(this, ...args)
    }
  }
)
function ArrayObserver(obj) {
  for (let i = 0; i < obj.length; i++) {
    let item = obj[i]
    // 如果是普通值 就不监控了
    observer(item) // 如果是对象会被 defineReactive
  }
}
function observer(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return obj
  }
  if (Array.isArray(obj)) {
    //  上面处理的是数组格式 push shift splice (如果调用这三个方法)应该把这个值判断一下是否是对象
    Object.setPrototypeOf(obj, proto) // 实现一个对数组的方法进行重写
    ArrayObserver(obj)
  } else {
    // 下面的是处理对象的
    for (let key in obj) {
      // 默认只循环第一层
      defineReactive(obj, key, obj[key])
    }
  }
}

function defineReactive(obj, key, value) {
  observer(value) // 递归创建 响应式数据,性能不好
  Object.defineProperty(obj, key, {
    get() {
      return value
    },
    set(newValue) {
      // 给某个key设置值的时候 可能也是一个对象
      if (value !== newValue) {
        observer(newValue)
        value = newValue
        console.log('视图更新')
      }
    }
  })
}
let data = {
  d: [1, 2, 3, { name: 'zf' }]
}
observer(data)
data.d = [1, 2, 3]
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

# 3. Vue 的语法

# 3.1 实例上的常用属性方法

属性及方法

  • $el: 拿到真实的 dom 元素
  • $watch: 监控属性的变化,两个参数(newValue, oldValue), 会等待数据更新后,重新调用回调函数, 是一个延迟更新,多次更新,只会执行最后一次的结果,视图是异步更新的
  • $nextTick: 数据更新后会有一个队列, 将 watch 的 callback 放到队列中, 会将 nextTick 往后叠加
  • $data: 当前实例的数据
  • $options: 当前实例的整个属性,也就是传入的那个对象
  • $set,$delete: 更新属性
let vm = new Vue({
  el: '#app',
  data() {
    return {
      name: 'hf',
      age: {}
    }
  }
})
console.log(vm.$el)
vm.$watch('name', function(newValue, oldValue) {
  console.log(newValue)
})
vm.name = 'lq'
vm.name = 'lq1'

// $nexttick
vm.$nexttick(() => {
  console.log(vm.$el.innerHtml)
})
vm.$data
vm.$options
// $set
vm.$set(vm.age, 'age', 10)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 3.2 自定义指令

vue 如何自定义指令?

注意点:

  • 指令的功能是封装 dom 操作的
  • v-for 和 v-if 不能一起用
  • template 不能和 v-show 连用

指令有 2 或 3 部分组成: v-开头, 指令名, 修饰符

<input v-model.trim="value" type="text" />
1

指令分为: 全局指令,和局部指令 全局指令

<div id="app">
  <input type="text" v-focus />
</div>
1
2
3
/**
 * @params el:当前指令元素
 * @params bindings:元素上绑定的属性
 * @params vnode:虚拟节点, 可以拿到当前指令所在的 context上下文
 */
Vue.directive('focus', function(el, bindings, vnode) {
  // 这个函数只会在默认 `绑定` 和 `更新` 的时候才会执行
  // 只有依赖的数据发生变化,才会重新执行
})
1
2
3
4
5
6
7
8
9

上边代码等价于 👇

Vue.directive("focus", {
    // 绑定时
    bind(el, bindings, vnode) {

    },
    // 更新时
    update(el,bindings, vnode){

    },
    // 当被绑定的元素插入到 DOM 中时
    inserted(el,bindings, vnode){
        // 聚焦元素
        el.focus()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

指令钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用


# 3.3 computed 和 watch

  • computedwatch 用起来的时候他俩没有啥关系,只不过有时候可以实现相同的功能
    • computed: 会根据其它的值来计算
    • watch: 监控某个值的变化
    • 这俩方法都是基于 vm.$watch 来实现的

# 👉 3.3.1 watch原理实现

// 核心代码,传入一个对象
function initWatch(watch) {
  for (let key in watch) {
    //   遍历对象, 监听 key 和 value
    vm.$watch(key, watch[key])
  }
}
// 初始化watch
initWatch({
  name(newValue) {
    console.log(newValue)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# 👉 3.3.2 watch用法

  1. 函数式写法
let vm = new Vue({
  data: {
    name: 'hello'
  },
  watch: {
    name(newValue) {
      console.log(this.newValue) // 打印出 world
    }
  }
})
// 手动调用
vm.name = 'world'
1
2
3
4
5
6
7
8
9
10
11
12

  1. 对象式写法
  • 可以传入多个属性
    • deep: 为了发现对象内部值的变化, 在选项参数中指定 deep: true
    • immediate: 指定 immediate: true 将立即以表达式的当前值触发回调
let vm = new Vue({
  data: {
    name: 'hello'
  },
  watch: {
    name: {
      // 内部对应的观察函数
      handler(newValue) {},
      deep: true,
      lazy: true, // 就是computed 的实现
      immediate: true // 表示立即执行这个handler
    }
  }
})
// 手动调用
vm.name = 'world'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 👉 3.3.2 computed内部原理实现

computed 基本使用

let vm = new Vue({
  data: {
    name: 'hello'
  },
  computed: {
    fullname() {}
  }
})
1
2
3
4
5
6
7
8

原理实现

计算属性最大的特点 就是缓存

// 让方法有缓存, 脏检查
let dirty = true // 标志, 判断这个数据是不是脏的
// 1代码第一步
function initComputed(key, handler) {
  let value // 该变量保存handler 执行的结果
  // 代码第二步
  Object.defineProperty(vm, key, {
    get() {
      if (dirty) {
        value = handler()
        dirty = false // 如果已经执行过一次了, 就让这个标志置为 false
      }
      return value
    }
  })
}
// 初始化Computed
initComputed('fullname', () => {
  return vm.name + 'hai'
})

// 如果值更新了,再让这个 dirty 设置成false,否则就错了,
// 源码内部做了监听处理这个东西,让它自动 变成 true || false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 4. Vue 中的动画

Vue 提供了 transition 的封装组件,,可以给任何元素和组件添加进入/离开过渡,在下列情形中

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件 (路由)
  • 组件根节点

# 4.1 Vue 过渡的类名

  • 在进入/离开的过渡中,会有 6 个 class 切换
  1. v-enter:定义进入过渡的开始状态。
  2. v-enter-active:定义进入过渡生效时的状态
  3. v-enter-to:定义进入过渡的结束状态
  4. v-leave: 定义离开过渡的开始状态
  5. v-leave-active: 定义离开过渡生效时的状态
  6. v-leave-to: 定义离开过渡的结束状态

注意

  • 如果你使用一个没有名字的 <transition>,则 v- 是这些类名的默认前缀。
  • 如果你使用了 <transition name="my-transition">,那么 v-enter 会替换为 my-transition-enter。

# 4.2 Vue 动画,过度钩子

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"
  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
  <!-- ... -->
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
methods: {
  // --------
  // 进入中
  // --------

  beforeEnter: function (el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  enter: function (el, done) {
    // ...
    done()
  },
  afterEnter: function (el) {
    // ...
  },
  enterCancelled: function (el) {
    // ...
  },

  // --------
  // 离开时
  // --------

  beforeLeave: function (el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  leave: function (el, done) {
    // ...
    done()
  },
  afterLeave: function (el) {
    // ...
  },
  // leaveCancelled 只用于 v-show 中
  leaveCancelled: function (el) {
    // ...
  }
}
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

注意

  • 当只用 JavaScript 过渡的时候,在 enterleave 中必须使用 done 进行回调。否则,它们将被同步调用,过渡会立即完成。

# 5 Vue 生命周期

# 5.1 生命周期图

# 5.2 生命周期执行阶段

  • 钩子函数都是同步的, 即使某个钩子里有异步操作, 也不会阻塞
let vm = new Vue({
  // 挂载阶段
  beforeCreated() {
    // 创建之前, 基本上业务业务逻辑不需要它, 再使用 混合时会用到
    // 可以再这里给实例 增加一些属性, 方法
    console.log("before created")
  },
  created() {
    // 这个阶段中, 当前组件的实例数据已经实现了 数据劫持(setter 和 getter)
    // 把方法, 计算属性也都挂载到了实例上, 不能获取到真实的 dom
    console.log("created")
  },
  beforeMount() {
    // 再挂载之前会调用render 方法
    console.log("挂载之前")
  },
  /*==============================*/
        // 在beforeMount和 mounted这两个阶段会先走完子组件的挂载,才会触发父组件的 mounted
  /*==============================*/
  mounted() {
    //可以拿到上边所有的属性和数据
    console.log("当前组件挂载完毕")
  }
  // 更新阶段
  // vue的更新方式是组件级别的
  beforeUpdate(){
    // 可以再这里增加一些数据更新, 不会导致视图多次更新,
    // 触发此函数的前提是,更新的数据被应用到视图上了
    console.log("更新之前")
  },
  updated(){
    // 不要再去更新数据, 可能会发生死循环
    console.log("更新完成")
  },
  //销毁阶段
  //再手动移除组件(vm.$destory), 路由切换时会触发此函数,
  //触发后,vue 内部会移除所有的观察者, 移除监听事件
  beforeDestory(){
    // 可以再这里进行事件的移除, 定时器销毁
    console.log("销毁前")
  },
  destoryed(){
    console.log("销毁后")
  }
})
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

# 👉 5.2.1 总结

钩子函数总结

  • beforeCreate: 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
  • created: 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
  • beforeMount: 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted: el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  • beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  • updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy: 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed: Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

# 👉 5.2.2 函数中该做的事

钩子函数中该做的事情

  • created: 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。
  • mounted: 实例已经挂载完成,可以进行一些 DOM 操作
  • beforeUpdate: 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated: 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
  • destroyed: 可以执行一些优化操作,清空定时器,解除绑定事件

# 5.3 组件的钩子执行顺序

参考 👉组件的钩子函数执行顺序


# 6. Vue 组件

  • 组件化的好处: 方便复用, 好维护, 减少不必要的渲染

# 6.1 组件的创建方式

组件分为: 全局组件局部组件

  • 全局组件演示
import HelloWorld from './components/HelloWorld.vue'
Vue.component('HelloWorld', HelloWorld)
1
2
  • 局部组件演示
import HelloWorld from './components/HelloWorld.vue'
export default {
  components: {
    HelloWorld
  }
}
1
2
3
4
5
6

基础组件的自动化注册

注意

  • 组件注册完使用时,可以小写使用, 也可以大写使用
  • 比如上边的 HelloWorld组件使用时可以像下边这样(指在 template 模板中)
  • <HelloWorld></HelloWorld> 或者 <hello-world></hello-world>

# 6.2 父子组件的钩子函数执行顺序

  • 挂载阶段
    • 在 beforeMount 和 mounted 分隔开
组件挂载顺序
  • 父子组件的更新过程
组件更新顺序
  • 销毁过程
组件销毁顺序

# 6.3 父子组件通信

  • 包含以下多种普通传值方法

# 👉 6.3.1 props

  • 父组件向子组件传值

父组件

<!-- 动态传值, 使用v-bind绑定属性 -->
<HelloWorld :msg="msg" />
<!-- 传入普通值 -->
<HelloWorld value="我是非动态的" />
1
2
3
4

传入一个对象的所有属性

<blog-post v-bind="post"></blog-post>
1
post: {
  id: 1,
  title: 'My Journey with Vue'
}
1
2
3
4

子组件props接收

props 类型校验

export default {
  name: 'HelloWorld',
  props: {
    msg: String,
    value: {
      // 类型校验
      type: String,
      default: ''
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 👉 6.3.2 $on和$emit

  • 子组件向父组件通信, 通过事件触发
  • (子组件通过 $emit 触发事件, 父组件通过 $on 接收)
  • 原理就是发布订阅

父组件

<template>
  <HelloWorld :msg="msg" @changeParent="changeHandle" />
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  components: {
    HelloWorld
  },
  data() {
    return {
      msg: '我是父组件的值'
    }
  },
  methods: {
    changeHandle(value) {
      this.msg = value
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

子组件

<template>
  <div class="hello">
    {{ msg }}
    <button @click="change">change父的值</button>
  </div>
</template>

<script>
export default {
  props: {
    msg: String
  },
  methods: {
    change() {
      this.$emit('changeParent', '我是改变后的')
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 👉 6.3.3 $parent

  • 子组件直接找到父组件

父组件

<HelloWorld :msg="msg" />
1

子组件

<div class="hello">{{$parent.msg}}</div>
1

# 👉 6.3.4 $children

  • 当前实例的直接子组件。需要注意 $children不保证顺序,也不是响应式的,它获取的是个数组
<!-- 父组件 -->
<div>
  {{$children[0].child}}
  <HelloWorld :msg="msg" />
</div>
1
2
3
4
5
// 子组件的数据
export default {
  data() {
    return { child: '我是子组件' }
  }
}
1
2
3
4
5
6

# 👉 6.3.5 v-model

  • 父子组件通过v-model传值, 原理就是使用原生的输入框 input 事件 和 属性绑定的 value 值组合起来的
  • 局限: 属性只能绑定 value 而且只能是一个, 可以使用 v-model, 事件名必须是 input

父组件






 





















<template>
  <div>
    {{ msg }}
    <HelloWorld :value="msg" @input="change"></HelloWorld>
    <!-- v-model 实现,就是上边这个方法和属性绑定运算的语法糖,跟上边的功能是一样的 -->
    <HelloWorld v-model="msg"></HelloWorld>
  </div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  components: {
    HelloWorld
  },
  data() {
    return {
      msg: '我是父组件的值'
    }
  },
  methods: {
    change(val) {
      this.msg = val
    }
  }
}
</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

子组件













 





<template>
  <div class="hello">
    <button @click="changeCount">更改序号</button>
  </div>
</template>
<script>
export default {
  props: {
    msg: String
  },
  methods: {
    changeCount() {
      this.$emit('input', 100)
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 👉 6.3.6 $attrs

  • 包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外),
  • 通过 v-bind="$attrs" 传入内部组件
  • 是获取父组件的所有属性的值
<!-- 父组件 -->
<HelloWorld :msg="msg" value="10"></HelloWorld>
1
2
<!-- 子组件 -->
<div class="hello">
  {{$attrs.msg}}
  <h2>{{$attrs.value}}</h2>
</div>
1
2
3
4
5

# 👉 6.3.7 $listeners

  • 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
<!-- 父组件中使用子组件-->
<HelloWorld @click="changeHandle"></HelloWorld>
1
2

通过$listeners.click,就可以拿到再组件上绑定的事件回调函数

<!-- 子组件 -->
<button @click="$listeners.click">点我</button>
1
2

组合使用, 可以一次想把属性和事件都传下去

<!-- v-bind  将属性全部向下传递   v-on 将方法全部进行向下传递-->
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>
1
2

# 👉 6.3.8 provide / inject

  • provide 和 inject(开发中基本不会用)
    • 作用: 再父组件中声明一个公共数据, 在子组件中可以注入
    • 原理: 再父组件中声明一个 provide 属性, 在注入的时候会通过这个链(也就是$parents)不停的向上查找
    • 问题: 由于不停的向上找, 就会比较混乱, 层级很多的时候不知道找的是谁的

大致原理模拟代码如下

vm._provide
while (parent._provide) {
  parent = parent._provide
}
1
2
3
4

# 👉 6.3.9 ref

Ref 是什么?

ref 被用来给元素或子组件注册引用信息。

  • 引用信息将会注册在父组件的 $refs 对象上
  • 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素
  • 如果用在子组件上,引用就指向组件实例
  • 可以再父组件中直接获取子组件的方法和数据
<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>
1
2
3
4
5

演示使用

父组件

<template>
  <div>
    <HelloWorld ref="helloworld"></HelloWorld>
    <button @click="getChild">获取子组件的数据和方法</button>
  </div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  components: {
    HelloWorld
  },
  methods: {
    getChild() {
      const { value, changeHandle } = this.$refs.helloworld
      // eslint-disable-next-line no-console
      console.log(value) // 我是子的数据
      changeHandle() // function 执行
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

子组件

<template>
  <div>
    {{ value }}
    <button @click="changeHandle">change Myself</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      value: '我是子的数据'
    }
  },
  methods: {
    changeHandle() {
      this.value = 'change child data'
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 👉 6.3.10 eventbus

  • 缺陷: 只能通过绑定 $on 的那个组件来触发,其它组件不能触发
  • 缺点:比较混乱, 任何事件都可以往上边绑事件
  • 用途: 再不使用 vuex 的情况下, 处理兄弟组件通信还是很好用的(偶尔用一次)
  • 原理: 就是发布订阅模式 用法

给Vue的原型扩展$bus方法

// 创建一个全局的发布订阅
Vue.prototype.$bus = new Vue()
1
2

组件中订阅

this.$bus.$on('change', payload => {
  console.log(payload)
})
1
2
3

组件中的 mountd 钩子中触发

this.$bus.$emit('change', payload)
1

组件中触发的晚一些

this.nextTick(() => {
  this.$bus.$emit('change', payload)
})
1
2
3

# 7. render 函数

  • 类型:(createElement: ( ) => VNode) => VNode
  • 字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力
  • 该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode
  • 如果组件是一个函数组件,渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息

注意

Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。

如果你的组件中只写下边的 js,那它会渲染出一个 h1

这样的组件也称为函数组件, 可以直接命名为 .js 文件

// template 不能和 render 共存, 有template 时会覆盖掉 render渲染的信息
export default {
  render(h) {
    //h 其实就是createElement 函数, 接收3个参数,
    // 第一个参数: 标签名
    // 第二个参数: 标签上的属性(可选)
    // 第三个参数: 渲染的内容(可选)
    return h('h1', {}, '我好') // 这里是虚拟节点 Vnode
  }
}
1
2
3
4
5
6
7
8
9
10

# 7.1 render 函数使用(官网例子)

官方例子

App.vue

<template>
  <div>
    <Level type="1"></Level>
    <Level type="2"></Level>
    <Level type="3"></Level>
    <Level type="4"></Level>
    <Level type="5"></Level>
    <Level type="6"></Level>
  </div>
</template>
<script>
import Level from './components/Level.js'
export default {
  components: {
    Level
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Level.js

最后渲染出来的就是 h1 -> h6 的 '我好'

export default {
  props: {
    type: {
      type: String
    }
  },
  render(h) {
    return h('h' + this.type, '我好')
  }
}
1
2
3
4
5
6
7
8
9
10

# 8. slot 插槽

  • 允许用户自定义标签渲染, 进行分发内容

# 8.1 具名插槽

# 👉8.1.1 普通具名插槽

App.vue

<template>
  <List>
    <h2 slot="header">header</h2>
  </List>
</template>

<script>
import List from './components/List'
export default {
  components: {
    List
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

子组件 List.vue

<template>
  <div class="container">
    <slot name="header"></slot>
  </div>
</template>
1
2
3
4
5

# 👉8.1.2 v-slot 具名插槽

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

<script>
import List from './components/List'
export default {
  components: {
    List
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 8.2 v-slot 插槽

App.vue

<template>
  <div>
    <List :arr="arr">
      <template v-slot="{ item }">{{ item }}</template>
    </List>
  </div>
</template>

<script>
import List from './components/List'
export default {
  components: {
    List
  },
  data() {
    return {
      arr: [1, 2, 3]
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

List.vue

<template>
  <ul>
    <li v-for="item in arr" :key="item">
      <slot :item="item"></slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: ['arr']
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8.3 作用域插槽

  • 用法:
    • 子组件 使用 <slot :b={item}></slot>
    • 父组件 使用 slot-scope="{b}"

App.vue

<template>
  <List :arr="arr">
    <template slot-scope="{ b }">
      <h1># {{ b }}</h1>
    </template>
  </List>
</template>

<script>
import List from './components/List'
export default {
  components: {
    List
  },
  data() {
    return {
      arr: [1, 2, 3]
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

List.vue

<template>
  <div>
    <template v-for="item in arr">
      <slot :b="item">
        <li :key="item">{{ item }}</li>
      </slot>
    </template>
  </div>
</template>

<script>
export default {
  props: ['arr']
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9. vuex 原理

# 9.1 实现 state

let Vue
// 提供一个 Stroe 方法
class Store {
  // 接收一个对象
  constructor(options = {}) {
    this.state = options.state
  }
}

// 当用户在外部调用 vue.use() 时, 会默认调用 插件的 install 方法
// install 方法 第一个参数是 Vue 构造器
// 参考 https://cn.vuejs.org/v2/api/#Vue-use
// https://cn.vuejs.org/v2/guide/plugins.html
const install = _Vue => {
  Vue = _Vue // 定义为全局的
  // 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,
  // 向组件注入自定义的行为, 参考 https://cn.vuejs.org/v2/api/#Vue-use
  Vue.mixin({
    beforeCreate() {
      // 由于,数据是共享的, 所以每个组件都需要拿到store中的数据,
      //   也就是每个组件都需要 有$store 属性
      // 1. 判断 根 实例 有木有传入store 数据源,如果传入了,就把它放到实例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子组件去取父级组件的$store属性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default { install, Store }
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

# 9.2 实现 getters

let Vue
// 提供一个 Stroe 方法
class Store {
  // 接收一个对象
  constructor(options = {}) {
    this.state = options.state
    this.getters = {}
    // 重新定义对象的每一个key, 将外部传入的函数,变成对象的形式
    Object.keys(options.getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })
  }
}

// 当用户在外部调用 vue.use() 时, 会默认调用 插件的 install 方法
// install 方法 第一个参数是 Vue 构造器
// 参考 https://cn.vuejs.org/v2/api/#Vue-use
// https://cn.vuejs.org/v2/guide/plugins.html
const install = _Vue => {
  Vue = _Vue // 定义为全局的
  // 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,
  // 向组件注入自定义的行为, 参考 https://cn.vuejs.org/v2/api/#Vue-use
  Vue.mixin({
    beforeCreate() {
      // 由于,数据是共享的, 所以每个组件都需要拿到store中的数据, 也就是每个组件都需要 有$store 属性
      // 1. 判断 根 实例 有木有传入store 数据源,如果传入了,就把它放到实例的 $store上
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store
      } else {
        // 2. 子组件去取父级组件的$store属性
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}

export default { install, Store }
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

# 9.3 让数据具有响应式

class Store {
  // 接收一个对象
  constructor(options = {}) {
    // 将数据放到 vue 的 data上
    this.myState = new Vue({
      data() {
        return {
          state: options.state
        }
      }
    })
    this.getters = {}
    Object.keys(options.getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => {
          return options.getters[key](this.state)
        }
      })
    })
  }
  // 参考 http://es6.ruanyifeng.com/#docs/class
  get state() {
    // 类的属性访问器
    return this.myState.state
  }
}
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

# 10. vue-router 实现原理

需要知道的几个 api

  1. location: 表示其链接到的对象的位置(URL)
location
  1. hashchange : 当 URL 的片段标识符更改时,将触发 hashchange 事件 (跟在#符号后面的 URL 部分,包括#符号)
  2. history:操作浏览器的曾经在标签页或者框架里访问的会话历史记录。
history
  1. popstate: 点击浏览器上的前进后退按钮触发的事件 popstate 事件

# 10.1 原生 js hash 路由原理实现

  • 利用 hashchange 事件监听 hash 的变化
<body>
  <a href="#/home">home</a>
  <a href="#/mine">mine</a>
  <div id="box"></div>
  <script>
    // 页面加载完毕
    window.addEventListener('load', () => {
      box.innerHTML = location.hash.slice(1)
    })
    //   监听路由变化
    window.addEventListener('hashchange', () => {
      box.innerHTML = location.hash.slice(1)
    })
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10.2 原生 js history 路由原理实现

  • 使用 history 自带的 api history.pushState() 方法
  • 结合 popstate 事件 监听浏览器的前进后退
  • 首次渲染会触发 404 页面的问题
<body>
  <a onclick="go('/home')">home</a>
  <a onclick="go('/mine')">mine</a>
  <div id="box"></div>
  <script>
    //   <!-- history 路由原理 -->
    function go(path) {
      history.pushState({}, '', path)
      box.innerHTML = path
    }
    //   监听前进后退
    window.addEventListener('popstate', function() {
      go(location.pathname)
    })
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 10.3 手写 vue-router

分析 vue 路由

  1. vue-router 对外暴露了 一个 VueRouter 类
  2. 既然 vue-router 是一个插件, 那就要提供一个 install 方法, 官方规定的, 那既然使用 install 方法注册组件 , 那就需要使用混入 mixin()
  3. 提供了 两个全局组件 <router-link/><router-view/>
  4. 提供了 两个全局属性 $router$route
  • 手写 1 创建一个 VueRouter 类, 并对外暴露, 还要续提供 install()

自己的vue-router.js

注意: 把引入路径换成自己的时候, 需要把 App.vue 中的route-viewrouter-link删除掉, 因为还没有写

<div id="app">
  App组件
  <!-- <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>-->
</div>
1
2
3
4
5
6
7
8
// 1 创建一个VueRoute类
class VueRoute {}

// 2.暴露一个 install 方法, 把Vue传进去
//
VueRoute.install = function(Vue) {
  console.log(Vue, 'install')
}
// 3. 导出VueRoute类
export default VueRoute
1
2
3
4
5
6
7
8
9
10

VueRoute.install 就是在这个类上提供一个静态方法,等价于 👇

class VueRoute {
  static install(Vue) {
    console.log(Vue, 'install')
  }
}
export default VueRoute
1
2
3
4
5
6

控制台输出

location