Vue 实例对象的
$set方法对应的是set方法,$delete对应的是del。
位置: src/core/instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del
$set
位置:src/core/observer/index.js
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
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)}`)
}
// 数组
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 (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或者val做响应式
defineReactive(ob.value, key, val)
// 通知更新
ob.dep.notify()
return val
}
$delete
位置:src/core/observer/index.js
/**
* Delete a property and trigger change if necessary.
*/
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
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
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
// 通知更新
ob.dep.notify()
}
$watch
位置: src/core/instance/state.js
// 使用
watch:{
list:{
handler(newVal,oldVal){},
immediate:true,
deep:true
}
}
this.$watch('exp',function(newVal,oldVal){
})
// 源码
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 如果回调cb是对象
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // 表明这是一个用户watcher,而不是组件的render watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// 设置immediate理解执行一次cb
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回取消监听的函数
return function unwatchFn () {
watcher.teardown()
}
}
}
异步更新队列
1. 两个核心点
- vue更新dom是异步的
- 是批量更新的
同时对同一个数据进行多次操作的时候,dep通知watcher更新的时候,watcher做了去重
2. 测试代码
<body>
<div id="demo">
<h1>异步更新</h1>
<p id="p1">{{foo}}</p>
</div>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: { foo: '' },
mounted() {
setInterval(() => {
this.foo = 1 // 执行入队
this.foo = 2
this.foo = 3 // 起作用的值是最后一行
console.log(p1.innerHTML)
this.$nextTick(() => {
console.log(p1.innerHTML)
})
}, 1000);
}
});
</script>
</body>
上面的代码给foo进行了3次赋值,这时候触发了Object.defineProperty中的3次set,那么dep通知了watcher3次去更新
3. 数据劫持-数据变更时通知watcher更新
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
// 将新值赋到旧值上
val = newVal
}
childOb = !shallow && observe(newVal)
// 通知更新
dep.notify()
}
})
注意:上面最重要的两点
// 将新值赋到旧值上
val = newVal
// 通知更新
dep.notify()
4. dep.js
通知watcher去更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()// 更新
}
}
5. watcher.js-入队操作
更新真正执行的是queueWatcher方法,也就是watcher入队操作,这时候需要思考一个问题,上面操作了3次foo,那么会入队3次吗?
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// watcher入队操作
queueWatcher(this)
}
}
6. queueWatcher方法
这里做了去重,所以上面3次对foo重新赋值,只有第一次入队了,入队操作后悔执行nextTick方法,那么这时候大家就会发出疑问了,如果是这样的话,foo最终的值是不是1呢?接着往下看nextTick
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 去重
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 不存在队列中才入队
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 异步刷新队列,下一帧,下一个时刻,下一个更新周期
nextTick(flushSchedulerQueue)
}
}
}
7. nextTick怎么实现异步刷新队列?
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
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.
// Techinically 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)
}
}
整个vue在初始化的时候会判断一下当前的运行环境,来决定我们用什么做异步,
- 首选Promise
promise.then是微任务,微任务会在浏览器更新前执行,也就是说微任务执行完,浏览器才更新
-
MutationObserver
-
setImmediate
-
setTimeout
8.执行队列中的方法
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
watcher.run()
综上所述:foo的数据是实实在在的修改了3次,但是更新到页面仅仅更新了1次,而且因为更新操作是异步的,所以尽管watcher只有第一次入队成功了,但是最后执行队列中的任务时,foo的值已经第3次修改完了,所以更新到页面上的时候,取了第3次的值
最后
vue的异步更新,涉及到了微任务与宏任务,单独出个专题介绍吧~
参考
vue源码 标题