vue源码探秘之响应式原理__Vue.js__前端
发布于 3 年前 作者 banyungong 1296 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

上两篇文章我们一起探秘了抽象语法树diff算法,今天来聊一聊响应式原理。

Vue2.x-数据响应式原理

Object.defineProperty() 方法

  • Object.defineProperty() 方法会直接在一个对象定义一个属性,或者修改一个对象的现有属性,并返回此对象。
var obj = {}

Object.defineProperty(obj, 'a', { value: 3 })
Object.defineProperty(obj, 'b', { value: 5 })

console.log(obj)
console.log(obj.a, obj.b)
var obj = {}

Object.defineProperty(obj, 'a', {
  // getter
  get() {
    console.log('访问a')
  },
  // setter
  set() {
    console.log('改变a')
  }
})

Object.defineProperty(obj, 'b', { value: 5, enumerable: true })

console.log(obj)
console.log(obj.a++, obj.b)

defineReactive 函数

  • getter/setter 需要变量周转才能工作
var obj = {}
var temp
Object.defineProperty(obj, 'a', {
  // getter
  get() {
    console.log('访问a')
    return temp
  },
  // setter
  set(newValue) {
    console.log('改变a')
    temp = newValue
  }
})
  • defineReactive 函数
function defineReactive(data, key, val) {
  // 闭包环境
  Object.defineProperty(data, key, {
    // 可枚举
    enumerable: true,
    // 可以被配置
    configurable: true,
    // getter
    get() {
      console.log('访问' + key)
      return val
    },
    // setter
    set(newValue) {
      console.log('改变' + key, newValue)
      if (val === newValue) return
      val = newValue
    }
  })
}

var obj = {}

defineReactive(obj, 'a', 10)
console.log(obj.a)
obj.a = 60
obj.a += 10
console.log(obj.a)

递归侦测对象全部属性

  • Observer:讲一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的)的 object
  • index.js
import observe from './observe'
var obj = {
  a: {
    m: {
      n: 10
    }
  },
  b: 20
}

observe(obj)
obj.b++
console.log(obj.a.m.n)
  • observe.js
import Observer from './Observer'
// 创建 Observer 函数
export default function (value) {
  // 如果 value 不是对象,什么都不做
  if (typeof value !== 'object') return
  // 定义 ob
  var ob
  if (typeof value.__ob__ !== 'undefined') {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}
  • Observer.js
import { def } from './utils'
import defineReactive from './defineReactive'
export default class Observer {
  constructor(value) {
    // 给实例(this 一定要注意,构造函数中的 this 不是表示类本身,而是表示实例)
    // 添加了 __ob__ 属性,值是这次 new 的实例
    def(value, '__ob__', this, false)
    console.log('Observer构造器', value)
    // Observer类的目的是:将一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的)的 object
    this.walk(value)
  }

  // 遍历
  walk(value) {
    for (const key in value) {
      defineReactive(value, key)
    }
  }
}
  • defineReactive.js
import observe from './observe'
export default function (data, key, val) {
  // 闭包环境
  if (arguments.length === 2) {
    val = data[key]
  }
  // 子元素要进行 observe,至此形成了递归,这个地柜不是函数自己调用自己,而是多个函数、类循环调用
  let childOb = observe(val)

  Object.defineProperty(data, key, {
    // 可枚举
    enumerable: true,
    // 可以被配置
    configurable: true,
    // getter
    get() {
      console.log('访问' + key)
      return val
    },
    // setter
    set(newValue) {
      console.log('改变' + key, newValue)
      if (val === newValue) return
      val = newValue
      // 当设置了新值,这个新值也要被 observe
      childOb = observe(newValue)
    }
  })
}
  • utils.js
export const def = function (obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true
  })
}

数组的响应式处理

  • Vue 底层改写了 Array 原生的 7 个方法:['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
  • 这七个方法在 Array.prototype 上
  • index.js
import observe from './observe'
var obj = {
  a: {
    m: {
      n: 10
    }
  },
  b: 20,
  g: [22, 33, 55]
}

observe(obj)
obj.g.splice(2, 1, [666, 888])
obj.g.push(66)
console.log(obj)
// obj.b++
// console.log(obj.a.m.n)
// defineReactive(obj, 'a')
// console.log(obj.a.m.n)
  • array.js
import { def } from './utils'
// 得到 Array.prototype
const arrayPrototype = Array.prototype
// 以 Array.prototype 为原型创建 arrayMethods 对象
export const arrayMethods = Object.create(arrayPrototype)
// 要被改写的7个数组方法
const methodsNeedChange = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsNeedChange.forEach((methodName) => {
  // 备份原来的方法,7个方法的功能不能被剥夺
  const original = arrayPrototype[methodName]
  // 把这个数组身上的 __ob__ 取出来,__ob__ 已经被添加了,为什么已经被添加了?因为数组肯定不是最高层
  // 比如 obj.g 属性是数组,obj 不能是数组,第一次遍历 obj 这个对象的第一层的时候,已经给 g 属性
  // (就是这个数组)添加了 __ob__ 属性
  // 定义新的方法
  def(
    arrayMethods,
    methodName,
    function () {
      // 恢复原来的功能
      const result = original.apply(this, arguments)
      const ob = this.__ob__
      // 将 arguments 变为数组
      const args = [...arguments]
      // 有三种方法 push/unshift/splice 能够插入新项,现在要把插入的新项也要变为 observe 的
      let inserted
      switch (methodName) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          // splice 格式是 splice(下标, 数量, 插入的新项)
          inserted = args.slice(2)
          break
      }
      // 判断有没有要插入的新项,让新项也变为响应的
      if (inserted) {
        ob.observeArray(inserted)
      }
      console.log('lalalala')
      return result
    },
    false
  )
})
  • Observer.js
import { def } from './utils'
import defineReactive from './defineReactive'
import { arrayMethods } from './array'
import observe from './observe'
export default class Observer {
  constructor(value) {
    // 给实例(this 一定要注意,构造函数中的 this 不是表示类本身,而是表示实例)
    // 添加了 __ob__ 属性,值是这次 new 的实例
    def(value, '__ob__', this, false)
    console.log('Observer构造器', value)
    // Observer类的目的是:将一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的)的 object
    // 检查它是数组还是对象
    if (Array.isArray(value)) {
      // 如果是数组,将这个数组的原型,指向 arrayMethods
      Object.setPrototypeOf(value, arrayMethods)
      // 让这个数组变得 observe
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  // 遍历
  walk(value) {
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  // 数组的特殊遍历
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      // 逐项 observe
      observe(arr[i])
    }
  }
}

依赖收集

什么是依赖?

  • 需要用到数据的地方,称为依赖
  • Vue1.x细粒度依赖,用到数据的DOM都是依赖
  • Vue2.x中等粒度依赖,用到数据的组件都是依赖
  • 在 getter 中收集依赖,在 setter 中触发依赖

Dep 类 和 Watcher 类

  • 把依赖收集的代码封装成一个 Dep 类,它专门用来管理依赖,每个 Observer 的实例,成员中都有一个 Dep 的实例
  • Wacther 是一个中介,数据发生变化时通过 Wacther 中转,通知组件

20210204155336231.png

20210204155344541.png

  • 依赖就是 Watcher 。只有 Watcher 触发的 getter 才会收集依赖,哪个 Wacther 触发了 getter ,就把哪个 Watcher 收集到 Dep 中。

  • Dep 使用发布-订阅模式,当数据发生变化时,会循环依赖列表,把所有的 Watcher 都通知一遍。

  • 代码实现的巧妙之处:Watcher 把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的 getter 。在 getter 中就能得到当前正在读取数据的 Watcher ,并把这个 Watcher 收集到 Dep 中

代码

  • index.js
import observe from './observe'
import Watcher from './Watcher'
var obj = {
  a: {
    m: {
      n: 10
    }
  },
  b: 20,
  g: [22, 33, 55]
}

observe(obj)
obj.a.m.n = 8
obj.g.splice(2, 1, [666, 888])
obj.g.push(66)
console.log(obj)
new Watcher(obj, 'a.m.n', (val) => {
  console.log('*我是Watcher,我在监控a.m.n', val)
})
obj.a.m.n = 8888
  • Observer.js
import { def } from './utils'
import defineReactive from './defineReactive'
import { arrayMethods } from './array'
import observe from './observe'
import Dep from './Dep'
export default class Observer {
  constructor(value) {
    // 在每个 Observer 的实例身上,都有一个 dep
    this.dep = new Dep()
    // 给实例(this 一定要注意,构造函数中的 this 不是表示类本身,而是表示实例)
    // 添加了 __ob__ 属性,值是这次 new 的实例
    def(value, '__ob__', this, false)
    console.log('Observer构造器', value)
    // Observer类的目的是:将一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的)的 object
    // 检查它是数组还是对象
    if (Array.isArray(value)) {
      // 如果是数组,将这个数组的原型,指向 arrayMethods
      Object.setPrototypeOf(value, arrayMethods)
      // 让这个数组变得 observe
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  // 遍历
  walk(value) {
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  // 数组的特殊遍历
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      // 逐项 observe
      observe(arr[i])
    }
  }
}
  • defineReactive.js
import observe from './observe'
import Dep from './Dep'
export default function (data, key, val) {
  const dep = new Dep()
  // 闭包环境
  if (arguments.length === 2) {
    val = data[key]
  }
  // 子元素要进行 observe,至此形成了递归,这个地柜不是函数自己调用自己,而是多个函数、类循环调用
  let childOb = observe(val)

  Object.defineProperty(data, key, {
    // 可枚举
    enumerable: true,
    // 可以被配置
    configurable: true,
    // getter
    get() {
      console.log('访问' + key)
      // 如果处于依赖收集阶段
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return val
    },
    // setter
    set(newValue) {
      console.log('改变' + key, newValue)
      if (val === newValue) return
      val = newValue
      // 当设置了新值,这个新值也要被 observe
      childOb = observe(newValue)
      // 发布-订阅模式,通知 dep
      dep.notify()
    }
  })
}
  • Dep.js
var uid = 0

export default class Dep {
  constructor() {
    console.log('Dep构造器')
    this.id = uid++
    // 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思
    // 这个数组里面放的是 Watcher 的实例
    this.subs = []
  }
  // 添加订阅
  addSub(sub) {
    this.subs.push(sub)
  }
  // 添加依赖
  depend() {
    // Dep.target 就是一个我们自己指定的全局的位置
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  // 通知更新
  notify() {
    console.log('我是notify')
    // 浅克隆一份
    const subs = this.subs.slice()
    // 遍历
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
  • Watcher.js
import Dep from './Dep'

var uid = 0

function parsePath(str) {
  var segments = str.split('.')
  return (obj) => {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

export default class Watcher {
  constructor(target, expression, callback) {
    console.log('Watcher构造器')
    this.id = uid++
    this.target = target
    this.getter = parsePath(expression)
    this.callback = callback
    this.value = this.get()
  }
  update() {
    this.run()
  }
  run() {
    this.getAndInvoke(this.callback)
  }
  getAndInvoke(cb) {
    const value = this.get()
    if (value !== this.value || typeof value === 'object') {
      const oldValue = this.value
      this.value = value
      cb.call(this.target, value, oldValue)
    }
  }
  get() {
    // 进入依赖收集阶段,让全局的 Dep.target 设置为 Watcher 本身,那么就是进入了依赖收集阶段
    Dep.target = this
    const obj = this.target
    var value
    // 只要能找,就一直找
    try {
      value = this.getter(obj)
    } catch (error) {
      console.log(error)
    } finally {
      Dep.target = null
    }
    return value
  }
}

最后附上源码地址

https://gitee.com/yokeney/vue-resource-reactive.git

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 殢无伤 原文链接:https://juejin.im/post/7026344440562188296

回到顶部