
前戏
在了解vue render函数之前, 需要先了解下Vue的整体流程(如上图)
通过上图, 应该可以理解一个Vue组件是如何运行起来的.
- 模板通过编译生成AST树
- AST树生成Vue的render渲染函数
- render渲染函数结合数据生成vNode(Virtual DOM Node)树
- Diff和Patch后生新的UI界面(真实DOM渲染)
在这张图中, 我们需要了解以下几个概念:
- 模板, Vue模板是纯HTML, 基于Vue的模板语法, 可以比较方便的处理数据和UI界面的关系
- AST, 即Abstract Syntax Tree的简称, Vue将HTML模板解析为AST,并对AST进行一些优化的标记处理, 提取最大的静态树,以使Virtual DOM直接跳过后面的Diff
- render渲染函数, render渲染函数是用来生成Virtual DOM的. Vue推荐使用模板来构建我们的应用程序, 在底层实现中Vue最终还是会将模板编译成渲染函数. 因此, 若我们想要得到更好的控制, 可以直接写渲染函数.(重点)
- Virtual DOM, 虚拟DOM
- Watcher, 每个Vue组件都有一个对应的
watcher
, 它会在组件render
时收集组件所依赖的数据, 并在依赖有更新时, 触发组件重新渲染, Vue会自动优化并更新需要更新DOM
在上图中, render
函数可以作为一道分割线:
render
函数左边可以称为编译期, 将Vue板转换为渲染函数render
函数右边, 是Vue运行时, 主要是将渲染函数生成Virtual DOM树, 以及Diff和Patch
render
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
渲染标题的例子
例如, 官网上一个渲染标题的例子
相关的实现, 大家可以查阅下, 这里不再细述了. 这里贴上template的实现和render函数的实现的代码:
.vue单文件的实现
<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>
</template>
<script>
export default {
name: 'anchored-heading',
props: {
level: {
type: Number,
required: true
}
}
}
</script>
render函数的实现
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // tag name 标签名称
this.$slots.default // 子组件中的阵列
)
},
props: {
level: {
type: Number,
required: true
}
}
})
是不是很简洁了?
Node & tree & Virtual DOM
在对Vue的基础概念和渲染函数有一定了解后, 我们也需要了解一些浏览器的工作原理. 这对我们学习render
函数很重要. 例如下面这段HTML代码:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
当浏览器读取到这些代码时, 它会建立一个DOM节点树来保持追踪, 如果你要画一张家谱树来追踪家庭成员的发展的话, HTML的DOM节点树的可能如下图所示:

每个元素和文字都是一个节点, 甚至注释也是节点. 一个节点就是页面的一部分, 就像家谱树中一样, 每个节点都可以有孩子节点.
高效的更新所有节点可能是比较困难的, 不过你不用担心, 这些Vue都会自动帮你完成, 你只需要通知Vue页面上HTML是什么?
可以是一个HTML模板, 例如:
<h1>{{title}}</h1>
也可以是一个渲染函数:
render(h){
return h('h1', this.title)
}
在这两种情况下,若title
值发生了改变, Vue 都会自动保持页面的更新.
虚拟DOM
Vue编译器在编译模板之后, 会将这些模板编译为渲染函数(render), 当渲染函数(render)被调用时, 就会返回一个虚拟DOM树.
当我们得到虚拟DOM树后, 再转交给一个Patch函数, 它会负责把这些虚拟DOM渲染为真实DOM. 在这个过程中, Vue自身的响应式系统会侦测在渲染过程中所依赖的数据来源, 在渲染过程中, 侦测到数据来源后即可精确感知数据源的变动, 以便在需要的时候重新进行渲染. 当重新进行渲染之后, 会生成一个新的树, 将新的树与旧的树进行对比, 就可以得到最终需要对真实DOM进行修改的改动点, 最后通过Patch函数实施改动.
简单来讲, 即: 在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在应该状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。

Vue支持我们通过data
参数传递一个JavaScript对象作为组件数据, Vue将遍历data对象属性, 使用Object.defineProperty
方法设置描述对象, 通过gett/setter
函数来拦截对该属性的读取和修改.
Vue创建了一层Watcher
层, 在组件渲染的过程中把属性记录为依赖, 当依赖项的setter
被调用时, 会通知Watcher
重新计算, 从而使它关联的组件得以更新.

对于虚拟DOM, 如果想深入了解, 可以看下Vue原理解析之Virtual DOM
通过前面的学习, 我们初步了解到Vue通过建立一个**虚拟DOM"对真实DOM发生变化保持追踪. 例如
return createElement('h1', this.title)
createElement
, 即createNodeDescription
, 返回虚拟节点(Virtual Node), 通常简写为"VNode". 虚拟DOM是由Vue组件树建立起来的整个VNode树的总称.
Vue组件树建立起来的整个VNode树是唯一的, 不可重复的. 例如, 下面的render函数是无效的.
render(createElement) {
const vP = createElement('p', 'hello james')
return createElement('div', [
// error, 有重复的vNode
vP, vP
])
}
若需要很多重复的组件/元素, 可以使用工厂函数来实现. 例如:
render(createElement){
return createElement('div', Array.apply(null, {length: 20}).map(() => {
return createElement('p', 'hi james')
}))
}
Vue 渲染机制
下图展示的是独立构建时, 一个组件的渲染流程图:

会涉及到Vue的2个概念:
- 独立构建, 包含模板编译器, 渲染过程: HTML字符串 => render函数 => vNode => 真实DOM
- 运行时构建, 不包含模板编译器, 渲染过程: render函数 => vNode => 真实DOM
运行时构建的包, 会比独立构建少一个模板编译器(因此运行速度上会更快). 在$mount
函数上也不同, 而$mount
方法是整个渲染过程中的起始点, 用下面这张流程图来说明:

从上图可以看出, 在渲染过程中, 提供了三种模板:
- 自定义render函数
- template
- el
均可以渲染页面, 也就对应我们使用Vue时的三种写法.
这3种模式最终都是要得到render
函数.
对于平时开发来讲, 使用template和el会比较友好些, 容易理解, 但灵活性较差. 而render函数, 能够胜任更加复杂的逻辑, 灵活性高, 但对于用户理解相对较差.
自定义render函数
Vue.component('anchored-heading', {
render(createElement) {
return createElement (
'h' + this.level,
this.$slots.default
)
},
props: {
level: {
type: Number,
required: true
}
}
})
template写法
const app = new Vue({
template: `<div>{{ msg }}</div>`,
data () {
return {
msg: 'Hello Vue.js!'
}
}
})
el写法
let app = new Vue({
el: '#app',
data () {
return {
msg: 'Hello Vue!'
}
}
})
理解&使用render函数
createElement
在使用render
函数时, createElement
是必须要掌握的.
createElement 参数
createElement
可以接受多个参数
第1个参数: {String | Object | Function }
, 必传
第一个参数是必传参数, 可以是字符串String
, 也可以是Object
对象或函数Function
// String
Vue.component('custom-element', {
render(createElement) {
return createElement('div', 'hello world!')
}
})
// Object
Vue.component('custom-element', {
render(createElement) {
return createElement({
template: `<div>hello world!</div>`
})
}
})
// Function
Vue.component('custom-element', {
render(createElement) {
const elFn = () => { template: `<div>hello world!</div>` }
return createElement(elFn())
}
})
以上代码, 等价于:
<template>
<div>hello world!</>
</template>
<script>
export default {
name: 'custom-element'
}
</script>
第2个参数: { Object }
, 可选
createElemen
的第二个参数是可选参数, 这个参数是一个Object, 例如:
Vue.component('custom-element', {
render(createElement) {
const self = this;
return createElement('div', {
'class': {
foo: true,
bar: false
},
style: {
color: 'red',
fontSize: '18px'
},
attrs: {
...self.attrs,
id: 'id-demo'
},
on: {
...self.$listeners,
click: (e) => {console.log(e)}
},
domProps: {
innerHTML: 'hello world!'
},
staticClass: 'wrapper'
})
}
})
等价于:
<template>
<div :id="id" class="wrapper" :class="{'foo': true, 'bar': false}" :style="{color: 'red', fontSize: '18px'}" v-bind="$attrs" v-on="$listeners" @click="(e) => console.log(e)"> hello world! </div>
</template>
<script>
export default {
name: 'custom-element',
data(){
return {
id: 'id-demo'
}
}
}
</script>
<style>
.wrapper{
display: block;
width: 100%;
}
</style>
第3个参数: { String | Array }
, 可选
createElement
第3个参数是可选的,可以给其传一个String
或Array
, 例如:
Vue.component('custom-element', {
render (createElement) {
var self = this
return createElement(
'div',
{
class: {
title: true
},
style: {
border: '1px solid',
padding: '10px'
}
},
[
createElement('h1', 'Hello Vue!'),
createElement('p', 'Hello world!')
]
)
}
})
等价于:
<template>
<div :class="{'title': true}" :style="{border: '1px solid', padding: '10px'}">
<h1>Hello Vue!</h1>
<p>Hello world!</p>
</div>
</template>
<script>
export default {
name: 'custom-element',
data(){
return {
id: 'id-demo'
}
}
}
</script>
使用template和render创建相同效果的组件
template方式
<template>
<div id="wrapper" :class="{show: show}" @click="clickHandler">
Hello Vue!
</div>
</template>
<script>
export default {
name: 'custom-element',
data(){
return {
show: true
}
},
methods: {
clickHandler(){
console.log('you had click me!');
}
}
}
</script>
render方式
Vue.component('custom-element', {
data () {
return {
show: true
}
},
methods: {
clickHandler: function(){
console.log('you had click me!');
}
},
render: function (createElement) {
return createElement('div', {
class: {
show: this.show
},
attrs: {
id: 'wrapper'
},
on: {
click: this.handleClick
}
}, 'Hello Vue!')
}
})
createElement解析过程
createElement解析流程图(摘至: segmentfault.com/a/119000000…)

createElement
解析过程核心源代码(需要对JS有一定功底, 摘至: segmentfault.com/a/119000000…)
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
// 兼容不传data的情况
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 如果alwaysNormalize是true
// 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
// 调用_createElement创建虚拟节点
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context, tag, data, children, normalizationType) {
/**
* 如果存在data.__ob__,说明data是被Observer观察的数据
* 不能用作虚拟节点的data
* 需要抛出警告,并返回一个空节点
*
* 被监控的data不能被用作vnode渲染的数据的原因是:
* data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
*/
if (data && data.__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()
}
// 当组件的is属性被设置为一个falsy的值
// Vue将不会知道要把这个组件渲染成什么
// 所以渲染一个空节点
if (!tag) {
return createEmptyVNode()
}
// 作用域插槽
if (Array.isArray(children) && typeof children[0] === 'function') {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根据normalizationType的值,选择不同的处理方法
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 如果标签名是字符串类型
if (typeof tag === 'string') {
let Ctor
// 获取标签名的命名空间
ns = config.getTagNamespace(tag)
// 判断是否为保留标签
if (config.isReservedTag(tag)) {
// 如果是保留标签,就创建一个这样的vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义
} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果找到了这个标签的定义,就以此创建虚拟组件节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 兜底方案,正常创建一个vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
// 当tag不是字符串的时候,我们认为tag是组件的构造类
// 所以直接创建
} else {
vnode = createComponent(tag, data, context, children)
}
// 如果有vnode
if (vnode) {
// 如果有namespace,就应用下namespace,然后返回vnode
if (ns) applyNS(vnode, ns)
return vnode
// 否则,返回一个空节点
} else {
return createEmptyVNode()
}
}
}
使用render函数代替模板功能
在使用Vue模板的时候,我们可以在模板中灵活的使用v-if
、v-for
、v-model
和<slot>
等模板语法。但在render
函数中是没有提供专用的API。如果在render使用这些,需要使用原生的JavaScript来实现。
v-if
& v-for
<ul v-if="items.length">
<li v-for="item in items">{{ item }}</li>
</ul>
<p v-else>No items found.</p>
render函数实现
Vue.component('item-list',{
props: ['items'],
render (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map((item) => {
return createElement('item')
}))
} else {
return createElement('p', 'No items found.')
}
}
})
v-model
<template>
<el-input :name="name" @input="val => name = val"></el-input>
</template>
<script>
export default {
name: 'app',
data(){
return {
name: 'hello vue.js'
}
}
}
</script>
render函数实现
Vue.component('app', {
data(){
return {
name: 'hello vue.js'
}
},
render: function (createElement) {
var self = this
return createElement('el-input', {
domProps: {
value: self.name
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
},
props: {
name: String
}
})
slot
在Vue中, 可以通过:
this.$slots
获取VNodes列表中的静态内容.
render(h){
return h('div', this.$slots.default)
}
等价于:
<template>
<div>
<slot> </slot>
</div>
</template>
在Vue中, 可以通过:
this.$scopedSlots
获取能用作函数的作用域插槽, 这个函数会返回VNodes
props: ['message'],
render (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
如果要用渲染函数向子组件中传递作用域插槽,可以利用VNode数据中的scopedSlots域:
<div id="app">
<custom-ele></custom-ele>
</div>
Vue.component('custom-ele', {
render: function (createElement) {
return createElement('div', [
createElement('child', {
scopedSlots: {
default: function (props) {
return [
createElement('span', 'From Parent Component'),
createElement('span', props.text)
]
}
}
})
])
}
})
Vue.component('child', {
render: function (createElement) {
return createElement('strong', this.$scopedSlots.default({
text: 'This is Child Component'
}))
}
})
let app = new Vue({
el: '#app'
}
JSX
如果写习惯了template
,然后要用render
函数来写,一定会感觉狠难受,特别是面对复杂的组件的时候。不过我们在Vue中使用JSX可以让我们回到更接近于模板的语法上。
import View from './View.vue'
new Vue({
el: '#demo',
render (h) {
return (
<View level={1}>
<span>Hello</span> world!
</View>
)
}
}
将 h 作为
createElement
的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的,如果在作用域中 h 失去作用,在应用中会触发报错。
总结
Vue渲染中, 核心关键的几步是:
new Vue
, 执行初始化- 挂载
$mount
, 通过自定义render
方法,template
,el
等生成render
渲染函数 - 通过
Watcher
监听数据的变化 - 当数据发生变化时,
render
函数执行生成VNode对象 - 通过
patch
方法, 对比新旧VNode对象, 通过DOM Diff
算法, 添加/修改/删除真正的DOM元素
至此, 整个new Vue
渲染过程完成.