Vue render函数

前戏

在了解vue render函数以前, 须要先了解下Vue的总体流程(如上图)javascript

经过上图, 应该能够理解一个Vue组件是如何运行起来的.html

  • 模板经过编译生成AST树
  • AST树生成Vue的render渲染函数
  • render渲染函数结合数据生成vNode(Virtual DOM Node)树
  • Diff和Patch后生新的UI界面(真实DOM渲染)

在这张图中, 咱们须要了解如下几个概念:vue

  • 模板, 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函数能够做为一道分割线:java

  • render函数左边能够称为编译期, 将Vue板转换为渲染函数
  • render函数右边, 是Vue运行时, 主要是将渲染函数生成Virtual DOM树, 以及Diff和Patch

render

Vue 推荐在绝大多数状况下使用模板来建立你的 HTML。然而在一些场景中,你真的须要 JavaScript 的彻底编程的能力。这时你能够用渲染函数,它比模板更接近编译器。node

渲染标题的例子

例如, 官网上一个渲染标题的例子git

相关的实现, 你们能够查阅下, 这里再也不细述了. 这里贴上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代码: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 都会自动保持页面的更新.

虚拟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个参数是可选的,能够给其传一个StringArray, 例如:

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-ifv-forv-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渲染过程完成.

相关连接

相关文章
相关标签/搜索