createRenderer
vue3有一个高阶api createRenderer,它可以自定义我们的渲染器
这章节模拟实现createRenderer,就目前代码,只支持dom类型渲染,进一步去修改整个render的逻辑
实现
修改render逻辑
- 暴露整个render.js,即createRenderer api
- 提取了element.ts 和 component.js 中的processElement、processComponent、processFragment、processTextNode等4个初始化函数
- 修改mountElement内部创建实例、设置属性、添加到根元素方法变为自定义传入,使其支持多种渲染器
- render暴露一个createApp的方法,供用户创建实例使用
/*
* @Author: Lin zefan
* @Date: 2022-03-21 22:04:58
* @LastEditTime: 2022-04-01 22:01:09
* @LastEditors: Lin zefan
* @Description:
* @FilePath: \mini-vue3\src\runtime-core\render.ts
*
*/
import { ShapeFlags } from "../shared/ShapeFlags";
import { createComponentInstance, setupComponent } from "./component";
import { createAppAPI } from "./createdApp";
import {
Fragment,
getChildrenShapeFlags,
getShapeFlags,
TextNode,
} from "./vnode";
export function createRenderer(options) {
// 改名字是为了 debug 方便
const {
createElement: hostCreateElement,
insert: hostInsert,
patchProp: hostPatchProp,
selector: hostSelector,
} = options;
function render(vnode, container) {
// 根组件没有父级,所以是null
patch(vnode, container, null);
}
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:
const shapeFlags = getShapeFlags(type);
if (shapeFlags === ShapeFlags.COMPONENT) {
// 是一个Component
processComponent(vnode, container, parentComponent);
} else if (shapeFlags === ShapeFlags.ELEMENT) {
// 是一个element
processElement(vnode, container, parentComponent);
}
break;
}
}
// 创建一个Fragment节点
function processFragment(vnode: any, container: any, parentComponent) {
mountChildren(vnode.children, container, parentComponent);
}
// 创建一个TextNode节点
function processTextNode(vnode: any, container: any) {
const textNode = document.createTextNode(vnode.children);
container.append(textNode);
}
// ---------------------Element----------------------
function processElement(vnode, container, parentComponent) {
mountElement(vnode, container, parentComponent);
}
// ---------------------Element创建流程----------------------
function mountElement(vnode, container, parentComponent) {
const { type, props, children } = vnode;
// 创建根元素、将元素挂载到实例
const el = (vnode.$el = hostCreateElement(type));
// 设置行内属性
for (const key in props) {
hostPatchProp(el, key, props);
}
// 设置children
const shapeFlags = getChildrenShapeFlags(children);
if (shapeFlags === ShapeFlags.TEXT_CHILDREN) {
el.textContent = children;
} else if (shapeFlags === ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el, parentComponent);
}
hostInsert(el, container);
}
function mountChildren(children, container, parentComponent) {
children.forEach((h) => {
patch(h, container, parentComponent);
});
}
// ---------------------Component---------------------------
function processComponent(vnode, container, parentComponent) {
// TODO,这里会比较vnode,然后做创建、更新操作,这里先处理创建
// 创建组件
mountComponent(vnode, container, parentComponent);
// TODO,更新组件
// updateComponent(vnode, container);
}
// -----------------Component创建流程-------------------
function mountComponent(vnode, container, parentComponent) {
// 初始化Component实例
const instance = createComponentInstance(vnode, parentComponent);
// 初始化setup函数return的数据
setupComponent(instance, container);
// setupRenderEffect
setupRenderEffect(instance, container);
}
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);
/** 挂载当前的dom元素到$el
* 1. 当遍历完所有Component组件后,会调用processElement
* 2. 在processElement中,会创建dom元素,把创建的dom元素挂载到传入的vnode里面
* 3. 当前的dom元素也就是processElement中创建的dom元素
*/
vnode.el = subTree.$el;
}
// 暴露
return {
/** 将createApp方法暴露出去
* 参数一为 render渲染函数,调用patch
* 参数二为 是一个函数,返回一个节点,是可选的
*/
createApp: createAppAPI(render, hostSelector),
};
}
这一步做完,不要忘记删掉多余的文件和函数
创建runtime-dom
runtime-dom 文件夹处理的都是dom的玩意; 之前dom的操作逻辑,抽离到这边来
/*
* @Author: Lin zefan
* @Date: 2022-04-01 16:53:01
* @LastEditTime: 2022-04-01 22:14:01
* @LastEditors: Lin zefan
* @Description: dom渲染
* @FilePath: \mini-vue3\src\runtime-dom\index.ts
*
*/
import { createRenderer } from "../runtime-core/render";
import { isDom } from "../shared/index";
export function createElement(type) {
return document.createElement(type);
}
const isEvents = (key: string = "") => {
const reg = /^on[A-Z]/;
if (reg.test(key)) {
// onClick -> click
return key.slice(2).toLocaleLowerCase();
}
return "";
};
export function patchProp(el, key, props) {
/** 注册事件
* 1. 判断是否on开头并包含一个大写字母开头
* 2. 是的话,截取on后面的内容
* 3. 注册元素事件
*/
const val = props[key];
if (isEvents(key)) {
el.addEventListener(isEvents(key), val);
} else {
el.setAttribute(key, val);
}
}
export function insert(el, parent) {
parent.appendChild(el);
}
export function selector(container) {
return isDom(container);
}
const renderer: any = createRenderer({
createElement,
patchProp,
insert,
selector,
});
/**
* 暴露 createApp,这个方法就是创建vue实例的方法
* @param args 当前的根节点,一般是App.js
*/
export const createApp = (...args) => {
return renderer.createApp(...args);
};
// runtime-core是底层逻辑,放到这边暴露出去
export * from "../runtime-core/index";
创建createAppAPI函数
createAppAPI其实就在createApp的基础上包了一层,把render函数传了过去,因为render不再暴露了
/*
* @Author: Lin zefan
* @Date: 2022-03-21 21:49:41
* @LastEditTime: 2022-04-01 22:02:20
* @LastEditors: Lin zefan
* @Description:
* @FilePath: \mini-vue3\src\runtime-core\createdApp.ts
*
*/
import { isDom } from "../shared/index";
import { createdVNode } from "./vnode";
/**
* 创建一个Vue实例
* @param renderer render函数,内部调用了patch
* @param selector selector函数,内部返回一个节点
* @returns
*/
export function createAppAPI(renderer, selector) {
return function createApp(rootComponent) {
return {
// 暴露一个mount方法
mount(rootContainer) {
/**
* 1. 将根组件(rootComponent)转换为vnode
* 2. 再通过render函数将vnode渲染到mount接收的容器(rootContainer)中
*/
const vnode = createdVNode(rootComponent);
renderer(
vnode,
selector ? selector(rootContainer) : isDom(rootContainer)
);
},
};
};
}
修改打包入口引用
rollup打包的入口文件改为 runtime-dom,因为现在createApp方法放在 runtime-dom/index里边
/*
* @Author: Lin zefan
* @Date: 2022-03-22 15:40:00
* @LastEditTime: 2022-04-01 18:19:05
* @LastEditors: Lin zefan
* @Description: 打包入口文件
* @FilePath: \mini-vue3\src\index.ts
*
*/
// 暴露runtime-core模块
export * from "./runtime-dom/index";
暴露createRenderer api
/*
* @Author: Lin zefan
* @Date: 2022-03-22 16:22:33
* @LastEditTime: 2022-04-01 21:34:44
* @LastEditors: Lin zefan
* @Description:
* @FilePath: \mini-vue3\src\runtime-core\index.ts
*
*/
export * from "./createdApp";
export { h } from "./h";
export { renderSlots } from "./helpers/renderSlots";
export { createTextVNode } from "./vnode";
export { getCurrentInstance } from "./component";
export { provide, inject } from "./apiInject";
export { createRenderer } from "./render";
贴一下新增的方法
/*
* @Author: Lin zefan
* @Date: 2022-03-15 19:28:09
* @LastEditTime: 2022-04-01 18:27:13
* @LastEditors: Lin zefan
* @Description: 公用hook
* @FilePath: \mini-vue3\src\shared\index.ts
*
*/
export const isDom = (rootContainer) => {
if (typeof rootContainer === "string") {
return document.querySelector(rootContainer);
}
return rootContainer;
};