16、实现初始化component功能
createApp
的开始是先将传入的App
组件转换成虚拟节点,调用createVnode
将根组件转换成为vnode
。
- 在一开始要搞清楚
vnode
的两种形态,当type
是对象的时候表示要patch
的是一个组件。 - 当
vnode
的type
是string
的时候表示要patch
的是元素,后面我们会添加ShapeFlag
来区分vnode
的类型。
这个时候是组件。createApp(App)
js
const App = {
// render 函数返回的vnode 是一个元素类型的,
// 如果其返回的是 组件类型的虚拟节点,同样也会走到path(sunTree)里面
render() {
return h('div', `${this.msg}`)
},
setup() {
return {
msg: 'hello world'
}
}
}
export default App // 对应到代码里面就是 App 是一个组件
当调用完instance.render()
的时候,返回的虚拟节点,同样也交给path(subTree)
函数去处理。
17、使用rollup打包库
bash
yarn add @rollup/plugin-typescript rollup tslib -D
json
"scripts": {
"build": "rollup -c rollup.config.js"
},
rollup.config.js
js
import typescript from '@rollup/plugin-typescript';
import pkg from './package.json'
export default {
input: './src/index.ts',
output: [
// cjs => common.js
// esm
{
format: 'cjs',
file: pkg.main
},
{
format: 'es',
file: pkg.module
}
],
plugins: [
typescript()
]
}
package.json
json
{
"main": "./lib/vue3-mini-vue.cjs.js", // common.js
"module": "./lib/vue3-mini-vue.esm.js", // esm
}
patch 的的时候,遇到的 vnode 的两周形态,组件和元素分别是什么样子的?如下图。
18、实现初始化 element 主流程
js
function patch(vnode, container) {
// 需要判断当前的 vnode 是不是 element
// 如果是一个 element 的话就应该处理 element
// 如何区分是 组件类型还是 element 类型??
if (typeof vnode.type === "string") {
processElement(vnode, container);
} else {
// 处理组件
processComponent(vnode, container);
}
}
// 处理元素
function processElement(vnode: any, container: any) {
mountElement(vnode, container);
}
// 挂载元素
function mountElement(vnode: any, container: any) {
const el = document.createElement(vnode.type);
// vnode.children 可能是 string 也可能是数组
const { children } = vnode;
if (typeof children === "string") {
el.textContent = children;
} else if (Array.isArray(children)) {
// 用 el 作为 vode 孩子的父节点去创建 DOM 元素
mountChildren(vnode, el);
}
// props
const { props } = vnode;
for (let key in props) {
let val = props[key];
el.setAttribute(key, val);
}
container.append(el);
}
// 挂载孩子节点
function mountChildren(vnode: any, container: any) {
// 如果孩子是数组就,进行递归的调用,往刚才创建的 根节点下面添加新的元素
vnode.children.forEach((v) => {
patch(v, container);
});
}
子组件渲染一个字符串
子组件渲染一个数组
js
const App = {
render() {
return h('div', {
id: 'root',
class: ['red', 'hard']
}, [
h('p', {}, 'A'),
h('p', {}, 'B'),
h('p', {}, 'C'),
])
},
setup() {
return {
msg: 'hello world'
}
}
}
export default App
19、实现组件代理对象
现在我们要实现的是在 render
函数里面可以访问到setup
函数返回的state
数据通过this.xxx
的形式。
如下:
js
const App = {
render() {
return h('div', {}, this.msg) // 这里的 this 是从哪里来的呢?想一想
},
setup() {
return {
msg: 'hello world'
}
}
}
代码实现:
实现效果
实现$el
js
window.self = null
const App = {
render() {
window.self = this // 这里
return h('div', {}, this.msg)
},
setup() {
return {
msg: 'hello world -'
}
}
}
下面来实现 获取$el
,获取当前 组件真实的DOM
-
就是把根元素真实的
DOM
赋值给组件实例的vnode
的el
,然后在组件代理对象获取的那块劫持对应的key
,从instance
上面取出对应的数据即可实现。 -
我们给组件
instance
上添加一个proxy
属性来进行代理,并作为render
函数执行的this
传递进去。
20、实现shapeFlags
shapeFlag
是什么?他就是用描述虚拟节点的类型,
- 可以对虚拟节点进行分类
- 位运算的修改(|)和查找(&)
给vnode
添加shapeFlag
属性, 用来区分虚拟节点的类型。
shared/shapeFlag.ts
vnode.ts
renderer.ts
21、实现注册事件功能
- 在处理
props
的时候处理特殊的key
(含有on开头的key)
22、实现props功能
- 在组件实例上添加
props
属性
- 组件代理对象的时候,如果
setupState
里面没有的话,就继续看props
里面有没有,有的话就从props
里面取值了。
- props 是不可以被修改的
23、实现emit
- 构造两个组件, 在子组件里面
emit
出去一个事件,然后在父组件里面on
。 - 在
setup
函数调用的时候需要传递一个对象。 - 找到
props
里面对应的onxxx
开头的函数,然后调用它。 - 支持
emit
传递参数 - 支持
emit('add-foo')
的形式调用props
上面的函数。
测试代码
js
import { h } from "../../lib/vue3-mini-vue.esm.js"
import Foo from "./Foo.js"
const App = {
setup(props) {
return {
}
},
render() {
return h('div', {}, [
h('div', {}, 'App组件'),
h(Foo, {
onAdd(a, b) {
console.log('onAdd触发了', a, b)
},
"onAddFoo"() {
console.log('on-add-foo触发了')
}
})
])
}
}
export default App
Foo 组件
js
import { h } from "../../lib/vue3-mini-vue.esm.js"
const Foo = {
setup(props, { emit }) {
const handleClick = () => {
console.log('emit add')
emit('add', 1, 2)
emit('add-foo')
}
return {
handleClick
}
},
render() {
return h('button', {
onClick: () => {
this.handleClick()
}
}, 'emit')
}
}
export default Foo
24、实现slot
- 需要用
this.$slots
获取到组件vnode
的children
然后渲染到组件里面。 - 指定元素渲染的位置
- 我们就要获取到渲染的元素,和获取到要渲染的位置。既"具名插槽"将
childeren
设计成一个对象。 - 实现作用域插槽, 需要在
slot
插槽上面访问到组件内部的变量, 这个时候就需要给虚拟节点外面包一层函数,用来接受内部的参数来当做props
使用。
代码实现
创建vnode
的时候添加 slot
的 shapeFlag
标识
vnode.ts
js
export function createVnode(type, props?, children?) {
const vnode = {
type,
props,
children,
shapeFlag: getShapeFlag(type),
el: null,
};
......
// 如果是有状态组件 并且 children 是 object 那么就是 slot children
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
if (typeof children === "object") {
vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN;
}
}
return vnode;
}
然后给组件实例上添加一个slot
的字段
component.ts
ts
import { initSlots } from "./componentSlots";
// 创建组件实例
export function createComponentInstance(vnode) {
const component = {
...
slots: {},
...
};
return component;
}
// 初始化组件
export function setupComponent(instance) {
// initProps
initProps(instance, instance.vnode.props);
initSlots(instance, instance.vnode.children);
...
}
初始化 slot
ts
import { ShapeFlags } from "../shared/shapeFlag";
export function initSlots(instance, children) {
// 把 slots 放到对象里面
const slots = {};
const { vnode } = instance;
// 如果 vnode 的形状里面包含 slot_children,那么
if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
for (const key in children) {
const value = children[key];
slots[key] = (props) => normalizeSlotValue(value(props));
}
instance.slots = slots;
}
}
function normalizeSlotValue(value) {
return Array.isArray(value) ? value : [value];
}
componentPublicInstance.ts
js
const publicPropertiesMap = {
$el: (i) => i.vnode.el,
$slots: (i) => i.slots, // 从组件实例上获取 传递给组件的 slot 数据
};
helpers/renderSlots.ts 从slots
中读取对应 name
的内容创建出虚拟节点,在子组件中渲染出来。
ts
import { createVnode } from "../vnode";
export function renderSlots(slots, name) {
console.log(slots);
const slot = slots[name];
if (slot) {
return createVnode("div", {}, slot);
}
}
测试代码
app.js
js
import { h } from "../../lib/vue3-mini-vue.esm.js";
import { Foo } from "./Foo.js";
const App = {
setup() {
return {};
},
render() {
const app = h("div", {}, "App");
return h("div", {}, [
app,
// 把组件的孩子渲染在 组件里面
h(
Foo,
{},
{
header: ({ age }) => h("p", {}, "header" + age),
footer: () => h("p", {}, "footer"),
}
),
]);
},
};
export default App;
foo.js
js
import { h, renderSlots } from "../../lib/vue3-mini-vue.esm.js";
export const Foo = {
setup() {
return {};
},
render() {
const age = 18;
const foo = h("p", {}, "foo");
return h("div", {}, [
renderSlots(this.$slots, "header", { age }),
foo,
renderSlots(this.$slots, "footer"),
]);
},
};
渲染结果。
25、实现Fragment 和 Text类型节点
- Fragment 只需要渲染
children
里面的vnode
。 - 渲染文本是不需要新创建
DOM
元素的。
代码实现
添加两种特殊的 type
用来进一步区分 vnode
的大类型
vnode.ts
javascript
export const Fragment = Symbol("Fragment");
export const Text = Symbol("Text");
/**
* 创建文本的虚拟节点
* @param text
* @returns
*/
export function createTextVNode(text: string) {
return createVNode(Text, {}, text);
}
在patch的时候进行区分,根据不同的 type 处理不同的vnode
js
function patch(vnode, container) {
// 需要判断当前的 vnode 是不是 element
// 如果是一个 element 的话就应该处理 element
// 如何区分是 组件类型还是 element 类型??
const { type, shapeFlag } = vnode;
switch (type) {
case Fragment:
processFragment(vnode, container);
break;
case Text:
processText(vnode, container);
break;
default:
.......
}
}
/**
* 其实就是只把 children 渲染出来就行
* @param vnode
* @param container
*/
function processFragment(vnode, container) {
mountChildren(vnode, container);
}
/**
* 根据文本虚拟节点创建真实的文本节点
* @param vnode
* @param container
*/
function processText(vnode, container) {
// 取出文本内容
const { children } = vnode;
const textNode = (vnode.el = document.createTextNode(children));
container.append(textNode);
}
测试代码 app.js
js
import { createTextVNode, h } from "../../lib/vue3-mini-vue.esm.js";
import { Foo } from "./Foo.js";
const App = {
setup() {
return {};
},
render() {
const app = h("div", {}, "App");
return h("div", {}, [
app,
// 把组件的孩子渲染在 组件里面
h(
Foo,
{},
{
header: ({ age }) => [
h("p", {}, "header" + age),
createTextVNode("我是文本节点!!!!"),
],
footer: () => h("p", {}, "footer"),
}
),
]);
},
};
export default App;
foo.js
js
import { h, renderSlots } from "../../lib/vue3-mini-vue.esm.js";
export const Foo = {
setup() {
return {};
},
render() {
const age = 18;
const foo = h("p", {}, "foo");
return h("div", {}, [
renderSlots(this.$slots, "header", { age }),
foo,
renderSlots(this.$slots, "footer"),
]);
},
};