【mini-vue】-【runtime】- 实现初始化component

86 阅读6分钟

实现初始化component

<script src="./main.js" type="module"></script>

module模块不会与其他script文件中的变量发生冲突

首先创建一个index.html

image.png

这个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可以说是这个应用实例所需要的一些配置项。

image.png

// 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函数

image.png

那么看看createVnode函数

image.png

目前来说啥也没干

从程序入口开始

那么下一步是干啥??

可以回过头看看,程序的入口为main.js,我们在main.js中写入了

const rootContainer = document.querySelector('#app')
createApp(App).mount(rootContainer)

OK,createApp接收了一个根组件实例后,调用了mount函数,在mount函数里首先将根组件转换成了VNode对象。然后,调用了render函数,准备将Vnode对象 挂在到外面的div id='app'上。

看看mount转换的结果

image.png 由于createVnode接收三个参数,这里只传递了一个参数,因此转换后的根组件的VNode只有type属性。

那么接下来看看render都做了些啥~

image.png 好的,我们看到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)
}

image.png

通过打印我们可以看到,h函数已经将所有的(元素?)都渲染成了VNode,因此我们继续递归调用patch实现节点挂载。

patch处理Element

然鹅,现在的patch函数是不健全的。 因为当我们需要处理根组件的children里的文本节点的时候,进入了createComponentInstance之后是会进入setupRenderEffect去调用自身对象身上的render函数的,但是p节点身上哪来的render函数?所以会报错undefined。

因此我们需要在patch里再添加这样的逻辑,去处理我们的element节点。

image.png

为什么组件的type是对象,而Dom节点的type是字符串呢?

image.png

因为我们在创建组件的VNode的时候,是把整个App对象直接传给了createVNode函数,所以它的Vnode的type其实就是它自己。

image.png

而当我们通过h函数创建DOM节点的VNode的时候,是将createVNode所需要的3个参数全部传进去的,所以Dom节点的type就是h函数的第一个参数,也就是字符串。

image.png

image.png

完善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问题

image.png

image.png

查看控制台发现这里的msg为undefined值,应该是调用setup的时候,没有给setup绑定this指向,导致setup的this只想了全局对象window。(只是猜测)