As mentioned before, the Object. DefifineProperty () is used for Vue’s responsive processing of data during initialization. The getter method for Object attributes is defined to intercept the access to Object attributes, and the dependency collection is used.
Inform the update
setter
Intercepting setter functions are triggered when reactive data changes. Let’s start with setters:
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// ...
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.// ...
// Hijack the modify operation
set: function reactiveSetter (newVal) {
[key] / / the old obj
const value = getter ? getter.call(obj) : val
// If the old and new values are the same, return without updating
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
/* eslint-enable no-self-compare */
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
// If the setter does not exist, the property is read-only and returns directly
if(getter && ! setter)return
// Set the new value
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// Observe the new value so that it is also responsivechildOb = ! shallow && observe(newVal)// Rely on notification updates
dep.notify()
}
})
}
Copy the code
dep.notify()
// src/core/observer/dep.js
// Notification update
notify () {
const subs = this.subs.slice()
if(process.env.NODE_ENV ! = ='production' && !config.async) {
subs.sort((a, b) = > a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Copy the code
To traverse the watcher stored in the DEP, execute watcher.update().
watcher.update()
// src/core/observer/watcher.js
export default class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
// Go here for lazy execution, such as computed watcher
// Set dirty to true, and the evaluation of the calculated attributes is recalculated
this.dirty = true
} else if (this.sync) {
$watch = vm.$watch = vm.$watch
// When true, the watcher does not run through the asynchronous update queue and executes the this.run method to update data
// This attribute does not appear in the official documentation
this.run()
} else {
// Put watcher in the watcher queue
queueWatcher(this)}}}Copy the code
queueWatcher
// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/** * Put watcher in queue */
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// If watcher already exists, skip it
if (has[id] == null) {
// Caches watcher. Id to determine whether watcher is in the queue
has[id] = true
if(! flushing) {// The queue is not refreshed. Watcher joins the queue
queue.push(watcher)
} else {
// Flushing the queue, the user might add a new Watcher and go here
// Find the first position where watcher.id is greater than the watcher.id in the current queue, and insert yourself into that position. Keep the queue in order.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
// Waiting ensures that nextTick is called only once
if(! waiting) { waiting =true
if(process.env.NODE_ENV ! = ='production' && !config.async) {
// Refresh the scheduling queue directly
Vue is executed asynchronously by default. If you change it to synchronous execution, performance will suffer
flushSchedulerQueue()
return
}
// nextTick => vm.$nextTick, vue.nexttick
nextTick(flushSchedulerQueue)
}
}
}
Copy the code
NextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue nextTick flushSchedulerQueue
Let’s see what flushSchedulerQueue does:
flushSchedulerQueue
// src/core/observer/scheduler.js
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort the queue from smallest to largest.
// 1. The component is updated from parent to child. Since the parent component is created before the child component, the watcher is also created after the parent component.
// 2. The user watcher of a component is executed before the render watcher, and the user Watcher is created before the render watcher.
// 3. If a component is destroyed during the parent component's Watcher execution, its corresponding Watcher execution can be skipped, so the parent component's Watcher should be executed first.
queue.sort((a, b) = > a.id - b.id)
// Queue.length is evaluated each time through the loop, because at watcher.run() it is likely that the user will add a new watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
Execute beforeUpdate lifecycle hooks that are passed in when Watcher is created in the mount phase
if (watcher.before) {
watcher.before()
}
// Clear the cached watcher
id = watcher.id
has[id] = null
// Execute watcher.run to trigger the update function
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}}}// Hold a copy of the queue until the state is reset
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
// Reset refresh queue status
resetSchedulerState()
// The keep-alive component is related
callActivatedHooks(activatedQueue)
// Executes the updated lifecycle hook
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')}}/** * Return some of these variables to their original values and clear the Watcher queue. * /
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if(process.env.NODE_ENV ! = ='production') {
circular = {}
}
waiting = flushing = false
}
/** * The updated lifecycle hook */ is executed from child to parent
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')}}}Copy the code
FlushSchedulerQueue executes an update queue. Trigger the final update with watcher.run().
watcher.run()
// src/core/observer/watcher.js
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
this.cb = cb
}
run () {
if (this.active) {
// Call the this.get method
const value = this.get()
if( value ! = =this.value || // New and old values are not equal
isObject(value) || // New values are objects
this.deep / / deep patterns
) {
// Update the old value to the new value
const oldValue = this.value
this.value = value
if (this.user) {
// If the user is watcher
const info = `callback for watcher "The ${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// Render watcher, this.cb = noop, an empty function
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
Copy the code
There are two cases in which this.user represents the user watcher when it is true, which was described earlier as the user Watcher, otherwise the logic of rendering Watcher is executed.
- user watcher
The first argument received by invokeWithErrorHandling is our custom listener callback. During initialization of the listener initWatch method, New Watcher(VM, expOrFn, CB, options) is passed in for instantiation. The third argument is [value, oldValue] (new value and oldValue), which is why new and old values can be obtained in callback functions that listen for properties.
// src/core/util/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
// Use try catch to do some error handling
try {
res = args ? handler.apply(context, args) : handler.call(context)
if(res && ! res._isVue && isPromise(res) && ! res._handled) { res.catch(e= > handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true}}catch (e) {
handleError(e, vm, info)
}
return res
}
Copy the code
- Render the watcher
Call this.cb.call(this.vm, value, oldValue) if watcher is rendered. Render Wather instantiation is performed in the mountComponent method at mount time:
// src/core/instance/lifecycle.js
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code
export function noop (a? : any, b? : any, c? : any) {} is an empty function, so this.cb.call(this.vm, value, oldValue) is executing an empty function.
The render watcher will call this.get() when watcher.run is executed, which in turn will call this.getter.call(vm, vm). Getter is actually the second parameter updateComponent passed in to instantiation.
// src/core/instance/lifecycle.js
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
Copy the code
So this is why when we modify the responsive data related to the component, the component re-rendering will be triggered, and then the patch process will be entered.
nextTick
FlushSchedulerQueue (flushSchedulerQueue) flushSchedulerQueue (queueWatcher) flushSchedulerQueue (queueWatcher)
nextTick(flushSchedulerQueue)
Copy the code
nextTick
// src/core/util/next-tick.js
const callbacks = []
let pending = false
export function nextTick (cb? :Function, ctx? :Object) {
let _resolve
// Store wrapped CB functions with callbacks arrays
callbacks.push(() = > {
if (cb) {
// Wrap callback functions with try catch to facilitate error catching
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
})
}
}
Copy the code
The first argument to nextTick is a callback that corresponds to flushSchedulerQueue. The callback function is wrapped with a try catch for error catching, and then put into the Callbacks.
The reason for using callbacks instead of executing callbacks directly on nextTick is to ensure that executing nextTick multiple times does not start multiple asynchronous tasks, but instead pushes them into a single synchronous task that will be executed on the nextTick.
TimerFunc is then executed when pending is false. Pending is true, indicating that the task is being placed in the browser’s task queue. Pending is false, indicating that the task has been placed in the browser task queue.
Finally, nextTick returns a promise providing a call to.then when no CB callback is passed in.
nextTick().then(() = > {})
Copy the code
timerFunc
// src/core/util/next-tick.js
// timerFunc simply puts the flushCallbacks function into the browser's asynchronous task queue
let timerFunc
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
/ / first Promise
p.then(flushCallbacks)
/** * In UIWebViews in question, promise.then does not break completely, but it may fall into a weird state, * in which the callback is pushed to the microtask queue, but the queue is not refreshed until the browser needs to do something else, such as handle a timer. * Therefore, we can "force" the microtask queue to refresh by adding an empty timer. * /
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Then use 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 (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
// Then setImmediate, the macro task
timerFunc = () = > {
setImmediate(flushCallbacks)
}
} else {
/ / the last setTimeout
timerFunc = () = > {
setTimeout(flushCallbacks, 0)}}Copy the code
flushCallbacks
// src/core/util/next-tick.js
Execute every function in the callbacks array (such as flushSchedulerQueue, the callback passed by the user when calling nextTick). * /
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Copy the code
Both the global API vue. nextTick and the instance method vm.$nextTick end up calling the nextTick method in next-tick.js.
A link to the
Vue (V2.6.14) source code detoxification (pre) : handwritten a simple version of Vue
Vue (V2.6.14) source code detoxification (a) : preparation
Vue (V2.6.14) source code detoxification (two) : initialization and mount
Vue (V2.6.14) source code detoxification (three) : response type principle
Vue (V2.6.14) source code detoxification (four) : update strategy
Vue (v2.6.14) source code detoxification (five) : render and VNode
Vue (v2.6.14) source code: Update and patch (to be continued)
Vue (v2.6.14) source code detoxification (seven) : template compilation (to be continued)
If you think it’s ok, give it a thumbs up!! You can also visit my personal blog at www.mingme.net/