您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > javascript

由浅入深,带你用JavaScript实现响应式原理

时间:2022-03-30 15:29:34  来源:博客园  作者:海椰人

前言

为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数据发生变化,依赖该数据的函数就会重新执行,达到更新的效果。那么我们如果想监听对象中的属性被设置和获取的过程,可以怎么做呢?

1.Object.defineProperty

在ES6之前,如果想监听对象属性的获取和设置,可以借助Object.defineProperty方法的存取属性描述符来实现,具体怎么用呢?我们来看一下。

const obj = {
  name: 'curry',
  age: 30
}

// 1.拿到obj所有的key
const keys = Object.keys(obj)

// 2.遍历obj所有的key,并设置存取属性描述符
keys.forEach(key => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    get: function() {
      console.log(`obj对象的${key}属性被访问啦!`)
      return value
    },
    set: function(newValue) {
      console.log(`obj对象的${key}属性被设置啦!`)
      value = newValue
    }
  })
})

// 设置:
obj.name = 'kobe' // obj对象的name属性被设置啦!
obj.age = 24 // obj对象的age属性被设置啦!
// 访问:
console.log(obj.name) // obj对象的name属性被访问啦!
console.log(obj.age) // obj对象的age属性被访问啦!

在Vue2.x中响应式原理实现的核心就是使用的Object.defineProperty,而在Vue3.x中响应式原理的核心被换成了Proxy,为什么要这样做呢?主要是Object.defineProperty用来监听对象属性变化,有以下缺点:

  • 首先,Object.defineProperty设计的初衷就不是为了去监听对象属性的,因为它的主要使用功能就是用来定义对象属性的;
  • 其次,Object.defineProperty在监听对象属性功能上有所缺陷,如果想监听对象新增属性、删除属性等等,它是无能为力的;

2.Proxy

在ES6中,新增了一个Proxy类,翻译为代理,它可用于帮助我们创建一个代理对象,之后我们可以在这个代理对象上进行许多的操作。

2.1.Proxy的基本使用

如果希望监听一个对象的相关操作,当Object.defineProperty不能满足我们的需求时,那么可以使用Proxy创建一个代理对象,在代理对象上,我们可以监听对原对象进行了哪些操作。下面将上面的例子用Proxy来实现,看看效果。

基本语法:const p = new Proxy(target, handler)

  • target:需要代理的目标对象;
  • handler:定义的各种操作代理对象的行为(也称为捕获器);
const obj = {
  name: 'curry',
  age: 30
}

// 创建obj的代理对象
const objProxy = new Proxy(obj, {
  // 获取对象属性值的捕获器
  get: function(target, key) {
    console.log(`obj对象的${key}属性被访问啦!`)
    return target[key]
  },
  // 设置对象属性值的捕获器
  set: function(target, key, newValue) {
    console.log(`obj对象的${key}属性被设置啦!`)
    target[key] = newValue
  }
})

// 之后的操作都是拿代理对象objProxy
// 设置:
objProxy.name = 'kobe' // obj对象的name属性被设置啦!
objProxy.age = 24 // obj对象的age属性被设置啦!
// 访问:
console.log(objProxy.name) // obj对象的name属性被访问啦!
console.log(objProxy.age) // obj对象的age属性被访问啦!
// 可以发现原对象obj同时发生了改变
console.log(obj) // { name: 'kobe', age: 24 }

2.2.Proxy的set和get捕获器

在上面的例子中,其实已经使用到了set和get捕获器,而set和get捕获器是最为常用的捕获器,下面具体来看看这两个捕获器吧。

(1)set捕获器

set函数可接收四个参数:

  • target:目标对象(被代理对象);
  • property:将被设置的属性key;
  • value:设置的新属性值;
  • receiver:调用的代理对象;

(2)get捕获器

get函数可接收三个参数:

  • target:目标对象;
  • property:被获取的属性key;
  • receiver:调用的代理对象;

2.3.Proxy的Apply和construct捕获器

上面所讲的都是对对象属性的操作进行监听,其实Proxy提供了更为强大的功能,可以帮助我们监听函数的调用方式。

  • apply:监听函数是否使用apply方式调用。
  • construct:监听函数是否使用new操作符调用。
function fn(x, y) {
  return x + y
}

const fnProxy = new Proxy(fn, {
  /*
    target: 目标函数(fn)
    thisArg: 指定的this对象,也就是被调用时的上下文对象({ name: 'curry' })
    argumentsList: 被调用时传递的参数列表([1, 2])
  */
  apply: function(target, thisArg, argumentsList) {
    console.log('fn函数使用apply进行了调用')
    return target.apply(thisArg, argumentsList)
  },
  /*
    target: 目标函数(fn)
    argumentsList: 被调用时传递的参数列表
    newTarget: 最初被调用的构造函数(fnProxy)
  */
  construct: function(target, argumentsList, newTarget) {
    console.log('fn函数使用new进行了调用')
    return new target(...argumentsList)
  }
})

fnProxy.apply({ name: 'curry' }, [1, 2]) // fn函数使用apply进行了调用
new fnProxy() // fn函数使用new进行了调用

2.4.Proxy所有的捕获器

除了上面提到的4种捕获器,Proxy还给我们提供了其它9种捕获器,一共是13个捕获器,下面对这13个捕获器进行简单总结,下面表格的捕获器分别对应对象上的一些操作方法。

捕获器handler

捕获对象

get()

属性读取操作

set()

属性设置操作

has()

in操作符

deleteProperty()

delete操作符

apply()

函数调用操作

construct()

new操作符

getPrototypeOf()

Object.getPrototypeOf()

setPrototypeOf()

Object.setPrototypeOf()

isExtensible()

Object.isExtensible()

preventExtensions()

Object.perventExtensions()

getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()

defineProperty()

Object.defineProperty()

ownKeys()

Object.getOwnPropertySymbols()

Proxy捕获器具体用法可查阅MDN:https://developer.mozilla.org/zh-CN/docs/Web/JAVAScript/Reference/Global_Objects/Proxy

3.Reflect

在ES6中,还新增了一个API为Reflect,翻译为反射,为一个内置对象,一般用于搭配Proxy进行使用。

3.1.Reflect有什么作用呢?

可能会有人疑惑,为什么在这里提到Reflect,它具体有什么作用呢?怎么搭配Proxy进行使用呢?

  • Reflect上提供了很多操作JavaScript对象的方法,类似于Object上操作对象的方法;
  • 比如:Reflect.getPrototypeOf()类似于Object.getPrototypeOf(),Reflect.defineProperty()类似于Object.defineProperty();
  • 既然Object已经提供了这些方法,为什么还提出Reflect这个API呢?这里涉及到早期ECMA规范问题,Object本是作为一个构造函数用于创建对象,然而却将这么多方法放到Object上,本就是不合适的;所以,ES6为了让Object职责单一化,新增了Reflect,将Object上这些操作对象的方法添加到Reflect上,且Reflect不能作为构造函数进行new调用

3.2.Reflect的基本使用

在上述Proxy中,操作对象的方法都可以换成对应的Reflect上的方法,基本使用如下:

const obj = {
  name: 'curry',
  age: 30
}

// 创建obj的代理对象
const objProxy = new Proxy(obj, {
  // 获取对象属性值的捕获器
  get: function(target, key) {
    console.log(`obj对象的${key}属性被访问啦!`)
    return Reflect.get(target, key)
  },
  // 设置对象属性值的捕获器
  set: function(target, key, newValue) {
    console.log(`obj对象的${key}属性被设置啦!`)
    Reflect.set(target, key, newValue)
  },
  // 删除对象属性的捕获器
  deleteProperty: function(target, key) {
    console.log(`obj对象的${key}属性被删除啦!`)
    Reflect.deleteProperty(target, key)
  }
})

// 设置:
objProxy.name = 'kobe' // obj对象的name属性被设置啦!
objProxy.age = 24 // obj对象的age属性被设置啦!
// 访问:
console.log(objProxy.name) // obj对象的name属性被访问啦!
console.log(objProxy.age) // obj对象的age属性被访问啦!
// 删除:
delete objProxy.name // obj对象的name属性被删除啦!

3.3.Reflect上常见的方法

对比Object,我们来看一下Reflect上常见的操作对象的方法(静态方法):

Reflect方法

类似于

get(target, propertyKey [, receiver])

获取对象某个属性值,target[name]

set(target, propertyKey, value [, receiver])

将值分配给属性的函数,返回一个boolean

has(target, propertyKey)

判断一个对象是否存在某个属性,和in运算符功能相同

deleteProperty(target, propertyKey)

delete操作符,相当于执行delete target[name]

apply(target, thisArgument, argumentsList)

对一个函数进行调用操作,可以传入一个数组作为调用参数,Function.prototype.apply()

construct(target, argumentsList [, newTarget])

对构造函数进行new操作,new target(...args)

getPrototypeOf(target)

Object.getPrototype()

setPrototypeOf(target, prototype)

设置对象原型的函数,返回一个boolean

isExtensible(target)

Object.isExtensible()

preventExtensions(target)

Object.preventExtensions(),返回一个boolean

getOwnPropertyDescriptor(target, propertyKey)


Object.getOwnPropertyDescriptor(),如果对象中存在该属性,则返回对应属性描述符,否则返回undefined

defineProperty(target, propertyKey, attributes)

Object.defineProperty(),设置成功返回true

ownKeys(target)

返回一个包含所有自身属性(不包含继承属性)的数组,类似于Object.keys(),但是不会受enumerable影响

具体Reflect和Object对象之间的关系和使用方法,可以参考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

3.4.Reflect的construct方法

construct方法有什么作用呢?具体的应用场景是什么?这里提一个需求,就明白construct方法的作用了。

需求:创建Person和Student两个构造函数,最终的实例对象执行的是Person中的代码,带上实例对象的类型是Student。

construct可接收的参数:

  • target:被运行的目标构造函数(Person);
  • argumentsList:类数组对象,参数列表;
  • newTarget:作为新创建对象原型对象的constructor属性(Student);
function Person(name, age) {
  this.name = name
  this.age = age
}

function Student() {}

const stu = Reflect.construct(Person, ['curry', 30], Student)
console.log(stu)
console.log(stu.__proto__ === Student.prototype)

打印结果:实例对象的类型为Student,并且实例对象原型指向Student构造函数的原型。

由浅入深,带你用JavaScript实现响应式原理

 

Reflect的construct方法就可以用于类继承的实现,可在babel工具中查看ES6转ES5后的代码,就是使用的Reflect的construct方法:

由浅入深,带你用JavaScript实现响应式原理

 

4.receiver的作用

在介绍Proxy的set和get捕获器的时候,其中有个参数叫receiver,具体什么是调用的代理对象呢?它的作用是什么?

如果原对象(需要被代理的对象)它有自己的getter和setter服务器属性时,那么就可以通过receiver来改变里面的this。

// 假设obj的age为私有属性,需要通过getter和setter来访问和设置
const obj = {
  name: 'curry',
  _age: 30,
  get age() {
    return this._age
  },
  set age(newValue) {
    this._age = newValue
  }
}

const objProxy = new Proxy(obj, {
  get: function(target, key, reveiver) {
    console.log(`obj对象的${key}属性被访问啦!`)
    return Reflect.get(target, key)
  },
  set: function(target, key, newValue, reveiver) {
    console.log(`obj对象的${key}属性被设置啦!`)
    Reflect.set(target, key, newValue)
  }
})

// 设置:
objProxy.name = 'kobe'
objProxy.age = 24
// 访问:
console.log(objProxy.name)
console.log(objProxy.age)

在没有使用receiver的情况下的打印结果为:name和age属性都被访问一次和设置一次。

由浅入深,带你用JavaScript实现响应式原理

 

但是由于原对象obj中对age进行了拦截操作,我们看一下age具体的访问步骤

  • 首先,打印objProxy.age会被代理对象objProxy中的get捕获器所捕获;
  • 紧接着Reflect.get(target, key)对obj中的age进行了访问,又会被obj中的get访问器所拦截,返回this._age;
  • 很显然在执行this._age的时候_age在这里是被访问了的,而这里的this指向的原对象obj;
  • 一般地,通过this._age的时候,应该也是要被代理对象的get捕获器所捕获的,那么就需要将这里的this修改成objProxy,相当于objProxy._age,在代理对象objProxy中就可以被get捕获到了;
  • receiver的作用就在这里,把原对象中this改成其代理对象,同理age被设置也是一样的,访问和设置信息都需要被打印两次;
// 假设obj的age为私有属性,需要通过getter和setter来访问和设置
const obj = {
  name: 'curry',
  _age: 30,
  get age() {
    return this._age
  },
  set age(newValue) {
    this._age = newValue
  }
}

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log(`obj对象的${key}属性被访问啦!`)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    console.log(`obj对象的${key}属性被设置啦!`)
    Reflect.set(target, key, newValue, receiver)
  }
})

// 设置:
objProxy.name = 'kobe'
objProxy.age = 24
// 访问:
console.log(objProxy.name)
console.log(objProxy.age)

再来看一下打印结果:

由浅入深,带你用JavaScript实现响应式原理

 

也可以打印receiver,在浏览器中进行查看,其实就是这里的objProxy:

由浅入深,带你用JavaScript实现响应式原理

 

5.响应式原理的实现

5.1.什么是响应式呢?

当某个变量值发生变化时,会自动去执行某一些代码。如下代码,当变量num发生变化时,对num有所依赖的代码可以自动执行。

let num = 30

console.log(num) // 当num方式变化时,这段代码能自动执行
console.log(num * 30) // 当num方式变化时,这段代码能自动执行

num = 1
  • 像上面这一种自动响应数据变化的代码机制,就称之为响应式;
  • 在开发中,一般都是监听某一个对象中属性的变化,然后自动去执行某一些代码块,而这些代码块一般都存放在一个函数中,因为函数可以方便我们再次执行这些代码,只需再次调用函数即可;

5.2.收集响应式函数的实现

在响应式中,需要执行的代码可能不止一行,而且也不可能一行行去执行,所以可以将这些代码放到一个函数中,当数据发生变化,自动去执行某一个函数。但是在开发中有那么多函数,怎么判断哪些函数需要响应式?哪些又不需要呢?

  • 封装一个watchFn的函数,将需要响应式的函数传入;
  • watchFn的主要职责就是将这些需要响应式的函数收集起来,存放到一个数组reactiveFns中;
const obj = {
  name: 'curry',
  age: 30
}

// 定义一个存放响应式函数的数组
const reactiveFns = []
// 封装一个用于收集响应式函数的函数
function watchFn(fn) {
  reactiveFns.push(fn)
}

watchFn(function() {
  let newName = obj.name
  console.log(newName)
  console.log('1:' + obj.name)
})

watchFn(function() {
  console.log('2:' + obj.name)
})

obj.name = 'kobe'
// 当obj中的属性值发送变化时,遍历执行那些收集的响应式函数
reactiveFns.forEach(fn => {
  fn()
})
由浅入深,带你用JavaScript实现响应式原理

 

5.3.收集响应式函数的优化

上面实现的收集响应式函数,目前是存放到一个数组中来保存的,而且只是对name属性的的依赖进行了收集,如果age属性也需要收集,不可能都存放到一个数组里面,而且属性值改变后,还需要通过手动去遍历调用,显而易见是很麻烦的,下面做一些优化。

  • 封装一个类,专门用于收集这些响应式函数;
  • 类中添加一个notify的方法,用于遍历调用这些响应式函数;
  • 对于不同的属性,就分别去实例化这个类,那么每个属性就可以对应一个对象,并且对象中有一个存放它的响应式数组的属性reactiveFns;
class Depend {
  constructor() {
    // 用于存放响应式函数
    this.reactiveFns = []
  }

  // 用户添加响应式函数
  addDependFn(fn) {
    this.reactiveFns.push(fn)
  }

  // 用于执行响应式函数
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

const obj = {
  name: 'curry',
  age: 30
}

const dep = new Depend()
// 在watchFn中使用dep的addDependFn来收集
function watchFn(fn) {
  dep.addDependFn(fn)
}

watchFn(function() {
  let newName = obj.name
  console.log(newName)
  console.log('1:' + obj.name)
})

watchFn(function() {
  console.log('2:' + obj.name)
})

obj.name = 'kobe'
// name属性发生改变,直接调用notify
dep.notify()

5.4.自动监听对象的变化

在修改对象属性值后,还是需要手动去调用其notify函数来通知响应式函数执行,其实可以做到自动监听对象属性的变化,来自动调用notify函数,这个想必就很容易了,在前面做了那么多功课,就是为了这里,不管是用Object.defineProperty还是Proxy都可以实现对象的监听,这里我使用功能更加强大的Proxy,并结合Reflect来实现。

class Depend {
  constructor() {
    // 用于存放响应式函数
    this.reactiveFns = []
  }

  // 用户添加响应式函数
  addDependFn(fn) {
    this.reactiveFns.push(fn)
  }

  // 用于执行响应式函数
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

const obj = {
  name: 'curry',
  age: 30
}

const dep = new Depend()
// 在watchFn中使用dep的addDependFn来收集
function watchFn(fn) {
  dep.addDependFn(fn)
}

// 创建一个Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 当set捕获器捕获到属性变化时,自动去调用notify
    dep.notify()
  }
})

watchFn(function() {
  let newName = objProxy.name
  console.log(newName)
  console.log('1:' + objProxy.name)
})

watchFn(function() {
  console.log('2:' + objProxy.name)
})

objProxy.name = 'kobe'
objProxy.name = 'klay'
objProxy.name = 'james'

注意:后面使用到的obj对象,需都换成代理对象objProxy,这样储能监听到属性值是否被设置了。

打印结果:name属性修改了三次,对应依赖函数就执行了三次。

由浅入深,带你用JavaScript实现响应式原理

 

5.5.对象依赖的管理(数据存储结构设计)

在上面实现响应式过程中,都是基于一个对象的一个属性,如果有多个对象,这多个对象中有不同或者相同的属性呢?我们应该这样去单独管理不同对象中每个属性所对应的依赖呢?应该要做到当某一个对象中的某一个属性发生变化时,只去执行对这个对象中这个属性有依赖的函数,下面就来讲一下怎样进行数据存储,能够达到我们的期望。

在ES16中,给我们新提供了两个新特性,分别是Map和WeakMap,这两个类都可以用于存放数据,类似于对象,存放的是键值对,但是Map和WeakMap的key可以存放对象,而且WeakMap对对象的引用是弱引用。如果对这两个类不太熟悉,可以去看看上一篇文章:ES6-ES12简单知识点总结

  • 将不同的对象存放到WeakMap中作为key,其value存放对应的Map;
  • Map中存放对应对象的属性作为key,其value存放对应的依赖对象;
  • 依赖对象中存放有该属性对应响应式函数数组;

如果有以下obj1和obj2两个对象,来看一下它们大致的存储形式:

const obj1 = { name: 'curry', age: 30 }
const obj2 = { name: 'kobe', age: 24 }
由浅入深,带你用JavaScript实现响应式原理

 

5.6.对象依赖管理的实现

已经确定了怎么存储了,下面就来实现一下吧。

  • 封装一个getDepend函数,主要用于根据对象和key,来找到对应的dep;
  • 如果没有找到就先进行创建存储;
// 1.创建一个WeakMap存储结构,存放对象
const objWeakMap = new WeakMap()
// 2.封装一个获取dep的函数
function getDepend(obj, key) {
  // 2.1.根据对象,获取对应的map
  let map = objWeakMap.get(obj)
  // 如果是第一次获取这个map,那么需要先创建一个map
  if (!map) {
    map = new Map()
    // 将map存到objWeakMap中对应key上
    objWeakMap.set(obj, map)
  }

  // 2.2.根据对象的属性,获取对应的dep
  let dep = map.get(key)
  // 如果是第一次获取这个dep,那么需要先创建一个dep
  if (!dep) {
    dep = new Depend()
    // 将dep存到map中对应的key上
    map.set(key, dep)
  }

  // 2.3最终将dep返回出去
  return dep
}

在Proxy的捕获器中获取对应的dep:

// 创建一个Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 根据当前对象target和设置的key,去获取对应的dep
    const dep = getDepend(target, key)
    console.log(dep)
    // 当set捕获器捕获到属性变化时,自动去调用notify
    dep.notify()
  }
})
由浅入深,带你用JavaScript实现响应式原理

 

5.7.对象的依赖收集优化

可以发现上面打印的结果中的响应式函数数组全部为空,是因为在前面收集响应式函数是通过watchFn来收集的,而在getDepend中并没有去收集对应的响应式函数,所以返回的dep对象里面的数组全部就为空了。如果对响应式函数,还需要通过自己一个个去收集,是不太容易的,所以可以监听响应式函数中依赖了哪一个对象属性,让Proxy的get捕获器去收集就行了。

  • 既然get需要监听到响应式函数访问了哪些属性,那么响应式函数在被添加之前肯定是要执行一次的;
  • 如何在Proxy中拿到当前需要被收集的响应式函数呢?可以借助全局变量;
  • 下面就来对watchFn进行改造;
// 定义一个全局变量,存放当前需要收集的响应式函数
let currentReactiveFn = null
function watchFn(fn) {
  currentReactiveFn = fn
  // 先调用一次函数,提醒Proxy的get捕获器需要收集响应式函数了
  fn()
  // 收集完成将currentReactiveFn重置
  currentReactiveFn = null
}

Proxy中get捕获器具体需要执行的操作:

// 创建一个Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    const dep = getDepend(target, key)
    // 拿到全局的currentReactiveFn进行添加
    dep.addDependFn(currentReactiveFn)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 根据当前对象target和设置的key,去获取对应的dep
    const dep = getDepend(target, key)
    console.log(dep)
    // 当set捕获器捕获到属性变化时,自动去调用notify
    dep.notify()
  }
})

下面测试一下看看效果:

watchFn(function() {
  console.log('1:我依赖了name属性')
  console.log(objProxy.name)
})
watchFn(function() {
  console.log('2:我依赖了name属性')
  console.log(objProxy.name)
})

watchFn(function() {
  console.log('1:我依赖了age属性')
  console.log(objProxy.age)
})
watchFn(function() {
  console.log('2:我依赖了age属性')
  console.log(objProxy.age)
})

console.log('----------以上为初始化执行,以下为修改后执行-------------')

objProxy.name = 'kobe'
objProxy.age = 24
由浅入深,带你用JavaScript实现响应式原理

 

5.8.Depend类优化

截止到上面,大部分响应式原理已经实现了,但是还存在一些小问题需要优化。

  • 优化一:既然currentReactiveFn可以在全局拿到,何不在Depend类中就对它进行收集呢。改造方法addDependFn
  • 优化二:如果一个响应式函数中多次访问了某个属性,就都会去到Proxy的get捕获器,该响应式函数会被重复收集,在调用时就会调用多次。当属性发生变化后,依赖这个属性的响应式函数被调用一次就可以了。改造reactiveFns,将数组改成Set,Set可以避免元素重复,注意添加元素使用add
// 将currentReactiveFn放到Depend之前,方便其拿到
let currentReactiveFn = null

class Depend {
  constructor() {
    // 用于存放响应式函数
    this.reactiveFns = new Set()
  }

  // 用户添加响应式函数
  addDependFn() {
    // 先判断一下currentReactiveFn是否有值
    if (currentReactiveFn) {
      this.reactiveFns.add(currentReactiveFn)
    }
  }

  // 用于执行响应式函数
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

Proxy中就不用去收集响应式函数了,直接调用addDependFn即可:

// 创建一个Proxy
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    const dep = getDepend(target, key)
    // 直接调用addDepend方法,让它去收集
    dep.addDependFn()
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 根据当前对象target和设置的key,去获取对应的dep
    const dep = getDepend(target, key)
    // 当set捕获器捕获到属性变化时,自动去调用notify
    dep.notify()
  }
})

5.9.多个对象实现响应式

前面都只讲了一个对象实现响应式的实现,如果有多个对象需要实现可响应式呢?将Proxy封装一下,外面套一层函数即可,调用该函数,返回该对象的代理对象。

function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      const dep = getDepend(target, key)
      // 直接调用addDepend方法,让它去收集
      dep.addDependFn()
      return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // 根据当前对象target和设置的key,去获取对应的dep
      const dep = getDepend(target, key)
      // 当set捕获器捕获到属性变化时,自动去调用notify
      dep.notify()
    }
  })
}

看一下具体使用效果:

const obj1 = { name: 'curry', age: 30 }
const obj2 = { weight: '130', height: '180' }

const obj1Proxy = reactive(obj1)
const obj2Proxy = reactive(obj2)

watchFn(function() {
  console.log('我依赖了obj1的name属性')
  console.log(obj1Proxy.name)
})
watchFn(function() {
  console.log('我依赖了age属性')
  console.log(obj1Proxy.age)
})

watchFn(function() {
  console.log('我依赖了obj2的weight属性')
  console.log(obj2Proxy.weight)
})
watchFn(function() {
  console.log('我依赖了obj2的height属性')
  console.log(obj2Proxy.height)
})

console.log('----------以上为初始化执行,以下为修改后执行-------------')

obj1Proxy.name = 'kobe'
obj1Proxy.age = 24
obj2Proxy.weight = 100
obj2Proxy.height = 165
由浅入深,带你用JavaScript实现响应式原理

 

5.10.总结整理

通过上面9步完成了最终响应式原理的实现,下面对其进行整理一下:

  • watchFn函数:传入该函数的函数都是需要被收集为响应式函数的,对响应式函数进行初始化调用,使Proxy的get捕获器能捕获到属性访问;
  • function watchFn(fn) { currentReactiveFn = fn // 先调用一次函数,提醒Proxy的get捕获器需要收集响应式函数了 fn() // 收集完成将currentReactiveFn重置 currentReactiveFn = null }
  • Depend类:reactiveFns用于存放响应式函数,addDependFn方法实现对响应式函数的收集,notify方法实现当属性值变化时,去调用对应的响应式函数;
  • // 将currentReactiveFn放到Depend之前,方便其拿到 let currentReactiveFn = null class Depend { constructor() { // 用于存放响应式函数 this.reactiveFns = new Set() } // 用户添加响应式函数 addDependFn() { // 先判断一下currentReactiveFn是否有值 if (currentReactiveFn) { this.reactiveFns.add(currentReactiveFn) } } // 用于执行响应式函数 notify() { this.reactiveFns.forEach(fn => { fn() }) } }
  • reactive函数:实现将普通对象转成代理对象,从而将其转变为可响应式对象;
  • function reactive(obj) { return new Proxy(obj, { get: function(target, key, receiver) { const dep = getDepend(target, key) // 直接调用addDepend方法,让它去收集 dep.addDependFn() return Reflect.get(target, key, receiver) }, set: function(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver) // 根据当前对象target和设置的key,去获取对应的dep const dep = getDepend(target, key) // 当set捕获器捕获到属性变化时,自动去调用notify dep.notify() } }) }
  • getDepend函数:根据指定的对象和对象属性(key)去查找对应的dep对象;
  • // 1.创建一个WeakMap存储结构,存放对象 const objWeakMap = new WeakMap() // 2.封装一个获取dep的函数 function getDepend(obj, key) { // 2.1.根据对象,获取对应的map let map = objWeakMap.get(obj) // 如果是第一次获取这个map,那么需要先创建一个map if (!map) { map = new Map() // 将map存到objWeakMap中对应key上 objWeakMap.set(obj, map) } // 2.2.根据对象的属性,获取对应的dep let dep = map.get(key) // 如果是第一次获取这个dep,那么需要先创建一个dep if (!dep) { dep = new Depend() // 将dep存到map中对应的key上 map.set(key, dep) } // 2.3最终将dep返回出去 return dep }

总结:以上通过Proxy来监听对象操作的实现响应式的方法就是Vue3响应式原理了。

6.Vue2响应式原理的实现

Vue3响应式原理已经实现了,那么Vue2只需要将Proxy换成Object.defineProperty就可以了。

  • 将reactive函数改一下即可;
function reactive(obj) {
  // 1.拿到obj所有的key
  const keys = Object.keys(obj)

  // 2.遍历所有的keys,添加存取属性描述符
  keys.forEach(key => {
    let value = obj[key]

    Object.defineProperty(obj, key, {
      get: function() {
        const dep = getDepend(obj, key)
        // 直接调用addDepend方法,让它去收集
        dep.addDependFn()
        return value
      },
      set: function(newValue) {
        value = newValue
        // 根据当前对象设置的key,去获取对应的dep
        const dep = getDepend(obj, key)
        // 监听到属性变化时,自动去调用notify
        dep.notify()
      }
    })
  })

  // 3.将obj返回
  return obj
}


本文来自
https://www.cnblogs.com/MomentYY/p/16065162.html



Tags:JavaScript   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
JavaScript的异步编程常见模式
在JavaScript中,异步编程是一种处理长时间运行操作(如网络请求或I/O操作)的常见方式。它允许程序在等待这些操作完成时继续执行其他任务,从而提高应用程序的响应性和性能。JavaS...【详细内容】
2024-04-12  Search: JavaScript  点击:(9)  评论:(0)  加入收藏
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  Search: JavaScript  点击:(7)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践——如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  Search: JavaScript  点击:(29)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  Search: JavaScript  点击:(23)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  Search: JavaScript  点击:(8)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  Search: JavaScript  点击:(7)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06  Search: JavaScript  点击:(58)  评论:(0)  加入收藏
18个JavaScript技巧:编写简洁高效的代码
本文翻译自 18 JavaScript Tips : You Should Know for Clean and Efficient Code,作者:Shefali, 略有删改。在这篇文章中,我将分享18个JavaScript技巧,以及一些你应该知道的示例...【详细内容】
2024-01-30  Search: JavaScript  点击:(79)  评论:(0)  加入收藏
使用 JavaScript 清理我的 200GB iCloud,有了一个意外发现!
本文作者在综合成本因素之下,决定用 Java 脚本来清理一下自己的 iCloud,结果却有了一个意外发现,即在 iCloud 中上传同一个视频和删除此视频之后,iCloud 的空间并不一致,这到底是...【详细内容】
2024-01-11  Search: JavaScript  点击:(111)  评论:(0)  加入收藏
JavaScript前端框架2024年展望
Angular、Next.js、React和Solid的维护者和创作者们展望2024年,分享了他们计划中的改进。译自2024 Predictions by JavaScript Frontend Framework Maintainers,作者 Loraine...【详细内容】
2024-01-05  Search: JavaScript  点击:(97)  评论:(0)  加入收藏
▌简易百科推荐
JavaScript的异步编程常见模式
在JavaScript中,异步编程是一种处理长时间运行操作(如网络请求或I/O操作)的常见方式。它允许程序在等待这些操作完成时继续执行其他任务,从而提高应用程序的响应性和性能。JavaS...【详细内容】
2024-04-12  靳国梁    Tags:JavaScript   点击:(9)  评论:(0)  加入收藏
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  前端新世界  微信公众号  Tags:JavaScript   点击:(7)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践——如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  前端新世界  微信公众号  Tags:JavaScript   点击:(29)  评论:(0)  加入收藏
又出新JS运行时了!JS运行时大盘点
Node.js是基于Google V8引擎的JavaScript运行时,以非阻塞I/O和事件驱动架构为特色,实现全栈开发。它跨平台且拥有丰富的生态系统,但也面临安全性、TypeScript支持和性能等挑战...【详细内容】
2024-03-21  前端充电宝  微信公众号  Tags:JS   点击:(29)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  前端历险记  微信公众号  Tags:JavaScript   点击:(23)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  WangLiwen    Tags:JavaScript   点击:(8)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  京东云开发者    Tags:JavaScript   点击:(7)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06    51CTO  Tags:JavaScript   点击:(58)  评论:(0)  加入收藏
JS小知识,使用这6个小技巧,避免过多的使用 if 语句
最近在重构我的代码时,我注意到早期的代码使用了太多的 if 语句,达到了我以前从未见过的程度。这就是为什么我认为分享这些可以帮助我们避免使用过多 if 语句的简单技巧很重要...【详细内容】
2024-01-30  前端达人  今日头条  Tags:JS   点击:(62)  评论:(0)  加入收藏
18个JavaScript技巧:编写简洁高效的代码
本文翻译自 18 JavaScript Tips : You Should Know for Clean and Efficient Code,作者:Shefali, 略有删改。在这篇文章中,我将分享18个JavaScript技巧,以及一些你应该知道的示例...【详细内容】
2024-01-30  南城大前端  微信公众号  Tags:JavaScript   点击:(79)  评论:(0)  加入收藏
站内最新
站内热门
站内头条