render & createElement & JSX

767 阅读1分钟

1.render

字符串模板( template 写法)的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode
如果组件是一个函数组件,渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息。 采用 render 函数创建一个标题标签

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

1.1 createElement 参数

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

d2admin 中侧边栏的渲染

render (createElement) {
    // 创建一个 1.1 div 标签, 后面是 div 的一些特性
    return createElement('div', {
    // 1.2 样式特性
      attrs: { class: 'd2-layout-header-aside-menu-side' }
    }, [
        // 子标签 1.3.1 el-menu
      createElement('el-menu', {
        key: this.keyId,
        props: {
          // 子标签的折叠属性
          collapse: this.asideCollapse,
          'default-openeds': this.currentOpens,
          uniqueOpened: false,
          defaultActive: this.indexPath1 || this.$route.fullPath || this.active
        },
        ref: 'menu',
        on: { select: this.handleMenuSelect }
        // 1.3.3.1 子子标签(二级标签),通过子标签的 children 创建,递归调用 createElement 方法
      }, this.aside.map(menu => (menu.children === undefined ? elMenuItem : elSubmenu).call(this, createElement, menu))),
      ...this.aside.length === 0 && !this.asideCollapse ? [
        createElement('div', {
          attrs: {
            class: 'd2-layout-header-aside-menu-empty',
            flex: 'dir:top main:center cross:center'
          }
        }, [
          createElement('d2-icon', { props: { name: 'inbox'} }),
          createElement('span', {}, '没有侧栏菜单')
        ])
      ] : []
    ])
  },

1.2 Vue 中的 JSX 语法

createElement 有的时候写起来比较复杂

createElement(
  'anchored-heading', {
    props: {
      level: 1
    }
  }, [
    createElement('span', 'Hello'),
    ' world!'
  ]
)

表示的模板为

<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>

JSX 语法让写法又回到模板语法,AnchoredHeading 还是原先用 createElement 创建的组件

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render: function (h) {
    return (
    // 因为 level={1} 是传入子组件中的数据,所以用 {} 区分
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement,这样你就可以去掉 (h) 参数了。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。

所以在新版 d2admin 里用了 JSX 的写法, {} 用来声明是引用值而不是字面量

render(h) {
    // 返回 DOM 元素
    return (
      <div class="d2-layout-header-aside-menu-side">
        <el-menu
          // 折叠
          collapse={this.asideCollapse}
          collapseTransition={this.asideTransition}
          uniqueOpened={true}
          defaultActive={this.$route.fullPath}
          ref="menu"
          onSelect={this.handleMenuSelect}
        >
          {/* 根据aside数组创建菜单项目,递归调用生成菜单 */}
          {this.aside.map((menu) => createMenu.call(this, h, menu))}
        </el-menu>
        {this.aside.length !== 0 && true ? (
          // 如果侧边数组的长度为0,显示没有侧边栏
          <div class="d2-layout-header-aside-menu-empty" flex="dir:top main:center cross:center">
            <d2-icon name="inbox"></d2-icon>
            <span>没有侧栏菜单</span>
          </div>
        ) : null}
      </div>
    )
  },