button组件
亮点:prop接收参数
<template>
<button :class="'i-button-size' + size" :disabled="disabled">
</button>
</template>
<script>
// 判断参数是否是其中之⼀
function oneOf (value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) { return true; }
}
return false;
}
export default {
props: {
size: {
validator (value)
{ return oneOf(value, ['small', 'large', 'default']); },
default: 'default' },
disabled: {
type: Boolean, default: false
} } }
</script>
使⽤组件:
<i-button size="large">
</i-button>
<i-button disabled></i-button>
mixins
如果你的项⽬⾜够复杂,或需要多⼈协同开发时,在 app.vue ⾥会 写⾮常多的代码,多到结构复杂难以维护。这时可以使⽤ Vue.js 的 混合 mixins,将不同的逻辑分开到不同的 js ⽂件⾥。
user.js
export default {
data () { return { userInfo: null } },
methods: {
getUserInfo(){
$.ajax('/user/info', (data) => { this.userInfo = data; }); } },
mounted () {
this.getUserInfo();
} }
然后在 app.vue 中混合: app.vue:
<script>
import mixins_user from '../mixins/user.js'
export default {
mixins: [mixins_user],
data() {
return {}
},
}
</script>
跟⽤户信息相关的逻辑,都可以在 user.js ⾥维护
$on 与 $emit
$emit 会在当前组件实例上触发⾃定义事件,并传递⼀些参数给监 听器的回调,⼀般来说,都是在⽗级调⽤这个组件时,使⽤ @on 的 ⽅式来监听⾃定义事件的,⽐如在⼦组件中触发事件:$on 监听了⾃⼰触发的⾃定义事件 test,因为有时不确定何时会触 发事件,⼀般会在 mounted 或 created 钩⼦中来监听。
子组件
methods: {
handleEmitEvent() {
this.$emit('test', 'Hello Vue.js')
},
},
父组件
mounted() {
this.$on('test', (text) => {
window.alert(text)
})
},
⾃⾏实现 dispatch 和 broadcast ⽅法
*思路:
在⼦组件调⽤ dispatch ⽅法,向上级指定的组件实例(最近 的)上触发⾃定义事件,并传递数据,且该上级组件已预先通 过 $on 监听了这个事件;
相反,在⽗组件调⽤ broadcast ⽅法,向下级指定的组件实例 (最近的)上触发⾃定义事件,并传递数据,且该下级组件已 预先通过 $on 监听了这个事件。*
该⽅法可能在很多组件中都会使⽤,复⽤起⻅,我们封装在混合(mixins)⾥。那它的使⽤样例可能是这样的:
有 A.vue 和 B.vue 两个组件,其中 B 是 A 的⼦组件,中间可能跨多级,在 A 中向 B 通信:
<!-- A.vue -->
<template><button @click="handleClick">触发事件</button></template>
<script>
import Emitter from '../mixins/emitter.js'
export default {
name: 'componentA',
mixins: [Emitter],
methods: {
handleClick() {
this.broadcast('componentB', 'on- message', 'Hello Vue.js')
},
},
}
</script>
// B.vue
export default {
name: 'componentB',
created () {
this.$on('on-message', this.showMessage);
},
methods: {
showMessage (text) { window.alert(text);
} } }
在独⽴组件 (库)⾥,每个组件的 name 值应当是唯⼀的,name 主要⽤于递归 组件
emitter.js
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name ===componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
} })
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent; if (parent) {
name = parent.$options.name;
} }
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
} },
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
} };
同理,如果是 B 向 A 通信,在 B 中调⽤ dispatch ⽅法,在 A 中使 ⽤ $on 监听事件即可。
因为是⽤作 mixins 导⼊,所以在 methods ⾥定义的 dispatch 和 broadcast ⽅法会被混合到组件⾥,⾃然就可以⽤ this.dispatch 和 this.broadcast 来使⽤。
这两个⽅法都接收了三个参数,第⼀个是组件的 name 值,⽤于向上 或向下递归遍历来寻找对应的组件,第⼆个和第三个就是上⽂分析的 ⾃定义事件名称和要传递的数据。 可以看到,在 dispatch ⾥,通过 while 语句,不断向上遍历更新当 前组件(即上下⽂为当前调⽤该⽅法的组件)的⽗组件实例(变量 parent 即为⽗组件实例),直到匹配到定义的 componentName 与 某个上级组件的 name 选项⼀致时,结束循环,并在找到的组件实例 上,调⽤ $emit ⽅法来触发⾃定义事件 eventName。broadcast ⽅法与之类似,只不过是向下遍历寻找。
*相⽐ Vue.js 1.x,有以下不同:
需要额外传⼊组件的 name 作为第⼀个参数;
⽆冒泡机制;
第三个参数传递的数据,只能是⼀个(较多时可以传⼊⼀个对 象),⽽ Vue.js 1.x 可以传⼊多个参数,当然,你对 emitter.js 稍作修改,也能⽀持传⼊多个参数,只是⼀般场景 传⼊⼀个对象⾜以*
组件的通信找到任意组件实例 ——findComponents 系列⽅法
是组件通信的终极⽅案。通过递归、遍历,找到指定组件的 name 选项 匹配的组件实例并返回。 findComponents 系列⽅法最终都是返回组件的实例,进⽽可以读 取或调⽤该组件的数据和⽅法
*它适⽤于以下场景:
由⼀个组件,向上找到最近的指定组件;
由⼀个组件,向上找到所有的指定组件;
由⼀个组件,向下找到最近的指定组件;
由⼀个组件,向下找到所有指定的组件;
由⼀个组件,找到指定组件的兄弟组件。*
utils/assits.js
// 由⼀个组件,向上找到最近的指定组件
//context 上下文
//componentName 组件名称
function findComponentUpward (context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
// 由⼀个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
let parents = []
const parent = context.$parent
if (parent) {
if (parent.$options.name === componentName) parents.push(parent)
return parents.concat(findComponentsUpward(parent, componentName))
} else {
return []
}
},
// 由⼀个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
//context.$children 得到的是当前组件的全部⼦组件
const childrens = context.$children
let children = null
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name
if (name === componentName) {
children = child
break
} else {
children = findComponentDownward(child, componentName)
if (children) break
}
}
}
return children
},
// 由⼀个组件,向下找到所有指定的组件
//使⽤ reduce 做累加器,为数组中的每一个元素依次执行回调函数,并 ⽤递归将找到的组件合并为⼀个数组并返回,
function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child)
const foundChilds = findComponentsDownward(child, componentName)
return components.concat(foundChilds)
}, [])
},
// 由⼀个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
//⽗组件的 全部⼦组件,这⾥⾯当前包含了本身
//exceptMe true是不包括自己
let res = context.$parent.$children.filter((item) => {
return item.$options.name === componentName
})
// Vue.js 在渲染组件时,都会给每个组件加⼀个内置的属 性 _uid,这个 _uid 是不会重复的,借此我们可以从⼀系列兄弟组 件中把⾃⼰排除掉
let index = res.findIndex((item) => item._uid === context._uid)
if (exceptMe) res.splice(index, 1)
return res
},
export { findComponentUpward,findComponentsUpward,findComponentDownward,findComponentsDownward,findBrothersComponents };
使用(A 是 B 的⽗组件)
<!-- component-a.vue -->
<template>
<div>组件 A <component-b></component-b></div>
</template>
<script>
import componentB from './component-b.vue'
import { findComponentDownward } from '../utils/assist.js';
export default {
name: 'componentA',
components: { componentB },
data() {
return { name: 'Aresn' }
},
methods: {
sayHello() {
console.log('Hello, Vue.js')
},
},
mounted(){
const comB = findComponentDownward(this, 'componentB');
if (comB) { console.log(comB.name);
}
}
</script>
<!-- component-b.vue -->
<template>
<div>组件 B</div>
</template>
<script>
import { findComponentUpward,findBrothersComponents } from '../utils/assist.js'
export default {
name: 'componentB',
mounted() {
const comA = findComponentUpward(this, 'componentA')
if (comA) {
console.log(comA.name) // Aresn
comA.sayHello() // Hello, Vue.js
}
const comsB = findBrothersComponents(this, 'componentB');
console.log(comsB); // ① [],空数组
//如果在 A 中再写⼀个 B:这时就会打印出 [VueComponent],有⼀个组件了
},
}
</script>
组合多选框组件—— CheckboxGroup & Checkbox
checkbox.vue
updateModel⽅法在 Checkbox ⾥的 mounted 初始化时调⽤。这 个⽅法的作⽤就是在 CheckboxGroup ⾥通过 findComponentsDownward ⽅法找到所有的 Checkbox,然后把 CheckboxGroup 的 value,赋值给 Checkbox 的 model,并根 据 Checkbox 的 label,设置⼀次当前 Checkbox 的选中状态。 这样⽆论是由内⽽外选择,或由外向内修改数据,都是双向绑定的, ⽽且⽀持动态增加 Checkbox 的数量。
<!-- checkbox.vue -->
<template>
<label>
<span>
<input
v-if="group"
type="checkbox"
:disabled="disabled"
:value="label"
v-model="model"
@change="change"
/>
<input
v-else
type="checkbox"
:disabled="disabled"
:checked="currentValue"
@change="change"
/>
</span>
<slot></slot>
</label>
</template>
<script>
import { findComponentUpward } from '../../utils/assist.js'
export default {
name: 'iCheckbox',
props: {
disabled: { type: Boolean, default: false },
value: { type: [String, Number, Boolean], default: false },
trueValue: { type: [String, Number, Boolean], default: true },
falseValue: { type: [String, Number, Boolean], default: false },
label: { type: [String, Number, Boolean] },
},
data() {
return {
currentValue: this.value,
model: [],
group: false,
parent: null,
}
},
methods: {
change(event) {
if (this.disabled) {
return false
}
const checked = event.target.checked
this.currentValue = checked
const value = checked ? this.trueValue : this.falseValue
this.$emit('input', value)
if (this.group) {
this.parent.change(this.model)
} else {
this.$emit('on-change', value)
this.dispatch('iFormItem', 'on-form-change', value)
}
},
updateModel() {
this.currentValue = this.value === this.trueValue
},
},
watch: {
value(val) {
if (val === this.trueValue || val === this.falseValue) {
this.updateModel()
} else {
throw 'Value should be trueValue or falseValue.'
}
},
},
mounted() {
this.parent = findComponentUpward(this, 'iCheckboxGroup')
if (this.parent) {
this.group = true
}
if (this.group) {
this.parent.updateModel(true)
} else {
this.updateModel()
}
},
}
</script>
checkbox-group.vue
<!-- checkbox-group.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script>
import { findComponentsDownward } from '../../utils/assist.js'
import Emitter from '../../mixins/emitter.js'
export default {
name: 'iCheckboxGroup',
mixins: [Emitter],
props: {
value: {
type: Array,
default() {
return []
},
},
},
data() {
return { currentValue: this.value, childrens: [] }
},
methods: {
updateModel(update) {
this.childrens = findComponentsDownward(this, 'iCheckbox')
if (this.childrens) {
const { value } = this
this.childrens.forEach((child) => {
child.model = value
if (update) {
child.currentValue = value.indexOf(child.label) >= 0
child.group = true
}
})
}
},
change(data) {
this.currentValue = data
this.$emit('input', data)
this.$emit('on-change', data)
this.dispatch('iFormItem', 'on-form-change', data)
},
},
mounted() {
this.updateModel(true)
},
watch: {
value() {
this.updateModel(true)
},
},
}
</script>
Vue的构造器extend 与⼿ 动挂载$mount
Vue 的$mount()为手动挂载,在项目中可用于延时挂载(例如在挂载之前要进行一些其他操作、判断等),之后要手动挂载上。new Vue时,el和$mount并没有本质上的不同。
创建⼀个 Vue 实例时,都会有⼀个选项 el,来指定 实例的根节点,如果不写 el 选项,那组件就处于未挂载状 态。Vue.extend 的作⽤,就是基于 Vue 构造器,创建⼀个“⼦ 类”,它的参数跟 new Vue 的基本⼀样,但 data 要跟组件⼀样, 是个函数,再配合 $mount ,就可以让组件渲染,并且挂载到任意 指定的节点上,⽐如 body。
import Vue from 'vue'
//创建了⼀个构造器,这个过程就可以解决异步获取 template 模板的问题
const AlertComponent = Vue.extend({
template: '<div>{{ message }}</div>',
data() {
return { message: 'Hello, Aresn' }
},
})
⼿动渲染组件,并把它挂载到 body 下:
const component = new AlertComponent().$mount();
//$mount ⽅法对组件进⾏了⼿动渲染,但它仅 仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的 component 已经是⼀个标准的 Vue 组件实例,因此它的 $el 属性 也可以被访问:
document.body.appendChild(component.$el);
$mount 也有⼀些快捷的挂载⽅式,以下两种都是可以的:
在 $mount ⾥写参数来指定挂载的节点
new AlertComponent().$mount('#app');
不⽤ $mount,直接在创建实例时指定 el 选项
new AlertComponent({ el: '#app' });
实现同样的效果,除了⽤ extend 外,也可以直接创建 Vue 实例, 并且⽤⼀个 Render 函数来渲染⼀个 .vue ⽂件
import Vue from 'vue'
import Notification from './notification.vue'
const props = {} // 这⾥可以传⼊⼀些组件的 props 选 项
const Instance = new Vue({
render(h) {
return h(Notification, { props: props })
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
渲染后, 操作 Render 的 Notification 实例
const notification = Instance.$children[0];
//因为 Instance 下只 Render 了 Notification ⼀个⼦组件,所以可以 ⽤ $children[0] 访问到。
⽤ $mount ⼿动渲染的组件,如果要销毁, 也要⽤ $destroy 来⼿动销毁实例,必要时,也可以⽤ removeChild 把节点从 DOM 中移除。
动态渲染 .vue ⽂件的组件—— Display
⼀个常规的 .vue ⽂件⼀般都会包含 3个部分:
<template>:组件的模板
<script>:组件的选项,不包含 el;
<style>:CSS 样式。
思路
1.⽗级传递 code 后,将其分割,并保存在 data 的 html、js、css
2.使⽤正则,基于 <> 和 </> 的特性进⾏分割:
utils/random_str.js
// ⽣成随机字符串
// 是从指定的 a-zA-Z0-9 中随机⽣成 32 位的字 符串。
export default function (len = 32) {
const $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV WXYZ1234567890';
const maxPos = $chars.length; let str = '';
for (let i = 0; i < len; i++) {
// Math.floor(Math.random() * maxPos) 0到32的整数
// charAt(int index) 方法用于返回指定索引处的字符
str += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return str;
},
组件display.vue
<!-- display.vue -->
<template>
<div ref="display"></div>
</template>
<script>
import Vue from 'vue'
import randomStr from '../../utils/random_str.js'
export default {
props: { code: { type: String, default: '' } },
data() {
return {
html: '',
js: '',
css: '',
component: null,
id: randomStr()
}
},
// 当 this.code 更新时,整个过程要重新来⼀次,所以要对 code 进 ⾏ watch 监听:
watch: {
code() {
this.destroyCode()
this.renderCode()
},
},
methods: {
getSource(source, type) {
const regex = new RegExp(`<${type}[^>]*>`)
let openingTag = source.match(regex)
if (!openingTag) return ''
else openingTag = openingTag[0]
return source.slice(
source.indexOf(openingTag) + openingTag.length,
source.lastIndexOf(`</${type}>`)
)
},
// getSource ⽅法接收两个参数: source:.vue ⽂件代码,即 props: code;
// type:分割的部分,也就是 template、script、style。
// 分割后,返回的内容不再包含 <template> 等标签,直接是对应的 内容,在 splitCode ⽅法中,把分割好的代码分别赋值给 data 中声 明的 html、js、css。
// 有两个细节需要注意: 1. .vue 的 <script> 部分⼀般都是以 export default 开始 的,可以看到在 splitCode ⽅法中将它替换为了 return,这 个在后⽂会做解释,当前只要注意,我们分割完的代码,仍然 是字符串; 2. 在分割的 <template> 外层套了⼀个 <div id="app">,这 是为了容错,有时使⽤者传递的 code 可能会忘记在外层包⼀ 个节点,没有根节点的组件,是会报错的。
splitCode() {
const script = this.getSource(this.code, 'script').replace(
/export default/,
'return '
)
const style = this.getSource(this.code, 'style')
const template =
'<div id="app">' + this.getSource(this.code, 'template') + '</div>'
this.js = script
this.css = style
this.html = template
},
renderCode() {
this.splitCode()
if (this.html !== '' && this.js !== '') {
// new Function ([arg1[, arg2[, ...argN]],] functionBody)
// arg1, arg2, ... argN 是被函数使⽤的参数名称,functionBody 是 ⼀个含有包括函数定义的 JavaScript 语句的字符串。也就是说,示 例中的字符串 return a + b 被当做语句执⾏了。
// 当前的 this.js 是字符串, ⽽ extend 接收的选项可不是字符串,⽽是⼀个对象类型,那就要先 把 this.js 转为⼀个对象
// const sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 6)); // 8
// new Function eval 函数也可以使⽤
// this.js 中是将 export default 替换为 return 的,如 果将 this.js 传⼊ new Function ⾥,那么 this.js 就执⾏了,这时 因为有 return,返回的就是⼀个对象类型的 this.js 了
const parseStrToFunc = new Function(this.js)()
parseStrToFunc.template = this.html
const Component = Vue.extend(parseStrToFunc)
this.component = new Component().$mount()
// extend 构造的实例通过 $mount 渲染后,挂载到了组件唯⼀的⼀ 个节点 <div ref="display"> 上。
this.$refs.display.appendChild(this.component.$el)
if (this.css !== '') {
const style = document.createElement('style')
style.type = 'text/css'
// 创建⼀个 <style> 标签,然后把 css 写进去,再插⼊到⻚ ⾯的 <head> 中,这样 css 就被浏览器解析了。为了便于后⾯在 this.code 变化或组件销毁时移除动态创建的 <style> 标签,我 们给每个 style 标签加⼀个随机 id ⽤于标识。
style.id = this.id
style.innerHTML = this.css
document.getElementsByTagName('head')[0].appendChild(style)
}
}
},
// 当 Display 组件销毁时,也要⼿动销毁 extend 创建的实例以及上 ⾯的 css:
destroyCode() {
const $target = document.getElementById(this.id)
if ($target) $target.parentNode.removeChild($target)
if (this.component) {
this.$refs.display.removeChild(this.component.$el)
this.component.$destroy()
this.component = null
}
},
},
mounted() {
this.renderCode()
},
beforeDestroy() {
this.destroyCode()
},
}
</script>
使⽤
新建⼀条路由,并在 src/views 下新建⻚⾯ display.vue 来使 ⽤ Display 组件
src/views/display.vue
<!-- src/views/display.vue -->
<template>
<div>
<h3>动态渲染 .vue ⽂件的组件—— Display</h3>
<i-display :code="code"></i-display>
</div>
</template>
<script>
import iDisplay from '../components/display/display.vue'
import defaultCode from './default-code.js'
export default {
components: { iDisplay },
data() {
return { code: defaultCode }
},
}
</script>
// src/views/default-code.js
const code = `<template>
<div>
<input v-model="message"> {{ message }}
</div>
</template>
<script> export default { data () { return { message: '' } } }
</script>`;
export default code;
如果使⽤的是 Vue CLI 3 默认的配置,直接运⾏时,会抛出下⾯的 错误:
*这涉及到另⼀个知识点,就是 Vue.js 的版本。
在使⽤ Vue.js 2 时,有独⽴构建(standalone)和运⾏时构建(runtime-only)
两 种版本可供选择,
Vue CLI 3 默认使⽤了 vue.runtime.js,
它不允许编译 template 模板,
因为我们在 Vue.extend 构造实例时,
⽤了 template 选 项,所以会报错。
解决⽅案有两种,⼀是⼿动将 template 改写为 Render 函数,但这成本太⾼;
另⼀种是对 Vue CLI 3 创建的⼯程做 简单的配置。我们使⽤后者。*
在项⽬根⽬录,新建⽂件 vue.config.js:
module.exports = { runtimeCompiler: true };
*它的作⽤是,是否使⽤包含运⾏时编译器的 Vue 构建版本。设置为 true 后就可以在 Vue 组件中使⽤ template 选项了,
这个⼩⼩的 Display 组件,能做的事还有很多,⽐如要写⼀套 Vue 组件库的⽂档,传统⽅法是在开发环境写⼀个个的 .vue ⽂件,然后 编译打包、上传资源、上线,如果要修改,哪怕⼀个标点符号,都要 重新编译打包、上传资源、上线。有了 Display 组件,只需要提供 ⼀个服务来在线修改⽂档的 .vue,就能实时更新,不⽤打包、上 传、上线*,
全局提示组件—— $Alert
this.$Alert 可以在任何位置调⽤,⽆需单独引⼊。
该⽅法接收两 个参数:
content:提示内容;
duration:持续时间,单位秒,默认 1.5 秒,到时间⾃动消 失
Alert 组件不同于常规的组件使⽤⽅式,它最终是通过 JS 来调⽤ 的,因此组件不⽤预留 props 和 events 接⼝
在 src/component 下新建 alert ⽬录,并创建⽂件 alert.vue
<template>
<div class="alert">
<div class="alert-main" v-for="item in notices" :key="item.name">
<div class="alert-content">{{ item.content }}</div>
</div>
</div>
</template>
<script>
let seed = 0
function getUuid() {
return 'alert_' + seed++
}
// JS 调⽤ Alert 的⼀个⽅法 add,并 将 content 和 duration 传⼊进来:
export default {
data() {
// 通知可以是多个,我们⽤⼀个数组 notices 来管理每条通知
return { notices: [] }
},
// 在 add ⽅法中,给每⼀条传进来的提示数据,加了⼀个不重复的 name 字段来标识,并通过 setTimeout 创建了⼀个计时器,当到 达指定的 duration 持续时间后,调⽤ remove ⽅法,将对应 name 的那条提示信息找到,并从数组中移除。 由这个思路,Alert 组件就可以⽆限扩展,只要在 add ⽅法中传递 更多的参数,就能⽀持更复杂的组件,⽐如是否显示⼿动关闭按钮、 确定 / 取消按钮,甚⾄传⼊⼀个 Render 函数都可以,完成本例 后,
methods: {
add(notice) {
const name = getUuid()
let _notice = Object.assign({ name: name }, notice)
this.notices.push(_notice)
// 定时移除,单位:秒
const duration = notice.duration
setTimeout(() => {
this.remove(name)
}, duration * 1000)
},
remove(name) {
const notices = this.notices
for (let i = 0; i < notices.length; i++) {
if (notices[i].name === name) {
this.notices.splice(i, 1)
break
}
}
},
},
}
</script>
<style>
.alert {
position: fixed;
width: 100%;
top: 16px;
left: 0;
text-align: center;
pointer-events: none;
}
.alert-content {
display: inline-block;
padding: 8px 16px;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
margin-bottom: 8px;
}
</style>
对 Alert 组件进⼀步封装,让它能够实例化,⽽不是常 规的组件使⽤⽅法
使⽤ Vue.extend 或 new Vue,然后⽤ $mount 挂载到 body 节点下。
notification.js 并不是最终的⽂件,它只是对 alert.vue 添加了⼀个 ⽅法 newInstance。虽然 alert.vue 包含了 template、script、 style 三个标签,并不是⼀个 JS 对象,那怎么能够给它扩展⼀个⽅法 newInstance 呢?事实上,alert.vue 会被 Webpack 的 vue- loader 编译,把 template 编译为 Render 函数,最终就会成为⼀ 个 JS 对象,⾃然可以对它进⾏扩展。
Alert 组件没有任何 props,这⾥在 Render Alert 组件时,还是给 它加了 props,当然,这⾥的 props 是空对象 {},⽽且即使传了内 容,也不起作⽤。这样做的⽬的还是为了扩展性,如果要在 Alert 上 添加 props 来⽀持更多特性,是要在这⾥传⼊的。不过话说回来, 因为能拿到 Alert 实例,⽤ data 或 props 都是可以的。
notification.js
import Alert from './alert.vue'
import Vue from 'vue'
Alert.newInstance = (properties) => {
const props = properties || {}
const Instance = new Vue({
data: props,
render(h) {
return h(Alert, { props: props })
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
const alert = Instance.$children[0]
return {
add(noticeProps) {
alert.add(noticeProps)
},
remove(name) {
alert.remove(name)
},
}
}
export default Alert
⼊⼝
最后要做的,就是调⽤ notification.js 创建实例,并通过 add 把数 据传递过去,这是组件开发的最后⼀步,也是最终的⼊⼝。
在 src/component/alert 下创建⽂件
alert.js:
// alert.js
import Notification from './notification.js'
let messageInstance
// getMessageInstance 函数⽤来获取实例,它不会重复创建,如 果 messageInstance 已经存在,就直接返回了,只在第⼀次调⽤ Notification 的 newInstance 时来创建实例。
function getMessageInstance() {
messageInstance = messageInstance || Notification.newInstance()
return messageInstance
}
function notice({ duration = 1.5, content = '' }) {
let instance = getMessageInstance()
instance.add({ content: content, duration: duration })
}
export default {
info(options) {
return notice(options)
},
}
// alert.js 对外提供了⼀个⽅法 info,如果需要各种显示效果,⽐如 成功的、失败的、警告的,可以在 info 下⾯提供更多的⽅法,⽐如 success、fail、warning 等,并传递不同参数让 Alert.vue 知道显 示哪种状态的图标。本例因为只有⼀个 info,事实上也可以省略 掉,直接导出⼀个默认的函数,这样在调⽤时,就不⽤ this.$Alert.info() 了,直接 this.$Alert()。
把 alert.js 作为插件注册到 Vue ⾥就⾏,在⼊⼝⽂件 src/main.js中,通过 prototype 给 Vue 添加⼀个实例⽅法:
src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Alert from '../src/components/alert/alert.js'
Vue.config.productionTip = false
Vue.prototype.$Alert = Alert
new Vue({ router, render: h => h(App) }).$mount('#app')
这样在项⽬任何地⽅,都可以通过 this.$Alert 来调⽤ Alert 组 件了
src/views/alert.vue
<template>
<div>
<button @click="handleOpen1">打开提示 1</button>
<button @click="handleOpen2">打开提示 2</button>
</div>
</template>
<script>
export default {
methods: {
handleOpen1() {
this.$Alert.info({ content: '我是提示信息 1' })
},
handleOpen2() {
this.$Alert.info({ content: '我是提示信息 2', duration: 3 })
},
},
}
</script>
是同类组件中值得注意的:
1. Alert.vue 的最外层是有⼀个 .alert 节点的,它会在第⼀次调 ⽤ $Alert 时,在 body 下创建,因为不在 <router-view> 内,
它不受路由的影响,也就是说⼀经创建,除⾮刷新⻚⾯,
这个节点是不会消失的,所以在 alert.vue 的设计中,并没有 主动销毁这个组件,
⽽是维护了⼀个⼦节点数组 notices。
2. .alert 节点是 position: fixed 固定的,因此要合理设计它 的 z-index,否则可能被其它节点遮挡。
3. notification.js 和 alert.vue 是可以复⽤的,如果还要开发其 它同类的组件,⽐如⼆次确认组件 $Confirm,
只需要再写⼀ 个⼊⼝ confirm.js,并将 alert.vue 进⼀步封装,将 notices 数组的循环体写为⼀个新的组件,
通过配置来决定是 渲染 Alert 还是 Confirm,这在可维护性上是友好的。
4. 在 notification.js 的 new Vue 时,使⽤了 Render 函数来渲 染 alert.vue,
这是因为使⽤ template 在 runtime 的 Vue.js 版本下是会报错的。
5. 本例的 content 只能是字符串,如果要显示⾃定义的内容,除 了⽤ v-html 指令,也能⽤ Functional Render。
结语Vue.js 的精髓是组件,组件的精髓是 JavaScript。将 JavaScript 开 发中的技巧结合 Vue.js 组件,就能玩出不⼀样的东⻄。
更灵活的组件:Render 函数与 Functional
Render Render 函数 返回的是⼀个 JS
对象,没有传统 DOM 的层级关系,配合上 if、 else、for 等语句,将节点拆分成不同JS对象再组装。
template 和 Render 写法的对照:
<template>
<div id="main" class="container" style="color: red">
<p v-if="show">内容 1</p>
<p v-else>内容 2</p>
</div>
</template>
<script>
export default {
data() {
return { show: false }
},
}
</script>
Render
export default {
data() {
return { show: false }
},
render: (h) => {
let childNode
if (this.show) {
childNode = h('p', '内容 1')
} else {
childNode = h('p', '内容 2')
}
return h(
'div',
{
attrs: { id: 'main' },
class: { container: true },
style: { color: 'red' },
},
[childNode]
)
},
}
这⾥的 h,即 createElement,是 Render 函数的核⼼。
可以看 到,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替 代了,
那 v-for ⾃然也会被 for 语句替代。
h 有 3 个参数,分别是:
- 要渲染的元素或组件,可以是⼀个 html 标签、组件选项或⼀ 个函数(不常⽤),该参数为必填项。示例:
// 1. html 标签
h('div');
// 2. 组件选项
import DatePicker from '../component/date- picker.vue'; h(DatePicker);
- 对应属性的数据对象,⽐如组件的 props、元素的 class、绑 定的事件、slot、⾃定义指令等,
该参数是可选的,上⽂所说 的 Render 配置项多,指的就是这个参数。
该参数的完整配置 和示例,可以到 Vue.js 的⽂档查看,没必要全部记住,
⽤到 时查阅就好:createElement 参数 (https://cn.vuejs.org/v2/guide... function.html#createElement-参数)。
- ⼦节点,可选,String 或 Array,它同样是⼀个 h。示例:
[
'内容', h('p', '内容'),
h(Component, { props: { someProp: 'foo' } })
]
所有的组件树中,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须唯⼀
重复渲染多个组件或元素,可以通过⼀个循环和⼯⼚函数来解决:
const Child = {
render: (h) => {
return h('p', 'text')
},
}
export default {
render: (h) => {
const children = Array.apply(null, { length: 5 }).map(() => {
return h(Child)
})
return h('div', children)
},
}
对于含有组件的 slot,复⽤⽐较复杂,需要将 slot 的每个⼦节点都 克隆⼀份,例如:
{
render: (h) => {
function cloneVNode(vnode) {
//递归遍历所有⼦节点,并克隆
const clonedChildren =
vnode.children && vnode.children.map((vnode) => cloneVNode(vnode))
const cloned = h(vnode.tag, vnode.data, clonedChildren)
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 === undefined ? [] : this.$slots.default
const clonedVNodes =
this.$slots.default === undefined
? []
: vNodes.map((vnode) => cloneVNode(vnode))
return h('div', [vNodes, clonedVNodes])
}
}
- 在 Render 函数⾥创建了⼀个 cloneVNode 的⼯⼚函数,通过递归 将 slot 所有⼦节点都克隆了⼀份,并对 VNode 的关键属性也进⾏ 了复制
深度克隆 slot 并⾮ Vue.js 内置⽅法,
在⼀些特殊的场景才会使⽤到,正常业务⼏乎是⽤不到的。⽐如 iView 组件库的穿梭框组件 Transfer,就⽤到了这种⽅法:
slot 是⼀个 Refresh 按钮,使⽤者只写了⼀遍,但在 Transfer 组件中,是通过克隆 VNode 的⽅法,显示了两遍。
如果 不这样做,就要声明两个具名 slot,但是左右两个的逻辑可能是完 全⼀样的,使⽤者就要写两个⼀模⼀样的 slot,这是不友好的*
Render 函数的基本⽤法还有很多,⽐如 v-model 的⽤ 法、事件和修饰符、slot 等,读者可以到 Vue.js ⽂档阅 读。Vue.js 渲染函数 (https://cn.vuejs.org/v2/guide... function.html)
Render 函数使⽤场景
⼀般情况下是不推荐直接使⽤ Render 函数的,使⽤ template ⾜以,在 Vue.js 中,使⽤ Render 函数的场景,主要有 以下 4 点
- 使⽤两个相同 slot。在 template 中,Vue.js 不允许使⽤两个 相同的 slot,⽐如下⾯的示例是错误的:
<template>
<div>
<slot></slot>
<slot></slot>
</div>
</template>
解决⽅案就是上⽂中讲到的约束,使⽤⼀个深度克隆 VNode 节点的⽅法
- 在 SSR 环境(服务端渲染),如果不是常规的 template 写 法,⽐如通过 Vue.extend 和 new Vue 构造来⽣成的组件实 例,是编译不过的,使⽤ Render 函 数来渲染
- 在 runtime 版本的 Vue.js 中,如果使⽤ Vue.extend ⼿动构 造⼀个实例,使⽤ template 选项是会报错的,解决⽅案也很简单,把 template 改写为 Render 就可以了。需要注意的是,在开发独⽴组件时,可以通过配置 Vue.js 版本来使 template 选项可⽤,但这是在⾃⼰的环境, ⽆法保证使⽤者的 Vue.js 版本,所以对于提供给他⼈⽤的组 件,是需要考虑兼容 runtime 版本和 SSR 环境的。
- 这可能是使⽤ Render 函数最重要的⼀点。⼀个 Vue.js 组 件,有⼀部分内容需要从⽗级传递来显示,如果是⽂本之类 的,直接通过 props 就可以,如果这个内容带有样式或复杂 ⼀点的 html 结构,可以使⽤ v-html 指令来渲染,⽗级传递 的仍然是⼀个 HTML Element 字符串,不过它仅仅是能解析 正常的 html 节点且有 XSS ⻛险。当需要最⼤化程度⾃定义显 示内容时,就需要 Render 函数,它可以渲染⼀个完整的 Vue.js 组件。你可能会说,⽤ slot 不就好了?的确,slot 的 作⽤就是做内容分发的,但在⼀些特殊组件中,可能 slot 也不 ⾏。⽐如⼀个表格组件 Table,它只接收两个 props:列配置 columns 和⾏数据 data,不过某⼀列的单元格,不是只将数 据显示出来那么简单,可能带有⼀些复杂的操作,这种场景只 ⽤ slot 是不⾏的,没办法确定是那⼀列的 slot。这种场景有两 种解决⽅案,其⼀就是 Render 函数,另⼀种是⽤作⽤域 slot(slot- scope)
Functional Render
*Vue.js 提供了⼀个 functional 的布尔值选项,设置为 true 可以 使组件⽆状态和⽆实例,也就是没有 data 和 this 上下⽂。这样⽤ Render 函数返回虚拟节点可以更容易渲染,因为函数化组件 (Functional Render)只是⼀个函数,渲染开销要⼩很多。
使⽤函数化组件,Render 函数提供了第⼆个参数 context 来提供临 时上下⽂。组件需要的 data、props、slots、children、parent 都 是通过这个上下⽂来传递的,⽐如 this.level 要改写为 context.props.level,this.$slots.default 改写为 context.children,您可以阅读 Vue.js ⽂档—函数式组件 (https://cn.vuejs.org/v2/guide... function.html#函数式组件) 来查看示例。
函数化组件在业务中并不是很常⽤,⽽且也有类似的⽅法来实现,⽐ 如某些场景可以⽤ is 特性来动态挂载组件。函数化组件主要适⽤于 以下两个场景*
1.程序化地在多个组件中选择⼀个;
2.在将 children、props、data 传递给⼦组件之前操作它们。
某个组件需要使⽤ Render 函数来⾃定义,⽽不是 通过传递普通⽂本或 v-html 指令,这时就可以⽤ Functional Render,来看下⾯的示例:
⾸先创建⼀个函数化组件 render.js:
// 它只定义了⼀个 props:render,格式为 Function,因为是 functional,所以在 render ⾥使⽤了第⼆个参数 ctx 来获取 props。这是⼀个中间⽂件,并且可以复⽤,其它组件需要这 个功能时,都可以引⼊它
// render.js
export default {
functional: true,
props: { render: Function },
render: (h, ctx) => {
return ctx.props.render(h)
},
}
创建组件:
<!-- my-component.vue -->
<template>
<div><Render :render="render"></Render></div>
</template>
<script>
import Render from './render.js'
export default {
components: { Render },
props: { render: Function }
}
</script>
使⽤上⾯的 my-compoennt 组件:
<template>
<div>
<my-component :render="render"></my-component>
</div>
</template>
<script>
import myComponent from '../components/my-component.vue'
export default {
components: { myComponent },
data() {
return {
render: (h) => {
return h('div', { style: { color: 'red' } }, '⾃定义内容')
},
}
},
}
</script>
这⾥的 render.js 因为只是把 demo.vue 中的 Render 内容过继, 并⽆其它⽤处,所以⽤了 Functional Render。 就此例来说,完全可以⽤ slot 取代 Functional Render,那是因为 只有 render 这⼀个 prop。如果示例中的 <Render> 是⽤ v-for ⽣成的,也就是多个时,⽤ ⼀个 slot 是实现不了的,那时⽤Render 函数就很⽅便了.
实战 5:可⽤ Render ⾃定义列 的表格组件——Table
*正规的表格,是由 <table>、<thead>、<tbody>、<tr>、<th>、<td> 这些标签 组成,⼀般分为表头 columns 和数据 data。
⽀持使⽤ Render 函数来⾃定义某⼀列*。
写⼀个个的 table 系列标签是很麻烦并且重复的,⽽组件的好处就是省去这些基础的⼯作,我们直接给 Table 组件传递列的配置columns 和⾏数据 data,其余的都交给 Table 组件做了。
**columns:列配置,格式为数组,其中每⼀列 column 是⼀个
对象,⽤来描述这⼀列的信息,它的具体说明如下:**
title:列头显示⽂字;
key:对应列内容的字段名;
render:⾃定义渲染列,使⽤ Vue 的 Render 函数,不
定义则直接显示为⽂本。
*column 定义的 key 值,与 data 是⼀⼀对应的,这是⼀种常⻅的
数据接⼝定义规则,*
**data:显示的结构化数据,格式为数组,其中每⼀个对象,就
是⼀⾏的数据,**
先来完成⼀个基础的表格组件,之后再接⼊ Render 来配置⾃定
义列。
在 src/components ⽬录下新建 table-render ⽬录,并创建
table.vue ⽂件:
<template>
<table>
<thead>
<tr>
<th v-for="(col,i) in columns" :key="i">
{{col.title}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in data" :key="index">
<td v-for="(col,inx) in columns" :key="inx">
<template v-if="'render' in col">
<Render :row="row" :column="col" :index="rowIndex" :render="col.render">
</Render>
</template>
<template v-else>
{{row[col.key]}}
</template>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Render from './render.js'
export default {
props:{
columns:{
type:Array,
default(){
return []
}
},
data:{
type:Array,
default(){
return[]
}
}
},
components:{
Render
}
}
</script>
<style lang="scss" scoped>
table{
width:100%;
border-collapse:collapse;
border-spacing:0;
empty-cells:show;//隐藏表格中空单元格上的边框和背景:
border:1px solid #e9e9e9;
}
table th{
background: #f7f7f7;
color:#5c6b77;
font-weight:600;
white-space:nowrap;//段落中的文本不进行换行
}
table td, table th{
padding:8px 16px;
border: 1px solid #e9e9e9;
text-align:center;
}
</style>
**新建路由 table-render,并在 src/views/ ⽬录下新建⻚⾯
table-render.vue**
*如果 columns 中的某⼀列配置了 render 字段,那就通过
render.js 完成⾃定义模板,否则以字符串形式渲染。⽐如对出⽣⽇
期这列显示为标准的⽇期格式,可以这样定义 column:*
<template>
<div>
<table-render :columns="columns" :data="data"></table-render>
</div>
</template>
<script>
// 整⾏数据编辑的功能。
// 操作这⼀列,默认是⼀个修改按钮,点击后,变为保存和取消两个按
// 钮,同时本⾏其它各列都变为了输⼊框,并且初始值就是刚才单元格
// 的数据。变为输⼊框后,可以任意修改单元格数据,点击保存按钮保
// 存整⾏数据,点击取消按钮,还原⾄修改前的数据。
// 当进⼊编辑状态时,每⼀列的输⼊框都要有⼀个临时的数据使⽤ vmodel 双向绑定来响应修改,所以在 data ⾥再声明四个数据:
import TableRender from './receive'
export default {
components: {
TableRender,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
// editIndex 默认给了 -1,也就是⼀个不存在的⾏号,当点击修改 按钮时,再将它置为正确的⾏号。
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
// 除编辑列,其它各数据列都有两种状态:
// 1. 当 editIndex 等于当前⾏号 index 时,呈现输⼊框状态;
// 2. 当 editIndex 不等于当前⾏号 index 时,呈现默认数据。
// edit 根据 editIndex 呈现不同的节点,还是先看 else,直接
// 显示了对应字段的数据。在聚焦时(this.editIndex ===
// index),渲染⼀个 input 输⼊框,初始值 value 通过 render 的
// domProps 绑定了 row.name(这⾥也可绑定 editName),并监
// 听了 input 事件,将输⼊的内容,实时缓存在数据 editName 中,
// 供保存时使⽤。事实上,这⾥绑定的 value 和事件 input 就是语法
// 糖 v-model 在 Render 函数中的写法,在 template 中,经常写作
// <input v-model="editName">。
{
title: '姓名',
key: 'name',
render:(h,{row,index})=>{
let edit;
// 当前行为聚焦行时
if(this.editIndex == index){
edit = [h('input',{
domProps:{
value:row.name
},
on:{
input:(event)=>{
this.editName = event.target.value
}
}
})]
}else{
edit = row.name
}
return h('div',[edit])
}
},
{
title: '年龄',
key: 'age',
render:(h,{row,index})=>{
let editAge;
if(this.editIndex == index){
editAge = [h('input',{
domProps:{
value: row.age
},
on:{
input:(event)=>{
this.editAge = event.target.value
}
}
})]
}else{
editAge = row.age
}
return h('div',[editAge])
}
},
{
title: '出⽣⽇期',
key: 'birthday',
render: (h, { row, column, index }) => {
let editBirth;
if(this.editIndex == index){
editBirth = [h('input',{
domProps:{
value:row.birthday
},
on:{
input:(event)=>{
this.editBirthday = event.target.value
}
}
})]
}else{
const date = new Date(parseInt(row.birthday))
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
editBirth = `${year}-${month}-${day}`
}
return h('div', [editBirth])
},
},
{
title: '地址',
key: 'address',
render:(h,{row,index})=>{
let editAddress;
if(this.editIndex ==index){
editAddress=[h('input',{
domProps:{
value:row.address
},
on:{
input:(event)=>{
this.editAdress = event.target.value
}
}
})]
}else{
editAddress = row.address
}
return h('div',[editAddress])
}
},
// 先定义操作列的 render:
// 为默认是⾮
// 编辑状态,
// 也就是说 editIndex 还是 -1。当点击修改按钮时,把 render 中
// 第⼆个参数 { row } 中的各列数据赋值给了之前在 data 中声明的
// 4 个数据,这样做是因为之后点击取消按钮时,editName 等值已经
// 修改了,还没有还原,所以在开启编辑状态的同时,初始化各输⼊框
// 的值(当然也可以在取消时重置)。最后再把 editIndex 置为了对
// 应的⾏序号 { index },此时 render 的 if 条件
// this.editIndex === index 为真,编辑列变成了两个按钮:保
// 存和取消。点击保存,直接修改表格源数据 data 中对应的各字段
// 值,并将 editIndex 置为 -1,退出编辑状态;点击取消,不保存源
// 数据,直接退出编辑状态。
{
title: '操作',
render:(h,{row,index})=>{
//如果当前行是编辑状态,则渲染两个按钮
if(this.editIndex === index){
return [
h('button',{
on:{
click:()=>{
this.data[index].name = this.editName
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birthday = this.editBirthday
this.editIndex = -1
}
}
},'保存'),
h('button',{
style:{
marginLeft:'6px'
},
on:{
click:()=>{
this.editIndex = -1
}
}
},'取消')
]
}else{
// 当前行是默认状态,渲染为一个按钮
return h('button',{
on:{
click:()=>{
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
}
}
},'修改')
}
}
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
}
</script>
<style lang="scss" scoped></style>
*columns ⾥定义的 render,是有两个参数的,第⼀ 个是 createElement(即 h),
第⼆个是从 render.js 传过来的对象,
它包含了当前⾏数据(row)、当前列配置(column)、当前
是第⼏⾏(index),使⽤者可以基于这 3 个参数得到任意想要的
结果。由于是⾃定义列了,显示什么都是使⽤者决定的,因此在使⽤
了 render 的 column ⾥可以不⽤写字段 key 。*
*columns ⾥定义的 render 字段,它
仅仅是名字叫 render 的⼀个普通函数,并⾮ Vue.js 实例的 render
选项,只是我们恰巧把它叫做 render ⽽已,如果愿意,也可以改为
其它名字,⽐如 renderRow。真正的 Render 函数只有⼀个地⽅,
那就是 render.js 中的 render 选项,只是它代理了 column 中的
render*。
*显示正常的⽇期 写⼀个计算属性(computed) 但对于操作这⼀列就不可取了,因为它带有业
务逻辑,点击编辑按钮,是可以对当前⾏数据进⾏修改的。这时就要 ⽤到 Render 函数。*
先在 src/components/table-render ⽬录下新建 render.js ⽂件
*使⽤ Render ⾃定义列模板
函数式组件 Functional Render 的⽤法,它 没有状态和上下⽂,主要⽤于中转⼀个组件*
// render.js 定义了 4 个 props:
// row:当前⾏的数据;
// column:当前列的数据;
// index:当前是第⼏⾏;
// render:具体的 render 函数内容。
// 这⾥的 render 选项并没有渲染任何节点,⽽是直接返回 props 中
// 定义的 render,并将 h 和当前的⾏、列、序号作为参数传递出去。
// 然后在 table.vue ⾥就可以使⽤ render.js 组件:
export default {
functional: true,
props:{
row:Object,
column:Object,
index:Number,
render:Function
},
render:(h,ctx)=>{
const params = {
row:ctx.props.row,
column:ctx.props.column,
index:ctx.props.index
};
return ctx.props.render(h,params)
}
}
*⼀个完整的 Table 组件功能要复杂的多,⽐如排序、筛选、列
固定、表头固定、表头嵌套等*
**很多 Vue.js 的开发难题,都可以⽤ Render 函数来解决,
它⽐ template 模板更灵活,可以完全发挥 JavaScript 的编程能
⼒,因此很多 JS 的开发思想都可以借鉴。**
实战 6:可⽤ slot-scope ⾃定 义列的表格组件——Table
Render 函数能够完全发挥 JavaScript 的编程能⼒,实
现⼏乎所有的⾃定义⼯作,但本质上,使⽤者写的是⼀个庞⼤的 JS
对象,它不具备 DOM 结构,可读性和可维护性都⽐较差。实现⼀种达到同样渲染效果,但对使⽤者更友好的 slot-scope
写法。
常规的 slot ⽆法实现对组件循环体的每⼀项进⾏不同的内容分发,
这就要⽤到 slot-scope,它本质上跟 slot ⼀样,只不过可以传递参
数。
<ul>
<li v-for="book in books" :key="book.id">
<slot :book="book">
{{book.name}}
</slot>
</li>
</ul>
*在 slot 上,传递了⼀个⾃定义的参数 book,它的值绑定的是当前
循环项的数据 book,这样在⽗级使⽤时,就可以在 slot 中访问它
了:*
<table-list>
<template slot-scope="slotProps">
<span v-if="slotProps.book.scale">限时优惠</span>
{{slotProps.book.name}}
</template>
</table-list>
*除了可以传递参数,其它⽤法跟 slot 是⼀样的,⽐如也可以“具
名”:使⽤ ES6 的解构,能让参数使⽤起来更⽅便*
<slot :book="book" name="book">
{{ book.name }}
</slot>
// ES6 的解构
<template slot-scope="{ book }" slot="book">
<span v-if="book.sale">限时优惠</span>
{{ book.name }}
</template>
*这就是作⽤域 slot(slot-scope),能够在组件的循环体中做内容
分发,有了它,Table 组件的⾃定义列模板就不⽤写⼀⻓串的
Render 函数了,slot-scope作用域插槽实现了父组件调用子组件内部的数据,子组件的数据通过slot-scope属性传递到了父组件*
⽤ 3 种⽅法来改写 Table,实现 slot-scope ⾃定义列模板
第⼀种⽅案,
⽤最简单的 slot-scope 实现,同时也兼容 Render 函
数的旧⽤法。拷⻉上⼀节的 Table 组件⽬录,更名为 tableslot,同时也拷⻉路由,更名为 table-slot.vue。为了兼容旧
的 Render 函数⽤法,在 columns 的列配置 column 中,新增⼀个
字段 slot 来指定 slot-scope 的名称:
<!-- src/components/table-slot/table.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="(col,i) in columns" :key="i">
{{col.title}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in data" :key="index">
<td v-for="(col,inx) in columns" :key="inx">
<template v-if="'render' in col">
<Render :row="row" :column="col" :index="rowIndex" :render="col.render">
</Render>
</template>
<template v-else-if="'slot' in col">
<slot :index="rowIndex" :name="row.slot" :column="col">
</slot>
</template>
<template v-else>
{{row[col.key]}}
</template>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Render from './render.js'
export default {
props:{
columns:{
type:Array,
default(){
return []
}
},
data:{
type:Array,
default(){
return[]
}
}
},
components:{
Render
}
}
</script>
<style lang="scss" scoped>
table{
width:100%;
border-collapse:collapse;
border-spacing:0;
empty-cells:show;//隐藏表格中空单元格上的边框和背景:
border:1px solid #e9e9e9;
}
table th{
background: #f7f7f7;
color:#5c6b77;
font-weight:600;
white-space:nowrap;//段落中的文本不进行换行
}
table td, table th{
padding:8px 16px;
border: 1px solid #e9e9e9;
text-align:center;
}
</style>
*相⽐原先的⽂件,只在 'render' in col 的条件下新加了⼀个
template 的标签,如果使⽤者的 column 配置了 render 字段,
就优先以 Render 函数渲染,然后再判断是否⽤ slot-scope 渲染。
在定义的作⽤域 slot 中,将⾏数据 row、列数据 column 和第⼏⾏
index 作为 slot 的参数,并根据 column 中指定的 slot 字段值,
动态设置了具名 name。使⽤者在配置 columns 时,只要指定了某
⼀列的 slot,那就可以在 Table 组件中使⽤ slot-scope。我们以上
⼀节的可编辑整⾏数据为例,⽤ slot-scope 的写法实现完全⼀样的
效果:*
<!-- src/views/table-slot.vue -->
<template>
<div>
<table-slot :columns="columns" :data="data">
<template slot-scope="{ row, index }" slot="name">
<input type="text" v-model="editName" v-if="editIndex === index" />
<span v-else>{{ row.name }}</span>
</template>
<template slot-scope="{ row, index }" slot="age">
<input type="text" v-model="editAge" v-if="editIndex === index" />
<span v-else>{{ row.age }}</span>
</template>
<template slot-scope="{ row, index }" slot="birthday">
<input type="text" v-model="editBirthday" v-if="editIndex === index" />
<span v-else>{{ getBirthday(row.birthday) }}</span>
</template>
<tempalte slot-scope="{ row, index }" slot="address">
<input type="text" v-model="editAddress" v-if="editIndex === index" />
<span v-else>{{ row.address }}</span>
</tempalte>
<template slot-scope="{ row, index }" slot="action">
<div v-if="editIndex === index">
<button @click="handleSave(index)">保存</button>
<button @click="editIndex = -1">取消</button>
</div>
<div v-else>
<button @click="handleEdit(row, index)">操作</button>
</div>
</template>
</table-slot>
</div>
</template>
<script>
import TableSlot from './table.vue'
export default {
components: {
TableSlot,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
{
title: '姓名',
slot: 'name',
},
{
title: '年龄',
slot: 'age',
},
{
title: '出⽣⽇期',
slot: 'birthday',
},
{
title: '地址',
slot: 'address',
},
{
title: '操作',
slot: 'action',
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
methods: {
handleEdit(row,index) {
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
},
handleSave(index){
this.data[index].name = this.editBirthday
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birtyhday = this.editBirthday
this.editIndex = -1
},
getBirthday(birthday){
const date = new Date(parseInt(birthday))
const year = date.getFullYear()
const month = date.getMonth()+1
const day = date.getDate()
return `${year}-${month}-${day}`
}
},
}
</script>
<style lang="scss" scoped></style>
*<table-slot> 内的每⼀个 <template> 就对应某⼀列
的 slot-scope 模板,通过配置的 slot 字段,指定具名的 slotscope。可以看到,基本是把 Render 函数还原成了 html 的写法,
这样看起来直接多了,渲染效果是完全⼀样的。在 slot-scope 中,
平时怎么写组件,这⾥就怎么写,Vue.js 所有的 API 都是可以直接
使⽤的。*
第⼆种⽅案,
不需要修改原先的 Table 组件代码,只是在使⽤层⾯
修改即可。先来看具体的使⽤代码,然后再做分析。注意,这⾥使⽤
的 Table 组件,仍然是上⼀节 src/components/table-render
的组件,它只有 Render 函数,没有定义 slot-scope:
<!-- src/views/table-render.vue -->
<template>
<div>
<table-render ref="table" :columns="columns" :data="data">
<template slot-scope="{ row, index }" slot="name">
<input type="text" v-model="editName" v-if="editIndex === index" />
<span v-else>{{ row.name }}</span>
</template>
<template slot-scope="{ row, index }" slot="age">
<input type="text" v-model="editAge" v-if="editIndex === index" />
<span v-else>{{ row.age }}</span>
</template>
<template slot-scope="{ row, index }" slot="birthday">
<input type="text" v-model="editBirthday" v-if="editIndex === index" />
<span v-else>{{ getBirthday(row.birthday) }}</span>
</template>
<tempalte slot-scope="{ row, index }" slot="address">
<input type="text" v-model="editAddress" v-if="editIndex === index" />
<span v-else>{{ row.address }}</span>
</tempalte>
<template slot-scope="{ row, index }" slot="action">
<div v-if="editIndex === index">
<button @click="handleSave(index)">保存</button>
<button @click="editIndex = -1">取消</button>
</div>
<div v-else>
<button @click="handleEdit(row, index)">操作</button>
</div>
</template>
</table-render>
</div>
</template>
<script>
import TableRender from './table.vue'
export default {
components: {
TableRender,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
{
title: '姓名',
render:(h,{row,column,index})=>{
// $scopedSlots获取到父组件中创建的插槽 向插槽内传递参数 name插槽传递
return h('div',this.$refs.table.$scopedSlots.name({
row:row,
column:column,
index:index
}))
}
},
{
title: '年龄',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.age({
row:row,
column:column,
index:index
}))
}
},
{
title: '出⽣⽇期',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.birthday({
row:row,
column:column,
index:index
}))
}
},
{
title: '地址',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.address({
row:row,
column:column,
index:index
}))
}
},
{
title: '操作',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots({
row:row,
column:column,
index:index
}))
}
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
methods: {
handleEdit(row,index) {
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
},
handleSave(index){
this.data[index].name = this.editBirthday
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birtyhday = this.editBirthday
this.editIndex = -1
},
getBirthday(birthday){
const date = new Date(parseInt(birthday))
const year = date.getFullYear()
const month = date.getMonth()+1
const day = date.getDate()
return `${year}-${month}-${day}`
}
},
}
</script>
<style lang="scss" scoped></style>
在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全
⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字
段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内
容,是指定来在 <table-render> 中定义的 slot-scope:
<!-- src/views/table-render.vue -->
<template>
<div>
<table-render ref="table" :columns="columns" :data="data">
<template slot-scope="{ row, index }" slot="name">
<input type="text" v-model="editName" v-if="editIndex === index" />
<span v-else>{{ row.name }}</span>
</template>
<template slot-scope="{ row, index }" slot="age">
<input type="text" v-model="editAge" v-if="editIndex === index" />
<span v-else>{{ row.age }}</span>
</template>
<template slot-scope="{ row, index }" slot="birthday">
<input type="text" v-model="editBirthday" v-if="editIndex === index" />
<span v-else>{{ getBirthday(row.birthday) }}</span>
</template>
<tempalte slot-scope="{ row, index }" slot="address">
<input type="text" v-model="editAddress" v-if="editIndex === index" />
<span v-else>{{ row.address }}</span>
</tempalte>
<template slot-scope="{ row, index }" slot="action">
<div v-if="editIndex === index">
<button @click="handleSave(index)">保存</button>
<button @click="editIndex = -1">取消</button>
</div>
<div v-else>
<button @click="handleEdit(row, index)">操作</button>
</div>
</template>
</table-render>
</div>
</template>
<script>
import TableRender from './table.vue'
export default {
components: {
TableRender,
},
data() {
return {
editName: '', // 第⼀列输⼊框
editAge: '', // 第⼆列输⼊框
editBirthday: '', // 第三列输⼊框
editAddress: '', // 第四列输⼊框
editIndex: -1, // 当前聚焦的输⼊框的⾏数
columns: [
{
title: '姓名',
render:(h,{row,column,index})=>{
// $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数 name插槽传递
return h('div',this.$refs.table.$scopedSlots.name({
row:row,
column:column,
index:index
}))
}
},
{
title: '年龄',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.age({
row:row,
column:column,
index:index
}))
}
},
{
title: '出⽣⽇期',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.birthday({
row:row,
column:column,
index:index
}))
}
},
{
title: '地址',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots.address({
row:row,
column:column,
index:index
}))
}
},
{
title: '操作',
render:(h,{row,column,index})=>{
return h('div',this.$refs.table.$scopedSlots({
row:row,
column:column,
index:index
}))
}
},
],
data: [
{
name: '王⼩明',
age: 18,
birthday: '919526400000',
address: '北京市朝阳区芍药居',
},
{
name: '张⼩刚',
age: 25,
birthday: '696096000000',
address: '北京市海淀区⻄⼆旗',
},
{
name: '李⼩红',
age: 30,
birthday: '563472000000',
address: '上海市浦东新区世纪⼤道',
},
{
name: '周⼩伟',
age: 26,
birthday: '687024000000',
address: '深圳市南⼭区深南⼤道',
},
],
}
},
methods: {
handleEdit(row,index) {
this.editName = row.name
this.editAge = row.age
this.editAddress = row.address
this.editBirthday = row.birthday
this.editIndex = index
},
handleSave(index){
this.data[index].name = this.editBirthday
this.data[index].age = this.editAge
this.data[index].address = this.editAddress
this.data[index].birtyhday = this.editBirthday
this.editIndex = -1
},
getBirthday(birthday){
const date = new Date(parseInt(birthday))
const year = date.getFullYear()
const month = date.getMonth()+1
const day = date.getDate()
return `${year}-${month}-${day}`
}
},
}
</script>
<style lang="scss" scoped></style>
在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全
⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字
段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内
容,是指定来在 <table-render> 中定义的 slot-scope:
{
title: '姓名',
render:(h,{row,column,index})=>{
// $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数 name插槽传递
return h('div',this.$refs.table.$scopedSlots.name({
row:row,
column:column,
index:index
}))
}
},
*name 这⼀列仍然是使⽤ Functional Render,只不过 Render 的是⼀个预先定义好的slot-scope 模板
有⼀点需要注意的是,示例中的 data 默认是空数组,⽽在
mounted ⾥才赋值的,是因为这样定义的 slot-scope,初始时读
取 this.$refs.table.$scopedSlots 是读不到的,会报错,当
没有数据时,也就不会去渲染,也就避免了报错。
这种⽅案虽然可⾏,但归根到底是⼀种 hack,不是⾮常推荐*
⽅案三
第 3 中⽅案的思路和第 2 种是⼀样的,它介于⽅案 1 与⽅案 2 之
间。这种⽅案要修改 Table 组件代码,但是⽤例与⽅案 1 完全⼀
致。
在⽅案 2 中,我们是通过修改⽤例使⽤ slot-scope 的,也就是说
Table 组件本身没有⽀持 slot-scope,是我们“强加”上去的,如果
把强加的部分,集成到 Table 内,那对使⽤者就很友好了,同时也
避免了初始化报错,不得不把 data 写在 mounted 的问题。
保持⽅案 1 的⽤例不变,修改 src/components/table-render
中的代码。为了同时兼容 Render 与 slot-scope,我们在 tablerender 下新建⼀个 slot.js 的⽂件:
export default {
functional:true,
inject:['tableRoot'],
props:{
row:Object,
column:Object,
index:Number
},
render:(h,ctx)=>{
return h('div',ctx.inject.tableRoot.$scopedSlots[ctx.props.column.slot]{
row:ctx.props.row,
column:ctx.props.column,
index:ctx.props.index
})
}
}
它仍然是⼀个 Functional Render,使⽤ inject 注⼊了⽗级组件
table.vue(下⽂改写) 中提供的实例 tableRoot。在 render
⾥,也是通过⽅案 2 中使⽤ $scopedSlots 定义的 slot,不过这
是在组件级别定义,对⽤户来说是透明的,只要按⽅案 1 的⽤例来
写就可以了。
table.vue 也要做⼀点修改:
<template>
<table>
<thead>
<tr>
<th v-for="(col,i) in columns" :key="i">
{{col.title}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,rowIndex) in data" :key="rowIndex">
<td v-for="(col,inx) in columns" :key="inx">
<template v-if="'render' in col">
<Render :row="row" :column="col" :index="rowIndex" :render="col.render">
</Render>
</template>
<template v-else-if="'slot' in col">
<slot-scope :index="rowIndex" :name="row.slot" :column="col">
</slot-scope>
</template>
<template v-else>
{{row[col.key]}}
</template>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Render from './render.js'
import SlotScope from './slot.js'
export default {
provide(){
return {
tableRoot:this
}
},
props:{
columns:{
type:Array,
default(){
return []
}
},
data:{
type:Array,
default(){
return[]
}
}
},
components:{
Render,
SlotScope
}
}
</script>
<style lang="scss" scoped>
table{
width:100%;
border-collapse:collapse;
border-spacing:0;
empty-cells:show;//隐藏表格中空单元格上的边框和背景:
border:1px solid #e9e9e9;
}
table th{
background: #f7f7f7;
color:#5c6b77;
font-weight:600;
white-space:nowrap;//段落中的文本不进行换行
}
table td, table th{
padding:8px 16px;
border: 1px solid #e9e9e9;
text-align:center;
}
</style>
*因为 slot-scope 模板是写在 table.vue 中的(对使⽤者来说,相当
于写在组件 <table-slot></table-slot> 之间),所以在
table.vue 中使⽤ provide 向下提供了 Table 的实例,这样在
slot.js 中就可以通过 inject 访问到它,继⽽通过 $scopedSlots
获取到 slot。需要注意的是,在 Functional Render 是没有 this 上
下⽂的,都是通过 h 的第⼆个参数临时上下⽂ ctx 来访问 prop、
inject 等的。
⽅案 3 也是推荐使⽤的,当 Table 的功能⾜够复杂,层级会嵌套的
⽐较深,那时⽅案 1 的 slot 就不会定义在第⼀级组件中,中间可能
会隔许多组件,slot 就要⼀层层中转,相⽐在任何地⽅都能直接使
⽤的 Render 就要麻烦了。所以,如果你的组件层级简单,推荐⽤第
⼀种⽅案;如果你的组件已经成型(某 API 基于 Render 函数),
但⼀时间不⽅便⽀持 slot-scope,⽽使⽤者⼜想⽤,那就选⽅案
2;如果你的组件已经成型(某 API 基于 Render 函数),但组件层
级复杂,要按⽅案 1 那样⽀持 slot-scope 可能改动较⼤,还有可
能带来新的 bug,那就⽤⽅案 3,它不会破坏原有的任何内容,但
会额外⽀持 slot-scope ⽤法,关键是改动简单*。
理论上,绝⼤多数能⽤ Render 的地⽅,都可以⽤ slot-scope
动态组件
*根据⼀些条件,动态地切换某个组件,或动态地选择渲染某个组件
使⽤ Render 或 Functional Render 可以解决动
态切换组件的需求,不过那是基于⼀个 JS 对象(Render 函数),
⽽ Vue.js 提供了另外⼀个内置的组件 <component> 和 is 特性,
可以更好地实现动态组件*
先来看⼀个 <component> 和 is 的基本示例,⾸先定义三个普通组
件:
<!--
* @Author: yang
* @Date: 2020-10-28 17:45:13
* @LastEditors: yang
* @LastEditTime: 2020-10-28 17:53:43
* @FilePath: \gloud-h5\src\component\index\a.vue
-->
<template>
<div>
a组件
</div>
</template>
<script>
export default {
beforeDestroy () {
console.log('组件销毁');
},
mounted () {
console.log('组件创建');
},
}
</script>
<style lang="scss" scoped>
</style>
<!--
* @Author: yang
* @Date: 2020-10-28 17:45:17
* @LastEditors: yang
* @LastEditTime: 2020-10-28 17:45:46
* @FilePath: \gloud-h5\src\component\index\b.vue
-->
<template>
<div>
b组件
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>
<!--
* @Author: yang
* @Date: 2020-10-28 17:45:22
* @LastEditors: yang
* @LastEditTime: 2020-10-28 17:45:54
* @FilePath: \gloud-h5\src\component\index\c.vue
-->
<template>
<div>
c组件
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>
然后在⽗组件中导⼊这 3 个组件,并动态切换:
只要切换到 A 组件,mounted 就会触发⼀次,切换到其它组
件,beforeDestroy 也会触发⼀次,说明组件再重新渲染,这样
有可能导致性能问题。为了避免组件的重复渲染,可以在
<component> 外层套⼀个 Vue.js 内置的 <keep-alive> 组件,
这样,组件就会被缓存起来:
*keep-alive 还有⼀些额外的 props 可以配置:
include:字符串或正则表达式。只有名称匹配的组件会被缓
存。
exclude:字符串或正则表达式。任何名称匹配的组件都不会
被缓存。
max:数字。最多可以缓存多少组件实例。*
<template>
<div>
<button @click="handleChange('a')">显示组件a</button>
<button @click="handleChange('b')">显示组件b</button>
<button @click="handleChange('c')">显示组件c</button>
<keep-alive>
<component :is="component"></component>
</keep-alive>
</div>
</template>
<script>
import a from './a.vue';
import b from './b.vue';
import c from './c.vue';
export default {
data() {
return {
component: a
}
},
methods: {
handleChange(item) {
switch(item){
case 'a':
this.component = a
break;
case 'b':
this.component = b
break;
case 'c':
this.component = c
break;
}
}
},
}
</script>
<style lang="scss" scoped>
</style
这⾥的 is 动态绑定的是⼀个组件对象(Object),它直接指向 a /
b / c 三个组件中的⼀个
动态渲染标签
除了直接绑定⼀个 Object,还可以是⼀ 个 String,⽐如标签名、组件名。下⾯的这个组件,将原⽣的按钮 button 进⾏了封装,如果传⼊了 prop: to,那它会渲染为⼀个 标签,⽤于打开这个链接地址,如果没有传⼊ to,就当作普通 button 使⽤。来看下⾯的示例:
button.vue
<template>
<component :is="tagName" v-bind="tagProps">
<slot></slot>
</component>
</template>
<script>
export default {
props:{
to:{
type:String,
default:""
},
target:{
type:String,
default:'_self'
}
},
computed: {
// 动态渲染不同的标签
tagName() {
return this.to === ''?'button':'a'
},
// 如果是链接,把这些属性绑定到component上
tagProps(){
let props = {}
if(this.to){
props={
target:this.target,
href:this.to
}
}
return props
}
},
}
</script>
<style lang="scss" scoped>
</style>
使用
<template>
<div>
<i-button>普通按钮</i-button>
<i-button to="http://www.baidu.com">链接按钮</i-button>
<i-button target="_bank" to="http://www.baidu.com">新窗口打开链接</i-button>
</div>
</template>
<script>
import iButton from './button.vue';
export default {
components: {
iButton,
},
}
</script>
<style lang="scss" scoped>
</style>
i-button 组件中的 <component> is 绑定的就是⼀个标签名称
button / a,并且通过 v-bind 将⼀些额外的属性全部绑定到了
<component> 上。
递归组件
*递归组件就是指组件在模板中调⽤⾃⼰,开启递归组件的必要条件,
就是在组件中设置⼀个 name 选项。*
在 Webpack 中导⼊⼀个 Vue.js 组件,⼀般是通过 import
myComponent from 'xxx' 这样的语法,然后在当前组件(⻚
⾯)的 components: { myComponent } ⾥注册组件。这种组件
是不强制设置 name 字段的,组件的名字都是使⽤者在 import 进来
后⾃定义的,但递归组件的使⽤者是组件⾃身,它得知道这个组件叫
什么,因为没有⽤ components 注册,所以 name 字段就是必须的
了。除了递归组件⽤ name,⽤⼀些特殊的⽅法,通过遍历匹配组件的 name 选项来寻找组件实例。
<template>
<div>
<my-component></my-component>
</div>
</template>
<script>
export default {
name: 'my-component'
}
</script>
<style lang="scss" scoped>
</style>
*不过呢,上⾯的示例是有问题的,如果直接运⾏,会抛出 max
stack size exceeded 的错误,因为组件会⽆限递归下去,死循
环。解决这个问题,就要给递归组件⼀个限制条件,⼀般会在递归组
件上⽤ v-if 在某个地⽅设置为 false 来终结。*
⽐如我们给上⾯的示例加⼀个属性 count,当⼤于 5 时就不再递归:
<template>
<div>
<my-component :count="count+1" v-if="count<=5"></my-component>
哈哈哈
</div>
</template>
<script>
export default {
name: 'my-component',
props:{
count:{
type:Number,
default:1
}
}
}
</script>
<style lang="scss" scoped>
</style>
实现⼀个递归组件的必要条件是:
1.要给组件设置 name;
2.要有⼀个明确的结束条件
递归组件常⽤来开发具有未知层级关系的独⽴组件
⽐如常⻅的有级联选择器和树形控件:
这类组件⼀般都是数据驱动型的,⽗级有⼀个字段 children,然后递归
assist.js
/*
* @Author: yang
* @Date: 2020-10-29 19:21:02
* @LastEditors: yang
* @LastEditTime: 2020-11-03 13:37:07
* @FilePath: \gloud-h5demo\src\component\index\utils\assist.js
*/
import Vue from 'vue'
// 当前Vue实例是否运行于服务器,属性值为true表示实例运行于服务器,每个Vue实例都可以通过该属性判断。该属性一般用于服务器渲染,用以区分代码是否在服务器上运行。
// vue脚手架,运行的项目的这个值为false
const isServer = Vue.prototype.$isServer
export function oneOf(value, validList) {
// for(let i = 0;i<validList.length;i++){
// if(value==validList[i]){
// return true
// }
// return false
// }
let bool = validList.some((item) => value === item)
return bool
}
// 驼峰结构
export function camelcaseToHyphen(str) {
// $1是第一个小括号里匹配的内容,$2是第二个小括号里面匹配的内容
return str.replace(/([a-z](A-Z))/g, '$1-$2').toLowerCase()
}
// 获取滚动条宽度
let cached
export function getScollBarSize(fresh) {
if (isServer) return 0
if (fresh || cached === undefined) {
const inner = document.createElement('div')
inner.style.width = '100%'
inner.style.height = '200px'
const outer = document.createElement('div')
const outerStyle = outer.style
outerStyle.position = 'absolute'
outerStyle.top = 0
outerStyle.left = 0
// 鼠标不会穿透当前层 元素永远不会成为鼠标事件的target
outerStyle.pointerEvents = 'none'
outerStyle.visibility = 'hidden'
outerStyle.width = '200px'
outerStyle.height = '150px'
outerStyle.overflow = 'hidden'
outer.appendChild(inner)
document.body.appendChild(outer)
// offsetWidth 是对象的可见宽度,包滚动条等边线
const widthContained = outer.offsetWidth
outer.style.overflow = 'scroll'
let widthScroll = inner.offsetWidth
if (widthContained === widthScroll) {
//clientWidth 是对象可见的宽度,不包滚动条等边线
widthScroll = outer.clientWidth
}
document.body.removeChild(outer)
cached = widthContained - widthScroll
}
return cached
}
//watch dom change
// Mutation Observer API 用来监视 DOM 变动。比如节点的增减、属性的变动、文本内容的变动。MutationObserver使用observe方法进行监听指定的元素节点变化
export const MutationObserver = isServer
? false
: window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver ||
false
const SPECIAL_CHARS_REGEXP = /([\:\-\_])+(.)/g
const MOZ_HACK_REGEXP = /^moz(A-Z)/
function camelCase(name) {
return name
.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter
})
.replace(MOZ_HACK_REGEXP, 'Moz$1') //$1第一次匹配的结果
}
// getStyle 获取样式
export function getStyle(element, styleName) {
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'cssFloat'
}
try {
// document.defaultView.getComputedStyle()可以获取对象的css样式;他返回的是一个CSS样式对象。
// 使用:document.defaultView.getComputedStyle(a, b); a这对象是要想要获取的对象。 b,伪类,若果不是则为null。
const computed = document.defaultView.getComputedStyle(element, '')
return element.style[styleName] || computed ? computed[styleName] : null
} catch (e) {
return element.style[styleName]
}
}
// firstUpperCase 将首字母大写
function firstUpperCase(str) {
return str.toString()[0].toUpperCase() + str.toString().slice(1)
}
export { firstUpperCase }
// warn
export function warnProp(component, prop, correctType, wrongType) {
correctType = firstUpperCase(correctType)
wrongType = firstUpperCase(wrongType)
console.error(
//eslint-disable-line
`[iView warn]:Invalid prop:type check failed for prop ${prop},Expected ${correctType},got ${wrongType}.(found in component:${component})`
)
}
function typeOf(obj){
const toString = Object.prototype.toString
const map = {
'[object Boolean]':'boolean',
'[object Number]':'number',
'[object String]':'string',
'[object Function]':'function',
'[object Array]':'array',
'[object Date]':'date',
'[object RegExp]':'regExp',
'[object Undefined]':'undefined',
'[object Null]':'null',
'[object Object]':'object'
}
return map[toString.call(obj)]
}
//deepCopy
function deepCopy(data){
const t = typeOf(data)
let o;
if(t==='array'){
o = []
for(let i = 0;i<data.length;i++){
o.push(deepCopy(data[i]))
}
}else if(t === 'object'){
o = {}
for(let key in data){
o[key] = deepCopy(data[key])
}
}else{
return data
}
return o
}
export {deepCopy}
//scrollTop animation
// window.requestAnimationFrame() 这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数,以满足开发者操作动画的需要。即可以这样说,该方法接受一个函数为参,该函数会在重绘前调用。
// 回调函数会被传入一个参数,DOMHighResTimeStamp,指示requestAnimationFrame() 开始触发回调函数的当前时间
export function scrollTop(el,from=0,to,duration = 500,endCallback){
if(!window.requestAnimationFrame){
window.requestAnimationFrame = (
window.webkitRequestAnimationFrame||
window.mozRequestAnimationFrame||
window.msRequestAnimationFrame||
function (callback){
return window.setTimeout(callback,1000/60);
}
)
}
// 返回一个数的绝对值
const difference = Math.abs(from - to)
// 向上取整
const step = Math.ceil(difference/duration*50)
function scroll(start,end,step){
if(start === end){
endCallback &&endCallback()
return
}
let d = (start + step >end)?end:start+ step;
if(start>end){
d = (start - step< end)?end : start-step
}
if(el === window){
// 滚动到文档中的某个坐标
// 需要在页面打开后,定位滚动条的位置,比如,横向和纵向滚动条居中,实现页面滚动的方法有三种:scroll、scrollBy和 scrollTo,三个方法都带两个参数:x(X轴上的偏移量)和y(Y轴上的偏移量)。因此我们只需修改x,y的偏移量来设置滚动条的位置。
window.scrollTo(d,d)
}else{
// 规定相对滚动条顶部的偏移
el.scrollTop = d
}
window.requestAnimationFrame(()=>scroll(d,end,step))
}
scroll(from,to,step)
}
//Find components upward
function findComponentUpward(contxt,componentName,componentNames){
if(typeof componentName === 'string'){
componentNames = [componentName]
}else{
componentNames = componentName
}
let parent = context.$parent
// $options 可以获取自定义属性
let name = parent.$options.name
while(parent&&(!name||componentNames.indexoOf(name)<0)){
parent = parent.$parent
if(parent)name = parent.$options.name
}
return parent
}
export {findComponentUpward}
//Find component downward
export function findComponentDownward(context,componentName){
const childrens = context.$children
let children = null
if(childrens.length){
for(const child in childrens){
const name = child.$options.name
if(name === componentName){
children = child
break;
}else{
children = findComponentDownward(child,componentName)
if(children)break
}
}
}
return children
}
//Find compoents downward
export function findComponentsDownward(context,componentName,ignoreComponentNames = []){
// Array.isArray用于判断一个对象是否为数组。
if(!Array.isArray(ignoreComponentNames)){
ignoreComponentNames = [ignoreComponentNames]
}
// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
return context.$children.reduce((components,child)=>{
if(child.$options.name = componentName)components.push(child)
if(ignoreComponentNames.indexOf(child.$options.name)<0){
const foundChilds = findComponentsDownward(child,componentName)
return components.concat(foundChilds)
}else{
return components
}
},[])
}
//Find components upward
export function findComponentsUpward(context,componentName){
let parents = []
const parent = context.$parent
if(parent){
if(parent.$options.name === componentName)parents.push(parent)
return parents.concat(findComponentUpward(parent,componentName))
}else{
return []
}
}
// Find brothers components
export function findBrothersComponents(context,componentName,exceptMe = true){
let res = context.$parent.$children.filter(item=>{
return item.$options.name === componentName
})
let index = res.findIndex(item=>item._uid === context._uid)
// splice 从数组中添加或者删除项目,返回被删除的项目,同时也会改变原数组。
if(exceptMe) res.splice(index,1)
return res
}
//istanbul ignore next
// 去掉前后空格的
const trim = function(string){
// \s 匹配任何不可见字符,包括空格、制表符、换页符等等
// \s:空格 \uFEFF:字节次序标记字符(Byte Order Mark),也就是BOM,它是es5新增的空白符 \xA0:禁止自动换行空白符,相当于html中的
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g,'')
}
// istanbul igonre next
// 确定元素中是否包含指定的类名 返回值为true 、false
export function hasClass(el,cls){
if(!el || !cls) return false
if(cls.indexOf(' ')!==-1)throw new Error('className should not contain space.')
if(el.classList){
//classList.contains 确定元素中是否包含指定的类名 返回值为true 、false
return el.classList.contains(cls)
}else{
return(' ' + el.className + ' ').indexoOf(' ' + cls + ' ')>-1
}
}
export function addClass(el,cls){
if(!el) return
let curClass = el.className
const classes = (cls || '').split(' ')
for(let i = 0; i<classes.length;i++){
const clsName = classes[i]
if(!clsName) continue
if(el.classList){
el.classList.add(clsName)
}else{
if(!hasClass(el,clsName)){
curClass += ' ' + clsName
}
}
}
if(!el.classList){
el.className = curClass
}
}
// istanbul ignore next
export function removeClass(el,cls){
if(!el || !cls) return
const classes = cls.split(' ')
let curClass = " " + el.className + ' '
for(let i =0;i<classes.length;i++){
const clsName = classes[i]
if(!clsName) continue
if(el.classList){
el.classList.removeClass(clsName)
}else{
if(hasClass(el,clsName)){
curClass = curClass.replace(' ' + clsName + '',' ')
}
}
}
if(!el.classList){
el.className = trim(curClass)
}
}
export const dimensionMap = {
xs:'480px',
sm:'576px',
md:'768px',
lg:'992px',
xl:'1200px',
xxl:'1600px'
}
export function setMatchMedia(){
if(typeof window !== 'undefined'){
const matchMediaPolyfill = mediaQuery => {
return {
media:mediaQuery,
matches:false,
on(){},
off(){}
}
}
// matchMedia() 返回一个新的 MediaQueryList 对象,表示指定的媒体查询字符串解析后的结果。
// alert(window.matchMedia("(max-width:100px)").media); //(max-width: 100px)
window.matchMedia = window.matchMedia||matchMediaPolyfill
}
}
export const sharpMatcherRegx = /#([^#]+)$/
checkbox.vue
<!--
* @Author: yang
* @Date: 2020-10-29 15:52:18
* @LastEditors: yang
* @LastEditTime: 2020-10-29 17:48:38
* @FilePath: \gloud-h5\src\component\index\checkbox.vue
-->
<template>
<label :class="wrapClasses">
<span :class="checkboxClasses">
<span :class="innerClasses"></span>
<input type="checkbox" v-if="group" :class="inputClasses" :disabled="disabled" :value="label" v-model="model" :name="name" @change="change" @focus="onFocus" @blur="onBlur">
<input type="checkbox" v-else :class="inputClasses" :disabled="disabled" :checked="currentValue" :name="name" @change="change" @focus="onFocus" @blur="onBlur">
</span>
<slot>
<span v-if="showSlot">{{label}}</span>
</slot>
</label>
</template>
<script>
import { findComponentUpward, oneOf } from './utils/assist'
import Emitter from './mixinx/emitter'
const prefixCls = 'ivu-checkbox'
export default {
mixins:[Emitter],
props:{
disabled:{
type:Boolean,
default: false
},
value:{
type:[String,Number,Boolean],
default:false
},
trueValue:{
type:[String,Number,Boolean],
default:true
},
falseValue:{
type:[String,Number,Boolean],
default:false
},
label:{
type:[String,Number,Boolean]
},
indeterminate:{
type:Boolean,
default:false
},
size:{
validator(value){
return oneOf(value, ['small','large','default'])
},
default(){
return !this.$IVIEW||this.$IVIEW.size===''?'default':this.$IVIEW.size;
}
},
name:{
type:String
}
},
data() {
return {
model: [],
currentValue:this.value,
group:false,
showSlot:true,
parent:findComponentUpward(this,'CheckboxGroup'),
focusInner:false
}
},
computed:{
wrapClasses(){
// class属性(数组语法) ,对象内如果右侧满足,则有这个class名称
return [
`${prefixCls}-wrapper`,
{
[`${prefixCls}-group-item`]:this.group,
[`${prefixCls}-wrapper-checked`]:this.currentValue,
[`${prefixCls}-wrapper-disabled`]:this.disabled,
[`${prefixCls}-${this.size}`]:!!this.size
}
]
},
checkboxClasses(){
return [
`${prefixCls}`,
{
[`${prefixCls}-checked`]:this.currentValue,
[`${prefixCls}-disabled`]:this.disabled,
[`${prefixCls}-indeterminate`]:this.indeterminate
}
]
},
innerClasses(){
return [
`${prefixCls}-inner`,
{
[`${prefixCls}-focus`]:this.focusInner
}
]
},
inputClasses(){
return `${prefixCls}-input`
}
},
mounted(){
this.parent = findComponentUpward(this,'CheckboxGroup');
if(this.parent){
this.group = true
}
if(this.group){
this.parent.updateModel(true)
}else{
this.updateModel()
this.showSlot = this.$slots.default!==undefined
}
},
methods: {
change(event) {
if(this.disabled){
return false
}
const checked = event.target.checked;
this.currentValue = checked
const value = checked?this.trueValue:this.falseValue
this.$emit('input',value)
if(this.group){
this.parent.change(this.model)
}else{
this.$emit('on-change',value)
this.dispatch('FormItem','on-form-change',value)
}
},
updateModel(){
this.currentValue = this.value===this.currentValue
},
onBlur(){
this.focusInner = false
},
onFocus(){
this.focusInner = true
}
},
watch:{
value(val){
if(val===this.currentValue||val===this.falseValue){
this.updateModel()
}else{
throw 'Value should be trueValue or falseValue.'
}
}
}
}
</script>
<style lang="scss" scoped></style>
*deepCopy 函数会递归地对数组或对象进⾏逐⼀判断,如果某项是
数组或对象,再拆分继续判断,⽽其它类型就直接赋值了,所以深拷
⻉的数据不会破坏原有的数据*
tree.vue
<template>
<div>
<tree-node v-for="(item,index)in cloneData" :key="index" :data="item" :show-checkbox="showCheckbox"></tree-node>
</div>
</template>
<script>
import TreeNode from './node.vue'
import {deepCopy} from './utils/assist.js'
export default {
name:tree,
components:{
TreeNode
},
props:{
data:{
type:Array,
default(){
return []
}
},
showCheckbox:{
type:Boolean,
default:false
}
},
data() {
return {
cloneData: []
}
},
created(){
this.rebuildData()
},
watch:{
data(){
this.rebuildData()
}
},
methods: {
rebuildData() {
this.cloneData = deepCopy(this.data)
},
emitEvent(eventName,data){
this.$emit(eventName,data,this.cloneData)
}
},
}
</script>
<style lang="scss" scoped>
</style>
*在组件 created 时(以及 watch 监听 data 改变时),调⽤了
rebuildData ⽅法克隆源数据,并赋值给了 cloneData。
在 template 中,先是渲染了⼀个 node.vue 组件(<treenode>),这⼀级是 Tree 的根节点,因为 cloneDate 是⼀个数
组,所以这个根节点不⼀定只有⼀项,有可能是并列的多项。不过这
⾥使⽤的 node.vue 还没有⽤到 Vue.js 的递归组件,它只处理第⼀
级根节点。*
<tree-node> 组件(node.vue)接收两个 props:
- showCheckbox:与 tree.vue 的 showCheckbox 相同,只
是进⾏传递;
- data:node.vue 接收的 data 是⼀个 Object ⽽⾮ Array,因
为它只负责渲染当前的⼀个节点,并递归渲染下⼀个⼦节点
(即 children),所以这⾥对 cloneData 进⾏循环,将每⼀
项节点数据赋给了 tree-node。
递归组件 node.vue
node.vue 是树组件 Tree 的核⼼,⽽⼀个 tree-node 节点包含 4
个部分:
- 展开与关闭的按钮(+或-);
- 多选框;
- 节点标题;
- 递归⼦节点。
<template>
<ul class="tree-ul">
<li class="tree-li">
<span class="tree-expand" @click="handleExpand">
<span v-if="data.children&&data.children.length&&!data.expand">+</span>
<span v-if="data.children&&data.children.length&&data.expand">-</span>
</span>
<i-checkbox v-if="showCheckbox" :value="data.checked" @input="handleCheck"></i-checkbox>
<span>{{data.title}}</span>
<tree-node v-if="data.expand" v-for="(item,index)in data.children" :key="index" :data="item" :show-checkbox="showCheckbox"></tree-node>
</li>
</ul>
</template>
<script>
import iCheckbox from './checkbox'
export default {
name:'TreeNode',
components:{
iCheckbox
},
data() {
return {
tree: findComponentUpward(this,'tree')
}
},
props:{
data:{
type:Object,
default(){
return {}
}
},
showCheckbox:{
type:Boolean,
default:false
}
},
// 点击 + 号时,会展开直属⼦节点,点击 - 号关闭,这⼀步只需在
// handleExpand 中修改 data 的 expand 数据即可,同时,我们通
// 过 Tree 的根组件(tree.vue)触发⼀个⾃定义事件 @on-toggleexpand(上⽂已介绍):
methods: {
handleExpand() {
this.$set(this.data,'expand',!this.data.expand)
if(this.tree){
this.tree.emitEvent('on-toggle-expand',this.data)
}
},
handleCheck(checked){
this.updateTreeDown(this.data,checked)
if(this.tree){
this.tree.emitEvent('on-check-change',this.data)
}
},
updateTreeDown(data,checked){
this.$set(data,'checked',checked)
if(data.children&&data.children.length){
data.children.forEach(item=>{
this.updateTreeDown(item,checked)
})
}
}
},
watch:{
// node.vue 是⼀个递归组
// 件,那每⼀个组件⾥都会有 watch 监听 data.children,要知
// 道,当前的节点有两个”身份“,它既是下属节点的⽗节点,同时也是
// 上级节点的⼦节点,它作为下属节点的⽗节点被修改的同时,也会触
// 发上级节点中的 watch 监听函数。这就是递归。
'data.children':{
handler(data){
if(data){
const checkedAll = !data.some(item=>!item.checked)
this.$set(this.data,'checked',checkedAll)
}
},
deep:true
}
}
}
</script>
<style lang="scss" scoped>
.tree-ul,.tree-li{
list-style:none;
padding-left:10px;
}
.tree-expand{
cursor:pointer;
}
</style>
v-model语法糖
v-model 常⽤于表单元素上进⾏数据的双向绑定,⽐如 <input>。除了原⽣的元素,它还能在⾃定义组件中使⽤。
v-model 是⼀个语法糖,可以拆解为 props: value 和 events:
input。就是说组件必须提供⼀个名为 value 的 prop,以及名为
input 的⾃定义事件,满⾜这两个条件,使⽤者就能在⾃定义组件上
使⽤ v-model。
⽐如下⾯的示例,实现了⼀个数字选择器:
input-number.vue
<template>
<div>
<button @click="increase(-1)">减一</button>
<span style="color:red;padding:6px">{{currentValue}}</span>
<button @click="increase(1)">加一</button>
</div>
</template>
<script>
export default {
name:'InputNumber',
props:{
value:{
type:Number
}
},
data(){
return{
currentValue:this.value
}
},
watch:{
value(value){
this.currentValue = this.value
}
},
methods:{
increase(val){
this.currentValue+=val
this.$emit('input',this.currentValue)
}
}
}
</script>
<style lang="scss" scoped>
</style>
*props ⼀般不能在组件内修改,它是通过⽗级修改的,因此实现 vmodel ⼀般都会有⼀个 currentValue 的内部 data,初始时从
value 获取⼀次值,当 value 修改时,也通过 watch 监听到及时更
新;组件不会修改 value 的值,⽽是修改 currentValue,同时将修
改的值通过⾃定义事件 input 派发给⽗组件,⽗组件接收到后,由
⽗组件修改 value。*
上面的数字选择器组件可以有下⾯两种使
⽤⽅式:
默认情况下,一个组件上的 v-model
会把 value
用作 prop 且把 input
用作 event
写法一
<template>
<div>
<input-number v-model="value"></input-number>
</div>
</template>
<script>
import inputNumber from './input-number'
export default {
components:{
inputNumber
},
data(){
return{
value:1
}
}
}
</script>
<style lang="scss" scoped>
</style>
或者
<template>
<div>
<input-number :value="value" @input="changeValue"></input-number>
</div>
</template>
<script>
import inputNumber from './input-number'
export default {
components:{
inputNumber
},
data(){
return{
value:1
}
},
methods: {
changeValue(val) {
this.value = val
}
},
}
</script>
<style lang="scss" scoped>
</style>
如果你不想⽤ value 和 input 这两个名字,从 Vue.js 2.2.0 版本
开始,提供了⼀个 model 的选项,可以指定它们的名字,根据写法一,所以数字选择器组件也可以这样写:
<template>
<div>
<button @click="increase(-1)">减一</button>
<span style="color:red;padding:6px">{{currentValue}}</span>
<button @click="increase(1)">加一</button>
</div>
</template>
<!-- model选项,在定义组件的时候,指定prop的值和监听的事件 -->
<script>
export default {
name:'InputNumber',
props:{
number:{
type:Number
}
},
model:{
prop:'number',
event:'change'
},
data(){
return{
currentValue:this.number
}
},
watch:{
value(value){
this.currentValue = value
}
},
methods:{
increase(val){
this.currentValue+=val
this.$emit('number',this.currentValue)
}
}
}
</script>
<style lang="scss" scoped>
</style>
*在 model 选项⾥,就可以指定 prop 和 event 的名字了,⽽不⼀定
⾮要⽤ value 和 input,因为这两个名字在⼀些原⽣表单元素⾥,有
其它⽤处。*
.sync 修饰符
. 是⼀个语法糖,修改数据还是在⽗组件完成的,并⾮在⼦组件。
自动更新父组件属性的 v-on 监听器
<input-number :value.sync="value"></input-number>
会被扩展为
<input-number:value="value"@update:value="val=>value=val"></input-number>
当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:
this.$emit('update:value', newValue)
子组件
<template>
<div>
<button @click="increase(-1)">减一</button>
<span style="color:red;padding:6px">{{value}}</span>
<button @click="increase(1)">加一</button>
</div>
</template>
<script>
export default {
name:'InputNumber',
props:{
value:{
type:Number
}
},
methods:{
increase(val){
this.$emit('update:value',this.value+val)
}
}
}
</script>
<style lang="scss" scoped>
</style>
父组件
<template>
<div>
<!-- 自动更新父组件属性的 v-on 监听器 -->
<input-number :value.sync="value"></input-number>
<!-- 会被扩展为<input-number :value="value" @update:value="val=>value=val"></input-number>
当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:
this.$emit('update:value', newValue) -->
</div>
</template>
<script>
import inputNumber from './input-number'
export default {
components:{
inputNumber
},
data(){
return{
value:1
}
}
}
</script>
<style lang="scss" scoped>
</style>
看起来要⽐ v-model 的实现简单多,实现的效果是⼀样的。vmodel 在⼀个组件中只能有⼀个,但 .sync 可以设置很多个。.sync
虽好,但也有限制,⽐如:
不能和表达式⼀起使⽤(如 vbind:title.sync="doc.title + '!'" 是⽆效的);
不能⽤在字⾯量对象上(如 v-bind.sync="{ title:
doc.title }" 是⽆法正常⼯作的)。
$set
在上⼀节已经介绍过 $set,有两种情况会⽤到它:
- 由于 JavaScript 的限制,Vue 不能检测以下变动的数组:
- 当利⽤索引直接设置⼀个项时,例
如:this.items[index] = value;
- 当修改数组的⻓度时,例如:vm.items.length =
newLength。
- 由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删
除。
data(){
return{
items:['q','a','c']
}
},
methods: {
handler() {
this.items[1] = 'x'//// 不是响应性的
}
},
使⽤ $set:
data(){
return{
items:['q','a','c']
}
},
methods: {
handler() {
this.$set(this.items,1,'X')//响应的
}
},
另外,数组的以下⽅法,都是可以触发视图更新的,也就是响应性
的:push()、shift()、 unshift()、 sort()、 splice()、 pop()
还有⼀种⼩技巧,就是先 copy ⼀个数组,然后通过 index 修改
后,再把原数组整个替换,⽐如:
methods: {
handler() {
let data = [...this.items]
data[1] = '11'
this.items = data
}
},
计算属性的 set
计算属性(computed)很简单,⽽且也会⼤量使⽤,但⼤多数时
候,我们只是⽤它默认的 get ⽅法,也就是平时的常规写法,通过
computed 获取⼀个依赖其它状态的数据。⽐如:
computed:{
fullName(){
return `${this.firstName} ${this.lastName}`
}
}
这⾥的 fullName 事实上可以写为⼀个 Object,⽽⾮ Function,只
是 Function 形式是我们默认使⽤它的 get ⽅法,当写为 Object
时,还能使⽤它的 set ⽅法:
computed:{
fullName:{
get(){
return `${firstName}-${lastname}`
},
set(val){
let name = val.split('')
this.firstName = name[0]
this.lastname = name[name.length-1]
}
}
}
计算属性⼤多时候只是读取⽤,使⽤了 set 后,就可以写⼊了,⽐
如上⾯的示例,如果执⾏ this.fullName = 'Aresn Liang',
computed 的 set 就会调⽤,firstName 和 lastName 会被赋值为
Aresn 和 Liang。
剩余值得注意的 API
还有⼀些 API,可能不常⽤,也⽐较简单,只需知道就好,可以通过指引到 Vue.js ⽂档查看。
delimiters
(https://cn.vuejs.org/v2/api/#...
改变纯⽂本插⼊分隔符,Vue.js 默认的是 {{ }},如果你使⽤其它
⼀些后端模板,⽐如 Python 的 Tornado 框架,那 Vue.js 和
Tornado 的 {{ }} 就冲突了,这时⽤它可以修改为指定的分隔符。
v-once
(https://cn.vuejs.org/v2/api/#...
只渲染元素和组件⼀次。随后的重新渲染,元素/组件及其所有的⼦
节点将被视为静态内容并跳过。这可以⽤于优化更新性能。
vm.$isServer
(https://cn.vuejs.org/v2/api/#...
当前 Vue 实例是否运⾏于服务器,如果你的组件要兼容 SSR,它会
很有⽤。
inheritAttrs
(https://cn.vuejs.org/v2/api/#...
⼀些原⽣的 html 特性,⽐如 id,即使没有定义 props,也会被集
成到组件根节点上,设置 inheritAttrs 为 false 可以关闭此特性。
当设置inheritAttrs: true(默认)时,子组件的顶层标签元素中会渲染出父组件传递过来的属性
当设置inheritAttrs: false时,子组件的顶层标签元素中不会渲染出父组件传递过来的属性
不管inheritAttrs为true或者false,子组件中都能通过$attrs属性获取到父组件中传递过来的属性。
errorHandler
(https://cn.vuejs.org/v2/api/#...
使⽤ errorHandler 可以进⾏异常信息的获取。
watch (https://cn.vuejs.org/v2/api/#...
监听状态的变化,⽤的也很多了,但它和 computed ⼀样,也有
Object 的写法,这样能配置更多的选项,⽐如:
handler 执⾏的函数
deep 是否深度
immediate 是否⽴即执⾏
var vm = new Vue({
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: 'someMethod',
immediate: true
},
// 你可以传入回调数组,它们会被逐一调用
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
/* ... */
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
})
vm.a = 2 // => new: 2, old: 1
不应该使用箭头函数来定义 watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue)
)。理由是箭头函数绑定了父级作用域的上下文,所以 this
将不会按照期望指向 Vue 实例,this.updateAutocomplete
将是 undefined。
完整的配置可以阅读⽂档。
comments
(https://cn.vuejs.org/v2/api/#...
开启会保留 html 注释。
transition
(https://cn.vuejs.org/v2/api/#...
内置的组件,可做过渡效果,⽐如 CSS 的⾼度从 0 到 auto(使⽤
纯 CSS 是⽆法实现动画的)。
Vue常见面试题
v-show 与 v-if 区别
- v-show 只是 CSS 级别的 display: none; 和 display:
block; 之间的切换,⽽ v-if 决定是否会选择代码块的内容(或组
件)。
- 频繁操作时,使⽤ v-show,⼀次性渲染完的,使⽤ v-if,只要意
思对就好。
使⽤ v-if在性能优化上有什么经验?
*因为当 v-if="false" 时,内部组件是不会渲染的,所以在特定条
件才渲染部分组件(或内容)时,可以先将条件设置为 false,需
要时(或异步,⽐如 $nextTick)再设置为 true,这样可以优先渲
染重要的其它内容,合理利⽤,可以进⾏性能优化。*
绑定 class 的数组⽤法
动态绑定 class 应该不陌⽣吧,这也是最基本的,但是这个问题却有
点绕,什么叫绑定 class 的数组⽤法?我们看⼀下,最常⽤的绑定
class 怎么写:
<template>
<div :class="{show: isShow}">内容</div>
</template>
<script>
export default {
data () {
return {
isShow: true
}
}
}
</script>
绑定 class 的对象⽤法能满⾜⼤部分业务需求,不过,在复杂的场景
下,会⽤到数组,来看示例:
<template>
<div :class="classes"></div>
</template>
<script>
export default {
computed: {
classes () {
return [
`${prefixCls}`,
`${prefixCls}-${this.type}`,
{
[`${prefixCls}-long`]: this.long,
[`${prefixCls}-${this.shape}`]:
!!this.shape,
[`${prefixCls}-${this.size}`]:
this.size !== 'default'
,
[`${prefixCls}-loading`]:
this.loading != null && this.loading,
[`${prefixCls}-icon-only`]:
!this.showSlot && (!!this.icon ||
!!this.customIcon || this.loading),
[`${prefixCls}-ghost`]: this.ghost
}
];
}
}
}
</script>
示例来⾃ iView 的 Button 组件,可以看到,数组⾥,可以是固定的
值,还有动态值(对象)的混合。
计算属性和 watch 的区别
- 计算属性是⾃动监听依赖值的变化,从⽽动态返回内容,监听是⼀个
过程,在监听的值变化时,可以触发⼀个回调,并做⼀些事情。
*所以区别来源于⽤法,只是需要动态值,那就⽤计算属性;需要知道
值的改变后执⾏业务逻辑,才⽤ watch,
这个问题会延伸出⼏个问题:
- computed 是⼀个对象时,它有哪些选项?
- computed 和 methods 有什么区别?
- computed 是否能依赖其它组件的数据?
- watch 是⼀个对象时,它有哪些选项?
*问题1:有 get 和 set 两个选项。
- 问题 2,methods 是⼀个⽅法,它可以接受参数,⽽ computed 不
能;computed 是可以缓存的,methods 不会;⼀般在 v-for
⾥,需要根据当前项动态绑定值时,只能⽤ methods ⽽不能⽤
computed,因为 computed 不能传参。
- 问题 3,computed 可以依赖其它 computed,甚⾄是其它组件的
data。
*问题 4,有以下常⽤的配置:
handler 执⾏的函数
deep 是否深度
immediate 是否⽴即执⾏
**怎样给这个⾃定义组件 custom-component 绑定⼀个原
⽣的 click 事件?**
<custom-component @click.native="xxx">内容
</custom-component>
exact 修饰符
exact 是 Vue.js 2.5.0 新加的,它允许你控制由精确的
系统修饰符组合触发的事件,⽐如:
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
stop
:等同于JavaScript中的event.stopPropagation()
,防止事件冒泡.prevent
:等同于JavaScript中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播).capture
:与事件冒泡的方向相反,事件捕获由外到内.self
:只会触发自己范围内的事件,不包含子元素.once
:只会触发一次
组件中 data 为什么是函数
为什么组件中的 data 必须是⼀个函数,然后 return ⼀个对象,⽽
new Vue 实例⾥,data 可以直接是⼀个对象?
因为组件是⽤来复⽤的,JS ⾥对象是引⽤关系,这样作⽤域没有隔
离,⽽ new Vue 的实例,是不会被复⽤的,因此不存在引⽤对象的
问题。
递归组件的要求
要给组件设置 name;
要有⼀个明确的结束条件。
⾃定义组件的语法糖 v-model 是怎样实现的
<template>
<div>
{{ currentValue }}
<button @click="handleClick">Click</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 0
}
},
data () {
return {
currentValue: this.value
}
},
methods: {
handleClick () {
this.currentValue += 1;
this.$emit('input'
, this.currentValue);
}
},
watch: {
value (val) {
this.currentValue = val;
}
}
}
</script>
这个组件中,只有⼀个 props,但是名字叫 value,内部还有⼀个
currentValue,当改变 currentValue 时,会触发⼀个⾃定义事件
@input,并把 currentValue 的值返回。这就是⼀个 v-model 的
语法糖,它要求 props 有⼀个叫 value 的项,同时触发的⾃定义事
件必须叫 input。这样就可以在⾃定义组件上⽤ v-model 了
<custom-component v-model="value"></customcomponent>
也可以用model 选项,
Vuex 中 mutations 和 actions 的区别
主要的区别是,actions 可以执⾏异步。actions 是调⽤
mutations,⽽ mutations 来修改 store。
Render 函数
什么是 Render 函数,它的使⽤场景是什么。
说到 Render 函数,就要说到虚拟 DOM(Virtual DOM),Virtual
DOM 并不是真正意义上的 DOM,⽽是⼀个轻量级的 JavaScript 对
象,在状态发⽣变化时,Virtual DOM 会进⾏ Diff 运算,来更新只
需要被替换的 DOM,⽽不是全部重绘。
它的使⽤场景,就是完全发挥 JavaScript 的编程能⼒,有时需要结
合 JSX 来使⽤。
createElement 是什么?
createElement 是 Render 函数的核⼼,它构成了 Vue Virtual
DOM 的模板
Render 函数有哪些常⽤的参数?
1 {String | Object | Function}
⼀个 HTML 标签、一个含有数据选项的对象、Function返回一个含有数据选项的对象
Vue.component('child', {
props: ['level'],
render: function (createElement) {
//string:html标签
return createElement('h1')
//object:一个含有数据选项的对象
return createElement({
template: '<div>谈笑风生</div>'
})
//function:返回一个含有数据选项的对象
var domFun = function () {
return {
template: `<div>谈笑风生</div>`
}
}
return createElement(domFun())
}
})
2.第二个参数是数据对象。只能是object
- class
- style
- attrs
- domProps
createElement('div', {
class: {
foo: true,
baz: false
},
style: {
height: '34px',
background: 'orange',
fontSize: '16px'
},
//正常的html特性(除了class和style)
attrs: {
id: 'foo',
title: 'baz'
},
//用来写原生的DOM属性
domProps: {
innerHTML: '<span >江心比心</span>'
}
第三个参数可选 代表子节点
String|Array
Vue.component('child', {
props: ['level'],
render: function (createElement) {
return createElement('div', [
createElement('h1', '我是大标题'),
createElement('h2', '我是二标题'),
createElement('h3', '我是三标题')
])
}
})
怎样理解单向数据流
这个概念出现在组件通信。⽗组件是通过 prop 把数据传递到⼦组件
的,但是这个 prop 只能由⽗组件修改,⼦组件不能修改,否则会报
错。⼦组件想修改时,只能通过 $emit 派发⼀个⾃定义事件,⽗组
件接收到后,由⽗组件修改。
⼀般来说,对于⼦组件想要更改⽗组件状态的场景,可以有两种⽅
案:
- 在⼦组件的 data 中拷⻉⼀份 prop,data 是可以修改的,但
prop 不能:
export default {
props: {
value: String
},
data () {
return {
currentValue: this.value
}
}
}
- 如果是对 prop 值的转换,可以使⽤计算属性:
export default {
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase();
}
}
}
⽣命周期
Vue.js ⽣命周期 (https://cn.vuejs.org/v2/api/#...⽣命周期
钩⼦) 主要有 8 个阶段:
*创建前 / 后(beforeCreate / created):在 beforeCreate
阶段,Vue 实例的挂载元素 el 和数据对象 data 都为
undefined,还未初始化。在 created 阶段,Vue 实例的数据
对象 data 有了,el 还没有。
*载⼊前 / 后(beforeMount / mounted):在 beforeMount
阶段,Vue 实例的 $el 和 data 都初始化了,但还是挂载之前
为虚拟的 DOM 节点,data 尚未替换。在 mounted 阶段,
Vue 实例挂载完成,data 成功渲染。
*更新前 / 后(beforeUpdate / updated):当 data 变化
时,会触发 beforeUpdate 和 updated ⽅法。这两个不常
⽤,且不推荐使⽤。
*销毁前 / 后(beforeDestroy / destroyed):
beforeDestroy 是在 Vue 实例销毁前触发,⼀般在这⾥要通
过 removeEventListener 解除⼿动绑定的事件。实例销毁
后,触发 destroyed。
组件间通信
- ⽗⼦通信:
⽗向⼦传递数据是通过 props,⼦向⽗是通过
events($emit);通过⽗链 / ⼦链也可以通信($parent /
$children);ref 也可以访问组件实例;provide / inject
API。
- 兄弟通信:
Bus;Vuex;
- 跨级通信:
Bus;Vuex;provide / inject API。
路由的跳转⽅式
⼀般有两种:
- 通过 ,router-link 标签会渲 染为 标签,在 template 中的跳转都是⽤这种;
- 另⼀种是编程式导航,也就是通过 JS 跳转,⽐如 router.push('/home')。
Vue.js 2.x 双向绑定原理
核⼼的 API 是通过 Object.defineProperty() 来劫持各个属性的
setter / getter,在数据变动时发布消息给订阅者,触发相应的监听
回调,这也是为什么 Vue.js 2.x 不⽀持 IE8 的原因(IE 8 不⽀持此
API,且⽆法通过 polyfill 实现)
Vue.js ⽂档已经对 深⼊响应式原理
(https://cn.vuejs.org/v2/guide/reactivity.html) 解释的很透彻
了。
什么是 MVVM,与 MVC 有什么区别
MVVM 模式是由经典的软件架构 MVC 衍⽣来的。当 View(视图
层)变化时,会⾃动更新到 ViewModel(视图模型),反之亦然。
View 和 ViewModel 之间通过双向绑定(data-binding)建⽴联
系。与 MVC 不同的是,它没有 Controller 层,⽽是演变为
ViewModel。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,
⽽ View 和 Model 之间的同步⼯作是由 Vue.js 完成的,我们不需
要⼿动操作 DOM,只需要维护好数据状态。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。