手写 vue 源码 === runtime-core 实现
目录
1. 创建 runtime-core 包:平台无关的运行时核心
2. 虚拟节点(VNode)的实现
形状标识(ShapeFlags):高效的类型标记
createVNode:创建虚拟节点
h 函数:开发者友好的 VNode 创建器
3. createRenderer:渲染器的核心
4. 创建真实 DOM(浏览器平台实现)
5.优化调用方法
总结:Vue3 运行时核心架构
1. 创建 runtime-core 包:平台无关的运行时核心
Vue3 的架构设计将平台相关代码和核心逻辑分离,runtime-core 包就是 Vue 的核心引擎,它不关心具体运行在浏览器、Node.js 还是其他环境。我们先创建它的基础结构:
// runtime-core/package.json
{"name": "@vue/runtime-core","module": "dist/runtime-core.esm-bundler.js","types": "dist/runtime-core.d.ts","files": ["index.js", "dist"],"buildOptions": {"name": "VueRuntimeCore","formats": ["esm-bundler", "cjs"]}
}
关键依赖:
-
@vue/shared
:公共工具函数 -
@vue/reactivity
:响应式系统
pnpm install @vue/shared@workspace @vue/reactivity@workspace --filter @vue/runtime-core
为什么这样设计?
这种架构让 Vue 可以轻松适配不同平台。浏览器端使用 runtime-dom 提供 DOM 操作,小程序端则实现特定平台的渲染逻辑,共享同一核心。
2. 虚拟节点(VNode)的实现
形状标识(ShapeFlags):高效的类型标记
Vue 使用位运算高效组合节点类型,就像给节点贴多个标签:
export const enum ShapeFlags {ELEMENT = 1, // 0000000001 -> 普通元素FUNCTIONAL_COMPONENT = 1 << 1, // 0000000010 -> 函数式组件STATEFUL_COMPONENT = 1 << 2, // 0000000100 -> 有状态组件TEXT_CHILDREN = 1 << 3, // 0000001000 -> 文本子节点ARRAY_CHILDREN = 1 << 4, // 0000010000 -> 数组子节点SLOTS_CHILDREN = 1 << 5, // 0000100000 -> 插槽子节点// ... 其他类型COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 0000000110 -> 组件(两种组件的组合)
}
位运算妙用:
-
|
组合类型:shapeFlag = ELEMENT | ARRAY_CHILDREN
(1 + 16 = 17) -
&
检查类型:if (shapeFlag & ShapeFlags.ELEMENT)
判断是否是元素
createVNode:创建虚拟节点
import { ShapeFlags, isString } from "@vue/shared";
export function isVNode(value: any) {return value ? value.__v_isVNode === true : false;
}
/*** 固定的参数* @param type 类型* @param props 属性* @param children 子节点* @returns*/
export function createVNode(type, props, children = null) {const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0;const vnode = {__v_isVNode: true, //判断是否是虚拟节点type, //虚拟节点的类型props, //虚拟节点的属性children, //虚拟节点的子节点key: props && props["key"], //diff算法中需要的keyel: null, //虚拟节点对应的实际节点shapeFlag,};if (children) {let type = 0;// 如果shapeFlag为9 说明元素中包含一个文本// 如果shapeFlag为17 说明元素中有多个子节点if (Array.isArray(children)) {type = ShapeFlags.ARRAY_CHILDREN;} else {type = ShapeFlags.TEXT_CHILDREN;}vnode.shapeFlag |= type;}// 返回vnodereturn vnode;
}
h 函数:开发者友好的 VNode 创建器
h
函数是 createVNode 的语法糖,处理多种参数形式:
import { isObject } from "@vue/shared";
import { createVNode, isVNode } from "./createVNode";export function h(type, propsOrChildren?, children?) {const l = arguments.length;// 只有属性,或者一个元素儿子的时候if (l === 2) {//是对象不是数组 「h (h1,虚拟节点 | 属性)」if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) {// 虚拟节点 「 h('div',h('span')) 」if (isVNode(propsOrChildren)) {return createVNode(type, null, [propsOrChildren]);} else {// 属性 h('div',{style:{color:'red'}});return createVNode(type, propsOrChildren);}}// 儿子纯文本 或者数组return createVNode(type, null, propsOrChildren);} else {if (l > 3) {// 超过3个除了前两个都是儿子children = Array.prototype.slice.call(arguments, 2);} else if (l === 3 && isVNode(children)) {children = [children]; // 儿子是元素将其包装成 h('div',null,[h('span')])}return createVNode(type, propsOrChildren, children); // h('div',null,'erxiao')}
}
使用示例:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><div id="app">
</div><body><script type="module">// import {// createRenderer,// render,// h// } from '/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js'import {// renderOptions,h,render} from "./runtime-dom.js"/*** 参数可能是一个「类型」* 参数可以有两个「类型,属性/儿子」* 或者三个(有可能超过三个,从第三个开始都是儿子)* h(类型,属性,儿子)* h(类型,儿子)* */// 两个参数,第二个可能是属性,或者虚拟节点「__v_isVNode」const ele1 = h('div', { a: 1 })const ele2 = h('div', h('p'))// 第二个参数就是一个数组==>儿子const ele3 = h('div', [h('p'), h("div")])// 直接传递非对象的,文本const ele4 = h('div', 'hello')// 不能出现三个参数 第二个只能是属性const ele5 = h('iv', {}, 'hello')const ele6 = h('div', {}, h('p')) //虚拟节点 包装成数组// 如果超过是三个参数后边都是儿子const ele7 = h('div', {style: {color: 'red'}}, h('p', 'p'), h('div', 'div'))// 其他情况就是属性render(ele7, app)console.log(ele7);</script>
</body></html>
3. createRenderer:渲染器的核心
渲染器是 Vue 的核心大脑,连接虚拟节点和平台具体的 DOM 操作:
// core 中不关心如何渲染 {完全不关心api里面的,可以跨平台}
import { ShapeFlags } from "@vue/shared";
export function createRenderer(renderOptions) {const {insert: hostInsert, // 插入remove: hostRemove, // 移除createElement: hostCreateElement, // 创建元素createText: hostCreateText, // 创建文本setText: hostSetText, // 设置文本setElementText: hostSetElementText, // 设置元素文本parentNode: hostParentNode, // 获取父节点nextSibling: hostNextSibling, // 获取下一个兄弟节点patchProp: hostPatchProp, // 更新属性} = renderOptions;// 创建数组子节点const mountChildren = (children, container) => {for (let i = 0; i < children.length; i++) {// children[i] 可能是纯文本。。。patch(null, children[i], container);}};const mountElement = (vnode, container) => {// type:元素 props:属性 children:子节点const { type, props, children, shapeFlag } = vnode;const el = hostCreateElement(type);if (props) {for (const key in props) {// 更新属性 「元素 属性名 旧属性,新属性」hostPatchProp(el, key, null, props[key]);}}//文本if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 设置元素文本 「元素,文本」hostSetElementText(el, children);} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 数组mountChildren(children, el);}// 插入元素 「元素,容器」hostInsert(el, container);};/**** @param n1 旧节点* @param n2 新节点* @param container 容器* @returns*/// 渲染走这里,更新也走这里const patch = (n1, n2, container) => {if (n1 === n2) {// 相同节点,直接跳过return;}// 初始化if (n1 === null) {mountElement(n2, container);}};// 多次渲染,会进行虚拟节点的比对,进行更新const render = (vnode, container) => {patch(container._vnode || null, vnode, container);container._vnode = vnode;};return {render,};
}
4. 创建真实 DOM(浏览器平台实现)
在 runtime-dom
包中提供浏览器环境的 DOM 操作:
// runtime-dom/src/nodeOps 这里存放常见 DOM 操作 API,
// 不同运行时提供的具体实现不一样,最终将操作方法传递到runtime-core中,
// 所以runtime-core不需要关心平台相关代码~
export const nodeOps = {insert: (child, parent, anchor) => {// 添加节点parent.insertBefore(child, anchor || null);},remove: (child) => {// 节点删除const parent = child.parentNode;if (parent) {parent.removeChild(child);}},createElement: (tag) => document.createElement(tag), // 创建节点createText: (text) => document.createTextNode(text), // 创建文本setText: (node, text) => (node.nodeValue = text), // 设置文本节点内容setElementText: (el, text) => (el.textContent = text), // 设置文本元素中的内容parentNode: (node) => node.parentNode, // 父亲节点nextSibling: (node) => node.nextSibling, // 下一个节点querySelector: (selector) => document.querySelector(selector), // 搜索元素
};
5.优化调用方法
export const render = (vnode, container) => {createRenderer(renderOptions).render(vnode, container);
};
export * from '@vue/runtime-core';
这样在页面中可以直接调用render
方法进行渲染啦~
总结:Vue3 运行时核心架构
-
分层架构:
-
runtime-core
:平台无关的核心逻辑 -
runtime-dom
:浏览器特定的 DOM 操作 -
reactivity
:独立的响应式系统
-
-
虚拟节点(VNode):
-
轻量级的 JS 对象描述 DOM
-
使用 ShapeFlags 高效标识节点类型
-
h()
函数简化创建过程
-
-
渲染器(Renderer):
-
createRenderer
工厂函数接收平台操作 -
patch
函数处理初始化和更新 -
递归处理子节点形成树形结构
-
-
跨平台能力:
-
通过抽象 DOM 操作接口
-
同一核心适用于 Web、小程序、Native
-
通过实现 runtime-core,我们深入理解了 Vue3 的核心工作原理。这种设计不仅提高了代码复用性,还使得 Vue3 能够灵活适应各种渲染环境,为开发者提供一致的开发体验。