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 流程

happy path 初始化流程.png

分析runtime-core主要流程 :

  1. 主要是根据组件生成创建出来的 vnode , 在 patch() 函数中判断 虚拟节点 vnode 的类型,
    1. Component : 初始化 组件
    2. Element: 创建 element

happy path

初始化 模板和实现 基本的流程

  1. 需要有一个根组件 App, createApp(App) 时候传入 , 并且在 createApp内部有一个对象, 包含 mount() 函数,mount() 函数接收一个 Element容器, 也就是 id=app的容器。
  2. 根据传入过来的组件创建 虚拟节点 vnode
  3. 调用 定义的 render()

    1. 执行 render() -> 就是调用 patch()函数
    2. patch() 函数根据 vnode 的类型 判断是 创建 组件 还是 Element
  4. 创建 example 文件夹,存放模板和测试文件

    1. 创建 helloworld 文件夹 , 测试初始化的文件
      1. index.html : 模板
      2. main.js : 入口文件
      3. App.js : App根组件
  1. <!-- index.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8" />
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  7. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  8. <title>Document</title>
  9. </head>
  10. <body>
  11. <!-- 根容器 -->
  12. <div id="app"></div>
  13. <!-- 引入 mian.js -->
  14. <script src="main.js" type="module"></script>
  15. </body>
  16. </html>
  1. /* main.js */
  2. import { App } from "./App.js"
  3. // 正常使用 vue3 一样
  4. createApp(App).mount('#app')
  1. /* App.js */
  2. // 创建APP组件
  3. export const App = {
  4. // 这里先不写 template,因为 template 最终会转为 runner 函数
  5. // 先写 runder
  6. render() {
  7. // ui 逻辑
  8. // 返回一个虚拟节点
  9. // h() 用于创建Element的虚拟节点
  10. return h("div", "hello" + this.msg)
  11. },
  12. // setup() 函数
  13. setup() {
  14. // 这里写 composition api 逻辑
  15. // 返回一个对象
  16. return {
  17. msg: "mini-vue"
  18. }
  19. }
  20. }

在Vue3 中,像App组件一般放在 .vue 文件中,而标签也是放在 <template> 中。在这里初始化时,因为 <template>标签最终都会编译为 render 函数。 而runtime-core 不具备编译功能,所以这里模拟组件都是使用 render() 函数。

4.2 实现Component 初始化的主流程

实现runtime-core 的过程,源代码文件放在 src/runtime-core

  1. 创建 createApp() 函数, 接收App的组件实例 , src/runtime-core/createApp.ts ```typescript / createApp.ts /

// createApp 逻辑 export function createApp(rootComponent) { // 接收一个根组件容器 -> App // 返回一个对象 return { // 对象内部必须有一个 rootContainer , 根容器 -> id=app 的容器 // 目的是为了将 runner 函数渲染后给他 添加到

里面 mount(rootContainer) { } } }

  1. 2. 根据 App 组件创建按虚拟节点 `VNode`
  2. ```typescript
  3. /* vnode.ts */
  4. // 导出创建虚拟节点的方法
  5. export function createVNode(type, props?, children?) {
  6. // 参数:
  7. // type -> 传入进来的数据类型, 组件|Element
  8. // props -> 可选 -> 属性 | 组件的props
  9. // children -> 可选 -> 子节点 | 子组件
  10. const vnode = {
  11. type, // 组件类型
  12. props,
  13. children,
  14. }
  15. // 返回虚拟节点
  16. return vnode
  17. }
  1. 创建 h.ts , 在 render() 函数中使用的 h()接收element,并且返回 虚拟节点VNode, 所以h()也也使用到了 createVNode() ```typescript / h.ts /

// 用于调用 createVNode 返回一个 VNode export function h(type, props?, children?) { return createVNode(type, props, children) }

  1. 4. `createApp.ts` 调用 `createVNode()`得到虚拟节点, 在创建 `render()` 函数
  2. ```typescript
  3. /* createApp.ts */
  4. // createApp 逻辑
  5. export function createApp(rootComponent) { // 接收一个根组件容器 -> App
  6. // 返回一个对象
  7. return {
  8. // 对象内部必须有一个 rootContainer , 根容器 -> id=app 的容器
  9. // 目的是为了将 runner 函数渲染后给他 添加到 <div id='app'></div> 里面
  10. mount(rootContainer) {
  11. // 1. 先将 rootComponent 根容器(App) 转为一个 虚拟节点 vnode
  12. // 将 组件转为虚拟节点
  13. // 将所有的逻辑操作 都会基于 vnode 做处理
  14. const vnode = createVNode(rootComponent)
  15. // vnode -> 返回出来的虚拟节点 vnode
  16. // createVNode(组件) -> 基于组件创建虚拟节点
  17. // 2.
  18. // 得到虚拟节点后 -> 可以调用 render() 函数
  19. reder(vnode, rootContainer)
  20. // runner(虚拟节点, 根容器)
  21. }
  22. }
  23. }
  1. 实现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() 函数 为了方便递归 }

  1. 6. `patch` 方法的实现, 通过判断 VNode 的类型
  2. 1. 如果是 Componet 类型 -> 调用 `processComponent()` 进行初始化组件的逻辑
  3. 1. `processComponent()`中主要 调用 `mountComponent()`初始化 组件。
  4. 2. 如果是 Element 类型 -> 调用 `processElement()`
  5. 1. `processElement()` 函数会在 Element初始化的主流程实现
  6. <br />这里先实现初始化组件 不用先判断
  7. ```typescript
  8. /* renderer.ts */
  9. // patch(虚拟节点, 根容器)
  10. function patch(vnode, container) {
  11. // 这里主要处理组件
  12. // 1. 判断 vnode 的类型 -> 可能是 组件类型 或者 Element 类型
  13. // 如果是 组件类型
  14. // 去处理组件
  15. // 实现初始化组件的逻辑
  16. // 挂载组件 processComponent 的函数
  17. processComponent(vnode, container)
  18. }
  19. // 初始化组件
  20. function processComponent(vnode: any, container: any) {
  21. // 1. 挂载组件
  22. // 使用 mountComponent 函数 挂载组件
  23. mountComponent(vnode, container)
  24. }
  1. 实现组件的挂载逻辑, mountComponent()
    1. mountComponent() 执行逻辑
      1. 调用 createComponentInstance() 通过 VNode 创建组件实例 instance,之后组件上的props、slots 都会挂载到这个组件的实例上
      2. 调用 setupComponent() , 主要的逻辑,初始化setup函数 , 执行组件的 setup() 函数
      3. 调用 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) }

  1. 将挂载组件的逻辑函数抽离到 `component.ts` ->`createComponentInstance()` `setupComponent()` 函数
  2. ```typescript
  3. /* src/runtime-core/component.ts */
  4. // 创建组件的实例 instance
  5. export function createComponentInstance(vnode) {
  6. // 通过 vnode 创建一个组件的实例对象 component
  7. // 返回一个component对象
  8. const component = {
  9. vnode,
  10. // 为了简化操作——> 获取 type, VNode的类型
  11. type: vnode.type
  12. }
  13. // 返回 component
  14. return component
  15. }

实现 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 }

  1. `setupStatefulComponent()` 函数用于初始化有状态的的组件,与其相对的是没有状态的函数式组件。
  2. - `setupStatefulComponent`函数中首先调用`setup`
  3. - 之后调用`handleSetupResult`函数处理该方法的返回值。
  4. - 调用 `finishComponentSetup()` `render()`函数绑定到 `instance`
  5. ```typescript
  6. /* src/runtime-core/component.ts */
  7. // ...
  8. function setupStatefulComponent(instance: any) {
  9. // 拿到 setup() 的返回值
  10. // 1. 获取到虚拟节点的type , 也就是用户定义的 App 组件
  11. const Component = instance.type
  12. // 2. 解构出 setup
  13. const { setup } = Component
  14. // 判断 setup 是否存在
  15. if (setup) {
  16. // 调用 setup() 拿到返回值
  17. const stupResult = setup()
  18. // 判断 stupResult 的类型
  19. handleStupResult(instance, stupResult)
  20. }
  21. }
  22. function handleStupResult(instance, stupResult: any) {
  23. // 因为 setup 可以返回出 Function | Object
  24. // 如果是 Function 类型,那么就是组件的 render 函数
  25. // 如果是 Object 类型,将这个对象 注入到这个组件的上下文中
  26. // TODO: 这里先实现 Object, 之后再去实现 Function
  27. if (typeof stupResult === 'object') {
  28. // 将这个值注入到 instance 实例上
  29. instance.setupState = stupResult
  30. }
  31. // 需要保证 组件必须要用 runner 函数
  32. // 调用 finishComponentSetup() 将render() 函数绑定到 instance上
  33. finishComponentSetup(instance)
  34. }
  35. // 添加render() 到 instance 上
  36. function finishComponentSetup(instance: any) {
  37. const Component = instance.type
  38. // 判断组件中是否具有 runner()
  39. if (Component.render) {
  40. // 组件中具有 runner(), 将 runner 挂载到 instance 上
  41. instance.render = Component.render
  42. }
  43. }
  1. 实现调用 render() 函数的逻辑 , setupRenderEffect()的实现。
  1. /* renderer.ts */
  2. // ... 其他代码
  3. // 实现调用render
  4. function setupRenderEffect(instance: any, container: any) {
  5. // 调用 runder() 函数 返回 subTree
  6. // subTree 是 h() 函数返回的 vnode, 也是虚拟节点树 subTree
  7. // 当然 subTree 也有可能是 componet 的类型
  8. const subTree = instance.render()
  9. // 再基于返回过来的虚拟节点 vnode, 再进一步的调用 patch() 函数
  10. // vnode -> subTree 是一个 Element类型 -> 挂载 mountElement
  11. // 递归调用 -> patch(虚拟节点,容器)
  12. patch(subTree, container)
  13. // patch()再去判断,-> subTree 的类型 -> 可能是 组件类型 或者 Element类型 -> 一直递归进行下去
  14. }

到此已经完成组件的初始化主流程了
回顾效果图
image.png
总结

  1. /**
  2. * 初始化 runtime-core 的流程 和 初始化 组件 的流程总结
  3. *
  4. * runtime-core: 实现的基本目的: 符合 vue3的设计思路 createApp(App).mount(#app)
  5. * 配合组件,实现组件的渲染,组件的更新,组件的销毁, 组件 Element 的渲染 等等
  6. *
  7. *
  8. *
  9. * 初始化组件:
  10. * 1. 在 mount(容器), 这个方法中, 对createApp 传入进来的 App组件容器, 进行转换为虚拟节点 vnode
  11. * 2. 通过得到转换的 vnode , 使用 runner(vnode, 容器) 函数,进一步处理 vnode
  12. * 3. runner() 函数的目的就是 负责调用 patch(vnode, 容器) 方法, 实现 patch()方法为了方便递归调用
  13. * 4. patch() 函数对vnode进行一个判断 ,如果 vnode 是一个组件,进行组件的初始化 ,如果它是 Element,就对 Element 进行渲染
  14. *
  15. *
  16. * 5. 当前只实现了组件的初始化
  17. * - 1. 在 patch() 调用 processComponent()
  18. * - 2. 在 processComponent() 进行组件的挂载 -> mountComponent(vnode, container)
  19. * - 3. mountComponent(vnode, container) 挂载组件的逻辑
  20. * - 通过 vnode 创建一个 组件的实例对象 -> instance ; 使用 createComponentInstance(vnode) 方法
  21. * - 设置 setupComponent(instance) 初始化组件, 解析处理组件的其他内置属性 比如: props , slots 这些 和组件中setup() 的返回值 和 挂载 runder()
  22. * - 开始调用 render 方法
  23. * - 调用 instance.render() -> h() 返回出来的 subTree 虚拟 Element 节点
  24. * - 递归调用 patch(subTree, container) 进行一个 Element 的渲染
  25. */

4.3 使用 rollup 打包

Webpack 一般用于项目的打包,rollup 常用于库的打包,Vue3 就是使用 rollup 进行打包的,因此 mini-vue3 也使用 rollup 进行打包。

  1. 在项目文件夹下执行yarn add rollup @rollup/plugin-typescript tslib -D命令分别安装 rollup、rollup 打包 TypeScript 项目时所需的插件 @rollup/plugin-typescript 和该插件依赖的 tslib
  2. 在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”

  1. ```typescript
  2. /* src/index.ts */
  3. // 导出所有配置
  4. export * from './runtime-core'
  1. 配置 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() ] }

  1. 4. 配置 `package.json`
  2. ```json
  3. {
  4. "name": "hidie-mini-vue",
  5. "version": "1.0.0",
  6. "main": "lib/guide-mini-vue.cjs.js",
  7. "module": "lib/guide-mini-vue.esm.js",
  8. "license": "MIT",
  9. "scripts": {
  10. "test": "jest"
  11. "build": "rollup -c rollup.config.js"
  12. },
  13. }
  1. tsconfig.json 设置 module

    1. {
    2. "module": "ESNext", /* Specify what module code is generated. */
    3. }
  2. 执行打包命令 yarn build , 在 lib 目录会生成 guide-mini-vue.cjs.js & guide-mini-vue.ems.js 文件

  3. helloworldmain.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)

  1. 打开`example/helloworld`目录下的`index.html`文件,可以看到 `Component `初始化的主流程正常运行,但是程序出现报错,这是由于 `Element `初始化的主流程还没有实现。
  2. <a name="OmJGW"></a>
  3. # 4.4 实现 Element 初始化的主流程
  4. 在实现 Element 初始化前, `App.js` 组件改动。 `render()` 返回的 h() 函数参数更复杂一些, 也就是能在页面上渲染内容更多
  5. ```typescript
  6. /* example/helloworld/App.js */
  7. render() {
  8. // ui 逻辑
  9. // 返回一个虚拟节点
  10. // return h("div", "hello" + this.msg)
  11. // return h("div", {
  12. // id: 'test1-id',
  13. // class: 'test2-class'
  14. // }, "hello mini-vue") // 当返回是 children 是 string时
  15. // 当children 是一个 Array 时
  16. return h('div', {
  17. id: 'test1-array-1',
  18. class: 'test2-array-1'
  19. }, [
  20. h('p', { class: 'red' }, '这是children的第一个数组'),
  21. h('p', { class: 'blue' }, '这是children的第二个数组')
  22. ])
  23. },
  24. // setup() 函数

index.html添加对应的样式

  1. <!-- index.html -->
  2. <head>
  3. <meta charset="utf-8">
  4. <title>Hello World</title>
  5. <style>
  6. .red {
  7. color: red;
  8. }
  9. .blue {
  10. color: blue;
  11. }
  12. </style>
  13. </head>

实现 Element 初始化的主流程是从 patch() 方法 判断 VNode 的类型为 Element 类型时,调用 procssElement() 函数开始。

  1. 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 类型 , 表示它是 组件类型

  1. // 如果是 组件类型
  2. // 去处理组件
  3. // 实现初始化组件的逻辑
  4. processComponent(vnode, container)

} }

  1. 2. `processElement()` 主要的逻辑就是对 Element 的初始化 Element 的更新 执行的逻辑 , 初始化调用 `mountElement()` 函数
  2. ```typescript
  3. /* renderer.ts*/
  4. // 当 vnode 是一个 Element 类型执行这个函数
  5. function processElement(vnode: any, container: any) {
  6. // 进行 Element 的渲染
  7. // Element 初始化 分为两种情况 -> init 初始化 -> update 更新
  8. // 实现初始化Element的逻辑
  9. mountElement(vnode, container)
  10. // 实现 Element 更新逻辑之后去实现
  11. }
  1. mountElement()对 Element 初始化挂载
    1. 创建一个真实的 Element-> vnode.type
    2. 对 vnode 的 props , 解析属性 -> vnode.props
    3. 处理children , 解析 children, 然后赋值
    4. 将创建好的 节点,添加到 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) }

  1. 4. 处理 `children`为数组的逻辑 `mountChildren()` 函数
  2. 因为 `children()`有两种情况, 是一个数组, 或者是一个字符串
  3. - `returnh("div",{class: 'text'}, "hello"+this.msg)`
  4. - `returnh("div", {class: 'text'}, [h('p', { class: 'red' }, '这是children的第一个数组'), h('p', { class: 'blue' }, '这是children的第二个数组'),]`
  5. `childer` `string` 时,就是一个简单的文本,直接赋值给 `el` 就行 `el.textContent = children`<br />当 `children` 时, 执行 `mountChildren()` 传入 `vnode & el` ,循环 `children` 再次递归调用 `patch(v,el)`,递归调用的区别是 `patch`的容器参数 变为了 `el`
  6. ```typescript
  7. /* renderder.ts */
  8. function mountElement(vnode: any, container: any) {
  9. // 省略代码
  10. const { children } = vnode
  11. if (typeof children === 'string') {
  12. // 直接设置 el 的文本内容
  13. el.textContent = children
  14. } else if (Array.isArray(children)) {
  15. // 表示 [h(), h()]
  16. // 使用 mountChildren 重构 children - Array 的渲染
  17. mountChildren(vnode, el)
  18. }
  19. }
  20. // 封装 渲染 children 为 children 的函数
  21. function mountChildren(vnode, container) {
  22. // 循环 children 内的虚拟节点, 然后调用 patch()进行递归->再去渲染
  23. vnode.children.forEach((v) => {
  24. // 这里 container 容器设置为 el
  25. // 递归调用 patch()
  26. patch(v, container)
  27. })
  28. }

以上就完成了初始化 Element 的主流程。 执行 yarn build 重新打包, 然后查看 index.html 渲染出来的内容
image.png
最后总结分析runtime-core Element的流程
image.png

4.5 实现组件代理对象

为了实现在render() -> h() 渲染时能够取到setup() 的返回值

  1. /* App.js */
  2. render() {
  3. // ui 逻辑
  4. return h('div', { class: 'test-class-id' }, this.msg)
  5. // return h('div', { class: 'test-class-id' }, [
  6. // h('p', { class: 'red' }, 'Hello Red'),
  7. // h('p', { class: 'blue' }, 'Hello Blue')
  8. // ])
  9. },
  10. // 在 render 中还需要实现 :
  11. // 1. 实现 render() 中能够 setupState -> setup 的返回值
  12. // 这里的this.msg 应该是 setup()返回出来的 msg , 怎么样能够拿到这个 setup() 中的 msg呢?
  13. // 只需要把 setup() 的返回值,绑定到 render() 的 this 上即可
  14. setup() {
  15. return {
  16. msg: 'Hello World'
  17. };
  18. }

实现逻辑 : 由于之前在Component已经实现将 setup() 的返回值赋值给了 instance 当前组件的实例 setupState 上, 所以要实现 render() 中获取 setup 的返回值就在 setupState 上获取。
因为在render()中通过 this.xxx访问数据,可以利用 Proxy 代理对象上的 get进行代理,当访问 this.xxx 返回 setupState对应的返回值 。

setupStatefulComponent() 时,开始进行代理,并将实现的代理 proxy 赋值到 instance 上,在调用 render() 时候 将 this的指向为这个 Proxy代理对象

  1. 初始化 Proxy ```typescript / component.ts / export function createComponentInstance(vnode) { const component = { // 其他代码 // 初始化 proxy proxy: null, } return component }
  1. 2. 实现代理对象 `new Proxy`
  2. ```typescript
  3. /* component.ts */
  4. // 执行 call setup()
  5. function setupStatefulComponent(instance) {
  6. // 因为这里确定时 组件逻辑, 通过 instance.type 获取当前的组件
  7. const component = instance.type
  8. // 创建组件代理对象 Proxy, 并添加到 instance组件实例上
  9. // Proxy的第一个参数 {} -> ctx 上下文
  10. instance.proxy = new Proxy({}, {
  11. // 在render() 中调用 this.xxx 执行 get()
  12. get(target, key) {
  13. // 这里 target 就是 ctx -> 上下文, key 就是访问的 key
  14. // console.log(target, key)
  15. // 1. 实现从setupState 拿到值
  16. const { setupState } = instance
  17. // 判断 setupState 中是否有 key 属性
  18. if (key in setupState) {
  19. // 返回 setupState 中的值
  20. return setupState[key]
  21. }
  22. }
  23. })
  24. }
  1. 在调用 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) }
  1. 重新打包项目文件 `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)
  2. 实现访问到 `setup() `的返回值逻辑, 再实现组件的其他API 比如 `$el & $data ` 这些 <br />这里实现 `$el` , `$el` 的作用: `该组件实例管理的 DOM 根节点。`
  3. 在实现访问 `$el`,通过 App 组件内部设置 测试的内容
  4. ```javascript
  5. // 创建APP组件
  6. // 使用 window 添加事件
  7. window.self = null
  8. export const App = {
  9. render() {
  10. // 增加调试 this -> window
  11. window.self = this
  12. // ... 其他代码
  13. }
  14. }

实现 $el 的API

  1. 在初始化 $el, 在创建虚拟节点时 ```typescript / vnode.ts / export function createVNode(type, props?, children?) { const vnode = { type, props, children, // 初始化 el el: null }

    return vnode }

  1. 2. 在初始化 `Element `时, `$el` 赋值为根节点 , 在获取 `VNode `树并递归地处理即调用`setupRenderEffect`函数时,将 `VNode `树的 `el `挂载到 `VNode `
  2. ```typescript
  3. /* renderer.ts */
  4. function mountElement(vnode, container) {
  5. // console.log(vnode)
  6. // 1. 创建 Element 对象
  7. const el = (vnode.el = document.createElement(vnode.type))
  8. // ... 其他代码
  9. }
  10. function setupRenderEffect(instance, vnode, container) {
  11. // ... 其他代码
  12. // 将 VNode 树的 el property 挂载到 VNode 上
  13. // 实现 $el 的关键时机, 在这个时候赋值 el
  14. // 当patch() 再次调用,可能是 进行Element流程
  15. // 将 subTree.el 赋值给 vnode.el
  16. vnode.el = subTree.el
  17. }
  1. 在代理对象中, 添加判断逻辑 , 当调用 $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 } } } )

/ 其他代码 / }

  1. 这个时候执行打包: `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)
  2. 4. 重构代码,参考 `reactive` 是使用 `Proxy` 的逻辑,把 Proxy 主要功能抽离代码, 实现重构
  3. 创建 `componentPublicInstance.ts` 文件
  4. ```typescript
  5. /* componentPublicInstance.ts */
  6. // 一个公共访问属性的逻辑
  7. const publicPropertiesMap = {
  8. // $el
  9. $el: (instance) => instance.vnode.el
  10. }
  11. export const PublicInstanceProxyHandlers = {
  12. // 使用 { _: instance } 的方式传递 instance 参数
  13. get({ _: instance }, key) {
  14. // 这里 target 就是 ctx -> 上下文, key 就是访问的 key
  15. // console.log(target, key)
  16. // 1. 实现从setupState 拿到值
  17. const { setupState } = instance
  18. // 判断 setupState 中是否有 key 属性
  19. if (key in setupState) {
  20. // 返回 setupState 中的值
  21. return setupState[key]
  22. }
  23. // // 2. 添加判断 实现访问 $el 的逻辑
  24. // if (key === "$el") {
  25. // // 直接返回 $el 属性
  26. // return instance.vnode.el
  27. // }
  28. // 重构
  29. // 实现公共访问 key 获取 对象的 value
  30. const publicGetter = publicPropertiesMap[key]
  31. // 判断是否具有该属性
  32. if (publicGetter) {
  33. // 执行 publicGetter 函数拿到 key 对应的 value, 将其返回
  34. return publicGetter(instance)
  35. }
  36. }
  37. }

component.ts 改为

  1. /* component.ts */
  2. function setupStatefulComponent(instance) {
  3. const Component = instance.type
  4. // 利用 Proxy 对组件实例对象的 proxy property 的 get 进行代理
  5. instance.proxy = new Proxy({ _: instance }, PublicInstanceHandlers)
  6. /* 其他代码 */
  7. }

再次打包,能够获取到 $el 说明重构没有问题

  1. // 总结
  2. // 在 render 中还需要实现 :
  3. // 1. 实现 render() 中能够 setupState -> setup 的返回值
  4. // 这里的this.msg 应该是 setup()返回出来的 msg , 怎么样能够拿到这个 setup() 中的 msg呢?
  5. // 只需要把 setup() 的返回值,绑定到 render() 的 this 上即可
  6. /**
  7. * 实现 setupState:
  8. * 因为当前的 render() 函数不仅仅需要访问到 setupState 或者 使用 $el setupState 这些
  9. * 可能还会添加一些 $data 能够让 render() 快速访问
  10. *
  11. * 解决: 可以通过 Proxy 实现 ,也就是 组件代理对象 -> 代理对象
  12. * 把 stateState & $el & $data 通过 Proxy 代理, 可以让 render() 快速访问
  13. *
  14. * 实现方式:
  15. * 1. 在 初始化 setupState -> 调用 setup() 时,创建 代理对象 Proxy
  16. * 2. 创建代理对象,实现 xxx 功能
  17. * 3. 把 代理对象绑定到 instace 组件实例上
  18. * 4. 当在调用 render() 函数时,通过 Proxy 访问 setupState
  19. *
  20. */
  21. // 2. 实现 this.$el 获取到当前组件的根节点 -> div
  22. // $el -> vue3 的API , $el 的目的:返回根节点 element, 也就是组件的根节点, DOM实例
  23. /**
  24. * 实现 $el:
  25. * $el 的作用: $el -> vue3 的API , $el 的目的:返回根节点 element, 也就是组件的根节点, DOM实例
  26. *
  27. * 实现方式:
  28. * 1. 在初始化 Element这里,需要将根节点保存起来, 使用el -> 在创建虚拟节点时候,初始化el 为null
  29. * 2. 在初始化 Element时,让 el 保存 根节点
  30. * 3. 在代理对象中, 添加判断逻辑,当在render中调用 this.$el时, 需要返回 el,
  31. *
  32. */

4.6 实现 shapeFlag

shapleFlag 的实现主要用于在代码中 判断 VNode 虚拟节点 和 VNode.childer 的类型
在 调用 patch() 是判断 VNodeComponent || Element
在 渲染 children 时 判断 childrenArray || string

实现 shapeFlag 的逻辑

  1. 先完成一个最简单的 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, }

  1. 状态的变化 ,如果当前节点是 `vnode` `stateful_component` , 设置它的状态为 `1`
  2. ```typescript
  3. /* shared/test-shapeFlag.ts*/
  4. // 如果当前节点 vnode 是 stateful_component
  5. // 设置 它的状态
  6. // 1. 可以设置修改 状态
  7. ShapeFlag.stateful_component = 1
  8. ShapeFlag.text_children = 1

状态的变化, 判断当前shapeFlag 属性的类型

  1. /* shared/test-shapeFlag.ts*/
  2. // 需要先判断 之前的节点类型 ShapeFlag
  3. // 2. 查找
  4. if (ShapeFlag.element) {
  5. // 节点是 Element 类型时执行
  6. }
  7. if (ShapeFlag.stateful_component) {
  8. // 节点是 component 类型 时执行
  9. }

出现问题: 这种使用对象形式的判断方法,不够高效 .
而在 Vue3 中使用的位运算法更高效

  1. 使用 位运算法来设置 shapeFlag 的状态
  • 与运算(&):两位都为 1,结果才为 1
  • 或运算(|):两位都为 0,结果才为 0
  • 左移运算符(<<):将二进制位全部左移若干位 ```typescript // 可以通过 位运算方式 提升代码性能 // 位运算-> 通过二进制 01 表示 当前节点的状态

0001 -> element 0010 -> stateful_component 0100 -> text_children 1000 -> array_children

// 表示两种状态 1010 -> stateful_component + array_children

// 实现位运算方式实现 修改和查找 的功能

  1. 可以定义 `shapeFlage`
  2. ```typescript
  3. /* shared/test-shapeFlag.ts*/
  4. const ShapeFlag = {
  5. element: 1, // 转为二进制 -> 0001
  6. stateful_component: 1 << 1, // 使用左移运算符 1 左移1位 -> 10 -> 转为二进制 -> 0010
  7. text_children: 1 << 2, // 1 左移 2 位 -> 100 -> 转为二进制 -> 0100
  8. array_children: 1 << 3, // 1 左移 3 位 -> 1000 -> 转为二进制 -> 1000
  9. }

实现修改的 | 逻辑

  1. // 1. 修改 : | -> 两位都为 0 ,才为 0
  2. 0000 -> | 进行或运算
  3. 0001
  4. ----
  5. 0001
  6. // 修改 0000 为 0001 得到 0001 -> 修改初始时的0状态 为 element
  7. 0001 -> | 进行或运算
  8. 0100
  9. ----
  10. 0101
  11. // 修改 0001 为 0001 得到 0101 -> 修改 element 状态为 element + text_children

实现 查找的 & 逻辑

  1. // 2. 查找 : & -> 两位都为 0 , 才为 1
  2. // &
  3. 0001 -> & 进行 & 运算
  4. 0001
  5. ----
  6. 0001
  7. // 查找0001是不是0001类型 -> 得到 0001 表示是
  8. 0010 -> & 进行 & 运算 ->
  9. 0001
  10. ----
  11. 0000
  12. // 查找0010是不是0001 类型 -> 得到 0000 表示不是

确定逻辑 ,实现shapeFlag 的代码

  1. 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 // 位运算的方式 -> 提高性能 -> 但是可读性不高

  1. 4. 在初始化 `vnode `时,声明 `ShapeFlags`, 给赋值类型 ` vnode.ts`
  2. ```typescript
  3. /* vnode.ts */
  4. export function createVNode(type, props?, children?) {
  5. const vnode = {
  6. type, // 组件类型
  7. props,
  8. children,
  9. el: null
  10. // 1. 初始化 ShapeFlags
  11. shapeFlag: getShapeFlag(type)
  12. }
  13. // 2. 处理 children
  14. if (typeof children === 'string') {
  15. // 如果 children 是 string , 给 vnode.ShapeFlag 进行赋值
  16. // vnode.ShapeFlag | ShapeFlags.text_children 进行一个 或 | 运算
  17. // vnode.ShapeFlag = vnode.ShapeFlag | ShapeFlags.text_children
  18. // 简化
  19. vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
  20. } else if (Array.isArray(children)) {
  21. // 如果 children 是数组,赋值 ShapeFlag 为 数组
  22. vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
  23. }
  24. return vnode
  25. }
  26. // 这里判断类型
  27. function getShapeFlag(type) {
  28. // 设置返回类型
  29. // 这里初始化时,先判断VNode的类型, Element || Component
  30. return typeof type === 'string' ? ShapeFlags.ELEMENT : ShapeFlags.STATEFUL_COMPONENT
  31. }
  1. 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) } }

  1. 打包项目 `yarn build`打包,检测 `index.html`没有报错说明实现了 `shapeFlags`
  2. <a name="qv4em"></a>
  3. # 4.8 **实现注册事件功能**
  4. 实现注册事件功能
  5. 1. 写测试的逻辑 再测试组件`App` 中添加测试的事件
  6. ```typescript
  7. /* App.js */
  8. window.self = null
  9. export const App = {
  10. render() {
  11. window.self = this
  12. return h("div", {
  13. id: 'test1-id',
  14. class: 'test2-class',
  15. // 添加事件
  16. onClick() {
  17. console.log('click')
  18. },
  19. }, "hello " + this.msg
  20. )
  21. },
  22. setup() {
  23. return {
  24. msg: 'Hello World'
  25. };
  26. }
  27. }

实现注册事件的主要逻辑和流程: 在初始化 Element 的主流程时,在 mountElement 中处理 props 时,循环key 拿到 props 对应的属性值 。
只需要判断这个属性值是否为 props一个事件名,再使用 addEventListener() 方法设置对应的事件挂载到元素上即可

  1. 在处理 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]) } } } }

  1. 实现`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)
  2. 3. 重构代码,实现通过的注册事件
  3. 因为是事件名称都是 `on + 事件名`的起名方式, `click -> onClick & onMouseDown -> MouseDown`
  4. ```typescript
  5. /* renderer.ts*/
  6. // 重构-> 实现通用注册事件
  7. function mountElement(vnode, container) {
  8. /* 其他代码 */
  9. // 2. 设置属性 props
  10. // 处理 组件 注册事件
  11. const { props } = vnode
  12. if (props) {
  13. for (const key in props) {
  14. // 实现通用的,可以根据 组件的命名规范,来实现,
  15. // 因为 on + 事件名称 : click -> onClick , onMouseDown -> MouseDown
  16. // 可以使用正则方式,匹配 on 之后的事件名,来注册不同事件
  17. // 封装 通过注册方法 isOn
  18. const isOn = (key) => /^on[A-Z]/.test(key);
  19. if (isOn(key)) {
  20. // 获取事件名称
  21. const event = key.slice(2).toLowerCase();
  22. el.addEventListener(event, val);
  23. }
  24. else {
  25. // 如果不是 on 之后的事件名称
  26. // 设置 el 的属性值
  27. el.setAttribute(key, val);
  28. }
  29. }
  30. }
  31. }

在App 中添加其他事件

  1. /* App.js */
  2. window.self = null
  3. export const App = {
  4. render() {
  5. window.self = this
  6. // ui 逻辑
  7. return h('div', {
  8. class: 'test-class-id',
  9. onClick: () => {
  10. console.log('click 事件触发')
  11. },
  12. onMouseDown: () => {
  13. console.log('mouse down 事件触发')
  14. },
  15. id: this.msg
  16. }, this.msg)
  17. }
  18. }

测试功能
image.png
执行打包命令 yarn build 重新打包, 在 index.html 中 点击标签, 检测是否初始事件 , 没有出现问题说明是西安注册事件功能

  1. // 实现 props 属性中 注册事件的功能
  2. // 实现逻辑
  3. /**
  4. * 1. 在处理 props 的逻辑中, 判断 key 是否是特殊的方法
  5. * 2. 在对 key 对应的值,绑定对应的事件
  6. */

4.9 实现 Props

props是 setup 的第一个参数,用于在 父组件 & 子组件中传参
初始化实现 Props 的文件 , 创建 ./example/componentProps文件夹。 赋值HelloWorld的内容到里面 , 添加 Foo.js文件,相当于一个组件

App.js 的逻辑

  1. import { Foo } from './Foo.js'
  2. /* App.js */
  3. window.self = null
  4. export const App = {
  5. render() {
  6. window.self = this
  7. // ui 逻辑
  8. return h('div', {
  9. class: 'test-class-id',
  10. onClick: () => {
  11. console.log('click 事件触发')
  12. },
  13. onMouseDown: () => {
  14. console.log('mouse down 事件触发')
  15. },
  16. id: this.msg
  17. }, [
  18. h('div', {}, "hello + Foo + " + this.msg),
  19. // 添加渲染 Foo 组件
  20. h(Foo,
  21. // 传入 props 参数
  22. {
  23. count: this.count
  24. })
  25. ])
  26. },
  27. setup() {
  28. let count = 0
  29. return {
  30. msg: 'Hello World',
  31. count
  32. };
  33. }
  34. }

Foo.js 的逻辑

  1. /* Foo.js */
  2. export const Foo = {
  3. // 1. setup 接收 App组件传入的props
  4. setup(props) {
  5. // 1.1 实现 setup 访问 props
  6. // 分析:只需要在调用setup时候把 props 传入 setup 函数即可
  7. // 实现:
  8. // 1. 在初始化setup 时候, 声明 initProps(), 把 instance 和 instance.vnode.props 传递进去
  9. // 2. initProps() 的逻辑-> 把props 挂载到 instance 上
  10. // 3. 在调用 setup() 时候传入 instance.props 作为参数
  11. console.log(props)
  12. // 3. props 是 只读的 Shallow readonly 的
  13. // 实现:
  14. // 1. 只需要在调用setup(props) 是否 设置 props 是 shallowReadonly 就行
  15. // 注意:
  16. // - props 在初始化时候位 null -> 在执行shallowReadonly 是 target 必须是一个对象,
  17. // 所以在initProps时候 初始化 props 时,如果为null, 设置props 为一个 空对象 {}
  18. props.count++ // 修改 count 会报错
  19. console.log(props)
  20. },
  21. render() {
  22. // 2. 在 render 中 通过 this.count 能够访问 props.count 的值
  23. // 分析:需要在 render() 中 this 调用 xxx,
  24. // 也就是之间实现 this.xxx = xxx -> 创建的代理对象中, 访问key, 然后拿到它的返回值, props 也可以通过这样的方式实现
  25. // 实现:
  26. // 1. 在实现代理对象Proxy的中的 PublicInstanceProxyHandlers 中 处理 props 的返回值
  27. // 2. PublicInstanceProxyHandlers 重构 通过key拿到返回值的方法, 通过 hasOwn(obj, key) 来判断对象具有的属性值
  28. // 3. 通过解构的方法 拿到 props 对象 , 再使用 hasOwn(props, key) 判断, 返回 props[key] 的值
  29. return h("div", {}, "这里是Foo 组件" + this.count)
  30. }
  31. }

在访问 index.html 页面结果
image.png

  1. 实现 setup访问 props, 分析:只需要在调用setup时候把 props传入 setup函数即可 ,

实现:
1. 在初始化setup时候, 声明过initProps() instanceinstance.vnode.props 传递进去
2.initProps()的逻辑-> 把props挂载到 instance
3. 在调用 setup() 时候传入 instance.props 作为参数
4 实现 props 只读的, 在实现响应式时, 使用过 shallowReadonly,把instance.props变为可读就行

在创建component是,初始化 prop, 设置为一个空对象

  1. /* component.ts*/
  2. export function createComponentInstance(vnode) {
  3. // 创建组件的实例
  4. const component = {
  5. // 其他代码
  6. // 初始化 props 为一个对象
  7. props: {},
  8. }
  9. return component
  10. }

setupComponent中, 也就是在执行 setup() 函数前,实现 initProps

  1. /* component.ts*/
  2. // 执行 组件 setup() 逻辑
  3. export function setupComponent(instance) {
  4. // 1. initProps
  5. // 传入 instance 当前组件的实例 & 当前组件的props
  6. initProps(instance, instance.vnode.props)
  7. }
  8. function setupStatefulComponent(instance) {
  9. // ... 其他代码
  10. if (setup) {
  11. // 执行 setup()
  12. // 传入 instance.props 参数
  13. // 使用 shallowReadonly 包裹起来, props 是可读的
  14. const setupResult = setup(shallowReadonly(instance.props))
  15. }
  16. }

创建 runderer-core/componentProps.ts , 实现 props的逻辑

  1. /* componentProps.ts*/
  2. // initProps 的逻辑
  3. // 两个参数 -> instance 组件的实例 , rawProps -> 没有经过初始化的 props
  4. export function initProps(instance, rawProps) {
  5. // 逻辑: 只需要把 props 挂载到 instance 上,
  6. // 然后在调用 setup() 时候传输 props 给到就行了
  7. // 需要props 在创建 instance 时 初始化 init
  8. // 如果 rawProps 为空 ,则设置为 {}
  9. instance.props = rawProps || {}
  10. }

此时在 Foo.js 组件里, 已经能够看到 props输出的结果了
image.png

  1. 实现在组件中的 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 属性时,返回的数据

  1. /* componentPublicInstance.ts */
  2. export const PublicInstanceProxyHandlers = {
  3. // 使用 { _: instance } 的方式传递 instance 参数
  4. get({ _: instance }, key) {
  5. // 1. 实现从setupState 拿到值
  6. // const { setupState } = instance
  7. // // 判断 setupState 中是否有 key 属性
  8. // if (key in setupState) {
  9. // // 返回 setupState 中的值
  10. // return setupState[key]
  11. // }
  12. // 实现一个公共方法 hasOwn() 判断 object 是否具有 key 属性
  13. const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key)
  14. // 因为 setup的返回值 setupState ,可以使用判断 object 是否具有 key 属性
  15. // 具有相同的逻辑
  16. // 解构出 props
  17. const { setupState, props } = instance
  18. if (hasOwn(setupState, key)) {
  19. return setupState[key]
  20. } else if (hasOwn(props, key)) {
  21. // 返回 props 中的值
  22. return props[key]
  23. }
  24. }
  25. }

这样在 render() 中就能访问到 props.count
image.png
重构 : 把hasOwn() 的代码抽离出去到公共的方法

  1. /* shared/index.ts */
  2. // 实现一个公共方法 hasOwn() 判断 object 是否具有 key 属性
  3. 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') 触发定义在 propsonAdd方法

实现 Emit 功能之前,初始化, 创建 example/componentEmit 文件夹。 复制App.js & Foo.js & index.html & main.js 文件

在App组件中 Emit功能需求

  1. /* App.js*/
  2. export const App = {
  3. name: 'App', // 组件name -> 根组件
  4. // 实现 emit 功能
  5. render() {
  6. return h('div', {}, [
  7. h('div', {}, "App"),
  8. h(Foo,
  9. // Foo 组件的 props
  10. {
  11. // 接收 emit 事件
  12. // 这里接收的 onAdd 和 在 Foo 组件调用的 emit('add') 名字不同
  13. // 在这里注册事件都是 on + 时间名 -> on + Add
  14. onAdd(a, b) {
  15. // 获取到参数 a ,b -> emit()传入参数
  16. console.log('onAdd -> emit触发 add 事件 回调函数', a, b)
  17. },
  18. onAddFoo(a, b) {
  19. console.log('onAddFoo -> ', a, b)
  20. }
  21. })
  22. ])
  23. },
  24. }

在Foo.js 组件中使用 Emit能够调用在 App.js定义的 Foo的 prosp内 方法

  1. /* Foo.js */
  2. // 实现 Emit 功能
  3. export const Foo = {
  4. // 1. 实现Emit
  5. // setup 接收的第二个参数 {}, 导出 Emit
  6. // 这里注意: 第二个参数是一个对象 , 而对象里面有一个emit
  7. // 实现:
  8. // - 在调用 setup() 时候传入第二个参数, 是一个对象, 对象中有一个emit 方法
  9. // - 这个emit是一个函数,需要在组件实例中进行初始化 -> emit: instance.emit -> 初始化emit: () => {}
  10. // - 定义 emit 方法,将他挂载到 component.emit 上面
  11. // 关系: Foo组件 emit('事件名') -> instance.emit这里只是函数名 -> component.emit 初始化的 emit = 创建的 emit("事件名") 函数
  12. // - 接下来的逻辑在 componetntEmit.ts 中
  13. setup(props, { emit }) {
  14. const emitAdd = () => {
  15. console.log('emit add —— 在setup() 中被调用')
  16. // 2. 当调用 emit 时,传入一个事件名,
  17. // - 在找事件时候,需要找当前组件 props 中有没有这个事件函数
  18. // - 实现: 匹配 on + 事件名 -> 需要首事件字母大写
  19. // 当执行 emitAdd 时,会触发 App 组件的 emit 事件
  20. // emit('add')
  21. // 实现emit 传参功能,
  22. // 1, 2 能够在 App 组件中获取到传入的参数
  23. // 实现逻辑
  24. // - 在实现emit逻辑中,使用解构 ...args 获取传入的参数, 然后调用 handler(...args) 是传入接收的参数就行
  25. emit('add', 1, 2)
  26. // 实现事件的命名方式 on-add
  27. // 实现逻辑-> add-foo 在实现emit 时候转为 AddFoo 命名
  28. // 只需要把 - 去掉, 首字符变为大写
  29. emit('add-foo', 1, 2)
  30. }
  31. // 返回 emitAdd 函数
  32. return {
  33. emitAdd
  34. }
  35. },
  36. render() {
  37. // 声明一个点击事件, 当点击这个按钮时, 触发 emit -> 调用App中的xxx方法
  38. const btn = h('button', {
  39. // 声明click 为 this.emitAdd
  40. onClick: this.emitAdd
  41. }, 'emitAdd')
  42. const foo = h('p', {}, 'Foo组件')
  43. // 返回Element逻辑
  44. return h('div', {}, [foo, btn])
  45. }
  46. }

此时的页面
image.png

  1. 实现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 } 参数

  1. /* component.ts */
  2. export function createComponentInstance(vnode) {
  3. const component = {
  4. // 其他代码 ...
  5. // 初始化 emit -> 是一个函数
  6. emit: () => {}
  7. }
  8. return component
  9. }
  10. // 调用 setup 时,传入 emit 参数
  11. function setupStatefulComponent(instance: any) {
  12. // 其他代码 ...
  13. if (setup) {
  14. // 实现emit -> 第二个参数{} -> 对象中有 emit
  15. // 将 emit 默认赋值给 instance.emit
  16. const stupResult = setup(shallowReadonly(instance.proxy), { emit: instance.emit })
  17. }
  18. }

在创建创建组件时,对 emit方法进行一个接收函数 emit
创建 componentEmit.ts 导出 emit方法

  1. /* componentEmit.ts */
  2. // 导出 emit 函数
  3. // 接收 Foo组件 中 emit('事件名') -> 中事件名 参数
  4. export function emit(event) {
  5. // 检测 emit 的传参
  6. console.log('emit', event, '在 emit实现逻辑中调用')
  7. }
  1. /* component.ts */
  2. export function createComponentInstance(vnode) {
  3. const component = {
  4. // 其他代码 ...
  5. // 初始化 emit -> 是一个函数
  6. emit: () => {}
  7. }
  8. // 导入 emit, 并使用
  9. component.emit = emit as any
  10. return component
  11. }

这里就建立了 组件中 emit 的关系
关系: Foo组件的emit('事件名') == instance.emit (这里只是函数名) == component.emit 初始化的emit = 创建的 emit("事件名") 函数

此时页面内容
image.png

实现 componentEmit.ts逻辑
1. 找到 instance.props 中有没有这个 event 对应的 函数名和回调函数
-> 实现: 传入 instance 拿到 props
-> 因为在Foo组件的emit 只会传入一个事件名的参数, 不是两个参数
-> 可以在componet.emit 调用 emit()时候 使用 bind() -> emit.bind(null, component), 代表传入第一个函数是this,第 二个参数是 emit接收的第一个参数instance 这是一个代码技巧

实现在componentEmit 中能够拿到 instance 才能做到返回 emit对应的函数内容

  1. /* component.ts */
  2. export function createComponentInstance(vnode) {
  3. const component = {
  4. // 其他代码 ...
  5. // 初始化 emit -> 是一个函数
  6. emit: () => {}
  7. }
  8. /* 可以在componet.emit 调用 emit()时候
  9. 使用 bind() -> emit.bind(null, component), 代表传入第一个函数是this,第
  10. 二个参数是 emit接收的第一个参数instance 这是一个代码技巧*/
  11. component.emit = emit.bind(null, component) as any
  12. return component
  13. }

emit 中接收到 instance 组件实例

  1. /* componentEmit.ts */
  2. // 导出 emit 函数
  3. // 接收 Foo组件 中 emit('事件名') -> 中事件名 参数
  4. export function emit(instance, event) { // 接收到 instance 组件实例
  5. // 检测 emit 的传参
  6. console.log('emit', event, '在 emit实现逻辑中调用')
  7. }
  1. 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() }

  1. 重构 -> 处理字符串 -> `add` 首字母大写 -> `on + 事件`
  2. ```typescript
  3. /* componentEmit.ts */
  4. export function emit(instance, event) { // 接收到 instance 组件实例
  5. // TPP 开发技巧 :先去写一个特定的行为,再去重构为通过的行为
  6. const { props } = instance;
  7. // 将 event -> add 变为 Add -> 首字符大写
  8. const capitalize = (str: string) => {
  9. // 将首字符获取到,并转换为大写,然后拼接后面的字符串
  10. return str.charAt(0).toUpperCase() + str.slice(1);
  11. }
  12. // 处理 on + 事件 的行为
  13. const toHandlerKey = (str: string) => {
  14. // 如果 str 有值,执行首字符大写,没有返回空字符串
  15. return str ? "on" + capitalize(str) : ""
  16. }
  17. const handlerName = toHandlerKey(event)
  18. // 根据props中的key, 找到对应的回调函数, 然后执行
  19. const handler = props[handlerName]
  20. handler && handler()
  21. }

重构, 让事件名能够以 add-foo 的形式命名

  1. /* componentEmit.ts */
  2. export function emit(instance, event) { // 接收到 instance 组件实例
  3. // TPP 开发技巧 :先去写一个特定的行为,再去重构为通过的行为
  4. const { props } = instance;
  5. // 实现 add-foo 变为 AddFoo
  6. const camelize = (str: string) => {
  7. // 返回一个正则替换
  8. return str.replace(/-(\w)/g, (_, c: string) => {
  9. return c ? c.toUpperCase() : ""
  10. })
  11. }
  12. // 将 event -> add 变为 Add -> 首字符大写
  13. const capitalize = (str: string) => {
  14. // 将首字符获取到,并转换为大写,然后拼接后面的字符串
  15. return str.charAt(0).toUpperCase() + str.slice(1);
  16. }
  17. // 处理 on + 事件 的行为
  18. const toHandlerKey = (str: string) => {
  19. // 如果 str 有值,执行首字符大写,没有返回空字符串
  20. return str ? "on" + capitalize(str) : ""
  21. }
  22. // 在这里使用 - 转为 驼峰命名 的函数 camelize()
  23. const handlerName = toHandlerKey(camelize(event))
  24. // 根据props中的key, 找到对应的回调函数, 然后执行
  25. const handler = props[handlerName]
  26. handler && handler()
  27. }
  1. 实现 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() 中被调用’)

  1. // 实现emit 传参功能,
  2. // 1, 2 能够在 App 组件中获取到传入的参数
  3. // 实现逻辑
  4. // - 在实现emit逻辑中,使用解构 ...args 获取传入的参数,
  5. // 然后调用 handler(...args) 是传入接收的参数就行
  6. emit('add', 1, 2)
  7. // 实现事件的命名方式 on-add
  8. // 实现逻辑-> add-foo 在实现emit 时候转为 AddFoo 命名
  9. // 只需要把 - 去掉, 首字符变为大写
  10. emit('add-foo', 1, 2)

} // 返回 emitAdd 函数 return { emitAdd } },

  1. 在实现emit逻辑中,使用解构 ...args 获取传入的参数 , 然后调用 handler(...args) 是传入接收的参数就行
  2. ```typescript
  3. /* componentEmit.ts */
  4. export function emit(instance, event, ...args) { // ...args 是Foo组件传入过来的参数
  5. //... 其他代码
  6. // 将 args 传入 handler 函数中,然后在APP组件上 emit(a,b)接收参数
  7. handler && handler(...args)
  8. }

查看控制台, 输出参数
image.png

  1. 重构把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) }

  1. ```typescript
  2. /* shared/index.ts */
  3. // 实现 add-foo 变为 AddFoo
  4. export const camelize = (str: string) => {
  5. // 返回一个正则替换
  6. return str.replace(/-(\w)/g, (_, c: string) => {
  7. return c ? c.toUpperCase() : ""
  8. })
  9. }
  10. // 将 event 的 add 变为 Add -> 首字符大写
  11. const capitalize = (str: string) => {
  12. // 将首字符获取到,并转换为大写,然后拼接后面的字符串
  13. return str.charAt(0).toUpperCase() + str.slice(1);
  14. }
  15. // 处理 on + 事件 的行为
  16. export const toHandlerKey = (str: string) => {
  17. // 如果 str 有值,执行首字符大写,没有返回空字符串
  18. return str ? "on" + capitalize(str) : ""
  19. }

重新执行打包命令 yarn build 检查index.html, 没有出现问题, 表示实现了 Emit 功能

4.11 实现 slots 功能

插槽的功能:父组件需要传递一些模板片段给子组件,子组件通过 插槽,让传递过来的内容渲染到这些片段
例如

  1. // FancyButton 父组件
  2. <FancyButton>
  3. Click me! <!-- 插槽内容 -->
  4. </FancyButton>
  5. // FancyButto
  6. <button class="fancy-btn">
  7. <slot></slot> <!-- 插槽插口 -->
  8. </button>
  9. // 最终渲染的 DOM
  10. <button class="fancy-btn">
  11. Click me!
  12. </button>

image.png
元素是一个插槽的插口,标示了父元素提供的插槽内容将在哪里被渲染。

1. 初始化slots&& this.$slots 基本的实现

example/componentSlots/ 创建 App & Foo & main & index.html

  1. /* App.js */
  2. export const App = {
  3. name: 'App',
  4. // 根组件
  5. // 实现 Slot 功能
  6. render() {
  7. // 定义组件
  8. const app = h('div', {}, "这是App 组件")
  9. // 在父组件的 Foo中, 指定 Slot 的内容
  10. const foo = h(Foo, {},
  11. // 在Foo 组件中能够渲染出来这里的内容
  12. // 1. 事件slot的基本需求,-> 在Foo中展示
  13. h('p', {}, "这是在父组件中slot, 传入Foo的内容"), // children 内容
  14. )
  15. // 实现 slot 的目标就是:将h() 渲染出来的节点,添加到 Foo 组件内部
  16. return h('div', {}, [app, foo])
  17. },
  18. }

因为 slots 标签内容是在 父组件中定义, 在子组件通过拿到 父组件的 VNode.children 属性, 也就是拿到的children中的VNode, 在子组件渲染出来就行
关系过程: Foo -> App.children -> render.slots

  1. // 实现 Slot 功能
  2. export const Foo = {
  3. // 实现 Slot 的功能
  4. // 因为 slot标签内容是在父组件中,在 Foo 组件 通过拿到 App 的 VNode .children 属性,
  5. // 也就拿到 childlen 中的 VNode。
  6. // 在 Foo 组件中 渲染出来就行
  7. // 关系过程 -> Foo -> App.children -> render slot 在Foo中渲染 slots
  8. // 1. 通过使用 this.$slots 渲染 App.children 的内容
  9. // 实现 :
  10. // 1. 初始化 $slots API -> component 初始化 slots -> componentPublicInstance 实现访问 $slots 返回内容
  11. // 2. initSlots -> 逻辑:将 vnode.children 赋值给 instance.slots
  12. // 3. 此时已经可以通过 this.$slots 拿到 App 需要渲染在Foo 中的 节点内容了
  13. render() {
  14. console.log(this.$slots)
  15. // 定义 Foo 组件
  16. const foo = h('p', {}, "这是Foo组件")
  17. // 在这里渲染 this.$slots
  18. // 1. 基本的功能
  19. return h('div', {}, [foo, this.$slots])
  20. }
  21. }

实现通过 this.$slots 访问App.children 的内容 , 也就拿到了父组件定义插槽的内容

初始页面
image.png

实现 this.$slots

  1. 1. 通过使用 this.$slots 渲染 App.children 的内容
  2. 实现
  3. 1. 初始化 $slots API -> component 初始化 slots -> componentPublicInstance 实现访问 $slots 返回内容
  4. 2. initSlots -> 逻辑:将 vnode.children 赋值给 instance.slots
  5. 3. 此时已经可以通过 this.$slots 拿到 App 需要渲染在Foo 中的 节点内容了
  • 初始化 $slots API

    1. /* component.ts */
    2. // 返回一个component对象
    3. const component = {
    4. // 其他代码
    5. // 初始化 slots
    6. slots: {},
    7. }

    ```typescript / src/runtime-core/componentPublicInstance.ts / // 定义 $slots API

const publicPropertiesMap = { $el: (instance) => instance.vnode.el // 初始化 $slots ,返回 instance.slots 的内容 -> App.children $slots: (instance) => instance.slots }

  1. - initSlots 的实现
  2. ```typescript
  3. /* src/runtime-core/component.ts */
  4. // 对组件初始化的逻辑函数
  5. export function setupComponent(instance) {
  6. // 解析处理组件的其他内置属性 比如: props , slots 这些
  7. // 2. 初始化 slots
  8. // initSlots()
  9. //初始化 initSlots 给定两个参数, instance, 和 children
  10. initSlots(instance, instance.vnode.children)
  11. }
  1. /* src/runtime-core/componentSlots.ts */
  2. // initSlots
  3. // 初始化 Slots
  4. export function initSlots(instance, children) {
  5. // 将 children 赋值给 instance.slots
  6. instance.slots = children
  7. }

完成 $slots 的基本功能 , 内容就是 App 传入的 虚拟节点
image.png

当slots 是一个数组时

基本的slots 只能实现单个虚拟节点的展示, 当多个时 slots 变为了 Array

  1. /* App.js*/
  2. export const App = {
  3. render() {
  4. const foo = h(
  5. Foo,
  6. {},
  7. // 在Foo 组件中能够渲染出来这里的内容
  8. // 1. 事件slot的基本需求,-> 在Foo中展示
  9. // h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
  10. // 2. 展示多个内容时 -> 在Foo中展示
  11. [
  12. h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
  13. h('p', {}, "这是让slots实现 Array功能")
  14. ]
  15. )
  16. return h('div', {}, [app, foo])
  17. },
  18. }

此时的 slots
image.png

解决:

  1. App中,目前只能支持 h(), App 能够实现 [h(), h()] 多个内容
  2. 2. 当传入过来的 this.$solts 是一个 Array VNode,而不是一个 VNode
  3. children -> Array VNode
  4. 处理逻辑:把内部的 this.$slots 转为虚拟节点 Array VNode-> VNode
  5. 解决: $slots 直接再次包裹 h()
  6. 实现:
  7. 1. 这里通俗使用h("div", {}, [foo, this.$slots])) -> 进行一个重构, 使用 renderSlots函数,来渲染,
  8. 2. 传入 this.$slots, 让它创建对应的虚拟节点
  9. 3. 导入 renderSlot() 进行使用
  10. 主要就是为了简化 h()

把 $slots 直接再次包裹 h()

  1. /* Foo.js */
  2. export const Foo = {
  3. render() {
  4. console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
  5. const foo = h("p", {}, "这是Foo组件")
  6. // 1. 基本的功能
  7. // return h('div', {}, [foo, this.$slots])
  8. // 2. App 中渲染多个内容
  9. return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
  10. }
  11. }

重构: 使用 renderSlots函数,封装 h() , 主要用于渲染 Array 的 slots

  1. /* Foo.js */
  2. export const Foo = {
  3. render() {
  4. console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
  5. const foo = h("p", {}, "这是Foo组件")
  6. // 1. 基本的功能
  7. // return h('div', {}, [foo, this.$slots])
  8. // 2. App 中渲染多个内容
  9. // return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
  10. // 导入 renderSlots
  11. return h('div', {}, [foo, renderSlots(this.$slots)])
  12. }
  13. }

src/runtime-core/helper/renderSlot.ts 定义

  1. /* src/runtime-core/helper/renderSlot.ts */
  2. export function renderSlots(slots) {
  3. // 使用 createVNode() 代替 h(xxx, {} xxx)
  4. // createVNode() 接收的参数,前两位 div {}
  5. // 这里只是实现了 Array -> 没有实现 slots 是单个组件的情况
  6. return createVNode('div', {}, slots)
  7. }

导入 renderSlot

  1. /* index.ts */
  2. export { renderSlots } from './helpers/renderSlots'

重构完成

出现问题: 在检查 实现1 , 发现之前实现的单组件已经渲染不行了, 因为单个虚拟节点,会转为Object, 而不是Array,使用renderSlot 渲染不出来, 修改 renderSlots的逻辑
解决: 判断 slots VNode, 如果是 Array 就使用 createVNode() 代替 h(xxx, {} xxx) , 如果不是 表示单个虚拟节点,直接返回就行

  1. /* src/runtime-core/helper/renderSlot.ts */
  2. export function renderSlots(slots) {
  3. // 这里的逻辑,就是当 this.$slots 是一个数组时
  4. // 转到 initSlots() 中进行判断
  5. // 需要对 slots 进行判断
  6. return Array.isArray(slots) ? createVNode('div', {}, slots) : slots
  7. }

这样就完成了针对第二种情况的实现,但是此时第一种情况就无法正常渲染,为了同时包括两种情况,可以在初始化 slots 时进行处理,若 children 是一个 VNode 则将其转为数组,完善src/runtime-core目录下的componentSlots.ts文件中的initSlots函数:

  1. /* componentSlots.ts */
  2. export function initSlots(instance, children) {
  3. instance.slots = Array.isArray(children) ? children : [children]
  4. }

2. 实现具名插槽

具名插槽指的是,在由父组件向子组件传入插槽时传入一个对象,其中 key 为插槽的 name,用于指定插槽的位置,value 为插槽,而在子组件中将插槽的 name 作为第二个参数传入renderSlots函数来指定该位置要渲染的插槽。

  1. /*App.js*/
  2. export const App = {
  3. render() {
  4. const foo = h(
  5. Foo,
  6. {},
  7. // 在Foo 组件中能够渲染出来这里的内容
  8. // 1. 事件slot的基本需求,-> 在Foo中展示
  9. // h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
  10. // 2. 展示多个内容时 -> 在Foo中展示
  11. // [
  12. // h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
  13. // h('p', {}, "这是让slots实现 Array功能")
  14. // ]
  15. // 3. 实现具名插槽
  16. // 将 children 转为 Object; 实现具名插槽 key -> value
  17. {
  18. header: h('p', {}, "这个具名插槽的内容 hander"),
  19. footer: h('p', {}, "这个具名插槽的内容 footer")
  20. }
  21. )
  22. return h('div', {}, [app, foo])
  23. },
  24. }
  1. /* Foo.js */
  2. export const Foo = {
  3. render() {
  4. console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
  5. const foo = h("p", {}, "这是Foo组件")
  6. // 1. 基本的功能
  7. // return h('div', {}, [foo, this.$slots])
  8. // 2. App 中渲染多个内容
  9. // return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
  10. // 导入 renderSlots
  11. // return h('div', {}, [foo, renderSlots(this.$slots)])
  12. // 3. 实现具名插槽
  13. // 1. 获取要渲染的元素
  14. // 2. 获取到渲染的位置
  15. return h('div', {}, [
  16. renderSlots(this.$slots, "header"),
  17. foo, // 定义的 foo 组件
  18. // slot 的内容
  19. renderSlots(this.$slots, "footer")
  20. ]
  21. )
  22. }
  23. }

实现逻辑

  1. 获取到要渲染的虚拟节点
  2. 获取到要渲染的 位置 ```typescript / src/runtime-core/componentSlots.ts / // 处理 Slots 的逻辑中

export function initSlot(instance, children) {

const slots = {} // 循环 children 数组 for (const key in children) { // 取到值 const value = children[key]

  1. // 赋值给 slots
  2. slots[key] = Array.isArray(value) ? value : [value];

}

// 最后赋值给 instance.slots instance.slots = slots }

  1. 完善 `renderSlots()` 函数
  2. ```typescript
  3. /* helpers/renderSlots.ts */
  4. // 导出
  5. export function renderSlots(slots, name) { // 增加参数 name
  6. // 传入参数 name 实现具名插槽
  7. // slots[name] 通过 key 获取值
  8. const slot = slots[name];
  9. if (slot) { // 判断是否具有
  10. // 具有 slot, 直接渲染
  11. return createVNode('div', {}, slot)
  12. }
  13. }

此时页面
image.png

重构 componentSlots

  1. export function initSlots(instance, children) {
  2. // 重构
  3. normalizeObjectSlots(children, instance.slots)
  4. }
  5. // 实现赋值
  6. function normalizeObjectSlots(children, slots) {
  7. for (const key in children) {
  8. const value = children[key];
  9. // 实现作用域插槽逻辑
  10. // 这里拿到 value 是一个函数
  11. // 需要进行调用, 实现传参
  12. slots[key] = normalizeSlotValue(value)
  13. }
  14. function normalizeSlotValue(value) {
  15. return Array.isArray(value) ? value : [value]
  16. }

3. 实现作用域插槽


在官网上定义的作用域插槽 : 同时使用父组件域内和子组件域内的数据, 要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。

  1. /*App.js*/
  2. export const App = {
  3. render() {
  4. const foo = h(
  5. Foo,
  6. {},
  7. // 在Foo 组件中能够渲染出来这里的内容
  8. // 1. 事件slot的基本需求,-> 在Foo中展示
  9. // h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
  10. // 2. 展示多个内容时 -> 在Foo中展示
  11. // [
  12. // h('p', {}, "这是在父组件中slot, 传入Foo的内容"),
  13. // h('p', {}, "这是让slots实现 Array功能")
  14. // ]
  15. // 3. 实现具名插槽
  16. // 将 children 转为 Object; 实现具名插槽 key -> value
  17. // {
  18. // header: h('p', {}, "这个具名插槽的内容 hander"),
  19. // footer: h('p', {}, "这个具名插槽的内容 footer")
  20. // }
  21. // )
  22. // 4. 实现作用域插槽
  23. {
  24. // value 改为 函数
  25. // age 作为参数, 由子组件传入
  26. header: ({ age }) => h('p', {}, "这个作用域插槽的内容 hander + " + age),
  27. footer: () => h('p', {}, "这个作用域插槽的内容 footer")
  28. }
  29. )
  30. return h('div', {}, [app, foo])
  31. },
  32. }
  1. /* Foo.js */
  2. export const Foo = {
  3. render() {
  4. console.log(this.$slots) // slots 本质就是一个 Element 的虚拟节点
  5. const foo = h("p", {}, "这是Foo组件")
  6. // 1. 基本的功能
  7. // return h('div', {}, [foo, this.$slots])
  8. // 2. App 中渲染多个内容
  9. // return h('div', {}, [foo, h('div', {}, this.$slots)]) // 用 h 包裹 this.slots()
  10. // 导入 renderSlots
  11. // return h('div', {}, [foo, renderSlots(this.$slots)])
  12. // 3. 实现具名插槽
  13. // 1. 获取要渲染的元素
  14. // 2. 获取到渲染的位置
  15. // return h('div', {}, [
  16. // renderSlots(this.$slots, "header"),
  17. // foo,
  18. // // slot 的内容
  19. // renderSlots(this.$slots, "footer")
  20. // ]
  21. // )
  22. // 4. 实现作用域插槽
  23. // 定义一个变量,在子组件中传入,让父组件能够访问到
  24. // 父组件使用作用域插槽需要变为 函数式: header: ({age}) => h('p', {}, "这个具名插槽的内容 hander" + age),
  25. // 实现
  26. // 1. 使用子组件传入 { age } 参数
  27. // 2. 在 renderSlots 中,改变 slot, 因为 slot 已经是一个函数了
  28. // 3. 在 initSlots 中,修改代码, 此时的 value 已经是一个函数, 需要调用才能获取到返回值, 并且实现函数的传参
  29. const age = 18
  30. return h('div', {}, [
  31. // 传入 age 参数 -> { age }, renderSlots 的第三个参数
  32. renderSlots(this.$slots, "header", { age }),
  33. foo,
  34. // slot 的内容
  35. renderSlots(this.$slots, "footer")
  36. ]
  37. )
  38. }
  39. }

renderSlots 处理传入的参数

  1. // 导出
  2. export function renderSlots(slots, name, props) { // 增加参数 name props
  3. // 传入参数 name 实现具名插槽
  4. // slots[name] 通过 key 获取值
  5. const slot = slots[name];
  6. if (slot) { // 判断是否具有
  7. // 具有 slot, 直接渲染
  8. // 实现 作用域插槽时 slot 是一个函数
  9. if (typeof slot === "function") {
  10. return createVNode('div', {}, slot(props)) // 执行函数 并传入参数
  11. }
  12. }
  13. }

initSlots 中调用 value , 并传入参数

  1. export function initSlots(instance, children) {
  2. // 重构
  3. normalizeObjectSlots(children, instance.slots)
  4. }
  5. // 实现赋值
  6. function normalizeObjectSlots(children, slots) {
  7. for (const key in children) {
  8. const value = children[key];
  9. // 实现作用域插槽逻辑
  10. // 这里拿到 value 是一个函数
  11. // 需要进行调用, 实现传参
  12. slots[key] = (props) => normalizeSlotValue(value(props))
  13. }
  14. function normalizeSlotValue(value) {
  15. return Array.isArray(value) ? value : [value]
  16. }

页面展示
image.png

4. 完善 slots 功能

在创建 VNode 时,判断当前的节点是否是 slot 类型的节点 , 新增

  1. /* src/runtime-core/vnode.ts */
  2. // 添加处理当children是一个 object 时 -> 也就是具有 slots
  3. // 判定条件 : 是一个组件 + 具有children 是一个 object
  4. if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 如果当前节点是一个组件
  5. if (typeof children === "object") { // 并且 children 是一个 object -> slots
  6. // 也就说明当前节点是一个 具有 slot 的逻辑
  7. // 给当前节点 赋值为具有 slot 的状态
  8. vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN
  9. }
  10. }
  11. // 返回创建组件的虚拟节点
  12. return vnode
  1. /* src/shared/ShapeFlags.ts */
  2. {
  3. // 添加 当前节点的 slots 的状态
  4. SLOT_CHILDREN = 1 << 4 // 1 左移 4 位 -> 10000 -> 转为二进制 -> 10000
  5. }

在初始化 initSlots 时增加判断

  1. /* src/runtime-core/componentSlots.ts */
  2. // 初始化 Slots
  3. export function initSlots(instance, children) {
  4. // 重构
  5. // 并不是所有的节点都是具有 slots 的
  6. // 所以需要一个 判断逻辑 和 ShapeFlags 进行的判断一样
  7. // 1. 在初始化 ShapeFlags 时, 添加判断 slots 是否存在 的逻辑
  8. // 2. 添加 Slots 的状态 到 ShapeFlags 中
  9. // 3. 判断当前节点是否具有 slots
  10. const { vnode } = instance
  11. if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
  12. // 执行 slot 的逻辑
  13. // 再次重构
  14. normalizeObjectSlots(children, instance.slots)
  15. }
  16. }
  17. /* 其他代码 */

打包yarn build , 测试 页面, 没有出现报错, 则实现Slots

4.12 实现 Fragment 功能

在实现插槽中, 使用 createVNode() 方法使用了 div 包裹 slots 的内容 , 这样处理不太合理。 多增加一层 div
image.png
可以使用 Fragment 对插槽进行包裹, 而处理 Fragment时直接调用 mountChildren 函数,对VNode 进行渲染

  • 定义 Fragment ```typescript / shared/index.ts /

// 定义 Fragment export const Fragment = Symble(“Fragment”)

  1. - `renderSlots.ts` 使用 `Fragment`标签
  2. ```typescript
  3. /* helpers/renderSlots.ts */
  4. // 用于利用 Fragment 对插槽进行包裹
  5. export function renderSlots(slots, name, props) {
  6. // 通过 name 获取创建相应插槽的方法
  7. const slot = slots[name]
  8. if (slot) {
  9. if (typeof slot === 'function') {
  10. // 将创建插槽方法的执行结果作为 children 传入
  11. return createVNode(Fragment, {}, slot(props))
  12. }
  13. }
  14. }

renderer.ts 中添加判断逻辑

  1. /* renderer.ts*/
  2. function patch(vnode, container) {
  3. // 实现Fragment的判断逻辑
  4. const { type } = vnode // type 组件的类型
  5. switch (type) {
  6. // 如果是 Fragment 包裹的标签
  7. case Fragment:
  8. // 就调用 processFragment 函数
  9. processFragment(vnode, container)
  10. break;
  11. // 如果不是,则走 默认的逻辑
  12. default:
  13. // 判断 vnode 是否是一个组件 还是 Element
  14. if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
  15. // if (typeof vnode.type === 'string') {
  16. // 如果是一个 Element
  17. processElement(vnode, container)
  18. } else if (vnode.shapeFlag & ShapeFlags.STATIC_COMPONENT) {
  19. // } else if (isObject(vnode.type)) {
  20. // 如果是一个组件
  21. processComponent(vnode, container)
  22. }
  23. break
  24. }
  25. }
  26. // 处理 Fragment 包裹的标签
  27. function processFragment(vnode, container) {
  28. // 调用 mountChildren
  29. mountChildren(vnode, container)
  30. }

此时页面
image.png

4.13 实现 TextNode 功能

在作用域插槽中添加测试,在实现插槽的函数中,返回一个数组,第二项是一个文本字符串, 表示要渲染到页面的文本内容

  1. /* App.js */
  2. const foo = h(
  3. Foo,
  4. {},
  5. // 4. 实现作用域插槽
  6. {
  7. // 添加数组第二项,文本内容
  8. header: ({ age }) => [h('p', {}, "这个作用域插槽的内容 hander + " + age), "这是文本内容"],
  9. footer: () => h('p', {}, "这个作用域插槽的内容 footer")
  10. }
  11. )
  12. return h('div', {}, [app, foo])
  13. },

实现文本节点的渲染与 Fragment类似

  1. /**
  2. * 实现TextNode 逻辑
  3. * 因为在实际的 Vue3 中,这些text 都是直接写的; 这里使用了 render() 逻辑; 写text节点,使用到 createTextNode()方法,
  4. * 把 text 节点创建 VNode , 然后渲染到页面
  5. *
  6. * 实现
  7. * 1. 在 VNode 中创建 createTextNode() 函数 ; 返回返回一个 createVNode(Text, {}, text);
  8. * 创建Text类型的 虚拟节点, 并且把 text 内容传入children 属性; Text需要初始化定义 与 Fragment 实现相同,不需要实际的 标签包裹
  9. *
  10. * 2. 在 patch()函数中,判断是否Text 类型的节点, 如果是 进行 Text 的渲染
  11. * - 节点的创建 -> 添加节点到页面
  12. */
  • 创建文本节点
    1. /* shared/index.ts */
    2. // 定义 Text
    3. export const Text = Symbol("Text")
    ```typescript / vnode.ts /

// 创建 createTextNode // 实现 渲染 文本的逻辑 export function createTextNode(text: string) { // 创建按 Text 类型的 虚拟节点 -> VNode // 在到 patch() 中处理 Text 的 渲染 return createVNode(Text, {}, text) }

  1. 导出`createTextNode`
  2. ```typescript
  3. // src/runtime-core/index.ts
  4. export { createTextNode } from "./vnode"

在组件中使用 creatTextNode

  1. /* App.js */
  2. const foo = h(
  3. Foo,
  4. {},
  5. // 4. 实现作用域插槽
  6. {
  7. // 添加数组第二项,文本内容
  8. // 直接封装是 对TextNode 进行 包裹
  9. // createTextNode("text")
  10. header: ({ age }) => [h('p', {}, "这个作用域插槽的内容 hander + " + age), createTextNode("这是文本内容")],
  11. footer: () => h('p', {}, "这个作用域插槽的内容 footer")
  12. }
  13. )
  14. return h('div', {}, [app, foo])
  15. },
  • 渲染文本节点

render.ts 中,与 Fragment 的实现一样,Text 文本节点的渲染也是单独处理

  1. /* renderer.ts*/
  2. function patch(vnode, container) {
  3. const { type } = vnode
  4. switch (type) {
  5. case Fragment:
  6. processFragment(vnode, container)
  7. break;
  8. // 用于处理 Text
  9. case Text:
  10. processText(vnode, container)
  11. break
  12. // 如果不是,则走 默认的逻辑
  13. default:
  14. // 其他代码
  15. ...
  16. }
  17. }
  18. // 用于处理 Text 文本节点
  19. function processText(vnode, container) {
  20. // 渲染 Text 的虚拟节点的 Vnode 的逻辑
  21. // 通过解构赋值获取 Text 对应 VNode 的 children,即文本内容
  22. const { children } = vnode
  23. // 2. 创建一个 TextNode
  24. // 记得使用 vnode.el 赋值
  25. const textNode = (vnode.el = document.createTextNode(children))
  26. // 3. 添加到 container 内容
  27. container.appendChild(textNode)
  28. }

此时页面 内容
image.png

重新打包 yarn build , 检查页面, 没有报错说明 实现 TextNode 功能

4.14 实现 getCurrentInstance 方法

getCurrentInstance 方法是获取到当前组件的实例对象
getCurrentInstance 只能在 setup生命周期钩子中调用

组件定义

  1. /*App.js*/
  2. export const App = {
  3. name: "App",
  4. render() {
  5. // render中渲染了 App , 还渲染了 Foo
  6. return h('div', {}, [h('p', {}, 'currentInstance demo'), h(Foo)])
  7. },
  8. // getCurrentInstance 方法的实现
  9. setup() {
  10. // getCurrentInstance 的作用: https://v3.cn.vuejs.org/api/composition-api.html#getcurrentinstance
  11. // getCurrentInstance -> 获取当前组件的实例对象
  12. // getCurrentInstance 必须要在 setup() 中使用
  13. const instance = getCurrentInstance()
  14. console.log("这是getCurrentInstance 获取的App组件实例:", instance)
  15. }
  16. }
  1. /* Foo.js */
  2. // Foo 组件
  3. export const Foo = {
  4. name: "Foo",
  5. setup() {
  6. // 与App组件实现的逻辑一样 调用 getCurrentInstance 方法 查看当前的组件实例对象
  7. const instance = getCurrentInstance()
  8. console.log("Foo组件实例:", instance)
  9. },
  10. render() {
  11. return h("div", {}, "这是Foo组件")
  12. }
  13. }

实现getCurrentInstance

  1. 1. component中实现 getCurrentInstance 方法
  2. 2. 定义一个全局变量,在 setup() 调用时 给全局变量赋值为当前组件实例
  3. 3. getCurrentInstance 返回 这个全局变量
  4. 4. 导出 getCurrentInstance 方法
  1. /* component */
  2. // 定义currentInstance 用于记录当前的组件实例
  3. let currentInstance = null
  4. function setupStatefulComponent(instance) {
  5. // 其他代码
  6. // call setup()
  7. const { setup } = component
  8. if (setup) {
  9. // 给currentInstance 赋值
  10. // currentInstance = instance
  11. // 重构
  12. setCurrentInstance(instance)
  13. // 这里调用 setup
  14. const setupResult = setup(shallowReadonly(instance.props), { emit: instance.emit })
  15. // 其他代码
  16. }
  17. }
  18. // 给currentInstance赋值
  19. function setCurrentInstance(instance) {
  20. // 改为在此处赋值, 可以达到一个中间层的位置
  21. // 方便调式代码
  22. currentInstance = instance
  23. }
  24. // 导出这个函数API
  25. export function getCurrentInstance() {
  26. // 返回全局变量; 也就是 instance
  27. return currentInstance
  28. }

导出 getCurrentInstance

  1. /* index.ts */
  2. export { getCurrentInstance } from "./component"

打印输出
image.png
完成 getCurrentInstance() 方法的实现,重新打包, 检查运行

4.15 使用 inject & provide 方法

provide & inject 的介绍
无论组件层次解构有多深, 父组件都可以作为其所有子组件的依赖提供者。
这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

实现基本的 provide & inject

  1. /* example/ApiInject&Provide/App.js */
  2. // Provider 组件
  3. const Provider = {
  4. name: 'Provider',
  5. setup() {
  6. // 使用 provide 方法提供数据
  7. provide('foo', '这是Provider提供的数据 fooValue')
  8. provide('bar', '这是Provider提供的数据 barValue')
  9. },
  10. render() {
  11. // children -> [] ; 渲染 Consumer
  12. return h('div', {}, [h('p', {}, "Provider"), h(Consumer)])
  13. }
  14. }
  15. /**
  16. * 介绍 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
  17. * provider 提供数据,inject 注入数据, 用在父组件和更深层级的组件之间的数据传输
  18. *
  19. * 无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。
  20. * 父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。
  21. *
  22. * Provider & Inject 只能在 setup 中使用
  23. */
  24. // Consumer 组件 -> 子组件
  25. const Consumer = {
  26. name: 'Consumer',
  27. setup() {
  28. // 取到 Provider 组件传输的数据
  29. // 使用 inject 方法取数据 -> 根据 key 来取
  30. const foo = inject('foo')
  31. const bar = inject('bar')
  32. return {
  33. foo,
  34. bar
  35. }
  36. },
  37. // 渲染数据
  38. render() {
  39. return h('div', {}, `Consumer渲染数据: - foo: ${this.foo} - bar: ${this.bar}`)
  40. }
  41. }
  42. export default {
  43. name: 'App',
  44. setup() { },
  45. render() {
  46. // App 组件 -children 使用 Provider 组件
  47. return h('div', {}, [h("p", {}, "apiInject"), h(Provider)])
  48. }
  49. }

src/runtime-core/ 创建 apiInject.ts 实现的逻辑

  1. /* apiInject.ts */
  2. export function provide(key, value) {
  3. // 存数据
  4. // 1. 存到当前组件的 provides 中
  5. // 需要在 component 初始化 provides
  6. // 2. 获取到当前组件的实例 instance
  7. const currentInstance: any = getCurrentInstance()
  8. // 3. 将 key 和 value 存入 currentInstance.provides 中
  9. if (currentInstance) {
  10. const { provides } = currentInstance
  11. provides[key] = value
  12. }
  13. }
  14. export function inject(key) {
  15. // 取数据
  16. // 1. 取到当前组件的实例 instance
  17. const currentInstance: any = getCurrentInstance()
  18. // 2. 根据key 取到 当前组件的 父级的 provides
  19. // parent.provides
  20. if (currentInstance) {
  21. // 取它父级里 存储数据的 parent
  22. // 通过 parent 取到 provides 对象
  23. const parentProvides = currentInstance.parent.provides
  24. // 需要在renderer 中传入 parent
  25. // 返回取到的数据
  26. return parentProvides[key]
  27. }
  28. }

初始化 provides & parent

  1. /*component.ts*/
  2. export function createComponentInstance(vnode, parent) { // 这里传入parent, 当前组件的 父组件
  3. // 返回 vnode 这个组件
  4. const component = {
  5. // 其他代码
  6. // 初始化 provides, 用于存储provide传入的数据
  7. provides: {},
  8. // 初始化 parent, 用于存储父组件, 方便取到 父组件的 parent
  9. parent,
  10. }
  11. }

renderer.ts 中 给parent 赋值

  1. /* renderer.ts */
  2. export function render(vnode, container) {
  3. // 调用 path
  4. // 初始化时,patch() 接收第三个参数,也就是之后赋值的 parent -> 赋值父组件的实例
  5. // 但是这里是初始化 patch() parent 为null
  6. patch(vnode, container, null)
  7. }
  8. // 接下来的函数都需要添加这个第三个参数
  9. function patch(vnode, container, parentComponent) {
  10. const { type } = vnode
  11. switch (type) {
  12. case Fragment:
  13. processFragment(vnode, container, parentComponent)
  14. break;
  15. case Text:
  16. processText(vnode, container)
  17. break
  18. default:
  19. if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
  20. processElement(vnode, container, parentComponent)
  21. } else if (vnode.shapeFlag & ShapeFlags.STATIC_COMPONENT) {
  22. processComponent(vnode, container, parentComponent)
  23. }
  24. break
  25. }
  26. }
  27. // 之后需要函数方法都需要添加 parentComponent 父组件这个参数
  28. // 这里给 parentComponent 赋值 为当前组件的实例
  29. function setupRenderEffect(instance, vnode, container) {
  30. const { proxy } = instance
  31. const subTree = instance.render.call(proxy) // 将proxy 注入到 render() 中
  32. // 这里 parent 赋值为 instance, 也就是父级组件
  33. patch(subTree, container, instance)
  34. vnode.el = subTree.el
  35. }

导出 provode & inject

  1. /* index.ts */
  2. export { provide, inject } from "./apiInject"

此时已经实例基本父子级组件的传输数据
image.png

实现跨层级传输

在App 组件中实现一个中间层组件

  1. const Provide = {
  2. name: 'Provide',
  3. setup() {
  4. provide('foo', "FooValue")
  5. provide('bar', "BarValue")
  6. },
  7. render() {
  8. return h('div', {}, [
  9. h('p', {}, 'Provide'),
  10. // 这里改为渲染中间层组件
  11. h(ProviderTwo)
  12. ])
  13. }
  14. }
  15. // 增加中间层 组件
  16. // 加上另一种情况
  17. // 在中间层使用 provide() 时
  18. // 在中间层使用 inject 时
  19. const ProviderTwo = {
  20. name: 'ProviderTwo',
  21. setup() {
  22. // 往子组件传输数据
  23. provide("foo", "这是ProviderTwo提供的数据 fooValueTow")
  24. // 往父组件获取数据
  25. const foo = inject('foo')
  26. return {
  27. foo
  28. }
  29. },
  30. render() {
  31. // 渲染中间层 组件的数据
  32. return h('div', {}, [
  33. h('p', {}, 'ProviderTwo 获取到它父级组件的数据 --' + this.foo),
  34. // 这里渲染 Consumer
  35. h(Consumer)
  36. ])
  37. }
  38. }
  39. const Consumer = {
  40. name: 'Consumer',
  41. // ...
  42. }
  43. export default {
  44. // ...
  45. }

处理 provides 初始时 & 后台组件时的指向

  1. /* component.ts */
  2. export function createComponentInstance(vnode, parent) {
  3. // 返回 vnode 这个组件
  4. const component = {
  5. // 初始化 provides, 用于存储provide传入的数据
  6. // provides: {},
  7. // 处理 provides 的指向,
  8. // 当 parent 没有值时-> 初始,provides 为 空 ,
  9. // 当 parent 有值时 -> 说明当前组件是一个子组件,provides 指向 parent 的 provides
  10. provides: parent ? parent.provides : {},
  11. // 初始化 parent, 用于存储父组件的 provide
  12. parent,
  13. }
  14. }

当前页面
image.png
出现问题: 当前组件使用 provide存储数据,与父组件 key 相同时, 会取当前组件provide 存储的值, 而不是取父级组件的
解决: 本质是通过原型链的方式查找 provides , 重点: 把中间层 provides指向父级组件的 provides, 这样就形成了一条基于原型链的方式取数据
子组件具有自己的 provideinject的值 当子组件没有 后代组件需求的 inject值数据时,会从父组件的 provide中查找 (找原型链)

  1. /* apiInject.ts */
  2. export function provide(key, value) {
  3. // 存数据
  4. // 1. 存到当前组件的 provides 中
  5. // 需要在 component 初始化 provides
  6. // 2. 获取到当前组件的实例 instance
  7. const currentInstance: any = getCurrentInstance()
  8. // 3. 将 key 和 value 存入 currentInstance.provides 中
  9. if (currentInstance) {
  10. let { provides } = currentInstance
  11. // provides[key] = value
  12. // 处理 procvide 的指向
  13. // 1. 获取 父级 provides 对象
  14. const parentProvides = currentInstance.parent && currentInstance.parent.provides
  15. // 注意点:这里可能是 init 初始化
  16. // 2. 判断初始化: 当 provides 等于它 父级的 provides 对象时,说明是初始化
  17. if (provides === parentProvides) {
  18. // 初始化时,给 provides 对象赋值 -> 形成原型链
  19. // 改写 provides 指向 parent的 provides : 形成原型链
  20. provides = currentInstance.provides = Object.create(parentProvides)
  21. // currentInstance.provides 当前组件的 provides
  22. }
  23. // 这里赋值
  24. provides[key] = value
  25. }
  26. }

此时页面, 完成原型链的逻辑
image.png
添加 inject 方法的两个功能点

  • 添加 inject 的默认值
  • 默认值可以是一个函数

添加默认值

  1. /* App.js */
  2. // Consumber 组件
  3. const Consumer = {
  4. name: 'Consumer',
  5. setup() {
  6. // 获取 provider 传输过来的数据
  7. const foo = inject('foo')
  8. const bar = inject('bar')
  9. // 添加功能
  10. // 1. 实现默认值
  11. const baz = inject('baz', "默认值Baz")
  12. // 2. 默认值是一个函数
  13. return {
  14. foo,
  15. bar,
  16. baz
  17. }
  18. },
  19. render() {
  20. // 这里渲染 foo 和 bar
  21. // 展示默认值 baz
  22. return h('div', {}, `Consumer渲染数据: - foo: ${this.foo} - bar: ${this.bar} --- baz: ${this.baz}`)
  23. }
  24. }

实现两个功能

  1. /* src/runtime-core/apiInject.ts */
  2. export function inject(key, defaultValue) { // 添加默认值
  3. // 取数据
  4. // 1. 取到当前组件的实例 instance
  5. const currentInstance: any = getCurrentInstance()
  6. // 2. 根据key 取到 当前组件的 父级的 provides
  7. // parent.provides
  8. if (currentInstance) {
  9. // 取它父级里 存储数据的 parent
  10. // 通过 parent 取到 provides 对象
  11. const parentProvides = currentInstance.parent.provides
  12. // 需要在renderer 中传入 parent
  13. if (key in parentProvides) {
  14. // 如果有 key 时
  15. // 返回取到的数据
  16. return parentProvides[key]
  17. } else if (defaultValue) {
  18. // 如果有 defaultValue 默认值时
  19. // return defaultValue
  20. // 如果默认值是 function 时
  21. if (typeof defaultValue === 'function') {
  22. return defaultValue()
  23. }
  24. return defaultValue
  25. }
  26. }
  27. }

展示页面
image.png

打包yarn build 检查页面 , 完成 inject & provide

4.16 实现自定义渲染器 custom renderer

自定义渲染器的定义
因为使用到的Vue模板不止是DOM平台,还可以自定义渲染其他平台

createRenderer函数接受两个泛型参数: HostNode 和 HostElement,对应于宿主环境中的 Node 和 Element 类型。

例如,对于 runtime-dom,HostNode 将是 DOM Node 接口,HostElement 将是 DOM Element 接口。

  1. // 实现 渲染 Canvas 平台的API
  2. /**
  3. * 为了让渲染器 能够实现多个 平台接口,不仅能够实现 Element 还能够渲染 Canvas , 所以修改代码让它不单单支持Element
  4. * 而是通过稳定的接口函数来 渲染 Element & Canvas
  5. *
  6. * 需要3接口:
  7. * createElement : 创建 Element
  8. * patchProp : 给Element 添加属性
  9. * insert : 添加标签到 container 容器
  10. *
  11. *
  12. * 实现方式
  13. * 1. 定义一个 createRender({}) 函数 -> 渲染器,传入对应的接口过来
  14. * 2. 把具体的实现函数传入给createRender()
  15. */

定义自定义渲染器函数createRender()

  1. /* renderer.ts*/
  2. // 导出 自定义渲染函数 createRender
  3. // 参数: options -> 传入对应的接口函数
  4. export function createRender(options) {
  5. // 这里内部的 render() 函数
  6. function render(vnode, container) {
  7. // export function render(vnode, container) {
  8. // 调用 path
  9. patch(vnode, container, null)
  10. }
  11. /* 省略所有代码*/
  12. }

导出 createRender

  1. /* runtime-core*/
  2. export { createRenderer } from "./renderer"

传入 options 对应的接口方法 , vue模板默认渲染 DOM平台,把DOM平台的接口封装到 runtime-dom/index.ts

  1. /* src/runtime-dom/index.ts */
  2. // 导入createRenderer
  3. import { createRenderer } from '../runtime-core'
  4. // 需要3接口:
  5. // createElement : 创建 Element
  6. // patchProp : 给Element 添加属性
  7. // insert : 添加标签到 container 容器
  8. // 定义 createElement -> 与在render中 createElement 逻辑相同相同
  9. function createElement(type) {
  10. // 返回创建的 Element 标签
  11. return document.createElement(type)
  12. }
  13. // 给Element 添加属性
  14. function patchProp(el, key, value) {
  15. const isOn = () => /^on[A-Z]/.test(key)
  16. if (isOn()) {
  17. // 这里实现注册事件
  18. el.addEventListener(key.slice(2).toLowerCase(), value)
  19. } else {
  20. el.setAttribute(key, value)
  21. }
  22. }
  23. // 添加标签到 parent 容器
  24. function insert(el, parent) {
  25. parent.append(el)
  26. }
  27. // 调用函数 传入 实现Element 渲染的接口
  28. createRender({ // 用一个 {} 包裹
  29. createElement,
  30. patchProp,
  31. insert
  32. })

renderer 逻辑中,使用传入的接口

  1. /* src/runtime-core/renderer.ts */
  2. export function createRender(options) {
  3. // 需要3接口:
  4. // createElement : 创建 Element
  5. // patchProp : 给Element 添加属性
  6. // insert : 添加标签到 container 容器
  7. const {
  8. createElement,
  9. patchProp,
  10. insert,
  11. } = options
  12. /* 省略所有代码*/
  13. // 在 挂载 Element时,使用定义的接口
  14. function mountElement(vnode, container, parentComponent) {
  15. // 1. 创建 element
  16. // 改为使用 createElement 接口
  17. const el = (vnode.el = createElement(vnode.type))
  18. // const el = (vnode.el = document.createElement(vnode.type)) // 这里给 el 赋值
  19. // 2. props
  20. const { props } = vnode
  21. if (props) {
  22. for (const key in props) {
  23. // console.log(key)
  24. const value = props[key]
  25. // 把赋值的逻辑写道 patchProp 方法中
  26. // 这里使用 patchProp 接口
  27. patchProp(el, key, value)
  28. }
  29. }
  30. // 3. children
  31. // 省略代码
  32. // 4. append
  33. // container.appendChild(el)
  34. // 使用 insert 接口
  35. insert(el, container)
  36. }
  37. }

因为在 runtime-core 中实现的 createApp.ts 使用到 render() 函数,而当前封装了createRender() 自定义渲染器, render 函数的逻辑被包裹起来了,访问不到 。
设置creatApp访问render() 函数的逻辑

createRender()的返回值

  1. /* src/runtime-core/renderer.ts */
  2. export function createRender(options) {
  3. // createRenderer 自定义渲染器导出的方法
  4. return {
  5. // createAppAPI(render) 传入 render 方法
  6. // 并且拿到 createAppAPI 的返回值, createApp() 函数
  7. // 使用 createApp 命名接收的 createApp
  8. createApp: createAppAPI(render)
  9. }
  10. }

createApp.ts 封装 createApp 方法

  1. /* src/runtime-core/createApp.ts */
  2. // 创建 createAppAPI () 接收 render 函数,
  3. export function createAppAPI(render) {
  4. // 并返回 createApp() 函数 -> 真实的 createApp 函数
  5. return function createApp(rootComponent) {
  6. return {
  7. mount(rootContainer) {
  8. const vnode = createVNode(rootComponent);
  9. render(vnode, rootContainer)
  10. }
  11. }
  12. }
  13. }

runtime-dom/index.ts 下接收 createRender.ts 的返回值 ,对createApp 再进一步封装

  1. /* src/runtime-dom/index.ts */
  2. /* 其他代码 */
  3. // 调用createRender函数传入实现Element渲染的接口
  4. // 使用 renderer 接收 createRender 的返回值
  5. const renderer: any = createRender({
  6. createElement,
  7. patchProp,
  8. insert
  9. })
  10. // 这里封装一个 createApp 函数, 直接调用的是 renderer.createApp()
  11. // 这个就是 给用户调用 createApp 接口的函数
  12. export function createApp(...args) {
  13. // 这里是封装的 createApp()
  14. return renderer.createApp(...args)
  15. }

导出 runtime-dom

  1. /* src/index.ts */
  2. // 整体出口
  3. export * from './runtime-dom'
  4. // export * from "./runtime-core"

因为 runtime-dom 作为 runtime-core 的上一层,把 导出runtime-core 放到 runtime-dom

  1. /* src/runtime-dom/index.ts */
  2. // 这里导出 runtime-core
  3. export * from "../runtime-core"

重新打包 yarn build, 检查所有 example 下的页面,没有出现问题说明 实现 runtime-dom 的逻辑

使用 PIXIJS 自定义渲染

让实现vue能够支持 Canvas 平台, 而 PixiJS 就是基于Canvas

  1. // 引入PIXIJS
  2. <script src="https://cdn.jsdelivr.net/npm/pixi.js@6.5.1/dist/browser/pixi.js"></script>
  1. // 导入 createRender -> 自定义渲染器
  2. import { createRenderer } from '../../lib/guide-mini-vue.esm.js'
  3. import { App } from "./App.js"
  4. // 打印 PIXI
  5. // console.log(PIXI)
  6. // 创建 PIXI 实例
  7. const game = new PIXI.Application({
  8. width: 800,
  9. height: 600,
  10. })
  11. // 添加实例
  12. document.body.append(game.view)
  13. // 实现使用自定义渲染器 渲染 Canvas
  14. const renderer = createRenderer({
  15. createElement(type) {
  16. if (type === 'rect') {
  17. const rect = new PIXI.Graphics()
  18. rect.beginFill(0xFF0000)
  19. rect.drawRect(0, 0, 100, 100)
  20. rect.endFill()
  21. return rect
  22. }
  23. },
  24. patchProp(el, key, val) {
  25. el[key] = val
  26. },
  27. insert(el, parent) {
  28. // append
  29. parent.addChild(el)
  30. }
  31. })
  32. // 渲染
  33. renderer.createApp(App).mount(game.stage)
  1. import { h } from "../../lib/guide-mini-vue.esm.js"
  2. export const App = {
  3. setup() {
  4. return {
  5. x: 100,
  6. y: 100,
  7. }
  8. },
  9. render() {
  10. // console.log(this.x, this.y)
  11. return h("rect", { x: this.x, y: this.y })
  12. }
  13. }

页面展示
image.png

总结

runtime-core 的内容完成