min-vue

注意:计数器案例之前,都是通过 tsc 编译成js,在浏览器中引用 或 直接通过 ts-node 直接运行。 计数器案例则是在webpack上配置的ts开发环境。

真实DOM渲染

传统的前端开发中,直接编写HTML,然后最终被渲染到浏览器上的过程。

  • image20220421154701294.png

虚拟DOM的优势

虚拟DOM是对真实DOM的抽象,这样做的好处

  • 首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作。
    • 直接对真实DOM操作会有很多限制,比如diff、clone等等,但是使用JS这门编程语言来操作这些,就变得非常简单。
    • 通过使用 JS 来表达非常多的逻辑,而对于DOM本身来说是非常补方便的。
  • 方便实现跨平台,我们可以将VNode节点渲染成任意想要的节点
    • 如渲染在Canvas, WebGL, SSR, Navtive(IOS、Android) 上;
    • 并且vue允许开发属于自己的渲染器(renderer),在平台上渲染;

虚拟DOM到真实DOM的渲染过程

image20220421161939791.png

Vue 的三大核心系统

  • compiler模块:编译模板系统,将模板编译成render函数
  • Runtime模块:Renderer, 渲染系统,vnode -> DOM
  • Reactivity模块:响应式系统, 数据劫持和响应

image20220421162324817.png

MIN-VUE

接下来尝试简单实现VUE的Runtime和Reactivity和应用程序入口。Compiler 属于编译器的范畴,需要编写大量的正则表达式,省略。

MIN-VUE 包括三个模块

  • 渲染系统模块
  • 可响应式系统模块
  • 应用程序入口模块

渲染系统

渲染系统三个主要功能

  • VNode 创建:h 函数,返回 VNode
  • VNode 渲染:mount 函数,用于将VNode 转化DOM 并挂载到DOM树上
  • 新旧VNode patch 对比:patch函数,用于两个VNode对比,决定如何处理新的VNode

h 函数 生成 VNode

  1. 声明VNode类型

    • // VNode 节点的格式
      interface VNode {
        tag: string;
        props: any;
        children: string | VNode[];
        el?: Element; // 用于保存VNode 转换成真实 元素的对象
      }
      
      
  2. 编写h函数

    • // tag 元素标签, props 元素标签上的属性, children: 元素标签内包裹的内容
      function h(tag: string, props: any, children: string | VNode[]): VNode {
        return {
          tag,
          props,
          children,
        };
      }
      
完整代码
  • // VNode 节点的格式
    interface VNode {
      tag: string;
      props: any;
      children: string | VNode[];
    }
    
    // tag 元素标签, props 元素标签上的属性, children: 元素标签内包裹的内容
    function h(tag: string, props: any, children: string | VNode[]): VNode {
      return {
        tag,
        props,
        children,
      };
    }
    // test h function
    const vnode = h("div", null, [h("h2", { name: "acwink" }, "哈哈哈")]);
    console.log(vnode);
    
测试h函数
  • // test h function
    const vnode = h("div", null, [h("h2", { name: "acwink" }, "哈哈哈")]);
    console.log(vnode);
    
  • image20220421165842968.png

  • 基本上来说,已经是一棵 VNode 树了

mount 函数 将VNode 渲染并挂载到DOM树上

  1. 创建真实元素

    • // 1. 将VNode 创建成真实的元素, 并将其保存到 当前的vnode上
        const el = (vnode.el = document.createElement(vnode.tag));
      
  2. 处理 props

    • // 2. 处理标签上的属性 props
      for (const key in vnode.props) {
          const value = vnode.props[key];
      
          // 2.1 处理 监听事件
          if (key.startsWith("on")) {
              el.addEventListener(key.slice(2).toLowerCase(), value);
          } else {
              // 2.2 处理其他属性
              el.setAttribute(key, value);
          }
      }
      
  3. 处理children

    • // 3. 处理 children
      // 3.1 如果 children 是一个字符串
      if (typeof vnode.children === "string") {
          el.textContent = vnode.children;
      } else {
          // 3.2 children 是个 VNode 数组
          // 递归处理 子节点的vnode, 将其挂载到当前节点上
          vnode.children.forEach((item) => {
              mount(item, el);
          });
      }
      
  4. 将当前节点挂载到Dom树

    • // 4. 将vnode 挂载到 container
      container.appendChild(el);
      
完整代码
  • // vnode: 要渲染并挂载的vnode节点, container: 该节点要挂载的DOM节点
    function mount(vnode: VNode, container: Element) {
      // 1. 将VNode 创建成真实的元素, 并将其保存到 当前的vnode上
      const el = (vnode.el = document.createElement(vnode.tag));
    
      // 2. 处理标签上的属性 props
      for (const key in vnode.props) {
        const value = vnode.props[key];
    
        // 2.1 处理 监听事件
        if (key.startsWith("on")) {
          el.addEventListener(key.slice(2).toLowerCase(), value);
        } else {
          // 2.2 处理其他属性
          el.setAttribute(key, value);
        }
      }
    
      // 3. 处理 children
      // 3.1 如果 children 是一个字符串
      if (typeof vnode.children === "string") {
        el.textContent = vnode.children;
      } else {
        // 3.2 children 是个 VNode 数组
        // 递归处理 子节点的vnode, 将其挂载到当前节点上
        vnode.children.forEach((item) => {
          mount(item, el);
        });
      }
    
      // 4. 将vnode 挂载到 container
      container.appendChild(el);
    }
    
    
测试mount 函数
  • <div id="app"></div>
    <script src="./renderer.js"></script>
    <script>
        // 生成vnode
        const vnode = h("div", { name: "acwink" }, [
            h("h2", null, "当前计数: 1000"),
            h(
                "button",
                {
                    onClick: () => {
                        console.log("+1");
                    },
                },
                "+1"
            ),
        ]);
    
        // 挂载
        const app = document.querySelector("#app");
        mount(vnode, app);
    </script>
    
  • !image20220421175437959.png

path 函数,当 vnode 发生更新时,对 DOM 的处理

  1. oldVNode.tag != newVNode.tag

    • // 1. 判断 n1, n2 tag 是否相同, 不同直接将以n1 生成成的DOM 及子DOM 全部 减掉
      if (n1.tag !== n2.tag) {
          // 获取 n1.el 的父元素
          const n1ElParent = n1.el?.parentElement;
          // 删除当前子树
          n1ElParent?.removeChild(n1.el!);
          // 将 n2 渲染并挂载到 n1ElParent 上
          mount(n2, n1ElParent!);
      }
      
  2. tag 相同,处理props

    • const el = (n2.el = n1.el);
      if (el) {
          // 2. tag 相同,处理props
          const oldProps = n1.props;
          const newProps = n2.props;
      
          // 2.1 添加或修改 到 el 属性 上
          for (const key in newProps) {
              const newValue = newProps[key];
              const oldValue = oldProps[key];
      
              if (newValue !== oldValue) {
                  if (key.startsWith("on")) {
                      el.addEventListener(key.slice(2).toLowerCase(), newValue);
                  } else {
                      el.setAttribute(key, newValue);
                  }
              }
          }
      
          // 2.2 删除旧的事件处理程序,和 newProps 没有的属性
          for (const key in oldProps) {
              if (key.startsWith("on")) {
                  const value = oldProps[key];
                  el.removeEventListener(key.slice(2).toLowerCase(), value);
              } else {
                  if (!(key in newProps)) {
                      el.removeAttribute(key);
                  }
              }
            }
      }
      
  3. tag 相同,children 处理

    • // 3. 处理children
      const oldChildren = n1.children;
      const newChildren = n2.children;
      
      // 3.1 如果 oldChildren 是一个字符串
      if (typeof oldChildren === "string") {
          if (typeof newChildren === "string") {
              el.textContent = newChildren;
          } else {
              el.textContent = "";
              newChildren.forEach((item) => {
                  mount(item, el);
              });
          }
      } else if (typeof newChildren === "string") {
          // oldChildren of Array
          el.innerHTML = newChildren;
      } else {
          // all Array
          // 3.2 处理公共长度部分
          const commonLength = Math.min(oldChildren.length, newChildren.length);
      
          let i = 0;
          for (i; i < commonLength; ++i) {
              // 进行对比
              patch(oldChildren[i], newChildren[i]);
          }
      
          // 删除 oldChildren 多余部分 或 添加 newChildren 多余部分
          for (let j = i; j < oldChildren.length; ++j) {
              el.removeChild(oldChildren[j].el!);
          }
      
          for (let j = i; j < newChildren.length; ++j) {
              mount(newChildren[j], el);
          }
      
完整代码
  • // n1: 旧的VNode, n2: 更新后的VNode
    function patch(n1, n2) {
      // 1. 判断 n1, n2 tag 是否相同, 不同 直接将以n1 生成的DOM 子孙 全部移除
      if (n1.tag !== n2.tag) {
        // 获取 n1.el 的父元素
        const n1ElParent = n1.el.parentElement;
        // 删除当前子树
        n1ElParent.removeChild(n1.el);
        // 将 n2 渲染并挂载到 n1ElParent 上
        mount(n2, n1ElParent);
      } else {
        const el = (n2.el = n1.el);
        // 2. tag 相同,处理props
        const oldProps = n1.props;
        const newProps = n2.props;
        // 2.1 添加或修改 到 el 属性 上
        for (const key in newProps) {
          const newValue = newProps[key];
          const oldValue = oldProps[key];
          if (newValue !== oldValue) {
            if (key.startsWith("on")) {
              el.addEventListener(key.slice(2).toLowerCase(), newValue);
            } else {
              el.setAttribute(key, newValue);
            }
          }
        }
        // 2.2 删除旧的事件处理程序,和 newProps 没有的属性
        for (const key in oldProps) {
          if (key.startsWith("on")) {
            const value = oldProps[key];
            el.removeEventListener(key.slice(2).toLowerCase(), value);
          } else {
            if (!(key in newProps)) {
              el.removeAttribute(key);
            }
          }
        }
        // 3. 处理children
        const oldChildren = n1.children;
        const newChildren = n2.children;
        // 3.1 如果 newChildren 是一个字符串
        if (typeof newChildren === "string") {
          if (typeof oldChildren === "string") {
            el.textContent = newChildren;
          } else {
            el.innerHTML = newChildren;
          }
        } else if (typeof oldChildren !== "string") {
          // 处理 子孙 vnode
          // 3.2 处理公共长度部分
          const commonLength = Math.min(oldChildren.length, newChildren.length);
          let i = 0;
          for (i; i < commonLength; ++i) {
            // 进行对比
            patch(oldChildren[i], newChildren[i]);
          }
          // 删除 oldChildren 多余部分 或 添加 newChildren 多余部分
          for (let j = i; j < oldChildren.length; ++j) {
            el.removeChild(oldChildren[j].el);
          }
          for (let j = i; j < newChildren.length; ++j) {
            mount(newChildren[j], el);
          }
        }
      }
    }
    
测试patch函数
  1. 测试 tag 不同

    • const vnode = h("div", { name: "acwink" }, [
          h("h2", null, "当前计数: 1000"),
          h(
              "button",
              {
                  onClick: () => {
                      console.log("+1");
                  },
              },
              "+1"
          ),
      ]);
      
      const app = document.querySelector("#app");
      mount(vnode, app);
      
      // 2. 测试patch
      
      // 2.1 测试 tag 不同
      const vnode1 = h("h2", null, "测试1");
      setTimeout(() => {
          patch(vnode, vnode1);
      }, 2000);
      
    • renderer01.gif

  2. 测试 tag 相同,但 children 为字符串

    • // 2.2 测试 tag 相同,但 children 为字符串
      const vnode2 = h("div", { name: "1231" }, "测试2");
      setTimeout(() => {
          patch(vnode, vnode2);
      }, 2000);
      
    • renderer02.gif

  3. 测试 tag 相同,children 为数组

    • const vnode3 = h("div", { name: "acwink100" }, [
          h("h2", null, "当前计数: 10"),
          h(
              "button",
              {
                  onClick: () => {
                      console.log("acwink");
                  },
              },
              "acwink"
          ),
          h("h1", null, "测试newChildren > oldChildren"),
      ]);
      
      setTimeout(() => {
          patch(vnode, vnode3);
      }, 2000);
      
    • renderer03.gif

渲染系统完整代码

renderer.ts

  • import { VNode } from "./types";
    
    // tag 元素标签, props 元素标签上的属性, children: 元素标签内包裹的内容
    function h(tag: string, props: any, children: string | VNode[]): VNode {
      return {
        tag,
        props,
        children,
      };
    }
    
    
    // vnode: 要渲染并挂载的vnode节点, container: 该节点要挂载的DOM节点
    function mount(vnode: VNode, container: Element) {
      // 1. 将VNode 创建成真实的元素, 并将其保存到 当前的vnode上
      const el = (vnode.el = document.createElement(vnode.tag));
    
      // 2. 处理标签上的属性 props
      for (const key in vnode.props) {
        const value = vnode.props[key];
    
        // 2.1 处理 监听事件
        if (key.startsWith("on")) {
          el.addEventListener(key.slice(2).toLowerCase(), value);
        } else {
          // 2.2 处理其他属性
          el.setAttribute(key, value);
        }
      }
    
      // 3. 处理 children
      // 3.1 如果 children 是一个字符串
      if (typeof vnode.children === "string") {
        el.textContent = vnode.children;
      } else {
        // 3.2 children 是个 VNode 数组
        // 递归处理 子节点的vnode, 将其挂载到当前节点上
        vnode.children.forEach((item) => {
          mount(item, el);
        });
      }
    
      // 4. 将vnode 挂载到 container
      container.appendChild(el);
    }
    
    // n1: 旧的VNode, n2: 更新后的VNode
    function patch(n1: VNode, n2: VNode) {
      // 1. 判断 n1, n2 tag 是否相同, 不同直接将以n1 生成成的DOM 及子DOM 全部 减掉
      if (n1.tag !== n2.tag) {
        // 获取 n1.el 的父元素
        const n1ElParent = n1.el?.parentElement;
        // 删除当前子树
        n1ElParent?.removeChild(n1.el!);
        // 将 n2 渲染并挂载到 n1ElParent 上
        mount(n2, n1ElParent!);
      } else {
        const el = (n2.el = n1.el);
        if (el) {
          // 2. tag 相同,处理props
          const oldProps = n1.props;
          const newProps = n2.props;
    
          // 2.1 添加或修改 到 el 属性 上
          for (const key in newProps) {
            const newValue = newProps[key];
            const oldValue = oldProps[key];
    
            if (newValue !== oldValue) {
              if (key.startsWith("on")) {
                el.addEventListener(key.slice(2).toLowerCase(), newValue);
              } else {
                el.setAttribute(key, newValue);
              }
            }
          }
    
          // 2.2 删除旧的事件处理程序,和 newProps 没有的属性
          for (const key in oldProps) {
            if (key.startsWith("on")) {
              const value = oldProps[key];
              el.removeEventListener(key.slice(2).toLowerCase(), value);
            } else {
              if (!(key in newProps)) {
                el.removeAttribute(key);
              }
            }
          }
    
          // 3. 处理children
          const oldChildren = n1.children;
          const newChildren = n2.children;
    
          // 3.1 如果 oldChildren 是一个字符串
          if (typeof oldChildren === "string") {
            if (typeof newChildren === "string") {
              el.textContent = newChildren;
            } else {
              el.textContent = "";
              newChildren.forEach((item) => {
                mount(item, el);
              });
            }
          } else if (typeof newChildren === "string") {
            // oldChildren of Array
            el.innerHTML = newChildren;
          } else {
            // all Array
            // 3.2 处理公共长度部分
            const commonLength = Math.min(oldChildren.length, newChildren.length);
    
            let i = 0;
            for (i; i < commonLength; ++i) {
              // 进行对比
              patch(oldChildren[i], newChildren[i]);
            }
    
            // 删除 oldChildren 多余部分 或 添加 newChildren 多余部分
            for (let j = i; j < oldChildren.length; ++j) {
              el.removeChild(oldChildren[j].el!);
            }
    
            for (let j = i; j < newChildren.length; ++j) {
              mount(newChildren[j], el);
            }
          }
        }
      }
    }
    
    export { h, mount, patch };
    
    

可响应式系统

当数据发生变化时,依赖该数据的函数将自动获取新的值。

响应式系统的基本逻辑

  • image20220421213137001.png

依赖收集器

收集依赖该数据的所有函数

  • // 依赖收集器
    class Dep {
      subscribe: Set<Function>;
      constructor() {
        this.subscribe = new Set();
      }
    
      // 收集依赖
      depend() {
        if (activeEffect) {
          this.subscribe.add(activeEffect);
        }
      }
    
      // 通知执行
      notify() {
        this.subscribe.forEach((effect) => {
          effect();
        });
      }
    }
    
    // 中间层
    let activeEffect: Function | null = null;
    function watchEffect(effect: Function) {
      activeEffect = effect;
      // 这个函数被调用的时候,会被所有依赖数据的get捕获器捕获
      effect();
      activeEffect = null;
    }
    
测试该依赖收集器
  • const dep = new Dep();
    
    const info = {
      name: "acwink",
    };
    
    const infoProxy = new Proxy(info, {
      // 获取值时 被get 捕获器 捕获
      get(target, p, receiver) {
        // 通知依赖收集器,收集该函数
        dep.depend();
        return Reflect.get(target, p, receiver);
      },
      // 修改值时 被set 捕获器 捕获
      set(target, p, value, receiver) {
        // 修改对象值
        const flag = Reflect.set(target, p, value, receiver);
    
        // 通知依赖器, 数据发生变化
        dep.notify();
    
        return flag;
      },
    });
    
    // function1
    watchEffect(function () {
      console.log("effect1: " + infoProxy.name);
    });
    
    // function2
    watchEffect(function () {
      console.log("effect2: " + infoProxy.name);
    });
    
    // 修改数值
    infoProxy.name = "acwinkTest";
    
  • image20220421220226949.png

  • 注意:目前上面的做法存在bug,当我的info数据,不仅仅有name时,修改其他属性,也会通知依赖收集器。

    • const info = {
        name: "acwink",
        age: 18,
      };
      // const infoProxy = ...;
      infoProxy.age = 20;
      
    • image20220421220537192.png

    • 很明显,该依赖收集器,只是争对info对象,我们必须把依赖收集器明确到精准的对象属性。也就是说,应为每个被依赖的属性,生成一个收集器。

为每个依赖属性,创建收集器

我们需要通过Map数据结构,将 对象属性 映射到 收集器。 同时,我们也需要将 对象 映射 到 Map(对象属性 => dep)

  • image20220421232713994.png
实现getDep

用于获取每个依赖属性的 收集器

  • // 用来保存,对象 => Map(prop => dep)
    const targetMap = new WeakMap<any, Map<string | Symbol, Dep>>();
    // 用于获取指定数据的 Dep
    function getDep(target: any, prop: string | Symbol) {
      // 1. 获取 obj => Map
      let depsMap = targetMap.get(target);
      // 不存在就创建
      if (!depsMap) {
        depsMap = new Map<string, Dep>();
        targetMap.set(target, depsMap);
      }
    
      let dep = depsMap.get(prop);
      // 不存在就创建
      if (!dep) {
        dep = new Dep();
        depsMap.set(prop, dep);
      }
    
      return dep;
    }
    
  • 注意:这里的使用的WeakMap。 weakMap 特性一句话概括:对键来说只是弱引用,不影响垃圾回收。对值引用来说不是弱,键在值在键亡值亡。 不影响 键 的垃圾回收机制。

修改代码并测试

修改Proxy 和 添加新的依赖函数

  • const info = {
      name: "acwink",
      age: 18,
    };
    
    const infoProxy = new Proxy(info, {
      // 获取值时 被get 捕获器 捕获
      get(target, p, receiver) {
        // 获取当前属性的 收集器 Dep
        const dep = getDep(target, p);
        // 通知依赖收集器,收集该函数, activeEffect 中保存的函数
        dep.depend();
        return Reflect.get(target, p, receiver);
      },
      // 修改值时 被set 捕获器 捕获
      set(target, p, value, receiver) {
        // 修改对象值
        const flag = Reflect.set(target, p, value, receiver);
    
        // 获取当前属性的 收集器 Dep
        const dep = getDep(target, p);
        // 通知依赖器, 数据发生变化
        dep.notify();
    
        return flag;
      },
    });
    
    // function1
    watchEffect(function () {
      console.log("effect1: " + infoProxy.name);
    });
    
    // function2
    watchEffect(function () {
      console.log("effect2: " + infoProxy.name + " " + infoProxy.age);
    });
    
    // function3
    watchEffect(function () {
      console.log("effect3: " + infoProxy.age);
    });
    

尝试修改name 和 age

  • // 修改数值
    infoProxy.name = "acwinkTest";
    infoProxy.age = 20;
    
  • image20220421234756097.png

  • effect2 函数即依赖 name 也 依赖 age, 所以name和age修改它都将重新执行。对于 effect1 和 effect3 一个只依赖 name 一个 age

注意:以上设置代理对象存在一个代码冗余。以目前的方法,如果我们需要在为一个对象设置代理对象时,需要重写大量重复的代码。

reactive 生成 Proxy 代理对象

作用:传入一个对象,返回它的一个代理对象

实现
  • // 代理对象
    function reactive(raw: any) {
      return new Proxy(raw, {
        get(target, p, receiver) {
          const dep = getDep(target, p);
          dep.depend();
          return Reflect.get(target, p, receiver);
        },
        set(target, p, value, receiver) {
          const flag = Reflect.set(target, p, value, receiver);
          const dep = getDep(target, p);
          dep.notify();
          return flag;
        },
      });
    }
    
    
测试
  • const info = reactive({
      name: "acwink",
      age: 18,
    });
    
    // function1
    watchEffect(function () {
      console.log("effect1: " + info.name);
    });
    
    // function2
    watchEffect(function () {
      console.log("effect2: " + info.name + " " + info.age);
    });
    
    // function3
    watchEffect(function () {
      console.log("effect3: " + info.age);
    });
    
    // 修改数值
    info.name = "acwinkTest";
    info.age = 20;
    
  • 结果依然是正确的

    • image20220421234756097.png
提出一个问题:当对象中包含一个score属性,并且该属性引用其他对象,此时修改score中的数据是否依然是响应式的?
const info = reactive({
  name: "acwink",
  age: 18,
  score: {
    math: 100,
    english: 99,
  },
});
验证
  • // function1
    watchEffect(function () {
      console.log("effect1: " + info.name);
    });
    
    // function2
    watchEffect(function () {
      console.log("effect3: " + info.age);
    });
    // function3
    watchEffect(function () {
      console.log("effect3: " + info.score.math);
    });
    // 修改数值
    info.name = "acwinkTest";
    info.score.math = 99;
    
    
  • 当我们修改info.score.math 时,依赖函数 function3 并未重新执行。但是,math 的值确实被修改了。

    • image20220422000617467.png
  • 原因:Proxy 只会对对象的表层进行代理。也就是说 info.score 这个对象引用值修改会被捕获到。而对于 info.score 所引用的对象 属性math,无法监听代理。

解决嵌套对象的代理问题

修改 reactive

  • // 配置文件
    interface IReactiveConfig {
      deep: boolean;
    }
    // 代理对象
    // raw 原始数据,config 代理配置模式
    function reactive(raw: any, config?: IReactiveConfig) {
      let rawCopy = Object.assign({}, raw);
      // 判断是否深度监听
      if (config?.deep) {
        for (const key in rawCopy) {
          const value = rawCopy[key];
    
          if (typeof value === "object") {
            // 递归创建该对象的响应式,并赋值给当前对象
            rawCopy[key] = reactive(value, config);
          }
        }
      }
    
      return new Proxy(rawCopy, {
        get(target, p, receiver) {
          const dep = getDep(target, p);
          dep.depend();
          return Reflect.get(target, p, receiver);
        },
        set(target, p, value, receiver) {
          const flag = Reflect.set(target, p, value, receiver);
          const dep = getDep(target, p);
          dep.notify();
          return flag;
        },
      });
    }
    
测试
  • const info = reactive(
      {
        name: "acwink",
        age: 18,
        score: {
          math: 100,
          english: 99,
        },
      },
      {
        deep: true,
      }
    );
    
    // function1
    watchEffect(function () {
      console.log("effect1: " + info.name);
    });
    
    // function2
    watchEffect(function () {
      console.log("effect3: " + info.age);
    });
    
    watchEffect(function () {
      console.log("effect3: " + info.score.math);
    });
    // 修改数值
    info.name = "acwinkTest";
    info.score.math = 99;
    console.log(info);
    
  • 结果:此时 info.score.math 被修改也会被响应式系统检测到,并通知其订阅者执行它的相关逻辑。

    • image20220422002725367.png

响应式系统完整代码

reactive.ts

  • import { IReactiveConfig } from "./types";
    /*
    // 代理对象
    export interface IReactiveConfig {
      deep: boolean;
    }
    */
    class Dep {
      subscribe: Set<Function>;
      constructor() {
        this.subscribe = new Set();
      }
    
      // 收集依赖
      depend() {
        if (activeEffect) {
          this.subscribe.add(activeEffect);
        }
      }
    
      // 通知执行
      notify() {
        this.subscribe.forEach((effect) => {
          effect();
        });
      }
    }
    
    // 中间层
    let activeEffect: Function | null = null;
    function watchEffect(effect: Function) {
      activeEffect = effect;
      // 这个函数被调用的时候,会被所有依赖数据的get捕获器捕获
      effect();
      activeEffect = null;
    }
    
    // 用来保存,对象 => Map(prop => dep)
    const targetMap = new WeakMap<any, Map<string | Symbol, Dep>>();
    // 用于获取指定数据的 Dep
    function getDep(target: any, prop: string | Symbol) {
      // 1. 获取 obj => Map
      let depsMap = targetMap.get(target);
      // 不存在就创建
      if (!depsMap) {
        depsMap = new Map<string, Dep>();
        targetMap.set(target, depsMap);
      }
    
      let dep = depsMap.get(prop);
      // 不存在就创建
      if (!dep) {
        dep = new Dep();
        depsMap.set(prop, dep);
      }
    
      return dep;
    }
    
    // raw 原始数据,config 代理配置模式
    function reactive(raw: any, config?: IReactiveConfig) {
      let rawCopy = Object.assign({}, raw);
      // 判断是否深度监听
      if (config?.deep) {
        for (const key in rawCopy) {
          const value = rawCopy[key];
    
          if (typeof value === "object") {
            // 递归创建该对象的响应式,并赋值给当前对象
            rawCopy[key] = reactive(value, config);
          }
        }
      }
    
      return new Proxy(rawCopy, {
        get(target, p, receiver) {
          const dep = getDep(target, p);
          dep.depend();
          return Reflect.get(target, p, receiver);
        },
        set(target, p, value, receiver) {
          const flag = Reflect.set(target, p, value, receiver);
          const dep = getDep(target, p);
          dep.notify();
          return flag;
        },
      });
    }
    
    const info = reactive(
      {
        name: "acwink",
        age: 18,
        score: {
          math: 100,
          english: 99,
        },
      },
      {
        deep: true,
      }
    );
    
    export { reactive, watchEffect };
    
    

应用程序入口模块

参考Vue3 的入口程序挂载操作

  • // App 为组件
    createApp(App).mount("#app");
    
createApp函数

enter.ts

  • import { mount, patch } from "./renderer";
    import { watchEffect } from "./reactive";
    import { IAcComponent, VNode } from "./types";
    
    function createApp(rootCompontent: IAcComponent) {
      return {
        mount(selector: string) {
          // 获取要挂载的容器
          const container = document.querySelector(selector)!;
          let isMounted = false;
          let oldVnode: VNode | null = null;
    
          // activeEffect = 当前函数
          // 执行该函数 会被对应响应式数据劫持,并添加到订阅者集合中
          watchEffect(function () {
            if (!isMounted) {
              // render 里面所使用的数据的订阅者集合都会收集该函数。
              oldVnode = rootCompontent.render();
              mount(oldVnode, container);
              isMounted = true;
            } else {
              const newVnode = rootCompontent.render();
              patch(oldVnode!, newVnode);
              oldVnode = newVnode;
            }
          });
        },
      };
    }
    
    export { createApp };
    
    

通过MIN-VUE 实现计数器案例

代码实现Index.ts

  • import { createApp } from "./enter";
    import { reactive } from "./reactive";
    import { h } from "./renderer";
    import { IAcComponent } from "./types";
    
    const App: IAcComponent = {
      data: reactive({
        counter: 0,
        name: "acwink",
      }),
    
      render() {
        return h("div", { class: "acwink" }, [
          h("h2", null, `当前计数: ${this.data.counter}`),
          h(
            "button",
            {
              class: "inc",
              onClick: () => {
                this.data.counter++;
              },
            },
            "+1"
          ),
          h(
            "button",
            {
              class: "dec",
              onClick: () => {
                this.data.counter--;
              },
            },
            "-1"
          ),
          h("h2", null, `${this.data.name}`),
          h(
            "button",
            {
              onClick: () => {
                this.data.name = "jack";
              },
            },
            "change name"
          ),
        ]);
      },
    };
    
    createApp(App).mount("#app");
    
    

案例响应式效果展示

  • counter.gif

案例下载地址

TsMinVue - Counter 提取码: 89lh