模拟实现Vue3数据响应式全过程

话不多说,我们直接进入正题,模拟实现所有Vue3响应式相关API

为了不混乱,我先将响应式相关API进行分类,如图所示

由于文章篇幅较长,为了避免大家疲劳,先作出两点改善:

  1. 分篇;将文章按照上述分类和内容量分为多篇文章
  2. 插入图片;我将尽量多插入一些相关图片,一来缓解疲劳,二来帮助大家理解

此篇目标是深入了解9个响应式基础API中的reactive,并模拟实现我们自己的数据响应式

上篇3.png

思路

我的思路其实非常简单和非常清晰,首先去了解API的基本使用,然后试着去使用和理解它,然后按照它所实现的功能模拟实现我们自己的功能,如下

工作准备

在开始前,我们需要做一点准备工作

  1. 需要创建一个vue3项目,方便使用对应的响应式API
    如果你不知道怎么创建,官网提供了多种创建方式:传送门
  2. 单独创建一个文件,用于模拟实现对应API

为了方便,我将上篇文章(从0开始手动实现Vue3初始化流程)所用的文件拿来继续使用,当然你也可以使用这个文件,简单来说,这个文件实现了Vue3的初始化流程相关的几个API,比如createAppmount方法,我们可以在这个文件的基础上进行模拟实现数据响应式API





    
    
    
    mini-vue3



    

有了以上的准备,下面开始深入理解reactive

reactive函数

我们分两部分来说:reactive的使用和模拟实现

image.png

reactive的使用

我们先来看官方对于reactive的解释,官方的解释也非常简单

返回对象的响应式副本

但从这句话我们可以得到以下信息

  1. reactive接受一个对象作为参数
  2. 其返回值是经reactive函数包装过后的数据对象,这个对象具有响应式

但同样会有一些疑问
比如,reactive的参数只能传递一个对象吗,如果传递其他值会怎么样?
比如,返回的响应式数据的本质是什么,为啥就能让数据变成响应式?
比如,"副本"是不是意味着响应式数据与原始数据没有关联?
比如,返回的响应式副本里头的数据是深度响应式吗,即是否递归监听对象的所有属性?等等

带着这些疑问我们一起来试试看
首先,通过reactive创建一个响应数据

import { reactive } from "vue";
export default {
  setup() {  
    const state = reactive({
      count: 0,
    });
  },
};

如上代码就可以创建一个响应式数据state,我具体来看一下这个

console.log(state)

可以看见,返回的响应副本state其实就是Proxy对象。所以reactive实现响应式就是基于ES2015 Proxy的实现的。那我们知道Proxy有几个特点:

  1. 代理的对象是不等于原始数据对象
  2. 原始对象里头的数据和被Proxy包装的对象之间是有关联的。即当原始对象里头数据发生改变时,会影响代理对象;代理对象里头的数据发生变化对应的原始数据也会发生变化。
    需要记住:是对象里头的数据变化,并不能将原始变量的重新赋值,那是大换血了

因此,既然reactive实现响应式是基于Proxy的实现的,那我们大胆猜测,原始数据与相应数据也是有关联的。那我们来测试一下



以上代码测试结果如下

验证,确实当响应式对象里头数据变化的时候原始对象的数据也会变化
如果反过来,结果也是一样

 // ++state.count
++obj.count;

当响应式对象里头数据变化的时候原始对象的数据也会变化
那问题来了,我们操作数据的时候通过谁来操作呢?
官方的建议是

建议只使用响应式代理,避免依赖原始对象

再来解决另外一个问题看看reactive是否会深度监听每一层呢?

const state = reactive({
    a:{
        b:{
            c:{name:'c'}
        }
    }
});    
console.log(state);  
console.log(state.a);
console.log(state.a.b);  
console.log(state.a.b.c); 

可以看到结果reactive是递归会将每一层包装成Proxy对象的,深度监听每一层的property

最后测试一下如果reactive传递是非对象而是原始值会怎么样

const state = reactive(0);  
console.log(state)

结果是,原始值并不会被包装,所以也没有响应式特点

到这我们已经了解了reactive,下面进行简单总结:

  1. reactive的参数可以传递对象也可以传递原始值。但是原始值并不会包装成响应式数据
  2. 返回的响应式数据的本质Proxy对象
  3. 返回的响应式"副本"与原始数据有关联,当原始对象里头的数据或者响应式对象里头的数据发生,会彼此相互影响。两种都可以触发界面更新,操作时建议只使用响应式代理对象
  4. 返回的响应式对象里头时深度递归监听每一层的,每一层都会被包装成Proxy对象

有了这些知识,我们下面开始模拟实现reactive函数

模拟实现reactive

修改测试用例

首先测试用例得变

const { createApp } = Vue
const app = createApp({
    setup() {
        const state = reactive({
            count: 0
        })
        setInterval(() => {
            console.log(state.count)
            state.count++
        }, 2000);
        return state
    }
});
app.mount('#app');

如上代码,我希望实现一个reactive函数,它接受一个对象,返回一个包装后的响应式对象,当响应式数据发生变化时,页面能及时跟新。

创建reactive函数

我们知道Vue3是基于Proxy实现响应式。作用是所以当数据发生变化时,我们可以拦截到并作出一些操作,比如更新UI视图。因此我们定义reactive接受一个对象obj,通过new Proxy返回包装后的响应式数据

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            return target[key]
        },
        set(target, key, val) {
            target[key] = val
            // 这里当数据变化时,更新界面,于是我们考虑到这里需要update方法用户更新
            // updata待实现...
        }
    })
}

上述代码中,我们需要封装一个update方法,当数据变化时调用,即用于更新和初始化,于是我们回到mount函数中实现封装

封装update

封装update.jpg

所以可以看到,update函数做了三件事:

  1. 得到最新的元素el
  2. 清空宿主元素parent的内容
  3. 追加el

另外我们还需要在初始化时执行一次

this.update()

下面我们希望当render函数的内部用到了响应式数据,并当数据发生变化时,再次执行update函数

因此我们回到reactive中,当执行set函数时,说明数据有变化,这是我们需要做更新,但是我们怎么调用update呢?使用app.update()吗?

虽然使用app.update()可以实现,但是耦合了app,失去了复用性。所以我们得想其他办法来解耦合

解耦合

其实我们希望当一个数据发生变化,一定要知道更新的是哪个对应的函数。因此我们需要一个依赖收集的过程,也叫添加副作用,于是我们可以创建一个effect函数,该函数接受一个函数fn作为参数,如果fn使用到了一些响应式数据,当数据发生变化,这个副作用函数fn将再次执行,同时返回副作用函数,如下

const effectStack = [];
function effect(fn) {
    const eff = function () {
        try {
            effectStack.push(eff)
            fn()
        } finally {
            effectStack.pop();
        }
    }
    eff();// 执行一次,触发依赖收集
    return eff
}

effectStack用于临时存储fn,将来在做依赖收集的时候把它拿出来,拿出来跟它相关的数据相映射

接着我们需要写一个依赖收集的函数track,track的作用是接受target、key,让traget[key]和副作用函数eff建立一个映射关系。兵器我们需要建立一个数据结构,来存储这个映射关系,于是实现如下:

function track(target, key) {
    // 获取副作用函数
    const effect = effectStack[effectStack.length - 1]
    // 建立target和key和eff关系
    if (effect) {
        console.log(targetMap)
        let map = targetMap[target]
        if (!map) {
            map = targetMap[target] = {}
        }
        let deps = map[key]
        if (!deps) {
            deps = map[key] = []
        }
        // 将副作用函数放入deps
        if (deps.indexOf(effect) === -1) {
            deps.push(effect)
        }
    }
}

然后,我们再reactive的get函数中,做依赖收集

track(target,key)

已经上面步骤,我们已将traget、key、和副作用函数建立一个映射关系,于是我们可以在用户改变值的时候去触发依赖。因此下面我们封装一个trigger方法来触发依赖

function trigger(target, key) {
    const map = targetMap[target]
    if (map) {
        const deps = map[key]
        if (deps) {
            deps.forEach(dep => dep());
        }
    }
}

接着,我们reactive的set中调用trigger,触发依赖

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            // 可以做依赖收集
            track(target, key)
            return target[key]
        },
        set(target, key, val) {
            target[key] = val
            // 触发依赖
            trigger(target, key)
        }
    })
}

最后要将update函数作为副作用函数,修改如下:

this.update = effect(() => {
    const el = ops.render.call(this.proxy)
    parent.innerHTML = ''
    insert(el, parent)
})

最终,我们模拟实现了数据响应式,如下


动画11111111.gif

总结

你可能感兴趣的:(模拟实现Vue3数据响应式全过程)