vue源码之数据检测

概述

vue是数据驱动页面,数据即状态的变化,页面即状态的变化导致页面的变化,也就是说可以用公式表示:UI=render(state)。UI和state都是用户设置的,都是可变的,不变的只有render(),所以vue起的就是render()的作用。
那么render有什么作用呢?即监听state什么时候读取和什么时候改变了,这个可以使用Object.defineProperty方法去实现。来个例子:

const obj = {
  name: 'sifan',
  age: 21
}

let name = obj.name

Object.defineProperty(obj, 'name', {
  enumerable: true,
  configurable:true,
  set(val) {
    console.log('调用set方法');
    name = val
  },
  get() {
    console.log('调用get方法');
    return name
  }
})


console.log(obj.name);
obj.name = '20'
console.log(obj.name);


如果只是定义对象而不设置Object.defineProperty的话,虽然也可以修改和得到数据,但是不能知道什么时候在调用或修改。那么既然可以监听到obj的一个属性了,也表示可以监听obj的其他所有属性。
为了可以把obj的所有属性都变得可观测,可以定义如下代码:

export default class Observer {
  constructor(val) {
    // 新建dep实例
    this.dep = new Dep()
    // 在这个属性上设置__ob__,避免重复遍历
    def(val, '__ob__', this, false)
    // 判断是不是数组,因为vue中数组方法不可用,所以需要对数组的方法进行重构
    if (!Array.isArray(val))
      // 如果该值不是数组,对这个值进行监听
      this.walk(val)
    else {
      // 是数组,将重构好的arrayMethods设置到val上
      Object.setPrototypeOf(val, arrayMethods)
      // 进行监听
      this.observeArray(val)
    }
  }
  walk(val) {
    for (const i in val) {
      defineReactive(val, i)
    }
  }
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      observe(arr[i])
    }
  }
}

export default function defineReactive(obj, str, val) {
  if (arguments.length == 2) {
    val = obj[str]
  }
  let childOb = observe(val)
  Object.defineProperty(obj, str, {
    configuration: true,
    enumerate: true,
    set(newData) {
      console.log('正在修改' + str);
      // 调用notify去通知依赖的watcher去修改
      val = newData
      // 再次进行监听,因为不知道修改的值是数组还是对象或者普通值
      observe(val)
      dep.notify()
    },
    get() {
      if(childOb) {
        childOb.dep.depend()
      }
      console.log('正在调用' + str);
      return val
    }
  }) 
}

export default function observe(val) {
  // 如果是普通值,直接返回
  if (typeof val != 'object') return
  let ob = null
  // 如果为空,进行遍历,否则将值返回
  if (typeof val.__obj__ !== 'undefined') {
    ob = val.__obj__
  } else {
    ob = new Observer(val)
  }
  return ob
}

//处理数组的方法:在vue中创建一个数组方法拦截器,拦截在数组实例和Array.prototype之间,在拦截器中重写操作数组的方法
let arrayPototype = Array.prototype

export const arrayMethods =Object.create(arrayPototype)

let methodsNeedChange = [
  'push',
  'pop',
  'unshift',
  'shift',
  'splice',
  'sort',
  'reverse'
]

//重写方法
methodsNeedChange.forEach(methodName => {
  const original = arrayPototype[methodName]
  def(arrayMethods, methodName, function () {
    let arg = [...arguments]
    const result = original.apply(this, arg)
    const ob = this.__ob__
    let inserted = []
    switch (methodName) {
      case 'push':
      case 'unshift':
        inserted = arg
        break;
      case 'splice':
        inserted = arg.slice(2)
    }
    if (inserted) {
      ob.arrayPototype(inserted)
    }
    ob.dep.notify()
    return result
  }, false)
})

那么简单的实现数据响应式更新已经完成了,通知数据的更新并去更改所有使用到该数据的代码如何实现呢?这个实现主要有两个类,一个是Watcher类,表示的就是依赖,大概意思是那个地方用到了某个数据,那watcher就是它的依赖;另一个是dep类,用来存储依赖。代码实现如下:

export default class Watcher {
  constructor(vm, expOrFn, cb) {
    //实例
    this.vm = vm
    this.cb = cb
    //这是什么?
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get() {
    //让别人知道现在正在用
    window.target = this;
    const vm = this.vm
    //获取依赖的数据,因为在获取数据中有dep.depend()的调用,
    // 然后就可以将window.target中的值存入到依赖的数组
    let value = this.getter.call(vm, vm)
    // 释放
    window.target = undefined
    return value
  }
  update() {
    const oldValue = this.value
    this.value = this.get()
    // 意思就是,将this.value和oldValue作为vm的参数运行
    // 调用数据更新回调函数,从而更新视图
    this.cb.call(this.vm, this.value, oldValue)
  }
}
// 正则表达式,将点语法的东西取出来
const baiRE = /[^\w.$]/
function parsePath(path) {
  if (baiRE.test(path)) {
    return
  }
  const segments = pat.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) {
        return
      }
      obj = obj[segments[i]]
    }
    return obj
  }
}

export default class Dep {
  constructor() {
    //存储有多少
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  removeSub(sub) {
    this.remove(this.subs, sub)
  }
  depend() {
    if (window.target) {
      this.addSub(window.target)
    }
  }
  notify() {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      // 调用watcher实例中的update,去调用数据变化的更新回调函数
      subs[i].update()
    }
  }
  remove(arr, item) {
    if (arr.length) {
      const index = arr.indexOf(item)
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
  }
}

如上就能实现数据的变化和侦测,但是仍然有不足之处,这个不足之处只是我看别人说的,但是我自己有点想不到,比如别人说添加一个属性或删除一个属性都无法检测到,所以vue设置了delete和set,这部分还没有看。以后看了再更。

posted @ 2021-08-17 16:08  卿六  阅读(103)  评论(0编辑  收藏  举报