深入Vue2原理

57 阅读1分钟

Vue2响应式框架

image.png

2.Object.defineProperty

  1. 实现数据的代理
class Vue {
  constructor(options) {
    this.$options = options
    // TODO:data可能是函数
    this._data = options.data
    this.initData();
  }
initData() {
  let data = this._data;
  let keys = Object.keys(data);
  for(let i = 0; i < keys.length; i++) {
    // 数据代理,实现vm.data
    Object.defineProperty(this, keys[i], {
      enumerable: true,
      configurable: true,
      get: function proxyGetter() {
        console.log('数据代理get')
        return data[keys[i]]
      },
      set: function proxySetter(value) {
        console.log('数据代理set')
        data[keys[i]] = value;
      }
    })
  }
 }
}
  1. 实现数据的劫持,变成响应式
class Vue {
  constructor(options) {
    this.$options = options
    // TODO:data可能是函数
    this._data = options.data
    this.initData();
  }
initData() {
  let data = this._data;
  let keys = Object.keys(data);
  for(let i = 0; i < keys.length; i++) {
    // 数据代理,实现vm.data
    Object.defineProperty(this, keys[i], {
      enumerable: true,
      configurable: true,
      get: function proxyGetter() {
        console.log('数据代理get')
        return data[keys[i]]
      },
      set: function proxySetter(value) {
        console.log('数据代理set')
        data[keys[i]] = value;
      }
    })
  }
  observe(data); // 数据劫持
 }
 
function observe(data) {
     // data是基本类型则返回
     let type = Object.prototype.toString.call(data);
     if(type !== '[object Object]' && type !== '[object Array]') {
    return
    }
     if(data.__ob__) {
    return data.__ob__ // $set
    }
    return new Observer(data);
  }
}

// Observer: 变成响应式
class Observer {
  constructor(data) {
    this.dep = new Dep() // $set
    if(Array.isArray(data)) {
      data.__proto__ = ArrayMethods;
      this.observeArray(data);
    } else {
      this.walk(data)
    }
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false,
      configurable: true,
      writable: true
    })
  }
  walk(data) {
    let keys = Object.keys(data);
    for(let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]])
    }
  }

  observeArray(arr) {
    for(let i = 0; i <arr.length; i++) {
      observe(arr[i])
    }
  }
}

function defineReactive(obj, key, value) {
  let childOb = observe(obj[key])
  let dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      dep.depend();
      if(childOb) {
        childOb.dep.depend(); // $set
      }
      console.log(`获取data的${key}值`)
      return value
    },
    set: function reactiveSetter(val) {
      console.log(`data的${key}发生了改变`)
      if(val === value) {
        return
      }
      dep.notice();
      value = val;
    }
  })
}

3. 收集依赖

class Dep {
  constructor() {
    this.subs = []
  }

  depend() {
    if(Dep.target) {
      this.subs.push(Dep.target)
    }
  }

  notice() {
    this.subs.forEach((watcher) => {
      // 依次执行回调函数
      watcher.run();
    })
  }
}

4. Watcher

我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。接着,它再负责通知通知其他地方。所以,我们要抽象的这个东西--Watcher

class Vue {
  constructor(options) {
    this.$options = options
    // TODO:data可能是函数
    this._data = options.data
    this.initData();
  }
initData() {
  let data = this._data;
  let keys = Object.keys(data);
  for(let i = 0; i < keys.length; i++) {
    // 数据代理,实现vm.data
    Object.defineProperty(this, keys[i], {
      enumerable: true,
      configurable: true,
      get: function proxyGetter() {
        console.log('数据代理get')
        return data[keys[i]]
      },
      set: function proxySetter(value) {
        console.log('数据代理set')
        data[keys[i]] = value;
      }
    })
  }
 observe(data);
 this.initWatch(); // 初始化watcher
 }
}

initWatch() {
  let watch = this.$options.watch;
  if(watch) {
    let keys = Object.keys(watch);
    for(let i = 0; i < keys.length; i++) {
      new Watcher(this, keys[i], watch[keys[i]]); // 收集watcher实例依赖
    }
  }
}

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.id = ++watcherId;
    this.get();
  }

  // 求值
  get() {
    Dep.target = this;
    this.vm[this.exp]; //触发getter,然后就收集依赖
    Dep.target = undefined;
  }

  run() {
    if(watcherQueue.indexOf(this.id) !== -1) { // 已经存在于队列中
      return
    }
    watcherQueue.push(this.id);
    let index = watcherQueue.length -1;
    Promise.resolve().then(() => {
      this.cb.call(this.vm); // callback执行一下
      watcherQueue.splice(index, 1);
    })
  }
}

5. 解决新增属性问题---$set

  1. 在创建observer实例时,也创建一个新的dep,挂在observer实例上,然后把observer实例挂载到对象的__ob__属性上。
  2. 触发getter时,不光把Watcher收集一份到之前的dep,也收集一份在这个新的dep上。
  3. 用户调用$set时,手动地触发__ob__.dep.notice()。
  4. 最后别忘了在notice()之前调用defineReactive把新的属性也定义成响应式。
class Observer {
  constructor(data) {
    this.dep = new Dep() // $set
    if(Array.isArray(data)) {
      data.__proto__ = ArrayMethods;
      this.observeArray(data);
    } else {
      this.walk(data)
    }
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false,
      configurable: true,
      writable: true
    })
  }
  
  walk(data) {
    let keys = Object.keys(data);
    for(let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]])
    }
  }

  observeArray(arr) {
    for(let i = 0; i <arr.length; i++) {
      observe(arr[i])
    }
  }
}

function observe(data) {
  // data是基本类型则返回
  let type = Object.prototype.toString.call(data);
  if(type !== '[object Object]' && type !== '[object Array]') {
    return
  }
  if(data.__ob__) {
    return data.__ob__ // $set
  }
  return new Observer(data);
}

function defineReactive(obj, key, value) {
  let childOb = observe(obj[key])
  let dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      dep.depend();
      if(childOb) {
        childOb.dep.depend(); // $set
      }
      console.log(`获取data的${key}值`)
      return value
    },
    set: function reactiveSetter(val) {
      console.log(`data的${key}发生了改变`)
      if(val === value) {
        return
      }
      dep.notice();
      value = val;
    }
  })
}

class Vue {
  constructor(options) {
    this.$options = options
    // TODO:data可能是函数
    this._data = options.data
    this.initData();
  }

  initData() {
    let data = this._data;
    let keys = Object.keys(data);
    for(let i = 0; i < keys.length; i++) {
      // 数据代理,实现vm.data
      Object.defineProperty(this, keys[i], {
        enumerable: true,
        configurable: true,
        get: function proxyGetter() {
          console.log('数据代理get')
          return data[keys[i]]
        },
        set: function proxySetter(value) {
          console.log('数据代理set')
          data[keys[i]] = value;
        }
      })
    }
    observe(data);
    this.initWatch();
  }
  initWatch() {
    let watch = this.$options.watch;
    if(watch) {
      let keys = Object.keys(watch);
      console.log(this)
      for(let i = 0; i < keys.length; i++) {
        new Watcher(this, keys[i], watch[keys[i]]);
      }
    }
  }

  $watch(key, cb) {
    new Watcher(this, key, cb);
  }
  $set(target, key, value) { // $set
    defineReactive(target, key, value); // 新增的属性也变成响应式。
    target.__ob__.dep.notice();
  }
}

6. 侦测数组的变化

  1. 因为前面已经说了,使用object.defineProperty的办法劫持数组,会存在问题(使用key索引)。所以在实现数据劫持的时候,数组本身不用管,而是去循环劫持数组的元素,因为元素也有可能是对象。 实现方法:数组的回调也通过__ob__.dep来收集,在数组调用push,pop等方法时手动去触发__ob__.dep.notice。
  2. 原型对象Array.prototype上的方法不能直接修改,因为这样会破坏其他用到这些方法的代码的功能。 实现方法:在数组和Array.prototype的原型链上插入一个自定义的对象,拦截原来的push等方法,在自定义对象中的同名方法中先执行原本对的方法,再去人为的调用__ob__.dep.notice()去执行之前收集的回调。
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype;
const methods = [
    'push',
    'pop'
    // 其他需要拦截的方法
]
methods.forEach(method => {
  ArrayMethods[method] = function (...args) {
    // args [1,2,3]
    if(method === 'push') {
      this.__ob__.observeArray(args);
    }
    const result = Array.prototype[method].apply(this, args)
    this.__ob__.dep.notice()
    return result
  }
})