vue 响应式解析

 约 20 分钟

Vue响应式解析

一个vue的小demo

<template>
  <div>price: {{price}}</div>
  <div>total: {{price * quantity}}</div>
  <div>totalPriceWithSale: {{totalPriceWithSale}}</div>
</template>
<script>
var vue = new Vue({
  el: '#app',
  data: {
    price: 5,
    quantity: 10,
    sum: 0
  },
  computed: {
    totalPriceWithSale() {
      return this.price * 0.8
    }
  },
  watch: {
    price (val) {
      this.sum += val
    }
  }
})
</script>

vue中当改变data中的price, quantity, sum中的值,其依赖这三个字段的地方就会触发更新,这就是响应式,那么vue具体是怎么实现的呢?

由于vue中的涉及的功能较多,所以我们先脱离vue,从最简单的开始了解

> let price = 5
> let quantity = 10
> let sum = 0
> let totalPriceWithSale = price * 0.8
> let total = price * quantity
> totalPriceWithSale
4
> total
50
> price = 10 // 修改 price 的值为10
10
> totalPriceWithSale // 未改变
4
> total // 未改变
50

vue中的代码改写成上面最原始的样子,当我们修改price的值为10,打印其依赖price的变量totalPriceWithSaletotal发现其值并没有改变,然而,在响应式中当price改变其依赖该属性的也会发生改变,那进一步优化一下吧, 将每次值更新需要重新执行的代码放在一个函数中,然后放在一个数组中保存起来,值更新后就重新执行函数

let price = 5
let quantity = 10
let sum = 0
let total = 0
let target = null // 当前需要执行的依赖函数
let storage = []
target = () => {
  total = price * quantity
}
function record() {
  storage.push(target)
}
record() // 保存值更新时需要执行的函数
target() // 初始化执行设置 total 值
// 遍历执行函数
function replay() {
  storage.forEach(run => run())
}
price = 20
console.log(total) // 50
replay() // 执行依赖函数
console.log(total) // 200:值更新

上面的代码中,price的值更新后执行replay函数,其依赖pricetotal变量的值就进行了更新。还可以将上面的代码中对依赖的收集以及执行使用一个类来进行优化

class Dep{
  constructor() {
    this.subs = []
  }
  depend() {
    if (target && !this.subs.includes(target)) {
      this.susb.push(target)
    }
  }
  notify() {
    this.subs.forEach(run => run())
  }
}

使用上面的类对以上的代码来进行优化

let price = 5
let quantity = 10
let sum = 10
let total = 0
const dep = new Dep()
let target = null
target = () => {
  total = price * quantity
}
dep.depend() // 收集依赖
target()
console.log(total) // 50
price = 10 // 修改 price 的值
dep.notify() // 更新其依赖 price 的值
console.log(total) // 100

vue中对data中每一个属性都进行了Dep的实例,用来保存对其的依赖,在依赖的属性改变的时候更新值.现在对依赖收集进行了优化,那么能否对需要target进行优化使其复用呢

target = () => {
  total = price * quantity
}
dep.depend() // 收集依赖
target()

上面的这段代码,如果监听依赖price的另一个变量,则需要进行重写,不能友好的复用

watcher(() => {
  total = price * quantity
})
watcher(() => {
  sum = price + quantity
})
function wathcer(myFun) {
  target = myFun
  dep.depend()
  target()
  target = null
}
console.log(total, sum) // 50 15
price = 10
dep.notify()
console.log(total, sum) // 100 20

使用watcher能够很好的进行复用,在watcher中收集依赖函数,添加依赖。那么又有一个问题,每次修改值都需要手动的去执行dep.notify()函数,能不能当值改变的时候自动执行notify函数呢。在vue中,数据是存放在data属性中,该属性返回的是一个对象,可以对对象中的每一个属性进行监听,当获取或者设置值的时候调用相关的函数

let price = 5
let quantity = 10
let sum = 10
let total = 0
let salePrice = 0
// const dep = new Dep()
let target = null
let data = {
  price: 5,
  quantity: 10
}
Object.keys(data).forEach(ele => {
  const dep = new Dep()
  let interVal = data[ele]
  Object.defineProperty(data, ele, {
    get: () => {
      dep.depend()
      console.log(ele, dep.subs)
      // 其中的属性以及subs, price的subs中有3个函数,quantity只有两个
      //   price, Array(1) []
      //   quantity, Array(1) []
      //   price, Array(2) [, ]
      //   quantity, Array(2) [, ]
      //   price, Array(3) [, , ]
      //   price, Array(3) [, , ]
      //   quantity, Array(2) [, ]
      return interVal
    },
    set: (val) => {
      interVal = val
      dep.notify()
    }
  })
})
function watcher(myFun) {
  target = myFun
  target()
  target = null
}
watcher(() => {
  total = data.price * data.quantity
})
watcher(() => {
  sum = data.price + data.quantity
})
watcher(() => {
  salePrice = data.price * 0.7
})

值为对象时的处理

上面就是响应式的基本原理,现在data中的值都为基本的类型值,很容易处理,如果其属性值仍然为对象呢?

值仍然为对象时,那么需要进行递归监听,将Object.defineProperty的监听操作进行相应的封装,使用observe函数进行值额类型判断
如果其值为对象,则对其中的每个属性设置gettersetter, 并且在设置gettersetter之前判断其值的类型,值为对象则继续添加gettersetter

let data = {
  // price: 5,
  // quantity: 10,
  obj: {
    a: 1,
    b: 2,
    c: {
      d: 3
    }
  }
}
function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]'
}
function observe(val) {
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => {
      defineProperty(val, key)
    })
  }
}
function defineProperty(obj, key) {
  const dep = new Dep()
  let internalVal = obj[key]
  observe(internalVal)
  Object.defineProperty(obj, key, {
    get: function () {
      // TODO 外层对象改变,内部对象也应该更新依赖
      dep.depend()
      // target 函数为 target = () => { console.log('render obj', data.obj, data.obj.a, data.obj.b, data.obj.c, data.obj.c.d) }
      // obj 被访问5次,向其中添加了5次 watcher 中的 target 函数
      // obj 中的 c 属性,被访问两次,则其对应的 subs 中有两个 watcher 中的 target 函数
      // 为了避免向 subs 中添加重复的 target 函数,需要判断是否存在 this.subs.includes(target)
      console.log(`get key: ${key} dep: ${dep} subs: ${dep.subs}`)
      return internalVal
    },
    set: function(val) {
      internalVal = val
      dep.notify()
    }
  })
}
observe(data)
function watcher(myFun) {
  target = myFun
  target()
  target = null
}
watcher(() => {
  total = data.obj.a + data.obj.b + data.obj.c.d
  // console.log('render obj', data.obj, data.obj.a, data.obj.b, data.obj.c, data.obj.c.d)
})
// 修改其中的每一个属性的值,total 的值
total // 6
data.obj.a = 2
total // 7
data.obj.b = 3
total // 8
data.obj.c.d = 4
total // 9

上面对值为对象时进行了循环递归添加,那么当值为数组时怎么处理呢?

值为数组时的处理

let data = {
  arr: [1, 2, 3, {a: 4}, [5, 6]]
}
function observer(val) {
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    // 对数组中的每一项绑定
    val.forEach(ele => defineProperty(val,))
  }
}

上面的代码中对数组中的每一项都会进行监听,然后在数组的每一项改变的时候都会进行依赖更新,由于数组的数据量可能会很大,这样会比较影响性能(我暂时是这么认为的)

将上面的代码优化一下,当数组的中的值是基本类型时,不进行监听,引用类型是再进行监听

let data = {
  arr: [1, 2, 3, [4, 5], {a:  7}
}
function observer(val) {
  if (isObject(val) || Array.isArray(val)) {
    addObserver(val)
  }
}
function addObserver(val) {
  if(isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    val.forEach(ele => {
      observer(ele)
    })
  }
}
watcher(() => {
  total = data.arr[0] + data.arr[1] + data.arr[2] + data.arr[3][0] + data.arr[3][1] + data.arr[4].a
})
total // 22
data.arr[0] = 2 // 修改其中某个依赖的值
total // 22 未更新
data.arr[3][0] = 5 // 修改数组中的数组中的值
total // 22 未更新
data.arr[4].a = 8 // 修改数组中的对象的某个属性值
total // 25 更新

上面的方法没有监听数组中值为基本类型的值,但出现了一个问题,数组内嵌套数组不会被监听到,这也就是在vue中直接修改数组内的值或者数组中嵌套的数组中的值不会触发视图更新的原因,但是vue给我们提供了一些改变数组值的方法可以触发更新或者$set方法

那么vue中是怎么做到可以使用数组的方法修改数组以此来触发视图更新的呢?

对数组中的方法进行处理

// 用Array原型中的方法创建一个新对象
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
// 需要进行重写的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(ele => {
  const original = arrayProto[ele]
  Object.defineProperty(arrayMethods, ele, {
    value: function(...args) {
      const result = original.apply(this, args)
      console.log(`调用 ${ele} 方法`)
      return result
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})
function protoAugment(target, proto) {
  targte.__proto__ = proto
}
function addObserver(val) {
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    protoAugment(val, arrayMethods)
    val.forEach(ele => observe(ele))
  }
}

上面的代码执行后,调用data.arr数组的push, pop, shift, unshift, splice, sort, reverse都会打印出对应的提示,这样在调用相应的方法时都可以进行依赖分发。那么,问题又来了,怎么可以拿到依赖的dep呢?当我们调用这些方法时,是通过data.arr.push(3)这样的方式进行调用的,那么在push方法内部就可以通过this拿到data.arr这个数组,所以我们可以在这个数组中保存所需要的依赖dep,然后进行分发即可

let data = {
  arr: [1, 2, 3]
}
function observe(val) {
  if (isObject(val) || Array.isArray(val)) {
    return addObserver(val)
  }
}
function addObserver(val) {
  const dep = new Dep()
  Object.defineProperty(val, '__dep__', {
    value: dep,
    enumerable: false,
    writable: true,
    configurable: true
  })
  if (isObject(val)) {
    const keys = Object.keys(val)
    keys.forEach(key => defineProperty(val, key))
  } else if(Array.isArray(val)) {
    val.forEach(ele => observe(ele))
  }
  return val
}
function defineProperty(val, key) {
  const dep = new Dep()
  const internalVal = val[key]
  const childOb = observe(internalVal) // 获取到值为引用类型的 __dep__
  Object.defineProperty(val, key, {
    get: function() {
      dep.depend()
      if (childOb) {
        childOb.__dep__.depend() // 添加依赖,当调用 push 等方法时进行依赖分发
      }
    },
    set: function(newVal) {
      internalVal = newVal
      dep.notify()
    }
  })
}
methodsToPatch.forEach(ele => {
  const original = arrayProto[ele]
  Object.defineProperty(arrayMethods, ele, {
    value: function(...args) {
      const result = original.apply(this, args)
      const dep = this.__dep__
      console.log(`调用 ${ele} 方法`)
      dep.notity()
      return result
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})
total // 6
data.arr.push(4)
total // 10 值进行了更新

以上内容为本人理解,如有不对处,请指正

仓库地址 欢迎 star

参考:

阅读 455

推荐阅读
目录