likes
comments
collection
share

小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

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

仓库地址

文章导航

26、实现 getCurrentInstance

  • getcurrentInstance 方法必须要在 setup函数中调用的。

代码实现 思路大概和之前依赖收集的套路一样,也是用一个全局变量来记录当前正在渲染的组件实例。

component.ts

// 处理有状态的组件
function setupStatefulComponent(instance: any) {
  .....
  if (setup) {
    setCurrentInstance(instance);
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    });
    setCurrentInstance(null);
  .....
  }
}

// 声明一个全局变量,用来记录当前正在渲染的组件
let currentInstance = null;

export function getCurrentInstance() {
  return currentInstance;
}

export function setCurrentInstance(instance) {
  currentInstance = instance;
}

测试代码 app.js

import { getCurrentInstance, h } from "../../lib/vue3-mini-vue.esm.js";
import { Foo } from "./Foo.js";

export const App = {
  name: "App",
  render() {
    return h("div", {}, [h("div", {}, "App"), h(Foo, {}, {})]);
  },

  setup() {
    const ctx = getCurrentInstance();
    console.log("App", ctx);
    return {};
  },
};

foo.js

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

export const Foo = {
  name: "Foo",
  render() {
    return h("Foo", {}, "Foo");
  },

  setup() {
    const ctx = getCurrentInstance();
    console.log("Foo", ctx);
    return {};
  },
};

实现效果

小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

27、实现provide-inject 功能

实现目标

  • provide方法提供数据。
  • inject方法取上层组件注入的值,在当前组件里面使用。

实现思路

  1. provide的值存放组件实例上的provides属性上面。
  2. 从父组件实例上面的provides属性,上面进行取值。
  3. 在父组件的provides属性上如果没找到要注入的值的时候,要一直往上层组件找。(原型链)让当前组件的provides属性的原型指向父级的provides属性。
  4. 区分创建子组件provides属性什么情况下是初次创建,后序在组件中调用多次provide方法,只需要创建一次provides,后面一直使用这个对象即可。
  5. inject可以有默认值。

代码实现

component.ts

export function createComponentInstance(vnode, parent) {
  const component = {
    ...
    // 取 父级的 provides 属性的值放到自己身上
    provides: parent ? parent.provides : {},
    parent,
    ....
  };
  component.emit = emit.bind(null, component) as any;

  return component;
}

renderer.ts

// 调用函数的时候传入 父组件
function mountComponent(initialVNode: any, container: any, parentComponent) {
  const instance = createComponentInstance(initialVNode, parentComponent);

  setupComponent(instance);
  setupRenderEffect(instance, initialVNode, container);
}

function setupRenderEffect(instance, initialVNode, container) {
  const { proxy } = instance;
  // 拿到组件内部  render 函数返回的虚拟节点
  const subTree = instance.render.call(proxy);

  // vnode -> patch
  // vnode -> element -> mountElement
  // 重要,在渲染 subTree 的时候父组件就是当前 实例
  patch(subTree, container, instance);   
  // 把根节点的 el 赋值给组件的虚拟节点
  initialVNode.el = subTree.el;
}

apiInject.ts

import { getCurrentInstance } from "./component";

export function provide(key, value) {
  const currentInstance: any = getCurrentInstance();

  if (currentInstance) {
    // 从当前实例上取出 provides
    let { provides } = currentInstance;
    // 再取父级组件的 provides
    const parentProvides = currentInstance.parent.provides;
    // 在第一次未赋值之前 他们都指向父组件的 provides
    if (provides === parentProvides) {
      // 采用原型链,让子组件的 provides 属性的原型指向 父组件的 provides 属性
      provides = currentInstance.provides = Object.create(parentProvides);
    }
    provides[key] = value;
  }
}

export function inject(key, defaultValue) {
  const currentInstance: any = getCurrentInstance();

  // 说明 inject 只能在 setup 函数中使用
  if (currentInstance) {
    const parentProvides = currentInstance.parent.provides;

    if (key in parentProvides) {
      return parentProvides[key];
    } else if (defaultValue) {
      if (typeof defaultValue === "function") {
        return defaultValue();
      }
      return defaultValue;
    }
  }
}


测试代码

app.js

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

// 爷爷组件
const Provider = {
  render() {
    return h("div", {}, [h("p", {}, "Provide"), h(ProviderTwo)]);
  },
  setup() {
    provide("foo", "fooVal");
    provide("bar", "barVal");
  },
};
// 父组件
const ProviderTwo = {
  render() {
    return h("div", {}, [
      h("p", {}, `ProvideTwo foo: ${this.foo}`),
      h(Consumer),
    ]);
  },
  setup() {
    provide("foo", "fooTwo");
    const foo = inject("foo");

    return {
      foo,
    };
  },
};
// 儿子组件
const Consumer = {
  render() {
    return h(
      "div",
      {},
      `Consumer 组件 Foo: ${this.foo}  Bar: ${this.bar} Baz: ${this.baz} `
    );
  },
  setup() {
    const foo = inject("foo");
    const bar = inject("bar");
    const baz = inject("baz", "bazValue");
    return {
      foo,
      bar,
      baz,
    };
  },
};

const App = {
  render() {
    return h("div", {}, [h(Provider)]);
  },

  setup() {
    return {
    };
  },
};

export default App;

没使用原型链之前,会修改父级注入的值 小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

使用之后

小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

最后再来看一下 儿子组件的 provides属性,

小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

28、实现自定义渲染器 cuntom renderer

实现目标

  1. 可以支持让用户传入要渲染到指定平台元素操作的API

小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

代码实现

renderer.ts 先来实现一个可以接收用户传递平台API的函数。

export function createRenderer(options) {
  const {
    createElement: hostCreateElement,
    patchProp: hostPatchProp,
    insert: hostInsert,
  } = options;
  .......
  function render(vnode, container) {
  .....
  }
  
    // 挂载元素
  function mountElement(vnode: any, container: any, parentComponent) {
    const el = (vnode.el = hostCreateElement(vnode.type));
    .....
    // props
    const { props } = vnode;
    for (let key in props) {
      let val = props[key];
      hostPatchProp(el, key, val);
    }
    hostInsert(el, container);
  }
  
  return {
    // 将 render 函数传递到 那边供内部使用
    createApp: createAppAPI(render),
  };
}

createApp.ts

import { createVNode } from "./vnode";

// 接收 render 函数在开启渲染
export function createAppAPI(render) {
  return function createApp(rootComponent) {
    return {
      mount(rootContainer) {
        // 根据传进来的  参数 把组件转换成 虚拟节点
        const vnode = createVNode(rootComponent);

        render(vnode, rootContainer);
      },
    };
  };
}

新增上层模块runtime-dom/index.ts

import { createRenderer } from "../runtime-core";

function createElement(type) {
  return document.createElement(type);
}

function patchProp(el, key, val) {
  const isOn = (key: string) => /^on[A-Z]/.test(key);
  if (isOn(key)) {
    const event = key.slice(2).toLowerCase();
    el.addEventListener(event, val);
  } else {
    el.setAttribute(key, val);
  }
}

function insert(el, parent) {
  parent.append(el);
}

const renderer: any = createRenderer({
  createElement,
  patchProp,
  insert,
});

export function createApp(...args) {
  return renderer.createApp(...args);
}

export * from "../runtime-core";

src/index.ts

// 在这里导出顶层模块,不再导出原来的 runtime-core
export * from "./reactivity";
export * from "./runtime-dom";

测试代码 main.js

import { createRenderer } from "../../lib/vue3-mini-vue.esm.js";
import { App } from "./App.js";

const game = new PIXI.Application({
  width: 500,
  height: 500,
});

// 将 canvas 添加到容器中去
document.body.append(game.view);

// 实现 一个自定义渲染器
const renderer = createRenderer({
  createElement(type) {
    if (type === "rect") {
      const rect = new PIXI.Graphics();
      rect.beginFill(0xff0000);
      rect.drawRect(0, 0, 100, 100);
      rect.endFill();
      return rect;
    }
  },
  patchProp(el, key, val) {
    el[key] = val;
  },
  insert(el, parent) {
    parent.addChild(el);
  },
});

renderer.createApp(App).mount(game.stage);

app.js

import { h } from "../../lib/vue3-mini-vue.esm.js";

export const App = {
  render() {
    return h("rect", {
      x: this.x,
      y: this.y,
    });
  },

  setup() {
    return {
      x: 100,
      y: 100,
    };
  },
};

最总效果

小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

29、更新element流程搭建

  1. 如何实现当响应式数据发生变化了之后,重新调用render函数?可以利用之前的effect来进行一个依赖收集。
  2. 区分什么时候是更新,什么时候是初始化逻辑?在instance上添加一个isMounted变量来进行区分。
  3. patch的时候也要区分是更新,还是初始化逻辑。

代码实现

renderer.ts

  1. 在调用组件实例render函数的外层包装一个effect,来收集render函数里面依赖了那些响应式数据。
  2. 当数据变化之后重新执行render函数,拿到最新的subTree和老的subTrer进行比较,然后对DOM进行局部更新。

画图来理解一下

小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

renderer.ts

// 在内部区分组件的初始化和更新逻辑
  function setupRenderEffect(instance, initialVNode, container) {
    effect(() => {
      if (!instance.isMounted) {
        console.log("初始化");
        const { proxy } = instance;
        // 拿到组件内部  render 函数返回的虚拟节点
        const subTree = (instance.subTree = instance.render.call(proxy));

        // vnode -> patch
        // vnode -> element -> mountElement
        patch(null, subTree, container, instance);
        // 把根节点的 el 赋值给组件的虚拟节点
        initialVNode.el = subTree.el;
        // 标记组件已经挂载完毕
        instance.isMounted = true;
      } else {
        console.log("更新");
        const { proxy } = instance;
        // 最新的 subTree
        const subTree = instance.render.call(proxy);
        // 获取老的 subTree
        const prevSubTree = instance.subTree;
        // 更新老的 subTree
        instance.subTree = subTree;
        console.log("prevSubTree", prevSubTree);
        console.log("subTree", subTree);
        patch(prevSubTree, subTree, container, instance);
      }
    });
  }
  
  // 对比两颗虚拟节点树,完成打补丁的操作
  function patch(n1, n2, container, parentComponent) {
    // 需要判断当前的 vnode 是不是 element
    // 如果是一个 element 的话就应该处理 element
    // 如何区分是 组件类型还是 element 类型??
    const { type, shapeFlag } = n2;
    switch (type) {
      case Fragment:
        processFragment(n1, n2, container, parentComponent);
        break;
      case Text:
        processText(n1, n2, container);
        break;
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(n1, n2, container, parentComponent);
        } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
          // 处理组件 ,其实这里只有挂载
          processComponent(n1, n2, container, parentComponent);
        }
        break;
    }
  }
  
  // 区分元素是初始化还是更新
    // 处理元素
  function processElement(n1, n2: any, container: any, parentComponent) {
    if (!n1) {
      mountElement(n2, container, parentComponent);
    } else {
      patchElement(n1, n2, container);
    }
  }
  function patchElement(n1, n2, container) {
    console.log("patchElement");
    console.log("n1", n1);
    console.log("n2", n2);
  }

测试代码

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

export const App = {
  render() {
    return h("div", {}, [
      h("div", {}, "count " + this.count),
      h(
        "button",
        {
          onClick: this.onClick,
        },
        "click"
      ),
    ]);
  },

  setup() {
    const count = ref(0);

    const onClick = () => {
      count.value++;
    };
    return {
      count,
      onClick,
    };
  },
};

可以看到组件的更新,最终还是体现在 patchElement既对比组件的新旧元素虚拟节点。 小狐狸学mini-vue(三、provide 和 自定义渲染器实现)

转载自:https://juejin.cn/post/7267865387278549026
评论
请登录