Vue | 22 可复用性&组合-渲染函数 & JSX

内容提要:

  1. 渲染函数基本用法
  2. 节点、树和虚拟DOM
  3. createElement参数详解
  4. 使用普通的JavaScript代替模板特性
  5. JSX插件介绍
  6. 函数式组件的用法:传递属性和事件给子元素或子组件,slots() vs children
  7. 模板编译的demo演示

基础

在大部分情况下我们推荐使用template去构建HTML页面。然而在某些情况下,你需要JavaScript的全部编程能力。这就是你需要使用render function,一个更接近编译器的模板替代方案。

让我们深入研究一个简单的例子,关于一个render函数的实践。假设你想要生成锚定标题:

<h1>
    <a name="hello-world" href="#hello-world">
        Hello world!
    </a>
</h1>

对于以上HTML,你决定这样定义组件接口:

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

当你开始使用一个组件的时候,它仅仅基于levelprop生成一个头,你可以很快使用以下这个方式达成:

<script type="text/x-template" id="anchored-heading-template">
	<h1 v-if="level === 1">
		<slot></slot>
	</h1>
	<h2 v-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">
	
    </h6>
	
</script>
Vue.component('anchored-heading',{
    template: '#anchored-heading-template',
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})

模板感觉不太好。它不仅啰嗦,而且我们重复使用<slot><slot>为每一个heading level,并且当我们追加相同的anchor 元素的时候做相同的事情。

虽然模板对大多数组件有用,但很明显这不是其中之一。所以试着让我们以一个render函数重写它。

Vue.component('anchored-heading', {
    render: function (createElement) {
        return createElement(
        'h' + this.level, // tag name
        this.$slots.default // array of children
        )
    },
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})

更简单的!稍稍。这个代码更短,但是这要求更熟悉Vue属性。在这个例子中,你必须知道在组件中传递没写携带slot属性的子元素时,比如anchored-heading中的 Hello world!,这些子组件被存储在组件实例中的$slots.default中。如果你还没有了解,建议你在继续深入了解渲染函数之前读instance properties API

节点,树,和虚拟DOM

在我们深入渲染函数之前,理解一点浏览器如何工作是重要的。以这个HTML为例:

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

当一个浏览器渲染这段代码的时候,它创建了一个tree of “DOM nodes” 去帮助浏览器追踪一切,就好像你创建了一个家谱来记录你的大家庭一样。

DOM的节点数看起来像这样:
dom tree

每一个元素是一个节点。每一个文本是一个节点。每一个评论也是一个节点!一个节点是一页的一部分。类似于家谱,每一个节点可以有子节点(例如:每一个部分可以包含其他部分)。

高效的更新所有这些节点是困难的,但是还好,你不用手动这么做。相反,你告诉Vue页面需要什么HTML,在一个模板中:

<h1>{{ blogTitle }}</h1>

或一个渲染函数:

render: function (createElement) {
    retrun createElement('h1', this.blogTitle)
}

在这些例子中,Vue自动更新页面,甚至当blogTitle改变的时候。

虚拟DOM

Vue通过创建虚拟DOM来实现对真实DOM改变的追踪。仔细看看这行:

return createElement('h1', this.blogTitle)

createElement实际返回什么?确实没有返回成真实的DOM元素。可能称为createNodeDescription更准确,因为它包含Vue应该在页面上呈现何种节点的信息,包括对所有子节点的描述,我们称这个节点描述为一个“虚拟节点”,简称VNode。“Virtual DOM” 就是我们所说的VNodes树,通过Vue组件树创建。

createElement 参数

下一件事是你必须熟悉如何在createElement函数中使用模板特性。下面是createElement接受的参数:

// @returns {VNode}
createElement(
	// {Stirng | Object | Function}
    // An HTML tag name, component options, or async 
    // funciton resolving to one of these. Required.
    'div',
    
    // {Object}
    // A data object corresponding to the attributes
    // you would use in a template. Optional.
    {
        // (see details in the next seciton below)
    },
    
    // {String | Array}
    // Children VNodes, built using `createElement()`,
    // or using strings to get 'text VNodes'. Optional.
    [
        'Some text comes first.',
        createElement('h1', 'A headline'),
        createElement(MyComponent, {
            props: {
                someProp: 'foobar'
            }
        })
    ]
)

深入数据对象

一件事情需要注意:类似于v-bind:classv-bind:style在模板中被特殊对待,他们在自己的VNode数据对象中有自己的顶层字段。这个对象也允许你绑定正常的HTML属性以及innerHTML(这将替换v-html指令)等DOM属性。

// 和‘v-bind:class’ 一样的API,也接受一个字符串,对象,或字符串和对象组成的数组
class: {
    foo: true,
    bar: false
}// 与‘v-bind:style’一样的API,也接收一个字符串,对象或对象组成的数组
style:{
    color: 'red',
    fontSize: '14px'
},
// 正常的Html属性
attrs: {
    id: 'foo'
},
// 组件 props
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
          }
        }
    ],
   // slots作用域格式
   // {name: props => VNode  | Array<VNode> }
        scopedSlots: {
            default: props => createElement('span', props.text)
        },
   // slot的名字,如果这个组件是另一个组件的子组件
   slot: 'name-of-slot',
   key: 'myKey',
   ref: 'myRef',
   // 在渲染函数中如果你使用了多个同名元素。这将是“¥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) {
        // 创建连字符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
        }
    }
})

约束

组件树中的所有VNodes必须唯一。这将意味着以下渲染函数是无效的:

render: function (createElement) {
    var myParagraphVNode = createElement('p', 'hi')
    return createElement('div', [
        // 呀 - 重复的 VNodes!
        myParagraphVNode, myParagraphVNode
    ])
}

如果你真的需要重复相同的元素/组件多次,你可以用一个工厂函数实现这个。例如,以下渲染函数是渲染20个完全相同段落的有效方法:

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

用JavaScript替换模板特性

v-ifv-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.')
        }
    }

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,它给了你更多而交互细节控制。

事件&键修饰符

对于.passive,.capture.once 事件修饰符,Vue提供了前缀能够被用于on

Mdoifiers Prefix
.passive &
.capture !
.once ~
.capture .once or .once.capture ~!

例如:

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

对于所有的其它事件和键修饰符,前缀是没有必要的,因为你可以在处理函数中使用事件方法:

Modifier(s) Equivalent in Handler
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
Keys:
.enter , .13
if (event.keyCode !== 13) return (change 13 to another key code)for other key modifiers
Modifiers Keys:
.ctrl , .alt,
.shift , .meta
if (!event.ctrlKey) return (change ctrlKey to altKey, shiftKey, or metaKey, respectively)

这里有一个使用所有这些修饰符的例子:

on: {
    keyup: function (event) {
        // 如果发出事件元素的没有事件绑定终止操作
        if (event.target !== event.currentTarget) return
        // 如果没有按下13键或没有同时按下shift键终止操作
        if (!event.shiftKey || event.keyCode !== 13) return
        // 停止事件传播
        event.stopPropagation()
        // 阻止该元素的默认keyup事件
        event.preventDefault()
        // ...
    }
}

Slots

你能够通过this.$slots访问静态的slot内容,作为VNodes的数组:

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

你也能够使用this.$scopedSlots访问作用域插槽,得到一个返回VNodes的函数:

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

为了使用渲染函数传递scoped slots给一个子组件,在VNode 数据对象里使用scopedSlots域。

render: function (createElement) {
    return createElement('div', [
        createElement('child', {
            // 传递`scopedSlots` 在data Object
            // 格式 { 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 plugin插件,在JSX中使用Vue语法,它让我们回到更接近模板的语法上:

import AnchoredHeading form './AnchoredHeading.vue'

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

你将看到在Vue的生态系统中将h作为createElement别名是通用的惯例,实际上也是JSX所要求的。如果在作用域中h失去作用,你的app将抛出一个错误。

对于更多的JSX如何映射JavaScript的用例,查看 usage docs.

函数式组件

之前我们创建的锚点标题组件相当简单。它没有管理和监听传递给它的任何状态,它没有生命周期方法,它仅仅是一个拥有一些props的一个函数。

在这个例子中,我们标记组件为functional,那意味着他们无状态(没有 reactive data响应式数据)和无实例(没有this上下文)。一个函数式组件看起来像这样:

Vue.component('my-component', {
    functional: true,
    // Props是可选的
    props: {
        // ...
    }// 作为缺少实例的补偿,我们现在提供第二个上下文参数
    render: function (createElement, context) {
    	// ...
	}
})

注意:在2.3.0版本之前,如果你希望在函数式组件里接受props,那么props操作符是需要的。在2.3.0+你能够忽略props操作符,在组件上的所有属性都会被当做props隐式取出。

在2.5.0+,如果你使用single-file components,模板基于函数式组件可以这样声明:

<template functional>
</template>

组件的所有东西都需要通过context传递。一个对象包含:

  • props:一个被提供给props的对象
  • children:一个VNode子节点数组
  • slots:一个函数返回一个slots对象
  • data:整个 data object,被传递给组件作为createElement的第二个参数
  • parent:一个父组件的引用
  • listeners: (2.3.0+)一个对象包含父节点注册的时间监听器,这是一个指向data.on的别名
  • injections:(2.3.0+)如果使用inject选项,則该选项应该包含被注入的属性。

增加了functional:true之后,我们的锚点标题组件将要求context参数更新渲染函数,this.$slots.default更新为context.children,然后this.level更新为context.props.level.

由于函数式组件仅仅是函数,渲染开销很低。然而,对于持久化实例的缺乏也意味着他们不会出现在 Vue devtools 组件树。

作为封装组件时他们也非常有用,例如,当你需要:

  • 程式化的选择要委托的几个组件之一
  • 在传递他们给一个子组件之前操作children、props或data

这是一个smart-list组件的例子,依赖于传入props的值,可以代表更多的具体组件。

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
        )
    }
})

传递属性和事件给子元素/子组件

在普通的组件里,没有被定义为props的特性会自动添加到组件的根元素、将现有的同名特性替换或智能合并

然而,函数式组件要求你显式的定义这个行为:

Vue.component('my-functional-button', {
    functional: true,
    render: function (createElement, context) {
        // 显式的传递任何属性,事件监听者,子节点,等等。
        return createElement('button', context.data, context.children)
    }
})

通过传递context.data作为给createElement的第二个参数。我们就把my-functional-button上面的所有属性和事件监听者都传递下去了。它是如此的透明,实际上,事件甚至不要求.native修饰符。

如果你使用基于模板的函数式组件 ,你也必须手动添加属性和监听器。由于我们可以访问独立的上下文内容,我们能使用data.attrs传递任何HTML属性,也可以使用listenersdata.on的别名)传递任何事件监听器。

<template functional>
    <button 
            class="btn btn-primary"
            v-bind="data.attrs"
            v-on="listeners">
        <slot/>
    </button>
</template>

slots() vs children

你可能想知道为什么我们slots()children都需要。slots().default不是和children相同吗?在一些情况下,是的 - 如果你用一个具有以下子节点的函数式组件,怎么办呢?

<my-functional-component>
    <p slot="foo">
        first
    </p>
    <p>second</p>
</my-functional-component>

对于这个组件,子节点都是paragraphs,slots().default将只给你第二个参数,slots().foo将给第一个参数。childrenslots()都有的情况下,因此允许你去选择是否这个组件知道这个slot系统或通过传递children将该责任委托给另一个组件。

模板编译

你可能对于知道Vue的模版实际编译为渲染函数感兴趣。这个实现细节你通常不需要知道,但是如果你想看看模板的功能是怎么被编译的,你可能对它感兴趣。下面是一个使用Vue.compile来实时编译模板字符串的简单demo:

<div>
  <header>
    <h1>I'm a template!</h1>
  </header>
  <p v-if="message">
    {{ message }}
  </p>
  <p v-else>
    No message.
  </p>
</div>  

渲染:

function anonymous(
) {
  with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}

staticRenderFns:

_m(0): function anonymous(
) {
  with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])}
}

猜你喜欢

转载自blog.csdn.net/wudizhanshen/article/details/86098362