4.1 了解 runtime-core
runtime-core 是vue3 的核心逻辑,主要流程 , 是实现 mini-vue 的主要核心 。
runtime-core 在vue3中的作用:
- 符合vue3的开发逻辑,API 的设计, 配合其他模块应用
runtime-core 是vue3的运行时,主要内容有 组件 和 视图 Element
- 初始化模板,定义组件, 声明 Element内容
- 组件 、 Element的初始化、更新
- 实现组件的主要API :
$el | props | emit | slot
的实现
初始化
runtime-core
的主要流程- 初始化组件的流程
- 初始化 Element 流程
分析runtime-core
主要流程 :
- 主要是根据组件生成创建出来的
vnode
, 在patch()
函数中判断 虚拟节点vnode
的类型,Component
: 初始化 组件Element
: 创建element
happy path
初始化 模板和实现 基本的流程
- 需要有一个根组件 App,
createApp(App)
时候传入 , 并且在createApp
内部有一个对象, 包含mount()
函数,mount()
函数接收一个Element
容器, 也就是id=app
的容器。 - 根据传入过来的组件创建 虚拟节点
vnode
调用 定义的 render()
- 执行 render() -> 就是调用
patch()
函数 patch()
函数根据 vnode 的类型 判断是 创建 组件 还是 Element
- 执行 render() -> 就是调用
创建
example
文件夹,存放模板和测试文件- 创建
helloworld
文件夹 , 测试初始化的文件- index.html : 模板
- main.js : 入口文件
- App.js : App根组件
- 创建
<!-- 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>
<!-- 引入 mian.js -->
<script src="main.js" type="module"></script>
</body>
</html>
/* main.js */
import { App } from "./App.js"
// 正常使用 vue3 一样
createApp(App).mount('#app')
/* App.js */
// 创建APP组件
export const App = {
// 这里先不写 template,因为 template 最终会转为 runner 函数
// 先写 runder
render() {
// ui 逻辑
// 返回一个虚拟节点
// h() 用于创建Element的虚拟节点
return h("div", "hello" + this.msg)
},
// setup() 函数
setup() {
// 这里写 composition api 逻辑
// 返回一个对象
return {
msg: "mini-vue"
}
}
}
在Vue3 中,像App组件一般放在 .vue
文件中,而标签也是放在 <template>
中。在这里初始化时,因为 <template>
标签最终都会编译为 render
函数。 而runtime-core
不具备编译功能,所以这里模拟组件都是使用 render()
函数。
4.2 实现Component 初始化的主流程
实现runtime-core
的过程,源代码文件放在 src/runtime-core
- 创建
createApp()
函数, 接收App的组件实例 ,src/runtime-core/createApp.ts
```typescript / createApp.ts /
// createApp 逻辑 export function createApp(rootComponent) { // 接收一个根组件容器 -> App // 返回一个对象 return { // 对象内部必须有一个 rootContainer , 根容器 -> id=app 的容器 // 目的是为了将 runner 函数渲染后给他 添加到
里面 mount(rootContainer) { } } }
2. 根据 App 组件创建按虚拟节点 `VNode`
```typescript
/* vnode.ts */
// 导出创建虚拟节点的方法
export function createVNode(type, props?, children?) {
// 参数:
// type -> 传入进来的数据类型, 组件|Element
// props -> 可选 -> 属性 | 组件的props
// children -> 可选 -> 子节点 | 子组件
const vnode = {
type, // 组件类型
props,
children,
}
// 返回虚拟节点
return vnode
}
- 创建 h.ts , 在 render() 函数中使用的
h()
接收element,并且返回 虚拟节点VNode, 所以h()也也使用到了createVNode()
```typescript / h.ts /
// 用于调用 createVNode 返回一个 VNode export function h(type, props?, children?) { return createVNode(type, props, children) }
4. 在`createApp.ts` 调用 `createVNode()`得到虚拟节点, 在创建 `render()` 函数
```typescript
/* createApp.ts */
// createApp 逻辑
export function createApp(rootComponent) { // 接收一个根组件容器 -> App
// 返回一个对象
return {
// 对象内部必须有一个 rootContainer , 根容器 -> id=app 的容器
// 目的是为了将 runner 函数渲染后给他 添加到 <div id='app'></div> 里面
mount(rootContainer) {
// 1. 先将 rootComponent 根容器(App) 转为一个 虚拟节点 vnode
// 将 组件转为虚拟节点
// 将所有的逻辑操作 都会基于 vnode 做处理
const vnode = createVNode(rootComponent)
// vnode -> 返回出来的虚拟节点 vnode
// createVNode(组件) -> 基于组件创建虚拟节点
// 2.
// 得到虚拟节点后 -> 可以调用 render() 函数
reder(vnode, rootContainer)
// runner(虚拟节点, 根容器)
}
}
}
- 实现
render()
方法的逻辑 , render() 的主要就是调用patch()
方法, 这样做是为了方便递归调用patch()
方法。 抽离实现render()
的方法 到/runtime-core/renderer.ts
文件 ```typescript / renderer.ts /
// 导出 render() 函数 export function render(vnode, container) { // render(虚拟节点, 根容器)
//render 的作用 -> 主要调用 patch() 方法
patch(vnode, container) // 使用 patch() 函数 为了方便递归 }
6. `patch` 方法的实现, 通过判断 VNode 的类型
1. 如果是 Componet 类型 -> 调用 `processComponent()` 进行初始化组件的逻辑
1. 在 `processComponent()`中主要 调用 `mountComponent()`初始化 组件。
2. 如果是 Element 类型 -> 调用 `processElement()`
1. `processElement()` 函数会在 Element初始化的主流程实现
<br />这里先实现初始化组件 , 不用先判断
```typescript
/* renderer.ts */
// patch(虚拟节点, 根容器)
function patch(vnode, container) {
// 这里主要处理组件
// 1. 判断 vnode 的类型 -> 可能是 组件类型 或者 Element 类型
// 如果是 组件类型
// 去处理组件
// 实现初始化组件的逻辑
// 挂载组件 processComponent 的函数
processComponent(vnode, container)
}
// 初始化组件
function processComponent(vnode: any, container: any) {
// 1. 挂载组件
// 使用 mountComponent 函数 挂载组件
mountComponent(vnode, container)
}
- 实现组件的挂载逻辑,
mountComponent()
mountComponent()
执行逻辑- 调用
createComponentInstance()
通过 VNode 创建组件实例instance
,之后组件上的props、slots 都会挂载到这个组件的实例上 - 调用
setupComponent()
, 主要的逻辑,初始化setup
函数 , 执行组件的setup()
函数 - 调用
setupRenderEffect()
主要就是调用 组件的 render() 函数 ```typescript / renderer.ts/
- 调用
// 挂载组件的逻辑 function mountComponent(vnode: any, container) { // 1. 通过 vnode 创建一个组件的实例对象 -> instance const instance = createComponentInstance(vnode)
// 2. setupComponent() 初始化 // 2.1 解析处理组件的其他内置属性 比如: props , slots 这些 // 2.2 组件中setup() 的返回值 和 挂载 runder() 返回 setupComponent(instance)
// 3. 开始调用 组件的 runner 函数 setupRenderEffect(instance, container) }
将挂载组件的逻辑函数抽离到 `component.ts` 中 ->`createComponentInstance()` 和 `setupComponent()` 函数
```typescript
/* src/runtime-core/component.ts */
// 创建组件的实例 instance
export function createComponentInstance(vnode) {
// 通过 vnode 创建一个组件的实例对象 component
// 返回一个component对象
const component = {
vnode,
// 为了简化操作——> 获取 type, VNode的类型
type: vnode.type
}
// 返回 component
return component
}
实现 setupComponent()
函数的逻辑
- 解析处理组件的其他内置属性, 比如: props , slots 这些
- 实现调用 setup(), 拿到返回值
- 设置 组件实例的 render() 函数 ```typescript / src/runtime-core/component.ts /
// …
// 通过 vnode 创建一个组件的实例对象 -> instance // instance 就是 虚拟节点 export function setupComponent(instance) { // 解析处理组件的其他内置属性 比如: props , slots 这些 // 和 组件中setup() 的返回值
// TODO: // 1. 先初始化 props // initProps()
// 2. 初始化 slots // initSlots()
// 3. 初始化component的状态, 也就是 调用 setup() 之后的返回值 setupStatefulComponent(instance) // 翻译: setupStatefulComponent -> 初始化有状态的 component }
`setupStatefulComponent()` 函数用于初始化有状态的的组件,与其相对的是没有状态的函数式组件。
- 在`setupStatefulComponent`函数中首先调用`setup`
- 之后调用`handleSetupResult`函数处理该方法的返回值。
- 调用 `finishComponentSetup()` 将 `render()`函数绑定到 `instance` 上
```typescript
/* src/runtime-core/component.ts */
// ...
function setupStatefulComponent(instance: any) {
// 拿到 setup() 的返回值
// 1. 获取到虚拟节点的type , 也就是用户定义的 App 组件
const Component = instance.type
// 2. 解构出 setup
const { setup } = Component
// 判断 setup 是否存在
if (setup) {
// 调用 setup() 拿到返回值
const stupResult = setup()
// 判断 stupResult 的类型
handleStupResult(instance, stupResult)
}
}
function handleStupResult(instance, stupResult: any) {
// 因为 setup 可以返回出 Function | Object
// 如果是 Function 类型,那么就是组件的 render 函数
// 如果是 Object 类型,将这个对象 注入到这个组件的上下文中
// TODO: 这里先实现 Object, 之后再去实现 Function
if (typeof stupResult === 'object') {
// 将这个值注入到 instance 实例上
instance.setupState = stupResult
}
// 需要保证 组件必须要用 runner 函数
// 调用 finishComponentSetup() 将render() 函数绑定到 instance上
finishComponentSetup(instance)
}
// 添加render() 到 instance 上
function finishComponentSetup(instance: any) {
const Component = instance.type
// 判断组件中是否具有 runner()
if (Component.render) {
// 组件中具有 runner(), 将 runner 挂载到 instance 上
instance.render = Component.render
}
}
- 实现调用 render() 函数的逻辑 ,
setupRenderEffect()
的实现。
/* renderer.ts */
// ... 其他代码
// 实现调用render
function setupRenderEffect(instance: any, container: any) {
// 调用 runder() 函数 返回 subTree
// subTree 是 h() 函数返回的 vnode, 也是虚拟节点树 subTree
// 当然 subTree 也有可能是 componet 的类型
const subTree = instance.render()
// 再基于返回过来的虚拟节点 vnode, 再进一步的调用 patch() 函数
// vnode -> subTree 是一个 Element类型 -> 挂载 mountElement
// 递归调用 -> patch(虚拟节点,容器)
patch(subTree, container)
// patch()再去判断,-> subTree 的类型 -> 可能是 组件类型 或者 Element类型 -> 一直递归进行下去
}
到此已经完成组件的初始化主流程了
回顾效果图
总结
/**
* 初始化 runtime-core 的流程 和 初始化 组件 的流程总结
*
* runtime-core: 实现的基本目的: 符合 vue3的设计思路 createApp(App).mount(#app)
* 配合组件,实现组件的渲染,组件的更新,组件的销毁, 组件 Element 的渲染 等等
*
*
*
* 初始化组件:
* 1. 在 mount(容器), 这个方法中, 对createApp 传入进来的 App组件容器, 进行转换为虚拟节点 vnode
* 2. 通过得到转换的 vnode , 使用 runner(vnode, 容器) 函数,进一步处理 vnode
* 3. runner() 函数的目的就是 负责调用 patch(vnode, 容器) 方法, 实现 patch()方法为了方便递归调用
* 4. patch() 函数对vnode进行一个判断 ,如果 vnode 是一个组件,进行组件的初始化 ,如果它是 Element,就对 Element 进行渲染
*
*
* 5. 当前只实现了组件的初始化
* - 1. 在 patch() 调用 processComponent()
* - 2. 在 processComponent() 进行组件的挂载 -> mountComponent(vnode, container)
* - 3. mountComponent(vnode, container) 挂载组件的逻辑
* - 通过 vnode 创建一个 组件的实例对象 -> instance ; 使用 createComponentInstance(vnode) 方法
* - 设置 setupComponent(instance) 初始化组件, 解析处理组件的其他内置属性 比如: props , slots 这些 和组件中setup() 的返回值 和 挂载 runder()
* - 开始调用 render 方法
* - 调用 instance.render() -> h() 返回出来的 subTree 虚拟 Element 节点
* - 递归调用 patch(subTree, container) 进行一个 Element 的渲染
*/
4.3 使用 rollup 打包
Webpack 一般用于项目的打包,rollup 常用于库的打包,Vue3 就是使用 rollup 进行打包的,因此 mini-vue3 也使用 rollup 进行打包。
- 在项目文件夹下执行
yarn add rollup @rollup/plugin-typescript tslib -D
命令分别安装 rollup、rollup 打包 TypeScript 项目时所需的插件@rollup/plugin-typescript
和该插件依赖的tslib
- 在src目录下创建index.ts文件作为 mini-vue3 的出口文件,在src/runtime-core目录下创建index.ts文件作为 runtime-core 的出口文件,并在其中将createApp和h导出: ```typescript / src/runtime-core/index.ts /
// runtime-core 的出口 export { createApp } from “./createApp” export { h } from “./h”
```typescript
/* src/index.ts */
// 导出所有配置
export * from './runtime-core'
- 配置
rollup.config.js
```javascript / rollup.config.js /
// 导出package.json设置的文件目录 import pkg from ‘./package.json’ import typescript from “@rollup/plugin-typescript” export default { input: ‘src/index.ts’, output: [ // 1. commonjs -> cjs // 2. esm { format: ‘cjs’, // file: ‘lib/guide-mini-vue.cjs.js’, file: pkg.main, // 在package.json 定义了 main 和 module 的模式, 可以直接使用, 一个小优化 }, { format: ‘es’, // file: ‘lib/guide-mini-vue.esm.js’, file: pkg.module, } ],
plugins: [ typescript() ] }
4. 配置 `package.json`
```json
{
"name": "hidie-mini-vue",
"version": "1.0.0",
"main": "lib/guide-mini-vue.cjs.js",
"module": "lib/guide-mini-vue.esm.js",
"license": "MIT",
"scripts": {
"test": "jest"
"build": "rollup -c rollup.config.js"
},
}
在
tsconfig.json
设置module
{
"module": "ESNext", /* Specify what module code is generated. */
}
执行打包命令
yarn build
, 在lib
目录会生成guide-mini-vue.cjs.js & guide-mini-vue.ems.js
文件在
helloworld
的main.js & App.js
中引入createApp() & h()
```typescript / main.js /
// 引入打包后的 createApp import { createApp } from ‘../../lib/guide-mini-vue.esm.js’ import { App } from “./App.js” // 正常使用 vue3 一样
// 获取容器 const rootContainer = document.querySelector(“#app”) createApp(App).mount(rootContainer)
打开`example/helloworld`目录下的`index.html`文件,可以看到 `Component `初始化的主流程正常运行,但是程序出现报错,这是由于 `Element `初始化的主流程还没有实现。
<a name="OmJGW"></a>
# 4.4 实现 Element 初始化的主流程
在实现 Element 初始化前, 对 `App.js` 组件改动。 对 `render()` 返回的 h() 函数参数更复杂一些, 也就是能在页面上渲染内容更多
```typescript
/* example/helloworld/App.js */
render() {
// ui 逻辑
// 返回一个虚拟节点
// return h("div", "hello" + this.msg)
// return h("div", {
// id: 'test1-id',
// class: 'test2-class'
// }, "hello mini-vue") // 当返回是 children 是 string时
// 当children 是一个 Array 时
return h('div', {
id: 'test1-array-1',
class: 'test2-array-1'
}, [
h('p', { class: 'red' }, '这是children的第一个数组'),
h('p', { class: 'blue' }, '这是children的第二个数组')
])
},
// setup() 函数
在 index.html
添加对应的样式
<!-- index.html -->
<head>
<meta charset="utf-8">
<title>Hello World</title>
<style>
.red {
color: red;
}
.blue {
color: blue;
}
</style>
</head>
实现 Element
初始化的主流程是从 patch()
方法 判断 VNode 的类型为 Element 类型时,调用 procssElement()
函数开始。
- 在
patch()
判断 VNode 的类型 -> 当 VNode.type 为string
时,代表VNode 是 Element ```typescript / renderer.ts /
// 目的为了根据判断 虚拟节点,都组件或者是 Element 进行一个渲染 // patch(虚拟节点, 容器) function patch(vnode, container) { // 1. 判断 vnode 的类型 -> 可能是 组件类型 或者 Element 类型
// console.log(vnode.type)
//可以看到 vnode.type 要么是 组件类型 -> Object , 要么是 Element 类型 -> string
if (typeof vnode.type === “string”) { // 如果 vnode.type 是 string 类型, 表示它是 Element // 执行 processElement 初始化 Element processElement(vnode, container) } else if (isObject(vnode.type)) { // 如果 vnode.type 是 object 类型 , 表示它是 组件类型
// 如果是 组件类型
// 去处理组件
// 实现初始化组件的逻辑
processComponent(vnode, container)
} }
2. `processElement()` 主要的逻辑就是对 Element 的初始化 和 Element 的更新 执行的逻辑 , 初始化调用 `mountElement()` 函数
```typescript
/* renderer.ts*/
// 当 vnode 是一个 Element 类型执行这个函数
function processElement(vnode: any, container: any) {
// 进行 Element 的渲染
// Element 初始化 分为两种情况 -> init 初始化 -> update 更新
// 实现初始化Element的逻辑
mountElement(vnode, container)
// 实现 Element 更新逻辑之后去实现
}
mountElement()
对 Element 初始化挂载- 创建一个真实的
Element
->vnode.type
- 对 vnode 的 props , 解析属性 ->
vnode.props
- 处理children , 解析 children, 然后赋值
- 将创建好的 节点,添加到
container
id=app
的容器上 ```typescript / renderer.ts/
- 创建一个真实的
// 初始化 Element function mountElement(vnode: any, container: any) {
// 1. 创建一个真实的 Element -> vnode.type const el = document.createElement(vnode.type)
// 2. props , 解析属性 -> vnode // 解构 props const { props } = vnode if (props) { // 当props 不为空时 for (const key in props) { // 拿到 prosp 对应的属性值 -> {class: ‘red’} const val = props[key] // 设置 el 的属性值 el.setAttribute(key, val) } }
// 3. children, 解析然后赋值 // children 有两种情况, 是一个数组, 或者是一个字符串 const { children } = vnode if (typeof children === ‘string’) { // 直接设置 el 的文本内容 el.textContent = children } else if (Array.isArray(children)) { // 表示 [h(), h()] // 使用 mountChildren 重构 children - Array 的渲染 mountChildren(vnode, el) }
// 4. 挂载 el 到 容器 container 上 container.appendChild(el) }
4. 处理 `children`为数组的逻辑 `mountChildren()` 函数
因为 `children()`有两种情况, 是一个数组, 或者是一个字符串
- `returnh("div",{class: 'text'}, "hello"+this.msg)`
- `returnh("div", {class: 'text'}, [h('p', { class: 'red' }, '这是children的第一个数组'), h('p', { class: 'blue' }, '这是children的第二个数组'),]`
当 `childer` 是 `string` 时,就是一个简单的文本,直接赋值给 `el` 就行 `el.textContent = children`<br />当 `children` 时, 执行 `mountChildren()` 传入 `vnode & el` ,循环 `children`, 再次递归调用 `patch(v,el)`,递归调用的区别是 `patch`的容器参数 变为了 `el`
```typescript
/* renderder.ts */
function mountElement(vnode: any, container: any) {
// 省略代码
const { children } = vnode
if (typeof children === 'string') {
// 直接设置 el 的文本内容
el.textContent = children
} else if (Array.isArray(children)) {
// 表示 [h(), h()]
// 使用 mountChildren 重构 children - Array 的渲染
mountChildren(vnode, el)
}
}
// 封装 渲染 children 为 children 的函数
function mountChildren(vnode, container) {
// 循环 children 内的虚拟节点, 然后调用 patch()进行递归->再去渲染
vnode.children.forEach((v) => {
// 这里 container 容器设置为 el
// 递归调用 patch()
patch(v, container)
})
}
以上就完成了初始化 Element
的主流程。 执行 yarn build
重新打包, 然后查看 index.html
渲染出来的内容
最后总结分析runtime-core
Element的流程
4.5 实现组件代理对象
为了实现在render() -> h()
渲染时能够取到setup()
的返回值
/* App.js */
render() {
// ui 逻辑
return h('div', { class: 'test-class-id' }, this.msg)
// return h('div', { class: 'test-class-id' }, [
// h('p', { class: 'red' }, 'Hello Red'),
// h('p', { class: 'blue' }, 'Hello Blue')
// ])
},
// 在 render 中还需要实现 :
// 1. 实现 render() 中能够 setupState -> setup 的返回值
// 这里的this.msg 应该是 setup()返回出来的 msg , 怎么样能够拿到这个 setup() 中的 msg呢?
// 只需要把 setup() 的返回值,绑定到 render() 的 this 上即可
setup() {
return {
msg: 'Hello World'
};
}
实现逻辑 : 由于之前在Component
已经实现将 setup() 的返回值赋值给了 instance
当前组件的实例 setupState
上, 所以要实现 render()
中获取 setup 的返回值就在 setupState
上获取。
因为在render()
中通过 this.xxx
访问数据,可以利用 Proxy
代理对象上的 get
进行代理,当访问 this.xxx
返回 setupState
对应的返回值 。
在setupStatefulComponent()
时,开始进行代理,并将实现的代理 proxy
赋值到 instance
上,在调用 render()
时候 将 this
的指向为这个 Proxy
代理对象
- 初始化
Proxy
```typescript / component.ts / export function createComponentInstance(vnode) { const component = { // 其他代码 // 初始化 proxy proxy: null, } return component }
2. 实现代理对象 `new Proxy`
```typescript
/* component.ts */
// 执行 call setup()
function setupStatefulComponent(instance) {
// 因为这里确定时 组件逻辑, 通过 instance.type 获取当前的组件
const component = instance.type
// 创建组件代理对象 Proxy, 并添加到 instance组件实例上
// Proxy的第一个参数 {} -> ctx 上下文
instance.proxy = new Proxy({}, {
// 在render() 中调用 this.xxx 执行 get()
get(target, key) {
// 这里 target 就是 ctx -> 上下文, key 就是访问的 key
// console.log(target, key)
// 1. 实现从setupState 拿到值
const { setupState } = instance
// 判断 setupState 中是否有 key 属性
if (key in setupState) {
// 返回 setupState 中的值
return setupState[key]
}
}
})
}
- 在调用
render()
前, 解构出proxy
代理对象, 在调用render()
前把render()
的this
指向proxy
```typescript / renderer.ts / function setupRenderEffect(instance, container) { // 解析出 proxy 代理对象,当调用 render时候,将this指向这个proxy 代理对象 const { proxy } = instance const subTree = instance.render.call(proxy) // 执行递归调用 patch patch(subTree, container) }
重新打包项目文件 :`yarn build`, 再次查看 `index.html` 就可以看到 setup() 的返回值已经出现在页面了<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25614690/1658569521894-c5f89e7b-3183-4ea3-8844-4027861b032d.png#clientId=u2e6ae8ae-f9b4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=217&id=u114694a6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=271&originWidth=618&originalType=binary&ratio=1&rotation=0&showTitle=false&size=24376&status=done&style=none&taskId=u61a367a1-f175-492a-acf8-45b620537ed&title=&width=494.4)
实现访问到 `setup() `的返回值逻辑, 再实现组件的其他API, 比如 `$el & $data ` 这些 <br />这里实现 `$el` , `$el` 的作用: `该组件实例管理的 DOM 根节点。`
在实现访问 `$el`,通过 App 组件内部设置 测试的内容
```javascript
// 创建APP组件
// 使用 window 添加事件
window.self = null
export const App = {
render() {
// 增加调试 this -> window
window.self = this
// ... 其他代码
}
}
实现 $el
的API
在初始化
$el
, 在创建虚拟节点时 ```typescript / vnode.ts / export function createVNode(type, props?, children?) { const vnode = { type, props, children, // 初始化 el el: null }return vnode }
2. 在初始化 `Element `时, 让 给 `$el` 赋值为根节点 , 在获取 `VNode `树并递归地处理即调用`setupRenderEffect`函数时,将 `VNode `树的 `el `挂载到 `VNode `上
```typescript
/* renderer.ts */
function mountElement(vnode, container) {
// console.log(vnode)
// 1. 创建 Element 对象
const el = (vnode.el = document.createElement(vnode.type))
// ... 其他代码
}
function setupRenderEffect(instance, vnode, container) {
// ... 其他代码
// 将 VNode 树的 el property 挂载到 VNode 上
// 实现 $el 的关键时机, 在这个时候赋值 el
// 当patch() 再次调用,可能是 进行Element流程
// 将 subTree.el 赋值给 vnode.el
vnode.el = subTree.el
}
- 在代理对象中, 添加判断逻辑 , 当调用
$el
时返回el
根节点 ```typescript / component.ts /
function setupStatefulComponent(instance) { const Component = instance.type
instance.proxy = new Proxy( {}, { get(target, key) { // 1. 实现从setupState 拿到值 const { setupState } = instance // 判断 setupState 中是否有 key 属性 if (key in setupState) { return setupState[key] } // 2. 添加判断 实现访问 $el 的逻辑 // 若获取 $el property 则返回 VNode 的 el property if (key === ‘$el’) { // 如果是 $el , 就返回真实的 DOM 元素 // 这里instance 组件实例 上已经挂载了 vnode , 通过vnode访问 el // 因为这里的 vnode 类型是 Object , 而不是 string ,说明这个 vnode 是组件的虚拟节点,不是element的。 // 所以,通过 vnode 获取 el 元素,返回 null // 解决: 就是当 再次调用patch()后,将 调用 render() 返回的 subTree.el 赋值给 vnode.el return instance.vnode.el } } } )
/ 其他代码 / }
这个时候执行打包: `yarn build ` , 在访问 `index.html` 时, 在控制台输入 `self.$el` 就可以看到组件的根节点了<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25614690/1658591602726-4d1565b0-0fdb-4240-b1a9-08962b36ec6f.png#clientId=u2e6ae8ae-f9b4-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=196&id=uf74a7a8b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=245&originWidth=502&originalType=binary&ratio=1&rotation=0&showTitle=false&size=18922&status=done&style=none&taskId=u055d9e65-2567-4712-baef-23a0341a6a1&title=&width=401.6)
4. 重构代码,参考 在 `reactive` 是使用 `Proxy` 的逻辑,把 Proxy 的 主要功能抽离代码, 实现重构
创建 `componentPublicInstance.ts` 文件
```typescript
/* componentPublicInstance.ts */
// 一个公共访问属性的逻辑
const publicPropertiesMap = {
// $el
$el: (instance) => instance.vnode.el
}
export const PublicInstanceProxyHandlers = {
// 使用 { _: instance } 的方式传递 instance 参数
get({ _: instance }, key) {
// 这里 target 就是 ctx -> 上下文, key 就是访问的 key
// console.log(target, key)
// 1. 实现从setupState 拿到值
const { setupState } = instance
// 判断 setupState 中是否有 key 属性
if (key in setupState) {
// 返回 setupState 中的值
return setupState[key]
}
// // 2. 添加判断 实现访问 $el 的逻辑
// if (key === "$el") {
// // 直接返回 $el 属性
// return instance.vnode.el
// }
// 重构
// 实现公共访问 key 获取 对象的 value
const publicGetter = publicPropertiesMap[key]
// 判断是否具有该属性
if (publicGetter) {
// 执行 publicGetter 函数拿到 key 对应的 value, 将其返回
return publicGetter(instance)
}
}
}
在 component.ts
改为
/* component.ts */
function setupStatefulComponent(instance) {
const Component = instance.type
// 利用 Proxy 对组件实例对象的 proxy property 的 get 进行代理
instance.proxy = new Proxy({ _: instance }, PublicInstanceHandlers)
/* 其他代码 */
}
再次打包,能够获取到 $el
说明重构没有问题
// 总结
// 在 render 中还需要实现 :
// 1. 实现 render() 中能够 setupState -> setup 的返回值
// 这里的this.msg 应该是 setup()返回出来的 msg , 怎么样能够拿到这个 setup() 中的 msg呢?
// 只需要把 setup() 的返回值,绑定到 render() 的 this 上即可
/**
* 实现 setupState:
* 因为当前的 render() 函数不仅仅需要访问到 setupState 或者 使用 $el setupState 这些
* 可能还会添加一些 $data 能够让 render() 快速访问
*
* 解决: 可以通过 Proxy 实现 ,也就是 组件代理对象 -> 代理对象
* 把 stateState & $el & $data 通过 Proxy 代理, 可以让 render() 快速访问
*
* 实现方式:
* 1. 在 初始化 setupState -> 调用 setup() 时,创建 代理对象 Proxy
* 2. 创建代理对象,实现 xxx 功能
* 3. 把 代理对象绑定到 instace 组件实例上
* 4. 当在调用 render() 函数时,通过 Proxy 访问 setupState
*
*/
// 2. 实现 this.$el 获取到当前组件的根节点 -> div
// $el -> vue3 的API , $el 的目的:返回根节点 element, 也就是组件的根节点, DOM实例
/**
* 实现 $el:
* $el 的作用: $el -> vue3 的API , $el 的目的:返回根节点 element, 也就是组件的根节点, DOM实例
*
* 实现方式:
* 1. 在初始化 Element这里,需要将根节点保存起来, 使用el -> 在创建虚拟节点时候,初始化el 为null
* 2. 在初始化 Element时,让 el 保存 根节点
* 3. 在代理对象中, 添加判断逻辑,当在render中调用 this.$el时, 需要返回 el,
*
*/
4.6 实现 shapeFlag
shapleFlag
的实现主要用于在代码中 判断 VNode
虚拟节点 和 VNode.childer
的类型
在 调用 patch()
是判断 VNode
是 Component || Element
在 渲染 children
时 判断 children
是 Array || string
实现 shapeFlag
的逻辑
- 先完成一个最简单的
shapeFlag
对象,设置它的属性值都为0
表示这个属性的状态 ```typescript / shared/test-shapeFlag.ts/
// 测试 ShapeFlag const ShapeFlagObject = { // 这里使用 0 | 1 判断节点的状态 -> 0 表示不是 , 1 表示是 element: 0, stateful_component: 0, text_children: 0, array_children: 0, }
状态的变化 ,如果当前节点是 `vnode` 是 `stateful_component` , 设置它的状态为 `1`
```typescript
/* shared/test-shapeFlag.ts*/
// 如果当前节点 vnode 是 stateful_component
// 设置 它的状态
// 1. 可以设置修改 状态
ShapeFlag.stateful_component = 1
ShapeFlag.text_children = 1
状态的变化, 判断当前shapeFlag
属性的类型
/* shared/test-shapeFlag.ts*/
// 需要先判断 之前的节点类型 ShapeFlag
// 2. 查找
if (ShapeFlag.element) {
// 节点是 Element 类型时执行
}
if (ShapeFlag.stateful_component) {
// 节点是 component 类型 时执行
}
出现问题: 这种使用对象形式的判断方法,不够高效 .
而在 Vue3
中使用的位运算法
更高效
- 使用
位运算法
来设置shapeFlag
的状态
- 与运算(&):两位都为 1,结果才为 1
- 或运算(|):两位都为 0,结果才为 0
- 左移运算符(<<):将二进制位全部左移若干位 ```typescript // 可以通过 位运算方式 提升代码性能 // 位运算-> 通过二进制 01 表示 当前节点的状态
0001 -> element 0010 -> stateful_component 0100 -> text_children 1000 -> array_children
// 表示两种状态 1010 -> stateful_component + array_children
// 实现位运算方式实现 修改和查找 的功能
可以定义 `shapeFlage`
```typescript
/* shared/test-shapeFlag.ts*/
const ShapeFlag = {
element: 1, // 转为二进制 -> 0001
stateful_component: 1 << 1, // 使用左移运算符 1 左移1位 -> 10 -> 转为二进制 -> 0010
text_children: 1 << 2, // 1 左移 2 位 -> 100 -> 转为二进制 -> 0100
array_children: 1 << 3, // 1 左移 3 位 -> 1000 -> 转为二进制 -> 1000
}
实现修改的 |
逻辑
// 1. 修改 : | -> 两位都为 0 ,才为 0
0000 -> | 进行或运算
0001
----
0001
// 修改 0000 为 0001 得到 0001 -> 修改初始时的0状态 为 element
0001 -> | 进行或运算
0100
----
0101
// 修改 0001 为 0001 得到 0101 -> 修改 element 状态为 element + text_children
实现 查找的 &
逻辑
// 2. 查找 : & -> 两位都为 0 , 才为 1
// &
0001 -> & 进行 & 运算
0001
----
0001
// 查找0001是不是0001类型 -> 得到 0001 表示是
0010 -> & 进行 & 运算 ->
0001
----
0000
// 查找0010是不是0001 类型 -> 得到 0000 表示不是
确定逻辑 ,实现shapeFlag
的代码
- 在
shared/shapeFlags.ts
实现 逻辑代码 ```typescript / shared/shapeFlags.ts /
// 实现 ShapeFlag
// 1. 声明 ShapeFlag 状态 , 使用枚举 并导出 ShapeFlags export const enum ShapeFlags { ELEMENT = 1, // 转为二进制 -> 0001 STATEFUL_COMPONENT = 1 << 1, // 使用左移运算符 1 左移1位 -> 10 -> 转为二进制 -> 0010 TEXT_CHILDREN = 1 << 2, // 1 左移 2 位 -> 100 -> 转为二进制 -> 0100 ARRAY_CHILDREN = 1 << 3, // 1 左移 3 位 -> 1000 -> 转为二进制 -> 1000 }
// 2. 在初始化 vnode 时,声明 ShapeFlags, 给赋值类型 ——> vnode.ts // 3. 在 renderer 中使用 ShapeFlags // 位运算的方式 -> 提高性能 -> 但是可读性不高
4. 在初始化 `vnode `时,声明 `ShapeFlags`, 给赋值类型 ,` vnode.ts`
```typescript
/* vnode.ts */
export function createVNode(type, props?, children?) {
const vnode = {
type, // 组件类型
props,
children,
el: null
// 1. 初始化 ShapeFlags
shapeFlag: getShapeFlag(type)
}
// 2. 处理 children
if (typeof children === 'string') {
// 如果 children 是 string , 给 vnode.ShapeFlag 进行赋值
// vnode.ShapeFlag | ShapeFlags.text_children 进行一个 或 | 运算
// vnode.ShapeFlag = vnode.ShapeFlag | ShapeFlags.text_children
// 简化
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
} else if (Array.isArray(children)) {
// 如果 children 是数组,赋值 ShapeFlag 为 数组
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
}
return vnode
}
// 这里判断类型
function getShapeFlag(type) {
// 设置返回类型
// 这里初始化时,先判断VNode的类型, Element || Component
return typeof type === 'string' ? ShapeFlags.ELEMENT : ShapeFlags.STATEFUL_COMPONENT
}
- 在
renderer.ts
代码逻辑中, 使用shapeFlags
, 判断类型 ```typescript / renderer.ts /
function patch(vnode, container) { // 实现 shapeFlag - vue // shapeFlag 的作用是, 描述当前节点的类型,是一个 Element 还是一个组件 | children 是一个字符串 还是一个数组
// 使用 ShapeFlags -> 进行判断 类型 const { shapeFlag } = vnode // 这里判断 vnode.type 类型 -> ELEMENT if (shapeFlag & ShapeFlags.ELEMENT) { processElement(vnode, container) // 这里判断是否组件类型 -> STATEFUL_COMPONENT } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 如果 vnode.type 是 object 类型 , 表示它是 组件类型 processComponent(vnode, container) } }
// 实现判断 children function mountElement(vnode: any, container: any) { // 3. children, 解析然后赋值 // children 有两种情况, 是一个数组, 或者是一个字符串
// 解构出 shapeFlag const { children, shapeFlag } = vnode // 这里也是用 shapeFlag 判断 children 的类型 -> 可能是数组,可能是字符串 // 如果是一个字符串 -> TEXT_CHILDREN if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 直接设置 el 的文本内容 el.textContent = children // 如果是一个数组 -> ARRAY_CHILDREN } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 使用 mountChildren 重构 children - Array 的渲染 mountChildren(vnode, el) } }
打包项目 `yarn build`打包,检测 `index.html`没有报错说明实现了 `shapeFlags`
<a name="qv4em"></a>
# 4.8 **实现注册事件功能**
实现注册事件功能
1. 写测试的逻辑 , 再测试组件`App` 中添加测试的事件
```typescript
/* App.js */
window.self = null
export const App = {
render() {
window.self = this
return h("div", {
id: 'test1-id',
class: 'test2-class',
// 添加事件
onClick() {
console.log('click')
},
}, "hello " + this.msg
)
},
setup() {
return {
msg: 'Hello World'
};
}
}
实现注册事件的主要逻辑和流程: 在初始化 Element
的主流程时,在 mountElement
中处理 props
时,循环key
拿到 props 对应的属性值 。
只需要判断这个属性值是否为 props
一个事件名,再使用 addEventListener()
方法设置对应的事件挂载到元素上即可
- 在处理
props
属性中,判断 key 是否为click
事件renderer.ts
```typescript
/ renderer.ts /
function mountElement(vnode, container) { / 其他代码 /
// 2. 设置属性 props // 处理 组件 注册事件 const { props } = vnode if (props) { for (const key in props) { // 先实现判断 click 的逻辑 // 具体的 针对于 click -> 通用 (小步骤的开发思想) if (key === ‘onClick’) { // 如果key时click -> 将click 添加到 el上 el.addEventListener(‘click’, props[key]) } else { // 设置属性值 el.setAttribute(key, props[key]) } } } }
实现`click`<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25614690/1658653869183-4a3f540f-1e89-4f7b-92b8-280400439b04.png#clientId=ue0843e6d-644a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=299&id=ud1a4735d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=374&originWidth=692&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23223&status=done&style=none&taskId=u393356f4-90fe-4733-a0ce-aa66d77f686&title=&width=553.6)
3. 重构代码,实现通过的注册事件
因为是事件名称都是 `on + 事件名`的起名方式, `click -> onClick & onMouseDown -> MouseDown`
```typescript
/* renderer.ts*/
// 重构-> 实现通用注册事件
function mountElement(vnode, container) {
/* 其他代码 */
// 2. 设置属性 props
// 处理 组件 注册事件
const { props } = vnode
if (props) {
for (const key in props) {
// 实现通用的,可以根据 组件的命名规范,来实现,
// 因为 on + 事件名称 : click -> onClick , onMouseDown -> MouseDown
// 可以使用正则方式,匹配 on 之后的事件名,来注册不同事件
// 封装 通过注册方法 isOn
const isOn = (key) => /^on[A-Z]/.test(key);
if (isOn(key)) {
// 获取事件名称
const event = key.slice(2).toLowerCase();
el.addEventListener(event, val);
}
else {
// 如果不是 on 之后的事件名称
// 设置 el 的属性值
el.setAttribute(key, val);
}
}
}
}
在App 中添加其他事件
/* App.js */
window.self = null
export const App = {
render() {
window.self = this
// ui 逻辑
return h('div', {
class: 'test-class-id',
onClick: () => {
console.log('click 事件触发')
},
onMouseDown: () => {
console.log('mouse down 事件触发')
},
id: this.msg
}, this.msg)
}
}
测试功能
执行打包命令 yarn build
重新打包, 在 index.html 中 点击标签, 检测是否初始事件 , 没有出现问题说明是西安注册事件功能
// 实现 props 属性中 注册事件的功能
// 实现逻辑
/**
* 1. 在处理 props 的逻辑中, 判断 key 是否是特殊的方法
* 2. 在对 key 对应的值,绑定对应的事件
*/
4.9 实现 Props
props
是 setup 的第一个参数,用于在 父组件 & 子组件中传参
初始化实现 Props 的文件 , 创建 ./example/componentProps
文件夹。 赋值HelloWorld
的内容到里面 , 添加 Foo.js
文件,相当于一个组件
App.js
的逻辑
import { Foo } from './Foo.js'
/* App.js */
window.self = null
export const App = {
render() {
window.self = this
// ui 逻辑
return h('div', {
class: 'test-class-id',
onClick: () => {
console.log('click 事件触发')
},
onMouseDown: () => {
console.log('mouse down 事件触发')
},
id: this.msg
}, [
h('div', {}, "hello + Foo + " + this.msg),
// 添加渲染 Foo 组件
h(Foo,
// 传入 props 参数
{
count: this.count
})
])
},
setup() {
let count = 0
return {
msg: 'Hello World',
count
};
}
}
Foo.js
的逻辑
/* Foo.js */
export const Foo = {
// 1. setup 接收 App组件传入的props
setup(props) {
// 1.1 实现 setup 访问 props
// 分析:只需要在调用setup时候把 props 传入 setup 函数即可
// 实现:
// 1. 在初始化setup 时候, 声明 initProps(), 把 instance 和 instance.vnode.props 传递进去
// 2. initProps() 的逻辑-> 把props 挂载到 instance 上
// 3. 在调用 setup() 时候传入 instance.props 作为参数
console.log(props)
// 3. props 是 只读的 Shallow readonly 的
// 实现:
// 1. 只需要在调用setup(props) 是否 设置 props 是 shallowReadonly 就行
// 注意:
// - props 在初始化时候位 null -> 在执行shallowReadonly 是 target 必须是一个对象,
// 所以在initProps时候 初始化 props 时,如果为null, 设置props 为一个 空对象 {}
props.count++ // 修改 count 会报错
console.log(props)
},
render() {
// 2. 在 render 中 通过 this.count 能够访问 props.count 的值
// 分析:需要在 render() 中 this 调用 xxx,
// 也就是之间实现 this.xxx = xxx -> 创建的代理对象中, 访问key, 然后拿到它的返回值, props 也可以通过这样的方式实现
// 实现:
// 1. 在实现代理对象Proxy的中的 PublicInstanceProxyHandlers 中 处理 props 的返回值
// 2. PublicInstanceProxyHandlers 重构 通过key拿到返回值的方法, 通过 hasOwn(obj, key) 来判断对象具有的属性值
// 3. 通过解构的方法 拿到 props 对象 , 再使用 hasOwn(props, key) 判断, 返回 props[key] 的值
return h("div", {}, "这里是Foo 组件" + this.count)
}
}
在访问 index.html
页面结果
- 实现
setup
访问props
, 分析:只需要在调用setup
时候把props
传入setup
函数即可 ,
实现:
1. 在初始化setup
时候, 声明过initProps()
instance
和 instance.vnode.props
传递进去
2.initProps()
的逻辑-> 把props
挂载到 instance
上
3. 在调用 setup()
时候传入 instance.props
作为参数
4 实现 props
只读的, 在实现响应式时, 使用过 shallowReadonly
,把instance.props
变为可读就行
在创建component
是,初始化 prop
, 设置为一个空对象
/* component.ts*/
export function createComponentInstance(vnode) {
// 创建组件的实例
const component = {
// 其他代码
// 初始化 props 为一个对象
props: {},
}
return component
}
在setupComponent
中, 也就是在执行 setup()
函数前,实现 initProps
/* component.ts*/
// 执行 组件 setup() 逻辑
export function setupComponent(instance) {
// 1. initProps
// 传入 instance 当前组件的实例 & 当前组件的props
initProps(instance, instance.vnode.props)
}
function setupStatefulComponent(instance) {
// ... 其他代码
if (setup) {
// 执行 setup()
// 传入 instance.props 参数
// 使用 shallowReadonly 包裹起来, props 是可读的
const setupResult = setup(shallowReadonly(instance.props))
}
}
创建 runderer-core/componentProps.ts
, 实现 props
的逻辑
/* componentProps.ts*/
// initProps 的逻辑
// 两个参数 -> instance 组件的实例 , rawProps -> 没有经过初始化的 props
export function initProps(instance, rawProps) {
// 逻辑: 只需要把 props 挂载到 instance 上,
// 然后在调用 setup() 时候传输 props 给到就行了
// 需要props 在创建 instance 时 初始化 init
// 如果 rawProps 为空 ,则设置为 {}
instance.props = rawProps || {}
}
此时在 Foo.js
组件里, 已经能够看到 props
输出的结果了
- 实现在组件中的
render()
函数中,通过this.count
能够访问到props.count
分析:需要在 render() 中 this 调用 xxx, 也就是之间实现 this.xxx = xxx -> 创建的代理对象中, 访问key, 然后拿到它的返回值, props 也可以通过这样的方式实现
实现:
1. 在实现代理对象Proxy的中的 PublicInstanceProxyHandlers
中 处理 props
的返回值
2. PublicInstanceProxyHandlers
重构 通过key
拿到返回值的方法, 通过 hasOwn(obj, key)
来判断对象具有的属性值
3. 通过解构的方法 拿到 props
对象 , 再使用hasOwn(props, key)
判断, 返回 props[key]
的值
因为在render()
中通过 this.xxx
访问值,由代理对象返回值的。 可以代理对象中设置在访问 props
属性时,返回的数据
/* componentPublicInstance.ts */
export const PublicInstanceProxyHandlers = {
// 使用 { _: instance } 的方式传递 instance 参数
get({ _: instance }, key) {
// 1. 实现从setupState 拿到值
// const { setupState } = instance
// // 判断 setupState 中是否有 key 属性
// if (key in setupState) {
// // 返回 setupState 中的值
// return setupState[key]
// }
// 实现一个公共方法 hasOwn() 判断 object 是否具有 key 属性
const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key)
// 因为 setup的返回值 setupState ,可以使用判断 object 是否具有 key 属性
// 具有相同的逻辑
// 解构出 props
const { setupState, props } = instance
if (hasOwn(setupState, key)) {
return setupState[key]
} else if (hasOwn(props, key)) {
// 返回 props 中的值
return props[key]
}
}
}
这样在 render()
中就能访问到 props.count
了
重构 : 把hasOwn()
的代码抽离出去到公共的方法
/* shared/index.ts */
// 实现一个公共方法 hasOwn() 判断 object 是否具有 key 属性
export const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key)
重新打包yarn build
,检查 index.html
页面内容,没有出现问题; 实现了 Props
4.10 实现 Emit
setup 的第二个参数是 context
, context
是一个普通的对象,暴露给 setup 中的值,主要有 attrs & slots & emit
这些。 其中 emit
主要用于触发 使用该组件在 props
对象中声明的方法。
例如 : 在声明Foo
组件的props
对象声明了 onAdd
方法, 则在 Foo
中的 setup 中通过 emit('add')
触发定义在 props
的onAdd
方法
实现 Emit
功能之前,初始化, 创建 example/componentEmit
文件夹。 复制App.js & Foo.js & index.html & main.js
文件
在App组件中 Emit
功能需求
/* App.js*/
export const App = {
name: 'App', // 组件name -> 根组件
// 实现 emit 功能
render() {
return h('div', {}, [
h('div', {}, "App"),
h(Foo,
// Foo 组件的 props
{
// 接收 emit 事件
// 这里接收的 onAdd 和 在 Foo 组件调用的 emit('add') 名字不同
// 在这里注册事件都是 on + 时间名 -> on + Add
onAdd(a, b) {
// 获取到参数 a ,b -> emit()传入参数
console.log('onAdd -> emit触发 add 事件 回调函数', a, b)
},
onAddFoo(a, b) {
console.log('onAddFoo -> ', a, b)
}
})
])
},
}
在Foo.js 组件中使用 Emit
能够调用在 App.js
定义的 Foo
的 prosp内 方法
/* Foo.js */
// 实现 Emit 功能
export const Foo = {
// 1. 实现Emit
// setup 接收的第二个参数 {}, 导出 Emit
// 这里注意: 第二个参数是一个对象 , 而对象里面有一个emit
// 实现:
// - 在调用 setup() 时候传入第二个参数, 是一个对象, 对象中有一个emit 方法
// - 这个emit是一个函数,需要在组件实例中进行初始化 -> emit: instance.emit -> 初始化emit: () => {}
// - 定义 emit 方法,将他挂载到 component.emit 上面
// 关系: Foo组件 emit('事件名') -> instance.emit这里只是函数名 -> component.emit 初始化的 emit = 创建的 emit("事件名") 函数
// - 接下来的逻辑在 componetntEmit.ts 中
setup(props, { emit }) {
const emitAdd = () => {
console.log('emit add —— 在setup() 中被调用')
// 2. 当调用 emit 时,传入一个事件名,
// - 在找事件时候,需要找当前组件 props 中有没有这个事件函数
// - 实现: 匹配 on + 事件名 -> 需要首事件字母大写
// 当执行 emitAdd 时,会触发 App 组件的 emit 事件
// emit('add')
// 实现emit 传参功能,
// 1, 2 能够在 App 组件中获取到传入的参数
// 实现逻辑
// - 在实现emit逻辑中,使用解构 ...args 获取传入的参数, 然后调用 handler(...args) 是传入接收的参数就行
emit('add', 1, 2)
// 实现事件的命名方式 on-add
// 实现逻辑-> add-foo 在实现emit 时候转为 AddFoo 命名
// 只需要把 - 去掉, 首字符变为大写
emit('add-foo', 1, 2)
}
// 返回 emitAdd 函数
return {
emitAdd
}
},
render() {
// 声明一个点击事件, 当点击这个按钮时, 触发 emit -> 调用App中的xxx方法
const btn = h('button', {
// 声明click 为 this.emitAdd
onClick: this.emitAdd
}, 'emitAdd')
const foo = h('p', {}, 'Foo组件')
// 返回Element逻辑
return h('div', {}, [foo, btn])
}
}
此时的页面
- 实现
Emit
setup
接收的第二个参数 {}, 导出Emit
这里注意: 第二个参数是一个对象 , 而对象里面有一个emit
实现:
- 在调用 setup()
时候传入第二个参数, 是一个对象, 对象中有一个emit
方法
- 这个emit
是一个函数,需要在组件实例中进行初始化 -> emit: instance.emit
-> 初始化emit: () => {}
- 定义 emit
方法,将他挂载到 component.emit
上面 ->component.emit = emit as any
关系: Foo组件 emit('事件名') == instance.emit (这里只是函数名) == component.emit 初始化的emit = 创建的 emit("事件名") 函数
在初始化component
时,在组件实例中添加 emit
方法 -> 初始化
并且在调用 setup
时传入 { emit: instance.emit }
参数
/* component.ts */
export function createComponentInstance(vnode) {
const component = {
// 其他代码 ...
// 初始化 emit -> 是一个函数
emit: () => {}
}
return component
}
// 调用 setup 时,传入 emit 参数
function setupStatefulComponent(instance: any) {
// 其他代码 ...
if (setup) {
// 实现emit -> 第二个参数{} -> 对象中有 emit
// 将 emit 默认赋值给 instance.emit
const stupResult = setup(shallowReadonly(instance.proxy), { emit: instance.emit })
}
}
在创建创建组件时,对 emit
方法进行一个接收函数 emit
创建 componentEmit.ts
导出 emit方法
/* componentEmit.ts */
// 导出 emit 函数
// 接收 Foo组件 中 emit('事件名') -> 中事件名 参数
export function emit(event) {
// 检测 emit 的传参
console.log('emit', event, '在 emit实现逻辑中调用')
}
/* component.ts */
export function createComponentInstance(vnode) {
const component = {
// 其他代码 ...
// 初始化 emit -> 是一个函数
emit: () => {}
}
// 导入 emit, 并使用
component.emit = emit as any
return component
}
这里就建立了 组件中 emit 的关系
关系: Foo组件的emit('事件名') == instance.emit (这里只是函数名) == component.emit 初始化的emit = 创建的 emit("事件名") 函数
此时页面内容
实现 componentEmit.ts
逻辑
1. 找到 instance.props 中有没有这个 event 对应的 函数名和回调函数
-> 实现: 传入 instance 拿到 props
-> 因为在Foo组件的emit 只会传入一个事件名的参数, 不是两个参数
-> 可以在componet.emit 调用 emit()时候 使用 bind() -> emit.bind(null, component), 代表传入第一个函数是this,第 二个参数是 emit接收的第一个参数instance 这是一个代码技巧
实现在componentEmit
中能够拿到 instance
才能做到返回 emit
对应的函数内容
/* component.ts */
export function createComponentInstance(vnode) {
const component = {
// 其他代码 ...
// 初始化 emit -> 是一个函数
emit: () => {}
}
/* 可以在componet.emit 调用 emit()时候
使用 bind() -> emit.bind(null, component), 代表传入第一个函数是this,第
二个参数是 emit接收的第一个参数instance 这是一个代码技巧*/
component.emit = emit.bind(null, component) as any
return component
}
在 emit
中接收到 instance
组件实例
/* componentEmit.ts */
// 导出 emit 函数
// 接收 Foo组件 中 emit('事件名') -> 中事件名 参数
export function emit(instance, event) { // 接收到 instance 组件实例
// 检测 emit 的传参
console.log('emit', event, '在 emit实现逻辑中调用')
}
- 由
instance
取到props
, 根据key
拿到对应的事件函数 ```typescript / componentEmit.ts /
// 导出 emit 函数 // 接收 Foo组件 中 emit(‘事件名’) -> 中事件名 参数 export function emit(instance, event) { // 接收到 instance 组件实例 console.log(‘emit’, event, ‘在 emit实现逻辑中调用’)
// TPP 开发技巧 :先去写一个特定的行为,再去重构为通过的行为 const { props } = instance; // 判断传入过来的参数事件名是不是一个 add const handler = props[‘onAdd’] // 判断 handler 有没有, 有就调用, 也就是执行 App中的Foo组件的props 定义的事件 handler && handler() }
重构 -> 处理字符串 -> `add` 首字母大写 -> `on + 事件`
```typescript
/* componentEmit.ts */
export function emit(instance, event) { // 接收到 instance 组件实例
// TPP 开发技巧 :先去写一个特定的行为,再去重构为通过的行为
const { props } = instance;
// 将 event -> add 变为 Add -> 首字符大写
const capitalize = (str: string) => {
// 将首字符获取到,并转换为大写,然后拼接后面的字符串
return str.charAt(0).toUpperCase() + str.slice(1);
}
// 处理 on + 事件 的行为
const toHandlerKey = (str: string) => {
// 如果 str 有值,执行首字符大写,没有返回空字符串
return str ? "on" + capitalize(str) : ""
}
const handlerName = toHandlerKey(event)
// 根据props中的key, 找到对应的回调函数, 然后执行
const handler = props[handlerName]
handler && handler()
}
重构, 让事件名能够以 add-foo
的形式命名
/* componentEmit.ts */
export function emit(instance, event) { // 接收到 instance 组件实例
// TPP 开发技巧 :先去写一个特定的行为,再去重构为通过的行为
const { props } = instance;
// 实现 add-foo 变为 AddFoo
const camelize = (str: string) => {
// 返回一个正则替换
return str.replace(/-(\w)/g, (_, c: string) => {
return c ? c.toUpperCase() : ""
})
}
// 将 event -> add 变为 Add -> 首字符大写
const capitalize = (str: string) => {
// 将首字符获取到,并转换为大写,然后拼接后面的字符串
return str.charAt(0).toUpperCase() + str.slice(1);
}
// 处理 on + 事件 的行为
const toHandlerKey = (str: string) => {
// 如果 str 有值,执行首字符大写,没有返回空字符串
return str ? "on" + capitalize(str) : ""
}
// 在这里使用 - 转为 驼峰命名 的函数 camelize()
const handlerName = toHandlerKey(camelize(event))
// 根据props中的key, 找到对应的回调函数, 然后执行
const handler = props[handlerName]
handler && handler()
}
- 实现
event
事件能够传递参数;达到Foo
组件,传递参数emit('add', 1,2)
在App->Foo->props->event
能够接收参数 ```typescript / Foo.js/
// - 接下来的逻辑在 componetntEmit.ts 中 setup(props, { emit }) { const emitAdd = () => { console.log(‘emit add —— 在setup() 中被调用’)
// 实现emit 传参功能,
// 1, 2 能够在 App 组件中获取到传入的参数
// 实现逻辑
// - 在实现emit逻辑中,使用解构 ...args 获取传入的参数,
// 然后调用 handler(...args) 是传入接收的参数就行
emit('add', 1, 2)
// 实现事件的命名方式 on-add
// 实现逻辑-> add-foo 在实现emit 时候转为 AddFoo 命名
// 只需要把 - 去掉, 首字符变为大写
emit('add-foo', 1, 2)
} // 返回 emitAdd 函数 return { emitAdd } },
在实现emit逻辑中,使用解构 ...args 获取传入的参数 , 然后调用 handler(...args) 是传入接收的参数就行
```typescript
/* componentEmit.ts */
export function emit(instance, event, ...args) { // ...args 是Foo组件传入过来的参数
//... 其他代码
// 将 args 传入 handler 函数中,然后在APP组件上 emit(a,b)接收参数
handler && handler(...args)
}
查看控制台, 输出参数
- 重构把
componentEmit
中可能复用的代码抽离到shared
中 ```typescript / componentEmit.ts /
export function emit(instance, event, …args) { const { props } = instance; // 使用重构 const handlerName = toHandlerKey(camelize(event)) const handler = props[handlerName] handler && handler(…args) }
```typescript
/* shared/index.ts */
// 实现 add-foo 变为 AddFoo
export const camelize = (str: string) => {
// 返回一个正则替换
return str.replace(/-(\w)/g, (_, c: string) => {
return c ? c.toUpperCase() : ""
})
}
// 将 event 的 add 变为 Add -> 首字符大写
const capitalize = (str: string) => {
// 将首字符获取到,并转换为大写,然后拼接后面的字符串
return str.charAt(0).toUpperCase() + str.slice(1);
}
// 处理 on + 事件 的行为
export const toHandlerKey = (str: string) => {
// 如果 str 有值,执行首字符大写,没有返回空字符串
return str ? "on" + capitalize(str) : ""
}
重新执行打包命令 yarn build
检查index.html
, 没有出现问题, 表示实现了 Emit
功能
4.11 实现 slots 功能
插槽的功能:父组件需要传递一些模板片段给子组件,子组件通过 插槽,让传递过来的内容渲染到这些片段
例如
// FancyButton 父组件
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
// FancyButto
<button class="fancy-btn">
<slot></slot> <!-- 插槽插口 -->
</button>
// 最终渲染的 DOM
<button class="fancy-btn">
Click me!
</button>
1. 初始化slots
&& this.$slots
基本的实现
在 example/componentSlots/
创建 App & Foo & main & index.html
/* App.js */
export const App = {
name: 'App',
// 根组件
// 实现 Slot 功能
render() {
// 定义组件
const app = h('div', {}, "这是App 组件")
// 在父组件的 Foo中, 指定 Slot 的内容
const foo = h(Foo, {},
// 在Foo 组件中能够渲染出来这里的内容
// 1. 事件slot的基本需求,-> 在Foo中展示
h('p', {}, "这是在父组件中slot, 传入Foo的内容"), // children 内容
)
// 实现 slot 的目标就是:将h() 渲染出来的节点,添加到 Foo 组件内部
return h('div', {}, [app, foo])
},
}
因为 slots 标签内容是在 父组件中定义, 在子组件通过拿到 父组件的 VNode.children
属性, 也就是拿到的children
中的VNode
, 在子组件渲染出来就行
关系过程: Foo -> App.children -> render.slots
// 实现 Slot 功能
export const Foo = {
// 实现 Slot 的功能
// 因为 slot标签内容是在父组件中,在 Foo 组件 通过拿到 App 的 VNode .children 属性,
// 也就拿到 childlen 中的 VNode。
// 在 Foo 组件中 渲染出来就行
// 关系过程 -> Foo -> App.children -> render slot 在Foo中渲染 slots
// 1. 通过使用 this.$slots 渲染 App.children 的内容
// 实现 :
// 1. 初始化 $slots API -> component 初始化 slots -> componentPublicInstance 实现访问 $slots 返回内容
// 2. initSlots -> 逻辑:将 vnode.children 赋值给 instance.slots
// 3. 此时已经可以通过 this.$slots 拿到 App 需要渲染在Foo 中的 节点内容了
render() {
console.log(this.$slots)
// 定义 Foo 组件
const foo = h('p', {}, "这是Foo组件")
// 在这里渲染 this.$slots
// 1. 基本的功能
return h('div', {}, [foo, this.$slots])
}
}
实现通过 this.$slots
访问App.children
的内容 , 也就拿到了父组件定义插槽的内容
初始页面
实现 this.$slots
1. 通过使用 this.$slots 渲染 App.children 的内容
实现 :
1. 初始化 $slots API -> component 初始化 slots -> componentPublicInstance 实现访问 $slots 返回内容
2. initSlots -> 逻辑:将 vnode.children 赋值给 instance.slots
3. 此时已经可以通过 this.$slots 拿到 App 需要渲染在Foo 中的 节点内容了
初始化
$slots
API/* component.ts */
// 返回一个component对象
const component = {
// 其他代码
// 初始化 slots
slots: {},
}
```typescript / src/runtime-core/componentPublicInstance.ts / // 定义 $slots API
const publicPropertiesMap = { $el: (instance) => instance.vnode.el // 初始化 $slots ,返回 instance.slots 的内容 -> App.children $slots: (instance) => instance.slots }
- initSlots 的实现
```typescript
/* src/runtime-core/component.ts */
// 对组件初始化的逻辑函数
export function setupComponent(instance) {
// 解析处理组件的其他内置属性 比如: props , slots 这些
// 2. 初始化 slots
// initSlots()
//初始化 initSlots 给定两个参数, instance, 和 children
initSlots(instance, instance.vnode.children)
}
/* src/runtime-core/componentSlots.ts */
// initSlots
// 初始化 Slots
export function initSlots(instance, children) {
// 将 children 赋值给 instance.slots
instance.slots = children
}
完成 $slots
的基本功能 , 内容就是 App 传入的 虚拟节点
当slots 是一个数组时
基本的slots 只能实现单个虚拟节点的展示, 当多个时 slots 变为了 Array
/* App.js*/
export const App = {
render() {
const foo = h(
Foo,
{},
// 在Foo 组件中能够渲染出来这里的内容
// 1. 事件slot的基本需求,-> 在Foo中展示
// h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
// 2. 展示多个内容时 -> 在Foo中展示
[
h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
h('p', {}, "这是让slots实现 Array功能")
]
)
return h('div', {}, [app, foo])
},
}
此时的 slots
解决:
在App中,目前只能支持 h(), 让 App 能够实现 [h(), h()] 多个内容
2. 当传入过来的 this.$solts 是一个 Array的 VNode,而不是一个 VNode
children -> Array VNode
处理逻辑:把内部的 this.$slots 转为虚拟节点 Array VNode-> VNode
解决: 把 $slots 直接再次包裹 h()
实现:
1. 这里通俗使用h("div", {}, [foo, this.$slots])) -> 进行一个重构, 使用 renderSlots函数,来渲染,
2. 传入 this.$slots, 让它创建对应的虚拟节点
3. 导入 renderSlot() 进行使用
主要就是为了简化 h()
把 $slots 直接再次包裹 h()
/* Foo.js */
export const Foo = {
render() {
console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
const foo = h("p", {}, "这是Foo组件")
// 1. 基本的功能
// return h('div', {}, [foo, this.$slots])
// 2. App 中渲染多个内容
return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
}
}
重构: 使用 renderSlots函数,封装 h() , 主要用于渲染 Array 的 slots
/* Foo.js */
export const Foo = {
render() {
console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
const foo = h("p", {}, "这是Foo组件")
// 1. 基本的功能
// return h('div', {}, [foo, this.$slots])
// 2. App 中渲染多个内容
// return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
// 导入 renderSlots
return h('div', {}, [foo, renderSlots(this.$slots)])
}
}
在 src/runtime-core/helper/renderSlot.ts
定义
/* src/runtime-core/helper/renderSlot.ts */
export function renderSlots(slots) {
// 使用 createVNode() 代替 h(xxx, {} xxx)
// createVNode() 接收的参数,前两位 div {}
// 这里只是实现了 Array -> 没有实现 slots 是单个组件的情况
return createVNode('div', {}, slots)
}
导入 renderSlot
/* index.ts */
export { renderSlots } from './helpers/renderSlots'
重构完成
出现问题: 在检查 实现1 , 发现之前实现的单组件已经渲染不行了, 因为单个虚拟节点,会转为Object, 而不是Array,使用renderSlot
渲染不出来, 修改 renderSlots
的逻辑
解决: 判断 slots VNode, 如果是 Array 就使用 createVNode() 代替 h(xxx, {} xxx) , 如果不是 表示单个虚拟节点,直接返回就行
/* src/runtime-core/helper/renderSlot.ts */
export function renderSlots(slots) {
// 这里的逻辑,就是当 this.$slots 是一个数组时
// 转到 initSlots() 中进行判断
// 需要对 slots 进行判断
return Array.isArray(slots) ? createVNode('div', {}, slots) : slots
}
这样就完成了针对第二种情况的实现,但是此时第一种情况就无法正常渲染,为了同时包括两种情况,可以在初始化 slots 时进行处理,若 children 是一个 VNode 则将其转为数组,完善src/runtime-core目录下的componentSlots.ts
文件中的initSlots
函数:
/* componentSlots.ts */
export function initSlots(instance, children) {
instance.slots = Array.isArray(children) ? children : [children]
}
2. 实现具名插槽
具名插槽指的是,在由父组件向子组件传入插槽时传入一个对象,其中 key 为插槽的 name,用于指定插槽的位置,value 为插槽,而在子组件中将插槽的 name 作为第二个参数传入renderSlots
函数来指定该位置要渲染的插槽。
/*App.js*/
export const App = {
render() {
const foo = h(
Foo,
{},
// 在Foo 组件中能够渲染出来这里的内容
// 1. 事件slot的基本需求,-> 在Foo中展示
// h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
// 2. 展示多个内容时 -> 在Foo中展示
// [
// h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
// h('p', {}, "这是让slots实现 Array功能")
// ]
// 3. 实现具名插槽
// 将 children 转为 Object; 实现具名插槽 key -> value
{
header: h('p', {}, "这个具名插槽的内容 hander"),
footer: h('p', {}, "这个具名插槽的内容 footer")
}
)
return h('div', {}, [app, foo])
},
}
/* Foo.js */
export const Foo = {
render() {
console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
const foo = h("p", {}, "这是Foo组件")
// 1. 基本的功能
// return h('div', {}, [foo, this.$slots])
// 2. App 中渲染多个内容
// return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
// 导入 renderSlots
// return h('div', {}, [foo, renderSlots(this.$slots)])
// 3. 实现具名插槽
// 1. 获取要渲染的元素
// 2. 获取到渲染的位置
return h('div', {}, [
renderSlots(this.$slots, "header"),
foo, // 定义的 foo 组件
// slot 的内容
renderSlots(this.$slots, "footer")
]
)
}
}
实现逻辑
- 获取到要渲染的
虚拟节点
- 获取到要渲染的
位置
```typescript / src/runtime-core/componentSlots.ts / // 处理 Slots 的逻辑中
export function initSlot(instance, children) {
const slots = {} // 循环 children 数组 for (const key in children) { // 取到值 const value = children[key]
// 赋值给 slots
slots[key] = Array.isArray(value) ? value : [value];
}
// 最后赋值给 instance.slots instance.slots = slots }
完善 `renderSlots()` 函数
```typescript
/* helpers/renderSlots.ts */
// 导出
export function renderSlots(slots, name) { // 增加参数 name
// 传入参数 name 实现具名插槽
// slots[name] 通过 key 获取值
const slot = slots[name];
if (slot) { // 判断是否具有
// 具有 slot, 直接渲染
return createVNode('div', {}, slot)
}
}
此时页面
重构 componentSlots
export function initSlots(instance, children) {
// 重构
normalizeObjectSlots(children, instance.slots)
}
// 实现赋值
function normalizeObjectSlots(children, slots) {
for (const key in children) {
const value = children[key];
// 实现作用域插槽逻辑
// 这里拿到 value 是一个函数
// 需要进行调用, 实现传参
slots[key] = normalizeSlotValue(value)
}
function normalizeSlotValue(value) {
return Array.isArray(value) ? value : [value]
}
3. 实现作用域插槽
在官网上定义的作用域插槽 : 同时使用父组件域内和子组件域内的数据, 要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。
/*App.js*/
export const App = {
render() {
const foo = h(
Foo,
{},
// 在Foo 组件中能够渲染出来这里的内容
// 1. 事件slot的基本需求,-> 在Foo中展示
// h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
// 2. 展示多个内容时 -> 在Foo中展示
// [
// h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
// h('p', {}, "这是让slots实现 Array功能")
// ]
// 3. 实现具名插槽
// 将 children 转为 Object; 实现具名插槽 key -> value
// {
// header: h('p', {}, "这个具名插槽的内容 hander"),
// footer: h('p', {}, "这个具名插槽的内容 footer")
// }
// )
// 4. 实现作用域插槽
{
// value 改为 函数
// age 作为参数, 由子组件传入
header: ({ age }) => h('p', {}, "这个作用域插槽的内容 hander + " + age),
footer: () => h('p', {}, "这个作用域插槽的内容 footer")
}
)
return h('div', {}, [app, foo])
},
}
/* Foo.js */
export const Foo = {
render() {
console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
const foo = h("p", {}, "这是Foo组件")
// 1. 基本的功能
// return h('div', {}, [foo, this.$slots])
// 2. App 中渲染多个内容
// return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
// 导入 renderSlots
// return h('div', {}, [foo, renderSlots(this.$slots)])
// 3. 实现具名插槽
// 1. 获取要渲染的元素
// 2. 获取到渲染的位置
// return h('div', {}, [
// renderSlots(this.$slots, "header"),
// foo,
// // slot 的内容
// renderSlots(this.$slots, "footer")
// ]
// )
// 4. 实现作用域插槽
// 定义一个变量,在子组件中传入,让父组件能够访问到
// 父组件使用作用域插槽需要变为 函数式: header: ({age}) => h('p', {}, "这个具名插槽的内容 hander" + age),
// 实现
// 1. 使用子组件传入 { age } 参数
// 2. 在 renderSlots 中,改变 slot, 因为 slot 已经是一个函数了
// 3. 在 initSlots 中,修改代码, 此时的 value 已经是一个函数, 需要调用才能获取到返回值, 并且实现函数的传参
const age = 18
return h('div', {}, [
// 传入 age 参数 -> { age }, renderSlots 的第三个参数
renderSlots(this.$slots, "header", { age }),
foo,
// slot 的内容
renderSlots(this.$slots, "footer")
]
)
}
}
在 renderSlots
处理传入的参数
// 导出
export function renderSlots(slots, name, props) { // 增加参数 name props
// 传入参数 name 实现具名插槽
// slots[name] 通过 key 获取值
const slot = slots[name];
if (slot) { // 判断是否具有
// 具有 slot, 直接渲染
// 实现 作用域插槽时 slot 是一个函数
if (typeof slot === "function") {
return createVNode('div', {}, slot(props)) // 执行函数 并传入参数
}
}
}
在 initSlots
中调用 value
, 并传入参数
export function initSlots(instance, children) {
// 重构
normalizeObjectSlots(children, instance.slots)
}
// 实现赋值
function normalizeObjectSlots(children, slots) {
for (const key in children) {
const value = children[key];
// 实现作用域插槽逻辑
// 这里拿到 value 是一个函数
// 需要进行调用, 实现传参
slots[key] = (props) => normalizeSlotValue(value(props))
}
function normalizeSlotValue(value) {
return Array.isArray(value) ? value : [value]
}
4. 完善 slots 功能
在创建 VNode
时,判断当前的节点是否是 slot
类型的节点 , 新增
/* src/runtime-core/vnode.ts */
// 添加处理当children是一个 object 时 -> 也就是具有 slots
// 判定条件 : 是一个组件 + 具有children 是一个 object
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 如果当前节点是一个组件
if (typeof children === "object") { // 并且 children 是一个 object -> slots
// 也就说明当前节点是一个 具有 slot 的逻辑
// 给当前节点 赋值为具有 slot 的状态
vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN
}
}
// 返回创建组件的虚拟节点
return vnode
/* src/shared/ShapeFlags.ts */
{
// 添加 当前节点的 slots 的状态
SLOT_CHILDREN = 1 << 4 // 1 左移 4 位 -> 10000 -> 转为二进制 -> 10000
}
在初始化 initSlots 时增加判断
/* src/runtime-core/componentSlots.ts */
// 初始化 Slots
export function initSlots(instance, children) {
// 重构
// 并不是所有的节点都是具有 slots 的
// 所以需要一个 判断逻辑 和 ShapeFlags 进行的判断一样
// 1. 在初始化 ShapeFlags 时, 添加判断 slots 是否存在 的逻辑
// 2. 添加 Slots 的状态 到 ShapeFlags 中
// 3. 判断当前节点是否具有 slots
const { vnode } = instance
if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
// 执行 slot 的逻辑
// 再次重构
normalizeObjectSlots(children, instance.slots)
}
}
/* 其他代码 */
打包yarn build
, 测试 页面, 没有出现报错, 则实现Slots
4.12 实现 Fragment 功能
在实现插槽中, 使用 createVNode()
方法使用了 div
包裹 slots 的内容 , 这样处理不太合理。 多增加一层 div
可以使用 Fragment
对插槽进行包裹, 而处理 Fragment
时直接调用 mountChildren
函数,对VNode 进行渲染
- 定义
Fragment
```typescript / shared/index.ts /
// 定义 Fragment export const Fragment = Symble(“Fragment”)
- 给 `renderSlots.ts` 使用 `Fragment`标签
```typescript
/* helpers/renderSlots.ts */
// 用于利用 Fragment 对插槽进行包裹
export function renderSlots(slots, name, props) {
// 通过 name 获取创建相应插槽的方法
const slot = slots[name]
if (slot) {
if (typeof slot === 'function') {
// 将创建插槽方法的执行结果作为 children 传入
return createVNode(Fragment, {}, slot(props))
}
}
}
在 renderer.ts
中添加判断逻辑
/* renderer.ts*/
function patch(vnode, container) {
// 实现Fragment的判断逻辑
const { type } = vnode // type 组件的类型
switch (type) {
// 如果是 Fragment 包裹的标签
case Fragment:
// 就调用 processFragment 函数
processFragment(vnode, container)
break;
// 如果不是,则走 默认的逻辑
default:
// 判断 vnode 是否是一个组件 还是 Element
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
// if (typeof vnode.type === 'string') {
// 如果是一个 Element
processElement(vnode, container)
} else if (vnode.shapeFlag & ShapeFlags.STATIC_COMPONENT) {
// } else if (isObject(vnode.type)) {
// 如果是一个组件
processComponent(vnode, container)
}
break
}
}
// 处理 Fragment 包裹的标签
function processFragment(vnode, container) {
// 调用 mountChildren
mountChildren(vnode, container)
}
此时页面
4.13 实现 TextNode 功能
在作用域插槽中添加测试,在实现插槽的函数中,返回一个数组,第二项是一个文本字符串, 表示要渲染到页面的文本内容
/* App.js */
const foo = h(
Foo,
{},
// 4. 实现作用域插槽
{
// 添加数组第二项,文本内容
header: ({ age }) => [h('p', {}, "这个作用域插槽的内容 hander + " + age), "这是文本内容"],
footer: () => h('p', {}, "这个作用域插槽的内容 footer")
}
)
return h('div', {}, [app, foo])
},
实现文本节点的渲染与 Fragment
类似
/**
* 实现TextNode 逻辑
* 因为在实际的 Vue3 中,这些text 都是直接写的; 这里使用了 render() 逻辑; 写text节点,使用到 createTextNode()方法,
* 把 text 节点创建 VNode , 然后渲染到页面
*
* 实现
* 1. 在 VNode 中创建 createTextNode() 函数 ; 返回返回一个 createVNode(Text, {}, text);
* 创建Text类型的 虚拟节点, 并且把 text 内容传入children 属性; Text需要初始化定义 与 Fragment 实现相同,不需要实际的 标签包裹
*
* 2. 在 patch()函数中,判断是否Text 类型的节点, 如果是 进行 Text 的渲染
* - 节点的创建 -> 添加节点到页面
*/
- 创建文本节点
```typescript / vnode.ts //* shared/index.ts */
// 定义 Text
export const Text = Symbol("Text")
// 创建 createTextNode // 实现 渲染 文本的逻辑 export function createTextNode(text: string) { // 创建按 Text 类型的 虚拟节点 -> VNode // 在到 patch() 中处理 Text 的 渲染 return createVNode(Text, {}, text) }
导出`createTextNode`
```typescript
// src/runtime-core/index.ts
export { createTextNode } from "./vnode"
在组件中使用 creatTextNode
/* App.js */
const foo = h(
Foo,
{},
// 4. 实现作用域插槽
{
// 添加数组第二项,文本内容
// 直接封装是 对TextNode 进行 包裹
// createTextNode("text")
header: ({ age }) => [h('p', {}, "这个作用域插槽的内容 hander + " + age), createTextNode("这是文本内容")],
footer: () => h('p', {}, "这个作用域插槽的内容 footer")
}
)
return h('div', {}, [app, foo])
},
- 渲染文本节点
在render.ts
中,与 Fragment
的实现一样,Text
文本节点的渲染也是单独处理
/* renderer.ts*/
function patch(vnode, container) {
const { type } = vnode
switch (type) {
case Fragment:
processFragment(vnode, container)
break;
// 用于处理 Text
case Text:
processText(vnode, container)
break
// 如果不是,则走 默认的逻辑
default:
// 其他代码
...
}
}
// 用于处理 Text 文本节点
function processText(vnode, container) {
// 渲染 Text 的虚拟节点的 Vnode 的逻辑
// 通过解构赋值获取 Text 对应 VNode 的 children,即文本内容
const { children } = vnode
// 2. 创建一个 TextNode
// 记得使用 vnode.el 赋值
const textNode = (vnode.el = document.createTextNode(children))
// 3. 添加到 container 内容
container.appendChild(textNode)
}
此时页面 内容
重新打包 yarn build
, 检查页面, 没有报错说明 实现 TextNode 功能
4.14 实现 getCurrentInstance 方法
getCurrentInstance 方法是获取到当前组件的实例对象
getCurrentInstance 只能在 setup 或生命周期钩子中调用
组件定义
/*App.js*/
export const App = {
name: "App",
render() {
// render中渲染了 App , 还渲染了 Foo
return h('div', {}, [h('p', {}, 'currentInstance demo'), h(Foo)])
},
// getCurrentInstance 方法的实现
setup() {
// getCurrentInstance 的作用: https://v3.cn.vuejs.org/api/composition-api.html#getcurrentinstance
// getCurrentInstance -> 获取当前组件的实例对象
// getCurrentInstance 必须要在 setup() 中使用
const instance = getCurrentInstance()
console.log("这是getCurrentInstance 获取的App组件实例:", instance)
}
}
/* Foo.js */
// Foo 组件
export const Foo = {
name: "Foo",
setup() {
// 与App组件实现的逻辑一样 调用 getCurrentInstance 方法 查看当前的组件实例对象
const instance = getCurrentInstance()
console.log("Foo组件实例:", instance)
},
render() {
return h("div", {}, "这是Foo组件")
}
}
实现getCurrentInstance
1. 在component中实现 getCurrentInstance 方法
2. 定义一个全局变量,在 setup() 调用时 给全局变量赋值为当前组件实例
3. getCurrentInstance 返回 这个全局变量
4. 导出 getCurrentInstance 方法
/* component */
// 定义currentInstance 用于记录当前的组件实例
let currentInstance = null
function setupStatefulComponent(instance) {
// 其他代码
// call setup()
const { setup } = component
if (setup) {
// 给currentInstance 赋值
// currentInstance = instance
// 重构
setCurrentInstance(instance)
// 这里调用 setup
const setupResult = setup(shallowReadonly(instance.props), { emit: instance.emit })
// 其他代码
}
}
// 给currentInstance赋值
function setCurrentInstance(instance) {
// 改为在此处赋值, 可以达到一个中间层的位置
// 方便调式代码
currentInstance = instance
}
// 导出这个函数API
export function getCurrentInstance() {
// 返回全局变量; 也就是 instance
return currentInstance
}
导出 getCurrentInstance
/* index.ts */
export { getCurrentInstance } from "./component"
打印输出
完成 getCurrentInstance()
方法的实现,重新打包, 检查运行
4.15 使用 inject & provide 方法
provide & inject
的介绍
无论组件层次解构有多深, 父组件都可以作为其所有子组件的依赖提供者。
这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。
实现基本的 provide & inject
/* example/ApiInject&Provide/App.js */
// Provider 组件
const Provider = {
name: 'Provider',
setup() {
// 使用 provide 方法提供数据
provide('foo', '这是Provider提供的数据 fooValue')
provide('bar', '这是Provider提供的数据 barValue')
},
render() {
// children -> [] ; 渲染 Consumer
return h('div', {}, [h('p', {}, "Provider"), h(Consumer)])
}
}
/**
* 介绍 Provider & Inject Api : https://v3.cn.vuejs.org/guide/component-provide-inject.html#%E5%A4%84%E7%90%86%E5%93%8D%E5%BA%94%E6%80%A7
* provider 提供数据,inject 注入数据, 用在父组件和更深层级的组件之间的数据传输
*
* 无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。
* 父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。
*
* Provider & Inject 只能在 setup 中使用
*/
// Consumer 组件 -> 子组件
const Consumer = {
name: 'Consumer',
setup() {
// 取到 Provider 组件传输的数据
// 使用 inject 方法取数据 -> 根据 key 来取
const foo = inject('foo')
const bar = inject('bar')
return {
foo,
bar
}
},
// 渲染数据
render() {
return h('div', {}, `Consumer渲染数据: - foo: ${this.foo} - bar: ${this.bar}`)
}
}
export default {
name: 'App',
setup() { },
render() {
// App 组件 -children 使用 Provider 组件
return h('div', {}, [h("p", {}, "apiInject"), h(Provider)])
}
}
在 src/runtime-core/
创建 apiInject.ts
实现的逻辑
/* apiInject.ts */
export function provide(key, value) {
// 存数据
// 1. 存到当前组件的 provides 中
// 需要在 component 初始化 provides
// 2. 获取到当前组件的实例 instance
const currentInstance: any = getCurrentInstance()
// 3. 将 key 和 value 存入 currentInstance.provides 中
if (currentInstance) {
const { provides } = currentInstance
provides[key] = value
}
}
export function inject(key) {
// 取数据
// 1. 取到当前组件的实例 instance
const currentInstance: any = getCurrentInstance()
// 2. 根据key 取到 当前组件的 父级的 provides
// parent.provides
if (currentInstance) {
// 取它父级里 存储数据的 parent
// 通过 parent 取到 provides 对象
const parentProvides = currentInstance.parent.provides
// 需要在renderer 中传入 parent
// 返回取到的数据
return parentProvides[key]
}
}
初始化 provides & parent
/*component.ts*/
export function createComponentInstance(vnode, parent) { // 这里传入parent, 当前组件的 父组件
// 返回 vnode 这个组件
const component = {
// 其他代码
// 初始化 provides, 用于存储provide传入的数据
provides: {},
// 初始化 parent, 用于存储父组件, 方便取到 父组件的 parent
parent,
}
}
在 renderer.ts
中 给parent 赋值
/* renderer.ts */
export function render(vnode, container) {
// 调用 path
// 初始化时,patch() 接收第三个参数,也就是之后赋值的 parent -> 赋值父组件的实例
// 但是这里是初始化 patch() parent 为null
patch(vnode, container, null)
}
// 接下来的函数都需要添加这个第三个参数
function patch(vnode, container, parentComponent) {
const { type } = vnode
switch (type) {
case Fragment:
processFragment(vnode, container, parentComponent)
break;
case Text:
processText(vnode, container)
break
default:
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
processElement(vnode, container, parentComponent)
} else if (vnode.shapeFlag & ShapeFlags.STATIC_COMPONENT) {
processComponent(vnode, container, parentComponent)
}
break
}
}
// 之后需要函数方法都需要添加 parentComponent 父组件这个参数
// 这里给 parentComponent 赋值 为当前组件的实例
function setupRenderEffect(instance, vnode, container) {
const { proxy } = instance
const subTree = instance.render.call(proxy) // 将proxy 注入到 render() 中
// 这里 parent 赋值为 instance, 也就是父级组件
patch(subTree, container, instance)
vnode.el = subTree.el
}
导出 provode & inject
/* index.ts */
export { provide, inject } from "./apiInject"
实现跨层级传输
在App 组件中实现一个中间层组件
const Provide = {
name: 'Provide',
setup() {
provide('foo', "FooValue")
provide('bar', "BarValue")
},
render() {
return h('div', {}, [
h('p', {}, 'Provide'),
// 这里改为渲染中间层组件
h(ProviderTwo)
])
}
}
// 增加中间层 组件
// 加上另一种情况
// 在中间层使用 provide() 时
// 在中间层使用 inject 时
const ProviderTwo = {
name: 'ProviderTwo',
setup() {
// 往子组件传输数据
provide("foo", "这是ProviderTwo提供的数据 fooValueTow")
// 往父组件获取数据
const foo = inject('foo')
return {
foo
}
},
render() {
// 渲染中间层 组件的数据
return h('div', {}, [
h('p', {}, 'ProviderTwo 获取到它父级组件的数据 --' + this.foo),
// 这里渲染 Consumer
h(Consumer)
])
}
}
const Consumer = {
name: 'Consumer',
// ...
}
export default {
// ...
}
处理 provides
初始时 & 后台组件时的指向
/* component.ts */
export function createComponentInstance(vnode, parent) {
// 返回 vnode 这个组件
const component = {
// 初始化 provides, 用于存储provide传入的数据
// provides: {},
// 处理 provides 的指向,
// 当 parent 没有值时-> 初始,provides 为 空 ,
// 当 parent 有值时 -> 说明当前组件是一个子组件,provides 指向 parent 的 provides
provides: parent ? parent.provides : {},
// 初始化 parent, 用于存储父组件的 provide
parent,
}
}
当前页面
出现问题: 当前组件使用 provide
存储数据,与父组件 key
相同时, 会取当前组件provide
存储的值, 而不是取父级组件的
解决: 本质是通过原型链的方式查找 provides
, 重点: 把中间层 provides
指向父级组件的 provides
, 这样就形成了一条基于原型链的方式
取数据
子组件具有自己的 provide
和 inject
的值 当子组件没有 后代组件需求的 inject
值数据时,会从父组件的 provide
中查找 (找原型链)
/* apiInject.ts */
export function provide(key, value) {
// 存数据
// 1. 存到当前组件的 provides 中
// 需要在 component 初始化 provides
// 2. 获取到当前组件的实例 instance
const currentInstance: any = getCurrentInstance()
// 3. 将 key 和 value 存入 currentInstance.provides 中
if (currentInstance) {
let { provides } = currentInstance
// provides[key] = value
// 处理 procvide 的指向
// 1. 获取 父级 provides 对象
const parentProvides = currentInstance.parent && currentInstance.parent.provides
// 注意点:这里可能是 init 初始化
// 2. 判断初始化: 当 provides 等于它 父级的 provides 对象时,说明是初始化
if (provides === parentProvides) {
// 初始化时,给 provides 对象赋值 -> 形成原型链
// 改写 provides 指向 parent的 provides : 形成原型链
provides = currentInstance.provides = Object.create(parentProvides)
// currentInstance.provides 当前组件的 provides
}
// 这里赋值
provides[key] = value
}
}
此时页面, 完成原型链的逻辑
添加 inject
方法的两个功能点
- 添加 inject 的默认值
- 默认值可以是一个函数
添加默认值
/* App.js */
// Consumber 组件
const Consumer = {
name: 'Consumer',
setup() {
// 获取 provider 传输过来的数据
const foo = inject('foo')
const bar = inject('bar')
// 添加功能
// 1. 实现默认值
const baz = inject('baz', "默认值Baz")
// 2. 默认值是一个函数
return {
foo,
bar,
baz
}
},
render() {
// 这里渲染 foo 和 bar
// 展示默认值 baz
return h('div', {}, `Consumer渲染数据: - foo: ${this.foo} - bar: ${this.bar} --- baz: ${this.baz}`)
}
}
实现两个功能
/* src/runtime-core/apiInject.ts */
export function inject(key, defaultValue) { // 添加默认值
// 取数据
// 1. 取到当前组件的实例 instance
const currentInstance: any = getCurrentInstance()
// 2. 根据key 取到 当前组件的 父级的 provides
// parent.provides
if (currentInstance) {
// 取它父级里 存储数据的 parent
// 通过 parent 取到 provides 对象
const parentProvides = currentInstance.parent.provides
// 需要在renderer 中传入 parent
if (key in parentProvides) {
// 如果有 key 时
// 返回取到的数据
return parentProvides[key]
} else if (defaultValue) {
// 如果有 defaultValue 默认值时
// return defaultValue
// 如果默认值是 function 时
if (typeof defaultValue === 'function') {
return defaultValue()
}
return defaultValue
}
}
}
展示页面
打包yarn build
检查页面 , 完成 inject & provide
4.16 实现自定义渲染器 custom renderer
自定义渲染器的定义
因为使用到的Vue模板不止是DOM平台,还可以自定义渲染其他平台
createRenderer
函数接受两个泛型参数: HostNode 和 HostElement,对应于宿主环境中的 Node 和 Element 类型。例如,对于 runtime-dom,HostNode 将是 DOM Node 接口,HostElement 将是 DOM Element 接口。
// 实现 渲染 Canvas 平台的API
/**
* 为了让渲染器 能够实现多个 平台接口,不仅能够实现 Element 还能够渲染 Canvas , 所以修改代码让它不单单支持Element
* 而是通过稳定的接口函数来 渲染 Element & Canvas
*
* 需要3接口:
* createElement : 创建 Element
* patchProp : 给Element 添加属性
* insert : 添加标签到 container 容器
*
*
* 实现方式
* 1. 定义一个 createRender({}) 函数 -> 渲染器,传入对应的接口过来
* 2. 把具体的实现函数传入给createRender()
*/
定义自定义渲染器函数createRender()
/* renderer.ts*/
// 导出 自定义渲染函数 createRender
// 参数: options -> 传入对应的接口函数
export function createRender(options) {
// 这里内部的 render() 函数
function render(vnode, container) {
// export function render(vnode, container) {
// 调用 path
patch(vnode, container, null)
}
/* 省略所有代码*/
}
导出 createRender
/* runtime-core*/
export { createRenderer } from "./renderer"
传入 options
对应的接口方法 , vue模板默认渲染 DOM
平台,把DOM平台的接口封装到 runtime-dom/index.ts
/* src/runtime-dom/index.ts */
// 导入createRenderer
import { createRenderer } from '../runtime-core'
// 需要3接口:
// createElement : 创建 Element
// patchProp : 给Element 添加属性
// insert : 添加标签到 container 容器
// 定义 createElement -> 与在render中 createElement 逻辑相同相同
function createElement(type) {
// 返回创建的 Element 标签
return document.createElement(type)
}
// 给Element 添加属性
function patchProp(el, key, value) {
const isOn = () => /^on[A-Z]/.test(key)
if (isOn()) {
// 这里实现注册事件
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
// 添加标签到 parent 容器
function insert(el, parent) {
parent.append(el)
}
// 调用函数 传入 实现Element 渲染的接口
createRender({ // 用一个 {} 包裹
createElement,
patchProp,
insert
})
renderer 逻辑中,使用传入的接口
/* src/runtime-core/renderer.ts */
export function createRender(options) {
// 需要3接口:
// createElement : 创建 Element
// patchProp : 给Element 添加属性
// insert : 添加标签到 container 容器
const {
createElement,
patchProp,
insert,
} = options
/* 省略所有代码*/
// 在 挂载 Element时,使用定义的接口
function mountElement(vnode, container, parentComponent) {
// 1. 创建 element
// 改为使用 createElement 接口
const el = (vnode.el = createElement(vnode.type))
// const el = (vnode.el = document.createElement(vnode.type)) // 这里给 el 赋值
// 2. props
const { props } = vnode
if (props) {
for (const key in props) {
// console.log(key)
const value = props[key]
// 把赋值的逻辑写道 patchProp 方法中
// 这里使用 patchProp 接口
patchProp(el, key, value)
}
}
// 3. children
// 省略代码
// 4. append
// container.appendChild(el)
// 使用 insert 接口
insert(el, container)
}
}
因为在 runtime-core
中实现的 createApp.ts
使用到 render()
函数,而当前封装了createRender()
自定义渲染器, render
函数的逻辑被包裹起来了,访问不到 。
设置creatApp
访问render()
函数的逻辑
createRender()
的返回值
/* src/runtime-core/renderer.ts */
export function createRender(options) {
// createRenderer 自定义渲染器导出的方法
return {
// createAppAPI(render) 传入 render 方法
// 并且拿到 createAppAPI 的返回值, createApp() 函数
// 使用 createApp 命名接收的 createApp
createApp: createAppAPI(render)
}
}
createApp.ts
封装 createApp
方法
/* src/runtime-core/createApp.ts */
// 创建 createAppAPI () 接收 render 函数,
export function createAppAPI(render) {
// 并返回 createApp() 函数 -> 真实的 createApp 函数
return function createApp(rootComponent) {
return {
mount(rootContainer) {
const vnode = createVNode(rootComponent);
render(vnode, rootContainer)
}
}
}
}
在 runtime-dom/index.ts
下接收 createRender.ts
的返回值 ,对createApp
再进一步封装
/* src/runtime-dom/index.ts */
/* 其他代码 */
// 调用createRender函数传入实现Element渲染的接口
// 使用 renderer 接收 createRender 的返回值
const renderer: any = createRender({
createElement,
patchProp,
insert
})
// 这里封装一个 createApp 函数, 直接调用的是 renderer.createApp()
// 这个就是 给用户调用 createApp 接口的函数
export function createApp(...args) {
// 这里是封装的 createApp()
return renderer.createApp(...args)
}
导出 runtime-dom
/* src/index.ts */
// 整体出口
export * from './runtime-dom'
// export * from "./runtime-core"
因为 runtime-dom
作为 runtime-core
的上一层,把 导出runtime-core
放到 runtime-dom
中
/* src/runtime-dom/index.ts */
// 这里导出 runtime-core
export * from "../runtime-core"
重新打包 yarn build
, 检查所有 example
下的页面,没有出现问题说明 实现 runtime-dom
的逻辑
使用 PIXIJS
自定义渲染
让实现vue能够支持 Canvas 平台, 而 PixiJS
就是基于Canvas
的
// 引入PIXIJS
<script src="https://cdn.jsdelivr.net/npm/pixi.js@6.5.1/dist/browser/pixi.js"></script>
// 导入 createRender -> 自定义渲染器
import { createRenderer } from '../../lib/guide-mini-vue.esm.js'
import { App } from "./App.js"
// 打印 PIXI
// console.log(PIXI)
// 创建 PIXI 实例
const game = new PIXI.Application({
width: 800,
height: 600,
})
// 添加实例
document.body.append(game.view)
// 实现使用自定义渲染器 渲染 Canvas
const renderer = createRenderer({
createElement(type) {
if (type === 'rect') {
const rect = new PIXI.Graphics()
rect.beginFill(0xFF0000)
rect.drawRect(0, 0, 100, 100)
rect.endFill()
return rect
}
},
patchProp(el, key, val) {
el[key] = val
},
insert(el, parent) {
// append
parent.addChild(el)
}
})
// 渲染
renderer.createApp(App).mount(game.stage)
import { h } from "../../lib/guide-mini-vue.esm.js"
export const App = {
setup() {
return {
x: 100,
y: 100,
}
},
render() {
// console.log(this.x, this.y)
return h("rect", { x: this.x, y: this.y })
}
}
页面展示
总结
runtime-core
的内容完成