美文网首页
理解 Vue 响应式原理

理解 Vue 响应式原理

作者: YeLqgd | 来源:发表于2020-03-13 16:26 被阅读0次

    简述

    之所以称「响应式」是因为,在 Vue 中, vm 中 Data(指 vm.$propsvm.$data 里的属性)的变化会自动触发页面上引用了这些 Data 的 DOM 的变化,我们已经知道 DOM 的变化可以通过对比 VNode 的差异由 vm 自行完成(这一过程称为 re-render), 现在的问题是 Data 变化时如何实现「自动触发」re-render。Vue 对此的实现,本质上是利用事件订阅/发布模式:在 vm.$mount 时去订阅页面上用到的 Data,然后在这些 Data 变化时,去通知 vm 进行 re-render。订阅的动作在 Data 的 getter 中完成,发布的动作在 Data 的 setter 中完成,于是 vm.$mount 的过程中订阅某些 Data 后(访问时触发 getter),当后续这些 Data 变化时(赋值时触发 setter),vm 就得到通知要去 re-render,这就是所谓 Vue 响应式的基本原理。

    源码简析

    在此之前,先了解源码里几个概念的作用,Dep、Watcher 和 Observer,它们是源码中的三个类。Dep 的作用相当于 EventEmitter,负责 on 和 fire;Watcher 的角色是订阅者;Observer 的作用是为 Data 添加 getter/setter。

    Dep

    interface DepInterface {
      id: number
      subs: Array<Watcher>
    
      addSub (sub: Watcher): void
      removeSub (sub: Watcher): void
      depend (): void
      notify (): void
    }
    
    let uid = 1
    
    class Dep implements DepInterface {
      static target = null
    
      id = uid++
      subs = []
    
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      removeSub (sub: Watcher) {
        if (this.subs.length) {
          const index = this.subs.indexOf(sub)
      
          if (index > -1) {
            this.subs.splice(index, 1)
          }
        }
      }
    
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
        for (let i = 0, length = this.subs.length; i < length; ++i) {
          this.subs[i].update()
        }
      }
    }
    
    const targetStack = []
    
    function pushTarget (_target) {
      if (Dep.target) { targetStack.push(Dep.target) }
      Dep.target = _target
    }
    
    function popTarget () {
      Dep.target = targetStack.pop()
    }
    

    前面说了,Dep 既然是一个 EventEmitter,那么它就包括(取消)订阅、发布的方法,depend 方法的最终结果是调用 addSubid 是每个 dep 实例的唯一标识,与一个 Data 一一对应,因为每个 Data 被添加 getter 时都会生成一个与之对应的 dep 实例,正是由这个 dep 实例来处理相关的订阅/发布的逻辑 ;subs 是由一组订阅了该 dep 的 watcher 实例组成的数组。静态属性 target 用来存储当前正在执行订阅动作的 watcher 实例,变量 targetStack 用来存储所有这样的实例,但同一时间只允许一个 watcher 实例执行订阅动作;因为可能出现一个 watcher 正在执行订阅动作的过程中,一个新的 watcher 要优先执行,所以 targetStack 的作用便是保存旧的 watcher,新 watcher 来了便将旧 watcher 入栈,新 watcher 订阅完成后便将旧 watcher 出栈并让其成为 Dep.target

    Observer

    interface ObserverInterface {
      value: Object | Array<any>
      vmCount: number
      dep: Dep
    
      walk (obj: Object): void
      observeArray (arr: Array<any>): void
    }
    
    class Observer implements ObserverInterface {
      value: Object | Array<any>
      vmCount = 0
      dep = new Dep()
    
      constructor (value: Object | Array<any>) {
        this.value = value
        def(value, '__ob__', this)
    
        if (Array.isArray(value)) {
          this.observeArray(value)
        } else {
          this.walk(value)
        }
      }
    
      observeArray (arr: Array<any>): void {
        for (let i = 0, l = arr.length; i < l; ++i) {
          observe(arr[i])
        }
      }
    
      walk (obj: Object): void {
        const keys = Object.keys(obj)
    
        for (let i = 0, l = keys.length; i< l; ++i) {
          defineReactive(obj, keys[i], obj[keys[i]])
        }
      }
    }
    
    function observe (value: any, asRootData?: boolean): Observer | void {
      if (!(typeof value === 'object' && value !== null) || value instanceof VNode) {
        return
      }
    
      let ob: Observer | void
    
      if (Object.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else if (Array.isArray(value) || ''.toString.call(value) === '[object Object]') {
        ob = new Observer(value)
      }
    
      if (ob && asRootData) {
        ob.vmCount++
      }
    
      return ob
    }
    
    function defineReactive (obj: Object, key: string, val: any): void {
      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, val)
      
      if (property && !property.configurable) {
        return
      }
    
      const getter = property && property.get
      const setter = property && property.set
    
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
    
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
    
          if (Dep.target) {
            dep.depend()
          }
    
          return value
        },
        set: function reactiveSetter (value: any) {
          if (setter) {
            setter.call(obj, value)
          } else {
            val = value
          }
    
          dep.notify()
        }
      })
    }
    
    function def (obj: Object, key: string, value: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
      })
    }
    

    Observer.constructor(value) 的逻辑为:

    1. 将 value 值赋给 observer 实例的 value 属性
    2. 将生成的 observer 实例本身挂在 ob 属性下
    3. 当 value 为数组时,调用 this.observeArray(value),这个方法会递归调用 new Observer
    4. 否则 value 会是一个对象,然后对 value 的每一个属性调用 defineReactive(value, key, value[key]),这个方法即为每一个属性添加一个 getter 和 setter,同时生成一个 dep 实例,它在 getter 里添加监听的 watcher 实例,这也是所谓的依赖收集(即对依赖 value[key] 这个属性的 watcher 的收集),然后在 setter 里 nontify。

    Watcher

    let uuid = 0
    
    interface WatcherInterface {
      vm: Component
      cb: Function
      id: number
      deps: Array<Dep>
      newDeps: Array<Dep>
      depIds: Set<number>
      newDepIds: Set<number>
      getter: Function | void
      value: any
    
      get (): any
      addDep (dep: Dep): void
      update (): void
    }
    
    class Watcher implements WatcherInterface {
      vm: Component
      cb: Function
      id: number = ++uid
      deps: Array<Dep> = []
      newDeps: Array<Dep> = []
      depIds: Set<number> = new Set()
      newDepIds: Set<number> = new Set()
      getter: Function | void
      value: any
    
      constructor (
        vm: Component,
        fn: Function,
        cb: Function,
        options?: Object,
        isRenderWatcher?: boolean
    ) {
        this.vm = vm
        this.cb = cb
        this.getter = fn
        this.value = this.get()
      }
    
      get (): any {
        pushTarget(this)
    
        const vm = this.vm
        const value = this.getter && this.getter.call(vm, vm)
    
        popTarget()
    
        return value
      }
    
      addDep (dep: Dep): void {
        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)
          }
        }
      }
    
      update (): void {
        const value = this.get()
        const oldValue = this.value;
    
        this.value = value
        this.cb.call(this.vm, value, oldValue);
      }
    }
    

    Vue 里 watcher 分为 renderWatcher(即每个 vm 实例用来监听 vm 里的 Data 然后专门 re-render 的 watcher)、computedWatcher(用来 watch computed)和由 watch () {} 以及人工手动调用 vm.$watch 生成的 watcher。

    renderWatcher 依赖收集的时机是 vm.$mount(),mount 的过程中访问了 data、props、computed 中的数据,在 created hook 之前;除了手动调用 vm.$watch 以外生成的其他 watcher 依赖收集的时机都是 initState,在 created hook 之后。可以看到在 new Watcher() 时会调用传进来的 fn(),而正是在 vm.$mount() 的里,每个 vm 会新建一个 renderWatcher(具体代码为 new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)),同时传给 new Watcherfn 的值 updateComponent 的作用就是 re-render,当某个依赖的 dep.notify() 时,vm 就会执行 updateComponent(),从而 re-render。

    心得

    • 理解原理是前提,原理理解了就等于知晓了大概的逻辑,大的脉络通了之后才能从整体上把握,而不至于半天云里雾里。
    • 源码里面不少都是增加健壮性和兼容性的代码,选择性无视它们会大大降低初看源码的压力。
    • 理解原理加以总结,甚至探索原理背后的原理,抽象的结果就是做出合理的判断,合理的判断就是知识。
    • 抽象的程度越高越普适,但相应地,针对性也小了。

    相关文章

      网友评论

          本文标题:理解 Vue 响应式原理

          本文链接:https://www.haomeiwen.com/subject/odeqshtx.html