set
Vue.set
与vm.$set
是向响应式对象中添加一个property
,并确保这个新property
同样是响应式的,且能触发视图更新- 在
vue
中,一般会对初始化的数据进行响应式处理,但是如果对一个对象新增一个属性,如有obj= {id:1}
,现在相对obj
添加一个name
属性,直接写obj.name=‘xxx’
并不会触发视图更新,需要通过vm.$set(vm.obj,'name','xxx')
进行属性添加,这样才会使得这个属性为响应式的且能触发视图更新 - 上面有了解到
Vue.set
与vm.$set
都是添加一个响应式对象,首先,来看一下这两个set
方法的定义,下面有列出Vue.set
与vm.$set
的定义,可以看到,其实这两个方法最终都指向了set
// src\core\instance\state.js
Vue.prototype.$set = set
// src\core\global-api\index.js
Vue.set = set
- 下面是
set
方法的源码 - 1)传入三个参数:
taregt
需要进行属性设置的对象;key
需要设置成响应式的属性名;val
为属性名赋值的属性值 - 2)如果
taregt
为数组,则会判断key
是否为合法索引,是的话则会调用target
上的splice
方法进行属性设置。之所以采用splice
是因为在vue
进行响应式处理时,会数组进行重写,并将其中会更改原数组的方法进行响应式处理; - 3)如果
key
在对象中已经存在,则直接对该属性进行复制操作; - 4)获取
target
上的__ob__
即observer
对象ob
- 5)如果ob不存在,target不是响应式对象直接赋值
- 6)把
key
通过defineReactive
设置成响应式属性,并通过ob
发送通知
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 判断target是否是对象,key是否是合法的索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 通过splice对key位置的元素进行替换
// splice在array.js进行了响应式处理
target.splice(key, 1, val)
return val
}
// 如果key在对象中已经存在直接复制
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 获取target中的observer对象
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 如果ob不存在,target不是响应式对象直接赋值
if (!ob) {
target[key] = val
return val
}
// 把key设置成响应式属性
defineReactive(ob.value, key, val)
// 发送通知
ob.dep.notify()
return val
}
delete
- 删除对象的属性。如果对象是响应式的,确保删除可以出发视图更新,这个方法主要用于避开
vue
不能检测到属性被删除的限制。Vue.delete
与vm.$delete
同上面的set
相同,最终都是指向同一函数del
,下面具体看一下del
函数的实现 - 1)
target
需要删除属性的对象,key
需要删除的属性 - 2)判断是否为数组,以及
key
是否是有效索引,如果是数组,通过splice
进行删除,上面有提到splice
方法是做过响应式处理 - 3)获取
target
上的observer
对象__ob__
- 4)
target
如果是Vue
实例或者$data
对象,直接返回 - 5) 如果
target
对象没有key
属性直接返回 - 6)删除属性,并通过
ob
发送通知
export function del (target: Array<any> | Object, key: any) {
// 如果是undefined或者原始值,错误警告
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 判断是否为数组,以及key是否是有效索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 如果是数组,通过splice进行删除
// splice做过响应式处理
target.splice(key, 1)
return
}
// 获取target对象
const ob = (target: any).__ob__
// target如果是Vue实例或者$data对象,直接返回
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 如果target对象没有key属性直接返回
if (!hasOwn(target, key)) {
return
}
// 删除属性
delete target[key]
// 不是响应式对象,则直接返回
if (!ob) {
return
}
// 通过ob发送通知
ob.dep.notify()
}
nextTick
-
在下次
DOM
更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM
,如下所示:// 修改数据 vm.msg = 'Hello' // DOM 还没有更新 Vue.nextTick(function () { // DOM 更新了 })
-
在静态方法中的
Vue.nextTick
与实例上的vm.$nextTick
最终都是指向的\src\core\util\next-tick.js
下的nextTick
方法,下面具体看一下nextTick的实现
-
1、在前面的文章中知道,当更改一个数据时,会进行派发更新,在
queueWatcher
(src\core\observer\scheduler.js)函数中最终会通过nextTick(flushSchedulerQueue)
来实现flushSchedulerQueue
的调用; -
2、那么在通过
nextTick
获取DOM
上的最新数据时,其实DOM
还未渲染到视图上,而DOM
真正渲染是是在这次的事件循环之后; -
3、而在
nextTick
之所以可以获取到DOM
上的最新数据,是由于此时已经生成了DOM
树,而最新的数据也是从DOM
树上获取到的
- 在
nextTick
函数中,会将加入异常处理的cb
存入callbacks
数组中,之后通过timerFunc
来进行回调的调用
// \src\core\util\next-tick.js
let _resolve;
function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// 1.将传入的 cb 方法添加到回调数组
callbacks.push(() => {
cb.call(ctx);
});
// 2.执行异步任务
// 此方法会根据浏览器兼容性,选用不同的异步策略
timerFunc();
}
- 下面来一下
timerFunc
,可以看到timerFunc
就是根据浏览器的兼容性,采用不同的异步策略,顺序为promise
->MutationObserver
->setImmediate
->setTimeout
// \src\core\util\next-tick.js
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} 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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
- 在
timerFunc
会执行flushCallbacks
,该函数会一次执行cb
// \src\core\util\next-tick.js
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}