超简单实现vue3-reactive

92 阅读1分钟

一问vue3的双向绑定原理用了什么,大家都会有异口同声的说proxy,但是具体的原理还没有了解过,接下来就带大家了解,并实现一个简易的reactive

vue2/vue3响应式原理区别

vue2的响应式原理是Object.defineProperty(),但是有缺点

  1. 一次性递归内存开销大,可能导致栈溢出
  2. 不能监听对象的新增属性和删除属性
  3. 无法正确的监听数组的方法

vue3的响应式原理是用proxy,可以劫持整个对象,并返回一个新对象。有多种劫持操作(13 种),但不兼容IE浏览器

接下来我们来看看proxy是否存在vue2的缺点

const obj = {
  name: '张三'
}

const proxyObj = new Proxy(obj,{
  get(target, key) {
    console.log(`获取obj变量里的${key}属性`, target[key])
  },
  set(target, key, val) {
    console.log(`设置obj变量里的${key}属性`)
  },
  deleteProperty(target, key) {
    console.log(`删除obj变量里的${key}属性`)
  }
})

proxyObj.name // 获取obj变量里的name属性 张三
proxyObj.age = 18 // 设置obj变量里的age属性
delete proxyObj.name // 删除obj变量里的name属性

proxy确实可以拦截到对象的新增和删除

reactive

插播一个小插曲,在学习proxy的时候,我们发现get的返回的是Reflect,很多人应该会有疑问,为什么不直接返回target[key],而是要用Reflect

Reflect主要是和Proxy配对使用,提供对象语义的默认行为。

上面的每个字和单词我都认识,为什么拼在一起我就不理解了?
我用下面的几个例子给大家解释一下

为什么要用Reflect

我们在文档里可以看到proxy的get的第三个参数是receiver很多人把它理解为代理对象,但这是不全面的

const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    console.log(receiver === proxy); // true
    return target[key];
  },
});

通过上面的例子的输出结果,receiver确实和代理对象proxy相等

我们再来看个例子,看看get方法中返回target[key]Reflect.get(target, key, receiver) 的区别

const obj = {
  _name: '张三',
  get name() {
    return this._name
  }
}

const proxyObj = new Proxy(obj,{
  get(target, key, receiver) {
    return target[key]
    // return Reflect.get(target, key)
    // return Reflect.get(target, key, receiver)
  }
})
const newObj = {
  _name: '李四'
}
// newObj的原型链为proxyObj
Object.setPrototypeOf(newObj, proxyObj);
console.log(newObj.name) // 张三

当我们执行上述代码,结果输出张三,我们再把get方法返回Reflect.get(target, key),发现还是输出张三,于是我使出杀手锏,返回Reflect.get(target, key, receiver),发现输出李四,符合我们的内心期望,但是为什么会这样呢?
其实是因为当我们访问对象newObj的name属性时,因为newObj本身没有该属性,所以会去其原型proxyObj对象中找name属性,这个时候receiver就指向了newObj,代表原始的读操作所在的那个对象。

  • Proxy 中 get 的 receiver 不仅仅代表的是 Proxy 代理对象本身,同时也会代表继承 Proxy 的那个对象。
  • 本质上来说它还是为了确保陷阱函数中调用者的正确的上下文访问

言归正传,我们还是继续来实现一下reactive

const isObject = val => val !== null && typeof val === 'object'

let proxyMap = new WeakMap() // 存储响应数据

const baseHandler = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // 收集依赖
    track(target,key)
    // 如果子元素存在引用类型递归处理
    return isObject(target[key]) ? reactive(res) : res
  },
  set (target,key, val, receiver) {
    const oldValue = Reflect.get(target, key, receiver)
    const res = Reflect.set(target, key, val, receiver)
    // 如果新旧值不一致,更新操作
    if (oldValue !== val) trigger(target, key, val)  
    return res
  }
}

function reactive(target) {
  const existingProxy = proxyMap.get(target)
  // 如果proxyMap存在target
  if (existingProxy) return existingProxy

  const proxy = new Proxy(target, baseHandler)
  // 更新proxyMap
  proxyMap.set(target, proxy)
  return proxy
}

上面的主要代码就是在get的时候进行依赖收集;在set的时候进行更新。可以看出核心重点是tracktrigger函数

effect

let targetMap = new WeakMap() // 存储响应数据对应effect
let activeEffect // 表示当前正在走的 effect

function effect (fn) {
  try {
    activeEffect = fn
    return fn()
  } finally{
    activeEffect = null
  }
}
const obj = reactive({
  name: '张三'
})
effect(() => console.log(`我是{obj.name}`))

以上面的调用例子,我们执行effect函数,先赋值activeEffect,然后执行fn,这个时候需要先获取响应对象obj的name属性,然后就会触发响应数据的get,接着就会调用track进行依赖收集。收集完毕之后就会对activeEffect进行置空。

响应式顺序:effect => track

track

function track (target,key) {
  const effect = activeEffect
  if (effect) {
    let depsMap = targetMap.get(target)
    if(!depsMap) {
      targetMap.set(target, depsMap = new Map())
    }
    let dep = depsMap.get(key)
    if(!dep) {
      depsMap.set(key, dep = new Set())
    }
    // 依赖收集
    if (!dep.has(effect)){
      dep.add(effect)
    }
  }
}

我们来看看track函数 可以看到有WeakMapMapSet,看的头晕眼花,我们用下面例子来看看targetMap长啥样

const obj = reactive({
  name: '张三',
  age: 18
})

effect(() => console.log(`我叫${obj.name}`))
effect(() => console.log(`今年${obj.age}岁`))

image.png

这样子看的话,应该比较清楚一点,代码总体就是收集依赖

为什么要用 WeakMap 来存储

原生的 WeakMap 持有的是每个键对象的 弱引用,这意味着在没有其他引用存在时垃圾回收能正确进行。
WeakMap 的键所指向的对象,不计入垃圾回收机制。
Vue 3 之所以使用 WeakMap 来作为缓冲区就是为了能将 不再使用的数据进行正确的垃圾回收

trigger

function trigger(target,key,val) {
  const depsMap = targetMap.get(target)
  // 用set去重
  const effects = new Set()
  if (!depsMap) return
  const deps = depsMap.get(key)
  for (const dep of deps) {
    if (dep) {
      effects.add(dep)
    }
  }
  effects.forEach(effect => {
    effect()
  })
}

更新操作的流程就是获取响应对象对应的属性关联的effect进行更新

demo

const isObject = val => val !== null && typeof val === 'object'
let proxyMap = new WeakMap() // 存储proxy响应后的值
let targetMap = new WeakMap()
let activeEffect 

const baseHandler = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // 收集依赖
    track(target,key)
    
    return isObject(target[key]) ? reactive(res) : res
  },
  set (target,key, val, receiver) {
    const oldValue = Reflect.get(target, key, receiver)
    const res = Reflect.set(target, key, val, receiver)
    if (oldValue !== val) trigger(target,key,val)
    
    return res
  }
}

function reactive(target) {
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const proxy = new Proxy(target, baseHandler)
  proxyMap.set(target, proxy)
  return proxy
}

function track (target,key) {
  const effect = activeEffect
  if (effect) {
    let depsMap = targetMap.get(target)
    if(!depsMap) {
      targetMap.set(target, depsMap = new Map())
    }
    let dep = depsMap.get(key)
    if(!dep) {
      depsMap.set(key, dep = new Set())
    }
    if (!dep.has(effect)){
      dep.add(effect)
    }
  }
}

function trigger(target,key,val) {
  const depsMap = targetMap.get(target)
  // 用set去重
  const effects = new Set()
  if (!depsMap) return
  const deps = depsMap.get(key)
  for (const dep of deps) {
    if (dep) {
      effects.add(dep)
    }
  }
  effects.forEach(effect => {
    effect()
  })
}

function effect (fn) {
  try {
    activeEffect = fn
    return fn()
  } finally{
    activeEffect = null
  }
  
}


const obj = reactive({
  name: '张三',
  age: 12,
  info: {
    school: 'xx小学',
    class: '六年二班'
  }
})

effect(() => console.log(`我叫${obj.name}今年${obj.age}岁,就读于${obj.info.school},在${obj.info.class}`))
obj.name = '李四'
obj.info.school = 'yy小学'

image.png

执行上述代码可以正常响应变化

本文很多边界情况没有考虑,只实现了一个基本功能的reactive,让大家了解一下其原理