javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > Vue2Vue3响应式原理

一篇搞懂Vue2、Vue3响应式源码的原理

作者:既白biu

这篇文章主要介绍了Vue2、Vue3响应式源码的原理,内容很详细,对大家的学习或者工作具有一定的参考学习价值,有需要的朋友可以借鉴参考下,希望能够有所帮助

前言

我们在编写Vue2,Vue3代码的时候,经常会在data中定义某些数据,然后在template用到的时候,可能会在多处用到这些数据,通过对这些数据的操作,可以达到改变视图的作用,即所谓数据驱动视图。

我们可以通过Mustache 语法,让data可以在页面上显示,随着data的变化,视图中也会随之改变。

那么,这种响应式操作在Vue2、Vue3中是怎么实现的呢?

Vue2响应式操作

响应式函数的封装

在进行响应式操作前,我们需要简单大致封装一个响应式函数,参数接收的是函数,凡是传入到响应式函数的函数,就是需要响应式的,其他默认定义的函数是不需要响应式的。

我们需要用一个数组将他们收集起来,(现在暂时使用函数,最好的办法是放入Set中,下文会讲),代码如下:

// 封装一个响应式的函数
let reactiveFns = []
function watchFn(fn) {
  reactiveFns.push(fn)
}

等到我们需要执行这些函数的时候(什么时候需要执行是后话,先简单提一下),可以遍历这个数组然后执行:

reactiveFns.forEach(fn => {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->
fn()
})

Depend类的封装

我们需要封装一个Depend类,这个类的作用是:这个类用于管理某一个对象的某一个属性的所有响应式函数。一个对象里面可能会有多个属性并且有他们对应的值,我们可能用到了这个对象里面多个属性,所以我们要给这里的每个用到的属性建立一个属于自己的类,用来管理对这个属性有依赖的所有函数。

所以我们得想办法拿到刚才在响应式函数里面传进去的函数,这里我们可以用activeReactiveFn暂时保存刚才传进去的函数。

所以我们对响应式函数的封装进行重构一下,如下:

// 保存当前需要收集的响应式函数
let activeReactiveFn = null

// 封装一个响应式的函数
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

因为某个属性可能会用多个函数进行依赖,所有在这个类的内部我们会定义一个Set, reactiveFns = new Set(),定义成Set而不是数组是因为Set数据结构没有重复的数据,从而防止了重复的操作。

这里定义了一个depend方法可以将activeReactiveFn在有值的情况下,放入reactiveFns中,notify函数就是将这些收集了的函数进行执行。

class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }
  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

监听对象的变化

在Vue2中使用的监听对象的变化使用的方法是:使用Object.defineProperty。

我们可以封装一个reactive函数,参数传入一个对象,函数内部对这个对象进行监听,遍历这个对象,获取所有的属性和属性值,对每个属性使用Object.defineProperty,在Object.defineProperty第三个参数中,get和set方法中,在set方法中,修改值为新的值之后,之前提到,每个属性都要有属于自己的Depend对象,那么如何获取这个对象呢?

那这里还有个问题,有不同的对象,对象里面又有多个属性,那么这该如何解决呢?

可以定义一个WeakMap将各个对象保存成Map形式,然后在每个单一对象里面,我们可以用Map形式保存属性的Depend类,如图所示:

那么如何根据对象名,属性名获取depend呢?可以在getDepend函数实现,参数传入对象名,属性名
,代码如下:

// 封装一个获取depend函数
const targetMap = new WeakMap()

function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}

获取到属性特定的depend后,回到原来的话题,那么在set方法中,修改值为新的值之后,获取到属性特定的depend后,要调用depend里面的notify方法,使对这个属性有依赖的所有函数执行,也就是对数据进行更新。

在get方法中,在返回属性值之前,要先获取到属性特定的depend后,调用depend里面的depend方法,将对此属性依赖的函数保存下来。

代码如下:

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

至此,Vue2的响应式操作就已经实现了

所有代码以及测试代码如下:

// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
    constructor() {
        this.reactiveFns = new Set()
    }
    depend() {
        if (activeReactiveFn) {
            this.reactiveFns.add(activeReactiveFn)
        }
    }

    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}

// 封装一个响应式的函数
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

// 封装一个获取depend函数
const targetMap = new WeakMap()

function getDepend(target, key) {
    // 根据target对象获取map的过程
    let map = targetMap.get(target)
    if (!map) {
        map = new Map()
        targetMap.set(target, map)
    }

    // 根据key获取depend对象
    let depend = map.get(key)
    if (!depend) {
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

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

// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
    name: "cy", // depend对象
    age: 18 // depend对象
})

const infoProxy = reactive({
    address: "安徽省",
    height: 1.88
})

watchFn(() => {
    console.log(infoProxy.address)
})

infoProxy.address = "北京市"

const foo = reactive({
    name: "foo"
})

watchFn(() => {
    console.log(foo.name)
})

foo.name = "aaa"
foo.name = "bbb"

// 安徽省
// 北京市
// foo
// aaa
// bbb

Vue3响应式操作

Proxy、Reflect

Proxy:
在Vue2中,使用Object.defineProperty来监听对象的变化,但是这样做有什么缺点呢?

首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。
我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。
所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象

在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:

也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);

之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;

如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):
set函数有四个参数:

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

get函数有三个参数:

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

实例代码如下;

const obj = {
  name: "cy",
  age: 18
}

const objProxy = new Proxy(obj, {
  // 获取值时的捕获器
  get: function(target, key) {
    console.log(`监听到对象的${key}属性被访问了`, target)
    return target[key]
  },

  // 设置值时的捕获器
  set: function(target, key, newValue) {
    console.log(`监听到对象的${key}属性被设置值`, target)
    target[key] = newValue
  }
})

console.log(objProxy.name)
console.log(objProxy.age)

objProxy.name = "kobe"
objProxy.age = 30

console.log(obj.name)
console.log(obj.age)
// 监听到对象的name属性被访问了 { name: 'cy', age: 18 }
// cy
// 监听到对象的age属性被访问了 { name: 'cy', age: 18 }
// 18
// 监听到对象的name属性被设置值 { name: 'cy', age: 18 }
// 监听到对象的age属性被设置值 { name: 'kobe', age: 18 }
// kobe
// 30

Reflect:

Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。
 

那么这个Reflect有什么用呢?

它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;

如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?

这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;

Reflect中常见的方法:

那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作;

我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?

如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this

Vue3响应式

Vue3响应式使用的是Proxy,我们需要在Vue2的reactive函数里面进行一些改变:

function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 根据target.key获取对应的depend
      const depend = getDepend(target, key)
      // 给depend对象中添加响应函数
      depend.depend()
  
      return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // depend.notify()
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}

其他方面的代码同Vue2基本没啥变化,Vue3的响应式操作就已经实现了

所有代码以及测试代码如下:

// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
    constructor() {
        this.reactiveFns = new Set()
    }
    depend() {
        if (activeReactiveFn) {
            this.reactiveFns.add(activeReactiveFn)
        }
    }

    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}

// 封装一个响应式的函数
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()

function getDepend(target, key) {
    // 根据target对象获取map的过程
    let map = targetMap.get(target)
    if (!map) {
        map = new Map()
        targetMap.set(target, map)
    }

    // 根据key获取depend对象
    let depend = map.get(key)
    if (!depend) {
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

function reactive(obj) {
    return new Proxy(obj, {
        get: function(target, key, receiver) {
            // 根据target.key获取对应的depend
            const depend = getDepend(target, key)
                // 给depend对象中添加响应函数
            depend.depend()

            return Reflect.get(target, key, receiver)
        },
        set: function(target, key, newValue, receiver) {
            Reflect.set(target, key, newValue, receiver)
                // depend.notify()
            const depend = getDepend(target, key)
            depend.notify()
        }
    })
}

// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
    name: "cy", // depend对象
    age: 18 // depend对象
})

const infoProxy = reactive({
    address: "安徽省",
    height: 1.88
})

watchFn(() => {
    console.log(infoProxy.address)
})

infoProxy.address = "北京市"

const foo = reactive({
    name: "foo"
})

watchFn(() => {
    console.log(foo.name)
})

foo.name = "bar"
// 安徽省
// 北京市
// foo
// bar

以上就是一篇搞懂Vue2、Vue3响应式源码的原理的详细内容,更多关于Vue2、Vue3响应式源码的原理的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文