vue3 source code analysis – implement slots

introduction

<< review >> past periods

  1. vue3 source code analysis – rollup package monsterpo
  2. vue3 Source Code Analysis – Implement the mounting process of components
  3. vue3 source code analysis – implements props, emit, event handling, etc

In this issue, slot – slot, divided into normal slots, named slots, scoped slots, all source code please see

body

Use slots in templates as follows:

<todo-button>
  Add todo
</todo-button>

The content in the template will eventually be converted into a render function, and the render function will call the h function to be converted to vnode, which is used in vnode as follows:

render() {
    return h(TodoButton, {}, this.$slots.default)
  },

After reading the basic usage of slots, let’s implement a slots together, so that you can understand the principle of slots!

Implement basic usage

Where slots are used is this.slots, and the property called is default, then slots, and the property called is default, then slots is an object, the object has the name of the slot, if the user does not pass, it can be accessed through default.

Test cases

attention!!! Since the test is dom, you need to write html first, etc., and here you need to create the corresponding node first

 let appElement: Element;
  beforeEach(() => {
    appElement = document.createElement('div');
    appElement.id = 'app';
    document.body.appendChild(appElement);
  })
  afterEach(() => {
    document.body.innerHTML = '';
  })

Testing of this case has officially begun

 test('test basic slots', () => {
    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>'
    )
  })

Requirements analysis

With the above test case, you can analyze the following:

  1. The way the parent component uses the child component to pass in the slot is in the third parameter of h, and what is passed in is an object, and the value value can be an object, or an array
  2. When slots are used in subcomponents, they are obtained in this.$slots
  3. And also implements a renderSlot method, renderSlot is to turn this.$slots call h into vnode

Problem Solving:

  1. If you need to bind to this, then add judgment to the setupStatefulComponent function proxy, pass in $slots;
  2. Determine whether $slot is in the component’s proxy, and then the proxy needs to bind the slots to the integration and bind the value to convert the incoming objects into arrays;
  3. The renderSlot method calls the h function to convert a piece of data to vnode

Encoding implementation


function setupStatefulComponent(instance: any) {
  instance.proxy = new Proxy({  }, {
      get(target,key){
       else if(key in instance.slots){
         return instance.slots[key]
       }
      }
  })
}
export function setupComponent(instance) {
  const { props, children } = instance.vnode
  const slots = {}
  for (const key in children) {
      slots[key] = Array.isArray(children[key]) ? children[key] : [children[key]]
  }
  instance.slots = slots
  }
  export function renderSlots(slots) {
    const slot = slots['default']
       if (slot) {
        return createVNode('div', {}, slot)
      }
}

With the above coding, the test case can be perfectly passed

Named slot

Named slots are, in addition to multiple slots, and in addition to default, you can add other names, see test cases

Test cases

 test('slots 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>')
  })

analyse

Through the above test case, the following were found:

  1. renderSlot passes in the second argument, and then you can get the slots for that

Problem solving

Just pass in the second parameter directly in the renderSlot

encode


  export function renderSlots(slots, name = 'default') {
    const slot = slots[name]
       if (slot) {
        return createVNode('div', {}, slot)
      }
}

This step is not relatively simple, relative to the front, just so-called, the front is considered, the back is comfortable, and then the scope slot is implemented

Scoped slots

The scope slot is that each slot can pass in data, and the data is only valid in the current slot, see the test case

Test cases

test('slots 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>')

  })

Requirements analysis

Through the above test case, the following are analyzed:

  1. When passing in a slot, a function is passed in, and the function can take the parameters passed by the subcomponent
  2. renderSlots can pass in a third parameter, props, to receive the parameters passed by the child component to the parent component

Problem Solving:

  1. Problem 1: You only need to make a judgment when you pass in the slot, if it is a function, you need to execute the function and pass in the parameters
  2. Problem 2: It is also the judgment of the incoming content, and the function does the incoming parameter processing

encode



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)
  }
}


 const slots = {}
  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

At this point, the entire test case can pass perfectly!

Be the first to comment

Leave a Reply

Your email address will not be published.


*