在了解vue render函数以前, 须要先了解下Vue的总体流程(如上图)javascript
经过上图, 应该能够理解一个Vue组件是如何运行起来的.html
在这张图中, 咱们须要了解如下几个概念:vue
watcher
, 它会在组件render
时收集组件所依赖的数据, 并在依赖有更新时, 触发组件从新渲染, Vue会自动优化并更新须要更新DOM在上图中, render
函数能够做为一道分割线:java
render
函数左边能够称为编译期, 将Vue板转换为渲染函数render
函数右边, 是Vue运行时, 主要是将渲染函数生成Virtual DOM树, 以及Diff和PatchVue 推荐在绝大多数状况下使用模板来建立你的 HTML。然而在一些场景中,你真的须要 JavaScript 的彻底编程的能力。这时你能够用渲染函数,它比模板更接近编译器。node
例如, 官网上一个渲染标题的例子git
相关的实现, 你们能够查阅下, 这里再也不细述了. 这里贴上template的实现和render函数的实现的代码:算法
<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> 复制代码
Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // tag name 标签名称 this.$slots.default // 子组件中的阵列 ) }, props: { level: { type: Number, required: true } } }) 复制代码
是否是很简洁了?编程
在对Vue的基础概念和渲染函数有必定了解后, 咱们也须要了解一些浏览器的工做原理. 这对咱们学习render
函数很重要. 例以下面这段HTML代码:segmentfault
<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 都会自动保持页面的更新.
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的2个概念:
运行时构建的包, 会比独立构建少一个模板编译器(所以运行速度上会更快). 在$mount
函数上也不一样, 而$mount
方法是整个渲染过程当中的起始点, 用下面这张流程图来讲明:
从上图能够看出, 在渲染过程当中, 提供了三种模板:
都可以渲染页面, 也就对应咱们使用Vue时的三种写法. 这3种模式最终都是要获得render
函数.
对于平时开发来说, 使用template和el会比较友好些, 容易理解, 但灵活性较差. 而render函数, 可以胜任更加复杂的逻辑, 灵活性高, 但对于用户理解相对较差.
Vue.component('anchored-heading', { render(createElement) { return createElement ( 'h' + this.level, this.$slots.default ) }, props: { level: { type: Number, required: true } } }) 复制代码
const app = new Vue({ template: `<div>{{ msg }}</div>`, data () { return { msg: 'Hello Vue.js!' } } }) 复制代码
let app = new Vue({ el: '#app', data () { return { msg: 'Hello Vue!' } } }) 复制代码
在使用render
函数时, createElement
是必需要掌握的.
createElement
能够接受多个参数
{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> 复制代码
{ 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> 复制代码
{ 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方式
<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解析流程图(摘至: 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() } } } 复制代码
在使用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 } }) 复制代码
在Vue中, 能够经过:
this.$slots
获取VNodes列表中的静态内容.render(h){ return h('div', this.$slots.default) } 复制代码
等价于:
<template> <div> <slot> </slot> </div> </template> 复制代码
在Vue中, 能够经过:
this.$scopedSlots
获取能用做函数的做用域插槽, 这个函数会返回VNodesprops: ['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' } 复制代码
若是写习惯了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
渲染过程完成.