从这节开始,reactivity暂告一段落,进入runtime-core部分。这部分负责的是组件渲染相关内容,即当我写好app.js文件后,如何挂载component使其显示在页面上,及其相关的问题。
组件的结构
一个典型的app.js的结构如下:
import { h } from '../../lib/guide-mini-vue.esm.js';
export const App = {
render() {
return h(
'div',
{
id: 'root',
class: 'red',
onClick: () => console.log('onclick'),
onMousedown: () => console.log('onmousedown')
},
'hi, mini-vue'
);
},
setup() {
return {
msg: 'mini-vue'
};
}
};
暂时不用理会h函数,只需要关注:组件是一个含有render()和setup()方法的对象,其中render处理vnode的生成,setup会返回一个对象或函数,作为该组件的状态值。
vnode
vnode大体上的数据结构为:
{
type: ...
props: ...
children: ...
}
创建vnode,需要使用createVNode函数,实现很简单,把传入的属性全部塞入一个对象,将其返回。
export function createVNode(type, props?, children?) {
const vnode = {
type,
props,
children,
};
return vnode;
}
至于上文提到的h函数,则是为了让用户使用,将createVNode封装并暴露出去的函数:
export function h(type, props?, children?) {
return createVNode(type, props, children);
}
暴露:在index.ts中将h函数export,该文件在rollup中配置为入口,这样打包后h函数就能出现在lib下的.js文件中,可以被用户使用。
render element 的流程
由main.js中的createApp(App).mount(rootContainer);知,起点是createApp函数,然后调用mount方法,流程是先生成vnode,然后调用render。
export function createApp(rootComponent) {
return {
mount(rootContainer) {
const vnode = createVNode(rootComponent);
render(vnode, rootContainer);
},
};
}
此处的rootComponent就是上面的App,所以生成的vnode结构如下:
{
type: { render() {...}, setup() {...} },
props: undefined,
children: undefined
}
这种vnode我们不妨称为“component vnode”,特点是type属性值为含有render和setup方法的对象,props就是组件的props,children用于插槽。
这边也一并介绍另一种vnode,即“element vnode”,这种就是比较常见的vnode,可以拿它渲染出真实dom。
{
type: 'div',
props: { class: 'red' },
children: 'hello world' // 或者是数组
}
之后的函数调用链比较长,先总结一下:
render -> patch -> processElement -> mountElement -> mountChildren
(挂载真实dom) (孩子的递归挂载)
/
processComponent -> mountComponent -> createComponentInstance
(创建instance,会传入下面的函数)
setupComponent -> setupStatefulComponent -> handleSetupResult -> finishComponentSetup
(setup为对象或函数,分类讨论) (从instance.type取出组件app,把其render赋值给instance.render)
setupRenderEffect -> patch
(这回处理element,进入processElement)
patch会对vnode进行判断,从而进入不同的分支:
function patch(vnode, container) {
if (typeof vnode.type === "string") {
processElement(vnode, container);
} else if (isObject(vnode.type)) {
processComponent(vnode, container);
}
}
这里面提到了instance,它又是一层封装,结构如下:
{
vnode,
type: vnode.type, // component vnode的type,即组件app
setupState: {}, // 用于组件代理
...
};
传入App到页面渲染出dom,流程大概是:先从patch进入processComponent分支,完成一系列初始化操作,构造了instance,最后执行render得到element vnode,传入patch,接着进入processElement分支,将其渲染为真实dom。
function processElement(vnode: any, container: any) {
mountElement(vnode, container);
}
function mountElement(vnode: any, container: any) {
const { type, props, children } = vnode;
// 将el保存到element node上
const el = (vnode.el = document.createElement(vnode.type));
// props
const isOn = (key) => /^on[A-Z]/.test(key);
for (const key in props) {
const value = props[key];
if (isOn(key)) {
const event = key.slice(2).toLocaleLowerCase();
el.addEventListener(event, value);
} else {
el.setAttribute(key, value);
}
}
// children
if (typeof children === "string") {
el.textContent = children;
} else if (Array.isArray(children)) {
mountChildren(vnode, el);
}
container.append(el);
}
function mountChildren(vnode, container) {
vnode.children.forEach((v) => {
patch(v, container);
});
}
组件代理
组件代理,指的是可以在组件内通过this访问到setup暴露出去的属性,或者是一些特殊变量,比如this.$el访问根结点的dom。
上文提及instance的数据结构时,提及了其中的setupState属性用于组件代理。setupState是在这里初始化的:
function setupStatefulComponent(instance: any) {
const Component = instance.type;
// 也用于组件代理
instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers);
const { setup } = Component;
if (setup) {
// 得到setupResult,可以是对象或函数
const setupResult = setup();
handleSetupResult(instance, setupResult);
}
}
function handleSetupResult(instance, setupResult: any) {
// 暂时只处理对象的情况
if (typeof setupResult === 'object') {
// 这里
instance.setupState = setupResult;
}
finishComponentSetup(instance);
}
const publicPropertiesMap = {
// 从component类型的vnode获取el
$el: (i) => i.vnode.el
};
const PublicInstanceProxyHandlers = {
// 把对象的_解构成instance变量
get({ _: instance }, key) {
const { setupState } = instance;
if (key in setupState) {
return setupState[key];
}
// 避免写过多if-else,要增加逻辑只用修改publicPropertiesMap
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
return publicGetter(instance);
}
}
};
并且可以看到,instance上还有proxy属性,将proxy对象作为this调用render,这样this.msg相当于proxy.msg,就会触发proxy的getter,进而从setupState中拿到属性值。
function setupRenderEffect(instance: any, initialVNode, container) {
const { proxy } = instance;
// 将proxy作为this传入render
const subTree = instance.render.call(proxy);
patch(subTree, container);
// initialVNode是component类型的vnode,需要从element类型的vnode获取
initialVNode.el = subTree.el;
}
注意到最下面设置了component vnode的el属性。由于instance其实是对component vnode的封装,并没有保存真实的dom结点,当调用this.$el去获取dom时,从instance的setupState中查找,显然是得不到dom的。而subTree是element vnode,可以获取el,所以最后一行的赋值操作必不可少,这样在publicPropertiesMap中,就能通过instance.vnode.el拿到dom了。
修改App.js:
import { h } from '../../lib/guide-mini-vue.esm.js';
window.self = null; // 在控制台直接打印self
export const App = {
render() {
window.self = this;
return h(
'div',
{
id: 'root',
class: 'red'
},
'hi, ' + this.msg
);
},
setup() {
return {
msg: 'mini-vue'
};
}
};
shapeFlags
前面在判断组件类型,以及组件的孩子是字符串或数组时,逻辑分散,而且每次比较都是“对象的某个属性 === 某个值”,性能也比较低,因此有必要使用位运算优化。
创建枚举,给vnode添加shapeFlag属性,使用|可以设置shapeFlag某一位为1,然后用&判断某一位是否为1。
初始化:
const enum ShapeFlags {
ELEMENT = 1, // 0001
STATEFUL_COMPONENT = 1 << 1, // 0010
TEXT_CHILDREN = 1 << 2, // 0100
ARRAY_CHILDREN = 1 << 3, // 1000
}
function createVNode(type, props?, children?) {
const vnode = {
type,
props,
children,
shapeFlag: getShapeFlags(type),
el: null
};
if (typeof children === 'string') {
vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN;
} else if (Array.isArray(children)) {
vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.ARRAY_CHILDREN;
}
return vnode;
}
function getShapeFlags(type) {
return typeof type === 'string'
? ShapeFlags.ELEMENT
: ShapeFlags.STATEFUL_COMPONENT;
}
判断是否为element vnode,以及孩子是text/array的逻辑修改如下:
function patch(vnode, container) {
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
processElement(vnode, container);
} else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
processComponent(vnode, container);
}
}
function mountElement(vnode, container) {
// ...
// children
if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
el.textContent = children;
} else if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(vnode, el);
}
// ...
}