生命周期
// initMixin initRender
beforeCreate // 一般不会做什么操作
// initState/computed/watch 完成 响应式系统建立
created // 有this实例 可以做请求数据,重新赋值this.data
// 判断el -> template/outHtml -> render函数
beforeMount //对用户来说一般也不会干啥
// 对el进行替换
mounted // 组件挂载页面完成 可以进行DOM操作
父子生命周期顺序:
// f 父组件 c 子组件
f.beforeCreate -> f.created -> f.beforeMount -> c.beforeCreate -> c.created -> c.beforeMount -> c.mounted -> f.mounted
f.beforeDestory -> c.beforeDestory -> c.destoryed -> f.destoryed
// 关于update 父更新可能会影响子
// 如果父组件更新时修改了传递给子组件的porps(或者别的改变子组件的情况),那么就会影响到子, 否则不会影响到子
f.beforeUpdate -> c.beforeUpdate -> c.updated -> f.updated
// 而 子更新 可能会 影响父组件更新
// 1. 子组件只改变自己内部数据 不会触发父组件更新
// 2. 子组件$emit一个事件, 父组件在收到$emit事件后改变自己的(父组件)内部数据 就会触发父组件更新
f.beforeUpdate -> c.beforeUpdate -> c.updated -> f.updated
注意: update 父更新会不会影响子 ,可以参考下面 <标题3:组件更新细粒度>
一段关于生命周期的问题:
使用路由query进行数据通信,mounted 时拿到数据 this.A=this.$route.query.A, computed 里C() {return this.A},由于computed 比mounted 执行时机早(在template render化时就会调用,computed在beforeMount到mounted之间),所以computed 会执行两次(第一次初始渲染, 第二次依赖数据A发生变化),在第一次时由于A还没有值所以C是undefined, 如果模板取{{C.xx}} 会报错。 第一时间想到是容错处理,{{C && C.xx}}。 但如果在created 中取$route.query的值,就解决了,因为先执行created然后才会执行computed。就不需要容错处理,可以放心食用了!!!
key的作用
- 在v-for时使用key, 让diff过程更快
diff的主要步骤(updateChildren),就是找sameVnode,如果没找到就会进行新建或删除(代价较大)。
查找的主要过程: 首先有4个指针(老开始,老结束,新开始,新结束),相互比较是不是sameVnode(老开始vs新开始, 老开始vs新结束, 老结束vs新开始, 老结束vs新结束)。
如果比较完没有找到sameVnode,就对别的老节点遍历生成一个关于它们key和Index的map,然后查找这个map是否包含新开始的key。如果包含则认为是sameVnode。否则就会进行新建或删除。
因此key可以多判断一次sameVnode,让diff过程更快
但不推荐使用index作为key,因为在90%的情况下,用index做key和没有key是一样的效果。最好用id等唯一字段做key。
- 使用key避免vue默认使用原地复用策略
具体看官网这个例子: cn.vuejs.org/v2/guide/co…
- 使用动画时,使用key,原理和第二点一样, 避免复用标签,从而每次都是重新创建新标签,可以触发动画
组件更新细粒度
vue中每个组件都有自己的渲染watcher,当父组件更新时,并不会递归更新自己包含的所有子组件去更新,而只会更新传递给子组件的props和绑定的事件(如果需要更新)(prepatch过程)。
因此如果父组件中包含一个完全不依赖父组件信息的子组件(不需要接收父组件任何参数),当父组件重新渲染时, 该子组件是不需要有任何变动。
那接收父组件props的子组件怎么更新呢,这就又回到响应式数据原理,子组件在props初始化时会被defineReactive劫持成响应式数据,并且在render时,触发props的getter收集该子组件的渲染watcher作为它们的依赖。
上面说了虽然父组件不会递归更新子组件,但会更新传递给子组件的props, 当props改变就会触发setter, 然后就会通知props收集的依赖(子组件 渲染watcher)触发update,这里就是子组件重新渲染。
从而做到在父组件update时,被父组件影响的子组件也update, 没影响的不需要任何变动。
Vue.set
直接通过索引修改数组值,或给对象直接添加一个没存在过的属性
上面的操作都是不能触发vue响应式的。 因此提供了 Vue.set 去 曲线救国。它是怎么实现的? Vue.set (target, key, val)(target只能是数组或对象,并且在data中声明过)
-
如果target是数组,则会使用splice替换
target.splice(key, 1, val) -
如果target是对象
key in target成立,则说明是修改一个被响应式监听过的key, 直接赋值就可以target[key] = val- 否则判断target有没有被响应式监听过(被监听的对象都会有一个不能遍历的属性__ob__并指向this),
- 如果target没有被监听过, 则直接赋值并返回(configurable===false 就不会监听(如手动冻结Object.freeze))
- 否则被监听过,则通过defineReactive把新属性也变成响应式,并手动触发一次更新
this.dep.notify()
Vue对数组的响应式监听
- 利用Array.isArray(value)判断到响应式对象是数组时, 会将value(数组)的
__proto__指向arrayMethods - arrayMethods的值是
Object.create(Array.prototype)。定义一个methodsToPatch=push/pop/shift/unshift/splice/sort/reverse(都是可以修改原数组的方法),会让methodsToPatch中的每个数组方法在arrayMethods中重写
methodsToPatch.forEach(function (method) {
// cache original method
const original = Array.prototype[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
首先缓存下原生方法,并且最后返回的值也是调用原生方法返回的。只不过是截获了所有的方法,在他们调用时手动触发notify实现响应式。
发布订阅与观察者
- 发布订阅模式(Vue种的 Bus总线)
有3个主体: 发布者/订阅者/调度中心。 主体之间耦合度低, 发布者只管发布事件, 订阅者只管订阅事件, 剩下的就交给调度中心去操作(on/emit)。
只适合少量数据时使用,数据量大时容易造成命名冲突。例如好多组件都发布(emit)click事件, 某个组件要订阅click事件时就需要知道要订阅哪个组件发布的click事件(依靠事件名区别)。
class MyBus {
constructor() {
this.listens = {}
}
on(type, fn) {
if(!this.listens[type]) {
this.listens[type] = []
}
this.listens[type].push(fn)
}
emit(type, ...args) {
if(this.listens[type]) {
this.listens[type].forEach(fn => fn(...args))
}
}
}
let bus = new MyBus()
function f1() {
console.log('去哪吃')
}
function f2(address) {
console.log(`去${address}吃把`)
}
bus.on('eat', f1)
bus.on('eat', f2)
bus.emit('eat', '食堂') // 去哪吃 / 去食堂吃把
- 观察者模式(Vue种的 响应式系统)
观察者模式只有两个主体:观察者/目标。 在目标中记录谁观察了它(addObserver),改变时通知所有观察者(notify)
会创建多个这种体系(props/data中每个属性都有自己的一套体系,记录着哪些watcher观察了它,属性改变时通知所有观察它的 watcher去update)
class MyObserver {
constructor() {
this.subs = []
}
addObserver(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
let uid = 0
class MyWatcher {
constructor() {
this.id = ++uid
}
update() {
console.log(`我是${this.id}号观察者,我观察的对象有动静`)
}
}
let w1 = new MyWatcher()
let w2 = new MyWatcher()
let ob = new MyObserver()
ob.addObserver(w1)
ob.addObserver(w2)
ob.notify() // 我是1号观察者,我观察的对象有动静 / 我是2号观察者,我观察的对象有动静
nextTick
nextTick(cb)
首先得到一个 timerFn = promise/mutationObserver/setImmediate/setTimeout 中的一个(优先级从左到右 优先使用microtask)
timerFn = () => Promise.resolve().then(flushCallbacks) timerFn作用是异步刷新callbacks
正常调用nextTick(cb)时,会把所有cb push 到 callbacks中
if (!pending) {
pending = true
timerFunc()
}
并在pending为false时执行timerFunc,即执行flushCallbacks, 在flushCallbacks中让pending=false并执行所有cb
在vue中,触发属性响应式set时,会通知(notify)所有该属性收集的依赖(watcher)去update,watcher.update时会把this(当前watcher)加入到一个队列中(根据wathcer.id限制一个watcher只加入一次),然后再调用nextTick刷新队列 nextTick(flushSchedulerQueue)(flushSchedulerQueue调用逻辑与nextTick中timerFn差不多),然后执行队列中watcher的run函数(有一个根据id排序的过程),最终执行wathcer.get重新渲染页面
所以重新渲染页面可以看成是一个异步的操作,因此多次改变某个变量时(this.a = 1; this.a = 2...)只会取到最后一次的那个值。

computed 与 watch
- computed
computed会缓存计算结果(只有依赖的变量变化后才会重新计算,dirty控制)并且执行的时机是在beforeMount和mounted之间。
computed会在 initState / initComputed 时初始化,创建一个computed wathcer, 此时它的值为undefined, 并且会创建一个dep,当某个页面(渲染watcher)需要渲染这个计算属性时,就会触发该计算属性的getter,执行下面操作:
首先计算属性的dep会记录该 渲染watcher 观察了它(后期计算属性变化时,要通知该渲染watcher update),然后执行求值操作,此时让dirty=false(dirty控制计算属性是否需要重新求值,为true时计算属性才会重新求值),计算属性求值时又会触发它依赖的属性的getter,然后这个被计算属性依赖的属性的dep中会记录该computed wathcer观察了它。
然后在被计算属性依赖的属性的值变化后触发setter,通知观察它的computed wathcer 去update(此时会把dirty置为true),然后触发计算属性的setter, 然后触发计算属性dep中添加的渲染watcher 去update。
关于计算属性getter是否重新执行:
计算属性的 dep.subs.length === 0(即没有任何watcher观测它(虽然定义了一个计算属性,但没任何地方用到)),此时如果计算属性依赖的值发生变化后,仅仅将dirty置为true(需要重新求值)
计算属性的 dep.subs.length !== 0 直接执行计算属性的getter, 并将dirty置为false
- watch
watcher 在响应式系统是一个很重要的角色(简单的看上图)。
(1. user watcher)我们在组件中写的watch都属于user watcher,在执行到watcher.run时,执行watch传入的回调函数
(2. deep watcher)deep为false,在wathcer.get求值时触发watch的那个属性的gettrer,然后在watch的属性的dep中添加该user watcher,而deep为true时,在wathcer.get求值时会递归watch的那个属性,让它的子属性的dep中添加该user watcher。达到子属性改变时也能通知到该user watcher去update
(3. computed watcher) 如上介绍
(4. sync watcher) 在介绍 nextTickw时,我们讲了watcher.udate 会把wathcer推入一个队列, 在nextTick 刷新该队列(走到watcher.run 执行回调),而如果设置sync为true, 在update我们就会直接执行回调。
this.$options.data()
保存着每个组件的data默认值,做表单时可以用来提交成功后快速恢复默认值。
observable
小型Vuex实现可以使用observable
// observable.js
import Vue from 'vue'
export const stateObs = Vue.observable({
count: 0,
})
export const mutations = {
setCount(val) {
stateObs.count = val
},
}
// xx.vue
<template>
<div>
{{count}}
<div @click="handleClick">点击</div>
</div>
</template>
<script>
import { stateObs, mutations } from '@/observable'
export default {
computed: {
count() {
return stateObs.count
},
},
methods: {
handleClick() {
mutations.setCount(3)
},
},
}
</script>