接着前面的内容: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开发业务的开发者通常并不需要关心这些内容,但是如果你想看看模板的功能具体是怎样被编译的,你可能会发现这是非常有意思的,而且对于你自身的成长也有很大的帮助。

"我还是很喜欢你,像星辰闪耀苍穹顶,不甘孤寂。"

vue2.x学习笔记(二十三)的更多相关文章

  1. python3.4学习笔记(二十三) Python调用淘宝IP库获取IP归属地返回省市运营商实例代码

    python3.4学习笔记(二十三) Python调用淘宝IP库获取IP归属地返回省市运营商实例代码 淘宝IP地址库 http://ip.taobao.com/目前提供的服务包括:1. 根据用户提供的 ...

  2. (C/C++学习笔记) 二十三. 运行时类型识别

    二十三. 运行时类型识别 ● 定义 运行时类型识别(Run-time Type Identification, RTTI) 通过RTTI, 程序能够使用基类的指针或引用来检查(check)这些指针或引 ...

  3. Java基础学习笔记二十三 Java核心语法之反射

    类加载器 类的加载 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,链接,初始化三步来实现对这个类进行初始化. 加载就是指将class文件读入内存,并为之创建一个Class对象.任 ...

  4. angular学习笔记(二十三)-$http(1)-api

    之前说到的$http.get和$http.post,都是基于$http的快捷方式.下面来说说完整的$http: $http(config) $http接受一个json格式的参数config: conf ...

  5. PHP学习笔记二十三【This】

    <?php Class Person { function test1() { $this->test2();//类里面的方法互相调用要加$this } protected functio ...

  6. Java学习笔记二十三:Java的继承初始化顺序

    Java的继承初始化顺序 当使用继承这个特性时,程序是如何执行的: 继承的初始化顺序 1.初始化父类再初始子类 2.先执行初始化对象中属性,再执行构造方法中的初始化 当使用继承这个特性时,程序是如何执 ...

  7. 学习笔记二十三——字符函数库cctype【转】

    本文转载自: 字符函数库cctype 在头文件cctype(ctype.h)中定义了一些函数原型,可以简化输入确定字符是否为大写字母.数字.标点符号等工作. 例如: 如果ch是一个字母,则isalph ...

  8. vue2.x学习笔记(十三)

    接着前面的内容:https://www.cnblogs.com/yanggb/p/12595860.html. 组件的注册 注册组件有一些规范约定与注意事项. 组件名的命名规范 在注册一个组件的时候, ...

  9. vue2.0学习笔记之路由(二)路由嵌套+动画

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. vue2.0学习笔记之路由(二)路由嵌套

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

随机推荐

  1. ASP.NET Core 奇淫技巧之伪属性注入

    一.前言 开局先唠嗑一下,许久未曾更新博客,一直在调整自己的状态,去年是我的本命年,或许是应验了本命年的多灾多难,过得十分不顺,不论是生活上还是工作上.还好当我度过了所谓的本命年后,许多事情都在慢慢变 ...

  2. HDU-1051 一个DP问题

    Problem Description There is a pile of n wooden sticks. The length and weight of each stick are know ...

  3. JSP+Servlet+C3P0+Mysql实现的简单新闻系统

    项目简介 项目来源于:https://gitee.com/glotion/servlet-jsp_news 本系统基于JSP+Servlet+C3P0+Mysql.涉及技术少,易于理解,适合JavaW ...

  4. spring-cloud-gateway静态路由

    为什么引入 API 网关 使用 API 网关后的优点如下: 易于监控.可以在网关收集监控数据并将其推送到外部系统进行分析. 易于认证.可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个 ...

  5. 深入理解Java AIO(一)—— Java AIO的简单使用

    深入理解Java AIO(一)—— Java AIO的简单使用 深入理解AIO系列分为三个部分 第一部分也就是本节的Java AIO的简单使用 第二部分是AIO源码解析(只解析关键部分)(待更新) 第 ...

  6. go server框架学习之路 - 写一个自己的go框架

    go server框架学习之路 - 写一个自己的go框架 用简单的代码实现一个go框架 代码地址: https://github.com/cw731/gcw 1 创建一个简单的框架 代码 packag ...

  7. JS 剑指Offer(五) 二叉树的重建

    题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字. 题目分析:已知二叉树的前序和中序遍历,根据前序遍历和中序遍历的规则,前序遍历的第一 ...

  8. CodeForces 687A NP-Hard Problem

    Portal:http://codeforces.com/problemset/problem/687/A 二分图染色 好模板题 有SPJ 值得注意的是,因为C++的奇妙的运算机制 若在vector变 ...

  9. 消息队列 NSQ 源码学习笔记 (一)

    nsqlookupd 用于Topic, Channel, Node 三类信息的一致性分发 概要 nsqlookup 知识点总结 功能定位 为node 节点和客户端节点提供一致的topic, chann ...

  10. js中常见的数据加密与解密的方法

    加密在我们前端的开发中也是经常遇见的.本文只把我们常用的加密方法进行总结.不去纠结加密的具体实现方式(密码学,太庞大了). 常见的加密方式 常见的加密算法基本分为这几类, 线性散列算法(签名算法)MD ...