Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

24 实现插槽 #26

Open
xwjie opened this issue Jan 26, 2018 · 0 comments
Open

24 实现插槽 #26

xwjie opened this issue Jan 26, 2018 · 0 comments

Comments

@xwjie
Copy link
Owner

xwjie commented Jan 26, 2018

实现思路

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

插槽的真正内容是在父组件上的,所以创建子组件之前,子组件里面的数据对应的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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant