乐趣区

Vue源码之响应式原理

Object 的变化侦测

像 Vue 官网上面说的,vue 是通过 Object.defineProperty 来侦测对象属性值的变化。

function defineReactive (obj, key, val) {let dep = new Dep()
    Object.defineProperty(obj, key, {
         enumerable: true,
          configurable: true,
          get () {return val},
          set (newVal) {if (val === newVal) return
                val = newVal
          }
    })
}

函数 defineReactive 是对 Object.defineProperty 的封装,作用是定义一个响应式的数据。

不过如果只是这样是没有什么用的,真正有用的是收集依赖。在 getter 中收集依赖,在 setter 触发依赖。

Dep (收集依赖)

// 还有几个方法没写,比如怎么移除依赖。class Dep {constructor () {
        // 依赖数组
        this.subs = []}
    addSub (sub) {this.subs.push(sub)
    }
    
    depend (target) {if (Dep.target) {
            // 这时的 Dep.target 是 Watcher 实例
            Dep.target.addDep(this)
        }
    }
    notity () {
        this.subs.forEach(val => {val.update()
        })
    }
    Dep.target = null
}

Watcher (依赖)

// 本来在 Watcher 中也要记录 Dep, 但是偷懒没写了,记录了 Dep 后可以通知收集了 Watcher 的 Dep 移除依赖。class Watcher {constructor (vm, expOrFn, cb) {
        // vm: vue 实例
        // expOrFn: 字符串或函数
        // cb: callback 回调函数
        this.vm = vm
        this.cb = cb
        // 执行 this.getter 就可以读取 expOrFn 的数据,就会收集依赖
        if (typeof expOrFn === 'function') {this.getter = expOrFn} else {
            // parsePath 是读取字符串 keypath 的函数, 具体的可以去浏览 Vue 的源码
            this.getter = parsePath(expOrFn)
        }
        this.value = this.get()}
    get () {
        Dep.target = this
        // 在这里执行 this.getter
        let value = this.getter(this.vm, this.vm)
        Dep.target = null
        return value
    }
    addDep (dep) {dep.addSub(this)
    }
    // 更新依赖
    update () {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}

接下来再改一下刚开始定义的 defineReactive 函数

function defineReactive (obj, key, val) {let dep = new Dep() // 闭包
    Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get () {
            // 触发 getter 时,收集依赖
            dep.addDep()
            return val
          },
          set (newVal) {if (val === newVal) return
                val = newVal
                // 触发 setter 时,触发 Dep 的 notify,便利依赖
                 dep.notity()}
    })
}

这个时候已经可以侦测数据的单独一个属性,最后再封装一下:

class Observer {constructor (value) {
        this.value = value
        // 侦测数据的变化和侦测对象的变化是有区别的
        if (!Array.isArray(value)) {this.walk(value)
        }
    }
    
    walk (value) {const keys = Object.keys(value)
        keys.forEach(key => {this.defineReactive(value, key, value[key])
        })
    }
}

最后总结一下:

实例化 Watcher 时通过 get 方法把 Dep.target 赋值为当前的 Wathcer 实例,并把 Watcher 实例添加在 Dep 中,当设置数据时,触发 defineReactiveset 运行 Dep.notify() 遍历 Dep 中收集的依赖 Watcher 实例,然后触发 Watcher 实例的 update 方法。

Array 的变化侦测

Object 可以通过 getter/setter 来侦测变化,但是数组是通过方法来变化,比如 push。这样就不能和对象一样,只能通过拦截器来实现侦测变化。

定义一个拦截器来覆盖 Array.prototype,每当使用数组原型上面的方法操作数组的时候,实际上执行的是拦截器上面的方法,然后再拦截器里面使用 Array 的原型方法。

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 缓存原始方法
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
      enumerable: false,
      configurable: true,
      writable: true,
      value: function mutator (...args) {return original.apply(this, args)
      }
  })

然后就要覆盖 Array 的原型:

// 看是否支持__proto__, 如果不支持__proto__,则直接把拦截器的方法直接挂载到 value 上。const hasProto = "__proto__" in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer {constructor (value) {
        this.value = value
        if (!Array.isArray(value)) {this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
        }
    }
    
    walk (value) {const keys = Object.keys(value)
        keys.forEach(key => {this.defineReactive(value, key, value[key])
        })
    }
}

function protoAugment (target, src: Object) {target.__proto__ = src}

function copyAugment (target: Object, src: Object, keys: Array<string>) {for (let i = 0, l = keys.length; i < l; i++) {const key = keys[i]
    def(target, key, src[key])
  }
}

Array 也是在 getter 中收集依赖,不过依赖存的地方有了变化。Vue.js 把依赖存在 Observer 中:

class Observer {constructor (value) {
        this.value = value
        this.dep = new Dep // 新增 Dep
        if (!Array.isArray(value)) {this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
        }
    }
    
    walk (value) {const keys = Object.keys(value)
        keys.forEach(key => {this.defineReactive(value, key, value[key])
        })
    }
}

至于为什么把 Dep 存在 Observer 是因为必须在 getter 和 拦截器中都能访问到。

function defineReactive (data, key, val) {let childOb = observer(val) // 新增
    let dep = new Dep() 
    Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get () {dep.addDep()
            if (childOb) {
                // 在这里收集数组依赖
                childOb.dep.depend()}
            return val
          },
          set (newVal) {if (val === newVal) return
                val = newVal
                 dep.notity()}
    })
    
}
// 如果 value 已经是响应式数据,即有了__ob__属性,则直接返回已经创建的 Observer 实例
// 如果不是响应式数据,则创建一个 Observer 实例
function observer (value, asRootData) {if (!isObject(value)) {return}
    let ob
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observe) {ob = value.__ob__} else {ob = new Observer(value)
    }
    return ob
}

因为拦截器是对 Array 原型的封装,所以可以在拦截器中访问到 this(当前正在被操作的数组),

dep 保存在 Observer 实例中,所以需要在 this 上访问到 Observer 实例:

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configerable: true
    })
}
class Observer {constructor (value) {
        this.value = value
        this.dep = new Dep
        // 把 value 上新增一个不可枚举的属性__ob__, 值为当前的 Observer 实例
        // 这样就可以通过数组的__ob__属性拿到 Observer 实例,然后就可以拿到 Observer 的 depp
        // __ob__不止是为了拿到 Observer 实例,还可以标记是否是响应式数据
        def(value, '__ob__', this) // 新增
        if (!Array.isArray(value)) {this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
        }
    }
    
    ...
}

在拦截器中:

methodsToPatch.forEach(function (method) {const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args)
      const ob = this.__ob__ // 新增
      ob.dep.notify() // 新增 向依赖发送信息
      return resullt
  })
})

到这里还只是侦测了数组的变化,还要侦测数组元素的变化:

class Observer {constructor (value) {
        this.value = value
        this.dep = new Dep
        def(value, '__ob__', this) 
        if (!Array.isArray(value)) {this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
            // 侦测数组中的每一项
            this.observeArray(value) // 新增
        }
    }
    
    observeArray (items) {
        items.forEach(item => {observe(item)
        })
    }
    
    ...
}

然后还要侦测数组中的新增元素的变化:

methodsToPatch.forEach(function (method) {const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args)
      const ob = this.__ob__ 
      // 新增开始
      let inserted
      switch (method) {
              case 'push'
              case 'unshift'
                  inserted = args
                  breaak
              case 'splice'
                  inserted = args.slice(2)
                break
      }
      if (inserted) ob.observeArray(inserted)
      // 新增结束
      ob.dep.notify() 
      return resullt
  })
})

总结一下:

Array 追踪变化的方式和 Object 不一样,是通过拦截器去覆盖数组原型的方法来追踪变化。

为了不污染全局的 Array.prototype,所以只针对那些需要侦测变化的数组,对于不支持 __proto__的浏览器则直接把拦截器布置到数组本身上。

Observer 中,对每个侦测了变化的数据都加了 __ob__ 属性,并且把this(Observer 实例) 保存在__ob__ 上,主要有两个作用:

  • 标记数据是否被侦测了
  • 可以通过数据拿到__ob__,进一步拿到 Observer 实例。

所以把数组的依赖存放在 Observer 中,当拦截到数组发生变化时,向依赖发送通知。

最后还要通过 observeArray 侦测数组子元素和数组新增元素的变化。

退出移动版