current position:Home>[Vue source code 03] watch listening attribute - initialization and update

[Vue source code 03] watch listening attribute - initialization and update

2021-08-25 09:33:27 woow_ wu7

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

    1. Handle watche object key Corresponding value All kinds of , hold object,array,string Are treated as objects function
    2. perform vm.$watchg
    3. 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
    }
    }

copyright notice
author[woow_ wu7],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2021/08/20210825093323280e.html

Random recommended