Skip to content

渲染函数render

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

让我们深入一个简单的例子,这个例子里 render 函数很实用。

假设我们要生成一些带锚点的标题:

html
<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>

对于上面的 HTML,你决定这样定义组件接口:

html
<anchored-heading :level="1">Hello world!</anchored-heading>

当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,你可能很快想到这样实现

javascript
<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>
javascript
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 <slot></slot>,在要插入锚点元素时还要再次重复。

render函数

javascript
render对象的属性值是一个函数 function createElement (){} 的返回值
javascript
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 中。

非简写

javascript
const App = {
    data(){
        return {
            num:1
        }
    },
    methods:{
        changeNum(){
            this.num++;
        }
    },
    template:(`
		<button @click="changeNum">{{num}}</button>
	`)
};
javascript
// 返回一个对象
render:function(createElement){
    return createElement(App)
}

简写

javascript
// 返回一个对象
render(createElement){
	return createElement(App)
}

箭头函数

javascript
// 返回一个对象
render:h=> h(App)

createElement参数

接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能。这里是 createElement 接受的参数:

javascript
// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)
javascript
1- template相较于render优先级要低。
2- render生成的虚拟DOM,而template的值是字符串(字符串也会在内部转为虚拟DOM)
3- render效率会更高。
javascript
render函数
接收一个函数(createElement)
将该函数运行的结果进行返回

返回一个字符串

javascript
render(createElement){
 	return createElement("h3","你好吗?")
},

返回一个组件对象

javascript
render(createElement){
    return createElement({
        data(){
            return {
                num:1
            }
        },
        methods:{
            changeNum(){
                this.num++;
            }
        },
        template:(`
			<button @click="changeNum">{{num}}</button>
		`)
    })
}

混入

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

特点:

js
mixin 中的生命周期函数会和组件中的生命周期函数一起合并执行
mixin 中的data数据在组件中也可以使用
mixin 中的方法在组件内部可以直接调用
生命周期函数 合并后 执行顺序:先执行mixin中的,后执行组件的

例子

js
// 定义一个混入对象
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!"

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

js
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" }
  }
})

同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

js
var mixin = {
  created: function () {
    console.log('混入对象的钩子被调用')
  }
}

new Vue({
  mixins: [mixin],
  created: function () {
    console.log('组件钩子被调用')
  }
})

// => "混入对象的钩子被调用"
// => "组件钩子被调用"

值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

js
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例子

js
以上代码引入 mixin 的方法很简单,直接使用vue提供给我们的mixins 属性:mixins:[mixins]
特点
mixin 中的生命周期函数会和组件中的生命周期函数一起合并执行
mixin 中的data数据在组件中也可以使用
mixin 中的方法在组件内部可以直接调用
生命周期函数 合并后 执行顺序:先执行mixin中的,后执行组件的
js
局部混入
首先在 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()
  }
};

运行结果

image-20240827150716174

那么多个组件使用相同的 mixin, 当mixin被修改了 其他组件数据会发生变化吗

例子2

demo.vue

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

js
// 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 = "我被修改了!!!"
    }
  }

image-20240827150847866

在app组件更改了 msg,demo没有变化,所以 不同组件中的mixin 是相互独立的

全局混入

在mian.js 挂载mixin就可以每个组件使用了

js

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中定义的属性或方法的名称与组件中定义的名称发生冲突怎么办?

从源码上看 可以分为如下几种类型

  • 替换型
  • 合并型
  • 队列型
  • 叠加型

替换型

替换型合并有propsmethodsinjectcomputed

js

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

同名的propsmethodsinjectcomputed会被后来者代替

合并型

和并型合并有:data

js

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

js
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被合并为一个数组,然后正序遍历一次执行

叠加型

叠加型合并有:componentdirectivesfilters

js
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
}

叠加型主要是通过原型链进行层层的叠加

js
替换型策略有props、methods、inject、computed,就是将新的同名参数替代旧的参数
合并型策略是data, 通过set方法进行合并和重新赋值
队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
叠加型有component、directives、filters,通过原型链进行层层的叠加

mixin给我们提供了方便的同时也给我们带来了灾难,所以有很多时候不建议滥用它,但是在有些场景下使用它又是非常合适的,这就得根据自己来取舍了。所以在很多时候我们需要考虑用公共组件还是使用mixin。

js
优点
提高代码复用性
无需传递状态
维护方便,只需要修改一个地方即可
缺点
命名冲突
滥用的话后期很难维护
不好追溯源,排查问题稍显麻烦
不能轻易的重复代码