likes
comments
collection
share

实现reactive - 源码系列2

作者站长头像
站长
· 阅读数 17

github同步进行了项目,方便的话,可以直接clone下来,切到reactive分支即可,看最终的效果。

安装 vue3,作为目标对齐

pnpm i vue -w

在写个 index.html,感受 reactive

reactivity/dist 新建 index.html

<body>
  <script type="module">
    import {
      reactive,
      effect,
    } from '../../../node_modules/@vue/reactivity/dist/reactivity.esm-browser.prod.js';
    const state = reactive({
      count: 0,
    });
    // Proxy {count:0}
    console.log(state);

    setTimeout(() => {
      state.count++;
      // 1
      console.log(state.count);
    }, 1000);
  </script>
</body>

在项目根目录,运行anywhere,如果没有就npm i anywhere -g之后在运行,页面打开控制台就能看到相关数据了

然后,换成我们自己的 reactivity 路径

// import { reactive } from '../../../node_modules/@vue/reactivity/dist/reactivity.esm-browser.prod.js';
import { reactive } from './reactivity.js';

改 reactivity 的目录结构

将 reactivity/src 的 index.ts 作为导出接口,具体文件另起

于是有 2 个文件:

reactive.ts

export const isObject = (param) => {
  return typeof param === 'object' && param !== null;
};
export function reactive() {}

index.ts

export * from './reactive';

分析 reactive

  1. reactive 肯定是个函数
  2. 输入:输入是一个对象,不是对象,直接返回
  3. 输出:输出是一个 Proxy 实例

还有 3 个要点,也是难点:

  • 解决对象的get有依赖属性的问题,需要将 this 的指向修改(后面细说)
  • 对象被代理过之后,再代理同一个对象的话,需要直接返回上一次代理
  • 对象被代理后的代理实例,如果再次被代理,需要仍然直接返回上一次代理

寻常返回 proxy 对象

先说下,所谓代理,就是代为处理。proxy 的基础不再赘述,简言之,访问对象,本质访问其代理。

先解决这些:

  1. reactive 肯定是个函数
  2. 输入:输入是一个对象,不是对象,直接返回
  3. 输出:输出是一个 Proxy 实例
export const isObject = (param) => {
  return typeof param === 'object' && param !== null;
};
export function reactive(target) {
  // 如果不是对象,直接返回
  if (!isObject(target)) {
    return;
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      console.log('get', key);
      return target[key];
    },
    set(target, key, value, receiver) {
      console.log('set', key, value);
      target[key] = value;
      return true;
    },
  });
  return proxy;
}

发现生效了!这算是最最基础版了! 接下来,解决难点,进阶版来了

解决 get 有依赖属性的问题 - Reflect

举个例子说明,建 test-proxy.js:

let person = {
  name: 'hua',
  get aliasName() {
    console.log(this);
    return `alias-${this.name}`;
  },
};
let proxy = new Proxy(person, {
  get(target, key, receiver) {
    console.log('读取key', key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value) {
    target[key] = value;
    return true;
  },
});

proxy.aliasName;

先不运行,猜,读取key输出几次。 其实是一次,aliasName

流程如下: proxy.aliasName => proxy的get方法,打印,target这里指person => person.aliasName是 get 属性,其this.namethis指向的是person,也就 alias-${person.name} => 这里注意,并没有走proxy.name

这就是BUG

解决方案的关键就是在this的指向,所以这里启动Reflect,将this的指向proxy。同理,set也一样。

let proxy = new Proxy(person, {
  get(target, key, receiver) {
    console.log('读取key', key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    return true;
  },
});

新流程如下: proxy.aliasName => proxy的get方法,打印,target这里指person => person.aliasName是 get 属性,此时this.namethis指向的是proxy,会再走一次 proxy的get方法

这里可以将代码粘贴到浏览器执行,看下this在两种情况的下输出情况,第一次就是person,第二次是proxy。注意,编辑器里直接执行,并不能感知到proxy

reactive.ts进行第一个优化:

// ...
const proxy = new Proxy(target, {
  get(target, key, receiver) {
    console.log('读取key', key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    Reflect.set(target, key, value, receiver);
    return true;
  },
});

对象第二次代理的话,直接返回第一次代理 - WeakMap

举个例子:

index.html里输入以下

import { reactive } from './reactivity.js';
const obj = {
  count: 0,
};
const state = reactive(obj);
const state2 = reactive(obj);
console.log(state === state2);

显然是 false,因为会做两次代理。

那怎么让第二次的代理,直接返回第一次代理呢?

你们可能想到了,存下呗(缓存)!

设置一个映射,代理的时候,若对象在映射表,直接返回,不在就继续,且加到映射里。

这里有个细节,普通对象,键不能是对象,这里非常适合用WeakMap

实现reactive - 源码系列2

reactive.ts进行第二个优化:

// 代理对象的映射
const reactiveMap = new WeakMap();

export function reactive(target) {
  // ...
  // 如果已经代理过了,直接返回
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target);
  }
  // const proxy ....
  // 如果没有代理过,缓存映射
  reactiveMap.set(target, proxy);
  return proxy;
}

index.html刷新下,true

代理,对象被代理后的代理实例,直接返回代理

举个例子:

还是index.html

import { reactive } from './reactivity.js';
const obj = {
  count: 0,
};
const state = reactive(obj);
// 就这里,reactive改成state了
const state2 = reactive(state);
console.log(state === state2);

就是对象被代理后的代理实例,再次被代理,现在肯定是false

那怎么返回第一次代理的值呢!

嗯,其实就是做个标记

  // ...
  // 如果已经代理过了,__v_isReactive肯定是true,那直接返回
  if (target.__v_isReactive) {
    return target
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      console.log('读取key', key);
      // 这里埋点,加上__v_isReactive属性,标识已经代理过了
      if (key === '__v_isReactive') {
        return true
      }
      // ...
      }
  })

一旦被代理过,就一定被埋点,那就标识了。

index.html刷新下,true

附上完整版reactive.ts

export const isObject = (param) => {
  return typeof param === 'object' && param !== null
}

// 代理对象的映射
const reactiveMap = new WeakMap()

export function reactive(target) {
  // 如果不是对象,直接返回
  if (!isObject(target)) {
    return
  }

  // 如果已经代理过了,直接返回
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target)
  }

  // 如果已经代理过了,__v_isReactive肯定是true,那直接返回
  if (target.__v_isReactive) {
    return target
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      console.log('读取key', key);
      // 这里埋点,加上__v_isReactive属性,标识已经代理过了
      if (key === '__v_isReactive') {
        return true
      }
      // Reflect将target的get方法里的this指向proxy上,也就是receiver
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);
      return true;
    },
  })
  // 如果没有代理过,缓存映射
  reactiveMap.set(target, proxy)
  return proxy
}