ES6实战1-实现 Vue3 reactive 源码
1. 前言
本节开始我们将进入 ES6 实战课程,首先会花费两节的时间来学习 Vue3 响应式原理,并实现 一个 基础版的 Vue3 响应式系统;然后通过 Promise 来封装 一个 真实业务场景中的 ajax 请求;最后我们会聊聊前端开发过程中的编程风格。
本实战主要通过对前面 ES6 的学习应用到实际开发中来,Vue3 的响应式系统涵盖了大部分 ES6 新增的核心 API,
如:Proxy、Reflect、Set/Map、WeakMap、Symbol 等 ES6 新特性的应用。更加深入地学习 ES6 新增 API 的应用场景。
由于篇幅 内容 有限,本实战不会完全实现 Vue3 响应式系统的所有 API,主要实现 reactive 、 effect 这四个核心 API,其他 API 可以参考 vue-next
源码。本节的目录结构和命名和 Vue3 源码基本一致,在阅读源码的时候我们能看到作者的思考,和 功能 细颗粒度的拆分,使得 代码 更易于扩展和复用。
2. 环境配置
2.1 rollup 配置
ES6 很多 API 不能在低版本浏览器自己运行,另外我们在开发源码的时候需要大量地使用模块化,以拆分源码的结构。在学习模块化一节时,我们使用了 Webpack 作用打包工具,由于 Vue3 使用的是 rollup,更加适合框架和库的大包,这里我们也和 Vue3 看齐,rollup 最大的特点是按需打包,也就是我们在源码中使用的才会引入,另外 rollup 打包的结果不会产生而外冗余的 代码 ,可以自己阅读。下面我们来看下 rollup 简单的配置:
// rollup.con fig .js import babel from "rollup-plugin-babel" ; import serve from "rollup-plugin-serve" ; export default { input : "./src/index.js" , output : { format : "umd" , // 模块化类型 file : " dis t/umd/reactivity.js" , name : "VueReactivity" , // 打包后的 全局变量 的名字 sourcemap : true , } , plugins : [ babel ( { exclude : "node_modules/**" , } ) , process . env . ENV === "development" ? serve ( { open : true , openPage : "/public/index.html" , port : , contentBase : "" , } ) : null , ] , } ;
上面的配置 内容 和 webpack 很相似,是最基础的编译 内容 ,有兴趣的小伙伴可以去了解一下。 本节源码 在 ES6-Wiki 仓库的 vue-next 目录下, 在这 个项目中可以直接启动,在启动前需要在项目根目录中安装依赖。本项目使用的是 yarn workspace 的工作环境,可以在根目录中共享 npm 包。
2.2 调试源码
在开发的过程中需要对我们编写的 代码 进行调试,这里我们在 public 目录中创建了 一个 html 文件 用于在浏览器中打开。并且引入了 reactivity 的源码可以参考对比我们实现的 API 的 功能 ,同学在使用时可以打开注释进行验证。
< ! DOCTYPE html > < html lang = "en" > < head > < Meta charset = "UTF-8" > < Meta name = "viewport" content = "width=device-width, initial-scale=1.0" > < title > Document < / title > < / head > < body > < div id = "app" > < / div > < ! -- 我们自己实现的 reactivity 模块 -- > < script src = "/ dis t/umd/reactivity.js" > < / script > < ! -- vue的 reactivity 模块,测试时可以使用 -- > < ! -- < script src = "./vue.reactivity.js" > < / script > -- > < script > const { reactive , effect } = VueReactivity ; const proxy = reactive ( { name : 'ES6 Wiki' , } ) document . getElementById ( 'app' ) . innerHTML = proxy . name ; < / script > < / body > < / html >
3. reactive 实现
在实现 Vue3 的响应式原理前,我们先来回顾一下 Vue2 的响应式存在什么缺陷,主要有以下三个缺陷:
默 认会劫持的数据进行递归; 不支持 数组,数组长度改变是无效的; 不能劫持不存在的 属性 。
Vue3 使用了 Proxy 去实现数据的代理,在实现 Vue3 的响应式原理的同时,我们需要思考 Proxy 会不会存在上面的缺陷,它的缺点又是什么呢?
3.1 数据劫持
首先我在 reactive 文件 中定义并导出 一个 reactive 函数 ,在 reactive 中返回 一个 创建响应式对象的 方法 。 createReactiveObject 函数 主要是为了创建响应式对象使用,在 reactive 的相关 API 中,很多都需要创建响应式对象,这样可以复用,而且更加直观。
// vue-next/reactivity-1/reactive.js import { isObject } from "shared/index" ; import { mutableHandlers } from './baseHandlers' ; export function reactive ( target : object ) { // 1.创建响应式对象 return createReactiveObject ( target , mutableHandlers ) } function createReactiveObject ( target , baseHandlers ) { // 3.对数据进行代理 const proxy = new Proxy ( target , baseHandlers ) ; return proxy ; }
下面的 代码 是 Proxy 处理对象的回调, 包括 get、set、deleteProperty 等回调 方法 ,具体 用法 可以参考 Proxy 小节 内容 。这样我们就实现了 拦截 数据的 功能 。
// vue-next/reactivity-1/baseHandlers.js function createGetter ( ) { return function get ( target , key , receiver ) { console . log ( ' 获取 值' ) ; return target [ key ] ; } } function createSetter ( ) { return function get ( target , key , value , receiver ) { console . log ( '设置值' ) ; target [ key ] = value ; } } function deleteProperty ( target , key ) { delete target [ key ] ; } const get = createGetter ( ) const set = createSetter ( ) export const mutableHandlers = { get , set , deleteProperty , // has, // ownKeys }
在 Vue3 源码中使用 Reflect 来操作对象的,Reflect 和 Proxy 方法 一一对应,并且 Reflect 操作后的对象有返回值,这样我们可以对返回值做异常处理等, 修改 上面的 代码 如下:
// vue-next/reactivity-1/baseHandlers.js function createGetter ( ) { return function get ( target , key , receiver ) { console . log ( ' 获取 值' ) ; const res = Reflect . get ( target , key , receiver ) ; return res ; } } function createSetter ( ) { return function get ( target , key , value , receiver ) { console . log ( '设置值' ) ; const result = Reflect . set ( target , key , value , receiver ) ; return result ; } }
下面是测试用例,可以放在 public/index.html 下执行。
const { reactive } = VueReactivity ; const proxy = reactive ( { name : 'ES6 Wiki' , } ) proxy . name = 'imooc ES6 wiki' ; // 设置值 console . log ( 'proxy.name' ) ; // 获取 值 // proxy.name
3.2 实现响应式逻辑
首先我们需要对传入的参数进行判断,如果不是对象则直接返回。
// shared/index.js const isObject = val => val !== null && typeof val === 'object' function createReactiveObject ( target , baseHandlers ) { if ( ! isObject ( target ) ) { return target } ... }
在使用时, 用户 可能多次代理对象或多次代理过的对象,如:
var obj = { a : , b : } ; var proxy = reactive ( obj ) ; var proxy = reactive ( obj ) ; // 或者 var proxy = reactive ( proxy ) ; var proxy = reactive ( proxy ) ;
像上面这样的情况我们需要处理,不能多次代理。所以我们这里要将代理的对象和代理后的结果做 一个 映射表,这样我们在代理时判断此对象是否被代理即可。这里的映射我们用到了 WeakMap 弱引用 。
export const reactiveMap = new WeakMap ( ) ; function createReactiveObject ( target , baseHandlers ) { if ( ! isObject ( target ) ) { return target } const proxyMap = reactiveMap ; const existingProxy = proxyMap . get ( target ) ; // 这里判断对象是否被代理,如果映射表上有,则说明对象已经被代理,则直接返回。 if ( existingProxy ) { return existingProxy ; } const proxy = new Proxy ( target , baseHandlers ) ; // 这里在代理过后把对象存入映射表中,用于判断。 proxyMap . set ( target , proxy ) ; return proxy ; }
上面我们已经基本实现了响应式,但是有个问题,我们只实现了一层响应式,如果是嵌套多层的对象这样就不行了。Vue2 是使用的是深层递归的方式来做的,而我们使用了 Proxy 就不需 要做 递归操作了。Proxy 在 获取 值的时候会调 get 方法 ,这时我们只需要在 获取 值时判断这个值是不是对象,如果是对象则继续代理。
import { isSymbol , isObject } from 'shared' ; import { reactive } from './reactive' ; function createGetter ( ) { return function get ( target , key , receiver ) { const res = Reflect . get ( target , key , receiver ) ; if ( isSymbol ( key ) ) { return res ; } console . log ( " 获取 值, 调用 get 方法 " ) ; // 拦截 get 方法 if ( isObject ( res ) ) { return reactive ( res ) ; } return res ; } ; }
在 获取 值的时候有很多边界值需要特殊处理,这里列出了如果 key 是 symbol 类型的话直接返回结果,当然还有其他场景,同学可以去看 Vue3 的源码。
在我们设置值的时候,如果是新增 属性 时 Vue2 是 不支持 的,使用 Proxy 是可以的,但是我们需要知道当前操作是新增还是 修改 ?所以需要判断有无这个 属性 ,如果是 修改 则肯定有值。一般判断有两种情况:
数组新增和 修改 逻辑,需要先进行数组判断,当我们 修改 的 key 小于数组的长度时说明是 修改 ,反之则是新增; 对象新增和 修改 逻辑,对象判断是否有 属性 就比较简单了,直接取值验证即可。
// 判断数组 export const isArray = Array . isArray ; export const isIntegerKey = key => '' + parseInt ( key , ) === key ; // 判断key是不是整型 // 使用 Number(key) < target.length 判断数组是不是新增,key 小于数组长度说明有key // 判断对象是否有某 属性 const hasOwnProperty = Object . prototype . hasOwnProperty export const hasOwn = ( val , key ) => hasOwnProperty . call ( val , key ) // 判断有无key const hadKey = isArray ( target ) && isIntegerKey ( key ) ? Number ( key ) < target . length : hasOwn ( target , key ) ;
最终我们可以得到下面的 createSetter 函数 。
function createSetter ( ) { return function get ( target , key , value , receiver ) { const oldValue = target [ key ] ; // 获取 旧值 const hadKey = isArray ( target ) && isIntegerKey ( key ) ? Number ( key ) < target . length : hasOwn ( target , key ) ; const result = Reflect . set ( target , key , value , receiver ) ; if ( ! hadKey ) { console . log ( '新增 属性 ' ) ; } else if ( hasChanged ( value , oldValue ) ) { console . log ( ' 修改 属性 ' ) ; } return result ; } ; }
4. 小结
以上 内容 就是 Vue3 中实现 reactive API 的核心源码, 文章 的完整 代码 放在了 reactivity-1 目录下。源码中的实现方式可能会有所改变,在对照学习时可以参考 Vue 3.0.0 版本。本节实现响应式的核心是 Proxy 对数据的劫持,通过对 set 和 get 方法 的实现来处理各种边界数据问题。在学习过程中需要注意多次代理、设置 属性 时判断是新增还是 修改 ,这对后面实现 effect 等 API 有很重要的作用。
查看更多关于ES6实战1-实现Vue3 reactive 源码的详细内容...