jiefancis / blogs

个人博客,学习笔记(issues)
1 stars 0 forks source link

Vue源码解读:computed和watch如何实现依赖收集 #8

Open jiefancis opened 3 years ago

jiefancis commented 3 years ago

如何computed的依赖收集

  1. 确保computed属性与data和props的属性不能重复
  2. 为每个computed属性创建一个watcher观察者实例并建立属性与观察者的键值对对应关系,vm._computedWatchers
  3. 将计算属性挂载在当前vm实例上,使用defineProperty实现数据劫持
  4. vm.computedProp访问计算属性时,触发watcher.getter(即属性回调函数)计算依赖的值,进而触发依赖的get操作实现依赖收集

如何实现user-watcher的依赖收集

  1. 为每个watch创建一个watcher观察者实例
  2. new Watcher创建实例中,观察属性对应的函数作为watcher的cb回调,getter是parsePath解析对象(vData.a.b)的链式调用的处理函数;通过对属性的取值触发属性的get操作实现依赖收集
  3. new Watcher中user-watcher直接触发this.get()对触发watcher.getter将观察者实例指向Dep.target 同时,this.getter绑定vData实现

proxy methods on vm

下面是简化版实现。

const vm = {
        data(){
          return {
            a: {
              b: 1
            },
            c: {
              d: {
                e: 2
              }
            }
          }
        },
        props: {},
        computed:{
          d(vData){
            // console.log('计算属性',vData)
            return this.a.b + this.c.d.e
          }
        },
        watch: {
          'a.b'(){
            console.log('a.b触发watcher')
          },
          'c.d'() {
            console.log('c.d触发watcher')
          }
        },
        methods: {
           twoSum() {
              return this?.a?.b + this?.c?.d?.e
           }
        }
      }

      function noop(){}
      let vData = vm.data();
      let id = 0;

      const sharedPropertyDefine = {
        enumerable: true,
        configurable: true,
        get(){},
        set(){}
      }
      // proxy data on instance
      function proxy(target, source, key) {
        sharedPropertyDefine.get = function(){
          return source[key]
        }
        sharedPropertyDefine.set = function(newVal){
          source[key] = newVal
        }
        Object.defineProperty(vm, key, sharedPropertyDefine)
      }
      /*
      * observer
      */
      function initData(data) {
        let keys = Object.keys(data),
            length = keys.length,
            i = 0;

          while(i < length) {
            key = keys[i]
            console.log(data, 'initData-proxy on instance', key)
            proxy(vm, data, key)
            ++i
          }
          observe(data)
      }
      function observe(data) {
        if(data && typeof data === 'object') {
          Object.keys(data).forEach(key => {
            defineReactive(key, data[key], data)
          })
        }
      }
      function defineReactive(key, value, obj) {

        const dep = new Dep()
        const descriptor = Object.getOwnPropertyDescriptor(obj, key)

        // 避免直接对obj属性进行get和set操作造成死循环。
        const getter = descriptor && descriptor.get
        const setter = descriptor && descriptor.set

        if(value && typeof value === 'object') {
          observe(value)
        }
        Object.defineProperty(obj, key, {
          configurable: true,
          // writable: true
          enumerable: true,
          get(){
            const result = getter && getter.call(obj,key) || value

            if(Dep.target) {
              dep.addSub(Dep.target)
            }
            return result;
          },
          set(newVal){
            if(value === newVal) return;
            if(setter) {
              setter.call(obj, newVal)
            } else {
              value = newVal
            }
            if(newVal && typeof newVal === 'object') {
              observe(newVal)
            }
            dep.notify()
          }
        })
      }
      /**
       * Dep
      */
    class Dep{
      constructor(){
        this.subs = []
      }
      addSub(watcher) {
        //避免重复添加wathcer
        if(!this.subs.some(watch => watch.id === watcher.id)){
          this.subs.push(watcher)
        }

      }
      notify(){
        this.subs && this.subs.forEach(watcher => watcher.update())
      }
    }

    Dep.target = null
    const targetStack = []
    function pushTarget(target) {
      targetStack.push(target)
      Dep.target = target
    }
    function popTarget(){
      targetStack.pop()
      Dep.target = targetStack[targetStack.length - 1]
    }

    /**
     * computed
    */
   function initComputed(vm, computed){
    const watchers = vm._computedWatchers = Object.create(null);
    Object.keys(computed).forEach(prop => {
      const watcher = watchers[prop] = new Watcher(vm, computed[prop], noop)

      if (vData[prop]) {
        console.error(`computed props ${prop} has allready defined in data`, vData)
      } else {
        defineComputed(vm,prop)
      }
    })
   }
   function defineComputed(vm, prop){
     Object.defineProperty(vm, prop, {
       configurable: true,
       enumerable: true,
       get() {
        const watcher = vm._computedWatchers[prop]
        const value = watcher.get()
        return value
       }
     })
   }

    /**
     * watcher
    */
   class Watcher{
     constructor(
       vm,
       expOrFn,
       cb,
       options
     ){
       this.cb = cb
      if(typeof expOrFn === 'function') {
        this.getter = expOrFn
      } else {
        this.getter = parsePath(expOrFn)
      }
      this.id = ++id;

      this.value = this.get()
     }
     get(){
      pushTarget(this)
      const value = this.getter.call(vData, vData)
      return value
     }
     update(){
       this.value = this.get()
       this.cb.call(vData, vData)
     }
   }

   /**
    * utils
   */
  function parsePath(expOrFn){
    let segments = expOrFn.split('.')
    return function(obj){
      for(let i = 0; i < segments.length; i++) {
        // console.log('obj', obj)
        obj = obj[segments[i]]
      }
      return obj
    }
  }

  function initWatcher(watch){
    Object.keys(watch).forEach(prop => {
      new Watcher(vData,prop, watch[prop])
    })
  }

/*
  * proxy method on instance
  */
  function initMethods(methods) {
      let fn = noop
      Object.keys(methods).forEach(prop => {
          if(vm.props[prop]) {
              console.error(`methods ${prop} has allready on props`)
              return 
          }
          if(vm.computed[prop]) {
              console.error(`methods ${prop} has allready on computed`)
              return 
          }
          fn = methods[prop]
          vm[prop] = typeof fn !== 'function' ? noop : fn.bind(vm)
      })
  }

  function Vue({data, computed, watch,methods}){
    initData(vData)
    initComputed(vm,computed)
    initWatcher(watch)
    initMethods(methods)
  }
  new Vue({
    data: vData,
    computed: vm.computed,
    watch: vm.watch,
    methods: vm.methods
  })