总结下 Vue2 中部分常用的 api 的原理,方便开发与复习时备用
主要内容如下:
- 一、Vue2 实现原理简述:简要介绍 Observer、Dep、Watcher的作用
- 二、Vue2 响应式原理
- 三、Vue2 双向绑定原理
- 四、computed 的实现原理
- 五、nextTick 实现原理
一、Vue2 实现原理简述:简要介绍 Observer、Dep、Watcher的作用
Observer 的作用:
Vue2 通过 Object.defineproperty(obj, key, handle) 将我们我们代码中data中的属性进行getter与setter的响应式转化 这样data中的数据获取,数据改变就会触发注册过的get、set事件,从而触发视图更新等其他操作,这个 Object.defineproperty() 的过程,就是有 Observer 实现的
Vue2 中 Observer 的作用
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log('数据获取时,被触发');
return val;
},
set: newVal => {
if (val === newVal) {
return;
}
val = newVal;
console.log("数据改变时,被触发");
}
})
}
let data = {
test: '初始值',
};
// 对data上的test属性进行绑定
defineReactive(data, 'test', data.test);
console.log(data.test); // 数据获取时,被触发
data.test = 'hello Vue'; // 数据改变时,被触发
Dep 的作用:
data中有很多属性,但是我们可能只是用部分属性,Dep 就是用来收集获取data中属性对应的依赖,然后当触发 set 时, 通过发布订阅模式,通知执行收集的各个依赖,执行视图更新等操作
注:所谓的依赖就是Watcher
Dep 类似中间调度的一个功能中心,Dep 帮我们收集(究竟要通知到哪里的)。如下案例,我们知道,data 中有 test 和 msg 属性,但是只有 msg 被渲染到页面上, 至于 test 无论怎么变化都影响不到视图的展示,因此我们仅仅对 msg 变化影响到的更新操作进行收集即可
现在,对应属性 msg 的 Dep 就收集到了一个依赖,这个依赖就是用来管理 data 中 msg 变化的
Vue2 中 Dep 的作用
<div>
<p>{{msg}}</p>
</div>
data: {
test: '初始值',
msg: 'hello vue',
}
当使用 watch 监听 msg 属性的变化时,当 msg 变化时我们就要通知到watch这个钩子,让它去执行回调函数。 这个时候 msg 对应的Dep就收集到了两个依赖,第二个依赖就是用来管理 watch 中 msg 变化的
watch: {
msg: function (newVal, oldVal) {
console.log('newVal:',newVal, 'oldVal:',oldVal)
},
}
自定义computed计算属性,如下 newMsg 属性,是依赖 msg 的变化的。所以 msg 变化时我们也要通知到computed,让它去执行回调函数。 这个时候 msg 的Dep就收集到第三个依赖,这个依赖就是用来对应管理computed中 msg 变化的
注:一个属性可能有多个依赖,每个响应式数据都有一个Dep来管理它的依赖
computed: {
newMsg() {
return this.msg + '新的';
}
}
我们根据 Observer 中 Object.defineProperty() ,当读取某个属性就会触发get方法,进而进行依赖收集。代码如下:
function defineReactive (obj, key, val) {
let Dep; // 依赖对象
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log('数据被获取');
// 数据获取,将当前依赖进行收集
Dep.depend(); // 本次收集依赖
return val;
},
set: newVal => {
if (val === newVal) {
return;
}
val = newVal;
// 数据改变,通知收集的依赖去执行更新操作
Dep.notify(); // 给订阅者发消息,执行更新操作
console.log("数据改变,执行更新操作");
}
})
}
Watcher 的作用:
Watcher 就是被收集的依赖,上例中 msg 就对应了三个 watcher 实例依赖,当 msg 变化,会通知这三个 watcher, 这三个 watcher 会执行各自的操作,watcher 能够控制自己属于 data 属性中,还是 watch 数据监听中的,或者 computed 中的, 因此,Watcher 中要有两个方法,一个通知变化,执行更新操作,另一个就是将自身实例添加到 Dep 的依赖收集中
class Watcher {
addDep() {
// 将 Watcher 实例添加到 Dep 依赖收集中
},
update() {
// 当数据变更时,对应的执行渲染等更新操作
},
}
二、Vue2 响应式原理
- 简单理解就是 vue 主要做了三件事:数据劫持、依赖追踪、派发更新
- 1、第一步数据劫持:组件实例初始化的时候,先通过Object.defineproperty()给每一个Data属性都注册getter,setter
- 2、第二步依赖追踪:当组件实例挂载mount时创建一个Watcher实例,组件挂载会执行render function,进而通过set获取到data中的属性,将依赖的属性进行收集跟踪
- 3、第三步派发更新:当数据变化时,会触发相应的set,通过watcher实例通知订阅的属性进行视图更新
三、Vue2 双向绑定原理
- Vue2 双向绑定采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调
- 1、首先需要对数据进行劫持监听,实现一个监听器Observer,监听所有属性,如果有变动就通知订阅者
- 2、实现一个订阅者Watcher,根据订阅的属性变化通知,执行相应的更新函数,从而更新view视图
- 3、还需要一个解析器Compiler,可以记录和解析相应节点指令,根据初始化模版数据去初始化相应的订阅器
四、computed 的实现原理
computed 是一个惰性求值的观察者,comupted 内部实现了一个惰性求值的 computed watcher, computed watcher 同样不会立即求值,同时也有一个 Dep 实例,内部实现中通过 this.dirty 属性标记计算属性是否重新求值, 当 computed 依赖的状态改变时,就会通知 computed watcher,computed watcher 通过 this.dep.subs.length(就是上文中的中间调度收集的依赖订阅者), 判断是否有订阅者,如果有订阅者,computed 会重新求值,并且比对新值与旧值是否相同,如果不同,通知订阅者 watcher 进行重新渲染等操作
注:Vue 目的是为了确认最终计算的值发生变化才会触发 watcher 重新渲染,不仅仅是依赖的属性发生变化,以此提高性能, 只有之后其他地方需要获取当前计算属性时,computed 才会真正计算,实现惰性执行的目的
五、nextTick 实现原理
注:理解 nextTick 之前要先理解 javascript 事件循环
通常我们会碰到这种需求:当某一个状态改变后, Dom 重新渲染完成,获取对应组件元素高度, 如果属性改变后我们直接获取元素高度,获取的数值是错误的, 这个时候就需要使用 nextTick,通过在 nextTick 的回调函数内进行 Dom 的操作
状态改变直接获取数值为什么会错误?
<template
<div class="test">
<el-button type="primary" @click="showContent">点击{{ !isShow ? '显示':'隐藏'}}</el-button>
<div class="content" ref="content" v-if="isShow">内容</div>
</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
isShow: false
}
},
methods: {
showContent() {
this.isShow = !this.isShow
const content = this.$refs.content
console.log('同步获取',content) // 同步获取 undefined
this.$nextTick(() => {
const content1 = this.$refs.content
console.log('异步获取',content1) // 异步获取 <div class="content">内容</div>
console.log('高度:', content1.clientHeight) //
})
}
}
};
</script>
<style>
.content {
width: 100%;
height: 60px;
line-height: 60px;
background: gray;
}
</style>
Vue 官方描述: 可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。 如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。 然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate, 如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替
直白的说,就是数据更新是同步的,视图更新是异步的,当我们数据更新完毕,视图的更新还处在任务队列中, 如果我们频繁更新数据,Vue 不会马上更新视图,通过将更新操作放入队列,同时进行去重处理,提高性能, 最后,我们需要通过 nextTick 的回调函数,告诉 Vue 底层,当视图更新完毕帮我们执行 nextTick 的回调函数, 完成我们的需求
在设置 this.isShow = !this.isShow 的时候,Vue并没有马上去更新DOM数据,而是将这个操作放进一个队列中;如果我们重复执行的话,队列还会进行去重操作; 等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿出来处理,提升整体性能
源码分析:nextTick 函数实现,pending 控制 timerFunc 同一时间只能执行一次
const callbacks = []
let pending = false
let timerFunc
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) { // 执行回调
try {
cb.call(ctx)
} catch (e) {
handlerError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true // 控制 timerFunc 同一时间只能执行一次
timerFunc() // 兼容异步函数的函数,用来执行callbacks队列
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc 函数实现目的: 对当前环境进行不断的降级处理,尝试使用原生的Promise.then、MutationObserver和setImmediate, 上述三个都不支持最后使用setTimeout;降级处理的目的都是将flushCallbacks函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4), 等待下一次事件循环时来执行
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 判断原生是否支持 Promise
const p = Promise.resolve()
timerFunc = () => {
// flushCallbacks 用来执行callbacks中的回调函数
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]') {
// 判断原生是否支持 MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 判断原生是否支持 setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 以上都不行,最终兼容 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
timerFunc 函数实现目的: 用来执行callbacks中的回调函数
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}