Vue-尤雨溪讲课4-render函数用法详解

平时写一个组件的流程:

  1. 全局注册

Vue.component(‘my-component-name’, { /* … */ })

  1. 局部注册
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

假设我们要写一个这样的组件

	<div>
        <h1>1</h1>
        <h2>2</h2>
        <h3>3</h3>
    </div> 
<example :tags="['h1', 'h2', 'h3']"></example>
var mycomponent = {
            props: [],
            template: `<div>
                <h1>1</h1>
                <h2>2</h2>
                <h3>3</h3>
            </div>
            `

        }
        var app = new Vue({
            el: '#app',
            data: {
                count: 1
            },
            components: {
                mycomponent
            }
        })

很简单,但是思考一下,一个组件的渲染流程是啥样的。
在这里插入图片描述

  1. 拿到template compile成ast树
  2. ast树经过render 函数转成虚拟Dom (VNode)
  3. 虚拟dom经过diff算法与真实dom对比得出差异 patch 更改差异。

Vue允许你直接第二步,也就是直接渲染。这就是包不包含包含模板编译器的区别。

  • 独立构建, 包含模板编译器, 渲染过程: HTML字符串 => render函数 => vNode => 真实DOM
  • 运行时构建, 不包含模板编译器, 渲染过程: render函数 => vNode => 真实DOM
render: function (createElement, context) {
                return createElement()
            }

一般是createElement()简写为h(),这是习惯推荐写法。
先看一下createElement()函数的写法;
h(…)

官方源码:

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
//参数...返回的是一个虚拟dom
  if (Array.isArray(data) || isPrimitive(data)) {
  // isPrimitive:检查一个值的数据类型是不是简单类型
  //就是对children 的规范化?_存疑_?有空再看看源码 位于vdom/normalize_children.js
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
  //这个才是真正的生成虚拟节点的函数
}
参数语义
tagString ,function,objecttag,一个 HTML 标签名、组件选项对象,或者 resolve 了上述任何一种的一个 async 函数。必填项
dataObject一个与模板中 attribute 对应的数据对象
childrenString,Objectchildren,子级虚拟节点 (VNodes),由 createElement() 构建而成,也可以使用字符串来生成“文本虚拟节点”。可选

官网案例:

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

还有一点组件可以分为函数式组件,与非函数式组件,所以他的render也是有区别的。
函数式组件特点:

  • 没有管理自己的状态(data属性)
  • 没有监听传给它的状态(props)
  • 没有生命周期函数 这也意味着它是一个无状态组件, 无实例组件(没有this)

所以在用render函数需留意,若是函数式组件,必须声明functional: true,render 用不了data里面的响应式数据。

<div id='app'>

    <mycomponent :tags="['h1', 'h2', 'h3']"></mycomponent>
</div>

<body>

    <script>
        var mycomponent = {
            functional: true,
            props: {
                tags: {
                    type: Array,
                    validator(arr) {
                        return !!arr.length
                    }
                },
            },
            data() {
                return {
                    msg: 233,
                    mcolor: 'red'
                }
            },

            render: function(h, context) {
                console.log(context.data); //{"attrs": {}}
                const tags = context.props.tags
                return h('div', {
                }, tags.map((tag, index) => h(tag, {
                    style: {
                        color: context.mcolor //没有生效
                    },
                }, index)))
            }
        }
        var app = new Vue({
            el: '#app',
            data: {
                count: 1
            },
            components: {
                mycomponent
            }
        })

一句话:props 传啥用啥。也要传this
非函数是组件:

  • 不用传参this
  • 可以管理自己状态
functional: false,
            props: {
                tags: {
                    type: Array,
                    validator(arr) {
                        return !!arr.length
                    }
                },
            },
            data() {
                return {
                    msg: 233,
                    mcolor: 'red'
                }
            },
            render: function(h) {
                let context = this
                const tags = context.tags
                return h('div', {}, tags.map((tag, index) => h(tag, {
                    style: {
                        color: context.mcolor //生效
                    },
                }, index)))
            }

来点复杂的:事件,slot,v-if,v-for,v-model,JFX
以非函数组件为列子:

  1. slot:
  • 默认:this.$slots 就已经是一个虚拟节点数组了Vnode
	<mycomponent :tags="['h1', 'h2', 'h3']">
        <div>2333</div>
    </mycomponent>
 		render: function(h) {
                let context = this
                const tags = context.tags
                let children = tags.map((tag, index) =>
                    h(tag, {
                        style: {
                            color: context.mcolor //没有生效
                        },
                    }, index))

                children.push(this.$slots.default)

                return h('div', {}, children)
            }
  • 作用域插槽:通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数,用法差不多。
	<mycomponent>
        <template v-slot:default="slotProps">
            {{ slotProps.user.firstName }}
          </template>
    </mycomponent>
 var mycomponent = {
            functional: false,
            props: {

            },
            data() {
                return {
                    user: {
                        firstName: '张',
                        lastName: '三'
                    }
                }
            },
            render: function(h) {
                console.log(this.$scopedSlots.default({
                    user: this.user,
                }));
                return h('span', this.$scopedSlots.default({
                    user: this.user,
                }))
            },
            // template: `<span>
            //             <slot v-bind:user="user">
            //                 {{ user.lastName }}
            //             </slot>
            //         </span>`
        }
  • 具名插槽:this.$slots.插槽名 就可以访问到
	<mycomponent>
        <template v-slot:header>
            <h1>Here might be a page title</h1>
        </template>
    </mycomponent>
       	var mycomponent = {
            functional: false,
            props: {

            },
            render: function(h) {
                    console.log(this.$slots);
                    return h('div', this.$slots.header)
                }
                // template: ` <header>
                //                 <slot name="header"></slot>
                //             </header>`
        }
  1. v-on 事件(按键修饰符也支持,这里省略可看官网)
<script>
        var mycomponent = {
            functional: false,
            props: {

            },
            data() {
                return {
                    count: 1
                }
            },
            methods: {
                clickBtn() {
                    this.count++;
                }
            },
            render: function(h) {
                let children = [
                    h('button', {
                        on: {
                            click: this.clickBtn
                        }
                    }, ["+", this.count])
                ]

                return h('div', {}, children)
            },
            // template: ` <div>
            //                 <button @click= "clickBtn">+{{count}}</button>
            //             </div>`
        }
  1. v-model
    render 并没有实现v-model的支持需要自己实现。
    回忆一下v-model的本质:
  • input :value="xx"
  • @input="xxx = $event.target.value"
		var mycomponent = {
            functional: false,
            props: {

            },
            data() {
                return {
                    count: 1
                }
            },
            render: function(h) {
                let children = []
                let context = this
                children.push(h('input', {
                    domProps: {
                        value: context.count
                    },
                    on: {
                        input(event) {
                            context.count = event.target.value
                        }
                    }
                }))
                children.push(this.count)
                return h('div', {}, children)
            },
            // template: ` <div>
            //                 <input type='text'v-bind:value='count' @input='count= $event.target.value'/>
            //                 {{count}}
            //             </div>`
        }
  1. v-if,v-for 省略 用if map 做就行
  2. JSX :

render 也是支持jfx的
尤大说过:

  • template 静态的用它好,
  • 而动态用jfx

jsx :javascript 与xml的结合
需要bable 插件把他转化为javascript
而对于jsx 语法还不是很熟悉,这个待述。
官方案例:

new Vue({
  el: '#demo',
  render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

但要注意一点:
官方描述:

将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。
从 Vue 的 Babel 插件的 3.4.0 版本开始,
我们会在以 ES2015 语法声明的含有 JSX 的任何方法
和 getter  (不是函数或箭头函数中) 自动注入 const h = this.$createElement,
这样你就可以去掉 (h) 参数了。
对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错

上面是动态渲染标签的内容:
但其实render 可以动态渲染组件的。
动态渲染组件:

<div id="app">
    <example :ok="ok"></example>
    <button @click="ok = !ok">+</button>
</div>
<script>
    const Foo = {
        functional: false,
        data() {
            return {

                isshow: true
            }
        },
        template: `<div>
                        <h1 v-if='isshow'>foo</h1>
                            <button @click="isshow = !isshow">show?</button>
                    </div>`
    }
    const Bar = {
        functional: true,
        render: h => h('div', 'bar')
    }
    Vue.component('example', {
        functional: true,
        props: {
            ok: Boolean
        },
        render: (h, context) => h(context.props.ok ? Foo : Bar)
    })
    new Vue({
        el: '#app',
        data: {
            ok: true
        }
    })
</script>

失望的是并没有keepalive 一样保存缓存,有空看看keepalive 源码

_createElement 待详诉。源码如下:


export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__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()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  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 = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值