js手写(三):简单实现Vue响应式

92 阅读3分钟

1.思路分析

响应式即数据发生变化时,页面自动更新,从数据层面来说,当某个数据发生改变时,依赖于该数据的关联函数全部重新执行,以达到重新渲染视图的效果

2.关键实现的四个类/函数

2.1 依赖类

该类作用是收集依赖和通知更新

包含一个属性:该属性用于存放某个数据对应的所有依赖,这里用集合表示,保证每个依赖只能添加一次;

包含两个方法:

(1)depend方法:用于向保存依赖的集合添加依赖函数,即某个函数访问到一个数据时,这个函数就是此数据的依赖

(2)notify方法:遍历保存依赖的集合,依次调用该集合中的依赖函数,即当数据发生改变时,将所有依赖执行一遍,以更新视图

let activeReactiveFn = null

class Depend {
  constructor() {
    // 使用集合,保证依赖只添加一次
    this.reactiveFns = new Set()
  }
  depend(){
    if(activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}
2.2 watchFns函数

该函数用于收集依赖,接受一个函数为入参,执行该函数以确保数据在一开始就被添加到响应式系统

function watchFns (fn) {
  activeReactiveFn = fn
  // 第一次就执行一次,以收集依赖
  fn()
  activeReactiveFn = null
}
2.3 getDepend函数

该函数用于获取对应对象的depend实例,目的是为了保证每个对象的每个key都对应一个唯一的depend实例,以便在对象的key发生变化时能够正确地收集依赖和通知更新。同时,使用WeakMap来存储depend实例,可以避免对象的key被占用,也可以在对象被垃圾回收时自动清除对应的depend实例。

// 获取depend的函数
// 使用weakmap,key必须是一个对象
let targetMap = new WeakMap()

function getDepend (target, key) {
  // 1.根据target取出对应的map对象
  let map = targetMap.get(target)
  if(!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  // 2.根据各对象的key取出具体的depend
  let depend = map.get(key)
  if(!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
2.4 reactive函数

该函数的作用是把传入的对象变为一个响应式的对象

2.4.1 vue3的响应式
const p = new Proxy(target, handler);

Vue3使用Proxy对对象进行代理,p为代理后的对象,如果访问p的属性,即执行handler对象中的get方法,在此时获取该属性对应的依赖,并执行depend实例的添加依赖方法;如果改变代理对象的属性,即执行handler对象中的set方法,在此时获取该属性对应的依赖,并执行depend实例触发依赖

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const depend = getDepend(target, key)
      depend.depend()
      // Reflect可以方便的操作对象,这里用于根据key获取值
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}
2.4.2 vue2的响应式

Vue2使用Object.defineProperty对数据进行劫持,访问对象时会执行get方法,此时添加依赖;改变对象时会执行set方法,此时执行依赖

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get() {
        const depend = getDepend(obj, key)
        depend.depend()
        return value
      },
      set(newValue) {
        value = newValue
        const depend = getDepend(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}

3.整合代码及简单测试

let activeReactiveFn = null
class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }
  depend(){
    if(activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

function watchFns (fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}


let targetMap = new WeakMap()
function getDepend (target, key) {
  let map = targetMap.get(target)
  if(!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  let depend = map.get(key)
  if(!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}

// Vue3写法
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const depend = getDepend(target, key)
      depend.depend()
      return Reflect.get(target, key, receiver)
    },
    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}

// 测试代码
let objProxy = reactive({
  name: 'zjm',
  age: 18
})

let infoProxy = reactive({
  info: 'good'
})

watchFns(() => {
  console.log(objProxy.name)
  console.log(objProxy.age)
})

objProxy.name = '123'
infoProxy.info = 'bad'