vue3源码学习(5)--runtime-core(1):初始化

834 阅读7分钟

Vue3源码学习(5) -- runtime-core(1):初始化

前言

之前已经实现了vue3最基本的数据响应式的happy path,接下来就来实现vue3源码中的另一大模块runtime-core模块。

runtime-core

runtime-core顾名思义就是运行的核心流程,其中包括初始化流程和更新流程,其中初始化流程又可以拆分为组件(component)初始化元素(element)初始化

通过这一模块的学习,我们可以理解一个component组件如何渲染成一个真实的DOM元素

下面为组件渲染主流程

image.png

本篇主要学习 component组件的初始化流程

Component初始化的主流程

happy path

目标:实现将一个组件转换为真实dom渲染到浏览器上

在实现Component初始化主流程之前,首先在项目目录下创建example文件夹,用于放置相关的测试用例,在创建一个happy path文件夹,用来放置组件初始化相关的测试代码,index.htmlapp.jsmain.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'
    }
  }
}

主要的实现部分流程如图所示

image.png

image.png

下面针对流程图中各各个阶段进行实现

实现createApp函数

在Vue3中,createApp用于创建一个实例对象,每个应用都是通过调用createApp函数创建一个新的实例开始的:


import { createApp } from 'vue'

const app = createApp({
    /** 选项*/
})

实例暴露一些方法,并允许链式调用


 app.component('SearchInput', SearchInputComponent)
  .directive('focus', FocusDirective)
  .use(LocalePlugin)

由此可得,createApp接受一个跟组件对象作为参数,返回一个包含componentdirectivemountuse等方法的对象。

// 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函数,对当前传入的组件的进行实例化,之后propsslotsemit等都会挂载到该实例对象上

  • 调用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函数,主要用于初始化propsslots,以及调用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 函数获取 VNodeconst subTree = instance.render()
    
    //递归调用patch方法处理vnode树
    patch(subTree,container)
}

此时,初始化组件就已经完成。接下来patch函数就会将传入的subTreeelement渲染成真实的DOM元素

Element初始化

本章继续学习runtime-core模块,上议长学习了component的初始化,将一个组件的vnode转变为element的vnode,递归调用patch函数。初始化element。后续流程图如下所示:

image.png

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

vnodechildren 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

未完待续