废话少说,直接上阅读总结后的干货!!!
模板语法
模板语法的优点:
- 上手简单方便。
- 可对
VNode
进行优化。
因为模板内容是静态的,所以可以实现对这些静态内容进行分析,得知那些节点,属性是动态的、那些节点、属性又是静态的,有了这些信息,就可以在创建或者对比(Diff)
VNode
时对其 靶向更新 —— 也就是传说中patchFlags
。
当然,模板语法也存在一些局部的学习成本与心智负担,例如:
- scopeSlots 作用域插槽传值。
- props 、attrs、domProps 的区分。
- 种类丰富且功能强大面向用户黑盒的指令系统。
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML
。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数或JSX,以获得更强的灵活性。
渲染函数
渲染函数指的就是optionsAPI
中提供的 render
选项,值是一个函数,接收一个 createELement
方法作为参数来创建虚拟DOM。
createElement
createElement
方法接收三个参数。
createElement(<tag | component | asyncComponent> [, VNodeAttrData, Children])
复制代码
<tag | component | asyncComponent>
必传,值可以是字符串形式的HTML标签
export default {
render(createElement) {
return createElement("h1");
},
};
复制代码
或者接收一个已经转换为渲染函数形式的组件对象:
import { compileToFunctions } from "vue-template-compiler";
//导入一个会被CLI转换为渲染函数的组件
import Content from "./Content.vue";
//自定义一个渲染函数形式的组件
const Badge = {
name: "Badge",
render(createElement) {
return createElement("span", [
createElement("i", { attrs: { class: "icon icon-badge" } }, this.$slots.default),
]);
},
};
//使用内置的compile方法将模板字符串转换为渲染函数
const Title = compileToFunctions("<h1>Title</h1>");
export default {
render(createElement) {
return createElement("div", [
createElement(Title),
createElement(Content),
createElement(Badge, ["open"]),
]);
},
};
复制代码
对于Vue的内置组件,注意组件名小写,采用 kebab-case
命名法。
export default {
render(createElement) {
return createElement("transition", { props: { appear: true } }, [
createElement("p", "this is p"),
]);
},
};
复制代码
除此之外,还可以接受一个方法,用来返回一个异步组件。
<script>
const asyncComponent = ()=>import('./Test.vue');
const Badge = {
name:'Badge',
render(createElement){
return createElement('div', 'lazy component');
}
};
const LazyBadge = ()=> new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve(Badge);
}, 1000)
})
export default {
render(createElement) {
return createElement('div', [asyncComponent, LazyBadge]);
},
};
</script>
复制代码
VNodeAttrData
可选参数,值是 VNode
的 Attributes
数据对象。
Attributes (Object) | 类型 | 示例 | 说明 |
---|---|---|---|
class | Object | {foo:true, bar:false} | 与 v-bind:class 的 API 相同 |
style | Object | {color:’red’, fontSize:’14px’} | 与 v-bind:style 的 API 相同 |
attrs | Object | {id:’avatar’, title:’user avatar’} | 普通的 HTML attribute |
props | Object | {level:1} | 组件的 prop |
domProps | Object | { innerHTML: “Rewrite the title” } | DOM 对象的 property,例如 innerHTML 、title 、lang 等 |
on | Object | {click: this.clickHandler} | 使用vue的事件监听 |
nativeOn | Object | {click: this.nativeClickHandler} | 使用原生事件监听 |
directives | Object | {name:’my-custom-directive’,value:’2′,expression:’1 + 1′,arg:’foo’,modifiers:{bar:true}} | 定义使用到的自定义指令。 |
slot | String | ‘default’ | 如果组件是其它组件的子组件,则为其指定插槽名称。 |
scopedSlots | Object | {default:(slotProps)=>createElement(‘span’, slotProps.message)} | 定义作用域插槽生成内容的方法,这块会在父组件中定义,然后传入到子组件中,最后再由子组件进行调用并传入插槽的属性 |
key | String | Math.random().toString() | 组件唯一标识的key |
ref | String | ‘myRef’ | – |
refsInFor | Boolean | true | 如果在渲染函数存在多个元素具有重名的ref名,则 $refs.myRef 会变成一个数组。 |
domProps 示例:
createElement(
"h1",
{ domProps: { innerHTML: "Rewrite the title" } },
"This is the first level heading"
);
复制代码
作用域插槽并传值:
先看下基于模板语法来使用作用域插槽。
<!--App.vue-->
<template>
<world-time>
<template v-slot:BeiJing="slotProps">
<b>{{ slotProps.time }}</b>
</template>
<template v-slot:London="slotProps">
{{ slotProps.time }}
</template>
</world-time>
</template>
<!--WorldTime.vue-->
<template>
<div>
<slot name="BeiJing" :time="new Date().toLocaleString()"></slot>
<slot name="London" :time="new Date().toUTCString()"></slot>
</div>
</template>
复制代码
现在将其改造成渲染函数方式,先在组件使用的地方通过 scopedSlots
选项声明作用域插槽方法,然后将其作为属性数据对象传入到定义接收插槽的组件 world-time
中。
import WorldTime from "./WorldTime.vue";
export default {
components: { "world-time": WorldTime },
render: function(c) {
return c("world-time", {
scopedSlots: {
BeiJing: (props) => c("b", props.message),
London: (props) => props.message,
},
});
},
};
复制代码
最终由 WorldTime
组件来调用作用域插槽方法,并传入要跨作用域传递的参数。
export default {
render(c) {
return c("div", [
this.$scopedSlots.BeiJing({ message: new Date().toLocaleString() }),
this.$scopedSlots.London({ message: new Date().toUTCString() }),
]);
},
};
复制代码
Children
可选参数,子级虚拟节点。
值可以是文本节点:
export default {
render(c) {
return c("h1", { attrs: { style: "color:red" } }, "This is a title!");
},
};
复制代码
也可以是包含多个虚拟子节点的数组。
export default {
render(c) {
return c("ul", [c("li", "item1"), c("li", "item2")]);
},
};
复制代码
虚拟DOM
Vue 通过创建虚拟DOM来追踪如何改变文档中的真实DOM。
虚拟DOM 是由 虚拟节点(Virtual Node) 组成的 VNode 树。每个VNode
都描述了对应DOM节点与其子节点的相关信息。
VNode 树中的所有
VNode
都必须唯一。这一点也与真实DOM节点的规范一直,如果对文档中的某一个DOM 对象进行多次添加操作,实际上执行的是移动操作。
对于确实需要重复很多次的元素/组件,可以使用工厂函数来实现:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
复制代码
JSX
JSX 是对 JavaScript 语法进行扩展的语法糖。
JSX = Javascript + XML;即在 JavaScript 里面写 XML,所以它即具备了 JavaScript 的灵活性,同时又兼具 HTML 的语义化和直观性。
Vue-CLI 3+
已经内置了对jsx
语法的支持,如果低于此版本,则需要单独对 babel进行配置
基础示例:
<script>
export default {
data() {
return {
search: "",
list: [
{ name: "peach", key: 1 },
{ name: "grape", key: 2 },
{ name: "mango", key: 3 },
],
};
},
computed: {
filters() {
return this.list.filter((item) => item.name.indexOf(this.search) !== -1);
},
isEmpty() {
return !this.filters.length;
},
},
render() {
const InputProps = {
class: { empty: this.isEmpty },
attrs: { type: "text", placeHolder: "请输入搜索内容", ...this.$attrs },
};
return (
<div>
<input
{...InputProps}
onInput={(e) => {
this.search = e.target.value;
}}
/>
<ul>
{this.filters.map((item) => (
<li key={item.key}>{item.name}</li>
))}
</ul>
</div>
);
},
};
</script>
复制代码
在 Vue 中使用 JSX 进行开发,还需要注意避免以下情况:
- 不能再
render
方法内定义名称为h
标识符,因为该变量名已经存在,再次定义会报重命名错误。 - 只能在组件实例内部将标签或组件赋值给另一个标量,否则会报
createElement
方法未定义。
const h1 = <h1></h1>; // Uncaught ReferenceError: h is not defined
export default {
data(){
return {
elements:[<li></li>,<li></li>] //correct!
}
},
methods:{
getImage(){ //correct!
return <img />
}
},
render() {
return h1;
},
};
复制代码
- JSX 的函数式组件可以在组件实例的内外都能定义,但是需要注意不能将其包装到对象或一个数组,否则编译程序将不能正确识别,只会返回一个渲染函数。
const H1 = () => <h1></h1>;
const H3 = [()=> <h3></h3>]; //bad
export default {
functional: true,
render() {
const H2 = () => <h2></h2>;
return (
<div>
<H1 />
<H2 />
</div>
);
},
};
复制代码
更多Vue 中的 JSX
实践,可以阅读 AntV 或 Vant 源码。
函数式组件
函数式组件就是无状态组件。
函数式组件本质就是一个函数,它没有生命周期、没有状态(响应式数据)、没有实例(this 上下文),只会通过 render
函数的第二个 context
参数传递 prop
,因此它执行起来的渲染开销要低很多。
我们分别可以通过模板语法、渲染函数、JSX来创建不同形式的“函数式组件”。
模板语法版函数式组件
<template functional>
<p>This is a functional component</p>
</template>
复制代码
JSX版函数式组件
const H1 = () => <h1></h1>; //子组件也是函数式
export default {
functional: true, //声明当前组件是函数式。
render() {
return <H1 />;
},
};
复制代码
渲染函数版函数式组件
export default {
functional: true,
render(c, context) {
return c("p", context.data, "This is a functional component!");
},
};
复制代码
更多关于函数式组件参数传递可以看这里>函数式组件
模板、JSX与渲染函数
Vue 创建 HTML
的方式主要有:模板、JSX、渲染函数,最终它们都会殊途同归,统一转换为“渲染函数”。
“JSX”与“渲染函数”都需要在 OptionsAPI
的 render
选项中使用,而模板语法则在 SFC 独立的 template
区块下使用。
生成渲染函数的方法有多种:
- 调用
compileToFunctions
方法获得转换后的结果。 - 通过 Vue-CLI 的辅助,导入一个模板语法的组件,获得编译后的渲染函数结果。
- 使用
import()
异步导入Vue组件,此种方式依然是借助 Vue-CLI 的辅助。 - 手动编写渲染函数,注意包装的结构:
{render:ƒ}
。
最后,一个很重要的问题,我们何时该选择使用模板、JSX还是渲染函数方式?
特性 | 模板 | JSX | 渲染函数 |
---|---|---|---|
是否需要简单直观易上手? | ★★★ | ★ | ☆ |
是否需要JS完全编程能力? | ✖ | ★★ | ★★★ |
是否需要在一个模块(SFC)中定义多个组件? | ✖ | ★★★ | ★★★ |
是否需要将单个组件或标签赋值给某个变量或者是定义为函数式组件,用于内容的灵活组装? | ✖ | ★★★ | ★★★ |
是否需要更好的复用 props 、attrs 、events ?例如通过 spread 或 reset 等运算符进行更好的组合、分解、绑定。 |
✖ | ★★★ | ★★★ |
是否需要动态修改标签或组件名称? | ✖ | ✖ | ★★★ |
是否需要访问组件实例外的变量或数据? | ✖ | ★★★ | ★★★ |
组件、props、attrs、events 等是否需要更灵活的参与条件运算? | ★★ | ★★★ | ★★★ |
是否需要支持 Typescript ? | ☆ | ★★★ | ★★★ |
递归调用是否需要更简单直观? | ☆ | ★★★ | ★★★ |
是否需要更简单易懂的方式使用 scopeSlots ? | ★ | ★★ | ★★★ |
是否需要JS完全编程能力?
由于组件模板与组件逻辑是脱离的,所以组件逻辑如果想对模板中的标签进行操作,则必须按照数据驱动的思想来实现。
export default {
props: {
reverse: Boolean,
},
render() {
const children = [
<li class="foo" style="color:red">
1
</li>,
<li>2</li>,
<li class="bar">3</li>,
];
if (this.reverse) {
children.reverse();
}
console.log(children);
return <ul>{children.map((item) => item)}</ul>;
},
};
复制代码
?,思考下如果采用模板的方式实现,我们则必须要基于数据驱动的思路来解决这个,要专门定义对应的数据结果,然后遍历渲染。
是否需要访问组件实例外的变量或数据?
模板的执行上下文都是在编译时绑定好的,而绑定的目标就是组件的实例 this,所以模板内要进行插值的变量都只能从组件实例已经挂载的数据中取,而不能从其它作用域中获取数据,限制了参与HTML内容生成的数据来源的灵活性。
模板编译
Vue.compile
方法可以将模板字符串转换为字符串形式的渲染函数并保留 AST
对象。
Vue.compile('<p></p>');
/*
{
"ast": {
"type": 1,
"tag": "p",
"attrsList": [],
"attrsMap": {},
"rawAttrsMap": {},
"children": [],
"plain": true,
"static": true,
"staticInFor": false,
"staticRoot": false
},
"render": "with(this){return _c('p')}",
"staticRenderFns": [],
"errors": [],
"tips": []
}
*/
复制代码
staticRenderFns
保存的是静态节点,例如使用v-once
指令的组件或标签。