VueStudyNote icon indicating copy to clipboard operation
VueStudyNote copied to clipboard

24 实现插槽

Open xwjie opened this issue 6 years ago • 0 comments

实现思路

插槽看上去很高级很复杂,其实实现起来并不复杂!

插槽的真正内容是在父组件上的,所以创建子组件之前,子组件里面的数据对应的vnode已经存在了。这里使用,我们把它归类到实例的 $slots 对象上,它是一个数组的对象。

然后再子组件渲染的时候,再把他的数据(就是vnode)拿出来,当到子组件的child里面,snabbdom自动就会渲染出来了!

归类子组件到$slots

再创建子组件的时候,增加代码。把 父组件 里面的vnode根据插槽归类。

/**
 * 实现组件功能
 *
 * 采用snabbdom的hook,在insert和update的时候更新数据。
 *
 * @param {*} vnode
 * @param {*} vm
 */
function setComponentHook(vnode: any, vm: Xiao) {
  if (!vnode.sel) {
    return
  }

  // 查看是否组成了组件?
  const Comp = Xiao.component(vnode.sel)

  if (Comp) {
    vnode.data.hook = {
      insert: (vnode) => {
        log('component vnode', vnode)

        // 创建子组件实例
        let app = new Comp()
        app.$parent = vm

        const propsData = vnode.data.props

        // 把计算后的props数据代理到当前vue里面
        initProps(app, propsData)

        // 处理插槽,把插槽归类
        resolveSlots(app, vnode.children)

        // 绑定事件
        if(vnode.data.on){
          initEvent(app, vnode.data.on)
        }

        // 保存到vnode中,更新的时候需要取出来用
        vnode.childContext = app

        // 渲染
        app.$mount(vnode.elm)
      },
      update: (oldvnode, vnode) => {
        const app = oldvnode.childContext

        // 更新update属性
        updateProps(app, vnode.data.props)

        vnode.childContext = app
      }

    }
  }

  // 递归
  if (vnode.children) {
    vnode.children.forEach(function (e) {
      setComponentHook(e, vm)
    }, this)
  }

}

/**
 * 归类插槽
 * 
 * @param {*} vm 
 * @param {*} children 
 */
function resolveSlots(vm: Xiao, children: Array<any>){
  log('resolveSlots', children)
  vm.$slots = {}

  children.forEach(vnode =>{
    let slotname = 'default'

    if(vnode.data.props && vnode.data.props.slot){
      slotname = vnode.data.props.slot
      delete vnode.data.props.slot
    }

    (vm.$slots[slotname] || (vm.$slots[slotname] = [])).push(vnode)
  })

  log('resolveSlots end', vm.$slots)
}

修改渲染函数

/**
 * 根据元素AST生成渲染函数。
 * 
 * 如果是插槽,生成 _t(插槽名字, [默认插槽内容])
 * 否则生成 h(tag, 属性。。。)
 * 
 * @param {*} node 
 */
function createRenderStrElemnet(node: any): string {
  log('createRenderStrElemnet', node)

  let str: string

  // 插槽使用 _t 函数, 参数为插槽名字
  if (node.tag == 'slot') {
    log('slot node', node)
    const slot = node.attrsMap.name || "default"
    str = `_t("${slot}",[`

    if (node.children && node.children.length > 0) {
      // 生成插槽默认的子组件的渲染函数
      for (let i = 0; i < node.children.length; i++) {
        str += createRenderStr(node.children[i])

        if (i != node.children.length - 1) {
          str += ','
        }
      }
    }

    str += '])'
    return str
  }


  // snabbdom 的语法,类名放在tag上。'div#container.two.classes'
  let tagWithIdClass = getTagAndClassName(node)
  str = `h(${tagWithIdClass},{`

  // 解析指令
  str += getDirectiveStr(node)

  // 解析属性
  str += genAttrStr(node)

  str += "}"

  if (node.children) {
    str += ',['

    // 保存上一次if指令,处理只有if没有else的场景
    let lastDir

    node.children.forEach(child => {
      // 如果这里节点有if指令
      let dir = getIfElseDirective(child)

      console.log('dir:', dir)

      if (dir) {
        if (dir.name == 'if') {
          str += `(${dir.exp})?`
          lastDir = dir
        } else if (dir.name == 'else') {
          str += `:`
        }
      }

      str += createRenderStr(child)

      if (dir) {
        if (dir.name == 'else') {
          str += `,`
          lastDir = null
        }
      }
      else if (lastDir) {
        str += `:"",`
        lastDir = null
      }
      else {
        str += `,`
      }
    })

    if (lastDir) {
      str += `:"",`
    }

    str += ']'
  }

  str += ')'

  return str
}

实现插槽渲染函数


class Xiao{
  /**
   * 插槽渲染函数
   * 
   * vue里面是 _t = renderSlot
   * @param {*} slot
   */
  _t(slot: string, child: ?any){
    // 如果父节点没有制定插槽内容,那么返回默认值(是个数组)
    return this.$slots[slot] || child
  }

}

更新组件的时候,把父组件的数据填充到子元素上

渲染函数执行之后,插槽渲染函数 _t 执行返回vnode节点数组。把他打散到原来的数组里面即可。

/**
 * 渲染组件
 * 
 * @param {*} vm 
 */
function updateComponent(vm: Xiao) {
  let proxy = vm

  // 虚拟dom里面的创建函数
  proxy.h = h

  // 新的虚拟节点
  // 指令的信息已经自动附带再vnode里面
  let vnode = vm.$render.call(proxy, h)

  log('before expandSlotArray: ', vnode)

  // 插槽后child里面应该为节点的可能变成了数组,所以要单独处理一下
  expandSlotArray(vnode)

  log('after expandSlotArray: ', vnode)

  // 把实例绑定到vnode中,处理指令需要用到
  setContext(vnode, vm)

  // 处理子组件
  setComponentHook(vnode, vm)

  // 上一次渲染的虚拟dom
  let preNode = vm.$options.oldvnode;

  log(`[lifecycle][uid:${vm._uid}] 第${++vm._renderCount}次渲染`)

  if (preNode) {
    vnode = patch(preNode, vnode)
  }
  else {
    vnode = patch(vm.$el, vnode)
  }

  log('vnode', vnode)

  // 保存起来,下次patch需要用到
  vm.$options.oldvnode = vnode;
}

测试代码

<h1>slot测试</h1>
<div id="demo">
  <h1>我是父组件的标题</h1>
  <my-component>
    <p>这是一些初始内容{{message}}</p>
    <p>这是更多的初始内容</p>
    <h3 slot="header">这是父组件放到子组件头插槽的数据</h3>
    <h2 slot="foot2">放到foot2插槽{{message}}</h2>
  </my-component>
</div>
<script>

Xiao.component('my-component', {
	data: function(){
		return {
			aa: 'child message'
		}
	},
	template: `
	<div>
	  <slot name="header"></slot>
	  <h2>我是子组件的标题{{aa}}</h2>
	  <slot>
	    只有在没有要分发的内容时才会显示。
	  </slot>
	  <slot name="foot"><h2>默认的foot slot:{{aa}}</h2></slot>
  	  <slot name="foot2">默认的foot2 slot:{{aa}}</slot>

	</div>	
	`
})

var app = new Xiao({
  el: '#demo',
  data:{
    message : 'parent message'
  }
})

</script>

xwjie avatar Jan 26 '18 15:01 xwjie