您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

ES6实战1-实现Vue3 reactive 源码

本节开始我们将进入 ES6 实战课程,首先会花费两节的时间来学习 Vue3 响应式原理,并实现基础版的 Vue3 响应式系统;然后通过 Promise 来封装真实业务场景中的 ajax 请求;最后我们会聊聊前端开发过程中的编程风格。

本实战主要通过对前面 ES6 的学习应用到实际开发中来,Vue3 的响应式系统涵盖了大部分 ES6 新增的核心 API,

如:Proxy、Reflect、Set/Map、WeakMap、Symbol 等 ES6 新特性的应用。更加深入地学习 ES6 新增 API 的应用场景。

由于篇幅有限,本实战不会完全实现 Vue3 响应式系统的所有 API,主要实现 reactiveeffect 这四个核心 API,其他 API 可以参考

源码。本节的目录结构和命名和 Vue3 源码基本一致,在阅读源码的时候我们能看到作者的思考,和细颗粒度的拆分,使得更易于扩展和复用。

ES6 很多 API 不能在低版本浏览器自己运行,另外我们在开发源码的时候需要大量地使用模块化,以拆分源码的结构。在学习模块化一节时,我们使用了 Webpack 作用打包工具,由于 Vue3 使用的是 rollup,更加适合框架和库的大包,这里我们也和 Vue3 看齐,rollup 最大的特点是按需打包,也就是我们在源码中使用的才会引入,另外 rollup 打包的结果不会产生而外冗余的,可以自己阅读。下面我们来看下 rollup 简单的配置:

// rollup.con.js
import babel from @H__53@"rollup-plugin-babel";
import serve from @H__53@"rollup-plugin-serve";

export default {
  input: @H__53@"./src/index.js",
  output: {
    format: @H__53@"umd", // 模块化类型
    file: @H__53@"dist/umd/reactivity.js",
    name: @H__53@"VueReactivity", // 打包后的的名字
    sourcemap: true,
  },
  plugins: [
    babel({
      exclude: @H__53@"node_modules/**",
    }),
    process.env.ENV === @H__53@"development"
      ? serve({
          open: true,
          openPage: @H__53@"/public/index.html",
          port: ,
          contentBase: @H__53@"",
        })
      : null,
  ],
};

上面的配置和 webpack 很相似,是最基础的编译,有兴趣的小伙伴可以去了解一下。 在 ES6-Wiki 仓库的 vue-next 目录下,个项目中可以直接启动,在启动前需要在项目根目录中安装依赖。本项目使用的是 yarn workspace 的工作环境,可以在根目录中共享 npm 包。

在开发的过程中需要对我们编写的进行调试,这里我们在 public 目录中创建了 html 用于在浏览器中打开。并且引入了 reactivity 的源码可以参考对比我们实现的 API 的,同学在使用时可以打开注释进行验证。

<!DOCTYPE html>
<html lang=@H__53@"en">
<head>
  < charset=@H__53@"UTF-8">
  < name=@H__53@"viewport" content=@H__53@"width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id=@H__53@"app"></div>

	<!-- 我们自己实现的 reactivity 模块 -->
	<script src=@H__53@"/dist/umd/reactivity.js"></script>

  <!-- vue的 reactivity 模块,测试时可以使用 -->
	<!-- <script src=@H__53@"./vue.reactivity.js"></script> -->

  <script>
    const { reactive, effect } = VueReactivity;
    const proxy = reactive({
      name: @H__53@'ES6 Wiki',
    })
    document.getElementById(@H__53@'app').innerHTML = proxy.name;
  </script>
</body>
</html>

在实现 Vue3 的响应式原理前,我们先来回顾一下 Vue2 的响应式存在什么缺陷,主要有以下三个缺陷:

Vue3 使用了 Proxy 去实现数据的代理,在实现 Vue3 的响应式原理的同时,我们需要思考 Proxy 会不会存在上面的缺陷,它的缺点又是什么呢?

首先我在 reactive 中定义并导出 reactive ,在 reactive 中返回创建响应式对象的。createReactiveObject 主要是为了创建响应式对象使用,在 reactive 的相关 API 中,很多都需要创建响应式对象,这样可以复用,而且更加直观。

// vue-next/reactivity-1/reactive.js
import { isObject } from @H__53@"../shared/index";
import { mutableHandlers } from @H__53@'./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(@H__53@'值');
    return target[key];
  }
}

function createSetter() {
  return function get(target, key, value, receiver) {
    console.log(@H__53@'设置值');
    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(@H__53@'值');
    const res = Reflect.get(target, key, receiver);
    return res;
  }
}

function createSetter() {
  return function get(target, key, value, receiver) {
    console.log(@H__53@'设置值');
    const result = Reflect.set(target, key, value, receiver);
    return result;
  }
}

下面是测试用例,可以放在 public/index.html 下执行。

const { reactive } = VueReactivity;
const proxy = reactive({
  name: @H__53@'ES6 Wiki',
})
proxy.name = @H__53@'imooc ES6 wiki';	// 设置值
console.log(@H__53@'proxy.name');		// 值
// proxy.name

首先我们需要对传入的参数进行判断,如果不是对象则直接返回。

// shared/index.js
const isObject = val => val !== null && typeof val === @H__53@'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 @H__53@'../shared';
import { reactive } from @H__53@'./reactive';

function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    if (isSymbol(key)) {
      return res;
    }
    console.log(@H__53@"值,get"); // get
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  };
}

在值的时候有很多边界值需要特殊处理,这里列出了如果 key 是 symbol 类型的话直接返回结果,当然还有其他场景,同学可以去看 Vue3 的源码。

在我们设置值的时候,如果是新增时 Vue2 是的,使用 Proxy 是可以的,但是我们需要知道当前操作是新增还是?所以需要判断有无这个,如果是则肯定有值。一般判断有两种情况:

// 判断数组
export const isArray = Array.isArray;
export const isIntegerKey = key => @H__53@'' + 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(@H__53@'新增');
    } else if (hasChanged(value, oldValue)) {
      console.log(@H__53@'');
    }

    return result;
  };
}

以上就是 Vue3 中实现 reactive API 的核心源码,的完整放在了 reactivity-1 目录下。源码中的实现方式可能会有所改变,在对照学习时可以参考 Vue 3.0.0 版本。本节实现响应式的核心是 Proxy 对数据的劫持,通过对 set 和 get 的实现来处理各种边界数据问题。在学习过程中需要注意多次代理、设置时判断是新增还是,这对后面实现 effect 等 API 有很重要的作用。


联系我
置顶