Vue3源码学习(5) -- runtime-core(1):初始化
前言
之前已经实现了vue3最基本的数据响应式的happy path,接下来就来实现vue3源码中的另一大模块runtime-core模块。
runtime-core
runtime-core顾名思义就是运行的核心流程,其中包括初始化流程和更新流程,其中初始化流程又可以拆分为组件(component)初始化和元素(element)初始化。
通过这一模块的学习,我们可以理解一个component组件如何渲染成一个真实的DOM元素
下面为组件渲染主流程
本篇主要学习 component组件的初始化流程
Component初始化的主流程
happy path
目标:实现将一个组件转换为真实dom渲染到浏览器上
在实现Component初始化主流程之前,首先在项目目录下创建example
文件夹,用于放置相关的测试用例,在创建一个happy path
文件夹,用来放置组件初始化相关的测试代码,index.html
,app.js
,main.js
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- 根容器 -->
<div id="app"></div>
<script src="main.js" type="module"></script>
</body>
</html>
/* main.js */
import { App } from './App'
const rootContainer = document.getElementById("app");
//console.log(rootContainer);
createApp(App).mount(rootContainer);
/* App.js */
// 根组件选项对象
export const App = {
// render 函数
render() {
// 在 render 函数中通过 this 获取 setup 返回对象的 property
return h('div', {}, 'hello, ' + this.name)
},
// composition API
setup() {
// 返回一个对象
return {
name: 'mini-vue3'
}
}
}
主要的实现部分流程如图所示
下面针对流程图中各各个阶段进行实现
实现createApp函数
在Vue3中,createApp
用于创建一个实例对象,每个应用都是通过调用createApp
函数创建一个新的实例开始的:
import { createApp } from 'vue'
const app = createApp({
/** 选项*/
})
实例暴露一些方法,并允许链式调用
app.component('SearchInput', SearchInputComponent)
.directive('focus', FocusDirective)
.use(LocalePlugin)
由此可得,createApp
接受一个跟组件对象作为参数,返回一个包含component
、directive
、mount
、use
等方法的对象。
// runtime-core/createApp.ts
//用于创建应用实例
export function createApp(rootComponet){
return {
mount(){},
component(){},
use(){}
directive(){}
}
}
本节只实现component的happy path
,,所以只实现mount
方法。
实现mount方法
mount
方法用于将实例挂载到一个DOM元素中,所以接受一个node节点,把Dnode节点转换为Vnode;然后通过render函数进行渲染
// runtime-core/createApp.ts
export function createApp(rootComponet){
return {
mount(rootContainer){
// 将根组件转换为 VNode
const vnode = createVnode(rootComponent)
//渲染
render(vnode,rootContainer)
},
}
}
createVnode函数以及h函数
createVnode函数作用就是创建Vnode
//runtime-core/vnode.ts
export function createVnode(type,props:any={},children :any = []){
const vnode = {
type, // HTML 标签名、组件
props, // 保存 attribute、prop 和事件的对象
children //
}
return vnode
}
}
h
函数是vue3
暴露给用户的,本质就是调用createVnode
//runtime-core/h.ts
import {createVndoe} from "./createVnode"
export function h(type, props: any = {}, children: any = []) {
return createVNode(type, props, children)
}
render函数
render函数就是渲染器,将虚拟DOM渲染成真实DOM。在render函数中调用了patch
函数。这样做的目的是为了后续进行递归处理
// runtime-core /renderer.ts
export function render(vnode,container){
patch(vnode,container)
}
patch函数
runtime-core模块最核心的部分就是patch
函数,patch
函数是用来处理接受的vnode
,首先对接受到的vnode
进行类型判断,判断是component组件类型还是element元素类型,如果是component组件则调用processComponent
函数,如果是 element元素则调用processElement
函数。
TODO这里主要实现的是组件初始化
,所以类型判断以及processElement
函数后面会实现。
//runtime-core/renderer.ts
function patch(vnode,container){
//TODO:根据Vndoe类型的不同调用不同的函数
precessComponent(vnode,container)
//TODO processElement
}
processComponent函数
processComponent函数对组件进行处理,包括三个部分:
-
调用
createComponentInstance
函数,对当前传入的组件的进行实例化,之后props
,slots
,emit
等都会挂载到该实例对象上 -
调用
setupComponent
设置组件状态 -
调用
setupRenderEffect
函数,执行传入组件的render函数完成组件初始化
// runtime-core/renderer.ts
function processComponent (vnode,container){
//一
cosnt instance = createComponentInstance(vnode)
//二
setupComponent(instance)
//三
setupRenderEffect(instance,container)
}
createComponentInstance函数
createComponentInstacne
函数用于创建组件实例对象
export function createComponentInstacne(vnode){
const component = {
vnode,
type:vnode.type,
setupState: {}
}
return component
}
setupComponent函数
setupComponent
函数,主要用于初始化props
,slots
,以及调用setupStatefulComponent
函数用于设置组件状态(我们通常写的Vue组件都是有状态的组件,而函数式组件就是没状态组件)。
// runtime-core/component.ts
export setupComponent (instance){
//TODO initProps
//TODO initSlots
setupStatefulComponent(instance);
}
setupStatefulComponent(instance){
const component = instance.type
const {setup} = component
if(setup){
const setupResutlt = setup()
handleSetupResult(instance,setupResult)
}
}
handleSetupResult函数
handleSetupResult
主要就是对setup的返回值进行判断,将setupResult
挂载到instance
上,然后在调用finishComponentSetup
函数。
// runtime-core/component.ts
function handleSetupResult(instance,setupResult){
if(isObject(setupResult)){
instance.setupResult = setupResult
}
finishComponentSetup(instance)
}
finishComponentSetup函数
而finishComponentSetup
主要就是将render函数赋值给instance。
finishComponentSetup(instance){
const component = instance.type
if(component.render){
instance.render = component.render
}
}
实现setupRenderEffect函数
setupRenderEffect
函数用于获取 VNode 树并递归地处理,在其中首先调用组件实例对象的render
函数获取 VNode 树,之后再调用patch
方法递归地处理 VNode 树
//runtime-core/renderer.ts
function setupRenderEffect(instance,container){
调用组件实例对象中 render 函数获取 VNode 树
const subTree = instance.render()
//递归调用patch方法处理vnode树
patch(subTree,container)
}
此时,初始化组件就已经完成。接下来patch
函数就会将传入的subTree
即element渲染成真实的DOM元素
Element初始化
本章继续学习runtime-core模块,上议长学习了component的初始化,将一个组件的vnode转变为element的vnode,递归调用patch
函数。初始化element。后续流程图如下所示:
element初始化
此时,我们需要更新一下patch
函数中的代码
patch函数
之前patch
函数中只存在一个逻辑——component初始化逻辑,现在我们需要进行的是Element的初始化。patch
函数需要根据vnode
来判断是Component类型还是Element类型。
那么在patch
方法中是如何判断 VNode 是 Component 还是 Element 的?
答案是:通过Vnode的type property类型来判断Vndoe的类型,若 VNode 的 type property 的值类型是 string 则 VNode 类型是 element类型,若是 object 则是 Component类型
组件就是一组DOM元素的封装 —— 《vuejs的设计与实现》
// runtime-core/renderer.ts
function patch(vnode,container){
//根据Vnode类型的不同调用不同的函数
if(typeof vnode.type === "string"){
processElement(vnode,container)
}else if(isObject(vnode.type)) { //isObject 自己封装的工具函数
processComponent(vnode,container)
}
processElement函数
processElement
函数会调用mountElement
函数
// runtime-core/renderer.ts
function processElement(vnode,container){
mountElement(vnode,container)
}
mountElement函数
在实现mountElement
函数之前,我们先了解一下如何向DOM元素中插入子元素。
假设向一个div
元素中加入一个p
元素,p
元素的attribute和prop保存在props
对象中,内容保存在变量content
中:
<div id="root"></div>
const props = {
id="p1",
class="child-1"
}
const content = "hello, mini-vue3"
//获取父元素
const root = document.querySelector("#root")
//创建子元素
const child = document.createElement("p")
//遍历props对象,将其中property添加到子元素上
for( const key in props){
const val = props[key]
child.setAttribute(key,val)
}
//将变量 content 的值赋值给子元素的 textContent property
child.textContent = content
//将子元素添加到父元素中
root.append(child)
参考以上操作实现mountElement
函数
// runtime-core/renderer.ts
function mountElement(vnode,container){
const {type , props, children} = vnode
//创建dom元素
const el = document.createElement(type)
//为dom元素el添加Attribute属性
for(const key in props){
const val = props[key]
el.setAttribute(key,val)
}
//判断children类型
//string类型,则将其赋值给 el 的 textContent property
if(typeof children === "string"){
el.textContent = children
}
//若 children 的类型是 Array,则调用 mountChildren 函数
else if(Array.isArray(children)){
mountChildren(children,el)
}
//将子元素添加到父元素中
constiner.append(el)
}
mountChildren函数
当vnode
的children property
为数组时,调用此函数。children property
为数组表示子元素仍未element类型的vnode,所以继续递归调用patch
函数
// runtime-core/renderer.ts
function mountChildren(childrenVnode,container){
children.forEach(child=>{
patch(child,container)
})
}
对实现的代码进行打包
通过上面的实现,我们已经简单的完成了初始化Component和ELment,而下面我们需要对这些代码进行打包。然后引用我们打包后的代码在浏览器进行查看。这里我们使用rollup
进行打包。
安装依赖
yarn add rollup @rollup/plugin-typescript tslib --dev
配置 rollup.config.js文件
import typescript from "@rollup/plugin-typescript";
export default {
input: "./src/index.ts",
output: [
{
format: "cjs",
file: "lib/mini-vue.cjs.js",
},
{
format: "es",
file: "lib/mini-vue.esm.js",
},
],
plugins: [typescript()],
};
在package.json添加build脚本
// package.json
"scripts":{
"test":"jest",
"build":" rollup -c rollup.config.js"
}
然后执行yarn build
即可打包我们的代码。根据实现目标的代码进行测试,会发现浏览器可以显示出hello mini-vue
.
END
到这里,我们实现了一个runtime-core的happy path
,接下来就是初始化过程中的一些补充和edge case
。