Vue 中的 Render 全面详解 (渲染函数 & JSX)

相信你们都或多或少的在 code 中见过 或使用过 Render,若是你对它仍是一脸懵逼,那就快上车!今天就带你来盘它。vue


@[toc]node


1、Render 的资料简介

Render 函数是 Vue2.x 新增的一个函数、主要用来提高节点的性能,它是基于 JavaScript 计算。使用 Render 函数将 Template 里面的节点解析成虚拟的 Dom 。express

Vue 推荐在绝大多数状况下使用模板来建立你的 HTML。然而在一些场景中,你真的须要 JavaScript 的彻底编程的能力。这时你能够用渲染函数,它比模板更接近编译器。编程

简单的说,在 Vue 中咱们使用模板 HTML 语法组建页面的,使用 Render 函数咱们能够用 Js 语言来构建 DOM。数组

由于 Vue 是虚拟 DOM,因此在拿到 Template 模板时也要转译成 VNode 的函数,而用 Render 函数构建 DOM,Vue 就免去了转译的过程。浏览器

2、与 Render 的初次相遇

你第一次邂逅它的时候,它多是这样的:bash

  • IView
render:(h, params)=>{
    return h('div', {style:{width:'100px',height:'100px',background:'#ccc'}}, '地方')
}
复制代码
  • Element
<el-table-column :render-header="setHeader">
</el-table-column>
setHeader (h) {
 return h('span', [
    h('span', { style: 'line-height: 40px;' }, '备注'),
      h('el-button', {
        props: { type: 'primary', size: 'medium', disabled: this.isDisable || !this.tableData.length },
        on: { click: this.save }
      }, '保存当前页')
    ])
  ])
},
复制代码

或者这样的:markdown

renderContent (createElement, { node, data, store }) {
	return createElement('span', [
		// 显示树的节点信息
		createElement('span', node.label)
		// ......
	])
}
复制代码

那它的真身究竟是什么样的呢?这还要从它的身世提及。app

2.一、节点、树

在深刻渲染函数以前,了解一些浏览器的工做原理是很重要的。如下面这段 HTML 为例:dom

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>
复制代码

当浏览器读到这些代码时,它会创建一个DOM 节点树来保持追踪全部内容,如同你会画一张家谱树来追踪家庭成员的发展同样。

上述 HTML 对应的 DOM 节点树以下图所示:

在这里插入图片描述
每一个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树同样,每一个节点均可以有孩子节点 (也就是说每一个部分能够包含其它的一些部分)。

高效地更新全部这些节点会是比较困难的,不过所幸你没必要手动完成这个工做。你只须要告诉 Vue 你但愿页面上的 HTML 是什么,这能够是在一个模板里:

<h1>{{ blogTitle }}</h1>
复制代码

或者一个渲染函数里:

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}
复制代码

在这两种状况下,Vue 都会自动保持页面的更新,即使 blogTitle 发生了改变。

2.二、虚拟 DOM

Vue 经过创建一个虚拟 DOM 来追踪本身要如何改变真实 DOM。请仔细看这行代码:

return createElement('h1', this.blogTitle)
复制代码

createElement到底会返回什么呢?其实不是一个_实际的_ DOM 元素。它更准确的名字多是 createNodeDescription,由于它所包含的信息会告诉 Vue 页面上须要渲染什么样的节点,包括及其子节点的描述信息。咱们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是咱们对由 Vue 组件树创建起来的整个 VNode 树的称呼。

注:==当使用render函数描述虚拟 DOM 时,vue 提供一个函数,这个函数是就构建虚拟 DOM 所须要的工具。官网上给他起了个名字叫 createElement。还有约定的简写叫 h,将 h 做为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。==

有点意思~ 其实它就是 createElement,接下来让咱们来走近一点点,来深刻的了解它吧~

3、与 Render 的约会

3.1 createElement 参数

createElement(TagName,Option,Content)接受三个参数 createElement(" 定义的元素 ",{ 元素的性质 }," 元素的内容"/[元素的内容])

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

  // {Object}
  // 一个与模板中属性对应的数据对象。可选。
  {
    // (详情见下一节-3.2 深刻数据对象)
  },

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

3.2 深刻数据对象

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 属性内,
  // 但再也不支持如 `v-on:keyup.enter` 这样的修饰器。
  // 须要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你没法对 `binding` 中的 `oldValue`
  // 赋值,由于 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 做用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 若是组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层属性
  key: 'myKey',
  ref: 'myRef',
  // 若是你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}
复制代码

3.3 举个小栗子

render:(h) => {
  return h('div',{
&emsp;&emsp;&emsp;//给div绑定value属性
     props: {
         value:''
     },
&emsp;&emsp;&emsp;//给div绑定样式
&emsp;&emsp;&emsp;style:{
&emsp;&emsp;&emsp;&emsp;&emsp;width:'30px'
&emsp;&emsp;&emsp;},&emsp;
&emsp;&emsp;&emsp;//给div绑定点击事件&emsp;&emsp;
     on: {
         click: () => {
            console.log('点击事件')
         }
     },
  })
}
复制代码

3.4 约束

它也是有小脾气的~ 要记得这个约束哟~

  • VNode 必须惟一

组件树中的全部 VNode 必须是惟一的。这意味着,下面的渲染函数是不合法的:

render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // 错误 - 重复的 VNode
    myParagraphVNode, myParagraphVNode
  ])
}
复制代码

若是你真的须要重复不少次的元素/组件,你可使用工厂函数来实现。例如,下面这渲染函数用彻底合法的方式渲染了 20 个相同的段落:

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}
复制代码

上面的都是基础,你们要记牢哟,下面介绍一些它的特性


4、Render 的小个性

4.1 v-if 和 v-for

只要在原生的 JavaScript 中能够轻松完成的操做,Vue 的渲染函数就不会提供专有的替代方法。好比,在模板中使用的 v-ifv-for

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
复制代码

这些均可以在渲染函数中用 JavaScript 的 if/elsemap 来重写:

props: ['items'],
render: function (createElement) {
  if (this.items.length) {
    return createElement('ul', this.items.map(function (item) {
      return createElement('li', item.name)
    }))
  } else {
    return createElement('p', 'No items found.')
  }
}
复制代码

4.2 v-model

渲染函数中没有与 v-model 的直接对应——你必须本身实现相应的逻辑:

props: ['value'],
render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}
复制代码

这就是深刻底层的代价,但与 v-model 相比,这可让你更好地控制交互细节。

4.3 事件 & 按键修饰符

对于 .passive.capture.once 这些事件修饰符, Vue 提供了相应的前缀能够用于 on

修饰符 前缀
.passive &
.capture !
.once ~
.capture.once.once.capture ~!

例如:

on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}
复制代码

对于全部其它的修饰符,私有前缀都不是必须的,由于你能够在事件处理函数中使用事件方法:

修饰符 处理函数中的等价操做
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键:.enter, .13 if (event.keyCode !== 13) return (对于别的按键修饰符来讲,可将 13 改成另外一个按键码)
修饰键:.ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (将 ctrlKey 分别修改成 altKeyshiftKey 或者 metaKey)

这里是一个使用全部修饰符的例子:

on: {
  keyup: function (event) {
    // 若是触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
    // 若是按下去的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
    // ...
  }
}
复制代码

4.4 插槽

你能够经过 this.$slots 访问静态插槽的内容,每一个插槽都是一个 VNode 数组:

render: function (createElement) {
  // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default)
}
复制代码

也能够经过 this.$scopedSlots 访问做用域插槽,每一个做用域插槽都是一个返回若干 VNode 的函数:

props: ['message'],
render: function (createElement) {
  // `<div><slot :text="message"></slot></div>`
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}
复制代码

若是要用渲染函数向子组件中传递做用域插槽,能够利用 VNode 数据对象中的 scopedSlots 字段:

render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}
复制代码

5、实战

Element 中的 Tree

// 树节点的内容区的渲染回调
renderContent(h, { node, data, store }) {
    let aa = () => {
        console.log(data)
    }
    return  h('span', [
        h('span', {
            class: "custom-tree-node"
        }, [
            h('i', { class: "icon-folder" }), h('span', { props: { title: node.label }, class: "text ellipsis" }, node.label),
            h('el-popover', {
                props: {
                    placement: "bottom",
                    title: "",
                    width: "61",
                    popperClass: "option-group-popover",
                    trigger: "hover"
                }
            }, [
                h('ul', { class: "option-group" }, [
                    h('li', {
                        class: "pointer-text",
                        on: {
                            click: aa
                        }
                    }, '编辑'),
                    h('li', { class: "pointer-text" }, '删除'),
                    h('li', { class: "pointer-text" }, '添加')
                ]),
                h('i', { slot: "reference", class: "el-icon-more fr more-icon",
                    on: {
                        click: (e) => {
                            e.stopPropagation();
                        }
                    }
                })
            ])
        ])
    ])
},
复制代码

6、扩展-JSX

若是你写了不少 render 函数,可能会以为下面这样的代码写起来很痛苦:

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

特别是对应的模板如此简单的状况下:

<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>
复制代码

这就是为何会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可让咱们回到更接近于模板的语法上。

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})
复制代码

码字不易,以为有帮助的小伙伴点个赞支持下~


在这里插入图片描述

扫描上方二维码关注个人订阅号~

相关文章
相关标签/搜索