模拟实现所有Vue3响应式API

模拟实现所有Vue3响应式API

文章资讯开源世界2021-06-14 15:35:003370A+A-

前言

本系列文章的目标是模拟实现所有Vue3响应式相关API

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

模拟实现所有Vue3响应式API  新闻 第1张

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

  1. 分篇;将文章按照上述分类和内容量分为上、中、下或更多文章

  2. 插入图片;我将尽量多插入一些相关图片,一来缓解疲劳,二来帮助大家理解

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

模拟实现所有Vue3响应式API  新闻 第2张

思路

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

模拟实现所有Vue3响应式API  新闻 第3张

工作准备

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

  1. 需要创建一个vue3项目,方便使用对应的响应式API

如果你不知道怎么创建,官网提供了多种创建方式:传送门

  1. 单独创建一个文件,用于模拟实现对应API

为了方便,我将上篇文章(从0开始手动实现Vue3初始化流程)所用的文件拿来继续使用,你也可以使用这个文件,简此文最下方附件 1

这个文件实现了Vue3的初始化流程相关的几个API,比如createAppmount方法,我们在这个文件的基础上进行模拟实现数据响应式API

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

reactive函数

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

模拟实现所有Vue3响应式API  新闻 第4张

reactive的使用

从reactive的定义引出疑问解答

定义

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

模拟实现所有Vue3响应式API  新闻 第5张

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

  1. reactive接受一个对象作为参数

  2. 其返回值是经reactive函数包装过后的数据对象,这个对象具有响应式

产生疑问

通过定义我们可能产生一些疑问

  1. 返回的响应式数据的本质是什么,为啥就能让数据变成响应式?

  2. "副本"是不是意味着响应式数据与原始数据没有关联?

  3. 返回的响应式副本里头的数据是深度响应式吗,即是否递归监听对象的所有属性?

  4. reactive的参数只能传递一个对象吗,如果传递其他值会怎么样?等

带着这些疑问我们一个一个来试验和解答

响应式数据的本质

首先,通过reactive创建一个响应数据,看看响应式数据具体是什么鬼

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

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

console.log(state) 复制代码

模拟实现所有Vue3响应式API  新闻 第6张

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

  1. 代理的对象是不等于原始数据对象

  2. 原始对象里头的数据和被Proxy包装的对象之间是有关联的。即当原始对象里头数据发生改变时,会影响代理对象;代理对象里头的数据发生变化对应的原始数据也会发生变化。

需要记住:是对象里头的数据变化,并不能将原始变量的重新赋值,那是大换血了

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

原始数据与响应式数据是否有关联

那我们来测试一下

<template>   <button @click="change">     {{ state.count }}   </button> </template> <script> import { reactive } from "vue"; export default {   setup() {     const obj = {       count: 0,     };     const state = reactive(obj);     function change(){         ++state.count         console.log(obj);         console.log(state);     }     return { state,change};   }, }; </script> 复制代码

以上代码测试结果如下

模拟实现所有Vue3响应式API  新闻 第7张

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

 // ++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);  复制代码

模拟实现所有Vue3响应式API  新闻 第8张

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

是否可以传递原始值

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

const state = reactive(0);   console.log(state) 复制代码

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

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

  1. reactive的参数可以传递对象也可以传递原始值。但是原始值并不会包装成响应式数据

  2. 返回的响应式数据的本质Proxy对象

  3. 返回的响应式"副本"与原始数据有关联,当原始对象里头的数据或者响应式对象里头的数据发生,会彼此相互影响。两种都可以触发界面更新,操作时建议只使用响应式代理对象

  4. 返回的响应式对象里头时深度递归监听每一层的,每一层都会被包装成Proxy对象

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

模拟实现reactive核心功能

模拟实现所有Vue3响应式API  新闻 第9张

修改测试用例

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

模拟实现所有Vue3响应式API  新闻 第10张

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

  1. 得到最新的元素el

  2. 清空宿主元素parent的内容

  3. 追加el

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

this.update() 复制代码

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

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

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

解耦合

模拟实现所有Vue3响应式API  新闻 第11张

首先我们希望当一个数据发生变化,一定要知道更新的是哪个对应的函数。因此我们需要一个依赖收集的过程,也叫添加副作用,于是我们可以创建一个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,将来在做依赖收集的时候把它拿出来,拿出来跟它相关的数据相映射

effectStack临时存储fn收集依赖时拿出来

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

function track(target, key) {     // 获取副作用函数     const effect = effectStack[effectStack.length - 1]     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)         }     } } 复制代码

记住,track的目的就是建立target和key和副作用eff之间关系

targetkeyeff

接着,我们再reactiveget函数中,做依赖收集

track(target,key) 复制代码

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

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

接着,我们reactiveset中调用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) }) 复制代码

最终,我们成功实现了reactive,完成了数据响应式

测试代码运行成功,如下 模拟实现所有Vue3响应式API  新闻 第12张

最终代码见文章底部 附件2

总结

模拟实现所有Vue3响应式API  新闻 第13张

reactive的作用其实就是将接收到的对象,通过Proxy打包成响应式对象,当响应式对象的数据发生变化时,页面视图可以对应进行更新。

整个的实现过程从创建reactive开始,里头通过Proxy拦截到对象的相关操作,当代理对象数据发生变化时,我们可以同时在set内部通知更新,于是这里封装了update方法,但是为了解决耦合问题,我们分别实现了添加副作用函数effect、依赖收集的函数track以及触发依赖的trigger方法等

END

附件 1

<!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <meta http-equiv="X-UA-Compatible" content="IE=edge">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>mini-vue3</title> </head> <body>     <div id="app"></div>     <script>         const Vue = {             createApp(ops) {                 const renderer = Vue.createRenderer({                     querySelector(selector) {                         return document.querySelector(selector)                     },                     insert(child, parent, anchor) {                         parent.insertBefore(child, anchor || null)                     }                 })                 return renderer.createApp(ops)             },             createRenderer({ querySelector, insert }) {                 return {                     createApp(ops) {                         return {                             mount(selector) {                                 const parent = querySelector(selector)                                 if (!ops.render) {                                     ops.render = this.compile(parent.innerHTML)                                 }                                 if (ops.setup) {                                     this.setupState = ops.setup()                                 } else {                                     this.data = ops.data();                                 }                                 this.proxy = new Proxy(this, {                                     get(target, key) {                                         if (key in target.setupState) {                                             return target.setupState[key]                                         } else {                                             return target.data[key]                                         }                                     },                                     set(target, key, val) {                                         if (key in target.setupState) {                                             target.setupState[k] = val                                         } else {                                             target.data[key] = val                                         }                                     }                                 })                                 const el = ops.render.call(this.proxy)                                 parent.innerHTML = ''                                 insert(el, parent)                             },                             compile(template) {                                 return function render() {                                     const h1 = document.createElement('h1')                                     h1.textContent = this.count                                     return h1;                                 }                             }                         }                     }                 }             }         }     </script>     <script>         // 测试用例         const { createApp } = Vue         const app = createApp({             setup() {                 let count = 1                 return { count }             }         });         app.mount('#app');     </script> </body> </html> 复制代码

附件 2

<!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <meta http-equiv="X-UA-Compatible" content="IE=edge">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>mini-vue3</title> </head> <body>     <div id="app"></div>     <script>         // `reactive`接受一个对象`obj`,返回包装后的响应式数据         function reactive(obj) {             // Vue3中基于Proxy实现响应式。作用是所以当数据发生变化时,我们可以拦截到并作出一些操作,比如更新UI视图,即数据响应式             return new Proxy(obj, {                 get(target, key) {                     // 可以做依赖收集                     track(target, key)                     return target[key]                 },                 set(target, key, val) {                     target[key] = val                     // 这里当数据变化时,更新界面,于是我们可以创建一个update方法,并在这里调用                     // updata()                     // app.update()                     //这有个问题,app耦合了,没有通用性                     // 为了解决这个问题                     // 我们希望有一条神秘的线,当一个数据发生变化,我一定要知道更新的是哪个对应的函数。                     // 因此,我们需要一个依赖收集的过程,或者叫添加副作用,即数据发生改变,产生一个副作用                     // 触发依赖                     trigger(target, key)                 }             })         }         //effectStack用于临时存储fn,将来在做依赖收集的时候把它拿出来,拿出来跟它相关的数据相映射         const effectStack = [];         // 添加副作用函数fn         function effect(fn) {             // effect的作用是将传入的fn作为副作用函数,如果fn使用到了一些响应式数据,当数据发生变化,这个副作用函数fn将再次执行             const eff = function () {                 try {                     effectStack.push(eff)                     fn()                 } finally {                     effectStack.pop();                 }             }//eff的作用是处理错误,入栈,执行函数,出栈             // 执行一次,触发依赖收集             eff();             return eff         }         // 依赖收集函数,希望在副作用函数执行时,去触发track         // track的作用是接受target、key,让traget[key]和副作用函数eff建立一个映射关系         // 所以,我建立一个数据结构,来存储这个映射关系         const targetMap = {}//大概结构是这样的{target: {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)                 }             }         }         function trigger(target, key) {             const map = targetMap[target]             if (map) {                 const deps = map[key]                 if (deps) {                     deps.forEach(dep => dep());                 }             }         }         const Vue = {             createApp(ops) {                 const renderer = Vue.createRenderer({                     querySelector(selector) {                         return document.querySelector(selector)                     },                     insert(child, parent, anchor) {                         parent.insertBefore(child, anchor || null)                     }                 })                 return renderer.createApp(ops)             },             createRenderer({ querySelector, insert }) {                 return {                     createApp(ops) {                         return {                             mount(selector) {                                 const parent = querySelector(selector)                                 if (!ops.render) {                                     ops.render = this.compile(parent.innerHTML)                                 }                                 if (ops.setup) {                                     // 经过上面修改,this.setupState已经是响应式对象                                     this.setupState = ops.setup()                                 } else {                                     this.data = ops.data();                                 }                                 this.proxy = new Proxy(this, {                                     get(target, key) {                                         if (key in target.setupState) {                                             return target.setupState[key]                                         } else {                                             return target.data[key]                                         }                                     },                                     set(target, key, val) {                                         if (key in target.setupState) {                                             target.setupState[k] = val                                         } else {                                             target.data[key] = val                                         }                                     }                                 })                                 // 封装一个update方法,当数据变化时调用,即用于更新和初始化                                 this.update = effect(() => {                                     // 得到最新的元素、清空、追加                                     const el = ops.render.call(this.proxy)                                     parent.innerHTML = ''                                     insert(el, parent)                                 })                                 // 在初始化是需要先执行一次                                 this.update()                             },                             compile(template) {                                 return function render() {                                     const h1 = document.createElement('h1')                                     h1.textContent = this.count                                     return h1;                                 }                             }                         }                     }                 }             }         }     </script>     <script>         // 测试用例         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');     </script> </body> </html>



Copyright 开源宇宙 版权所有 精品源码推荐:http://github.crmeb.net/u/defu
#转载请注明出处!

点击这里复制本文地址 以上内容由开源世界整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

开源世界-开源独尊-开源中国-ThinkPHP-CRMEB-商城源码-项目下载站-CMS企业模板-互站网-悟空源码-A5资源网-CSDN-ASP300 © All Rights Reserved.  Copyright 开源世界 版权所有
Powered by 百搜科技 Themes by www.baisou.ltd
联系我们| 关于我们| 留言建议| 网站管理

分享:

支付宝

微信

嘿,欢迎咨询
请先 登录 再评论,若不是会员请先 注册