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 源码的详细内容...