开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情
Vue.set 方法实现
我们开发过程,常遇到一种情况,需要在对象上添加一个属性, 如果我们直接用 test.a = 1 的方式进行添加,这个过程 Vue 是无法检测到的。 因为 Vue 在对对象进行依赖收集是,会对每个对象属性都进行依赖收集,而通过 test.b 的方式添加的属性没有在依赖收集的过程,那么属性 b 也就无法进行响应式化。
为了解决这一问题, Vue 提供了 Vue.set(target, property, value) 的静态方法和 vm.$set(target, property, value) 的实例方法来往对象上添加属性,来看一下 set 的具体实现
function set (target: Array<any> | Object, key: any, val: any): any {
// target 必须是非空对象
if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 对于 target 是数组的情况下,调用重写的 splice 方法,对新添加的元素进行依赖收集
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 新增对象的属性已经存在时,直接返回
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (!ob) {
target[key] = val
return val
}
// 调用 defineReactive , 为新增的属性设置 getter setter
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
对上面代码进行总结
- 目标对象必须是非空对象,可以是数组
- 如果目标对象是数组时,调用重写的
splice方法 - 如果新增的属性在目标对象中已经存在,则手动访问新的属性值,这一过程或触发依赖收集
- 调用
defineReactive方法,为新增的属性设置settergetter
nextTick
在前面的分析中,我们知道,当数据发生改变时,会触发 setter 方法进行依赖的派发更新,更新时会将 watcher 推到一个队列中,等待下一个 tick 到来时在执行 DOM 的渲染更新操作。现在我们来了解一下 nextTick 方法的实现。
事件循环机制
首先先来了解一下浏览器的事件循环机制
- 完整的事件循环机制包括两种异步队列
macro-task和micro-task macro-task成为宏观任务队列,常见的有setTimeout,setInterval,setImmediate,script 脚本,I/O操作,UI渲染micro-task称为微观异步队列,常见的有promise,process.nextTick,MutationObserver- 完整的事件循环流程为:
micro-task为空,macro-task队列只有script脚本,推出macro-task的script任务执行,脚本执行期间产生的macro-taskmicro-task推到对应的队列中- 执行
micro-task里面的微任务事件 - 执行
DOM操作,更新渲染页面 - 执行
web worker等相关任务 - 执行
macro-task队列中的宏任务
从上面的流程中可以看出,最好的渲染过程发生在微任务执行过程中,因此我们可以借助微任务队列来实现异步更新,这样可以让复杂的运算操作运行在 JS 层面,而视图层只关心最终的结果。
nextTick 基本实现
function renderMixin() {
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
}
const callbacks = []
let pending = false
function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// callbacks 是维护微任务的数组
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 将维护的队列推送到微任务队列中维护
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
nextTick 定义为一个函数,通过 Vue.nextTick(callback, ctx) 的方式进行使用,当 callback 经过 nextTick 封装之后, callback 会在下一个 tick 中执行调用。 callbacks 维护的是一个需要在下一个 tick 中执行的任务队列,它的每个元素都是需要执行的函数。 pending 是判断是否在等到执行微任务队列的标志。 timerFunc 是真正将任务队列推入微任务队列中的函数。
timerFunc 函数根据不同浏览器对 API 的支持不同,具有不同的实现
- 使用
Promise实现
上面关于浏览器的事件循环机制中提到, promise 属于微任务,因此可以通过 promise.then(callback) 的方式,将任务推入到微任务队列中。
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
// 使用为任务队列的标志
isUsingMicroTask = true
}
// 取出 callbacks 中的每一个任务函数并执行
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
- 使用
MutationObserver实现
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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
}
- 不支持上面两种微任务的方法,则使用宏任务的方法,优先使用
setImmediate
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
}
- 上述方法都不适用,则使用宏任务的中
setTimeout
else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
nextTick 使用场景
由于异步更新的原理,当我们在更新数据之后,并不会立即更新视图,需要等到下一次 tick 到来才会更新视图
<input v-if="show" type="text" ref="myInput">
// js
data() {
show: false
},
mounted() {
this.show = true;
this.$refs.myInput.focus();// 报错
}
数据发生变化时,视图并不会同时改变,因此需要使用 nextTick
mounted() {
this.show = true;
this.$nextTick(function() {
this.$refs.myInput.focus();// 正常
})
}