实现初始化component
<script src="./main.js" type="module"></script>
module模块不会与其他script文件中的变量发生冲突
首先创建一个index.html
这个div id='app',是我们要挂载vue实例的容器 因此我们需要在main.js中,创造出一个的vue实例对象,将这个对象挂载到容器内部
// main.js
import { createApp } from '../../lib/even-mini-vue.esm.js'
import { App } from './App.js'
const rootContainer = document.querySelector('#app')
createApp(App).mount(rootContainer)
首先需要一个createApp函数,创建出一个实例对象,再在这个对象身上定义一个mount方法,用于挂载对象。那么容器可以通过document.querySelector('#app')直接获取到。
createApp函数的定义可以参考vue官方文档,可以看到createApp创建了一个vue应用实例,传入的APP可以说是这个应用实例所需要的一些配置项。
// creatApp.js
import { render } from "./renderer"
import { createVNode } from "./vnode"
export function createApp(rootComponent) {
return {
mount(rootContainer) {
// component -> vnode
// vnode patch render
const vnode = createVNode(rootComponent) // createVnode接收三个参数,这里只传递了一个参数
render(vnode, rootContainer)
}
}
}
可以看到createApp接受了一个根组件对象,并返回了一个mount函数,在mount函数中调用了createVNode方法将根组件对象 转换成 虚拟节点VNode,最后通过render函数将根组件的VNode挂载到外部的容器上。
// APP.js
import { h } from '../../lib/even-mini-vue.esm.js'
export const App = {
render() {
return h('div', {
id: 'root',
class: ['success']
}, [
h('p', { class: 'blue' }, 'even'),
h('p', { class: 'red' }, 'what is this'),
h('p', { class: 'yellow' }, 'this is mini vue'),
])
},
setup() {
return {
msg: 'hello world'
}
}
}
App.js中可以看到这个根组件实例身上有一个render函数,和setup函数。 render返回了h函数的返回值,h函数实际上是返回了一个VNode。 下图是h函数
那么看看createVnode函数
目前来说啥也没干
从程序入口开始
那么下一步是干啥??
可以回过头看看,程序的入口为main.js,我们在main.js中写入了
const rootContainer = document.querySelector('#app')
createApp(App).mount(rootContainer)
OK,createApp接收了一个根组件实例后,调用了mount函数,在mount函数里首先将根组件转换成了VNode对象。然后,调用了render函数,准备将Vnode对象 挂在到外面的div id='app'上。
看看mount转换的结果
由于createVnode接收三个参数,这里只传递了一个参数,因此转换后的根组件的VNode只有type属性。
那么接下来看看render都做了些啥~
好的,我们看到render把脏活累活都交给patch去做了。
看看patch
// src/runtime-core/renderer.ts
/**
* @description 能够处理 component 类型和 dom element 类型
*
* component 类型会递归调用 patch 继续处理
* element 类型则会进行渲染
*/
export function patch(vnode, container) {
// 处理 component 类型
processComponent(vnode, container);
}
function processComponent(vnode: any, container: any) {
mountComponent(vnode, container);
}
function mountComponent(vnode: any, container) {
// 根据 vnode 创建组件实例
const instance = createComponentInstance(vnode);
// setup 组件实例
setupComponent(instance);
setupRenderEffect(instance, container);
}
function setupRenderEffect(instance, container) {
const subTree = instance.render();
// subTree 可能是 Component 类型也可能是 Element 类型
// 调用 patch 去处理 subTree
// Element 类型则直接挂载
patch(subTree, container);
}
可以看到我们目前只实现了patch处理组件的功能。
由于第一次渲染根组件对象,需要创建组件实例。
export function createComponentInstance(vnode: any) {
const component = {
vnode,
type: vnode.type
}
console.log('component', component)
return component
}
可以看到我们把Vnode身上的type属性,又抽离了出来,放到了组件的type属性上。 接下来再通过 setupComponent(instance); 和 setupRenderEffect(instance, container); 去初始化组件的一些状态(数据)。也就是让组件状态化。
setupComponent 从组件的type属性上,取出setup函数执行,然后再取出render函数添加到给具有setupState状态的实例身上。
// Component.js
export function setupComponent(instance) {
// TODO
// initProps()
// initSlots()
setupStatefulComponent(instance)
}
function setupStatefulComponent(instance: any) {
// 这里的component为什么等于instance.type??
const component = instance.type
const { setup } = component
if (setup) {
const setupResult = setup()
// setupResult可能是function 也可能是object
// 如果是function, 那么将它作为组件的render函数
// 如果是object,那么将它注入到组件的上下文中
handleSetupResult(instance, setupResult)
}
}
function handleSetupResult(instance, setupResult: any) {
// TODO
// 目前只处理了,setupResult是ojbect的情况
if (typeof setupResult === 'object') {
instance.setupState = setupResult
}
finishComponentSetup(instance)
}
function finishComponentSetup(instance) {
const Component = instance.type
instance.render = Component.render
}
到目前为止,我们已经创建了根组件实例,并且调用了根组件的setup函数让其stateful了。 接下来就是调用根组件的render函数,实现挂载渲染了。
export function mountComponent(vnode: any, container) {
// 根据vnode创建组件实例
const instance = createComponentInstance(vnode)
// 组件实例创建完成之后,需要初始化组件的一些状态
setupComponent(instance)
setupRenderEffect(instance, container)
}
function setupRenderEffect(instance, container) {
const subTree = instance.render()
console.log('renderer--subTree', subTree)
// subTree 可能是 Component 类型也可能是 Element 类型
// 调用 patch 去处理 subTree
// Element 类型则直接挂载
patch(subTree, container)
}
通过打印我们可以看到,h函数已经将所有的(元素?)都渲染成了VNode,因此我们继续递归调用patch实现节点挂载。
patch处理Element
然鹅,现在的patch函数是不健全的。 因为当我们需要处理根组件的children里的文本节点的时候,进入了createComponentInstance之后是会进入setupRenderEffect去调用自身对象身上的render函数的,但是p节点身上哪来的render函数?所以会报错undefined。
因此我们需要在patch里再添加这样的逻辑,去处理我们的element节点。
为什么组件的type是对象,而Dom节点的type是字符串呢?
因为我们在创建组件的VNode的时候,是把整个App对象直接传给了createVNode函数,所以它的Vnode的type其实就是它自己。
而当我们通过h函数创建DOM节点的VNode的时候,是将createVNode所需要的3个参数全部传进去的,所以Dom节点的type就是h函数的第一个参数,也就是字符串。
完善patch函数
补上patch函数对Element的处理
export function patch(vnode, container) {
// patch 去处理组件
console.log('render--patch--fistline', vnode)
const { type } = vnode
if (typeof type === 'string') {
processElement(vnode, container)
} else if (isObject(type)) {
processComponent(vnode, container)
}
}
export function processElement(vnode, container) {
mountElement(vnode, container)
}
export function mountElement(vnode, container) {
// 根据节点的属性给自己创建对应的元素
const el = document.createElement(vnode.type)
const { children } = vnode
// 如果当前vnode有children,判断children的类型
// 如果children为数组类型,那么递归用patch去处理
if (typeof children === 'string') {
el.textContent = children
} else if (Array.isArray(children)) {
mountChildren(children, el)
}
// 当前vnode处理完子节点之后,再处理自己的props属性
// props
const { props } = vnode
for (const [key, value] of Object.entries(props)) {
el.setAttribute(key, value)
}
// props,children处理完之后,就是挂载啦
// 往容器里面添加自己这个元素
container.append(el)
}
function mountChildren(children, el) {
children.forEach((item) => {
patch(item, el)
})
}
目前暂时over~,因为还有个this问题没有解决
this问题
查看控制台发现这里的msg为undefined值,应该是调用setup的时候,没有给setup绑定this指向,导致setup的this只想了全局对象window。(只是猜测)