渲染函数render
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
让我们深入一个简单的例子,这个例子里 render 函数很实用。
假设我们要生成一些带锚点的标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
对于上面的 HTML,你决定这样定义组件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
当开始写一个只能通过 level
prop 动态生成标题 (heading) 的组件时,你可能很快想到这样实现
<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 <slot></slot>
,在要插入锚点元素时还要再次重复。
render函数
render对象的属性值是一个函数 function createElement (){} 的返回值
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
在这个例子中,向组件中传递不带v-slot
指令的子节点时,比如 anchored-heading
中的 Hello world!
,这些子节点被存储在组件实例中的 $slots.default
中。
非简写
const App = {
data(){
return {
num:1
}
},
methods:{
changeNum(){
this.num++;
}
},
template:(`
<button @click="changeNum">{{num}}</button>
`)
};
// 返回一个对象
render:function(createElement){
return createElement(App)
}
简写
// 返回一个对象
render(createElement){
return createElement(App)
}
箭头函数
// 返回一个对象
render:h=> h(App)
createElement参数
接下来你需要熟悉的是如何在 createElement
函数中使用模板中的那些功能。这里是 createElement
接受的参数:
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
1- template相较于render优先级要低。
2- render生成的虚拟DOM,而template的值是字符串(字符串也会在内部转为虚拟DOM)
3- render效率会更高。
render函数
接收一个函数(createElement)
将该函数运行的结果进行返回
返回一个字符串
render(createElement){
return createElement("h3","你好吗?")
},
返回一个组件对象
render(createElement){
return createElement({
data(){
return {
num:1
}
},
methods:{
changeNum(){
this.num++;
}
},
template:(`
<button @click="changeNum">{{num}}</button>
`)
})
}
混入
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
特点:
mixin 中的生命周期函数会和组件中的生命周期函数一起合并执行
mixin 中的data数据在组件中也可以使用
mixin 中的方法在组件内部可以直接调用
生命周期函数 合并后 执行顺序:先执行mixin中的,后执行组件的
例子
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
new Vue({
mixins: [mixin],
data: function () {
return {
message: 'goodbye',
bar: 'def'
}
},
created: function () {
console.log(this.$data)
// => { message: "goodbye", foo: "abc", bar: "def" }
}
})
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
}
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
注意:Vue.extend() 也使用同样的策略进行合并。
csdn例子
以上代码引入 mixin 的方法很简单,直接使用vue提供给我们的mixins 属性:mixins:[mixins]
特点
mixin 中的生命周期函数会和组件中的生命周期函数一起合并执行
mixin 中的data数据在组件中也可以使用
mixin 中的方法在组件内部可以直接调用
生命周期函数 合并后 执行顺序:先执行mixin中的,后执行组件的
局部混入
首先在 src 同级目录下 创建 mixin/index.js 文件
export const mixins = {
data() {
return {
msg: "我是mixin中的数据",
};
},
computed: {},
created() {
console.log("我是 mixin 中的 created 生命周期函数");
},
mounted() {},
methods: {
geMes(){
console.log("我是点击事件 hhhhh");
}
},
};
在组件中使用
<script>
import { mixins } from "./mixin/index";
export default {
name: "app",
mixins: [mixins],
created() {
console.log("我是app的生命周期 created",this.msg);
this.geMes()
}
};
运行结果
那么多个组件使用相同的 mixin, 当mixin被修改了 其他组件数据会发生变化吗
例子2
demo.vue
// demo.vue
<template>
<div>
<p>我是 demo 组件</p>
<p>我使用了 mixin 中的数据 msg —————— {{ msg }}</p>
</div>
</template>
<script>
import { mixins } from "@/mixin/index";
export default {
name: "app",
mixins: [mixins],
}
</script>
app.vue
// app.vue
<template>
<div>
<!-- 组件 -->
<demo/>
<!-- app 本体内容 -->
<div>
我是app页面的按钮 点击修改mixin中的msg
<p>mixin —————— {{ msg }}</p>
<button @click="setMsg">修改msg</button>
</div>
</div>
</template>
<script>
import demo from './components/demo.vue';
import { mixins } from "./mixin/index";
export default {
components: { demo },
name: "app",
mixins: [mixins],
methods: {
setMsg() {
this.msg = "我被修改了!!!"
}
}
在app组件更改了 msg,demo没有变化,所以 不同组件中的mixin 是相互独立的
全局混入
在mian.js 挂载mixin就可以每个组件使用了
import Vue from 'vue'
import App from './App.vue'
import { mixins } from "./mixin/index";
Vue.mixin(mixins)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
选项合并
当mixin中定义的属性或方法的名称与组件中定义的名称发生冲突怎么办?
从源码上看 可以分为如下几种类型
- 替换型
- 合并型
- 队列型
- 叠加型
替换型
替换型合并有props
、methods
、inject
、computed
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal
const ret = Object.create(null) // 创建一个第三方对象 ret
extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中
if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中
return ret
}
strats.provide = mergeDataOrFn
同名的
props
、methods
、inject
、computed
会被后来者代替
合并型
和并型合并有:data
strats.data = function(parentVal, childVal, vm) {
return mergeDataOrFn(
parentVal, childVal, vm
)
};
function mergeDataOrFn(parentVal, childVal, vm) {
return function mergedInstanceDataFn() {
var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象
var parentData = parentVal.call(vm, vm)
if (childData) {
return mergeData(childData, parentData) // 将2个对象进行合并
} else {
return parentData // 如果没有childData 直接返回parentData
}
}
}
function mergeData(to, from) {
if (!from) return to
var key, toVal, fromVal;
var keys = Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
toVal = to[key];
fromVal = from[key];
// 如果不存在这个属性,就重新设置
if (!to.hasOwnProperty(key)) {
set(to, key, fromVal);
}
// 存在相同属性,合并对象
else if (typeof toVal =="object" && typeof fromVal =="object") {
mergeData(toVal, fromVal);
}
}
return to
mergeData函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:
当目标 data 对象不包含当前属性时,调用 set 方法进行合并(set方法其实就是一些合并重新赋值的方法)
当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性
队列型
队列性合并有:全部生命周期和watch
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// watch
strats.watch = function (
parentVal,
childVal,
vm,
key
) {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
/* istanbul ignore if */
if (!childVal) { return Object.create(parentVal || null) }
{
assertObjectType(key, childVal, vm);
}
if (!parentVal) { return childVal }
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
生命周期钩子和watch
被合并为一个数组,然后正序遍历一次执行
叠加型
叠加型合并有:component
、directives
、filters
strats.components=
strats.directives=
strats.filters = function mergeAssets(
parentVal, childVal, vm, key
) {
var res = Object.create(parentVal || null);
if (childVal) {
for (var key in childVal) {
res[key] = childVal[key];
}
}
return res
}
叠加型主要是通过原型链进行层层的叠加
替换型策略有props、methods、inject、computed,就是将新的同名参数替代旧的参数
合并型策略是data, 通过set方法进行合并和重新赋值
队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
叠加型有component、directives、filters,通过原型链进行层层的叠加
mixin给我们提供了方便的同时也给我们带来了灾难,所以有很多时候不建议滥用它,但是在有些场景下使用它又是非常合适的,这就得根据自己来取舍了。所以在很多时候我们需要考虑用公共组件还是使用mixin。
优点
提高代码复用性
无需传递状态
维护方便,只需要修改一个地方即可
缺点
命名冲突
滥用的话后期很难维护
不好追溯源,排查问题稍显麻烦
不能轻易的重复代码