Vue render 渲染函数
render 函数
什么是Virtual DOM
Virtual DOM 并不是真正意义上的 DOM, 而是一个轻量级的 JavaScript 对象, 在状态发生改变的时候, Virtual DOM 会进行Diff
运算, 来更新只需要被替换的 DOM, 而不是全部都进行重绘.
用 Virtual DOM 创建的 JavaScript 对象一般如下,
var vNode = {
tag: 'div',
attributes: {
id: 'main'
},
children: {
// p 节点
}
}
VNode
VNode 对象通过一些特定的选项描述了真实的 DOM
在Vue.js
中每一个元素或者组件都对应一个 VNode 对象.源码中的定义如下
export interface VNode {
tag?:String;
data?:VNodeData;
children?:VNode[];
text?:String;
elm?:Node;
ns?:String;
context?:Vue;
key?: String | Number;
componentOptions?:VNodeComponentOptions;
componentInstance?:Vue;
parent?:VNode;
row?: Boolean;
isStatic?: Boolean;
isRootInsert?: Boolean;
isComment?: Boolean;
...
}
tag
标签名
data
数据对象
export interface VNodeData {
key?: String | Number;
slot?: String;
scopedSlot?:{[key: String]: scopedSlot};
ref?: String;
tag?: String;
staticClass?: String;
class?: any;
staticStyle:{[key: String]: any};
style?: Object[] | Object;
props?: {[key: String]: any};
attrs?: {[key: String]: any};
domProps?: {[key: String]: any};
hook?: {[key: String]: Function};
on?: {[key: String]: Function [] | Function};
nativeOn?: {[key: String]: Function | Function []};
transition?: Object;
show?: Boolean;
inlineTemplate?:{
render: Function,
staticRenderFns: Function []
};
directives?: VNodeDirective [];
keepAlive:? Boolean;
}
children
子节点, 数组, 也是 VNode 类型
text
当前节点的文本, 一般文本节点或注释节点会有该属性
elm
当前虚拟节点对应的真实的 DOM 节点
ns
节点的 namespace
content
编译作用域
functionalContext
函数化组件的作用域
key
节点的 key 属性,用于节点的标识, 有利于 patch 的优化
componentOptions
创建组件时用到的选项信息
child
当前节点对应的组件实例
parent
组件的占位节点
raw
原始html
isStatic
静态节点的标识
isRootInsert
是否作为根节点被插入
被<transition>
包裹的节点, 该属性为 false
isComment
当前节点是否是注释节点
isCloned
当前节点是否是克隆节点
isonce
当前节点是否有 once
指令
VNode 的分类
- TextVNode
- 文本节点
- ElementVNode
- 普通元素节点
- ComponentVNode
- 组件节点
- EmptyVNode
- 没有内容的注释节点
- CloneVNode
- 克隆节点, 可以是以上的任意节点, 唯一的区别在于
isCloned
的属性为true
- 克隆节点, 可以是以上的任意节点, 唯一的区别在于
什么是 Render 函数
creatElememnt
基本用法
createElement(
// {String | Object | Function}
// 一个 HTML 标签, 组件选项, 或者一个函数
// 必须return 一个标签或者一个组件选项
'div',
// {Object}
// 一个对应属性的数据对象, 可选
// 您可以在 template 中使用
{
//
},
// {String | Array},
// 子节点,可选
[createElement('h1', 'hello world'),
createElement('myComponent', {
props: {
someProp: 'foo'
}
}),
'bar'
]
)
对于数据对象的用法如下:
// 数据对象的用法
object = {
// 和 v-bind 的用法一样
class: {
foo: true,
bar: false
},
// 和 v-bind 一样的 API
style: {
color: 'red',
fontSize: '14px'
},
// 正常的 HTML 属性
attrs: {
id: 'foo'
},
// 组件的 props
props: {
myProp: 'bar'
},
// DOM 属性
domProps: {
innerHTML: 'baz'
},
// 自定义事件监听器'on'
// 不支持如 v-on: keyup.enter 的修饰符
// 需要手动匹配 enter
on: {
click: this.clickHandler
},
// 仅对于组件, 用于触发原生事件
// 而不是用 vm.$emit 触发的事件
nativeOn: {
click: this.nativeClickhandler
},
// 自定义指令
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1+ 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域 slot
// {name: props => VNode | VNode[]}
scopedSlots: {
default: props => h('span', props.text)
},
// 如果子组件有定义 slot 的名称
slot: 'name-of-slot',
// 其他特殊顶层属性
key: 'myKey',
ref: 'myRef'
}
- 实例
template 版本的代码链接在这儿!!
如果使用 template
<div id="app">
<ele></ele>
</div>
<script>
Vue.component('ele', {
template: `
<div id="element"
:class="{show: show}"
@click="handleClick"> 文本内容 </div>
`,
data: function () {
return {
show: true
}
},
methods: {
handleClick: function () {
// this.show = !this.show
window.alert('clicked!')
}
}
})
const app = new Vue({
el: '#app'
});
render 版本的代码链接在这儿!!
使用 render 函数的话
Vue.component('ele', {
// template: `
// <div id="element"
// :class="{show: show}"
// @click="handleClick"> 文本内容 </div>
// `,
render: function (creatElement) {
return creatElement('div', {
attrs: {
id: 'element'
},
class: {
show: this.show
},
on: {
click: this.handleClick
}
}, "文本内容")
},
data: function () {
return {
show: true
}
},
methods: {
handleClick: function () {
// this.show = !this.show
window.alert('clicked!')
}
}
})
关于 render 函数的一些约束
如果 VNode 是组件或者是含有组件的 slot, 那么 VNode 必须唯一
- 错误1. 组件的重复使用
// 组件选项
var child = {
render: function (createElement) {
return createElement('p', 'text')
}
}
// 注册组件
Vue.component('ele', {
render: function (createElement) {
// 创建一个子节点, 使用组件
const childNode = createElement(child)
return createElment('div', [childNode, childNode])
}
})
// 实例化组件
const app = new Vue({
el: '#app'
})
- 错误2. 重复使用带有组件的 slot
<div id="#app">
<ele>
<!-- render 需要在组件里面 -->
<div>
<!-- slot -->
<child></child> <!--组件-->
</div>
<ele>
</div>
<script>
// 全局注册组件 child 正确的使用
Vue.component('child', {
render: function (createElement) {
return createElement('p', 'text')
}
})
// 现在开始错误的使用 render
Vue.component('ele', {
render: function (createElement) {
return createElement('div', [this.$slots.default,
this.$slots.default])
}
})
// 实例化
const app = new Vue({
el: '#app'
})
</script>
关于上面的问题的解决方法
- 重复的组件作为 VNode []
- 使用
Array.apply(null, {length:5}).map(....)
- 使用
var child = {
render: function (createElement) {
return createElement('p', 'text')
}
}
Vue.component('ele', {
render: function (createElement) {
return createElement('div', Array.apply(null, {length: 2}).map(function () {
return createElement(child)
}))
}
})
var app = new Vue({
el: '#app'
})
重复的 VNode 作为 Array 传入第三个参数的代码链接!!
- 带组件的 slot
- 深度克隆节点.(其实就是为了不让数组里指向同一个引用)
Vue.component('child', {
render: function (createElement) {
return createElement('p', 'text')
}
})
Vue.component('ele', {
render:function (createElement) {
// 深度克隆 slot 节点
function cloneVNode(VNode):
var child = VNode.children && VNode.children.map(function (vnode) {
return cloneNode(vnode)
})
const cloned = createElement(
VNode.tag,
VNode.data,
child
)
cloned.text = VNode.text
cloned.isComment = VNode.isComment
cloned.componentOptions = VNode.componentOptions
cloned.elm = VNode.elm
cloned.context = VNode.context
cloned.ns = VNode.ns
cloned.isStatic = VNode.isStatic
cloned.key = VNode.key
return cloned
}
const vNodes = this.$slots.default
const clonedNodes = cloneVNode(vNodes)
return createElement('div', [vNodes, clonedNodes])
})
使用 JS 代替模板功能
不能使用v-if
和 v-for
v-if
<div id="app">
<ele :show="show">
<button @click="show = !show"> 切换 show </button>
</ele>
</div>
<script>
Vue.component('ele', {
render: function (createElement) {
if (this.show) {
return createElement('p', 'show 的值是 true')
}
else {
return createElement('p', 'show 的值是 false')
}
},
props: {
show: {
type: Boolean,
required: true,
default: true
}
}
})
// 实例化
const app = new Vue({
el: '#app',
data: {
show: false
}
})
</script>
v-for
<div id="app">
<ele :list="list"></ele>
<button @click= 'handleClick'>切换 list</button>
</div>
</body>
<script>
const list1 = []
const list2 = [
'Vue.js 实战',
'JS 实战',
'python 实战'
]
Vue.component('ele', {
props: {
list: {
type: Array,
default: function () {
return []
}
}
},
render: function (createElement) {
if(this.list.length){
nodes = this.list.map(function (item) {
return createElement('p', item)
})
return createElement('ul', nodes)
}
else {
return createElement('p', '列表为空')
}
}
})
const app = new Vue({
el: '#app',
data: {
list: list1
},
methods: {
handleClick: function () {
if (this.list === list1) {
this.list = list2
}
else {
this.list = list1
}
}
}
})
</script>
v-model
render
里面也没有 v-model
的api
js 替代 v-model 语法糖的代码链接在这儿!!
修饰符
修饰符也无法使用, 需要使用句柄
修饰符 | 句柄 | 说明 |
---|---|---|
.stop | event.stopPropagation | 阻止事件冒泡 |
.prevent | event.preventDefault | 阻止默认事件 |
.self | if(event.target !== event.currentTarget) reuturn ; | 事件恰好是自身时触发 |
.enter,.13 | if(event.keyCode !== 13) return ; | 按下回车键的时候触发 |
.ctrl, .alt, .shift, .meta | if(!event.ctrlKey) return ; 可替换按键 | 按下特殊按键的时候触发 |
.capture | ! | 事件捕获 |
.once | ~ | 只触发一次 |
.capture.once 或者 .once.capture | ~! | 第一次事件捕获时触发 |
比如, 现在可以完善一个简单的聊天室的功能的页面
聊天模拟的代码链接在这儿!!
slot 的备用分发内容
如果slot为空, 在使用 template
的时候,可以在 template
中添加默认分发的备用内容. 可是如果使用render
的话, 需要使用 js 进行逻辑判断, 不能直接定义备用的分发内容了
this.$slots.default === undefined
=> 组件中, 未命名的(默认) slot 没有被使用
函数化组件
- Vue.js 提供了一个
functional
的布尔值, 设置为true
时可以使组件无状态和无实例, 也就是没有data
和this
上下文. 这样用render
函数返回虚拟节点可以更容易渲染, 因为函数化组件只是一个函数, 渲染开销要小很多 - 在使用函数化组件的时候,
render
函数提供饿了第二个参数context
来提供临时上下文. 组件需要的data
,props
,parent
,children
,slot
都是通过这个上下文来进行传递的.e.g.this.level
=>context.props.level
this.$slots.default
=>context.children
通过数据智能选择组件的 render 函数的代码链接在这儿!!(html)
通过数据智能选择组件的 render 函数的代码链接在这儿!!(js)
注意:三个组件对象都接收了一个
props: data
. 在组件ele
中也有props:data
, 此时通过getComponent()
来确定具体选择哪一个组件进行渲染.
将getComponent()
的结果作为一个组件选项的对象传入createElement
的第一个参数当中, 这时候通过把data
作为 props 传给第二个参数, 这样子就可以传递给更里面的组件.
函数化组件的一个解决方法是可以用指令is
进行替代
函数化组件主要适用于以下的两种情况
- 程序化的在多个组件中选择一个
- 再将
children
,props
,data
传递给子组件之前操作它们