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.config.js
import babel from "rollup-plugin-babel";
import serve from "rollup-plugin-serve";
export default {
input: "./src/index.js",
output: {
format: "umd", // 模塊化類型
file: "dist/umd/reactivity.js",
name: "VueReactivity", // 打包后的全局變量的名字
sourcemap: true,
},
plugins: [
babel({
exclude: "node_modules/**",
}),
process.env.ENV === "development"
? serve({
open: true,
openPage: "/public/index.html",
port: 3000,
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="/dist/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:1, b:2};
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, 10) === 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 有很重要的作用。