Vue原理解析(一):任何人都能看懂的响应式原理和数据劫持原理以及实现一个$set

273 阅读2分钟

实现数据代理和数据递归劫持

在vue2.x的源码中使用Object.defineProperty()这个api实现数据代理, 也可以实现数据劫持, 下面是数据代理和数据劫持的简单实现,下文使用MVue实现一个类vue的类

class MVue{
  constructor(options){
    this.$options = options
    // TODO: data有可能是一个function
    this._data = options.data
    this.initData()
  }
    
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // 实现数据代理
    for(let i = 0; i< keys.length; i++){
      Object.defineProperty(this, keys[i], {
        enumerable: true,
        configurable: true,
        get: function proxyGetter() {
          console.log(`${keys[i]}取值`);
          return data[keys[i]]
        },
        set: function proxySetter(newValue) {
          if(value === newValue) {
            return
          }
          data[keys[i]] = newValue
          console.log(`${keys[i]}设置值`);
        }
      })
    }

    // 实现数据劫持
    for(let i = 0; i< keys.length; i++){
      let value = data[keys[i]]
      Object.defineProperty(data, keys[i], {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
          console.log(`${keys[i]}reactiveGetter取值`);
          return value
        },
        set: function reactiveSetter(newValue) {
          if(value === newValue) {
            return
          }
          value = newValue
          console.log(`${keys[i]}reactiveSetter设置值`);
        }
      })
    }
  }
}

上面代码存在的问题: 无法深度劫持到对象中属性仍存在对象的情况

const vm = new MVue({
  data:{
    person:{
      name: 'phil' // 无法监听到person中name属性的变化
    }
  }
})

递归实现深度劫持

function defineReactive(data, key, value) {
  observe(data[key])
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log(`${key}reactiveGetter取值`);
      return value
    },
    set: function reactiveSetter(newValue) {
      if(value === newValue) {
        return
      }
      value = newValue
      console.log(`${key}reactiveSetter设置值`);
    }
  })
}

function observe(data) {
  let type = Object.prototype.toString.call(data)
  if( type !== '[object Object]' && type !== '[object Array]' ) {
    return
  }
  new Observer(data)
}

class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk(data) {
    // 实现数据劫持
    let keys = Object.keys(data)
    for(let i = 0; i< keys.length; i++){
      let value = data[keys[i]]
      defineReactive(data, keys[i], value)
    }
  }
}

实现一个watcher类,实现vue中watch侦听器的功能

class MVue{
   ...
   $watch(exp, cb){
       new Watcher(this, exp, cb)
   }
   // vue中使用watch监听属性变化
   initWatch(){
    let watch = this.$options.watch
    if(watch) {
      Reflect.ownKeys(watch).forEach(watcher => {
        new Watcher(this, watcher, watch[watcher])
      })
    }
  }
}

class Dep {
  constructor() {
    this.subs = []
  }
  //收集依赖
  depend() {
    if(Dep.target) {
      this.subs.push(Dep.target)
    }
  }
  //派发更新
  notify() {
    this.subs.forEach(watcher => {
      watcher.run()
    })
  }
}

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.get()
  }
  get() {
    Dep.target = this
    this.vm[this.exp]
    Dep.target = null
  }
  run() {
    this.cb.call(this.vm)
  }
}

这样可以实现一个简单的watch实现对数据的监听,但是此时的watch与vue中的watch还是有差距:1.vue中的watch是异步的,2.vue中会将多次赋值操作合并成一个,并不会多次调用就多次执行,是异步更新的。实现异步我们自然想到的就是使用promise实现。 将上面的run方法进行如下改造: 即可实现

run() {
    if(watcherQueue.includes(this.id)) {
      return
    }
    watcherQueue.push(this.id)
    Promise.resolve().then(() =>{
      this.cb.call(this.vm)
      watcherQueue.pop()
    })
}

实现一个$set

接下来我们再讨论vue中set方法的实现思路,通过上面的分析,我们不难知道vue中为什么会有set方法的实现思路,通过上面的分析, 我们不难知道vue中为什么会有set的出现。因为vue2.x中的data中属性是一次性深度监听的,对于新增的属性,vue是无法监听到的。实现思路如下:

    1. 在创建observer实例的时候,创建一个container, 挂载到Observer的实例上,然后把Observer实例挂载到对象的__ob__属性上。
  • 2.触发getter的时候,收集一份watcher到container中
  • 3.用户调用$set的时候,手动触发__ob__.dep.notify()
  • 4.在notify之前调用defineReactive把新的属性定义成响应式的。
// 收集Observer实例上的依赖
function defineReactive(data, key, value) {
  let childOb = observe(data[key])
  const dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // console.log(`${key}reactiveGetter取值`);
      dep.depend()
      if( childOb ) {
        childOb.dep.depend()
      }
      return value
    },
    set: function reactiveSetter(newValue) {
      if(value === newValue) {
        return
      }
      value = newValue
      dep.notify()
      // console.log(`${key}reactiveSetter设置值`);
    }
  })
}
// 在监听的对象上定义一个__ob__属性
class Observer {
  constructor(data) {
    this.dep = new Dep()
    this.walk(data)
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false,
      writable: true,
      configurable: true
    })
  }
}

熟悉上述原理之后,vue2.x中的一些困惑也就迎刃而解了。

  1. 为什么vue不能直接对新增的属性实现响应式? vue在页面初始化的时候就实现了data中数据的响应式,后面直接对象点一个属性是无法不能被Object.defineproperty这个监听到的,vue中新增和删除属性都无法被vue监听到,所以setset和delete就应运而生了。

  2. vue项目中常见性能优化? data中数据的层级尽量不要嵌套太深,data中数据尽量少,这都能减轻页面在初始化的时候的压力。 3.vue2.x中实现数据响应式的缺点? -> vue3.0使用proxy可以解决这些问题

1) 深度监听,一次递归到底,一次性计算量巨大,
2) 无法监听新增和删除的属性,所以vue2.0 中提供了Vue.set 和 Vue.delete, 
3) 无法实现对数组的监听。

下期我们一起来分析, vue中是如何实现对数组的处理的?computed属性是如何实现的?vue是如何实现模板编 译的?如何实现一个vdom?