render函数~

1,055 阅读4分钟
new Vue({
  el: '#app',
  render: h => h(App)
})

2021年了不会还有人对上面的代码不熟悉吧,通过脚手架构建的项目,main.js中都会有上面这些代码。

el是挂载的DOM元素,可以是DOM对象、选择器;

Render就是今天的大哥,如上是一个箭头函数,形参h也是一个函数,传入一个组件对象,将这个组件挂在到el上

createElement函数

这个函数就是上面Render函数的参数,运行后会返回一个虚拟DOM对象(Virtual Dom),最多传三个参数

createElement (el, ?description, ?VNode)
el 必选: { String | Object | Function }
一个HTML标签、组件选项或一个函数(用于函数化组件),例:"div"

description 可选: { Object }
一个对应属性的数据对象,作用于el参数上
具体选项如下:
{
// 动态绑定class, :class = { 'active': true }
  'class': {
    active: true
  },
// 动态绑定style, :style = { 'color': 'red' }
  'style': {
    color: 'red'
  },
// DOM元素的attribute属性, id = 'szgg', name = 'szgg'
  'attrs': {
    id: 'szgg',
    name: 'szgg'
  },
// 组件接收到的prop属性, 给子组件传值时使用
  {
    props: {
      count: '0'
    }
  },
// DOM元素的property属性
  domProps: {
    innerHTML: '优雅永不过时'
  },
// 事件监听器,修饰符不可用,需自己实现 @click=clickFn1
  on: {
    click: this.clickFn1
  },
// 监听组件的原生事件,仅用于组件上,实际上还是使用$emit发送的事件 @click.native=clickFn2
  nativeOn: {
    click: this.clickFn2
  },
// 自定义指令, v-my-directives:szgg.bar="1 + 1"
  directives: [
    {
      name: 'my-directives', // 指令名
      value: 2, // 指令的绑定值
      expression: '1 + 1', // 指令的表达式
      arg: 'szgg', // 指令的参数
      modifiers: { // 指令的修饰符
        'bar': true
      }
    }
  ],
// 作用域插槽, { name: props => VNode | Array<VNode> } 
/**
<template #default="scope">
  <span>{{scope.text}}</span>
</template>
*/
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
// 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'szgg',
// 其它特殊顶层 property
  key: 'myKey',
// ref属性
  ref: 'myRef',
// ref与v-for同时使用时,添加此属性,$refs.myRef会变成一个数组
  refInFor: true  
}

VNode 可选: { String | Array }
子节点,多个子节点就放入数组中传入
例:'西内' | ['无情铁手', '致残打击']

实现一个点击功能,点击文字变色

Snipaste_2021-04-19_10-40-28.png 点击后=> Snipaste_2021-04-19_10-40-16.png

// 挂载Vue实例,使用全局组件
<div id="app">
  <ele></ele>
</div>
...
.active {
  color: pink;
}
// template属性写法
<script>
  Vue.component('ele', {
    template: `
      <div id="szgg" :class="{'active': isActive}" ref="szgg" @click="clickFn">{{message}}</div>
    `,
    data(){
      return {
        isActive: true,
        message: 'SZGG'
      }
    },
    methods: {
      clickFn () {
        this.isActive = !this.isActive
      }
    }
  });
  new Vue({
    el:'#app',
    data:{
      message:'HelloWorld'
    }
  })
</script>
// render函数改写
<script>
  Vue.component('ele', {
    data(){
      return {
        isActive: true
      }
    },
    render(createElement) {
      return createElement('div', {
        domProps: {
          id: 'szgg'
        },
        class: {
          'active': this.isActive
        },
        ref: 'szgg',
        on: {
          click: this.clickFn
        }
      }, [createElement('p', 'SZGG')])
    },
    methods: {
      clickFn () {
        this.isActive = !this.isActive
      }
    }
  });
  new Vue({
    el:'#app'
  })
</script>

上面只是Render函数的初体验,我们可以使用它实现页面的逻辑,同时也可以用js模板实现v-if,v-for,v-model

v-if,v-for,v-model

我们使用逻辑实现以上三个指令,而不是写自定义的指令实现,有兴趣的可以自己写写

v-if在render函数中的实现

// v-if 的实现,就是if/else重写
<script>
  Vue.component('ele', {
    data(){
      return {
        isActive: true
      }
    },
    render(createElement) {
      if (this.isActive) {
        return createElement('div', {
          domProps: {
            id: 'szgg'
          },
          class: {
            'active': this.isActive
          },
          ref: 'szgg',
          on: {
            click: this.clickFn
          }
        }, [createElement('p', 'SZGG')])
      } else {
        return createElement('div', {
          on: {
            click: this.clickFn
          }
        }, '啥也没有')
      }
    },
    methods: {
      clickFn () {
        this.isActive = !this.isActive
      }
    }
  });
  new Vue({
    el:'#app'
  })
</script>

v-for在render函数中的实现

// v-for 的实现,就是数组的map方法重写
<script>
  Vue.component('ele', {
    data(){
      return {
        list: [1, 2, 3, 4, 5]
      }
    },
    render(createElement) {
      if (this.list.length) {
        return createElement('ul', [this.list.map(i => createElement('li', i))])
      } else {
        return createElement('p', '你咋没数据呢')
      }
    }
  });
  new Vue({
    el: '#app'
  })
</script>

v-model在render函数中的实现

<script>
  Vue.component('ele', {
    data(){
      return {
        value: '来了'
      }
    },
    render(createElement) {
      // 保存一下当前的this对象,即组件对象ele
      let _this = this;
      return createElement('input', {
        domProps: {
          value: _this.value
        },
        on: {
          input(event){
            _this.value = event.target.value
          }
        }
      })
    }
  });
  new Vue({
    el: '#app'
  })
</script>

事件修饰符和按键修饰符

表1-1 部分事件修饰符和按键修饰符及对应的句柄

修饰符对应句柄
.stopevent.stopPropagation()
.preventevent.preventDefault()
.selfif (event.target !== event.currentTarget) return
.enter/.13if (event.keyCode !== 13) return
.ctrl,.alt,.shift,.metaif (!event.ctrlKey) return

对于事件修饰符.capyure,.once,Vue提供了特殊的前缀

表1-2 事件修饰符.capture,.once的前缀

修饰符前缀
.capture!
.once~
.once.capture~!

修饰符有很多,这里我们只实现一个 .enter功能,在输入框里输入文字按enter打在公屏上,如下

<script>
  Vue.component('ele', {
    data(){
      return {
        value: '',
        list: []
      }
    },
    render(createElement) {
      // 保存一下当前的this对象,即组件对象ele
      let _this = this;
      let listNode;
      if (this.list.length) {
      // 获取list列表中的VNodes
        listNode = createElement('ul', this.list.map(i => createElement('li', i)))
      } else {
        listNode = createElement('p', '你快整点优雅的句子')
      }
      return createElement('div', [
        createElement('input', {
          attrs: {
            placeholder: '请输入优雅的语句'
          },
          domProps: {
            value: _this.value
          },
          on: {
            input(event){
              _this.value = event.target.value
            },
            // 监听键盘按下事件,是Enter的话再执行逻辑
            keydown(event){
              if (event.key !== 'Enter') return;
              _this.list.push(event.target.value);
              _this.value = ''
            }
          }
        }),
        listNode
      ])
    }
  });
  new Vue({
    el: '#app'
  })
</script>

render函数中的插槽

首先要先判断组件是否使用了插槽,即组件标签之间是否有内容,可以用$slots.default获取

默认插槽

<script>
  Vue.component('ele', {
    render(createElement) {
      console.log(this.$slots.default);
      if (this.$slots.default) {
        return createElement('div', this.$slots.default)
      } else {
        return createElement('div', '你没用插槽啊')
      }
    }
  });
  new Vue({
    el: '#app'
  })
</script>

作用域插槽

作用域插槽的实现有两个点:

1.在slot标签上绑定数据,即<slot :text="message"></slot>

2.在父组件中使用绑定的数据,即<child v-slot="props"><span>{{ props.text }}</span></child>

代码实现

<div id="app">
  <ele>
    <template #default="scope">
      <span>{{scope.text}}</span>
    </template>
  </ele>
</div>
<script>
  Vue.component('ele', {
    data(){
      return {
        message: 'HelloWorld'
      }
    },
    render(createElement) {
    // 相当于`<div><slot :text="message"></slot></div>`,通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数
      return createElement('div', [
          this.$scopedSlots.default({
            text: this.message
          })
      ])
    }
  });
  new Vue({
    el: '#app'
  })
</script>

上面的代码实现了第一步,通过测试我们可以在页面上显示出正确的内容,但是还是用template获取绑定的数据,下面我们再使用Render函数实现第二步。

<div id="app">
</div>
<script>
  Vue.component('ele', {
    data(){
      return {
        message: 'HelloWorld'
      }
    },
    render(createElement) {
      return createElement('div', [
          this.$scopedSlots.default({
            text: this.message
          })
      ])
    }
  });
  new Vue({
    render(createElement) {
      return createElement('div', [
      // 相当于`<div><ele v-slot="props"><span>{{ props.text }}</span></ele></div>`
          createElement('ele', {
            scopedSlots: {
              default: function (scope) {
                return createElement('span', scope.text)
              }
            }
          })
      ])
    }
  }).$mount('#app')
</script>

上面代码为完成代码,使用Render函数实现了作用域插槽的功能

函数化组件

了解完上述信息,我们可以尝试用Render函数去描绘一个组件,在函数中定义组件的属性、行为、样式,那么这么做的好处是什么?

文章开头说到Render函数返回的是一个VNode,更容易进行渲染,如果我们把组件用函数化,就能提高渲染的效率,减少渲染开销

Vue.js中提供了一个属性functional,设置为true时可以是组件无状态和无实例,也就是没有data和this上下文,那么我们如何获取数据呢?这时Render函数提供了第二个参数context来提供临时上下文,可以通过context来获取组件需要的data,props,slots,children,parent属性, 例如:this.message要改写为context.props.level, this.$slots.default要改写为context.children

下面看一个例子:

<div id="app">
<!--2. 给组件传入Vue实例中的数据-->
  <ele :l="l"></ele>
  <button @click="changeFn(1)">H1</button>
  <button @click="changeFn(2)">H2</button>
  <button @click="changeFn(3)">H3</button>
</div>
<script>
// 定义一个组件对项
  let levelTitle = {
  // 5. 数据最终传入函数化的组件
    props: ['level'],
    render(createElement) {
      return createElement(`h${this.level}`, '我是' + this.level)
    }
  };
  Vue.component('ele', {
    // 函数化组件开启
    functional: true,
    render(createElement, context) {
      return createElement(levelTitle, {
      // 4. 拿到传入本组件的props,再传入组件ele
        props: {
          level: context.props.l
        }
      })
    },
    // 3. 组件中接受数据
    props: {
      l: {
        type: Number
      }
    }
  });
  new Vue({
  // 1. 定义初始化数据
    data: {
      l: 1
    },
    methods: {
      changeFn(level){
        this.l = level
      }
    }
  }).$mount('#app')
</script>

相信通过上面的例子应该对函数化组件有相应的了解了,也让我们折服在Render函数的魅力之下,但相比模块化的template来说,它的代码写起来太多,很不方便,所以函数化组件主要用于以下两个场景:

程序化的在多个组件中选择一个

在children,props,data传递给子组件之前操作他们

Render函数的学习告一段落,它给我们提供了另一种描绘组件的方法,让我们感受到js的强大之处,通过这些练习,我受益良多。

借鉴:Vue官网Vue.js实战(书籍)