为啥研究这个?在以前开发组件库的过程当中,遇到了许多遗留的问题,包括数据模板渲染、组件按需加载、引入自定义组件插槽等等,因此为了修复和避免这些问题,学习一波更接近编译器的编写方式,看看如何经过这种彻底编程方式来解决一波这些问题~固然这里只是一些最基本的使用和探索,由于官网例子太少了,只能一个个本身搭=。=javascript
Vue 推荐在绝大多数状况下使用 template 来建立你的 HTML。然而在一些场景中,你真的须要 JavaScript 的彻底编程的能力,这就是render 函数,它比 template 更接近编译器。(从官网复制的,慌得一批,其实简单来讲就是以函数的方式写HTML,可控性更强一些~)css
固然,官网已经给出了一个使用template来编写的不方便的demo,因此在这里就不反复提起了,初次使用或者有兴趣的大佬能够直接戳这个连接了解一下~Vue Renderhtml
了解基本概念的客官能够直接下拉到实例,实例已上传githubvue
slot
属性的用法scopedSlots
的用法DOM 就是浏览器解析 HTML 得来的一个树形逻辑对象。java
用 Object 来表明一个节点,这个节点叫作虚拟节点( Virtual Node )简写为 VNode,由 VNode 树组成虚拟DOM。node
Web 页面的大多数操做和逻辑的本质就是不停地修改 DOM 元素,可是 DOM 操做太慢了,过于频繁的 DOM 操做可能会致使整个页面掉帧、卡顿甚至失去响应。仔细想想,不少 DOM 操做是能够打包(多个操做压成一个)和合并(一个连续更新操做只保留最终结果)的,同时 JS 引擎的计算速度要快得多,因此为何不把 DOM 操做先经过JS计算完成后统一来一次大招操做DOM呢,因而就有了虚拟DOM的概念。固然,虚拟DOM操做的核心是Diff算法,也就是比较变化先后Vnode的不一样,计算出最小的DOM操做来改变DOM,提升性能。webpack
经过`createElement(tag, options, VNodes)`,下面就来介绍这个函数的基本概念。git
简单来讲CreateElement就是用来生成Vnode的函数github
CreateElement 到底会返回什么呢?其实不是一个实际的 DOM 元素(返回的是Vnode)。它更准确的名字多是 createNodeDescription,由于它所包含的信息会告诉 Vue 页面上须要渲染什么样的节点,及其子节点。web
【Tips】 CreateElement
函数在惯例中一般也写做h
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签字符串,组件选项对象,或者 解析上述任何一种的一个 async 异步函数,必要参数。
'div',
// {Object}
// 一个包含模板相关属性的数据对象
// 这样,您能够在 template 中使用这些属性。可选参数。
{
// 详情见下方
},
// {String | Array}
// 子节点 (VNodes),由 `createElement()` 构建而成,或使用字符串来生成“文本节点”。可选参数。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
复制代码
【Tips】 文档中此处说VNodes子节点必须是惟一的,也就是说第三个参数的Array里不能出现相同指向的VNodes,实际验证之后,就算写重复的VNodes,也并不会报错,估计此处会有些坑,如今还没踩到,建议按照文档要求,保持子节点惟一。
如下属性为简单介绍,具体用法和一些 _备注解释 _能够参考后面会讲到的【包含属性配置较完整的实例】
{
// 和`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
}
}
],
// 做用域插槽格式
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 若是组件是其余组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其余特殊顶层属性
key: 'myKey',
ref: 'myRef'
}
复制代码
这是一个基础的Demo,包含了
简单的渲染用法
标签
props
slot
点击事件
如下示例Demo均采用单文件组件的方式,工程用vue-cli
搭建的webpack-simple
工程。
组件wii-first
<script>
export default {
name: 'wii-first',
data() {
return {
msg: 0
}
},
props: {
level: {
type: [Number, String],
required: true
}
},
render: function(createElement) {
this.$slots.subtitle = this.$slots.subtitle || []
// this.level = 1时, 等价于
// <h1 class="wii-first">
// 第一个组件, <slot></slot>
// <slot name="subtitle"></slot>,此处是data的值: {{msg}}
// <button @click="clickHandler">点我改变内部data值</button>
// </h1>
return createElement(
'h' + this.level, // tag name 标签名称
{
class: 'wii-first'
},
// this.$slots.default, // 子组件中的slot 单个传递
// this.$slots.subtitle,
[
'第一个组件, ',
...this.$slots.default, // 默认slots传递
...this.$slots.subtitle, // 具名slots传递
',此处是data的值: ',
this.msg,
createElement('button', {
on: {
click: this.clickHandler
},
}, '点我改变内部data值')
]
)
},
methods: {
clickHandler() {
this.msg = Math.ceil(Math.random() * 1000)
}
}
}
</script>
复制代码
【Tips】:CreateElement的第三个参数在文档中规定组件树中的全部 VNode 必须是惟一的,也就是说在第三个参数中有两个指向相同的Vnode是无效的。但通过实践发现,其实是能够渲染出来的,在此不推荐这么写哦,可能会掉到不可预料的大坑hiahiahia~
引入方式
<template>
<div id="app">
<wii-first level="1">我是标题 <span slot="subtitle">我是subtitle</span></wii-first>
</div>
</template>
<script>
import WiiFirst from './components/first/index.vue'
export default {
name: 'app',
components: {
WiiFirst
},
data() {
return {
}
}
}
</script>
复制代码
这个Demo主要展现了createElement属性用法,包含
click.stop
的转换示例不包含
组件wii-second
export default {
name: 'wii-second',
data() {
return {
myProp: '我是data的值, 只是为了证实props不是走这儿'
}
},
props: {
},
render: function(createElement) {
// 等价于
// <div id="second" class="wii-second blue-color" style="color: green;" @click="clickHandler">
// 我是第二个组件测试, 点我触发组件内部click和外部定义的@click.native事件。
// <div>{{myProp}}</div>
// <button @click="buttonClick">触发emit</button>
// </div>
return createElement(
'div', {
//【class】和`v-bind:class`同样的 API
// 接收一个字符串、对象或字符串和对象组成的数组
// class: 'wii-second',
// class: {
// 'wii-second': true,
// 'grey-color': true
// },
class: [{
'wii-second': true
}, 'blue-color'],
//【style】和`v-bind:style`同样的 API
// 接收一个字符串、对象或对象组成的数组
style: {
color: 'green'
},
//【attrs】正常的 HTML 特性, id、title、align等,不支持class,缘由是上面的class优先级最高[仅猜想]
// 等同于DOM的 Attribute
attrs: {
id: 'second',
title: '测试'
},
// 【props】组件 props,若是createElement定义的第一个参数是组件,则生效,此处定义的数据将被传到组件内部
props: {
myProp: 'bar'
},
// DOM 属性 如 value, innerHTML, innerText等, 是这个DOM元素做为对象, 其附加的内容
// 等同于DOM的 Property
// domProps: {
// innerHTML: 'baz'
// },
// 事件监听器基于 `on`, 用于组件内部的事件监听
on: {
click: this.clickHandler
},
// 仅对于组件,同props,等同@click.native,用于监听组件内部原生事件,而不是组件内部使用 `vm.$emit` 触发的事件。
// nativeOn: {
// click: this.nativeClickHandler
// },
// 若是组件是其余组件的子组件,需为插槽指定名称,见 wii-third 组件
// slot: 'testslot',
// 其余特殊顶层属性
// key: 'myKey',
// ref: 'myRef'
}, [
`我是第二个组件测试, 点我触发组件内部click和外部定义的@click.native事件。`,
createElement('div', `${this.myProp}`),
createElement('button', {
on: {
click: this.buttonClick
}
}, '触发emit')
]
)
},
methods: {
clickHandler() {
console.log('我点击了第二个组件,这是组件内部触发的事件')
},
buttonClick(e) {
e.stopPropagation() // 阻止事件冒泡 等价于 click.stop
console.log('我点击了第二个组件的button,将会经过emit触发外部的自定义事件')
this.$emit('on-click-button', e)
}
}
}
复制代码
引入方式
<template>
<div id="app">
<wii-second @click.native="nativeClick" @on-click-button="clickButton"></wii-second>
</div>
</template>
<script>
import WiiSecond from './components/second/index.vue'
export default {
name: 'app',
components: {
WiiSecond
},
data() {
return {
}
},
methods: {
nativeClick() {
console.log('这是组件外部click.native触发的事件,第二个组件被点击了')
},
clickButton() {
console.log('这是组件外部触发的【emit】事件,第二个组件被点击了')
}
}
}
</script>
复制代码
上面例子中用到了e.stopPropagation
这个方法,等价于 template 模板写法的click.stop
,其余的事件和按键修饰符也有对应的方法,对应状况以下。
事件修饰符对应的前缀
template事件修饰符 | render写法前缀 |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.capture.once 或 .once.capture | ~! |
例如
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
复制代码
其余事件修饰符,对应的事件处理函数中使用事件方法
template事件修饰符 | 对应的事件方法 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
Keys: .enter, .13 | if (event.keyCode !== 13) return (对于其余的键盘事件修饰符,将13换成其余的键盘code就行) |
Modifiers Keys: .ctrl, .alt, .shift, .meta | if (!event.ctrlKey) return (将 ctrlKey 换成 altKey, shiftKey, 或者 metaKey, respectively) |
例如
on: {
keyup: function (event) {
// 若是触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 若是按下去的不是 enter 键或者
// 没有同时按下 shift 键
// 则返回
if (!event.shiftKey || event.keyCode !== 13) return
// 阻止 事件冒泡
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
}
复制代码
slot
属性的用法这个Demo主要展现render中createElement的配置slot属性用法。
由于此处一直很疑惑什么状况下可以用到这个slot属性,因此就试了一下,仅供参考,具体使用场景须要根据业务逻辑来定
组件wii-third
<script>
export default {
name: 'wii-third',
data() {
return {}
},
components: {
WiiTestSlot: {
name: 'wii-test-slot',
render(createElement) {
this.$slots.testslot = this.$slots.testslot || []
// 等价于
// <div>
// 第三个组件,测试在组件中定义slot, <slot name="testslot"></slot>
// </div>
return createElement(
'div', [
'第三个组件,测试在组件中定义slot, ',
...this.$slots.testslot
]
)
}
},
WiiTestSlotIn: {
name: 'wii-test-slot-in',
render(createElement) {
// 等价于
// <span>我是组件中的slot内容</span>
return createElement(
'span', [
'我是组件中的slot内容'
]
)
}
}
},
props: {
},
render: function(createElement) {
// 等价于
// <div style="margin-top: 15px;">
// <wii-test-slot>
// <wii-test-slot-in slot="testslot"></wii-test-slot-in>
// </wii-test-slot>
// </div>
return createElement(
'div', {
style: {
marginTop: '15px'
}
}, [
createElement(
'wii-test-slot',
//这么写不会被渲染到节点中去
// createElement(
// 'wii-test-slot-in',
// {
// slot: 'testslot'
// }
// ),
[
// createElement再放createElement须要放入数组里面,建议全部的组件的内容都放到数组里面,统一格式,防止出错
createElement(
'wii-test-slot-in', {
slot: 'testslot'
}
)
]
)
]
)
},
methods: {
}
}
</script>
复制代码
【Tips】:若是createElement里面的第三个参数传递的是createElement生成的VNode对象,将不会被渲染到节点中,须要放到数组中才能生效,此处猜想是由于VNode对象不会被直接识别,由于文档要求是String或者Array。
引入方式
<template>
<div id="app">
<wii-third></wii-third>
</div>
</template>
<script>
import WiiThird from './components/third/index.vue'
export default {
name: 'app',
components: {
WiiThird
},
data() {
return {}
}
}
</script>
复制代码
scopedSlots
的用法这个Demo主要展现scopedSlots的用法,包括定义和使用。scopedSlots的template用法和解释参考vue-slot-scope。
组件wii-forth
<script>
export default {
name: 'wii-forth',
data() {
return {}
},
components: {
WiiScoped: {
name: 'wii-scoped',
props: {
message: String
},
render(createElement) {
// 等价于 <div><slot :text="message"></slot></div>
return createElement(
'div', [
this.$scopedSlots.default({
text: this.message
})
]
)
}
}
},
render: function(createElement) {
// 等价于
// <div style="margin-top: 15px;">
// <wii-scoped message="测试scopedSlots,我是传入的message">
// <span slot-scope="props">{{props.text}}</span>
// </wii-scoped>
// </div>
return createElement(
'div', {
style: {
marginTop: '15px'
}
}, [
createElement('wii-scoped', {
props: {
message: '测试scopedSlots,我是传入的message'
},
// 传递scopedSlots,经过props(自定义名称)取值
scopedSlots: {
default: function(props) {
return createElement('span', props.text)
}
}
})
]
)
}
}
</script>
复制代码
引入方法
<template>
<div id="app">
<wii-forth></wii-forth>
</div>
</template>
<script>
import WiiForth from './components/forth/index.vue'
export default {
name: 'app',
components: {
WiiForth
},
data() {
return {}
}
}
</script>
复制代码
写了这么多createElement,眼睛都花了,有的写起来也挺麻烦的。咱们试试来换个口味,试试JSX的写法。
工欲善其事,必先利其器。
npm install babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-helper-vue-jsx-merge-props babel-preset-env --save-dev
复制代码
安装完成之后,在.babelrc
文件配置"plugins": ["transform-vue-jsx"]
。
将webpack配置文件中的js解析部分改为test: /\.jsx?$/
表示对jsx的代码块进行解析。
这个示例作了以下功能:
父组件wii-jsx
<script type="text/jsx">
import WiiJsxItem from './item.vue'
export default {
name: 'wii-jsx',
components: {
WiiJsxItem
},
data() {
return {
color: 'red'
}
},
props: {
},
render: function (h) {
return (
<div class="wii-jsx"> <wii-jsx-item color={this.color} nativeOnClick={this.clickHandler}> <span>我是wii-jsx-item组件的slot, color经过变量传入: {this.color}</span> </wii-jsx-item> </div> ) }, methods: { clickHandler() { this.color = this.color == 'red' ? 'blue' : 'red' console.log(`点击了wii-jsx-item,经过native触发,改变了颜色为【${this.color}】`) } } } </script>
复制代码
子组件wii-jsx-item
该子组件在父组件中被引入,并用JSX的写法渲染。
export default {
name: 'wii-jsx-item',
data() {
return {}
},
props: {
color: String
},
render: function(createElement) {
// 等价于 <div class="wii-jsx-item"><slot></slot></div>
return createElement(
'div', {
class: 'wii-jsx-item',
style: {
color: this.color
}
},
this.$slots.default
)
},
methods: {
}
}
复制代码
引入方式
<template>
<div id="app"> <wii-jsx></wii-jsx> </div> </template>
<script>
import WiiJsx from './components/jsx/index.vue'
export default {
name: 'app',
components: {
WiiJsx
},
data() {
return {}
}
}
复制代码
JSX的主要转换仍是依靠咱们以前安装的babel插件,而JSX的事件以及属性的用法见babel插件的使用说明,这里面包含了vue里面事件和属性对应的用法说明。
下面来进行最后一个模块的介绍,函数式组件functional,这个东西的用法就见仁见智了,这里也没啥好的方案,只是给出了一些示例,各位大佬若是有一些具体的使用到的地方,阔以指点一下哇~thx~(害羞.jpg)。
官方文档的定义是functional组件须要的一切都是经过上下文传递,包括:
_在添加 _functional: true
以后,组件的render函数会增长第二个参数context(第一个是createElement),数据和节点经过context传递。
Tips:
在 2.3.0 以前的版本中,若是一个函数式组件想要接受 props,则props
选项是必须的。在 2.3.0 或以上的版本中,你能够省略props
选项,全部组件上的属性都会被自动解析为 props。
我我的的理解是:
Functional至关于一个纯函数同样,内部不存储用于在界面上展现的数据,传入什么,展现什么,传入的是相同的数据,展现的必然是相同的。无实例,无状态,没有this上下文,均经过context来控制。
优势:
由于函数式组件只是一个函数,因此渲染开销低不少。
使用场景:
接下来就经过两个组件来看看如何使用的吧,这里也仅仅只是示例而已,使用的场景仍在探索中,具体的使用场景还须要在开发过程当中根据需求复杂的和性能要求来酌情选择~
wii-functional
用在动画的functional这个Demo的做用是在输入框中输入字符,对数据列表进行筛选,筛选时加入显示和消失的动画。
组件主体
<script>
import Velocity from 'velocity-animate' // 这是一个动画库
export default {
name: 'wii-functional',
functional: true, //代表是函数式组件
render: function(createElement, context) {
// context是在functional: true时的参数
let data = {
props: {
tag: 'ul',
css: false
},
on: {
// 进入前事件
beforeEnter: function(el) {
el.style.opacity = 0
el.style.height = 0
},
// 进入事件
enter: function(el, done) {
let delay = el.dataset.index * 150
setTimeout(function() {
Velocity(el, {
opacity: 1,
height: '1.6em'
}, {
complete: done
})
}, delay)
},
// 离开事件
leave: function(el, done) {
let delay = el.dataset.index * 150
setTimeout(function() {
Velocity(el, {
opacity: 0,
height: 0
}, {
complete: done
})
}, delay)
}
}
}
return createElement('transition-group', data, context.children)
}
}
</script>
复制代码
上面这个组件至关于建立了一个ul-li
标签组成的vue动画,经过functional方式包裹到组件外部,能够做为通用的动画。
引入方式
<template>
<div id="app"> <input v-model="query"/> <wii-functional> <li v-for="(item, index) in computedList" :key="item.msg" :data-index="index"> {{item.msg}} </li> </wii-functional> </div> </template> <script> import WiiFunctional from './components/functional/index.vue' export default { name: 'app', components: { WiiFunctional }, data() { return { // 关键字 query: '', // 数据列表 list: [{ msg: 'Bruce Lee' }, { msg: 'Jackie Chan' }, { msg: 'Chuck Norris' }, { msg: 'Jet Li' }, { msg: 'Kung Furry' }, { msg: 'Chain Zhang' }, { msg: 'Iris Zhao' }, ] } }, computed:{ computedList: function() { var vm = this // 过滤出符合条件的查询结果 return this.list.filter(function(item) { return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1 }) } }, watch: { computedList(newVal, oldVal) { console.log(newVal) } } } </script> 复制代码
wii-choose-comp
用在组件切换的functional在这个示例中,经过props来切换加载不一样的组件,而且在props传递给子组件以前操做它,组件内部定义了click.native事件来展现示例。若是对一批组件进行一样的操做,则能够用这个functional,相似于加工厂。
固然若是组件须要不一样的点击事件或者表现方式也能够在各个组件内部单独写逻辑或者监听~由于wii-choose-comp
这个外壳本质不过就是个函数而已~
组件主体 wii-choose-comp
<script>
export default {
name: 'wii-choose-comp',
functional: true,
props: { // 2.3.0版本以上也能够不写props,会将组件属性默认绑定成props,为了统一标准仍是写上
componentName: String // 组件名
},
render: function(createElement, context) {
// 给组件加上class
context.data.class = [context.props.componentName]
// 在props传给子组件以前操做它
context.data.props = {
compName: context.props.componentName
}
context.data.nativeOn = {
click() {
alert('我是functional里面统一的点击事件')
}
}
return createElement(context.props.componentName, context.data, context.children)
}
}
</script>
复制代码
切换组件1 wii-comp-one
<script>
export default {
name: 'wii-comp-one',
props: {
compName: String
},
render: function(createElement) {
return createElement('div', [
'我是第一个comp, 我有点击效果, ',
`个人名字叫${this.compName}, `,
...this.$slots.default
])
}
}
</script>
复制代码
切换组件2 wii-comp-two
<script>
export default {
name: 'wii-comp-two',
props: {
compName: String
},
render: function(createElement) {
return createElement('div', [
'我是第二个comp, 点我试试呗, ',
`个人名字叫${this.compName}, `,
...this.$slots.default
])
}
}
</script>
复制代码
引入方式
<template>
<div id="app">
<button @click="changeComponent">点击切换组件</button>
<wii-choose-comp :component-name="componentName">
<span>我是{{componentName}}的slot</span>
</wii-choose-comp>
</div>
</template>
<script>
import WiiChooseComp from './components/functional/chooseComp.vue'
import WiiCompOne from './components/functional/comp1.vue'
import WiiCompTwo from './components/functional/comp2.vue'
export default {
name: 'app',
components: {
WiiChooseComp,
WiiCompOne,
WiiCompTwo
},
data() {
return {
componentName: 'wii-comp-one'
}
},
methods: {
changeComponent() {
this.componentName = this.componentName == 'wii-comp-one' ? 'wii-comp-two' : 'wii-comp-one'
}
}
}
</script>
复制代码
【Tips】 须要将待切换的组件所有引入到外层。(不造有没有更好的办法?)
以上就是最近对Vue Render的一个探索,由于对于公共组件库开发来讲,须要考虑的问题有不少,因此灵活性要求也更高,若是用Vue Render这种更接近编译的方式来编写组件库,可能会让逻辑更清晰,虽然不停的建立元素的写法是挺恶心的哈哈哈哈~~
接下来就是用来进行一下实战了,在实战的时候有什么坑就到时候再慢慢填咯~~