preface
Watch is a user-defined data listener. When the listening attribute changes, the callback will be triggered. This configuration is very common in services. It is also a must-ask in interviews, and is used for comparison with computed data.
So this article will take you to understand the workflow of Watch from the source code, as well as the implementation of dependency collection and deep listening. Before I do, I hope you have some understanding of the reactive principle flow, dependency collection flow, so that it will be easier to understand.
Previous articles:
Touch your hand to understand the Vue responsive principle
Hand touching takes you to understand the Computed principles of Vue
Watch the usage
“Know yourself and know your opponent, to win every battle”, before analyzing the source code, first know how it is used. This has a certain auxiliary effect for the later understanding.
First, string declarations:
var vm = new Vue({
el: '#example'.data: {
message: 'Hello'
},
watch: {
message: 'handler'
},
methods: {
handler (newVal, oldVal) { / *... * /}}})Copy the code
Second, function declarations:
var vm = new Vue({
el: '#example'.data: {
message: 'Hello'
},
watch: {
message: function (newVal, oldVal) { / *... * /}}})Copy the code
Third, object declaration:
var vm = new Vue({
el: '#example'.data: {
peopel: {
name: 'jojo'.age: 15}},watch: {
// Fields can use the dot operator to listen for a property of an object
'people.name': {
handler: function (newVal, oldVal) { / *... * /}}}})Copy the code
watch: {
people: {
handler: function (newVal, oldVal) { / *... * / },
// The callback will be invoked immediately after the listening starts
immediate: true.// The object depth listens for any property change in the object to trigger a callback
deep: true}}Copy the code
Fourth, array declarations:
var vm = new Vue({
el: '#example'.data: {
peopel: {
name: 'jojo'.age: 15}},// Pass in an array of callbacks, which are called one by one
watch: {
'people.name': [
'handle'.function handle2 (newVal, oldVal) { / *... * / },
{
handler: function handle3 (newVal, oldVal) { / *... * /}}]},methods: {
handler (newVal, oldVal) { / *... * /}}})Copy the code
The working process
Entry file:
/ / source location: / SRC/core/instance/index, js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '.. /util/index'
function Vue (options) {
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
Copy the code
_init
:
/ / source location: / SRC/core/instance/init. Js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options? : Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// mergeOptions merges the mixin option with the options passed in by new Vue
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// Initialize the data
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
Copy the code
initState
:
/ / source location: / SRC/core/instance/state. Js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
// This initializes watch
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code
initWatch
:
/ / source location: / SRC/core/instance/state. Js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
/ / 1
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
/ / 2
createWatcher(vm, key, handler)
}
}
}
Copy the code
- Array declared
watch
There are multiple callbacks that need to be created in a loop - Other declarations are created directly
createWatcher
:
/ / source location: / SRC/core/instance/state. Js
function createWatcher (vm: Component, expOrFn: string | Function, handler: any, options? : Object) {
/ / 1
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
/ / 2
if (typeof handler === 'string') {
handler = vm[handler]
}
/ / 3
return vm.$watch(expOrFn, handler, options)
}
Copy the code
- Object declared
watch
Retrieves the corresponding callback from the object - string-declared
watch
, directly take the method on the instance (note:methods
Can be obtained directly on the instance. expOrFn
是watch
的key
Value,$watch
Use to create a userWatcher
“
So, in addition to the watch configuration, you can also call the instance’s $watch method to achieve the same effect when creating a data listener.
$watch
:
/ / source location: / SRC/core/instance/state. Js
export function stateMixin (Vue: Class<Component>) {
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options? : Object) :Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
/ / 1
options = options || {}
options.user = true
/ / 2
const watcher = new Watcher(vm, expOrFn, cb, options)
/ / 3
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}/ / 4
return function unwatchFn () {
watcher.teardown()
}
}
}
Copy the code
StateMixin is already called in the entry file, adding the $watch method to the Vue prototype.
- All Users
Watcher
“options
, will carryuser
logo - create
watcher
, for dependency collection immediate
When true, the callback is invoked immediately- The returned function can be used to cancel
watch
Listening to the
Rely on the collection and update process
After going through the above process, you will eventually enter the logic of New Watcher, which is also the trigger point for dependency collection and updates. Now let’s see what happens in this.
Depend on the collection
/ / source location: / SRC/core/observer/watcher. Js
export default class Watcher {
constructor( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {this.vm = vm
// options
if (options) {
this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
}
Copy the code
Inside the Watcher constructor, the callbacks and options passed in are saved, which is beside the point. Let’s focus on this code:
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
Copy the code
The exported FN is the key value of watch, because the key value may be obJ.A.B, parsePath is needed to parse the key value, which is also the key step of dependent collection. What it returns is a function, so let’s not worry about what parsePath is doing, but let’s move on.
The next step is to call get:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
Copy the code
PushTarget attaches the current “user Watcher” (i.e. current instance this) to dep. target, which is used to collect dependencies. And then you call the getter function, and that’s where you get parsePath’s logic.
// SRC /core/util/lang.js
const bailRE = new RegExp(` [^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string) :any {
if (bailRE.test(path)) {
return
}
const segments = path.split('. ')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
}
Copy the code
The obj parameter is a VM instance, and segments is a parsed array of key values that loops over the value of each key, triggering a data-jacking GET. Dep. Depend is then triggered to collect the dependency (the dependency is the Watcher hanging on dep. target).
At this point, dependency collection is complete, and we know from the above that dependency collection is triggered for each key value, meaning that a change in the value of any of the above keys triggers the Watch callback. Such as:
watch: {
'obj.a.b.c': function(){}}Copy the code
Not only do changes to C trigger callbacks, but also changes to B, A, and obj trigger callbacks. The design is also clever, with a simple loop to collect dependencies for each item.
update
The first thing that triggers an update is a “data hijacking set”, which calls dep.notify to notify each watcher update method.
update () {
if (this.lazy) {dirty is set totrue
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
Then we go to queueWatcher for asynchronous updates, not asynchronous updates. All you need to know is that the last thing it calls is the run method.
run () {
if (this.active) {
const value = this.get()
if( value ! = =this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
Copy the code
Get gets the new value, call this.cb, and pass in the new value and the old value.
The depth of the listening
Depth monitoring is a very important configuration of watch monitoring, which can observe the change of any attribute in the object.
Back to the get function, there is a code that looks like this:
if (this.deep) {
traverse(value)
}
Copy the code
To determine if you need a deep listen, call traverse and pass in the values
/ / source location: / SRC/core/observer/traverse by js
const seenObjects = new Set(a)export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
/ / 1
const depId = val.__ob__.dep.id
/ / 2
if (seen.has(depId)) {
return
}
seen.add(depId)
}
/ / 3
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
Copy the code
depId
Is a unique identifier for each property being observed- Deduplication prevents the same attribute from executing logic repeatedly
- Using different strategies for arrays and objects, the ultimate goal is to recursively acquire each property, triggering their “data hijacking.
get
“Collect dependencies, andparsePath
The effect is similar
It can be concluded from this that deep monitoring using recursion to monitor, there will certainly be performance loss. Because each attribute has to go through the dependency collection process, avoid this in your business.
Uninstall listening
This is one of those rare but useful ways of doing business that is rarely used and not a priority. As a part of Watch, its principle is also explained here.
use
Here’s how it’s used:
data(){
return {
name: 'jojo'
}
}
mounted() {
let unwatchFn = this.$watch('name', () => {})
setTimeout((a)= >{
unwatchFn()
}, 10000)}Copy the code
When you use $watch to listen for data, a corresponding unload listener is returned. As the name suggests, calling it, of course, stops listening for data.
The principle of
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options? : Object) :Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
// Call watch immediately
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {
watcher.teardown()
}
}
Copy the code
You can see that the returned unwatchFn actually performs teardown.
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)}let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)}this.active = false}}Copy the code
The action in teardown is also very simple, iterating through deps and calling the removeSub method to remove the current Watcher instance. Watcher will not be notified of the next property update. Deps stores the DEP of an attribute.
Strange place
While looking at the source code, I noticed something strange about Watch, which led to its usage being like this:
watch:{
name: {handler: {
handler: {
handler: {
handler: {
handler: {
handler: {
handler: (a)= >{console.log(123)},
immediate: true
}
}
}
}
}
}
}
}
Copy the code
Normally, a handler passes a function as a callback, but for object types, the internal recursive fetch is performed until the value is a function. So you can send unlimited doll objects.
The recursive point in $watch is this code:
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
Copy the code
If you know the actual application of this code, please let me know
conclusion
The watch listening implementation uses traversal to obtain properties and triggers “data hijacking GET” to collect dependencies one by one. The advantage of this is that the callback can be executed even when the properties of its superior are modified.
Unlike Data and computed, which collect dependencies during page rendering, Watch collects dependencies before page rendering.
In an interview, if we are asked about the differences between computed and watch, here are some answers:
- One is that
computed
Want to rely ondata
Returns a value for the property change on,watch
Observing data triggers a callback; - The second is
computed
和watch
Dependency collection occurs at different points; - The third is
computed
The update requires “renderingWatcher
“With the help of,watch
No, I mentioned this in my last article.