Skip to content

前言

在vue的生态圈下有个重要但是又不是经常会使用的:响应式原理。而vue2和vue3中又使用了不同的api去实现这个数据响应式。听说前端是最卷的,所以为了使自己成为浪潮了一员,我又去偷偷学习了。

废话不多说,直接上

Vue2响应式原理

先看一张来自Vue2官网的一张图:

vue2响应式.png

这张图我是这么理解的:

DOM树被“Touch”,然后getter告诉Watcher去收集depend依赖,通过setter去notify通知Watcher,Watcher再去通过虚拟Dom和diff算法,最后render函数渲染上树。

说人话就是是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。其中有dep类进行了依赖收集和watcher类通过模板编译最后改变真实DOM里面的数据。有什么不对欢迎告知,纯属个人理解。

回忆了一波官方图,回到正轨上

Object.defineProperty()

作用:在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

语法:Object.defineProperty(obj, prop, descriptor)

参数:

  1. 要添加属性的对象
  2. 要定义或修改的属性的名称或 [Symbol]
  3. 要定义或修改的属性描述符

看一个简单的例子

js
let person = {}
let personName = 'lihua'

//在person对象上添加属性namep,值为personName
Object.defineProperty(person, 'namep', {
    //但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true
    //默认不可以修改,可:wirtable:true
    //默认不可以删除,可:configurable:true
    get: function () {
        console.log('触发了get方法')
        return personName
    },
    set: function (val) {
        console.log('触发了set方法')
        personName = val
    }
})

//当读取person对象的namp属性时,触发get方法
console.log(person.namep)

//当修改personName时,重新访问person.namep发现修改成功
personName = 'liming'
console.log(person.namep)

// 对person.namep进行修改,触发set方法
person.namep = 'huahua'
console.log(person.namep)
let person = {}
let personName = 'lihua'

//在person对象上添加属性namep,值为personName
Object.defineProperty(person, 'namep', {
    //但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true
    //默认不可以修改,可:wirtable:true
    //默认不可以删除,可:configurable:true
    get: function () {
        console.log('触发了get方法')
        return personName
    },
    set: function (val) {
        console.log('触发了set方法')
        personName = val
    }
})

//当读取person对象的namp属性时,触发get方法
console.log(person.namep)

//当修改personName时,重新访问person.namep发现修改成功
personName = 'liming'
console.log(person.namep)

// 对person.namep进行修改,触发set方法
person.namep = 'huahua'
console.log(person.namep)

监测对象上多个属性

js
var person = {
    name:"张三",
    age:18
}
function  defineProperty(obj,key,val){
    Object.defineProperty(obj,key,{
        get(){
            return val
        },
        set(newVal){
            val = newVal
        }
    })
}
function Observer(obj){
    Object.keys(obj).forEach( (key) =>{
        defineProperty(obj,key,obj[key])    
    })
}
Observer(person)
var person = {
    name:"张三",
    age:18
}
function  defineProperty(obj,key,val){
    Object.defineProperty(obj,key,{
        get(){
            return val
        },
        set(newVal){
            val = newVal
        }
    })
}
function Observer(obj){
    Object.keys(obj).forEach( (key) =>{
        defineProperty(obj,key,obj[key])    
    })
}
Observer(person)

监测多层嵌套对象

上面只是简单的对象,如果是一个层层嵌套的对象呢?答案是递归!

js
//在defineProperty()中对传入的属性进行递归
function defineProperty(obj,key,val){
    Object.defineProperty(obj,key,{
        get(){
            return val
        },
        set(newVal){
           if(newVal === val) return
           Observer(newVal)
            val = newVal
        }
    })
}

//在Observer中加一个递归停止的条件
function Observer(obj){
    if (typeof obj !== "object" || obj === null) {
        return
    }
    Object.keys(obj).forEach( (key) =>{
        defineProperty(obj,key,obj[key])
    })
}
//在defineProperty()中对传入的属性进行递归
function defineProperty(obj,key,val){
    Object.defineProperty(obj,key,{
        get(){
            return val
        },
        set(newVal){
           if(newVal === val) return
           Observer(newVal)
            val = newVal
        }
    })
}

//在Observer中加一个递归停止的条件
function Observer(obj){
    if (typeof obj !== "object" || obj === null) {
        return
    }
    Object.keys(obj).forEach( (key) =>{
        defineProperty(obj,key,obj[key])
    })
}

在vue2中这里还有些许不完美的地方,那就是无法监听到对象的增删。这里也可以使用vue2中的Vue.$set()Vue.$delete()或者是vm.$set()vm.$delete()

监听数组

js
let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj,'arr',{
    get(){
        console.log('触发了get方法')
        return arr
    },
    set(newVal){
        console.log("触发了set方法")
        arr = newVal
    }
})
obj.arr = []      //触发了set方法
obj.arr.push(1)   //触发了get方法
obj.arr[0] = 1   //触发了get方法
obj.arr.pop()     //触发了get方法
obj.arr = [1,2,3] //触发了set方法
let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj,'arr',{
    get(){
        console.log('触发了get方法')
        return arr
    },
    set(newVal){
        console.log("触发了set方法")
        arr = newVal
    }
})
obj.arr = []      //触发了set方法
obj.arr.push(1)   //触发了get方法
obj.arr[0] = 1   //触发了get方法
obj.arr.pop()     //触发了get方法
obj.arr = [1,2,3] //触发了set方法

我们可以看到数组的一些方法例如pop、push为数组添加或者删除元素时,不能触发属性arr的setter,无法对属性arr中元素进行实时监听,为此vue2重写了这些数组添加了响应式。

同时我们注意到的上面的push、pop直接通过索引为数组添加元素时会触发arr的getter方法,这是由于这些操作的返回值。比如使用push为数组添加元素时,push方法返回值总是添加之后数组的新长度,当访问数组新长度时自然会触发getter方法。

首先作为新手来说,除非你一开始看官网文档就很仔细,看了你还得能记住,记住了你还得知道怎么使用,最后能一看就只知道这些问题所在。不然跟我一开始都会遇到的问题:我数据明明更新了为什么我的视图没有更新呢,为什么我的watch监听不到数据变化呢?很奇怪,百思不得其解。我最后用了一个简单的方法去使用:vm.$forceUpdate强制刷新页面,或者是利用一个computed去计算需要监听的对象JSON.stringify转成字符串的变化,然后watch打印出来,即页面数据更新总是留到下次页面更新的时候更新的,watch更新了所以到下次页面更新了,页面数据也就更新了。

后面我在仔细看文档的时候才放心,其实vue2并不能监听对象和数组的变化,如果通过特定的方法,具体可以看一下vue2 官网的深入响应式解释

Vue3响应式原理

鉴于vue2中监听数据时的一些弊端,vue3监听数据的时候弃用了Object.defineProperty(),而使用了Proxy()

这里打算从源码出发,源码中找到根本所在,然后用之间的关系进行梳理,最后以一个实战的例子去大致实现这个过程,方便自己更好的吸收。

大致的源码的初始化流程这里就不具体去展示了,就直接讲在哪个模块吧。

首先我们vue3中通过creatApp创建实例然后通过mount挂载实例。 在runtime-dom/index.ts中有个creatApp里面有着返回的一个app的实例,这个app 实例使用过ensureRenderer方法中的createApp返回的

image.png

接着我们通过ensureRenderer->createRenderer->baseCreateRenderer,这里截止到baseCreateRenderer方法,中间的过程由于篇幅的问题就不做具体展示。baseCreateRenderer()是这个函数重载方法,存在于runtime-core/renderer.ts中有将近2000行的代码,它里面有个初始化组件的方法叫做mountComponent,它的里面有个setupComponent调用,主要是用于初始化组件的data、props、watch、methods、computed、响应式等等,但是具体在哪里呢?

还是要接着往下看。为了防止看的很迷糊,时不时还需要加个图片来展示一下。

image.png

上图可以看出这里有initProps和initSlots单看名字就知道什么意思了。

通过setupComponent->setupStatefulComponent->finishComponentSetup,这里有需要截止一下,因为到了重要地方,finishComponentSetup方法中有个applyOptions的调用,这个是干什么的呢?这个就是上面说的初始化内容的地方了,很接近了

image.png

这些内容小伙伴们应该很熟悉了吧。但是这些东西从哪里来的并不是今天的主题,接着往下看

image.png

在reactive中特也是个函数重载,他里面调用了一个createReactiveObject的函数,返回的是一个由new Proxy的实例对象,但是里面的参数从哪里来呢?

这就需要回到我们的reactive中寻找

image.png

通过mutableHandlers->get->createGetter->track->createDep->trackEffects,就是这么个顺序拿到我的mutableHandlers

这里重要的就是track方法中的内容,其中trackEffects就是用来收集activeEffect依赖的。

image.png

其中targetMap是个new WeakMap(),往里面set了一个new Map(),而createDep下面中的返回的是一个new Set(),

数据结构是这样的{WeakMap:{Map:{Set}}},这样有什么好处呢?

这个就是有关于这三个ES6的新特性内容相关了。WeakMap有着友好的垃圾收回机制,垃圾回收后key自动消失,Map有着很好的键值对的表现形式,Set有着自动去重的功能。一石三鸟,不得不惊讶的赞叹,给大佬们竖个大拇指啊。

可以获取了但是我们会更数据吧,接下来就是mutableHandlers中的set

通过mutableHandlers->set->createSetter->trigger->triggerEffects->triggerEffect,triggerEffect去执行更新机制就可顺利的更新数据了

更新机制在哪里呢?就在一开始的mountComponent方法的中setupComponent下面一点就能看见一个初始化更新机制的setupRenderEffect方法,里面有着初始化更新机制,以便后边更新数据时更新页面

image.png

这里就很像React的useEffect一样,清除副作用的效果。

说了这么多,来总结一下内容

  • 数据结构会是一个{WeakMap:{Map:{Set}}},有着很好的垃圾回收机制,返回的是一个Proxy实例
  • 在初始化的时候需要创建一个effect更新机制
  • 需要一个track去收集更新机制
  • 需要一个trigger执行更新机制

这里的WeakMap、Map、Set就不做详细的介绍,直接进入重要内容。

Proxy()

作用:用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

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

参数:

target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。这个的连中有很多的方法可以使用,这里也不做作一一解释了。

Reflect

Proxy还需要Reflect的配合使用,Reflect的方法跟Proxy handler 的方法是一样的。它是一个内置的对象,它提供拦截 JavaScript 操作的方法

简单例子

js
const handler = {
  get(target, prop) {
    const result = Reflect.get(target, prop)
    console.log('拦截了读取数据', prop, result)
    return result
  },
  // 修改属性值或者是添加属性
  set(target, prop, value) {
    const result = Reflect.set(target, prop, value)
    console.log('拦截了修改数据或者是添加属性', prop, value)
    return result
  },
  // 删除某个属性
  deleteProperty(target, prop) {
    const result = Reflect.deleteProperty(target, prop)
    console.log('拦截了删除数据', prop)
    return result
  }
};
var obj = {}
const obj_1 = new Proxy(obj, handler);
setTimeout(()=>{
  obj_1.a = "你好"
},3000)
console.log(obj_1.a)
const handler = {
  get(target, prop) {
    const result = Reflect.get(target, prop)
    console.log('拦截了读取数据', prop, result)
    return result
  },
  // 修改属性值或者是添加属性
  set(target, prop, value) {
    const result = Reflect.set(target, prop, value)
    console.log('拦截了修改数据或者是添加属性', prop, value)
    return result
  },
  // 删除某个属性
  deleteProperty(target, prop) {
    const result = Reflect.deleteProperty(target, prop)
    console.log('拦截了删除数据', prop)
    return result
  }
};
var obj = {}
const obj_1 = new Proxy(obj, handler);
setTimeout(()=>{
  obj_1.a = "你好"
},3000)
console.log(obj_1.a)

image.png

当然这里如果是嵌套对象或者是数组的时候就需要递归实现了。

上图中setTimeout目前就相当于更新机制了,前面也说了我们的更新机制并不是通过setTimeout去实现的,而是有个effect, 这个只有一个更新机制还好,要是有很多个呢?每个都写一次effect函数吗?这时候我们就需要封装起来通过track收集effect和trigger执行effect

完整代码

js
const createGetter = () =>{
  return function get(target, prop){
    const result = Reflect.get(target, prop)
    console.log('拦截了读取数据', prop, result,target)
    track(target, prop)
    return result
  }
}
const get = createGetter()
const createSetter = () =>{
  return function set(target, prop, value){
    const result = Reflect.set(target, prop, value)
    console.log('拦截了修改数据或者是添加属性', prop, value)
    trigger(target, prop)
    return result
  }
}
const set = createSetter()
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
    // 如果此时activeEffect为null则不执行下面
    // 这里判断是为了避免例如console.log(person.name)而触发track
    if (!activeEffect) return
    console.log("我进来了",target,key)
    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())
    }
    dep.add(activeEffect) // 把此时的activeEffect添加进去
}
function trigger(target, key) {
    let depsMap = targetMap.get(target)
    if (depsMap) {
        const dep = depsMap.get(key)
        if (dep) {
            dep.forEach(effect => effect())
        }
    }
}
const handler = {
  get,
  // 修改属性值或者是添加属性
  set,
};
function reactive (target) {
  // 判断当前的目标对象是不是object类型(对象/数组)
  if (target && typeof target === 'object') {
    // 对数组或者是对象中所有的数据进行reactive的递归处理
    // 先判断当前的数据是不是数组
    if (Array.isArray(target)) {
      // 数组的数据要进行遍历操作
      target.forEach((item, index) => {
        target[index] = reactive(item)
      })
    } else {
      // 再判断当前的数据是不是对象
      // 对象的数据也要进行遍历的操作
      Object.keys(target).forEach(key => {
        target[key] = reactive(target[key])
      })
    }
    return new Proxy(target, handler)
  }
  // 如果传入的数据是基本类型的数据,那么就直接返回+///-++++
  
  return target
}
function effect(fn) {
    activeEffect = fn
    activeEffect()
    activeEffect = null
}

function ref(target) {
  target = reactive(target)
  console.log("目标对象",target)
  return {
    _is_ref: true, // 标识当前的对象是ref对象
    // 保存target数据保存起来
    _value: target,
    get value () {
      console.log('劫持到了读取数据')
      return this._value
    },
    set value (val) {
      console.log('劫持到了修改数据,准备更新界面', val)
      this._value = val
    }
  }
}
function computed(fn) {
    const result = ref()
    console.log(result)
    effect(() => result.value = fn())
    return result
}

var obj = ref({name:"张三",age:20})
let name1 = computed(()=>'你好,'+obj.value.name)

console.log(name1.value,targetMap)
console.log("分割线-------------")
obj.value.name = "李四"

console.log(name1.value,targetMap)
const createGetter = () =>{
  return function get(target, prop){
    const result = Reflect.get(target, prop)
    console.log('拦截了读取数据', prop, result,target)
    track(target, prop)
    return result
  }
}
const get = createGetter()
const createSetter = () =>{
  return function set(target, prop, value){
    const result = Reflect.set(target, prop, value)
    console.log('拦截了修改数据或者是添加属性', prop, value)
    trigger(target, prop)
    return result
  }
}
const set = createSetter()
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
    // 如果此时activeEffect为null则不执行下面
    // 这里判断是为了避免例如console.log(person.name)而触发track
    if (!activeEffect) return
    console.log("我进来了",target,key)
    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())
    }
    dep.add(activeEffect) // 把此时的activeEffect添加进去
}
function trigger(target, key) {
    let depsMap = targetMap.get(target)
    if (depsMap) {
        const dep = depsMap.get(key)
        if (dep) {
            dep.forEach(effect => effect())
        }
    }
}
const handler = {
  get,
  // 修改属性值或者是添加属性
  set,
};
function reactive (target) {
  // 判断当前的目标对象是不是object类型(对象/数组)
  if (target && typeof target === 'object') {
    // 对数组或者是对象中所有的数据进行reactive的递归处理
    // 先判断当前的数据是不是数组
    if (Array.isArray(target)) {
      // 数组的数据要进行遍历操作
      target.forEach((item, index) => {
        target[index] = reactive(item)
      })
    } else {
      // 再判断当前的数据是不是对象
      // 对象的数据也要进行遍历的操作
      Object.keys(target).forEach(key => {
        target[key] = reactive(target[key])
      })
    }
    return new Proxy(target, handler)
  }
  // 如果传入的数据是基本类型的数据,那么就直接返回+///-++++
  
  return target
}
function effect(fn) {
    activeEffect = fn
    activeEffect()
    activeEffect = null
}

function ref(target) {
  target = reactive(target)
  console.log("目标对象",target)
  return {
    _is_ref: true, // 标识当前的对象是ref对象
    // 保存target数据保存起来
    _value: target,
    get value () {
      console.log('劫持到了读取数据')
      return this._value
    },
    set value (val) {
      console.log('劫持到了修改数据,准备更新界面', val)
      this._value = val
    }
  }
}
function computed(fn) {
    const result = ref()
    console.log(result)
    effect(() => result.value = fn())
    return result
}

var obj = ref({name:"张三",age:20})
let name1 = computed(()=>'你好,'+obj.value.name)

console.log(name1.value,targetMap)
console.log("分割线-------------")
obj.value.name = "李四"

console.log(name1.value,targetMap)

image.png

上面代码中的大部分都是简写的,位置在reactivity这个文件下

结语

本次针对Vue2、3原理的进行了理解阐述,如果有不正确的地方,欢迎小伙伴指出。

如果想更好的学习的,我推荐大家可以看一个阿崔cxr mini-vue