小狐狸学mini-vue(三、provide 和 自定义渲染器实现)
仓库地址
文章导航
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 {};
},
};
实现效果
27、实现provide-inject 功能
实现目标
provide
方法提供数据。inject
方法取上层组件注入的值,在当前组件里面使用。
实现思路
provide
的值存放组件实例上的provides
属性上面。- 从父组件实例上面的
provides
属性,上面进行取值。 - 在父组件的
provides
属性上如果没找到要注入的值的时候,要一直往上层组件找。(原型链)让当前组件的provides
属性的原型指向父级的provides
属性。 - 区分创建子组件
provides
属性什么情况下是初次创建,后序在组件中调用多次provide
方法,只需要创建一次provides
,后面一直使用这个对象即可。 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;
没使用原型链之前,会修改父级注入的值
使用之后
最后再来看一下 儿子组件的 provides
属性,
28、实现自定义渲染器 cuntom renderer
实现目标
- 可以支持让用户传入要渲染到指定平台元素操作的
API
。
代码实现
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,
};
},
};
最总效果
29、更新element流程搭建
- 如何实现当响应式数据发生变化了之后,重新调用
render
函数?可以利用之前的effect
来进行一个依赖收集。 - 区分什么时候是更新,什么时候是初始化逻辑?在
instance
上添加一个isMounted
变量来进行区分。 patch
的时候也要区分是更新,还是初始化逻辑。
代码实现
renderer.ts
- 在调用组件实例
render
函数的外层包装一个effect
,来收集render
函数里面依赖了那些响应式数据。 - 当数据变化之后重新执行
render
函数,拿到最新的subTree
和老的subTrer
进行比较,然后对DOM
进行局部更新。
画图来理解一下
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
既对比组件的新旧元素虚拟节点。
转载自:https://juejin.cn/post/7267865387278549026