• vue3源码分析——实现slots


    引言

    <<往期回顾>>

    1. vue3源码分析——rollup打包monorepo
    2. vue3源码分析——实现组件的挂载流程
    3. vue3源码分析——实现props,emit,事件处理等

    本期来实现, slot——插槽,分为普通插槽,具名插槽,作用域插槽,所有的源码请查看

    正文

    在 模板中使用插槽的方式如下:

    <todo-button>
      Add todo
    </todo-button>
    复制代码
    
    • 1
    • 2
    • 3
    • 4

    template中的内容最终会被complierender函数,render函数里面会调用h函数转化成vnode,在vnode的使用方法如下:

    render() {
        return h(TodoButton, {}, this.$slots.default)
      },
    复制代码
    
    • 1
    • 2
    • 3
    • 4

    看完slots的基本用法,一起来实现个slots,方便自己理解slots的原理哦!😀😀😀

    实现基本的用法

    使用slots的地方是this.slots,并且调用的属性是default,那么slots,并且调用的属性是default,那么slots则是一个对象,对象里面有插槽的名称,如果使用者没有传递,则可以通过default来进行访问

    测试用例

    attention!!! 由于测试的是dom,需要先写入html等,在这里需要先创建对应的节点

     let appElement: Element;
      beforeEach(() => {
        appElement = document.createElement('div');
        appElement.id = 'app';
        document.body.appendChild(appElement);
      })
      afterEach(() => {
        document.body.innerHTML = '';
      })
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    本案例的测试正式开始

     test('test basic slots', () => {
     // 子组件Foo
        const Foo = {
          name: 'Foo',
          render() {
            return h('div', { class: 'foo' }, [h('p', {}, this.count), renderSlots(this.$slots)]);
          }
        }
    
        const app = createApp({
          render() {
            return h('div', { class: 'container' }, [
              h(Foo, { count: 1 }, { default: h('div', { class: 'slot' }, 'slot1') }),
              h(Foo, { count: 2 }, { default: [h('p', { class: 'slot' }, 'slot2'), h('p', { class: 'slot' }, 'slot2')] }),
            ])
          }
        })
    
        const appDoc = document.querySelector('#app')
        app.mount(appDoc);
        // 测试挂载的内容是否正确
        const container = document.querySelector('.container') as HTMLElement;
        expect(container.innerHTML).toBe('<div class="foo"><p>1</p><div><div class="slot">slot1</div></div></div><div class="foo"><p>2</p><div><p class="slot">slot2</p><p class="slot">slot2</p></div></div>'
        )
      })
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    需求分析

    通过上面的测试用例,可以分析以下内容:

    1. 父组件使用子组件传入插槽的方式是在h的第三个参数,并且传入的是一个对象,value的值可以是对象,或者是数组
    2. 子组件中使用插槽的时候,是在this.$slots中获取的
    3. 并且还实现了一个renderSlot的方法,renderSlot是将this.$slots调用h转变为vnode

    问题解决:

    1. 需要在绑定在this上面,那就在setupStatefulComponent函数代理中加入判断,传入的$slots ;
    2. 判断$slot是否在组件的代理中,然后代理需要把slots绑定在instance上面并且绑定值的时候需要把传入的对象统一转成数组;
    3. renderSlot方法调用了h函数,把一个数据转成vnode

    编码实现

    // 需要把$slots绑定在this上面,那就需要在代理里面在加入一个判断即可
    function setupStatefulComponent(instance: any) {
      // 代理组件的上下文
      instance.proxy = new Proxy({  }, {
          get(target,key){
           // 省略其他
           else if(key in instance.slots){
             return instance.slots[key]
           }
          }
      })
    }
    
    // 接下里在instance上面加上slots属性
    export function setupComponent(instance) {
      // 获取props和children
      const { props, children } = instance.vnode
    
      // 处理props
      
      const slots = {}
      for (const key in children) {
          slots[key] = Array.isArray(children[key]) ? children[key] : [children[key]]
      }
      instance.slots = slots
      
      // ……省略其他
      }
      
      // 最后还需要使用renderSlot函数
      export function renderSlots(slots) {
        const slot = slots['default']
           if (slot) {
            return createVNode('div', {}, slot)
          }
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    通过上面的编码,测试用例就可以完美通关啦

    具名插槽

    具名插槽就是,插槽除了可以有多个,并且除了default外,可以加入其他的名字,具体请看测试用例

    测试用例

     test('测试具名插槽', () => {
        const Foo = {
          name: 'Foo',
          render() {
            return h('div', { class: 'foo' },
              [
                renderSlots(this.$slots, 'header'),
                h('div', { class: 'default' }, 'default'),
                renderSlots(this.$slots, 'footer')
              ]
            );
          }
        }
    
        const app = createApp({
          name: 'App',
          render() {
            return h('div', { class: 'container' }, [h(Foo, {}, {
              header: h('h1', {}, 'header'),
              footer: h('p', {}, 'footer')
            })])
          }
        })
    
        const appDoc = document.querySelector('#app')
        app.mount(appDoc);
    
        const container = document.querySelector('.container') as HTMLElement
    
        expect(container.innerHTML).toBe('<div class=\"foo\"><div><h1>header</h1></div><div class=\"default\">default</div><div><p>footer</p></div></div>')
      })
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    分析

    通过上面测试用例,发现以下内容:

    1. renderSlot传入第二个参数,然后可以获取对于的slots

    问题解决

    直接在renderSlot里面传入第二个参数即可

    编码

      // 最后还需要使用renderSlot函数
      export function renderSlots(slots, name = 'default') {
        const slot = slots[name]
           if (slot) {
            return createVNode('div', {}, slot)
          }
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这一步是不是比较简单,相对起前面来说,正所谓,前面考虑好了,后面就舒服,接下来实现作用域插槽

    作用域插槽

    作用域插槽是,每个slot里面可以传入数据,数据只在当前的slot有效,具体请看测试用例

    测试用例

    test('测试作用域插槽', () => {
        const Foo = {
          name: 'Foo',
          render() {
            return h('div', { class: 'foo' },
              [
                renderSlots(this.$slots, 'header', { children: 'foo' }),
                h('div', { class: 'default' }, 'default'),
                renderSlots(this.$slots, 'footer')
              ]
            );
          }
        }
    
        const app = createApp({
          name: 'App',
          render() {
            return h('div', { class: 'container' }, [h(Foo, {}, {
              header: ({ children }) => h('h1', {}, 'header ' + children),
              footer: h('p', {}, 'footer')
            })])
          }
        })
    
        const appDoc = document.querySelector('#app')
        app.mount(appDoc);
    
        const container = document.querySelector('.container') as HTMLElement
    
        expect(container.innerHTML).toBe('<div class=\"foo\"><div><h1>header foo</h1></div><div class=\"default\">default</div><div><p>footer</p></div></div>')
    
      })
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    需求分析

    通过上面的测试用例,分析出以下内容:

    1. 传入插槽的时候,传入一个函数,函数可以拿到子组件传过来的参数
    2. renderSlots可以传入第三个参数props, 用于接收子组件往父组件传入的参数

    问题解决:

    1. 问题1: 只需要在传入插槽的时候进行一下判断,如果是函数的话,需要进行函数执行,并且传入参数
    2. 问题2: 也是对传入的内容进行判断,函数做传入参数处理

    编码

    // 在renderSlot里面传入第三个参数
    
    export function renderSlots(slots, name = 'default', props = {}) {
      const slot = slots[name];
    
      if (slot) {
        if (isFunction(slot)) {
          return createVNode('div', {}, slot(props))
        }
        return createVNode('div', {}, slot)
      }
    }
    
    // initSlot时候,需要进行函数判断
    
     const slots = {}
      // 遍历children
      for (const key in children) {
      // 判断传入的是否是函数,如果是函数的话,需要进行执行,并且传入参数
        if (isFunction(children[key])) {
          slots[key] = (props) => Array.isArray(children[key](props)) ? children[key](props) : [children[key](props)]
        } else {
          slots[key] = Array.isArray(children[key]) ? children[key] : [children[key]]
        }
      }
    
      instance.slots = slots
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    到此,整个测试用例就可以完美通过啦!😃😃😃

  • 相关阅读:
    机器学习 | Python实现GA-XGBoost遗传算法优化极限梯度提升树特征分类模型调参
    TMC2660步进电机驱动
    「MySQL」MySQL面试题全解析:常见问题与高级技巧详解
    基于JAVA的门禁管理系统【附源码】
    PyCharm+PyQT5之五Widgets QT程序
    开源数据库postgresql在统信系统上的离线安装
    mysql sql语句遍历树结构
    ubuntu20 extundelete 不能工作
    P3~P6函数模板
    物联网AI MicroPython学习之语法 GPIO输入输出模块
  • 原文地址:https://blog.csdn.net/qq_41499782/article/details/125372967