Vue随笔:Render()函数&JSX

1. 前言

1.1 HTML DOM树与和Vue的virtual DOM

  • 我们知道,浏览器在解析HTML文件时,会将HTML标签解析成一个DOM树(tree of DOM nodes) 。通过结构化的组织节点元素,浏览器可以很方便的跟踪整个页面的情况,但频繁的局部更新节点代价很高。
  • 为了更高效的渲染HTML,Vue.jsReact以及Ember.js一样,根据真实DOM的映射构建对应的JS对象,也就是虚拟DOM(Virtual DOM)。在数据和DOM之间创建一个缓冲地带,不用每次都更新DOM,详情见下面React Virtual DOM的示意图:
React Virtual DOM示意图

1.2 Vue组件中的Slot

  • Vue.js的Slot机制是指,父组件在嵌套使用子组件时,可以传递HTML代码片段给子组件渲染,但完全不用关心这段HTML应该放在子组件什么位置。
  • 是不是很懵逼,没看懂?举个例子吧:
<!-- 【需求】
  封装一个文章标题组件,但是要根据情况给组件传递"分享到站外"的html代码片段
  因为在调用组件的时候,才能根据当前用户角色确定是分享到微信还是微博的按钮 -->

<!-- 组件<article>期望的最终渲染的代码是这样的 -->  
<div>
    <h1>
        vue.js Render()函数&JSX
    </h1>
    <!-- 这里是可能传递的代码片段1 -->
    <span><Button>分享到新浪微博</Button></span>
    <!-- 这里是可能传递的代码片段2 -->
    <span><Button>分享到微信</Button></span>
</div>
  • 我们开始用Vvue.jsSlot机制来实现
  1. 定义article.vue组件
<tempalte>
    <div>
        <h1>
            {{title}}
        </h1>
        <!-- 子组件只关心slot摆放的位置,而不关心传进来的是什么 -->
        <slot></slot>
    </div>
</tempalte>
<script>
export default{
    name : 'Article',
    props : ['title']
}
</script>
  1. main.vue使用标题组件
<template>
    <div>
        <article :title="titleStr">
            <span>
                <Button v-if="userType === 'weibo'">分享到新浪微博</Button>
                <Button v-if="userType === 'weixin'">分享到微信</Button>
            </span>
        </article>
    </div>
</template>
<script>
export default{
    name : 'Main',
    data() {
        return: {
            userType: 'weibo',
            titleStr: 'vue.js Render()函数&JSX'
        }
    }
}
</script>   
  • 再说slot,顾名思义,Vue.js通过给程序设置一个类似插槽/投币口的机制,实现内容分发:
  1. 子组件定义好插槽位置;
  2. 父组件可以插入任何,符合Slot规范的代码片段;
  • 如此一来,既实现了程序的解耦,使用又很方便。
  • 当然啦,Vue.js还支持具名插槽,也就是通过定义key名来区分不同的slot代码片段,这里就不展开讨论了。

2. render()函数

2.1 为什么要使用render()函数

  • 使用过Vue.js的朋友都知道,好像大部分时间都在使用template的方式来创建HTML,因为vue提供了v-ifv-for等一系列的控制指令,让我们开发体验轻松又愉悦。
  • 但除此之外,其实Vue.js还提供了render()函数来创建HTML。让我们可以通过JS逻辑代码,更灵活的创建HTML。
  • 如同vue官网的例子所说:在封装文章标题的<Title>组件时,不确定最终生成的,具体是h1~h6的标签。这个时候,如果用v-if做判断,那么代码量太大,但用render()函数来实现就很简单。
<!-- 用template实现 -->
<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
    }
  }
})
<!-- 用render()函数实现 -->
Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 由子节点构成的数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

2.2 render()函数的传参

render(crateElmentFn,context){}

  • createElemntFn是Vue用来动态创建HTML的核心方法【注意:把createElement函数命名为h是vue.js的通用惯例,记得两者是同一个东西】
  • context是组件实例的上下文环境,包括了组件实例的所有属性
    • props: 提供props 的对象
    • children: VNode 子节点的数组
    • slots: slots 对象
    • data: 传递给组件的 data 对象
    • parent: 对父组件的引用

3. createElement()函数

3.1 createElement() 传参

createElemnt(newNode,newNodeConfig,childVNodeList)

  • createElemnt()函数主要有三个参数:
    • newNode:要创建的节点【必填参数】
      • 参数类型: {String | Object | Function},可以是要创建的HTML 标签名称,也可以是组件对象,也可以是返回为String或Vue Object的异步函数
    • newNodeConfig:新节点的配置对象【选填】
    • childVNodeList:新节点要包含的子节点集合【选填】
      • 参数类型: {String | Array}
      • 注意事项:vue官方教程标明传递的VNodes必须是唯一的,如果想重复创建相同的HTML元素,需要用工厂函数来实现
  • 调用示例:
// @returns {VNode}
createElement(
  'div',
  {},
  [
    'Some text comes first.',
    createElement('h1', 'A headline'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

3.2 newNodeConfig参数详解

{
  // 和 `v-bind:class` 的 API 相同【注意:由于是关键字,要用单引号包含】
  'class': {
    foo: true,
    bar: false
  },
  // 和 `v-bind:style` 的 API 相同【注意:由于是关键字,要用单引号包含】
  'style': {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 属性
  attrs: {
    id: 'foo'
  },
  // 组件 props
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件处理程序嵌套在 `on` 字段下,
  // 然而不支持在 `v-on:keyup.enter` 中的修饰符。
  // 因此,你必须手动检查
  // 处理函数中的 keyCode 值是否为 enter 键值。
  on: {
    click: this.clickHandler
  },
  // 仅对于组件,
  // 用于监听原生事件,而不是组件内部
  // 使用 `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。
  // 注意,由于 Vue 会追踪旧值,
  // 所以不能对`绑定`的`旧值`设值
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽(scoped slot)的格式如下
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果此组件是另一个组件的子组件,
  // 需要为插槽(slot)指定名称
  slot: 'name-of-slot',
  // 其他特殊顶层(top-level)属性
  key: 'myKey',
  ref: 'myRef'
}

3.3 使用createElement()替代template创建HTML

3.3.1 条件判断和循环渲染

template代码:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

render代码:

props: ['items'],
render: function (createElement) {
  // 使用原生JS代码来做条件判断
  if (this.items.length) {
    // 使用map()来循环调用createElement()函数
    return createElement('ul', this.items.map(function (item) {
      return createElement('li', item.name)
    }))
  } else {
    return createElement('p', 'No items found.')
  }
}

3.3.2 数据绑定

render代码:

props: ['value'],
render: function (createElement) {
  // 【注意:由于JS可以用function作为函数的参数传递,为了避免this指针的混乱,在render函数里,要记得对this指针进行缓存】
  var self = this
  return createElement('input', {
    // 手动vue emit事件的触发,来手动控制value值的维护,这是深入底层实现需要付出的代价
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.$emit('input', event.target.value)
      }
    }
  })
}

3.3.3 事件绑定

  • Vue.js在createElemnt()函数中,映射了一系列事件指令修饰符的前缀:
修饰符 前缀
.passive &
.capture !
.once ~
.capture.once.once.capture ~!
  • 示例:
createElement('input',{
    on: {
      '!click': this.doThisInCapturingMode,
      '~keyup': this.doThisOnce,
      '~!mouseover': this.doThisOnceInCapturingMode
    }
})

3.3.4 slot传递

  • 如果像前文所述的,父组件在调用子组件的时候,想传递Slot,可以通过this.$slots对象来访问
render: function (createElement) {
  // 默认传递所有Slot
  // 等同于:<div><slot></slot></div>
  return createElement('div', this.$slots.default)
}
  • 如果在传递Slot时要传参,则可通过this.$scopedSlots
props: ['message'],
render: function (createElement) {
  // 等同于`<div><slot :text="message"></slot></div>`
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}

4. JSX

  • 虽然render()函数创建HTML代码片段很灵活,但整段整段的JS配置代码,的确阅读性很差。为了让代码更简单,我们可以使用JSX,直接在JS代码中书写HTML:
  render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
  • 但为了支持JSX,需要通过一个JSX的Babel的插件。配置示例代码如下:

package.json

devDependencies: {
    "babel-helper-vue-jsx-merge-props": "2.0.3",
    "babel-plugin-syntax-jsx": "6.18.0",
    "babel-plugin-transform-vue-jsx": "3.7.0"
}

.babelrc

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime"],
  "env": {
    "test": {
      "presets": ["env", "stage-2"],
      "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
    }
  }
}

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容