天天看点

render函数&高级组件&jsx基本使用

前言

在学习了解组件复用的时候查看资料,看到了

mixins,extend

方法同时也查到了

高级函数(Higher-Order Components)和jsx

等字眼。听上去hoc和jsx有种高级的感觉,那得好好研究下。

概念

高阶组件定义 : 高阶组件是一个方法,这个方法接收一个原始组件作为参数,并返回新的组件。
jsx定义 : JSX 是一种类似于 XML 的 JavaScript 语法扩展 JSX 不是由引擎或浏览器实现的。相反,我们将使用像 Babel 这样的转换器将 JSX 转换为常规 JavaScript。基本上,JSX 允许我们在 JavaScript 中使用类似 HTML 的语法。

template模板生成方式

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

其实我们传入template模板之后,在

vue底层也会将其转换成render函数

vue源码 src/platform/web/entry-runtime-with-compiler.js文件中代码如下,我们只需要

关注tempalte变量

,首先判断配置项中有无传入render,如果没有也会有很多种情况,无论哪种情况都会给template变量

赋值一个DOM字符串

,最终通过

compileToFunctions函数将其转换成render函数

,通过

结构赋值的方式赋值给render变量

,然后添加到实例上 具体可以看这篇文章

if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
    }
  }
复制代码
           

render语法

使用之前需要的配置和使用语法这里我们主要来看

props 、class、on(事件)、slots

这四个属性,首先来看代码

// 子组件
<script>
export default {
  name: 'judge',
  props: {
    type: {
      type: Number | String,
      default: 1
    }
  },
  render(h) {
    return h(
      'div',
      {
        class: {
          default: true,
          first: this.type == 1,
          second: this.type == 2,
          third: this.type == 3
        },
        on: {
          click: this.onShowTypeClick
        }
      },
      [this.$slots.default, this.$slots.tag]
    )
  },
  methods: {
    onShowTypeClick() {
      console.log(this.type)
    }
  }
}
</script>

<style hljs-string">'scss' scoped>
.default {
  text-align: center;
  width: 200px;
  line-height: 50px;
  background-color: #eee;
}
.first {
  background-color: #f66;
}
.second {
  background-color: #c66;
}
.third {
  background-color: #804;
}
</style>

复制代码
           
// 父组件
   <input type="text"
      v-model="number">
    <judge :type="number">
      <span class="render__incoming"> 外来者</span>
      <p class="render__incoming-p"
        slot="tag">我是p标签</p>
    </judge>
复制代码
           

render函数的

功能创建虚拟节点

就是代替template所以 在.vue 文件中

必须省略template标签

createElement

render函数默认接受

createElement 函数

createElement默认接受三个参数

createElement(
 // {String | Object | Function}
 // 一个 HTML 标签字符串,组件选项对象,或者
 // 解析上述任何一种的一个 async 异步函数。必需参数。
 'div',

 // {Object}
 // 一个包含模板相关属性的数据对象
 // {String | Array}
 // 子虚拟节点 (VNodes),由 `createElement()` 构建而成,
 [
   '先写一些文字',
   createElement('h1', '一则头条'),
   this.$slots.default
 ]
)
复制代码
           

除了第一个参数,

后面两个都为可选参数

,第二个参数是

配置选项

如:style、attrs、props等,

注意的是使用了domProps属性下的innerHTML后,会覆盖子节点和slot插入的元素

第三个参数是关于子节点

如:再用createElement函数创建一个虚拟子节点,或者是接受slot传递过来的DOM节点

hoc高阶函数

同样来看

props 、class、on(事件)、slots

这四个属性的实现。

// hoc 组件
export default componentA => {
 return {
   // hoc 组件本身没有设置props 需要设置传入的组件相同的props
   props: componentA.props,
   render(h) {
     let slots = {}
     Object.keys(this.$slots).map(item => {
       slots[item] = () => this.$slots[item]
       return slots
     })
     return h(componentA, {
       attrs: this.$attrs,
       props: this.$props,
       scopedSlots: slots,
       on:this.$listeners
     })
   }
 }
}

复制代码
           
// 父组件
   <packer :test="15"
     v-on:customize-click="onCuClick">
     插入信息
     <p slot="test">1516545</p>
   </packer>
复制代码
           
// componentA 组件
<template>
 <div>
   <span @click="handleClick">props: {{prop}}</span>
   <slot name="test"></slot>
   ============
   <slot></slot>
 </div>
</template>
复制代码
           

实现过程

在vue中

组件可以看作是一个对象

,里面包括一些方法属性如:

data、props、methods、钩子函数等

,那hoc函数最终也是要返回这样一个对象。我们重新来温习下hoc高阶组件的定义,

一个函数接收一个组件,并且返回一个新的组件

。可以这样理解,

接受一个组件,并且对整个组件进行包装然后返回出去

。当然hoc函数返回出去的对象应该也具有组件应该有的options如:

data、props、methods、钩子函数等

来解释下本例中的实现过程,给hoc函数设置props和

componentA

相同的props属性,这样我们也可以通过packer向hoc组件传入props。在hoc接受到外部传入的值后通过this.$props将hoc实例中的props值都传递给

componentA

,当然我们也可以给hoc设置多于

componentA

的props值。

attrs 指的是那些没有被声明为 props 的属性

通过scopedSlots实现hoc组件传入slot插槽

scopedSlots: {
   // 实现默认插槽和具名插槽
   default: ()=>this.$slots.default,
   test: ()=>this.$slots.test
 }
复制代码
           
listeners: (2.3.0+) 一个包含了所有在父组件上注册的事件侦听器的对象。这只是一个指向 data.on 的别名。

listeners指的就是在父组件上绑定的所有事件(是一个对象)格式为 { [key: string]: Function | Array } 键名为字符串,值为函数多个用数组

<packer v-on:customize-click="onCuClick"></packer>
复制代码
           

所以我们需要在hoc组件中设置

componentA

的父组件事件

jsx

首先我们来看下用render函数实现使用element-ui table组件

export default {
 render(h) {
     return  h(
         'el-table',
         {
           props: {
             data: [
               {
                 date: '2016-05-02',
                 name: '王小虎',
                 address: '上海市普陀区金沙江路 1518 弄'
               }
             ]
           }
         },
         [
           h('el-table-column', {
             attrs: {
               prop: 'date',
               label: '日期',
               width: '180'
             }
           })
         ]
       )
 }
复制代码
           

可以看到只是两次嵌套关系的render函数写起来的代码很难维护,这里就要用到jsx语法。它可以让我们回到更接近于模板的语法上。

export default {
  data() {
    return {
      tableData: [
        {
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄'
        }
      ]
    }
  },
  render(h) {
    return (
      <el-table data={this.tableData}>
        <el-table-column prop="date" label="日期" width="180" />
      </el-table>
    )
  }
}

复制代码
           

实现jsx中v-model

<script>
export default {
  data() {
    return {
      initValue: ''
    }
  },
  render() {
    return (
      <div class="input__wrap">
        <div>initValue的值:{this.initValue}</div>
        <el-input
          value={this.initValue}
          on-input={val => {
            this.initValue = val
          }}
        />
      </div>
    )
  }
}
</script>

<style hljs-string">'scss' scoped>
.input__wrap {
  width: 200px;
}
</style>

复制代码
           

实现jsx中 v-for

<script>
export default {
  data() {
    return {
      tagList: ['标签1', '标签2', '标签3']
    }
  },
  render() {
    return (
      <div class="tag__wrap">
        {this.tagList.map((item, index) => {
          return <el-tag key={index}>{item}</el-tag>
        })}
      </div>
    )
  }
}
</script>
<style>
.tag__wrap {
  width: 200px;
  display: flex;
  justify-content: space-between;
}
</style>
复制代码
           

jsx 扩展符

扩展符将一个对象的属性智能的合并到组件optiion中

<script>
export default {
  name: 'sync',
  created() {
    console.log(this)
  },
  data() {
    return {
      obj: {
        class: ['sync__class1', 'sync__class2']
      }
    }
  },
  render() {
    return <div {...this.obj} />
  }
}
</script>

复制代码
           

使用扩展符能方便我们插入多个class时,案例中发现如下报错

提示我们这个对象以及observed查看源码如下,定义在src/core/vdom/create-element.js 中的_createElement函数中

if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
复制代码
           

这段代码判断data中中定义的对象以及data的__ob__,如果已经定义(代表已经被observed,上面绑定了Oberver对象)则提示

Avoid using observed data object ...

错误信息。

经过分析后,改写成

<script>
export default {
  name: 'sync',
  render() {
    const obj = {
      class: ['sync__class1', 'sync__class2']
    }
    return <div {...obj} />
  }
}
</script>

复制代码
           

jsx sync实现

<script>
export default {
  name: 'sync',
  render() {
    return (
      <div class="sync__click" onClick={this.onClick}>
        <el-pagination
          total={10}
          current-page={this.index}
          page-size={5}
          {...{
            on: {
              'update:currentPage': value => {
                this.index = value
                console.log('页数改变了')
              }
            }
          }}
        />
      </div>
    )
  },
  data() {
    return {
      index: 1
    }
  },
  methods: {
    onClick() {
      console.log('点击事件触发了')
    }
  }
}
</script>
<style>
.sync__click {
  width: 400px;
  height: 500px;
  background-color: #efefef;
}
</style>

复制代码
           

总结

本文主要是认识了

高阶组件和jsx的定义和基本的使用方

式。再这之前梳理了template生成的过程,底层都是

通过render函数

来实现的。接下来讲了

render的相关语

法,以及书写render会带来维护的困难,不易读懂的困扰。最后使用

jsx语法简化render书写

,并且

举了几个vue常用的指令,除了v-show其他的实现都要通过自己来实现

。当然jsx的知识点还有很多,在有了以上知识的基础上,再去研究jsx的更多语法以及使用场景也会更加轻松些。

参考

hoc issues

babel-plugin-transform-vue-jsx

腾讯web

vue 官网

render 函数

继续阅读