Vue3的createApp和h函数实现
前言
大家好,上一篇学习记录Vue3的reactive、computed等api的简单实现 ,今天继续来完成从组件到vnode再渲染成dom元素的完整过程,也就是createApp和h函数的简单实现。
createApp和h函数的实现
基本使用
const { reactive, createApp, h } = VueRuntimeDom;
// App组件
const App = {
setup(props) {
const state = reactive({ name: '李四' });
return {
state
}
},
render(proxy) {
return h('div', proxy.state.name);
}
}
// 组件挂载在div下
createApp(App, { name: '张三', age: 18 }).mount('#app');
实现
createApp方法
- createApp函数会调用createRenderer,返回一个包含createApp方法的对象
- 再去执行对象的createApp方法,返回一个app实例
- 实例上有mount方法,覆盖该方法,先执行清空容器,再执行原先的mount方法
这样的好处是创建app实例和挂载到页面分开执行
// dom操作和dom属性的更新的方法
const rendererOptions = {
createElemet,
//...
}
function createApp(rootComponent, rootProps) {
let app = createRenderer(rendererOptions).createApp(rootComponent, rootProps)
let { mount } = app;
// 执行mount之前将容器清空,再传入
app.mount = function (container) {
container = document.querySelector(container);
container.innerHTML = ''
mount(container)
}
return app
}
createRenderer方法
- createRenderer方法返回一个对象,包含createApp方法
- createApp方法返回app实例,包含一些属性和mount方法
- mount方法和vue2差不多,先创建vnode,再根据vnode将其渲染成真实dom元素 下面再去看createVnode和render函数的实现
function createRenderer(renderOptiondom) {
const render = (vnode, container) => {
// ...
}
return {
createApp(rootComponent, rootProps) {
const app = {
_component: rootComponent,
_props: rootProps,
_container: null,
mount(container) {
//
const vnode = createVnode(rootComponent, rootProps);
render(vnode, container);
app._container = container;
}
}
return app
}
}
}
createVnode方法
- 首先vnode是通过ShapeFlags的位运算来区分vnode的类型,减少判断
- vnode实际就是一个对象,对dom属性进行描述和vue2一样
- normalizeChildren方法,标记子元素
const ShapeFlags = {
ELEMENT: 1, // 普通div标签
FUNCTION_COMPONENT: 1 << 1, // 函数式组件
STATEFUL_COMPONENT: 1 << 2, // 普通组件
TEXT_CHILDREN: 1 << 3, // 子元素是文本
ARRAY_CHIDLREN: 1 << 4, // 子元素是数组
SLOTS_CHILDREN: 1 << 5, // 子元素是插槽
TELEPORT: 1 << 6,
SUSPENS: 1 << 7,
COMPOENNT_SHOULD_KEEP_ALIVE: 1 << 8,
COMPONENT_KEPT_ALIVE: 1 << 9,
COMPONENT: 1 << 1 | 1 << 2 // 组件
}
function createVnode(type, props, children = null) {
const shapeFlag = typeof type === 'string' ? ShapeFlags.ELEMENT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : 0;
const vnode = {
_v_isVnode: true,
type,
props,
children,
key: props && props.key,
el: null,
shapeFlag,
component: {}
}
// 标记子元素
normalizeChildren(vnode, children);
return vnode;
}
function normalizeChildren(vnode, children) {
let type = 0;
if(Array.isArray(children)) {
type = ShapeFlags.ARRAY_CHIDLREN
} else if(children != null) {
type = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag |= type;
}
h函数的实现
- h函数和createVnode方法类似,主要在render函数返回时一个vnode
function h(type, propsOrChildren, children) {
const i = arguments.length;
if (i === 2) {
if (isObject(propsOrChildren)) {
// h('div', h('span', 'hello'))
if (propsOrChildren._v_isVnode) {
return createVnode(type, null, [propsOrChildren])
}
// h(App, {name: '123'})
return createVnode(type, propsOrChildren)
} else {
// h('div', 'hello')
return createVnode(type, null, propsOrChildren)
}
} else {
if (i > 3) {
// h('div', {}, '1', '2', '3')
children = Array.prototype.slice.call(arguments, 2);
} else if (i === 3 && children._v_isVnode) {
// h('div', {name: '123'}, h('span', 'hello'));
children = [children]
}
return createVnode(type, propsOrChildren, children)
}
}
render方法
- render方法实际执行的patch方法
- patch方法,传入新旧vnode,再传入容器,新建是传入的老vnode是null
- patch传入的vnode分三种情况: 组件、元素、文本
- 文本:文本将会被直接加入到父容器
- 元素:dom元素会被直接创建,加入到父容器,子元素数组的话,会递归将子元素patch
- 组件:创建组件实例,组件执行setup函数,然后在effect中执行render函数,让effect函数和render关联,实现数据更新时再次执行render
render函数被执行完之后,元素即被挂载到了指定容器
const TEXT = Symbol('text');
function createRenderer() {
const processComponent = () => {}
const processElement = () => {}
const processText = () => {}
const patch = (n1, n2, contanier) => {
const { shapeFlag, type } = n2;
// 处理文本节点
if (type === TEXT) {
processText(n1, n2, contanier)
} else if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理普通标签
processElement(n1, n2, contanier)
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 处理组件
processComponent(n1, n2, contanier)
}
}
const render = (vnode, container) => {
patch(null, vnode, container)
}
return {
createApp(rootComponent, rootProps) {
const app = {
// ...
mount(container) {
// ....
render(vnode, container);
}
}
return app
}
}
}
processComponent/processElement/processText方法
- 处理文本,使用ocument.createTextNode,再传入到容器
- 处理普通标签,直接创建,children数组需要递归处理
const TEXT = Symbol('text');
// 普通标签
const processElement = (n1, n2, contanier) => {
mountElement(n2, contanier)
}
const mountElement = (vnode, contanier) => {
const { props = {}, shapeFlag, type, children } = vnode;
// 创建标签
const el = document.createElement(type);
// ...设置标签属性省略
// 处理子元素
if (children) {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
el.textContent = children;
} else if (shapeFlag & ShapeFlags.ARRAY_CHIDLREN) {
// children数组,递归处理
for (const child of children) {
let n2 = child;
// h('div', {}, ['1', '2']) 针对文本,先生成文本类型vnode
if (!n2._v_isVnode) {
n2 = createVnode(TEXT, null, child)
}
// 递归挂载
patch(null, n2, el);
}
}
}
// 元素插入容器
contanier.insertBefore(el, null)
}
// 处理文本
function processText(n1, n2, contanier) {
contanier.insertBefore(document.createTextNode(n2.children), null)
}
processComponent方法
- createComponentInstance:根据vnode创建组件实例
- steupComponent:执行vnode传入的setup,对setup返回的对象或者函数挂载在instance上
- setupRenderEffect:在effect函数中执行render函数,让render函数和effect函数产生关联,方便数据更新时再次触发
const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key);
// 处理组件
const processComponent = (n1, n2, contanier) => {
mountComponent(n2, contanier)
}
const mountComponent = (initialVnode, contanier) => {
const instance = initialVnode.component = createComponentInstance(initialVnode)
steupComponent(instance);
setupRenderEffect(instance, contanier);
}
// 创建组件实例
const createComponentInstance = (vnode) => {
const instance = {
vnode,
type: vnode.type,
props: {},
attrs: {},
setupState: {},
ctx: {}, // 代理数据
proxy: {},
render: null,
isMounted: false
}
instance.ctx = { _: instance }
return instance;
}
// 处理组件中setup数据
const steupComponent = (instance) => {
// 代理,快速访问到响应式数据 proxy.props.name ===> proxy.name
// setupState是setup函数执行返回的对象
instance.proxy = new Proxy(instance.ctx, {
get({ _: instance }, key) {
const { props, setupState } = instance;
if (hasOwn(props, key)) {
return props[key]
} else if (hasOwn(setupState, key)) {
return setupState[key]
}
},
set(target, key, value) {
const { props, setupState } = target._;
if (hasOwn(props, key)) {
props[key] = value;
} else if (hasOwn(setupState, key)) {
setupState[key] = value;
}
}
})
const { props, children, shapeFlag, type, attrs } = instance.vnode;
instance.props = props;
instance.children = children;
// 组件
if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
const { setup, render } = type; // 组件定义的render函数
// setup存在将会被执行
if (setup) {
const res = setup(props, { attrs });
// ... 省略template模板解析生成的render函数
// res对象赋值给setupState,函数就赋值给render
instance[isObject(res) ? 'setupState' : 'render'] = setupRes;
}
// setup未返回函数,取组件的render函数
instance.render = instance.render || render;
}
const setupRenderEffect = (instance, contanier) => {
effect(() => {
if (!instance.isMounted) {
const proxy = instance.proxy;
// render执行,取到一个vnode
const subTree = instance.render.call(proxy, proxy);
// 继续挂载vnode
patch(null, subTree, contanier)
instance.isMounted = true
}
})
}
}
最后
这里主要就是对于Vue3从createApp函数,将App组件挂载到页面的简单实现过程,这里只是对创建dom到页面,对于更新的diff场景未涉及,将在下一遍中更新。对Vue3响应式感兴趣的朋友看上一篇文章。
转载自:https://juejin.cn/post/7258850748149268536