runtime-core(一)

runtime-core

初始化 component 流程

我们先创建 example 目录,先写好示例文件,其中包括以下三个文件

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>
  <style>
    .red {
      color: red;
    }

    .green {
      color: green;
    }
  </style>
  <div id="app"></div>
  <script src="main.js" type="module"></script>
</body>

</html>

Main.js

import { createApp } from '../lib/guide-mini-vue.esm.js';
import { App } from './App.js'

const root = document.querySelector('#app');
createApp(App).mount(root)

App.js

import { h } from '../lib/guide-mini-vue.esm.js';

export const App = {
  render() {
    return h('div',
      {
        id: 'root',
        class: ['red'],
      },
      'Hi, ' + this.msg
    )
  },
  setup() {
    // composition API
    return {
      msg: 'mini-vue',
    }
  }
}

有了 example 我们来实现其功能,首先创建 createApp.ts 文件

import { render } from "./renderer"
import { createVNode } from "./vnode"

export function createApp(rootComponent: any) {
  return {
    mount(rootContainer: any) {
      // 首先转 vNode
      // component => vNode
      // 所有的逻辑都基于 vNode 操作
      const vNode = createVNode(rootComponent)

      render(vNode, rootContainer)
    }
  }
}

这里用到了 render 和 createVNode,我们来创建 vnode.ts 和 renderer.ts 文件来完成这两部分

export function createVNode(type: any, props?: any, children?: any) {
  const vNode = { 
    type, props, children
  }

  return vNode
}
export function render(vNode: any, container: any) {
  // patch 为了方便后续递归
  patch(vNode, container)
}

这里 render 函数为了方便后续的递归处理,将所有的逻辑抽离了出去

function patch(vNode: any, container: any) {
  // TODO component 和 Element 需要区分开
  processComponent(vNode, container)
}


function processComponent(vNode: any, container: any) {
  mountComponent(vNode, container)
}

function mountComponent(vNode: any, container: any) {
  const instance = createComponentInstance(vNode)

  setupComponent(instance)
  setupRenderEffect(instance, container)
}


function setupRenderEffect(instance: any, container: any) {
  const subTree = instance.render()

  // vNode -> patch
  // vNode -> element -> mountElement
  patch(subTree, container)
}

然后还有对组件的处理函数,我们将他们提取到 component.ts 中

export function createComponentInstance(vNode: any) {
  const component = {
    vNode,
    type: vNode.type
  }

  return component
}


export function setupComponent(instance: any) {
  // TODO
  // initProps()
  // initSlots()

  setupStatefulComponent(instance)
}

function setupStatefulComponent(instance: any) {
  const Component = instance.vNode.type

  const { setup } = Component
  
  if (setup) {
    // function | object
    // function: 组件的 render 函数
    // object: 把 object 注入到上下文
    const setupResult = setup()

    handleSetupResult(instance, setupResult)
  }
}

function handleSetupResult(instance: any, setupResult: any) {
  // TODO function
  if (typeof setupResult === 'object') {
    instance.setupState = setupResult 
  }

  finishComponentSetup(instance)
}

function finishComponentSetup(instance: any) {
  const Component = instance.type

  if (Component.render) {
    instance.render = Component.render
  }
}

这里大体的流程我们已经走完了,现在还不能实现效果,因为浏览器不认识 typescript,所以我们需要使用打包工具将源代码进行打包构建

Rollup 打包

安装 rollup

$ yarn add rollup --dev

在 package.json 中添加构建脚本

"scripts": {
  "test": "jest",
  "build": "rollup -c rollup.config.js"
},

在项目根目录创建 rollup.config.js

import typescript from '@rollup/plugin-typescript'

export default {
  input: './src/index.ts', // 输入文件
  // 输出相关
  output: [
    // 1. cjs
    {
      format: 'cjs',
      file: 'lib/guide-mini-vue.cjs.js'
    },
    // 2. mjs
    {
      format: 'es',
      file: 'lib/guide-mini-vue.esm.js'
    }
  ],
  // typescript 语法
  plugins: [typescript()]
}

在入口文件导出模块

export * from './runtime-core'

由于我们使用了 ts,所以需要配置 ts 语法解析,并且需要额外安装 tslib

image-20220731213013730

$ yarn add @rollup/plugin-typescript tslib --dev

然后我们再执行 yarn build,此时已经可以成功打包

image-20220731213236241

初始化 Element

经过上一小节的打包之后我们并不能成功的运行,因为我们并没有对 element 元素进行处理,导致在代码执行过程中发生了报错

image-20220801154022475

通过调试我可能够定位到问题发生在 element 元素上,element 元素并没有 render 方法,所以我们需要在 patch 的时候根据 vNode 的类型进行不同的处理

function patch(vNode: any, container: any) {
  /*
  * 处理组件
  * 判断是不是 element, 
  * 如果是 element 则处理 element, 
  * 如果是 component 就处理 component
  */
  // console.log(vNode.type);
  if (typeof vNode.type === 'string') {
    // element
    processElement(vNode, container)
  } else if (isObject(vNode.type)) {
    // component
    processComponent(vNode, container)
  }
}

function processElement(vNode: any, container: any) {
  mountElement(vNode, container)
}

function mountElement(vNode: any, container: any) {
  const el = document.createElement(vNode.type)

  // 内容
  const {children, props} = vNode

  // string
  el.textContent = children

  // props
  for (let key in props) {
    const val = props[key]
    el.setAttribute(key, val)
  }

  container.appendChild(el)
}

此时浏览器打开 index.html 已经可以看到效果了(可以开启 rollup 的监听模式,yarn build –watch)

image-20220801155502286

可以发现class 已经添加上了,至于这里为什么是 undefined ,因为我们在 main.js 中写的是 this.msg,但这我们目前并没有绑定 this,所以这里的 this.msg获取不到值

挂载 this 的逻辑后面再搞,这里我们先处理其他情况。

这里我们只是做了一个children 是字符串的例子,真实的场景是 children可能是一个 vnode 或者多个 vnode

所以我们需要对这两种情况做一下适配

function mountElement(vNode: any, container: any) {
  const el = document.createElement(vNode.type)

  // 内容
  const {children, props} = vNode

  if (typeof children === 'string') {
    // string
    el.textContent = children
  } else if (Array.isArray(children)) {
    // array
    children.forEach(child => {
      patch(child, el)
    })
  } else {
    // vnode
    patch(children, el)
  }

  // props
  for (let key in props) {
    const val = props[key]
    el.setAttribute(key, val)
  }

  container.appendChild(el)
}

这里对 vnode 的处理方式是使用 patch,如果是数组就遍历 patch

我们再修改一下 App.js 来测试一下

export const App = {
  render() {
    return h('div',
      {
        id: 'root',
        class: ['bg'],
      },
      // 'Hi, ' + this.msg, 
      h('p', { class: ['green'] },
        [h('p', { class: ['red'] }, 'hi'), h('p', { class: ['green'] }, 'mini-vue')]
      ),
    )
  },
  setup() {
    // composition API
    return {
      msg: 'mini-vue',
    }
  }
}

完成

image-20220801163708488

至此我们已经完成了流程图中款选的部分,下一节我们将处理组件的渲染。

image-20220801173155958

组件代理

上一节留下了一个获取不到 this.msg 的问题,这里我们来解决一下,我们其实只需要将 setup 的返回值挂载到函数的 this上即可,但是这个过程并不止绑定 setup 返回值这一点,还有其他属性比如$el$data……

所以我们使用组建代理的形式来将这些属性添加到 this 上

首先我们来处理 setup,将 component.ts 中的setupStatefulComponent函数稍作修改

function setupStatefulComponent(instance: any) {
  const Component = instance.vNode.type

  instance.proxy = new Proxy({}, {
    get(target, key) {
      const {setupState} = instance
      if (key in setupState) {
        return setupState[key]
      }
    }
  })

  const { setup } = Component
  
  if (setup) {
    // function | object
    // function: 组件的 render 函数
    // object: 把 object 注入到上下文
    const setupResult = setup()

    handleSetupResult(instance, setupResult)
  }
}

除了这里还要将 renderer.ts 中的setupRenderEffect 函数改一下

function setupRenderEffect(instance: any, container: any) {
  const {proxy} = instance
  const subTree = instance.render.call(proxy)

  // vNode -> patch
  // vNode -> element -> mountElement
  patch(subTree, container)
}

将 render 时的 this 显示绑定为上面创建的 proxy 对象

此时页面上已经可以正确显示了

image-20220802200822076

接下来我们来处理$el,先来看一下文档中对于这个属性的功能描述

组件实例正在使用的根 DOM 元素。

对于使用了片段的组件,$el 是占位 DOM 节点,Vue 使用它来跟踪组件在 DOM 中的位置。建议使用模板引用来直接访问 DOM 元素,而不是依赖于 $el

知道了功能,我们就会发现其实这里我们很好实现,因为我们之前处理过 el

image-20220802202315152

这是我们上一小节处理 element 流程时添加的代码,在这里我们只需要将 el 元素保存到 vNode 对象中就可以传递出去

const el = vNode.el = document.createElement(vNode.type)

然后再判断 proxy 中 get 的 key 是否为$el即可

function setupStatefulComponent(instance: any) {
  const Component = instance.vNode.type

  instance.proxy = new Proxy({}, {
    get(target, key) {
      const {setupState} = instance
      if (key in setupState) {
        // setup 中返回的属性
        return setupState[key]
      }

      if (key === '$el') {
        // $el
        return instance.vNode.el
      }
    }
  })

然后我们在浏览器控制台中看一下效果

image-20220802202717396

这里看到 self 已经拿到了刚才设置的 proxy 对象,但是$el并没有成功地获取到根元素,然后我们可以断点调试一下

image-20220802203734459

这里可以看见 vNode 中 el 是 null,即没有成功的赋值,可是我们上面已经有过赋值了啊!这里其实是因为我们赋值的元素并不是 instance.vNode,我们赋值的过程在处理 element 元素中。

那么要怎么传递这个 el 元素呢?这里我们可以思考一下这个 mount 的过程。

我们在哪里调用的 mountElement 呢?顺着我们之前的代码往前找可以发现实在 patch 中最终调用了mountElement。那么 component 的 patch 什么情况下会走到 createElement 呢?答案是 render 完成之后。

render 之后会重新调用 patch,这时的组件已经被render 为 element,所以 patch 会走到 createElement 函数

image-20220802205139953

这里的 subTree 其实就是我们需要获取的 el 元素

function mountComponent(vNode: any, container: any) {
  const instance = createComponentInstance(vNode)

  setupComponent(instance)
  setupRenderEffect(instance, vNode, container)
}


function setupRenderEffect(instance: any,vNode: any, container: any) {
  const {proxy} = instance
  const subTree = instance.render.call(proxy)

  // vNode -> component -> render -> patch
  // vNode -> element -> mountElement
  patch(subTree, container)
  vNode.el = subTree
}

这时候已经能够正确的拿到 el 了

image-20220802205906511

后续还有有其他的api 绑定

image-20220802214918793

这一小节我们完成了组件的代理,下一小节我们将会进行 shapeFlags 的处理

shapeFlags

vue3 中使用位运算处理 shapeFlags,提高了性能,简化了逻辑。位运算执行效率高我们都知道,那么我们来了解一下位运算怎么简化代码

如果我们不使用位运算的形式,我们来看一下应该用什么代码处理

const shapeFlags = { 
  element: 0,
  stateful_component: 0,
  text_children: 0,
  array_children: 0,
}

// 修改
shapeFlags.element = 1
shapeFlags.stateful_component = 1

// 查找
if (shapeFlags.element) { }
if(shapeFlags.stateful_component) {}

这样处理的话当条件多了就会看起来非常的繁杂,我们再来看一下位运算的形式怎么处理这些逻辑,我们需要提前定义好各种元素对应的数

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

// 修改
0000 | element => 0001

// 查找
0001 & element => 0001
0010 & element => 0000

下面我们就将之前的形式修改为位运算的形式

新建/src/shared/ShapeFlags.ts

export const enum ShapeFlags {
  ELEMENT = 1, // 0001
  STATEFUL_COMPONENT = 1 << 1, // 0010
  TEXT_CHILDREN = 1 << 2, // 0100
  ARRAY_CHILDREN = 1 << 3 // 1000
}

然后在创建 vnode 的时候将 shapeFlags 注入

import { ShapeFlags } from '../shared/ShapeFlags'

export function createVNode(type: any, props?: any, children?: any) {
  const vNode = {
    type,
    props,
    children,
    shapeFlags: getShapeFlags(type),
    el: null as Element
  }

  // children
  if (typeof children === 'string') {
    vNode.shapeFlags |= ShapeFlags.TEXT_CHILDREN
  } else if (Array.isArray(children)) {
    vNode.shapeFlags |= ShapeFlags.ARRAY_CHILDREN
  }

  return vNode
}

function getShapeFlags(type: any) {
  return typeof type === 'string'
    ? ShapeFlags.ELEMENT
    : ShapeFlags.STATEFUL_COMPONENT
}

然后将 render 中的判断方法改为位运算形式

function patch(vNode: any, container: any) {
  const { shapeFlags } = vNode
  // console.log(vNode.type);
  if (shapeFlags & ShapeFlags.ELEMENT) {
    // element
    processElement(vNode, container)
  } else if (shapeFlags & ShapeFlags.STATEFUL_COMPONENT) {
    // component
    processComponent(vNode, container)
  }
}

function mountElement(vNode: any, container: any) {
  const el = (vNode.el = document.createElement(vNode.type))

  // 内容
  const { children, props, shapeFlags } = vNode

  // here ↓↓↓↓↓
  if (shapeFlags & ShapeFlags.TEXT_CHILDREN) {
    // string
    el.textContent = children
  } else if (shapeFlags & ShapeFlags.ARRAY_CHILDREN) {
    // array
    children.forEach((child: any) => {
      patch(child, el)
    })
  } else {
    // vnode
    patch(children, el)
  }

  // props
  for (let key in props) {
    const val = props[key]
    el.setAttribute(key, val)
  }

  container.appendChild(el)
}

注册事件

我们在使用 vue 时会用到@click或者v-on:click,这是绑定事件,在处理模板的时候需要把绑定事件进行注册。

在处理注册事件的时候我们要在处理 props 的时候拦截指定的 key,判断 key 是否是事件规范,如果是事件 API 则进行注册事件。

function mountElement(vNode: any, container: any) {
  const el = (vNode.el = document.createElement(vNode.type))

  // 内容
  const { children, props, shapeFlags } = vNode

  if (shapeFlags & ShapeFlags.TEXT_CHILDREN) {
    // string
    el.textContent = children
  } else if (shapeFlags & ShapeFlags.ARRAY_CHILDREN) {
    // array
    children.forEach((child: any) => {
      patch(child, el)
    })
  } else {
    // vnode
    patch(children, el)
  }

  // props
  for (let key in props) {
    const val = props[key]
    if (isOn(key)) {
      const eventName = key.slice(2).toLowerCase()
      el.addEventListener(eventName, val)
    } else {
      el.setAttribute(key, val)
    }
  }

  container.appendChild(el)
}


// 判断是否为事件
export const isOn = (key: string) => {
  return /^on[A-Z]/.test(key)
}

就这么简单我们已经完成了注册事件。

image-20220803204917268

组件 props

我们都知道组件之间的通信主要是通过 props 的形式实现的,那么这一节我们就来实现一下组件的 props 功能。

首先新建一个组件作为测试用例

import { h } from "../lib/guide-mini-vue.esm.js";
export const Foo = {
  setup(props) {
    console.log(props);
  },

  render() {
    h('div', {}, 'foo: ' + this.count)
  }
}

这里我们的 props 有三个要求:

  • 在 setup 中作为参数接收
  • 可以使用 this 访问到 props 属性
  • props 是只读的

然后在 App 组件中引入 Foo 组件

import { h } from '../lib/guide-mini-vue.esm.js';
import { Foo } from './Foo.js'

export const App = {
  render() {
    return h('div',
      {
        id: 'root',
        class: ['bg'],
      },
      [
        h('div', {}, 'Hi, ' + this.msg),
        h(Foo, { count: 1 })
      ]
    )
  },
  setup() {
    return {
      msg: 'mini-vue',
    }
  }
}

此时若访问页面将会看到结果为 undefined。

我们之前留下过一个 TODO initProps,我们只需要将其实现一下并将 props 作为参数传递到 setup 中即可

image-20220804170914349

export function initProps(instance: any, rawProps: any) {
  instance.props = rawProps || {}

  // attrs
}

这时候已经完成了第一步,接下来我们要把 props 的属性绑定到组件的 this 上,这里继续使用之前绑定 state 的方式将 props也挂载到组件 this 上

export const publicInstanceProxyHandlers = {
  get({ _: instance }: { _: any }, key: string | symbol) {
    const { setupState, props } = instance

    if (hasOwn(setupState, key)) {
      // setup 中返回的属性
      return setupState[key]
    } else if (hasOwn(props, key)) {
      // props 中的属性
      return props[key]
    }

    const publicGetter = publicPropertiesMap[key]
    if (publicGetter) {
      return publicGetter(instance)
    }
  }
}


// 检查制定对象上是否有 key
export const hasOwn = (target: any, key: string | symbol) =>
  Object.prototype.hasOwnProperty.call(target, key)

此时已经可以通过 this 获取 props 的值了

image-20220804181314574

下一步我们要将这个值变为 readonly,这里在向 setup 传值的时候调用一下我们之前reactive 模块的 readonly 即可,这里 vue 源码其实用的是 shallowReadonly,那我们也是用 shallowReadonly 即可。

function setupStatefulComponent(instance: any) {
  const Component = instance.vNode.type

  instance.proxy = new Proxy({ _: instance }, publicInstanceProxyHandlers)

  const { setup } = Component

  if (setup) {
    const setupResult = setup(shallowReadonly(instance.props))

    handleSetupResult(instance, setupResult)
  }
}

OK,我们已经完成了对 props 的处理

组件 emit

emit 是组件通信的另一个利器,有了 props 我们实现了组件之间的单向通信,而 emit 可以实现反向发送数据,这一小节,我们将实现 emit 的功能。

首先我们准备一个带有 emit 功能的 Foo 组件

mport { h } from "../lib/guide-mini-vue.esm.js";
export const Foo = {
  setup(props, { emit }) {

    function emitAdd() {
      console.log('emit add');
      emit('add', 2)
    }

    return { emitAdd }
  },

  render() {
    const btn = h('button', {
      onClick: this.emitAdd
    }, '+')
    const foo = h(
      'p',
      {},
      'foo count: + 'this.count
    )
    return h('div', {}, [btn, foo])
  }
}

组件中有两个元素,一个 p 标签用于显示 props 接受的 count 值,一个 button 按钮用于触发 emit 事件。

然后在 App 组件进行调用

export const App = {
  render() {
    return h('div',
      [
        h('div', {}, 'Hi, ' + this.msg),
        h(Foo, {
          count: this.count,
          onAdd(num) {
            console.log('on-add', num);
          }
        })
      ]
    )
  },
  setup() {
    return {
      msg: 'mini-vue',
      count: 1
    }
  }
}

从这个用例我们来看一下 emit 需要实现哪些功能点:

  • setup 的第二个参数是一个对象,可以从这个对象中获取 emit 方法;
  • emit 第一个参数是一个事件名,用于触发父组件绑定的事件;
  • emit 函数可以接收参数,从第二个参数开始就是事件的参数。

第一步我们将 emit 方法传入 setup

function setupStatefulComponent(instance: any) {
  const Component = instance.vNode.type

  instance.proxy = new Proxy({ _: instance }, publicInstanceProxyHandlers)

  const { setup } = Component

  if (setup) {
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit
    })

    handleSetupResult(instance, setupResult)
  }
}

export function createComponentInstance(vNode: any) {
  const component = {
    vNode,
    type: vNode.type,
    setupState: {},
    props: {},
    emit: (args: any): any => {}
  }

  component.emit = emit // 组合方式从外部引入

  return component
}

这里的 emit 我们将其初始化在 component 中,然后通过组合的方式将其赋值

export function emit(eventName: string) {
  console.log('init event')
}

此时我们点击按钮就可以看待事件触发了

image-20220804201415960

然后我们要使用 emit 来触发父组件绑定的事件

绑定事件获取在 props 中,所以我们处理绑定事件时需要拿到 props,我们可以直接将 instance 传到 emit 中,但是每次调用都传一个 instance 的体验非常不好,所以我们可以使用 bind 来直接传入

export function createComponentInstance(vNode: any) {
  const component = {
    vNode,
    type: vNode.type,
    setupState: {},
    props: {},
    emit: (args: any): any => {}
  }

  component.emit = emit.bind(null, component)

  return component
}

然后我们来处理 emit 函数

import { capitalize } from '../shared'

export function emit(instance: any, eventName: string) {
  console.log('init event')
  const { props } = instance

  const toHandlerKey = (str: string) => (str ? 'on' + capitalize(str) : '')

  const handler = props[toHandlerKey(eventName)]
  handler && handler()
}

// 首字母转为大写
export const capitalize = (str: string) =>
  str.charAt(0).toUpperCase() + str.slice(1)

这里我们已经可以触发组件上绑定的事件了

image-20220804210855582

最后一步,处理传参,这一步也很简单,只需要将函数的参数全部转发给执行函数即可

export function emit(instance: any, eventName: string, ...args: any[]) {
  console.log('init event')
  const { props } = instance

  const toHandlerKey = (str: string) => (str ? 'on' + capitalize(str) : '')

  const handler = props[toHandlerKey(eventName)]
  handler && handler(...args)
}

完成

image-20220804211239574

slots 插槽

插槽我们都使用过,是父组件向子组件传递组件参数的一种形式,子组件可以设置 slot 的默认值,如果父组件声明了组件则覆盖。

那都是 template 的写法,需要 sfc 解析配合,其实 vue 提供了$slots 获取插槽

image-20220805212051007

首先我们准备一个 slots 的 example

// App.js
export const App = {
  render() {
    return h('div',
      {},
      [
        h('div', {}, 'App'),
        h(Foo, {}, h('p', {}, '1213'))
      ]
    )
  },
  setup() {
    return {
    }
  }
}

// Foo.js
export const Foo = {
  setup() {
    return {}
  },

  render() {
    const foo = h(
      'p',
      {},
      'foo')
    console.log(this.$slots);
    return h('div', {}, [foo, this.$slots])
  }
}

还记得我们之前在 component 中的另一个 TODO 吗,initSlots

export function createComponentInstance(vNode: any) {
  const component = {
    vNode,
    type: vNode.type,
    setupState: {},
    props: {},
    slots: {},
    emit: (args: any): any => {}
  }

  component.emit = emit.bind(null, component)

  return component
}

export function setupComponent(instance: any) {
  initProps(instance, instance.vNode.props)
  initSlots(instance, instance.slots.children)

  setupStatefulComponent(instance)
}

我们先来直接将 children 赋值给 slots 看一下,这里只是看一下效果

export function initSlots(instance: any, children: any[]) {
  instance.slots = children
}

image-20220805213501496

这只是看一下效果,接下来顺着这个思路继续,slots 的位置在子组件中是任意摆放的,如果取下标就太麻烦了。如果你刚才仔细啊看了官网文档的截图,你会发现使用 slots 时是用 object 的方式来取值,所以我们对刚才的代码稍加修改。

export function initSlots(instance: any, children: any[]) {
  // instance.slots = Array.isArray(children) ? children : [children]

  const slots: Record<string, any> = {}

  for (const key in children) {
    const value = children[key]

    // slot
    slots[key] = Array.isArray(value) ? value : [value]
  }

  instance.slots = slots
}

然后就可以在任意位置摆放插槽了

export const Foo = {
  setup() {
    return {}
  },

  render() {
    const foo = h(
      'p',
      {},
      'foo')
    console.log(this.$slots);
    return h('div',
      {},
      [
        renderSlots(this.$slots, 'main'),
        foo,
        renderSlots(this.$slots, 'footer')
      ])
  }
}

renderSlots 只是一个工具,作用类似于 this.$slots.main

export function renderSlots(
  slots: Record<string, any>,
  name: string
) {
  const slot = slots[name]

  if (slot) {
    return createVNode('div', {}, slot)
  }
}

现在我们实现的就是具名插槽,slots 还有另一种作用域插槽,可以将子组件的数据传到父组件slot 作用域中。

在刚才的官网文档截图中,还有一个细节,每个 slot 是函数调用的形式来渲染的,所以,通过函数调用我们可以实现将子组件的数据放到 slot 作用域中。

我们继续对刚才的代码动刀。

修改刚才的 App 组件,同时在 Foo 组件中将 slots 替换为函数调用形式

export const App = {
  render() {
    return h('div',
      {},
      [
        h('div', {}, 'App'),
        h(Foo, {}, {
          main: ({ age }) => h('p', {}, 'age: ' + age),
          footer: () => h('div', {}, '456')
        })
      ]
    )
  },
  setup() {
    return {
    }
  }
}

初始化插槽时将参数传入

function normalizeObjectSlots(children: any[], slots: Record<string, any>) {
  for (const key in children) {
    const value = children[key]

    // slot
    slots[key] = (props: any) => normalizeSlotValue(value(props))
  }
}

这样就实现了作用域插槽

image-20220805223603976


前端小白