Vue源码之Object数据劫持

目录

  • vue中如何侦测Object变化?
    • Object.defineProperty
    • 数据劫持原理
  • Object 响应式中的问题
    • vm.$set内部原理
    • vm.$delete内部原理
  • vm.$watch内部原理
    • 用法
    • watch内部实现原理
    • deep实现原理

vue中如何侦测Object变化?

js中两种侦测对象变化方法:

  • 使用Object.defineProperty
  • ES6的proxy(ES6浏览器支持不理想)

Object.defineProperty

该方法定义一个响应式数据,当数据的属性发生变化的时候,通知依赖更新,即向使用到它的地方发送通知

Object可以通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter

Object.defineProperty(obj, key, {
        get() {
            return val;
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal
            }
        }
})

这样,修改数据时,我们可以主动通知依赖进行更新,那么问题来了:

  1. 如何收集依赖?什么时机收集依赖?

    思路:

    1. 任何地方,只要涉及到数据读取,就会触发get函数
    2. 先收集依赖,然后等属性发生变化的时候,再把之前收集好的依赖循环触发一遍就好了

    总结:在getter中收集依赖,在setter中触发依赖

  2. 依赖收集在哪里?

    定义一个Dep类,用来存储当前key的依赖。DEP类主要负责对依赖的收集、删除、发送通知。

  3. 依赖是谁?

    依赖就是用到数据的地方,用到这个数据的地方可能是视图中对应的坑({{name}}),也可能是开发者写的一个watch,于是抽象一个类Watcher

至此,问题告一段落

  • 定义一个响应式数据
//数据劫持,添加依赖追踪
    defineReactive(obj, key, val) {
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                //只有Watcher触发的getter才会收集依赖
                Dep.target && dep.depend(); 
                return val;
            },
            set(newVal) {
                if (newVal !== val) {
                    val = newVal;
                    // 数据发生变更通知所有的观察者
                    dep.notify()
                }
            }
        })
        //递归树
        this.observer(val);
    }
  • 定义一个Dep类,收集依赖
// Dep发布者相当于vue data 对象中的某一个属性如:name
class Dep {
    constructor(vm, key) {
        this.subs = [];
    }
    
    depend() {
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    removeSub(sub) {
        remove(this.subs, sub);
    }

    notify() {
        //只负责通知更新,具体是否更新以及更新操作由watcher做
        this.subs.forEach(sub => sub.update())
    }
}

  • 定义依赖 – Watcher类

当new Watcher()实例时,会自动将依赖收集到Dep中

// watcher 相当于视图中对应的坑({{name}}),一个坑对应有一个观察者,监听此处的数据变化
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm;
        this.key = key;
        this.cb = cb;

        //这段代码会自动将Watcher添加到Dep中
        //先将Watcher赋值给Dep.target,然后读取一下值,会触发get,这时依赖就被收集了,再将Dep.target置空
        Dep.target = this;
        this.vm[this.key];
        Dep.target = null;
    }

    update() {
        //每个watcher所做的更新大不一样,所以将具体的更新操作放到回调里面去做
        this.cb.call(this.vm, this.vm[this.key])
    }
  };
}
  1. 封装类Observer,将数据内的所有属性都转换成getter/setter形式

完整代码:

class Observer{
    constructor(obj){
        this.obj = obj
        if(!Array.isArray(obj)){
            this.walk(obj)
        }
    }
    walk(obj){
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}
function defineReactive(obj, key, val) {
    // 递归子属性
    if(typeof val === 'object'){
        new Observer(val)
    }
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        get() {
            //如果有创建了一个观察者,那么Dep.target就会被它的实例赋值,此时就会通过访问实例的值(见Watcher 构造函数)将其add到dep
            //同时这里的判断是为了防止重复添加
            Dep.target && dep.depend();
            return val;
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal;
                // 数据发生变更通知所有的观察者
                dep.notify()
            }
        }
    })
}

数据劫持原理

Vue源码之Object数据劫持_第1张图片

  1. Data通过Observer转换成响应式对象
  2. 当外界通过Watcher读取数据时,会触发getter从而将watcher添加到Dep中
  3. 当数据发生变化时,会触发setter,Dep像依赖(Watcher)发送通知
  4. Watcher收到通知后,向外界发送通知,可能会触发视图更新,或者执行回调函数。

tip: 至于何时new Watcher() ,我们在模板编译的时候进行说明

Object 响应式中的问题

通过defineProperty方法定义obj之后, 对Obj新增属性和删除属性的操作,将无法追踪到这个变化。
原因: ES6之前,JS没有提供元编程能力

//新增属性
this.obj.name = 'xxx'
//删除属性
delete this.obj.name

为了解决这个问题,vue.js提供的两个API——vm. s e t 和 v m . set和vm. setvm.delete

vm.$set

用法:vm.$set(target, key, value)

import {set} from './observer/index' Vue.prototype.$set = set

代码中数组的部分可以在读了《vue源码之Array》之后再了解,源码及解读:

function set (target, key, val) {
    // 当target是一个数组并且key是一个有效的索引
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val
    }
    // 如果key 已经存在于对象中,直接修改值即可
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val
    }
    
    
    // 源码中,在Observer一个对象时,会给对象添加__ob__属性,标识这是一个响应式对象
    var ob = (target).__ob__;
    
    // 如果target上并没有__ob__属性,说明它本身就不是一个响应式的对象,不做处理
    if (!ob) {
      target[key] = val;
      return val
    }
    //否则,将属性转换成响应式,并向依赖发出更新通知
    defineReactive(ob.value, key, val);
    ob.dep.notify();
    return val
  }

vm.$delete

用法:vm.$delete(target, key)

import {del} from './observer/index' Vue.prototype.$delete = del

源码解读:

function del (target, key) {
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.splice(key, 1);
      return
    }
    var ob = (target).__ob__;

    // 不存在此属性时,直接返回
    if (!hasOwn(target, key)) {
      return
    }
    delete target[key];
    if (!ob) {
      return
    }
    // 删除属性后,像依赖发送通知
    ob.dep.notify();
  }

vm.$watch内部原理

1. 用法

vm.$watch( expOrFn, callback, [options] )

用于观察一个表达式或computed函数的变化,来达到todo something的目的。

返回一个取消观察的函数,用于停止触发回调

var unwatch = vm.$watch('a.b.c', (newVal, oldVal) => {})
unwatch()
  • expOrFn
    1. 表达式只支持以点分隔的路径
    2. computed函数,如 function(){ return this.name + this.age}
  • options
    1. deep:true 深度监听对象内部值的变化,监听数组的变化不需要这么做
    2. immediate:true 立即以当前值触发一次回调

2. watch的实现原理

vm. w a t c h 实 际 上 是 对 W a t c h e r 的 一 种 封 装 。 无 论 是 视 图 中 的 多 个 依 赖 ( 如 n a m e 出 现 的 地 方 ) , 还 是 使 用 watch实际上是对Watcher的一种封装。无论是视图中的多个依赖(如{{name}}出现的地方),还是使用 watchWatchername使watch监听值的变化,都可以看做是观察者。

实质:当用户使用vm.$watch()来监听一个数据变化时,实际跟我们在视图中新增一个坑{{name}},所做的事情是一样的。

  1. 实例化了一个Watcher
  2. 将Watcher收集到Dep中
  3. 当expOrFn对应的值发生变化时,会在原有的基础上新增一个依赖通知,触发cb调用

源码解读:

 Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      options = options || {};
      // 创建一个依赖,并收集到Dep
      var watcher = new Watcher(vm, expOrFn, cb, options);
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
        }
      }
      return function unwatchFn () {
      // 依赖将自己从被收集到的所有Dep中移除,终止订阅数据变更通知
        watcher.teardown();
      }
    };
  }
  • Watcher 和 Dep 多对多的关系
    1. expOrFn是一个表达式,则只会对应一个Dep
    2. expOrFn是一个函数时,当函数中使用了多个数据时,那么Watcher就要收集多个Dep
    3. 同时一个数据在多处运用,所以Dep对应多个Watcher
//Watcher会收集name和age两个的Dep
// 同时这两个Dep中也会收集Watcher
// 任意一个数据发生变化,Watcher都会收到通知
this.$watch(function(){
    return this.name + this.age
},(newVal, oldVal) => {})

3. deep实现原理

思路:

  1. 无非就是收集依赖,将数据以及数据内部的子值都触发一遍依赖收集
  2. 在Watcher类中实现

初始化实例时,判断是否deep,注意代码在之前执行

Watcher类的修改:

 Dep.target = this;
 this.vm[this.key]; //收集到当前数据Dep
 
 // 新增
 if(!!options.deep){
     traverse(value) // 递归收集到当前数据子值的Dep
 }
 
 Dep.target = null;

添加到子值Dep

  var seenObjects = new Set();
  
  function traverse (val) {
    _traverse(val, seenObjects);
    seenObjects.clear();
  }

  function _traverse (val, seen) {
    var i, keys;
   
    if (val.__ob__) {
      var depId = val.__ob__.dep.id;
      if (seen.has(depId)) {
        return
      }
      seen.add(depId);
    }
    // 重点看这里
    // 如果值是一个对象,则递归
      keys = Object.keys(val);
      i = keys.length;
      while (i--) {
      // 其中val[keys[i]] 会触发getter,也就是此处会进行依赖收集
      _traverse(val[keys[i]], seen); }
  }

_traverse(val[keys[i]], seen)

其中val[keys[i]]会触发getter,也就是此处会进行依赖收集。

至此,所有值的变化,都会触发cb。


为了便于理解,文章中的代码都是源码的删减版

你可能感兴趣的:(Vue)