三、派发更新
当执行响应式对象的set
的时候,会触发派发更新的逻辑,首先他会尝试拿到value,如果value和传入的newVal的值相同,那么他会结束执行。之后会执行到val = newVal
进行赋值,然后会判断newVal
是否是一个对象,如果是对象那么会执行observe
把这个对象变成响应式。最后会触发 dep.notify()
进行派发更新
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
...
// #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()
}
dep.notify
函数首先拿到subs,然后会遍历subs,执行subs[i].update()
实际上就是执行和subs绑定的watcher的update(),wathcer
的update方法会执行到queueWatcher(this)
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()
}
}
queueWatcher
方法会先拿到watcher的id,watcher的id 是执行this.id = ++uid
得到的,这个id是一个数字的自增,所以每一个wathcher都有一个唯一的id。has
在最开始的时候是一个空的对象,拿到watcher的id,他会把has中这个id的key设置为true,如果当前的has没有这个id,那么,才会执行下边的逻辑,这样做的目的是,假如你修改了同一个渲染wathcer中的数据,他会执行多次的queueWatcher
,会往之后的执行队列中放入重复的watcher和更新,这样做避免了添加重复的watcher,即使修改了同一个渲染watcher的数据,那么在之后的flushSchedulerQueue
当中,也只会执行一次重新渲染,相当于是多次数据操作合并为了一次更新。之后会执行queue.push(watcher)
把当前的wathcer,push到queue数组当中,最后执行nextTick(flushSchedulerQueue)
// src/core/observer/scheduler.js
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)
}
}
}
nextTick(flushSchedulerQueue)
,其中的nextTick
是把flushSchedulerQueue
放到了下一个队列当中执行。flushSchedulerQueue
首先调用queue.sort((a, b) => a.id - b.id)
对queue队列根据id做一个排序,wathcer的id是先创建的id小,之后创建的id大,也就是父组件的watcher是要小于子组件的id的。为什么要做一个排序,1.组件的更新是由父到子的过程。2.组件的user watcher会在渲染watcher创建之前先创建。3.如果父组件执行了销毁,那么子组件也会被销毁,他所做的数据处理将会被跳过。排序之后会遍历queue
,首先判断watcher是否有before这个方法,渲染watcher在创建的时候,会传入before
这个函数, if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') }
实际上是会去触发他的beforeUpdate
生命周期。之后执行has[id] = null
,把has中的watcher id的保留置空,然后会执行watcher.run()
,watcher.run()
方法首先会调用const value = this.get()
对新的value进行求值,在调用this.get()
的过程当中,就会再次触发wathcer的getter,也就是之前传入的 updateComponent = () => { vm._update(vm._render(), hydrating) }
函数,对页面进行重新渲染。执行完run之后会调用resetSchedulerState
对has进行重置,执行到flushSchedulerQueue
的最后会调用callUpdatedHooks(updatedQueue)
,实际上是对queue队列中的wathcer去调用他们的update生命周期,执行顺序是由后向前,也就是会先触发子组件的updated
然后再去触发父组件的updated
函数。
// src/core/observer/scheduler.js
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.
...
}
...
resetSchedulerState()
// call component updated and activated hooks
...
callUpdatedHooks(updatedQueue)
...
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
// src/core/observer/watcher.js
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
...
}
}
}
四、nextTick
nextTick
的是在执行flushSchedulerQueue
的时候进行的一层处理,平时在开发中使用的this.$nextTick()和Vue.nextTick(),都是由nextTick函数实现。nextTick
函数首先会定义_resolve
,然后push我们传入的内容,之后会判断pending
,然后把pending
的值进行修改这样就保证了, 只会执行一次timerFunc
// src/core/util/next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
.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
})
}
}
timerFunc
的目的是把当前放入的任务,放到下一个执行队列去执行,其中做了大量的降级判断和处理,首先他会判断if (typeof Promise !== 'undefined' && isNative(Promise))
当前环境是否支持Promise
,如果支持那么会通过promise.then()
,来实现异步队列(promise.then是一个微任务),如果不支持promsie的话,他会判断当前环境是否支持MutationObserver
,MutationObserver
函数提供了监视对DOM树所做更改的能力,他也是一个微任务,如果支持的话,他会首先创建一个变量let counter = 1
,然后执行 const observer = new MutationObserver(flushCallbacks)
,把flushCallbacks
作为监听的回调函数,之后创建一个文本节点,通过observe
方法去监听这个文本节点,其中他设置了characterData: true
这个属性是用于监听这个元素的文本是否被修改,默认是flase,在每一次触发timerFunc
的时候,对文本节点的内容进行修改, counter = (counter + 1) % 2; textNode.data = String(counter)
,这样去模的操作,会保证这个文本节点的内容只会在1和0之间修改,不会造成最终这个文本节点的内容溢出。这样通过修改文本节点的内容,来触发MutationObserver
的observe
方法,再去触发对应的flushCallbacks
回调函数。如果不支持MutationObserver
的话他会采取setImmediate
,如果不支持setImmediate
函数,那么他会最终采用,setTimeout(flushCallbacks, 0)
作为下一个任务队列,这两个方式都是下一个队列当中的宏任务。也就是说vue的渲染实际上是异步的,他会通过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.
// 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)
}
}
五、set
在vue当中,我们给data中的数据进行修改,如果是一个对象,我们给他直接添加属性,或者我们在修改数组的时候,我们这样赋值arr[2]=xxx
,这两种操作对于vue都是监听不到的,在页面中不会进行重新重新渲染,vue官方也提供了set
这个api来帮助我们进行操作。set第第一个参数可以传递一个数组或者一个对象,第二个参数传入的是key,第三个参数传的是value。如果传入的值是一个数组,首先他会让target(当前数组)的length等于当前最大的index,然后会调用当前数组的splice
方法对内容进行一个插入。如果是一个对象的话,那么他会首先判断这个key是否存在于当前的对象当中, 如果存在的话那么他会直接返回,不需要进行重新的渲染。如果当前的key不存在于当前的对象当中,也就是说他是一个新的key,那么他会尝试拿到当前对象的__ob__
属性,然后他会判断当前的对象是否是根部的data,如果是的话,他会报一个警告。如果!ob
也就是说不是一个响应式对象,也就是说给一个普通对象进行,set操作,那么他会往这个普通对象当中去进行一个赋值。如果该对象是一个响应式对象,那么他会首先执行defineReactive(ob.value, key, val)
,把新添加的key,变为一个响应式对象,最后手动调用ob.dep.notify()
,调用ob.dep.notify()
之后会触发页面的重新渲染,他会在渲染的时候执行响应式对象的时候let childOb = !shallow && observe(val)
,首先定义一个childOb
,把其中的对象定义下来,然后通过childOb.dep.depend()
进行对象的依赖收集,这样调用ob.dep.notify()
就会通知到对应的渲染watcher进行重新渲染。
// 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 (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
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
如果是一个数组,他会调用数组的splice
方法。在创建new Observer
的时候,如果传入的是一个数组,他会调用protoAugment
或copyAugment
方法。protoAugment
方法他会传入当前的数组和arrayMethods
,arrayMethods
实际上是Object.create(Array.prototype)
,也就是拿到了Array的原型对象。protoAugment
做的事是target.__proto__ = src
也就是让当前数组的原型指向了src,也就是形成了,当前数组的原型指向一个空的对象,这个空的对象的原型指向了Array.prototype
。
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
...
在初始化的时候,还会对arrayMethods
进行一个补充,methodsToPatch
中定义了一些数组的方法,之后对这些方法数组进行了遍历,首先定义const original = arrayProto[method]
,然后通过def
函数,对数据进行劫持,其中的value
就是mutator
。mutator
函数首先通过original.apply(this, args)
借带了Array.prototype
上对应的方法,拿到result,在最后把result,return出去。返回result的过程中,会先定义inserted
插入的值,如果有插入的值的话,会通过ob.observeArray
绑定响应式,最后调用ob.dep.notify()
进行重新渲染。无论是set
还是调用了push,splice这些方法,最终都是由vue去触发了ob.dep.notify()
进行重新的渲染。所以对data中的数据如果直接使用Array.prototype[methods]修改,也是不会动态的渲染到页面中。
// src/core/observer/array.js
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[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
})
})
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!