9.1 什么是Virtual Dom
Virtual Dom并不是真正意义上的DOM,而是一个轻量级的JavaScript对象,在状态发生变化时,Virtual Dom会进行Diff运算,来更新只需要被替换的DOM,而不是全部重绘。
与DOM操作相比,Virtual Dom是基于JavaScript计算的,所以开销会小很多。
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;
raw?: boolean;
isStatic?: boolean;
isRootInsert?: boolean;
isComment?: boolean;
}
具体含义如下:
· tag 当前节点的标签名
· data 当前节点的数据对象
VNodeData代码如下:
export interface VNodeData {
key?: string | number;
slot?: string;
scopedSlots?: { [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。
· context 编译作用域。
· functionalContext 函数化组件的作用域。
· key 节点的key属性,用于作为节点的标识,有利于patch的优化。
· componentOptions 创建组件实例时会用到的信息选项。
· child 当前节点对应的组件实例。
· parent 组件的占位节点。
· raw 原始html。
· isStatic 静态节点的标识。
· isRootInsert 是否作为根节点插入,被<transition>包裹的节点,该属性的值为false。
· isComment 当前节点是否是注释节点。
· isCloned 当前节点是否为克隆节点。
· isOnce 当前节点是否有v-once指令。
使用Virtual Dom就可以完全发会JavaScript的编程能力。在多数场景中,我们使用template就足够了,但在一些特定的场景下,使用Virtual Dom会更简单。
9.2 什么是Render函数
<div id="app">
<anchor :level="2" title="特性">特性</anchor>
</div>
<script>
Vue.component('anchor', {
props: {
level: {
type: Number,
required: true
},
title: {
type: String,
default: ''
}
},
render: function (createElement) {
return createElement(
'h' + this.level,
[
createElement(
'a',
{
domProps: {
href: '#' + this.title
}
},
this.$slots.default
)
]
)
}
});
var app = new Vue({
el: '#app'
})
</script>
9.3 createElement用法
9.3.1 基本参数
createElement(
// {String | Object | Function}
// 一个HTML标签,组件选项,或一个函数
// 必须return上述其中一个
'div',
// {Object}
// 一个对应属性的数据对象,可选
// 可以在template中使用
{
// 稍后详细介绍
},
// {String | Array}
// 子节点(VNodes),可选
[
createElement('h1', 'hello world'),
createElement(MyComponent, {
props: {
someProp: 'foo'
}
}),
'bar'
]
)
第一个参数必选,可以是一个HTML标签,也可以是一个组件或函数;第二个是可选参数,数据对象,在template中使用。第三个是子节点,也是可选参数,用法一致。
对于第二个参数“数据对象”。具体的选项如下:
{
//和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
},
//自定义指令
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1+1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
//作用域slot
//{name: props => VNode | Array<Vnode> }
scopedSlots: {
default: props => h('span', props.text)
},
//如果子组件有定义slot的名称
slot: 'name-of-slot',
//其他特殊顶层属性
key: 'myKey',
ref: 'myRef'
}
Render函数并不是在任何场景都是最优写法,只有当代码中有很多相似或重复的代码块时使用该函数可读性更高,否则还是使用template写法更加简洁。
9.3.2 约束
所有的组件树中,如果VNode是组件或含有组件的slot,那么VNode必须是唯一的。
重复渲染多个组件的方法很多。重复子组件如下示例:
<div id="app">
<ele></ele>
</div>
<script>
//局部生命组件
var Child = {
render:function(createElement){
return createElement('p','text');
}
};
Vue.component('ele',{
render:function(createElement){
return createElement('div',
Array.apply(null,{
length:5
}),map(function(){
return createElement(Child);
})
)
}
});
var app = new Vue({
el:'#app'
})
</script>
含有组件的slot,重复多个代码如下:
<div id="app">
<ele>
<div>
<Child></Child>
</div>
</ele>
</div>
<script>
//全局注册组件
Vue.component('Child',{
render:function(createElement){
return createElement('p','text');
}
});
Vue.component('ele',{
render:function(createElement){
//克隆slot节点的方法
function cloneVNode(vnode){
//递归所有子节点
const cloneChildren = vnode.children &&
vnode.children.map(function(vnode){
return cloneVNode(vnode);
});
const cloned = createElement(
vnode.tag,
vnode.data,
cloneChildren
);
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 cloneVNodes = vNodes.map(function(vnode){
return cloneVNode(vnode);
});
return createElement('div',
vNodes,
cloneVNodes,
)
}
});
var app = new Vue({
el:'#app'
})
</script>
9.3.3 使用JavaScript代替模板功能
在render函数中不能够使用Vue内置的指令,可以用原生的JavaScript实现。
比如v-if:
render:function(createElement){
if(this.show){
return createElement('p','show的值为true');
}else{
return createElement('p','show的值为false');
}
}
render:function(createElement){
var nodes = {};
for(var i=0;i<this.list.length;i++){
nodes.push(createElement('p',this.list[i]));
}
return createElement('div',nodes);
}
<div id="app">
<ele></ele>
</div>
<script>
Vue.component('ele',{
render:function(createElement){
var _this = this;
return createElement('div',{
createElement('input',{
domProps:{
value:this.value
},
on:{
input:function(event){
_this.value = event.target.value;
}
}
}),
createElement('p','value:'+this.value)
})
},
data:function(){
return {
value:''
}
});
var app = new Vue({
el:'#app'
})
</script>
9.4 函数化组件
Vue.js提供了一个functionnal的布尔值选项,设置为true可以使组件无状态和无实例,也就是没有data和this上下文。这样更容易渲染,开销小很多。
使用函数化组件时,render函数提供第二个参数context来提供临时上下文组件需要的data、props、slots、children、parent都是通过上下文传递。This.level改写为context.props.level,this.$slots.default改写为context.children。
9.5 JSX
JSX是一种看起来像HTML,但实际上是JavaScript的语法扩展,它用更接近DOM结构的形式描述一个组件的UI和状态信息。需要插件babel-plugin-transform-vue-jsx来支持jsx语法。