今天看啥  ›  专栏  ›  shisanOnly

Vue源码之响应式原理

shisanOnly  · 掘金  ·  · 2019-08-02 06:01
阅读 24

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侦测数组子元素和数组新增元素的变化。




原文地址:访问原文地址
快照地址: 访问文章快照