Navigation
[[ thorough 01] Execution context ](https://juejin.im/post/684490...)
[[ thorough 02] Prototype chain ](https://juejin.im/post/684490...)
[[ thorough 03] Inherit ](https://juejin.im/post/684490...)
[[ thorough 04] The event loop ](https://juejin.im/post/684490...)
[[ thorough 05] currying Partial function Function memory ](https://juejin.im/post/684490...)
[[ thorough 06] Implicit conversion and Operator ](https://juejin.im/post/684490...)
[[ thorough 07] Browser caching mechanism (http Caching mechanisms )](https://juejin.im/post/684490...)
[[ thorough 08] Front end security ](https://juejin.im/post/684490...)
[[ thorough 09] Depth copy ](https://juejin.im/post/684490...)
[[ thorough 10] Debounce Throttle](https://juejin.im/post/684490...)
[[ thorough 11] Front-end routing ](https://juejin.im/post/684490...)
[[ thorough 12] Front-end modularization ](https://juejin.im/post/684490...)
[[ thorough 13] Observer mode Publish subscribe mode Two way data binding ](https://juejin.im/post/684490...)
[[ thorough 14] canvas](https://juejin.im/post/684490...)
[[ thorough 15] webSocket](https://juejin.im/post/684490...)
[[ thorough 16] webpack](https://juejin.im/post/684490...)
[[ thorough 17] http and https](https://juejin.im/post/684490...)
[[ thorough 18] CSS-interview](https://juejin.im/post/684490...)
[[ thorough 19] Handwriting Promise](https://juejin.im/post/684490...)
[[ thorough 20] Handwritten functions ](https://juejin.im/post/684490...)
[[react] Hooks](https://juejin.im/post/684490...)
[[ Deploy 01] Nginx](https://juejin.im/post/684490...)
[[ Deploy 02] Docker Deploy vue project ](https://juejin.im/post/684490...)
[[ Deploy 03] gitlab-CI](https://juejin.im/post/684490...)
[[ Source code -webpack01- Pre knowledge ] AST Abstract syntax tree ](https://juejin.im/post/684490...)
[[ Source code -webpack02- Pre knowledge ] Tapable](https://juejin.im/post/684490...)
[[ Source code -webpack03] Handwriting webpack - compiler Simple compilation process ](https://juejin.im/post/684490...)
[[ Source code ] Redux React-Redux01](https://juejin.im/post/684490...)
[[ Source code ] axios ](https://juejin.im/post/684490...)
[[ Source code ] vuex ](https://juejin.im/post/684490...)
[[ Source code -vue01] data Response type and Initialize rendering ](https://juejin.im/post/684490...)
[[ Source code -vue02] computed Response type - initialization , visit , The update process ](https://juejin.im/post/684490...)
[[ Source code -vue03] watch Listening properties - Initialization and update ](https://juejin.im/post/684490...)
[[ Source code -vue04] Vue.set and vm.$set ](https://juejin.im/post/684490...)
[[ Source code -vue05] Vue.extend ](https://juejin.im/post/684490...)
[[ Source code -vue06] Vue.nextTick and vm.$nextTick ](https://juejin.im/post/684790...)
Pre knowledge
Some words
somewhat: somewhat
( somewhat expensive operation The operation is a little expensive )
teardown: uninstall
Use cases
<template>
<div class="about">
<h1>This is a watch page</h1>
<div>count = {{count}}</div>
<button @click="changeCount">change count</button>
<br />
<br />
<div>immediate Execute now :count1 = {{count1}}</div>
<button @click="changeCount1">change count1</button>
<br />
<br />
<div>deep Depth observation - The problem is that the old and new values are the same , It can be used commputed Do deep copy :count2 = {{nestObj.count2}}</div>
<button @click="changeCount2">change count2</button>
<br />
<br />
<div>count3 = {{nestObj.count3}}</div>
<button @click="changeCount3">change count3</button>
<br />
<br />
<button @click="changeTestArr">change testArr</button>
<br />
<br />
<button @click="changeAll"> Change all data - verification sync</button>
</div>
</template>
<script> export default { data() { return { count: 0, count1: 1, nestObj: { count2: 2, count3: 3 }, testArr: { count4: 4, count5: 5 }, testHandlerIsFunctionName: 6, }; }, computed: { deepCopyNestObj() { return JSON.parse(JSON.stringify(this.nestObj)) } }, watch: { count: function(val, newVal) { // ---------------------------------- function console.log(val, newVal); }, count1: { handler(v, oldv) { console.log(v, oldv, "immediate Execute now , No need to rely on change ", " After execution "); }, immediate: true }, nestObj: { // ------------------------------------------------------ object handler(v, oldv) { console.log(v.count2, oldv.count2, "sync Again nextTick Execute before "); }, deep: true, sync: true // Sync Precede Asynchronous watch perform , The default is asynchronous }, deepCopyNestObj(newVal, oldVal) { console.log(newVal.count2, oldVal.count2, 'deep Depth observation - The problem is that the old and new values are the same , It can be used commputed Do deep copy ') }, "nestObj.count3": function() { // Listen for a property in the object , have access to obj.xxx As a string key console.log("watch here we are nestObj.count3"); }, testArr: [ // ------------------------------------------------------ Array function handler1() { console.log(1111); }, function handler2() { console.log(2222); } ], testHandlerIsFunctionName: 'watchHandlerIsFnName' // --------------- character string // watchHandlerIsFnName It's a method , stay methods Method of definition // When testHandlerIsFunctionName change , Will call watchHandlerIsFnName Method }, methods: { watchHandlerIsFnName(v, oldV) { console.log(v, oldV, 'watch Object's handler Is a string , That is, a method name ') }, changeCount() { this.count = this.count + 1; }, changeCount1() { this.count1 = this.count1 + 1; }, changeCount2() { this.nestObj.count2 = this.nestObj.count2 + 1; }, changeCount3() { this.nestObj.count3 = this.nestObj.count3 + 1; }, changeAll() { this.count = this.count + 1; this.count1 = this.count1 + 1; this.nestObj.count2 = this.nestObj.count2 + 1; this.nestObj.count3 = this.nestObj.count3 + 1; this.testArr = this.testArr + 1; this.testHandlerIsFunctionName = this.testHandlerIsFunctionName + 1 }, changeTestArr() { this.testArr = this.testArr + 1 } } }; </script>
Learning goals
watch In two ways
- Through the parameters of the component ,watch As object
- adopt vm.$watch() Method to call
Avoid the dead cycle
- such as watch: { count: {this.count = this.count + 1}}
- It will observe count The change of , Modify... After change count,count The change continues to call cb To modify count, Dead cycle
wath Object's key Object's value The type of
- function
- object
- array
- string
- Eventually, different types of handler Convert to function
watch Object's options Object supports properties
deep
- Deep monitoring
loop ( visit ) watch Object key Corresponding vm.key Every attribute of a nested object , This triggers the response of dependent data get, adopt dep.depend()
- towards user watcher Of newDeps Add dep
- towards dep Of subs Add user watcher
immediate
- Execute now cb, namely wache Object handler function , You don't have to wait for dependency changes to execute
- Call directly cb(watcher.value)
sync
- Guarantee ( Sync wath Object's handler ) stay ( ordinary watch Object's handler ) Go ahead
- sync Just call directly watcher.run() => this.cb.call(this.vm, value, oldValue) So that we can directly execute cb function
watch Initialization process
- Handle watche object key Corresponding value All kinds of , hold object,array,string Are treated as objects function
- perform vm.$watchg
new userWatcher()
- constructor Pass through this.get() call getter function , hold watch Object key adopt this.getter = parsePath(expOrFn) Method into an array , adopt vm[key] To visit , return watch In the object key Corresponding responsive data
- At the time of the visit , It will trigger the of responsive data get Method , So we can do dependency collection , stay dep To collect user watcher, Used to update the
Update process
Depending on change , Trigger dep.notify(), Fantasy dep.subs In the data watcher.update() To update
- If sync=true Just call directly watcher.run => this.cb.call(this.vm, value, oldValue)
- If sync=false, queueWatcher => nextTick(flushSchedulerQueue) => watcher.run() => this.cb.call(this.vm, value, oldValue)
watch Source code
Vue.prototype._init
=>initState
=>initWatch(vm, opts.watch)
=>createWatcher(vm, key, handler)
=>vm.$watch(expOrFn, handler, options)
initWatch - src/core/instance/state.js
function initWatch (vm: Component, watch: Object) { // initWatch(vm, opts.watch) for (const key in watch) { const handler = watch[key] // handler // watch Object key Corresponding value // May be function , Array , object , character string ( Method name ) if (Array.isArray(handler)) { // handler It's an array , Will traverse , Pass each member into createWatcher // Members are generally functions // such as // watch: { // testArr: [ // function handler1() { // console.log(1111); // }, // function handler2() { // console.log(2222); // } // ] // } for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { // handler It's the object , function , character string // such as // watch: { // count: function(val, newVal) { // console.log(val, newVal); // }, // count1: { // handler(v, oldv) { // console.log(v, oldv, "immediate Execute now , No need to rely on change ", " After execution "); // }, // immediate: true, // deep: true, // sync: true, // }, // testHandlerIsFunctionName: 'watchHandlerIsFnName' // } createWatcher(vm, key, handler) } } }
createWatcher - src/core/instance/state.js
function createWatcher ( vm: Component, expOrFn: string | Function, // watch Object key handler: any, // watch Object key Corresponding value => object , function , Array member , character string options?: Object // Yes when it's initialized undefined ) { if (isPlainObject(handler)) { // handler It's an object // such as // count1: { // handler(v, oldv) { // console.log(v, oldv); // }, // immediate: true, // deep: true, // sync: true // } options = handler handler = handler.handler // handler It's the object , Just put handler Assign a value to options, hold handler Object's handler Method to handler Variable // It's actually processing parameters } if (typeof handler === 'string') { handler = vm[handler] // handler Is a string , Just assign the method represented by this string , stay methods Methods defined in objects } return vm.$watch(expOrFn, handler, options) // Pass in vm.$watch Of handler It's all handled into function }
Vue.prototype.$watch - src/core/instance/state.js
Vue.prototype.$watch = function ( expOrFn: string | Function, // expOrFn // watch Object's key cb: any, // cb // cb yes watcher In the object key Corresponding value After the conversion of various situations handler function ( Possible values are functions , Array , object , character string , It's all turned into a function here ) // If not through watch Object to new Vue() The way , But directly through vm.$watch Pass in , be cb It could also be ( function , object , Array , character string ) options?: Object // options It's the configuration object // options The properties of may be the following // handler immediate deep sync etc. ): Function { const vm: Component = this if (isPlainObject(cb)) { // Here's another judgment cb Is it the object , Here's why // 1. because $watch Can pass vm.$watch Method call // 2. If it is passed in new Vue({}) With watch Object method , cd It's already processed Function , You don't have to judge the situation of the object return createWatcher(vm, expOrFn, cb, options) // So if it's above 1 The situation of , Will call again createWatcher() To deal with handler The type of , Handle as a function } options = options || {} options.user = true // towards options Object to add user attribute , The value is true const watcher = new Watcher(vm, expOrFn, cb, options) // new One user watcher if (options.immediate) { // immediate Attributes exist , Do it immediately cb, namely handler function try { cb.call(vm, watcher.value) // cb namely handler function , Is to receive two parameters , Only the first parameter is passed here , So if you agree, the second parameter is undefined // The first parameter newValue // The second parameter oldValue // watch: { // count: function(val, newVal) { // console.log(val, newVal); // } // } } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { // Vue.prototype.$watch function , Returns the unwatchFn function watcher.teardown() // watcher.teardown() // 1. Delete _watchers Medium user watcher // 2. Delete user watcher Medium deps All in dep // teardown () { // if (this.active) { // // this.active = true The default is true // if (!this.vm._isBeingDestroyed) { // remove(this.vm._watchers, this) // // remove watchers Array watcher // } // let i = this.deps.length // while (i--) { // this.deps[i].removeSub(this) // // At the same time to delete watcher Of deps All in watcher // // such as stay user watcher,$watch Method will eventually be deleted user watcher Of deps The dep // } // this.active = false // // this.active = false // } // } } }
watcher - scr/core/observer/watcher.js
export default class Watcher { vm: Component; expression: string; cb: Function; // such as user watcher Medium handler function id: number; deep: boolean; user: boolean; lazy: boolean; // computed watcher The logo of sync: boolean; // user watcher Of options Object sync attribute dirty: boolean; // be used for computed watcher active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user // user watcher Of options.user The default is true this.lazy = !!options.lazy // computed watcher Of options.lazy The default is true this.sync = !!options.sync // be used for user watcher this.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() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // expOrFn Not a function // because :user watcher Medium expOrFn Namely watch Object key, It's a string // therefore : use parsePath Function just works this.getter = parsePath(expOrFn) // this.getter // 1. parsePath(expOrFn) // Returns a function , The parameters of the return function are vm example // return function (obj) { // // 1. path => such as expOrFn = path = 'a.b' // // 2. ojb => vm // // above 1 and 2, So the following loop : // // vm.a => The visit came to a // // vm.a.b => The visit came to b // for (let i = 0; i < segments.length; i++) { // if (!obj) return // obj = obj[segments[i]] // } // return obj // } // 2. this.getter Is in watcher.get() In the // this.getter.call(vm, vm) // therefore :1 The parameters of the return function in are vm // export function parsePath (path: string): any { // if (bailRE.test(path)) { // return // } // const segments = path.split('.') // // segments Possible situation // // 1.'a.b' Namely observation a Object's b attribute => [a, b] // // 2. a => [a] // return function (obj) { // // 1. path => such as expOrFn = path = 'a.b' // // 2. ojb => vm // // above 1 and 2, So the following loop : // // vm.a => The visit came to a // // vm.a.b => The visit came to b // for (let i = 0; i < segments.length; i++) { // if (!obj) return // obj = obj[segments[i]] // } // return obj // // return Response type get The value returned in the function // } // } if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. */ 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 "${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 } /** * Add a dependency to this directive. */ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } /** * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } /** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { // such as user watcher Of options Configuration of the sync:true when , call run Method this.run() } else { queueWatcher(this) // 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) // } // } // } } } /** * Scheduler job interface. * Will be called by the scheduler. */ 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 if (this.user) { // If it is user watcher try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false } /** * Depend on all deps collected by this watcher. */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // this.active = true The default is true // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) // remove watchers Array watcher } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) // At the same time to delete watcher Of deps All in watcher // such as stay user watcher,$watch Method will eventually be deleted user watcher Of deps The dep } this.active = false // this.active = false } } }
parsePath - src/core/util/lang.js
export function parsePath (path: string): any { if (bailRE.test(path)) { return } const segments = path.split('.') // segments Possible situation // 1.'a.b' Namely observation a Object's b attribute => [a, b] // 2. a => [a] return function (obj) { // 1 // 1. path => such as expOrFn = path = 'a.b' // 2. ojb => vm // above 1 and 2, So the following loop : // vm.a => The visit came to a // vm.a.b => The visit came to b // 2 // 1. path => such as expOrFn = path = 'a' // 2. ojb => vm for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj // 1. Will eventually return to vm.a.b // 2. Eventually return vm.a } }