Object.defineProperty

  1. var arr = [1,2,3];
  2. arr.forEach((item, index) => {
  3. Object.defineProperty(arr, index, {
  4. get() {
  5. console.log('数组被getter拦截')
  6. return item
  7. },
  8. set(value) {
  9. console.log('数组被setter拦截')
  10. return item = value
  11. }
  12. })
  13. })
  14. arr[1] = 4;
  15. console.log(arr)
  16. // 结果
  17. 数组被setter拦截
  18. 数组被getter拦截
  19. 4

已知长度的数组是可以通过索引属性来设置属性的访问器属性的,但数据的 add 无法拦截,因为数组所添加的索引值并没有预先加入数据拦截中。 所以 Vue 在响应式系统中对数组的方法进行了重写。

Proxy

和 Object.defineProperty 一样,Proxy 可以修改某些操作的默认行为,但是不同的是,Proxy 针对目标对象会创建一个新的实例对象,并将目标对象代理到新的实例对象上。 本质的区别是后者会创建一个新的对象对原对象做代理,外界对原对象的访问,都必须先通过这层代理进行拦截处理。而拦截的结果是我们只要通过操作新的实例对象就能间接的操作真正的目标对象了

  1. var arr = [1, 2, 3]
  2. let obj = new Proxy(arr, {
  3. get: function (target, key, receiver) {
  4. // console.log("获取数组元素" + key);
  5. return Reflect.get(target, key, receiver);
  6. },
  7. set: function (target, key, receiver) {
  8. console.log('设置数组');
  9. return Reflect.set(target, key, receiver);
  10. }
  11. })
  12. // 1. 改变已存在索引的数据
  13. obj[2] = 3
  14. // result: 设置数组
  15. // 2. push,unshift添加数据
  16. obj.push(4)
  17. // result: 设置数组 * 2 (索引和length属性都会触发setter)
  18. // // 3. 直接通过索引添加数组
  19. obj[5] = 5
  20. // result: 设置数组 * 2
  21. // // 4. 删除数组元素
  22. obj.splice(1, 1)

数据初始化

数据初始化的过程就是进行响应式设计的过程

  1. function initState (vm) {
  2. vm._watchers = [];
  3. var opts = vm.$options;
  4. // 初始化props
  5. if (opts.props) { initProps(vm, opts.props); }
  6. // 初始化methods
  7. if (opts.methods) { initMethods(vm, opts.methods); }
  8. // 初始化data
  9. if (opts.data) {
  10. initData(vm);
  11. } else {
  12. // 如果没有定义data,则创建一个空对象,并设置为响应式
  13. observe(vm._data = {}, true /* asRootData */);
  14. }
  15. // 初始化computed
  16. if (opts.computed) { initComputed(vm, opts.computed); }
  17. // 初始化watch
  18. if (opts.watch && opts.watch !== nativeWatch) {
  19. initWatch(vm, opts.watch);
  20. }
  21. }

initProps

  1. function initProps (vm, propsOptions) {
  2. var propsData = vm.$options.propsData || {};
  3. var loop = function(key) {
  4. ···
  5. defineReactive(props,key,value,cb);
  6. if (!(key in vm)) {
  7. proxy(vm, "_props", key);
  8. }
  9. }
  10. // 遍历props,执行loop设置为响应式数据。
  11. for (var key in propsOptions) loop( key );
  12. }

响应式构建

1387789-20190326154616030-430894473.png

  • Observer 类:实例化一个Observer类会通过Object.defineProperty对数据的getter,setter方法进行改写,在getter阶段进行依赖的收集,在数据发生更新阶段,触发setter方法进行依赖的更新
  • Watcher 类:实例化watcher类相当于创建一个依赖,简单的理解是数据在哪里被使用就需要产生了一个依赖。当数据发生改变时,会通知到每个依赖进行更新
  • Dep 类:收集依赖和派发更新依赖 ```javascript // 使一个对象响应化 function observify(val) { if (!isObject(val)) {

    1. return;

    }

    if (Array.isArray(val)) {

    1. observifyArray(val);
    2. val.forEach(item => {
    3. observify(item);
    4. })

    } else {

    1. Object.keys(val).forEach(key => {
    2. defineReactive(val, key, val[key]); // 遍历每个键使其响应化
    3. })

    }

}

  1. defineReactive 中实例化一个 Dep 类,为每个数据都创建一个依赖的管理。
  2. ```javascript
  3. // 为对象的一个键应用 Object.defineProperty
  4. function defineReactive(obj, key, value) {
  5. //为每个键都创建一个 dep
  6. let dep = new Dep();
  7. observify(value); // 递归
  8. Object.defineProperty(obj, key, {
  9. enumerable: true,
  10. configurable: true,
  11. get() {
  12. if(Dep.target) {
  13. dep.depend();
  14. }
  15. return value;
  16. },
  17. set: (newVal) => {
  18. if (newVal === value) {
  19. return;
  20. }
  21. value = newValue;
  22. observify(newValue);
  23. // 通知该数据收集的watcher依赖,遍历每个watcher进行数据更新,
  24. dep.notify(newVal, oldVal);
  25. }
  26. })
  27. }
  1. // Dep 类,保存数据源的所有订阅,并在接收到数据源的变动通知后,触发所有订阅
  2. let uid = 1;
  3. class Dep {
  4. constructor() {
  5. this.id = uid++; // 为每个 dep 标记一个 uid
  6. this.subs = [];
  7. }
  8. // 添加订阅
  9. addSub(sub) {
  10. this.subs.push(sub);
  11. }
  12. // 依赖收集函数,Dep.target为当前执行的watcher
  13. // 在 getter 中执行,在 Dep.target 上找到当前 watcher,并添加依赖
  14. depend() {
  15. Dep.target && Dep.target.addDep(this);
  16. }
  17. notify(newVal, oldVal) {
  18. this.subs.forEach(sub => {
  19. sub.update(newVal, oldVal); // 触发订阅
  20. })
  21. }
  22. }
  23. // Dep.target 用来暂存正在收集依赖的当前 watcher
  24. Dep.target = null;
  1. // Watcher 类,每个 Watcher 为一个订阅源
  2. class Watcher {
  3. /**
  4. *
  5. * @param {*} expFn 依赖收集函数
  6. * @param {*} cb 回调
  7. * @param {*} options 附加配置
  8. */
  9. constructor(expFn, cb, options = {}) {
  10. this.context = options.context
  11. this.expFn = expFn
  12. this.depIds = new Set() //标记当前 watcher 已经加入到了哪些 dep
  13. this.cb = cb
  14. this.value = this.subAndGetValue()
  15. this.clonedOldValue = _.cloneDeep(this.value)
  16. }
  17. // 执行回调
  18. update() {
  19. let value = this.subAndGetValue() //获取 newValue
  20. if(!_.isEqual(value, this.clonedOldValue)) { // 比对前后两次值是否相等时借助一下 lodash 中的 isEqual 函数进行比较
  21. this.value = value
  22. this.cb.call(this.context, value, this.clonedOldValue)
  23. this.clonedOldValue = _.cloneDeep(value) // 缓存本次结果,会成为下次的 oldValue, 对于对象使用深拷贝
  24. }
  25. }
  26. //执行依赖收集函数,订阅依赖!
  27. subAndGetValue() {
  28. Dep.target = this // 把当前 watcher 放到 Dep.target 上,这样 getter 就知道应该把哪个 watcher 加入 dep 中了。
  29. let value = this.expFn.call(this.context)
  30. Dep.target = null // 订阅完置回空。
  31. return value
  32. }
  33. // 在 dep 上添加订阅
  34. // 为每个数据依赖收集器添加需要被监听的watcher
  35. addDep(dep) {
  36. if(!this.depIds.has(dep.id)) { //防止重复订阅,防止在一个 dep 中订阅两次
  37. this.depIds.add(dep.id)
  38. dep.addSub(this)
  39. }
  40. }
  41. }
  42. //简单封装下 new Watcher
  43. function watch(expFn, cb, context) {
  44. return new Watcher(expFn, cb, context)
  45. }

参考资料

  1. Vue 响应式原理
  2. 深入响应式系统构建
  3. 深入响应式原理
  4. 现代前端科技解析 —— 数据响应式系统 (Data Reactivity System)