平时写一个组件的流程:
- 全局注册
Vue.component(‘my-component-name’, { /* … */ })
- 局部注册
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
}
})
很简单,但是思考一下,一个组件的渲染流程是啥样的。
- 拿到template compile成ast树
- ast树经过render 函数转成虚拟Dom (VNode)
- 虚拟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)
//这个才是真正的生成虚拟节点的函数
}
参数 | 值 | 语义 |
---|---|---|
tag | String ,function,object | tag,一个 HTML 标签名、组件选项对象,或者 resolve 了上述任何一种的一个 async 函数。必填项 |
data | Object | 一个与模板中 attribute 对应的数据对象 |
children | String,Object | children,子级虚拟节点 (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
以非函数组件为列子:
- 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>`
}
- 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>`
}
- 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>`
}
- v-if,v-for 省略 用if map 做就行
- 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()
}
}