前言
上一篇博客vue3响应式原理的内容得到了大家的认可以及支持,在此非常感谢家人们,希望能为大家更多地输出自己所学的知识!
最近在学习vue3中runtime-core 运行时的源码,对vue3对组件以及dom元素初始化的流程有了全新的认知,今天为大家带来的是vue3如何对组件以及dom元素进行初始化, 先来看一下初始化流程图
这里的todo代表之后要处理的步骤,我们先把初始化的大体流程过一遍
初始化的核心
VNode
我们知道vue3对dom元素的处理是基于VNode(虚拟节点) ,所以当我们传入的组件以及元素也会转化成VNode,做后续处理,关于vue3为什么用VNode,这里就不展开讨论了,其他博主写的应该很清楚,我们这里只将如何实现
render
另外一个核心就是render函数,它可以渲染我们的虚拟dom,通过渲染函数h来生成虚拟dom, 在vue3中,有两种办法生成虚拟dom,一种是通过template模板,另外一种就是通过render函数,但是第一种底层也是将其转化为render函数来生成的,可见render函数的重要性
patch
pacth函数是来处理虚拟dom的,我们知道VNode是一种树形结构,一层一层的,最终VNode的形式肯定就是element元素了,我们最终要的是element元素,所以我们必须得考虑到递归的情况,这也是流程图中又反过来再次调用patch的原因
手写源码
我们先来初始化一下,创建一个example来实现一下最终效果
这个就是我们传入的根组件结构,也就是createAPP(App)传入的App
export const App = { render() { return h("div", { id: 'root', class: ["red", "layhead"] } , [h("p", { class: "red" }, "red"), h("p", { class: 'blue' }, "blue")]) }, setup() { return { msg: 'meng-vue' } } }
这里是我们最终完成的结构,要能够将元素渲染在页面上
const rootContainer = document.querySelector("#app") createApp(App).mount(rootContainer)
下面会分模块进行,依次实现createApp,render patch以及流程图上的功能
createApp()
我们在接受到App之后,要调用mount方法将其绑定到根容器中,并且将App根组件转化为虚拟节点
import { createVNode } from "./vnode" import { render } from "./render" export function createApp(rootComponent) { return { mount(rootContainer) { // 先转换成vNode // component -> VNode //所有components基于vNode操作 const vNode = createVNode(rootComponent) render(vNode, rootContainer) } } }
createVNode
我们要实现的VNode它可能没有属性,也可能没有children所以可选,我们这里的createVNode操作还是很简单的,我们这里的type可以看到就是上面传过来的App对象
export function createVNode(type, props?, children?) { return { type, props, children } }
h函数
import { createVNode } from "./vnode"; export function h(type, props, children){ return createVNode(type, props, children) }
render
在render中我们要处理的步骤就多起来了,在render内部我们会将处理的逻辑交给patch,在pacth中,我们可以看到流程图对VNode的类型做处理,这里的类型就是是一个组件,还是一个element元素,对这两种,我们有不同的初始化方案,基于什么判断呢,我们要根据type判断,我们知道当传入的是组件时是一个对象类型,在它内部的render函数处理之后,最终是一个element元素,它的type就是一个string了,后面会为大家证明这个,我们这里先写逻辑
export function render(vNode, container) { //patch patch(vNode, container) } function patch(vNode, container) { //处理组件 判断是不是element类型 //是element走element逻辑 //可以log一下vNode看看类型 是object->组件 是string -> element console.log(vNode.type); if (typeof vNode.type === 'string') { processElement(vNode, container) } else if (isObject(vNode.type)) { processComponent(vNode, container) } }
在我们判断VNode类型之后会出现不同的解决方案,也就是processElement来处理element,processComponent来处理component
接下来先来实现processComponent
processComponent
function processComponent(vNode, container) { //init 以及unpate //init mountComponent(vNode, container) } }
我们这里只做初始化处理init,所以在processComponent函数内,它会将component初始化,也就交给了mountComponent函数来处理
看流程图在mountComponent函数内部它做了三件事
- 将组件实例化这样组件身上的属性以及后续的props,slots我们都可以获取到
- 对setup的处理,在此阶段我们初始化props slots,以及处理setup的返回值,最后设置好返回之后的一个render函数
- 对上一步处理之后的VNdoe进行处理,得到subTree进而再次调用pacth方法进行递归,以获取最终的element,做进一步的处理
function mountComponent(vNode, container) { const instance = createComponentInstance(vNode) setupComponentInstance(instance) setupRenderEffect(instance, container) }
接下来分别实现以下这三个函数
createComponentInstance
export function createComponentInstance(vNode) { const component = { vNode, type: vNode.type } return component }
setupComponentInstance
上面我们提到会对setup的返回值做处理,这里处理的步骤在setupStatefulComponent函数内
export function setupComponentInstance(instance) { //todo //initProps //initSlots setupStatefulComponent(instance) } function setupStatefulComponent(instance) { const component = instance.type const { setup } = component if (setup) { const setupResult = setup() //判断返回值是Function还是Object handlerSetupResult(instance, setupResult) } }
基于setup返回值的类型,我们做进一步处理对函数类型,对象类型做不同处理,我们这里先实现Object类型的处理,将我们setup的返回值挂载到组件实例上,最后设置好返回之后的一个render函数,我们要为组件对象转换为element元素做准备
function handlerSetupResult(instance, setupResult) { //todo //function if (typeof setupResult === 'object') { instance.setupState = setupResult } //判断是否有render finishComponentSetup(instance) }
如果我们没有了render说明已经到了element元素这一步了,如果还有我们就将组件身上的VNode给到它的实例对象身上,方便之后的调用
function finishComponentSetup(instance) { const component = instance.type instance.render = component.render }
setupRenderEffect
我们接收到来自上面的instance,调用它的render来渲染虚拟dom,得到一个虚拟节点树也就是subTree,然后递归调用patch方法得到最终的element元素
function setupRenderEffect(instance, container) { const subTree = instance.render() //vnode -> patch -> Mountelement patch(subTree, container) }
processElement
到这里我们的VNode类型是component的处理已经结束,接下来要处理初始化element的了,根据流程图可以看到类似于组件的处理方式
function processElement(vNode, container) { //init 以及unpate //init mountElement(vNode, container) }
在这一步对element元素的处理,我们传入h函数的结构是这样的h("div", {class: "red"}, "hi meng"),因此我们可以看到这个type就是我们VNode的type这里也就是div元素, props以及children就是属性以及子类
这一步我们要将虚拟dom转为真实dom, 如果子类是string那么我们dom的值就是string类型,如果子类是一个数组,类似于一个父元素包含了很多的子元素,那么它的子类也是一堆虚拟dom,我们要通过patch将其也通过processElement的过程进行处理,注意这里要挂载的容器就是我们的el了
function mountElement(vNode, container) { //type就是元素类型 const el = document.createElement(vNode.type) //children就是el的值如果是基本类型就这样处理, 如果children是Array代表有后代,就用另外一种方式 const { children } = vNode if (typeof children === 'string') { el.textContent = children } else if (Array.isArray(children)) { mountChildren(vNode, el) } //props就是属性 const { props } = vNode for (const key in props) { const val = props[key] el.setAttribute(key, val) } container.append(el) }
//挂载元素后代 function mountChildren(vNode, container) { vNode.children.forEach((v) => { patch(v, container) }) }
到这里我们初始化的大体流程已经走完了,我们将虚拟dom渲染成真实dom,这里的逻辑就是渲染逻辑,接下来我将通过例子为大家演示我们的成果
最终展示
我会将我们的结果能够在一个html页面中展示处理
import { h } from '../../lib/guide-meng-vue.esm.js' export const App = { render() { return h("div", { id: 'root', class: ["red", "layhead"] } , [h("p", { class: "red" }, "red"), h("p", { class: 'blue' }, "blue")]) }, setup() { return { msg: 'meng-vue' } } }
import { createApp } from '../../lib/guide-meng-vue.esm.js' import { App } from './app.js' //vue3 const rootContainer = document.querySelector("#app") createApp(App).mount(rootContainer)
因为我们写的ts在html中无法使用,我们需要将其转化为js文件,这里我选择的是用rollup来打包,将我们的ts文件输出为js文件,这里的安装过程就不说了,主要展示如何配置,这里在引入package.json时要在断言 assert是运行时的一个断言,不然会报错
在根目录下创建一个rollup.config.js
import typescript from '@rollup/plugin-typescript'; import pkg from './package.json' assert { type: "json" }; export default { input: "./src/index.ts", output: [ //1.cjs -> commonJs //2.esm { format: "cjs", file: pkg.main }, { format: "es", file: pkg.module }, ], plugins: [typescript()] }
主要要改一下tsconfig.json里面的文件以及packge.json的文件, 在tsconfig.json中将module改为ESNext
以及package.json的文件
{ "name": "reactive", "version": "1.0.0", "description": "", "type": "module", "main": "lib/guide-meng-vue.cjs.js", "module": "lib/guide-meng-vue.esm.js", "scripts": { "test": "jest", "build": "rollup -c rollup.config.js", "prepare": "husky install", "commitlint": "commitlint --config commitlint.config.cjs -e -V" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@types/jest": "^29.5.3", "jest": "^29.6.1", "rollup": "^3.27.0", "tslib": "^2.6.1", "typescript": "^5.1.6" }, "devDependencies": { "@babel/core": "^7.22.9", "@babel/preset-env": "^7.22.9", "@babel/preset-typescript": "^7.22.5", "@commitlint/cli": "^17.6.7", "@commitlint/config-conventional": "^17.6.7", "@rollup/plugin-typescript": "^11.1.2", "babel-jest": "^29.6.1", "husky": "^8.0.0" } }
万事具备,我们运行build命令,就能够在lib文件夹下面看到打包好的js文件了,这里我们用esm就可以 用live server打开index.html
可以看到以及渲染出来了,另外看log,我们在前面说如何区分component和element,根据一个是Object一个是string这里的上面两个是不是就是的,另外看一下dom结构
也是正确的渲染出来了
总结
vue3的渲染过程相比于reactivity更加抽象了一点,还是需要多debug好好理解里面的过程,主要就是Vnode,render,patch,然后再对后续的处理一步步理解,这里只是最基本的初始化流程,后续的todo功能我会慢慢学习再次完善的,有不妥的地方,欢迎指出,最后,希望大家能够多多支持,要是能跟上一篇一样的效果就很好啦哈哈!