从源码看vue响应式原理

342 阅读2分钟

之前一直对VUE的响应式原理一知半解,这次好好看了这部分源码,发现还有很多可以说的点,索性把最近的学到的写一篇博文供大家观赏

生成响应式对象

我们知道Vue使用ES5中的Object.defineProperty方法生成响应式对象,看下这部分源码

New Vue时会initState, 会对propsmethods, data等分别进行init, 我们这次主要看对data的处理

src/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)  // 对data进行处理,生成响应式对象
        } else {
            observe(vm._data = {}, true /* asRootData */)
        }
        if (opts.computed) initComputed(vm, opts.computed)
        if (opts.watch && opts.watch !== nativeWatch) {
            initWatch(vm, opts.watch)
        }
    }

initData

    在src/instance/state.js中
    
    function initData (vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function' // 获取定义的data对象
        ? getData(data, vm)
       : data || {}
    
  ...
   
  // observe data
  observe(data, true /* asRootData */) // 基于observe生成响应式对象
}

    export function observe (value: any, asRootData: ?boolean): Observer | void {
          if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob: Observer | void
      
      ...
      
       ob = new Observer(value) // 生成Observer的实例
      return ob
}

该方法实例化一个Observer

我们同时看看Observer这个class是如何写的

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value) // 在正常情况下进入walk方法
    }
  }

最终走入walk方法,该方法最终会调用 defineReactive方法


export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  
  ...

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}


defineReactive方法中,将传入的data对象变成了一个响应式对象,在触发get的时候搜集依赖,在触发set的时候派发更新

依赖搜集

上面我们看到了对datainit最终是将data变成了一个响应式对象,那vue是如何在get被触发时实现依赖搜集的呢?

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
  })
}

对于依赖搜集看代码有两点重要: 实例化Dep; 同时触发dep.depend()

我们看下关于Dep的部分

    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array<Watcher>;

      constructor () {
        this.id = uid++
        this.subs = []
      }
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
      removeSub (sub: Watcher) {
        remove(this.subs, sub)
      }
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
}

Dep这个类有个静态属性是target, 在每次render时,该target都是固定且一样的,即是每个实例化出来的dep都会有相同的target属性

那这个target到底是什么?target是一个Watcher的实例

我们看下Watcher这个class

    export default class Watcher {
          constructor (
            vm: Component,
            expOrFn: string | Function,
            cb: Function,
            options?: ?Object,
            isRenderWatcher?: boolean
          ) {
            this.vm = vm
            
            ...
            
            this.value = this.lazy
              ? undefined
              : this.get()
          }

          /**
           * Evaluate the getter, and re-collect dependencies.
           */
          get () {
            pushTarget(this)
            
            ...
            
            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)
              }
            }
  }
  ...

addDep方法相当于调用了对应dep中的addSub方法,将对应的watcher搜集放到dep.subs这个数组中.

需要注意的是:只有在render过程中才会触发get,所以在首次initdata的时候,其实watcher还没有实例化。在全局中只会有一个渲染watcher,最后add进入数组的是userwatcher

当每次get触发时,就会创建一个dep属性,就会将所有的watcher都搜集起来作为订阅者,放入每个属性dep.subs数组中,等待后续的notify,这种时观察者模式的使用

派发更新

那既然get搜集依赖,那set就是派发更新,即是在set被触发时,通知所有搜集的watcher,并完成更新

那派发更新是如何实现的呢?

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()

      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        set: function reactiveSetter (newVal) {
          dep.notify()
        }
      })
    }

在set被触发时,调用了dep中的notify方法

     notify () {
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }

由于subs中是由多个watcher组成的数组,因此会调用watcher中的update方法

    update () {
          queueWatcher(this)
      }

queueWatcher会将watcher中对应的回调按照id大小进行排序,并在nextTick时按序执行

最终运行会走到watcher.run

    run () {
    if (this.active) {
      const value = this.get()
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

最终执行对应的callback

全部流程

  • new Vue时会做两件事: 首先是对data进行initData,创建响应式对象; 其次是在执行mountComponent方法
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        。。。
        
      } else {
        updateComponent = () => {
          vm._update(vm._render(), hydrating)
        }
      }

      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      hydrating = false

      return vm
    }

在执行mountComponent方法时,对Watcher进行实例化,而实例化时会调用watcherget方法,进而会调用pushTarget方法

    export function pushTarget (target: ?Watcher) {
        targetStack.push(target)
        Dep.target = target
    }

该方法实现对Dep类中target复制,使得所有实例化中的target都是同一个渲染watcher

同时定义callback函数为updateComponent, 在set触发notify后会触发dep.run进入触发updateComponent完成更新

  • 响应式对象已经创建完毕后,在第一次reder时,会触发get,将所有的watcher都搜集进入对应的dep.subs数组中
    第一次render
    const vnode = vm._render()

在后面每次触发set时都是走到updateComponent方法

    updateComponent = () => {
          vm._update(vm._render(), hydrating)
    }

写在最后

在网上找到一张图,Vue基于响应式对象和Dep实现watcher管理,数据发生变动通知watcher进而触发render

image.png

撰文不易,看完还请点赞再走