接着前面的内容: https://www.cnblogs.com/yanggb/p/12639440.html.
渲染函数 & JSX
基础
vue 推荐在绝大多数的情况下使用模板来创建 HTML. 然而在一些场景中, 你真的需要 JavaScript 的完全编程能力. 因此这时你就可以使用渲染函数, 它比模板更接近编译器.
这里先深入一个简单的例子, 这个例子里面的 render 函数十分实用, 假设我们要生成一些带锚点的标题:
- <h1>
- <a name="hello-world" href="#hello-world">
- Hello world!
- </a>
- </h1>
对于上面的 HTML, 我们决定这样定义组件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
当开始写一个只能通过 level 这个 prop 动态生成标题 (heading) 的组件的时候, 你可能很快想到要这样实现:
- <script type="text/x-template" id="anchored-heading-template">
- <h1 v-if="level === 1">
- <slot></slot>
- </h1>
- <h2 v-else-if="level === 2">
- <slot></slot>
- </h2>
- <h3 v-else-if="level === 3">
- <slot></slot>
- </h3>
- <h4 v-else-if="level === 4">
- <slot></slot>
- </h4>
- <h5 v-else-if="level === 5">
- <slot></slot>
- </h5>
- <h6 v-else-if="level === 6">
- <slot></slot>
- </h6>
- </script>
- Vue.component('anchored-heading', {
- template: '#anchored-heading-template',
- props: {
- level: {
- type: Number,
- required: true
- }
- }
- })
这里用模板并不是最好的选择: 不但代码冗长, 而且在每一个级别的标题中重复书写了 < slot></slot>, 在要插入锚点元素的时候还要再次重复.
虽然模板在大多数的组件中都非常好用, 但是显然在这里并不合适. 那么, 我们尝试着使用 render 函数来重写上面的例子:
- Vue.component('anchored-heading', {
- render: function (createElement) {
- return createElement(
- 'h' + this.level, // 标签名称
- this.$slots.default // 子节点数组
- )
- },
- props: {
- level: {
- type: Number,
- required: true
- }
- }
- })
这样看起来就简单得多了, 至少代码精简了很多. 但是使用 render 函数是需要非常熟悉 vue 的实例属性的, 比如在这个例子中, 你就需要知道, 向组件中传递不带[v-slot] 指令的子节点的时候, 比如 anchored-heading 中的 hello world, 这些子节点被存储在组件实例中的[$slot.default] 中.
节点, 树以及虚拟 dom
在深入渲染函数之前, 了解一些浏览器的工作原理是非常必要且重要的. 以下面这一段 HTML 为例:
- <div>
- <h1>My title</h1>
- Some text content
- <!-- TODO: Add tagline -->
- </div>
当浏览器读到这些代码的时候, 它就会建立一个 dom 节点树, 来保持追踪所有的内容, 就好像你会画一张家谱树来追踪家庭成员的发展一样.
这是上面的 HTML 对应的 dom 节点树. 每个元素都对应的一个节点, 每段文字也是对应着一个节点, 甚至注释也是对应着一个节点, 每一个节点就是页面的一个部分. 就像家谱树一样, 每个节点都可以有孩子节点(也就是说, 每个部分都可以包含其它的一些部分).
要去高效地更新所有的这些节点是十分困难的, 传统的页面开发一般是通过手动操作 dom 节点来更新页面. 而在 vue 中则不需要手动完成这项工作, 只需要告诉 vue 你希望页面上的 HTML 是什么, 这可以是在一个模板里:
<h1>{{ blogTitle }}</h1>
也可以是在渲染函数里:
- render: function (createElement) {
- return createElement('h1', this.blogTitle)
- }
在这两种情况下, 只要 blogTitle 发生了改变, vue 都会自动保持页面的更新.
虚拟 dom
vue 是通过建立一个虚拟 dom 来追踪自己要如何改变真实的 dom 的, 请仔细看这行代码:
return createElement('h1', this.blogTitle)
在这里, createElement 函数实际上返回的并不是一个真实的 dom 元素. 这个函数更准确的名字可能是 creatNodeDescription, 因为它所包含的信息会告诉 vue 页面上需要渲染什么样的节点, 包括其子节点的描述信息. 官方文档把这样的节点描述为[虚拟节点(virtual node)] , 也常简写它为 vnode. 虚拟 dom 是我们对由 vue 组件树建立起来的整个 vnode 树的称呼.
createElement 参数
接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能, 这里是 createElement 接受的参数:
- // @returns {VNode}
- createElement(
- // {String | Object | Function}
- // 一个 HTML 标签名, 组件选项对象, 或者
- // resolve 了上述任何一种的一个 async 函数. 必填项.
- 'div',
- // {Object}
- // 一个与模板中属性对应的数据对象. 可选.
- {
- // (详情见下一节)
- },
- // {String | Array}
- // 子级虚拟节点 (VNodes), 由 `createElement()` 构建而成,
- // 也可以使用字符串来生成 "文本虚拟节点". 可选.
- [
- '先写一些文字',
- createElement('h1', '一则头条'),
- createElement(MyComponent, {
- props: {
- someProp: 'foobar'
- }
- })
- ]
- )
深入数据对象
有一点需要特别注意: 正如[v-bind:class] 和[v-bind:style] 在模板语法中会被特别对待一样, 它们在 vnode 数据对象中也有对应的顶层字段. 该对象也允许你绑定普通的 HTML 属性, 也允许绑定像 innerHTML 这样的 dom 属性(会覆盖[v-HTML] 指令).
- {
- // 与 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
- }
完整示例
有了上面的这些知识, 我们现在就可以完成我们最开始想要实现的组件:
- var getChildrenTextContent = function (children) {
- return children.map(function (node) {
- return node.children
- ? getChildrenTextContent(node.children)
- : node.text
- }).join('')
- }
- Vue.component('anchored-heading', {
- render: function (createElement) {
- // 创建 kebab-case 风格的 ID
- var headingId = getChildrenTextContent(this.$slots.default)
- .toLowerCase()
- .replace(/\W+/g, '-')
- .replace(/(^-|-$)/g, '')
- return createElement(
- 'h' + this.level,
- [
- createElement('a', {
- attrs: {
- name: headingId,
- href: '#' + headingId
- }
- }, this.$slots.default)
- ]
- )
- },
- props: {
- level: {
- type: Number,
- required: true
- }
- }
- })
约束
组件树中的所有 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')
- })
- )
- }
使用 JavaScript 代替模板功能
只要在原生的 JavaScript 中可以轻松完成的操作, vue 的渲染函数就不会提供专有的替代方法. 比如, 在模板中使用的[v-if] 和[v-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/else 和 map 来重写:
- 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.')
- }
- }
渲染函数中没有与[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] 相比, 则是可以让你更好地控制交互细节, 其中的得失需要自己去衡量.
而对于[.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 分别修改为 altKey、shiftKey 或 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()
- // ...
- }
- }
插槽
在 render 函数中我们可以通过[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 数据对象中的[scopeSlots] 字段:
- render: function (createElement) {
- return createElement('div', [
- createElement('child', {
- // 在数据对象中传递 `scopedSlots`
- // 格式为 { name: props => VNode | Array<VNode> }
- scopedSlots: {
- default: function (props) {
- return createElement('span', props.text)
- }
- }
- })
- ])
- }
- 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>
- )
- }
- })
将 h 作为 createElement 的别名, 是 vue 生态系统中的一个通用惯例, 实际上也是 jsx 所要求的. 从 vue 的 Babel 插件的 3.4.0 版本开始, vue 会在 es2015 语法声明的含有 jsx 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement, 这样你就可以去掉 (h) 参数了. 而对于更早版本的插件, 如果 h 在当前作用域中不可用, 应用就会报错.
函数式组件
之前创建的锚点标题组件是比较简单的, 没有管理任何的状态, 也没有监听任何传递给它的状态, 也没有生命周期方法. 实际上, 它只是一个接受一些 prop 的函数. 在这样的场景下, 我们可以将组件标记为 functional, 这意味着它无状态(没有响应式数据), 也没有实例(没有 this 上下文). 一个函数式组件就像这样:
- Vue.component('my-component', {
- functional: true,
- // Props 是可选的
- props: {
- // ...
- },
- // 为了弥补缺少的实例
- // 提供第二个参数作为上下文
- render: function (createElement, context) {
- // ...
- }
- })
这里要注意, 在 2.3.0 之前的版本中, 如果一个函数式的组件想要接收 prop, 则 props 选项是必须的. 在 2.3.0 或以上的版本中, 你可以省略 props 选项, 所有组件上的 attribute 都会被自动隐式解析为 prop.
而当使用函数式组件的时候, 该引用将会是 htmlelement, 因为他们是无状态的也是无实例的.
在 2.5.0 以及以上的版本中, 如果你使用了单文件组件, 那么基于模板的函数式组件可以这样声明:
- <template functional>
- </template>
组件需要的一切都是通过 context 参数来传递, 它是一个包含以下字段的对象:
1.props: 提供所有 prop 的对象.
2.children:vnode 子节点的数组.
3.slots: 一个函数, 返回了包含所有插槽的对象.
4.scopedSlots:2.6.0 + 新增的一个暴露传入的作用域插槽的对象, 也以函数的形式暴露普通插槽.
5.data: 传递给组件的整个数据对象, 作为 createElement 的第二个参数传入组件.
6.parent: 对父组件的引用.
7.listeners:2.3.0 + 中新增的一个包含了所有父组件为当前组件注册的事件监听器的对象, 这是 data.on 的一个别名.
8.injections:2.3.0 + 中新增的一个包含了应当被注入的属性的对象.
在添加了[functional:true] 之后, 需要更新我们的锚点标题组件的渲染函数, 为其增加 context 参数, 并将[this.$slots.default] 更新为[context.children] , 然后将[this.level] 更新为[context.props.level] .
因为函数式组件只是函数, 所以渲染的开销也低了很多.
在作为包装组件的时候它们也同样非常有用, 比如, 当你需要做这些的时候:
1. 程序化地在多个组件中选择一个来代为渲染;
2. 在将 children,props 或 data 传递给子组件之前操作它们.
下面是一个 smart-list 组件的例子, 它能根据传入 prop 的值来代为渲染更具体的组件:
- var EmptyList = { /* ... */ }
- var TableList = { /* ... */ }
- var OrderedList = { /* ... */ }
- var UnorderedList = { /* ... */ }
- Vue.component('smart-list', {
- functional: true,
- props: {
- items: {
- type: Array,
- required: true
- },
- isOrdered: Boolean
- },
- render: function (createElement, context) {
- function appropriateListComponent () {
- var items = context.props.items
- if (items.length === 0) return EmptyList
- if (typeof items[0] === 'object') return TableList
- if (context.props.isOrdered) return OrderedList
- return UnorderedList
- }
- return createElement(
- appropriateListComponent(),
- context.data,
- context.children
- )
- }
- })
向子元素或子组件传递 attribute 和事件
在普通的组件中, 没有被定义为 prop 的属性会自动添加到组件的根元素上, 将已有的同名属性进行替换或与其进行智能合并.
然而函数式组件要求必须显式定义该行为:
- Vue.component('my-functional-button', {
- functional: true,
- render: function (createElement, context) {
- // 完全透传任何 attribute, 事件监听器, 子节点等.
- return createElement('button', context.data, context.children)
- }
- })
通过向 createElement 传入 context.data 作为第二个参数, 我们就把 my-functonal-button 上面所有的 attribute 和事件监听器都传递下去了. 事实上这是非常透明的, 以至于那些事件甚至并不要求[.native] 修饰符.
如果你使用基于模板的函数式组件, 那么你还需要手动添加 attribute 和监听器. 因为我们可以访问到其独立的上下文内容, 所以我们可以使用 [data.attrs] 传递任何 HTML 属性, 也可以使用[listeners] (即[data.on] 的别名) 传递任何事件监听器.
- <template functional>
- <button
- class="btn btn-primary"
- v-bind="data.attrs"
- v-on="listeners">
- <slot/>
- </button>
- </template>
[slots()] 和[children] 的对比
你可能会想知道为什么同时需要 [slots()] 和[children] , 你会觉得[slots().default] 不是和[children] 类似的吗? 事实上, 在一些场景中(比如带有子节点的函数式组件) 是同时需要这两个的了.
- <my-functional-component>
- <p v-slot:foo>
- first
- </p>
- <p>second</p>
- </my-functional-component>
对于这个组件,[children] 会给你两个段落标签, 而[slots().default] 只会传递第二个匿名段落标签,[slots().foo] 则是传递第一个具名段落标签. 同时拥有[children] 和[slots()] 可以让你选择是让组件去感知某个插槽机制, 还是简单地通过传递 children 去移交给其他组件去处理.
模板编译
vue 的模板实际上是被编译成了渲染函数, 这是一个 vue 的实现细节(底层). 虽然使用 vue 开发业务的开发者通常并不需要关心这些内容, 但是如果你想看看模板的功能具体是怎样被编译的, 你可能会发现这是非常有意思的, 而且对于你自身的成长也有很大的帮助.
"我还是很喜欢你, 像星辰闪耀苍穹顶, 不甘孤寂."
来源: https://www.cnblogs.com/yanggb/p/12663909.html