Vue 2x 中使用 render 和 jsx 的最佳实践

1,165 阅读17分钟

什么是JSX

最初,JSX是React中所特有的,

它的长相是这样的

let jsx = <h1>hello world</h1>;

从表面上来看,这就是个普通的html标签。但是注意看左边,我们将这个html标签赋值给了一个js变量!

嗯哼?

这就是JSX的功能了。

JSX的全称应该翻译为Javscript + xml(Javscript中的xml),而没有翻译成Javascriptxml,这是因为比起xml/html标签,这更像是在javascript中扩展了一个功能,它把xml当做变量的值赋给js变量,这是对javascript语法的延伸。

因为JSX的这个特性,所以他即具备了Javascript的灵活性,同时又兼具html的语义化和直观性。

另外如果只考虑JSX的长相,我们也常常把它称之为jsx tag

JSX在Vue中的具体使用场景和相对优势

为什么我们要抛弃Vue的优势和各种指令去使用JSX

我们会有一个思考:

函数组件,即简单的无状态组件,适合使用jsxvue文件会很简洁。如果逻辑复杂了,使用vue但不用template,那就等于放弃了vue的优势:丰富的模板指令。还不如直接用react

好的!

现在,让我们来看看Vue官方对渲染函数(Render)& JSX的介绍:

传送门

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 函数重写上面的例子:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

看起来简单多了!这样代码精简很多,但是需要非常熟悉 Vue 的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot 指令的子节点时,比如 anchored-heading 中的 Hello world!,这些子节点被存储在组件实例中的 $slots.default 中。

相信我!认真读完官方对于渲染函数 & JSX这块的介绍文档,你不会再有这个像小朋友一样有那么多问号~

不可否认,

vue中大部分场景是不需要用render函数的,还是用模板更简洁直观.

JSX 书写规范

JSX 支持换行

let jsx = (
    <div>
    	<h1>hello world</h1>
        <button/>
    </div>
)
  • JSX的顶层只能有一个根元素,我们很多时候会在最外层包裹一个div(后续React推出了不占据Dom结构的Fragment,同时,<></>空标签有同样的效果,但是测试后发现这个在vue中不生效,)
  • 为了方便阅读,我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读,并且jsx可以进行换行书写;
  • JSX中的标签可以使单标签,也可以是双标签如果是单标签,必须以/>结尾

JSX 注释都要用花括号{}包起来

{
    //我是单行注释
}

{/*我是一段注释*/}

JSX 插入变量

const t = 'hello world';
let jsx = (
    <div>
    	<h1>{t}</h1>
	<button/>
    </div>
)

JSX 嵌入表达式

  • 运算表达式
const arg1 = 1;
const arg2 = 2;
let jsx = (
    <div>
    	<h1>{arg1 + arg2}</h1>
	<button/>
    </div>
)
  • 三元表达式
const t = 'hello world';
const arg1 = 1;
const arg2 = 2;
const hasButton = true;
let jsx = (
    <div>
    	<h1>
        {
            t === 'hello world' ?  arg1 : arg2
        }
        </h1>
	{
            //如果hasButton为true,则渲染button组件
            hasButton && <button/>
        }
    </div>
)
  • 函数调用
const t = 'hello world';
const arg1 = 1;
const arg2 = 2;
const hasButton = true;
const func1 = ()=>{ return (<div>func1</div>) }
let jsx = (
    <div>
    	<h1>
        {
            //如果在render外定义的函数,请注意加this:this.func1()
            func1()
        }
        </h1>
    </div>
)

JSX 绑定属性

  • 绑定普通属性Attrs
const title = 'hello world';
let jsx = (
    <div>
    	<h1 title={title}>hello world</h1>
        <button/>
    </div>
)
  • 绑定class

在jsx中,class属性需要指定为className,因为class在JavaScript中是保留字段

const hasCss = true;
const h1Css = [
    'flex',
    hasCss ? 'css' : 'noCss',
]
let jsx = (
    <div>
        <h1 className='flex'>hello world</h1>
    	<h1 className={h1Css}>hello world</h1>
        <button/>
    </div>
)
  • 绑定style

在jsx中,windows风格的命名方式(属性、style、方法、event)都需要转换成驼峰式的写法,比如,正常写一个style指定左边的外边距:margin-left:‘10px’,

这里需要改成:marginLeft:‘10px’

const title = 'hello world';
let jsx = (
    <div>
    	<h1 style={{ marginLeft:'10px',width:'100%' }}>hello world</h1>
        <button/>
    </div>
)

JSX 绑定事件

  • JSX中绑定事件类似在HTML原生中绑定事件,只不过React中事件命名采用小驼峰(camelCase),而不是纯小写;
  • 但是我们会发现在我们绑定的回调事件中访问我们对应的this会是undefined,这是因为对应的回调函数是React内部帮我们去进行调用的,React无法确定对应的this所以采用的是callback.apply(undefined,[])方式调用,改变了this的指向为undefined。[这条规则不适用于Vue,因为在Vue中对this做了特殊处理]
function func1(){
    console.log(this); // undefined
}

render(){
    let jsx = (
        <div>
            <button onClick={this.func1}/>
        </div>
    )
    return jsx;
}

如果我们需要在事件中通过this来访问React组件本身属性和方法,有以下几条解决方案:

  • 通过bind绑定this(显示绑定)
function func1(arg1, arg2, e){
    console.log(this); // ReactCom
    
    console.log(arg1); // param1
    
    console.log(arg2); // param2
    
    console.log(e); // Event from buttonClick
}

render(){
    let jsx = (
        <div>
            <button onClick={this.func1.bind(this,'param1','param2')}/>
        </div>
    )
    return jsx;
}

使用bind绑定的方式除了可以非常简单的获取到事件对象event之外,还可以传递我们想要传递的参数

  • 除了显示绑定之外,我们可以使用匿名函数(箭头函数)的方式
function func1(arg1, arg2, e){
    console.log(this); // ReactCom
    
    console.log(arg1); // param1
    
    console.log(arg2); // param2
    
    console.log(e); // Event from buttonClick
}

render(){
    let jsx = (
        <div>
            <button onClick={(e)=> {
                this.func1('param1','param2', e);
            }}/>
        </div>
    )
    return jsx;
}
  • 同理,在声明函数的时候我们使用箭头函数的方式也可以达到同样效果[如果想要传递我们自己的参数,还是需要用到bind]
const func1 = (e) => {
    console.log(this); // ReactCom
    
    console.log(e); // Event from buttonClick
}

render(){
    let jsx = (
        <div>
            <button onClick={this.func1}/>
            <button onClick={this.func1.bind(this,'param1')}/>
        </div>
    )
    return jsx;
}

这里高阶的同学要注意!

如果是在JSX中使用事件绑定,请不要使用箭头函数的方式去声明方法甚至直接在JSX中使用箭头函数绑定事件。

因为根据VR的render渲染机制,如果使用箭头函数,那么每当组件的state发生改变,推动render渲染执行的时候,如果存在箭头函数,每次浏览器都会分配新的内存和额外的开销来执行事件的绑定,组件绑定的层级越深,额外开销越大。

所以,为了最优的性能考虑,请在constructor构造函数中对需要绑定的事件做显示绑定


constructor() {
   this.func1 = this.func1.bind(this);
}

function func1(e){
    console.log(this); // ReactCom
    
    console.log(e); // Event from buttonClick
}

render(){
    let jsx = (
        <div>
            <button onClick={this.func1}/>
        </div>
    )
    return jsx;
}

JSX 条件渲染

  • jsx中,不允许使用ifif-else,请使用三元运算符或者逻辑与&&
  • 同样,也允许使用for循环,请使用JS中的高阶函数mapfilter……
const t = 'hello world';
const arg1 = 1;
const arg2 = 2;
const hasButton = true;
const list = [1,2,3,4,5,6,7,8,9];
let jsx = (
    <div>
    	<h1>
        {
            t === 'hello world' ?  arg1 : arg2
        }
        </h1>
	{
            //如果hasButton为true,则渲染button组件
            hasButton && <button/>
        }
        <ul>
        {
            list.map((item) => <li>{item}</li>)
        }
        </ul>
    </div>
)

createElement

要更透彻的了解和学习JSX,浅析其本质,那么一定要先了解createElement

因为提到JSX,不可避免的需要提到createElement,所以,是不是奇奇怪怪的知识又增加了 : )

从React中看createElement

JSX实际上仅仅是React.createElement(type, config, children)方法的语法糖,该方法接收三个参数:

  • type
    • 当前ReactElement的类型,如果是标签元素,那么使用字符串表示“div”,如果是组件元素直接使用组件的名称就可以。
  • config
    • 我们在JSX中绑定的属性会在config对象中以键值对的形式存在。
  • children
    • 存放标签中的内容,以children数组的形式存储

我们都知道,JSX是通过babel进行解析的,而我们编写JSX的时候必须依赖babel

我们可以再babel的官网查看JSX的转换过程:传送门

<!-- 转换示例代码 -->
<div>
        <h1 className='flex'>hello world</h1>
    	<h1 style={{marginLeft:'10px'}}>hello world</h1>
        <button/>
</div>

image.png

如果我们直接使用React.createElement()来编写代码,就不需要以来bable进行解析也可以正常的渲染显示

render(){
    return 'use strict';
    /*#__PURE__*/
    React.createElement("div", null, /*#__PURE__*/React.createElement("h1", {
      className: "flex"
    }, "hello world"), /*#__PURE__*/React.createElement("h1", {
      style: {
        marginLeft: '10px'
      }
    }, "hello world"), /*#__PURE__*/React.createElement("button", null));
}

我们通过React.createElement()方法最后返回得到的是一个ReactElement对象,

这个ReactElement对象作用是什么?

其实React利用ReactElement对象组成了一个JavaScript对象树,这个对象树就是我们经常讲的一个概念--虚拟DOM(VR DOM),我们可以将之前jsx返回的结果进行打印来查看对应的ReactElemnt对象:

render(){
  const arg1 = 1;
  const arg2 = 4;
  let jsx = (
      <div>
        <div>{arg1 + arg2}</div>
        <div className="flex">
            <button/>
        </div>
        <div className={{marginLeft:'10px'}}>hellow world</div>
      </div>
  )
  console.log(jsx);
  return jsx;
},

3.png

我们编写的JSX代码经过bable编译解析成对应的React.createElement()方法形式,

经过React.createElement()方法调用返回我们对应的ReactElement对象树(虚拟DOM树),对应的ReactElement对象树经过ReactDOM.render()方法转换为真正的DOM在我们的浏览器进行渲染。

JSX -> VR DOM -> DOM

为什么要用VR DOM

  • 很难跟踪状态发生的改变:原有的开发模式,我们很难跟踪到状态发生的改变,不方便针对我们应用程序进行调试;
  • 操作真实DOM性能较低:传统的开发模式会进行频繁的DOM操作,而这一的做法性能非常的低;
  • DOM操作非常耗费性能
    • document.createElement本身创建出来的就是一个非常复杂的对象:传送门
    • DOM操作会引起浏览器的回流和重绘,所以在开发中应该避免频繁的DOM操作

不是用了VR DOM性能就一定会变好

React 从来没有说过 “React 比原生操作 DOM 快”。

React 的基本思维模式是每次有变动就整个重新渲染整个应用。

如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。

很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。

我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:

  • innerHTML: render html string O(template size)  + 重新创建所有 DOM 元素 O(DOM size)
  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

Virtual DOM render + diff 显然比渲染 html 字符串要慢。

但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。

可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。

前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM

它保证了:

  • 不管你的数据变化多少,每次重绘的性能都可以接受;
  • 你依然可以用类似 innerHTML 的思路去写你的应用。

从Vue编译后的代码看createElement

你是否看过写的Vue代码经过编译之后的样子,比如下面这段代码

<template>
    <div>Hellow world<span class="flex">Hellow world</span></div>
</template>

image.png

通过对上面的代码进行分析,不难发现,Vue模板中的每一个元素编译之后都会对应一个createElement

无论是Vue还是React,都存在createElement,而且作用基本一致。

createElement函数返回的值称之为虚拟节点,即VNode,而由VNode扎堆组成的树便是大名鼎鼎的虚拟DOM

到这里,是不是逻辑和上面React提到的是一样的?

(o゜▽゜)o☆[BINGO!]

我们来看看Vue官方文档定义的createElement

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

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

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

从上面可以看出createElement同样有三个参数,三个参数分别是:

  • String | Object | Function
    • 一个 HTML 标签名、组件选项对象(比如div),或者resolve 了上述任何一种的一个 async 函数。
    • 必填项。
  • Object
    • 一个与模板中 attribute 对应的数据对象。
    • 可选。
  • String | Array
    • 子级虚拟节点 (VNodes),由 createElement() 构建而成,也可以使用字符串来生成“文本虚拟节点”
    • 可选。

所以本质上面来说,在Vue里面,你也可以像写React一样,通过Render来使用JSX

在Vue中使用 RenderJSX

在Vue中,通常大家习惯了使用template的语法。

尽管templateJSX 都属于xml的写法,而且他们也比较像,但是本质还是有许多不一样的地方:

老规矩,上传送门

v-model

image.png 当你选择使用JSX的时候,你就要做好和指令说拜拜的时候了。

在JSX中, 你唯一可以使用的指令是v-show,除此之外,其他指令都是不可以使用的,有没有感到很慌,这就对了。不过呢,换一个角度思考,指令只是Vue在模板代码里面提供的语法糖,现在你已经可以写Js了,那些语法糖用Js都可以代替了。

在新版脚手架vue-cli4中,已经默认集成了对v-model的支持,大家可以直接使用,如果你的项目比较老,也可以安装插件babel-plugin-jsx-v-model来进行支持

export default {
  name:'vInput',
  props: {
      value:[String,Number]
  },
  data() {
    return {
      name: ''
    }
  },
  methods: {
    // 监听 onInput 事件进行赋值操作
    handleInput(e) {
      this.name = e.target.value
      
      // 这里对组件实现了v-model语法糖
      this.$emit('input', e.target.value);
    }
  },
  render() {
    // 传递 value 属性 并监听 onInput事件
    return <input value={this.name} onInput={this.handleInput}></input>
    
    // 如果安装了插件或者使用vue-cli4 ,可以和template一样舒服的使用v-model
    return <input v-model={this.name} />
  }
}

// JSX : <Vinput v-model={this.value} />
// or template : <v-input v-model="value" />

注意上面的代码最后注释的代码,因为在JSX中,我们已经通过babel可以得到v-model语法糖的支持,那么我们在使用JSX写自己的组件的时候,一定要注意实现组件的v-model语法糖,去支持该特性。

什么? 你还不懂什么是v-model

快去学习!!!

自定义model

export default {
  name:'vInput',
  props: {
      defaultCode:[String,Number]
  },
  data() {
    return {
      name: ''
    }
  },
  model: {
    prop: 'defaultCode',
    event: 'update'
  },
  methods: {
    // 监听 onInput 事件进行赋值操作
    handleInput(e) {
      this.name = e.target.value
      
      // 这里对组件实现了v-model语法糖
      this.$emit('update', e.target.value);
    }
  },
  render() {
    // 传递 value 属性 并监听 onInput事件
    return <input value={this.name} onInput={this.handleInput}></input>
  }
}

.sync

v-model一样,.sync也需要用属性+事件的方式来实现

.sync目前在JSX中没有任何babel支持:(

export default {
  data(){
      return {
          defaultCode:'',
      },
  },
  methods: {
    handleChangeDefaultCode(value) {
      this.visible = value
    }
  },
  render() {
    return (
      <v-input
        defaultCode={this.defaultCode}
        on={{ 'update:defaultCode': this.handleChangeDefaultCode }}
      ></v-input>
    )
  }
}

v-bind

在template中,我们一般通过 v-bind:prop="value":prop="value"来给组件绑定属性,在JSX里面写法也类似:

render() {
    return <v-input defaultCode={this.defaultCode}></v-input>
}

v-if 与 v-for

5.png 不要着急,这些指令只是黑魔法,用js很容易实现。

  • v-if
render(){
    const arg1 = 1;
    const arg2 = 4;
    return (
        <div>
            {this.show? arg1 : arg2}
        </div>
    )
}

写三元表达式只能写简单的,那么复杂的还得用if/else

  render(){
    const { show } = this;
    let ifText;
    if(show){
        ifText = (<p>1</p>)
    }else{
        ifText = (<p>4</p>);
    }
    // let ifText = show ? (<p>1</p>) : (<p>4</p>);
    
    const showButton = true;
    return (
      <div>
        {ifText}
        {
            showButton && <button/>
        }
      </div>
    )
  }

复制代码
  • v-for
render(){
    const t = 'hello world';
    const arg1 = 1;
    const arg2 = 2;
    const hasButton = true;
    const list = [1,2,3,4,5,6,7,8,9];
    let jsx = (
        <div>
            <h1>
            {
                t === 'hello world' ?  arg1 : arg2
            }
            </h1>
            {
                //如果hasButton为true,则渲染button组件
                hasButton && <button/>
            }
            <ul>
            {
                // 替代 v-for
                list.map((item) => <li>{item}</li>)
            }
            </ul>
        </div>
    )
    return jsx;
}

自定义组件

很简单,只需要导入进来,不用再在components属性声明了,直接写在jsx中比如

<script>
  import Vinput from './vInput'
    export default {
      name: "item",
      render(){
        return (
            <Vinput/>
        )
      }
    }
</script>

v-html 与 v-text

在说v-htmlv-text之前,我们需要先了解一下Vue中的属性,Vue中的属性一共分为三种:

  • props,即组件自定义的属性;
  • attrs,是指在父作用域里面传入的,但并未在子组件内定义的属性。
  • domProps,主要包含三个,分别是innerHTML,textContent/innerTextvalue

v-html

template中,我们用v-html指令来更新元素的innerHTML内容,而在JSX里面,如果要操纵组件的innerHTML,就需要用到domProps

// v-html 指令在JSX的写法是 domPropsInnerHTML

renderContent(h,{ node, data, store }){
      const { dataModel , showIcon, icon, hasOptions} = this;
      const { title, valueFormat } = dataModel;
      const key = isEmpty(title) ? 'label' : title;
      const label = isEmpty(valueFormat) ? data[key] : valueFormat(data);
      if(icon) data.icon = icon;

      const add = this.nodeOptionClick.bind(this,'add', node, data);
      const edit = this.nodeOptionClick.bind(this,'edit', node, data);
      const remove = this.nodeOptionClick.bind(this,'remove', node, data);
      //nativeOnClick={(e)=>{e.stopPropagation();}}
      return (
          <span class="custom-tree-node">
            <div class="left-all" title={data[key]}>
              {
                showIcon && <wg-icon name={data.icon}/>
              }
              // v-html 指令在JSX的写法是 domPropsInnerHTML
              <span class="node-label" domPropsInnerHTML={label}/>
            </div>
            {
              hasOptions && (
                <span class="right-op">
                  <wg-icon class="op-button" onClick={add} name="icon-xinzeng1"> </wg-icon>
                  <wg-icon class="op-button" onClick={edit} name="icon-jianyi"> </wg-icon>
                  <wg-icon class="op-button-danger" onClick={remove} name="icon-shanchu"> </wg-icon>
                </span>
              )
            }
          </span>);
    },

v-text

举一反三,v-text 指令在JSX的写法是 domPropsInnerText

但实际上我们不需要使用domPropsInnerText,而是将文本作为元素的子节点去使用即可

renderContent(h,{ node, data, store }){
      ……
      return (
          <span class="custom-tree-node">
            ……
            <span class="node-label" domPropsInnerText={label}/>
            
            // 但实际上我们不需要使用domPropsInnerText,而是将文本作为元素的子节点去使用即可 
            <span class="node-label">
            { label }
            </span>
            ……
          </span>);
    },

事件 v-on

当我们开发一个组件之后,一般会通过this.$emit('change')的方式对外暴露事件,然后通过v-on:change的方式去监听事件,很遗憾,在JSX中你无法使用v-on指令,但你将解锁一个新的姿势

return (
  <wg-el-select
    {...{ props }}
    {...{ on }}
    v-loading={loading}
    value={this.value}
    onChange={this.mychange}
  >
    {dataSourceM.map((item) => {
      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;
    })}
  </wg-el-select>
 )

JSX中,通过on + 事件名称的大驼峰写法来监听,比如事件change,在JSX中写为onChange

事件监听 .native

监听原生事件的规则与普通事件是一样的,只需要将前面的on替换为nativeOn

return (
  <wg-el-select
    {...{ props }}
    {...{ on }}
    v-loading={loading}
    value={this.value}
    nativeOnChange={this.mychange}
  >
    {dataSourceM.map((item) => {
      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;
    })}
  </wg-el-select>
 )

除了上面的监听事件的方式之外,我们还可以使用对象的方式去监听事件

注意是双花括号,第一个花括号{}表示v-bind,第二个表示这是个对象json

return (
  <wg-el-select
    on={{
        click:this.myclick
    }}
    nativeOn={{
        change:this.mychange,
    }}
  >
    {dataSourceM.map((item) => {
      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;
    })}
  </wg-el-select>
 )

事件修饰符

这里是一个使用所有修饰符的例子:


on: {
  keyup: function (event) {
    // 如果触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
    // ...
  }
}
  • .stop : 阻止事件冒泡,在JSX中使用event.stopPropagation()来代替
    // 阻止 事件冒泡
    event.stopPropagation()
}
  • .prevent:阻止默认行为,在JSX中使用event.preventDefault() 来代替
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
}
  • .self:只当事件是从侦听器绑定的元素本身触发时才触发回调,使用下面的条件判断进行代替
    // 如果触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
}
  • .enter与keyCode: 在特定键触发时才触发回调
    // 如果按下去的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return

除此之外,官方还对此做了一定的优化,提供了前缀语法来帮助我们简化代码:

 render() {
    return (
      <div
        on={{
          // 相当于 :click.capture
          '!click': this.click,
          // 相当于 :input.once
          '~input': this.input,
          // 相当于 :mousedown.passive
          '&mousedown': this.mousedown,
          // 相当于 :mouseup.capture.once
          '~!mouseup': this.mouseup
        }}
      ></div>
    )
  }

7.png

插槽 slots

插槽就是子组件中提供给父组件使用的一个占位符,插槽分为默认插槽具名插槽作用域插槽

8.png

默认插槽

  • 使用默认插槽

JSX中使用默认插槽的用法与普通插槽的用法基本是一致的,如下代码所示:

return (
  <wg-el-select
    on={{
        click:this.myclick
    }}
    nativeOn={{
        change:this.mychange,
    }}
  >
     // 这里就是默认插槽
    {dataSourceM.map((item) => {
      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;
    })}
  </wg-el-select>
 )
  • 自定义默认插槽 你可以通过 this.$slots 访问静态插槽的内容,

这个上面就挂载了一个这个组件内部的所有插槽

this.$slots.default 即代表默认插槽

render() {
    return (
      <div>
        {
            // 通过this.$slots.default定义默认插槽
            this.$slots.default
        }
      </div>
    )
  }

具名插槽

  • 使用具名插槽

    有时候我们一个组件需要多个插槽,这时候就需要为每一个插槽起一个名字,比如element-ui的弹框可以定义底部按钮区的内容,就是用了名字为footer的插槽:

<template>
  <el-dialog
    :title="title"
    :destroy-on-close="destroy"
    :visible.sync="dialogVisible"
    :width="width"
    :before-close="handleClose"
    :append-to-body="true"
  >
    <components :is="model" :_dataForm="formData" :formOtp="formOtp" ref="model">
    </components>
    
    <!-- 具名插槽 -->
    
    <span slot="footer" class="dialog-footer">
      <el-button type="primary" v-if="hasContinue" @click="handleContinue">保存并继续</el-button>
      <el-button type="primary" @click="handleSubmit">保存</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </span>
  </el-dialog>
</template>

修改为JSX:

 render() {
    return (
      <el-dialog title={this.title} visible={this.visible}>
        ……
        {/** 具名插槽 */}
        <template slot="footer">
          <el-button>保存</el-button>
          <el-button>取消</el-button>
        </template>
      </el-dialog>
    )
  }
  • 自定义具名插槽

对于默认插槽使用this.$slots.default

而对于具名插槽,可以使用this.$slots.footer进行自定义

render() {
    return (
      <div>
        {
            // 通过this.$slots.footer
            this.$slots.footer
        }
      </div>
    )
  }

作用域插槽

  • 使用作用域插槽

    有时让插槽内容能够访问子组件中才有的数据是很有用的,这时候就需要用到作用域插槽,

    JSX中,因为没有v-slot指令,所以作用域插槽的使用方式就与模板代码里面的方式有所不同了。

    比如在element-ui中,我们使用el-table的时候可以自定义表格单元格的内容,这时候就需要用到作用域插槽

// TODO: 创建操作表头
    createColumnsOption(data){
      const { switchChange, editClick, removeClick } =this;
      if(data.switchKey) this.tableData = this.dataSource.map(c=>c[data.switchKey]  === 1);
      const noRemove = data?.noRemove || false;
      const noEdit = data?.noEdit || false;
      const p = {
        props:{
          type:'operation',
          label:'操作',
          width: data.switchKey ? noRemove || noEdit ? '92' : '120' : noRemove || noEdit ? '55' : '80',
          fixed:'right',
          ...data,
        },
        scopedSlots:{default:(props)=>{
        // scopedSlots即作用域插槽,default为默认插槽,如果是具名插槽,将default该为对应插槽名称即可
          const { row, $index } = props;
          
          return (
            <div class="flex ac tableOption">
              {
                noEdit === false && <wg-button onClick={editClick.bind(this,props)} icon="icon-bianji1" type="text"/>
              }
              {
                noRemove === false && <wg-button onClick={removeClick.bind(this,props)} icon="icon-shanchu" type="text"/>
              }
              {
                data.switchKey && (
                  <el-switch v-model={this.tableData[$index]} onchange={(v)=>switchChange(v, props)}  class="ml12"/>
                )
              }
            </div>
          )
        }}
      };
      return <wg-table-column {...p}/>
    },

  • 自定义作用域插槽

假如我们自定义了一个列表项组件,用户希望可以自定义列表项标题,这时候就需要将列表的数据通过作用域插槽传出来。

```
render() {
    const { data } = this
    // 获取标题作用域插槽
    const titleSlot = this.$scopedSlots.title
    return (
      <div class="item">
        {/** 如果有标题插槽,则使用标题插槽,否则使用默认标题 */}
        {titleSlot ? titleSlot(data) : <span>{data.title}</span>}
      </div>
    )
  }
```

JSX除了在render中可以被使用外,还可以在method定义的任何一个方法中使用

 methods: {
    renderFooter() {
      return (
        <div>
          <el-button>保存</el-button>
          <el-button>取消</el-button>
        </div>
      )
    },
  },
 render() {
    return (
      <el-dialog title={this.title} visible={this.visible}>
        ……
        {/** 具名插槽 */}
        <template slot="footer">
          {
              this.renderFooter()
          }
        </template>
      </el-dialog>
    )
  }

指令

虽然大部分内置的指令无法直接在JSX里面使用,但是自定义的指令可以在JSX里面使用,就拿element-uiv-loading指令来说,可以这样用

  render() {
    /**
     * 一个组件上面可以使用多个指令,所以是一个数组
     * name 对应指令的名称, 需要去掉 v- 前缀
     * value 对应 `v-loading="value"`中的value
     */
    const directives = [{ name: 'loading', value: this.loading }]
    return (
      <div
        {...{
          directives
        }}
      ></div>
    )
  }
复制代码

指令修饰符

有些指令还可以使用修饰符,比如上例中的v-loading,你可以通过修饰符指定是否全屏遮罩,是否锁定屏幕的滚动,这时候就需要这样写 v-loading.fullscreen.lock = "loading"

  render() {
    /**
     * modifiers指定修饰符,如果使用某一个修饰符,则指定这个修饰符的值为 true
     * 不使用可以设置为false或者直接删掉
     */
    const directives = [
      {
        name: 'loading',
        value: this.loading,
        modifiers: { fullscreen: true, lock: false }
      }
    ]
    return (
      <div
        {...{
          directives
        }}
      ></div>
    )
  }

JSX中的函数式组件

Vue 官方传送门

函数式组件意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

因为函数式组件只是一个函数,所以渲染开销也低很多。然而,对持久化实例的缺乏也意味着函数式组件不会出现在 Vue devtools 的组件树里。

因为函数式组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。

实际上,它只是一个接受一些 prop 的函数。

所以在少了很多响应式处理和操作的基础上,函数式组件可以会提高速度和减少内存占用。

又因为只是函数,所以渲染开销也低很多

在template中,函数式组件可以这样(注意是Vue 2.5.0 及以上版本):

<template functional>

</template

而在JSX中,

我们只需增加配置functional: true就可以了

export default {
      functional:true,
      render(h, context){
        return (
          <div />
        )
      }
}

函数式组件render相比普通组件render的变化:

  • 对于函数式组件 vue 增加了context对象,需要作为render(h,context) 第二个参数传入

  • this.$slots.default更新为context.children

  • props原本是直接挂在this上的,现在变为context.props挂在了context.props上。

  • this.data变为了context.data

需要注意的是对于函数式组件,没有被定义为prop的特性不会自动添加到组件的根元素上,意思就是需要我们手动添加到组件根元素了,看个例子吧

//父组件
 ...
 render(){
      return (
        <Item data={this.data} class="large"/>
      )
    }
//Item.vue组件
export default {
    functional:true,
      name: "item",
      render(h,context){
        return (
          <div class="red" >
            {context.props.data}
          </div>
        )
      }
    }

上面代码期待的是.large类名传入到了Item的根元素上,但是其实没有。我们需要增加点东西

// Item.vue
export default {
    functional:true,
      name: "item",
      render(h,context){
        return (
          <div class="red" {...context.data}>
            {context.props.data}
          </div>
        )
      }
    }

注意到,通过展开运算符把所有的属性添加到了根元素上,这个context.data就是你在父组件给子组件增加的属性,他会跟你在子元素根元素的属性智能合并,现在.large类名就传进来了。这个很有用,当你在父组件给子组件绑定事件时就需要这个了。

向 createElement 通过传入 context.data 作为第二个参数,我们就把 my-functional-button 上面所有的特性和事件监听器都传递下去了。事实上这是非常透明的,那些事件甚至并不要求 .native 修饰符

上面是vue官网的一段话

9.png

在函数式组件中,不需要.native修饰符,所以在函数式组件中,nativeOn并不会生效

总结

10.png

在Vue中像写React一样使用RenderJSX,可能并不是多么一件美好的事情,正如官方文档告诉我们的,“这就是深入底层的代价”。

但是,

“这可以让你更好地控制交互细节

不是吗?