21-实现provide/inject

131 阅读2分钟

基础版

  1. 实现 provide/inject 声明
  2. 实现父子组件的注入/取值

案例

/*
 * @Author: Lin zefan
 * @Date: 2022-03-21 21:46:14
 * @LastEditTime: 2022-03-31 18:33:22
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\example\provide-inject\App.js
 *
 */

import { h, provide, inject } from "../../lib/mini-vue.esm.js";

export default {
  name: "Provider",
  render() {
    return h("div", {}, [h("div", {}, "Provider"), h(Consumer)]);
  },
  setup() {
    // 在上层 provide
    provide("foo", "foo");
  },
};

const Consumer = {
  name: "Consumer",
  render() {
    return h("div", {}, "Consumer: " + `inject foo: ${this.foo}`);
  },
  setup() {
    return {
      // 在下层 inject
      foo: inject("foo"),
    };
  },
};

实现

创建provide/inject

apiInject.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-31 18:34:20
 * @LastEditTime: 2022-04-01 11:37:02
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\apiInject.ts
 *
 */

import { getCurrentInstance } from "./component";

export function provide(key, value) {
  const currentInstance = getCurrentInstance() as any;
  if (currentInstance) {
    const { providers } = currentInstance;
    providers[key] = value;
  }
}
export function inject(key) {
  const currentInstance = getCurrentInstance() as any;
  if (currentInstance) {
    /**
     * 1. 获取的是父组件的providers,而不是自身
     * 2. 所以我们需要把父组件实例注入到实例对象
     */
    const { providers } = currentInstance.parent;
    return providers[key];
  }
}

拓展实例对象

component.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-21 22:08:11
 * @LastEditTime: 2022-04-01 11:36:28
 * @LastEditors: Lin zefan
 * @Description: 处理组件类型
 * @FilePath: \mini-vue3\src\runtime-core\component.ts
 *
 */
 // 初始化Component结构
function createComponentInstance(initVNode, parent) {
  const component = {
    vnode: initVNode,
    type: initVNode.type,
    proxy: null,
    setupState: {},
    props: {},
    slots: {},
    providers: {},
    emit: () => {},
    // 挂载父组件实例
    parent,
  };

  /** 注册emit
   * 1. 通过bind把当前实例给到emit函数
   */
  component.emit = emit.bind(null, component) as any;

  return component;
}

createComponentInstance 新增了 parent 形参,接收的是父组件实例,其他相关函数也要把 parent 传递过来

components.ts

function processComponent(vnode, container, parentComponent)
function mountComponent(vnode, container, parentComponent) 


// 这里是最关键的一部分,会通过patch把父级实例传递下去
function setupRenderEffect(instance, container) {
  const { proxy, vnode } = instance;
  // 通过render函数,获取render返回虚拟节点,并绑定render的this
  const subTree = instance.render.call(proxy);
  /**
   * 1. 调用组件render后把结果再次给到patch
   * 2. 再把对应的dom节点append到container
   * 3. 把当前实例传过去,让子组件可以通过parent获取父组件实例
   */
  patch(subTree, container, instance);
}

render.ts

export function patch(vnode, container, parentComponent) {
  if (!vnode) return;
  const { type } = vnode;

  switch (type) {
    case Fragment:
      processFragment(vnode, container, parentComponent);
      break;
    case TextNode:
      processTextNode(vnode, container);
      break;

    default:
      if (isObject(type)) {
        // 是一个Component
        processComponent(vnode, container, parentComponent);
      } else if (typeof type === "string") {
        // 是一个element
        processElement(vnode, container, parentComponent);
      }
      break;
  }
}

element.ts

function processElement(vnode, container, parentComponent)
function mountElement(vnode, container, parentComponent) 
function mountChildren(children, container, parentComponent)
function processFragment(vnode: any, container: any, parentComponent)

升级版

  • provide可深层注入,子组件会依次往上级组件查找

例子

App.js

/*
 * @Author: Lin zefan
 * @Date: 2022-03-21 21:46:14
 * @LastEditTime: 2022-04-01 12:11:58
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\example\provide-inject\App.js
 *
 */

import { h, provide, inject } from "../../lib/mini-vue.esm.js";

export default {
  name: "Provider",
  render() {
    return h("div", {}, [h("div", {}, "Provider"), h(Provider2)]);
  },
  setup() {
    // 在上层 provide
    provide("foo", "foo");
  },
};

const Provider2 = {
  render() {
    return h("div", {}, [h("div", {}, `Provider2:${this.foo}`), h(Provider3)]);
  },
  setup() {
    provide("foo", "foo2");
    return {
      // 在下层 inject
      foo: inject("foo"),
    };
  },
};
const Provider3 = {
  render() {
    return h("div", {}, [h("div", {}, `Provider3:${this.foo}`), h(Consumer)]);
  },
  setup() {
    provide("foo", "foo3");
    return {
      // 在下层 inject
      foo: inject("foo"),
    };
  },
};

const Consumer = {
  name: "Consumer",
  render() {
    return h("div", {}, "Consumer: " + `inject foo: ${this.foo}`);
  },
  setup() {
    return {
      // 在下层 inject
      foo: inject("foo"),
    };
  },
};

简单版

指向父级providers

function createComponentInstance(initVNode, parent) {
  const component = {
    vnode: initVNode,
    type: initVNode.type,
    proxy: null,
    setupState: {},
    props: {},
    slots: {},
    /** 当前的 providers 指向父级的 providers,解决跨层取值,但是有缺陷
     * 1. 引用的关系会影响父组件,当子组件注入同名的foo,就会影响到父组件的foo
     * const father = { foo:1};
       const children = father;
       children.foo =2;
       console.log(father, children)
     */
    providers: parent ? parent.providers : {},
    emit: () => {},
    // 挂载父组件实例
    parent,
  };

  /** 注册emit
   * 1. 通过bind把当前实例给到emit函数
   */
  component.emit = emit.bind(null, component) as any;

  return component;
}

这个方案是有缺陷的,由于引用的关系会影响父组件,当子组件注入同名的foo,就会影响到父组件的foo,导致父组件的值也被修改。

优化版

借助 Object.create 去创建对应的 prototype

/*
 * @Author: Lin zefan
 * @Date: 2022-03-31 18:34:20
 * @LastEditTime: 2022-04-01 12:50:13
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\runtime-core\apiInject.ts
 *
 */

import { getCurrentInstance } from "./component";

export function provide(key, value) {
  const currentInstance = getCurrentInstance() as any;
  if (currentInstance) {
    let { providers } = currentInstance;
    const parentProviders =
      currentInstance.parent && currentInstance.parent.providers;
    /** 初始化判断
     * 1. 根组件没有parent,这个判断不会走
     * 2. 判断当前providers与父级providers是否相等,相等即初始化
     */
    if (providers === parentProviders) {
      /** 初始化组件providers
       * 1. 通过Object.create创建一个新对象,避免引用导致的问题
       * 2. 通过Object.create传入父组件数据,Object.create内部会挂载prototype
       * 3. 当前组件获取不到数据,可以通过prototype向上级寻找(原型链)
       */
      providers = currentInstance.providers = Object.create(parentProviders);
    }
    providers[key] = value;
  }
}

默认值

inject支持默认值,可以是string或者函数

// App.js
/*
 * @Author: Lin zefan
 * @Date: 2022-03-21 21:46:14
 * @LastEditTime: 2022-04-01 13:48:26
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\example\provide-inject\App.js
 *
 */

import { h, provide, inject } from "../../lib/mini-vue.esm.js";

export default {
  name: "Provider",
  render() {
    return h("div", {}, [h("div", {}, "Provider"), h(Provider2)]);
  },
  setup() {
    // 在上层 provide
    provide("foo", "foo");
  },
};

const Provider2 = {
  name: "Provider2",
  render() {
    return h("div", {}, [h("div", {}, `Provider2:${this.foo}`), h(Provider3)]);
  },
  setup() {
    provide("foo", "foo2");
    return {
      // 在下层 inject
      foo: inject("foo"),
    };
  },
};
const Provider3 = {
  name: "Provider3",
  render() {
    return h("div", {}, [
      h("div", {}, `Provider3:${this.foo}`),
      h("div", {}, `Provider3-baseFoo:${this.baseFoo}`),
      h("div", {}, `Provider3-baseBar:${this.baseBar}`),
      h(Consumer),
    ]);
  },
  setup() {
    provide("foo", "foo3");
    const baseFoo = inject("baseFoo", "base");
    const baseBar = inject("baseBar", () => "bar");
    return {
      // 在下层 inject
      foo: inject("foo"),
      baseFoo,
      baseBar,
    };
  },
};

const Consumer = {
  name: "Consumer",
  render() {
    return h("div", {}, "Consumer: " + `inject foo: ${this.foo}`);
  },
  setup() {
    return {
      // 在下层 inject
      foo: inject("foo"),
    };
  },
};

// apiInject.ts
export function inject(key, defaultVal) {
  const currentInstance = getCurrentInstance() as any;
  if (currentInstance) {
    /**
     * 1. 获取的是父元素的providers,而不是自身
     * 2. 所以我们需要把父组件实例注入到实例对象
     */
    const { providers } = currentInstance.parent;
    // 支持默认值,string | array
    if (!providers[key] && defaultVal) {
      if (typeof defaultVal === "function") {
        return defaultVal();
      }
      return defaultVal;
    }
    return providers[key];
  }
}