vue3 响应式 借助Proxy 版代码解析

528 阅读3分钟

Reactivity 响应式

1. object.defineProperty的缺点和vue2.0的hack

let vm = new Vue({
  data() { a: 1 }
})

如果数据的层次过多 需要递归的去解析对象中的属性,依次增加set和get方法

1.1新属性设置不上

正常来说,被监听的数据在初始化时就已经被全部监听了。后续新增的属性无法监听,不得不通过vm.$set来处理新增的属性。

语法:this.$set( target, key, value )

文档地址: cn.vuejs.org/v2/api/#Vue…

1.2 数组监听不上

vue2 的做法是把数组原型方法都劫持,从而达到监听数组的目的

let oldArrayMethods = Array.prototype
export const arrayMethods = Object.create(oldArrayMethods)
// 创建一个新对象, arrayMethods.__proto__ = oldArrayMethods

const methods = [
  'push',
  'shift',
  'unshift',
  'pop',
  'sort',
  'splice',
  'reverse'
]
methods.forEach(method=>{
  arrayMethods[method] = function (...args) { 
    const result = oldArrayMethods[method].apply(this, args) // 调用原生的数组方法
    // push unshift 添加的元素可能还是一个对象
    let inserted // 当前用户插入的元素
    let ob = this.__ob__
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice': // 3个 新增的属性 splice 有删除 新增的的功能 arr.splice(0,1,{name:1})
        inserted = args.slice(2)
      default:
        break
    }
    if(inserted) ob.observerArray(inserted) // 将新增属性继续观测
 
 
    return result
  }
})

2. Proxy——响应式

Proxy来说,Object.defineProperty 是ie8支持的方法。vue3最低支持ie 11。

defineProperty,总是会用一层对象循环来遍历对象的属性,一个个调整其中变化:

Object.keys(data).forEach(key => {
  Object.defineProperty(data, key, { get() {
    return data[key]
  }, 
  set(value) {
    // 监听点
    data[key] = value
  }})
})

而Proxy监听一个对象:

new Proxy(data, {  get(key) { },  set(key, value) { },});

可以看到Proxy的语法非常简洁,根本不需要关心具体的 key,它去拦截的是 「修改 data 上的任意 key」 和 「读取 data 上的任意 key」。所以,不管是已有的 key 还是新增的 key,都能监听到。

Proxy 更加强大的地方还在于 Proxy 除了 get 和 set,还可以拦截更多的操作符。Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。

vue3响应式简版

尤大简版mini-vue在线代码

响应式最核心的原理其实就是发布-订阅+代理模式,Vue3采用了Proxy来重构整个响应式代码,下面是尤大写的Proxy简版代码

let activeEffect

class Dep {
  subscribers = new Set()
  _value
  constructor (value) {
    this._value = value
  }

  get value () {
    this.depend()
    return this._value
  }

  set value (value) {
    this._value = value
  }
  // 依赖收集
  depend () {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify () {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}
// 模仿 Vue3 的 watchEffect 函数
function watchEffect (effect) {
  activeEffect = effect
  // 调用传递的方法,如果函数里面有响应式对象,会触发getter依赖收集
  effect()
  // 防止不在watchEffect这个函数中触发getter的时候也执行依赖收集操作
  activeEffect = null
}

// proxy version
// proxy对象的响应拦截方法
const reactiveHandlers = {
  get(target, key) {
    const value = getDep(target, key).value
    if (value && typeof value === 'object') {
      // value对象 也变成一个响应式对象
      return reactive(value)
    } else {
      // 基本数据类型直接返回
      return value
    }
  },
  set(target, key, value) {
    // 调用 getDep 函数并将存放的value重新赋值成set的value
    getDep(target, key).value = value
  }
}

const targetToHashMap = new WeakMap()

// Vue3是用到哪的数据,再把数据变成响应式的。而Vue2遍历、递归成响应式数据
function getDep (target, key) {
  let depMap = targetToHashMap.get(target)
  if (!depMap) {
    depMap = new Map()
    targetToHashMap.set(target, depMap)
  }

  let dep = depMap.get(key)
  if (!dep) {
    dep = new Dep(target[key])
    depMap.set(key, dep)
  }

  return dep
}
// 模仿 Vue3 reactive
function reactive(obj) {
  return new Proxy(obj, reactiveHandlers)
}
// reactive 定义对象{ count: 0 },这个对象会传给Proxy的target
const state = reactive({
  count: 0
})
// 赋值给actibveEffect,然后立即执行这个方法。发现state.count读取getter
// 1、state.count 读取调用get
// 2、调用getDep,target是{ count: 0 },key是count
// 3、创建对象weakmap: {count:0} : new Map({count: new Dep(0)})
// 4、getDep(target, key).value 获取value触发Dep的getter,调用depend
// 5、activeEffect有值,把activeEffect加入到`subscribes Set结构中
watchEffect(() => {
  console.log(state.count)
}) // 0

// 改变state.count的值
// 1、Proxy拦截住set,然后调用getDep函数
// 2、获取到dep new Dep(0),就会修改它的value属,触发setter
// 3、notify,通知到subscribers 缓存列表,然后触发订阅的函数() => console.log(state.count)
// 4、调用state.count 重复上面state.count读取getter 但此时无activeEffect,不会触发depend里的代码逻辑,直接返回value 1
state.count++ // 1

vue-next Vue3源码仓库,Vue3采用lerna做package的划分,
响应式@vue/reactivity

参考图:

image.png

参考链接: 丐版响应式实现 响应式实现原理