指令
v-model 双向数据绑定指令
v-model是双向数据绑定指令,用于表单控件。v-model其实是语法糖就是对不同的表单类型进行了单独的封装。- 当然 Vue 的组件也是支持
v-model双向数据绑定的。 v-model为表单数据录入提供了很大的帮助!
input
1.input 实现的双向数据绑定,其实就是运用了 input 控件的 value 属性,和监听 input 事件。
- 下面我们来模拟一个
input的v-model:
<template>
<div>
<span>v-model 双数据绑定</span>
<div>
input 的数据是:{{ a1 }}
<input type="text" v-model="a1" />
</div>
<div>
<span>模拟 input textarea v-model</span>
<div>input 绑定是数据是:{{ a2 }}</div>
<div>
<input
type="text"
:value="a2"
@input="handleSelfInput"
/>
</div>
</div>
</div>
</template>
<script>
export default {
name: "component-input-v-model-test",
data() {
return {
a1: 0,
a2: 222,
};
},
methods: {
handleChange() {
debugger;
},
handleInput(args) {
debugger;
},
handleSelfInput(value) {
const target = value.target.value;
this.a2 = target;
},
},
};
</script>
a1 是一个普通 v-model 双向绑定 input 数据,a2 是使用 value 和 input 事件模拟的 v-model。原理很简单,使用 使用 v-bind 动态绑定数据 a2 然后,当 input 事件触发的时候,从 事件 event 中拿到 value 值,将 a2 用新的 value 值覆盖即可。
textarea
textarea 其实和 text 是一致的:
<template>
<div>
<div>
<div>使用 v-model: {{ textarea }}</div>
<textarea v-model="textarea"></textarea>
</div>
<div>
<div>模拟 text-area v-model: {{ textarea_self }}</div>
<textarea :value="textarea_self" @input="onInput"></textarea>
</div>
</div>
</template>
<script>
export default {
name: "v-model-v-text-area",
data() {
return {
textarea: "",
textarea_self: "",
};
},
methods: {
onInput(e) {
this.textarea_self = e.target.value;
},
},
};
</script>
radio
单选按钮 type = radio 与 input 和 textarea 不同, radio 不在使用 value 属性,而是使用 checked 属性,事件也从 text 的 input 事件,变成了 change 事件
- 表单控件:text -> radio/checkbox(checkbox 大得多使用数组保存)
- 绑定控件属性:value -> checked
- 监听的表单事件:input -> change
首先要搞明白 radio 也是需要一个值的,用于表示 radio 当前的选择,书写 Radio 的时候,不在 radio 元素内部写 radio 的内容,而是配合 label 标签来扩大可点击范围。
<template>
<div>
<div>
<div>默认使用 v-model</div>
<div>
绑定的数据: {{ radio }}
<input type="radio" value="radio1" v-model="radio" />
<input type="radio" value="radio2" v-model="radio" />
<input type="radio" value="radio3" v-model="radio" />
</div>
</div>
<div>
<div>模拟一个radio v-model: {{ checked }}</div>
<div>
<input
type="radio"
value="r1"
:checked="checked === 'r1'"
@change="onChange"
/>
<input
type="radio"
value="r2"
:checked="checked === 'r2'"
@change="onChange"
/>
<input
type="radio"
value="r3"
:checked="checked === 'r3'"
@change="onChange"
/>
</div>
</div>
</div>
</template>
<script>
export default {
name: "v-model-radio",
data() {
return {
radio: "",
checked: '',
};
},
methods: {
onChange(e) {
this.checked = e.target.value;
},
},
};
</script>
checkbox 的基本模拟 v-mode
<template>
<div>
<div>
<div>使用 checkbox: {{ checkbox }}</div>
<div>
<input type="checkbox" value="c1" v-model="checkbox" />
<input type="checkbox" value="c2" v-model="checkbox" />
<input type="checkbox" value="c3" v-model="checkbox" />
</div>
</div>
<div>
<div>模拟 checkbox {{ checkbox_self }}</div>
<div>
<input
type="checkbox"
value="cc1"
:checked="checkbox_self.indexOf('cc1') > -1 ? true : false"
@change="onChange"
/>
<input
type="checkbox"
value="cc2"
:checked="checkbox_self.indexOf('cc2') > -1 ? true : false"
@change="onChange"
/>
<input
type="checkbox"
value="cc3"
:checked="checkbox_self.indexOf('cc3') > -1 ? true : false"
@change="onChange"
/>
</div>
</div>
</div>
</template>
<script>
export default {
name: "v-checkbox-v-demo",
data() {
return {
checkbox: [],
checkbox_self: [],
};
},
methods: {
onChange(e) {
const value = e.target.value;
if (this.checkbox_self.indexOf(value) > -1) {
this.checkbox_self = this.checkbox_self.filter((item) => {
item !== value;
});
} else {
this.checkbox_self = [...this.checkbox_self, value];
}
},
},
};
</script>
简单模拟 select 和被选中之间的关系, 这里实现了 select 的单选
<template>
<div>
<div>
<div>在 select 中使用 v-model: {{ selected }}</div>
<div>
<select name="my-select" id="" v-model="selected">
<option value="s1">s111</option>
<option value="s2">s222</option>
<option value="s3">s333</option>
</select>
</div>
</div>
<div>
<div>模拟一个单选 select 的 v-model: {{ selected_self }}</div>
<div>
<select name="ss1" id="" @change="onChange">
<option value="v1" :selected="selected_self === 'V1'">V1</option>
<option value="v2" :selected="selected_self === 'V2'">V2</option>
<option value="v3" :selected="selected_self === 'V3'">V3</option>
</select>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
selected: "s1",
selected_self: "V2",
};
},
methods: {
onChange(e) {
const value = e.target.value;
this.selected_self = value;
},
},
};
</script>
小结
其实就是注意:不同的表单控件有不同的属性和事件监听,v-model 会根据不同的表单进行处理。但是我们也要学会如何自己实现 v-model,因为在 render 函数或者 jsx 中, vue 指令可能就不能得到很好的支持。
渲染
-
- 为什么还有 render 函数?
-
- 为什么还有 jsx?
- 我们写Vue的时候,大部分是用 html 模板来写的,但是模板虽然简单粗暴,但是在灵活度方面还是没有 javascript 灵活, 从 Vue 的运行生命周期原理图中,我们就能知道,Vue 是判断是否含有模板,有模板都会编译到 render 函数中,所以学 Vue 重要的是学些这个 render 函数做了哪些事情,原理是什么。
- JSX 是 JavaScipt 的扩展,允许在 JavaScript 中编写 html 的能力,也就是让 javascript 天生就具有模板的能力,让 render 函数更加清晰。
render
render 是一个渲染函数,即将创建我们写的组件编译成浏览器识别的html,css,js,
- render 函数的参数是 createElement(可以简单的写为 h),
- render 函数的返回值是 createElement 传入参数 options之后的创建的元素
- createElement 的 options 如下:
位置:src/core/vdom/create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
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()
}
}
- createComponent
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
- VNode 是一个 class 的类,保存了组件信息,dom节点信息。这里其实就是一个 Vue 组件的创建过程。Vue 组件最终都会被编译成 render 函数,render 函数会实例化 VNode虚拟节点,Vue 本质上就是 VNode 的对比然后生成 dom 的过程。所以说在创建组件的过程中 render 对于开发者是最为重要的。
jsx
github.com/vuejs/jsx 是 babel 的一个插件,
- 安装
npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props -D
- babel 配置文件
{
"presets": ["@vue/babel-preset-jsx"]
}
- Vue 语法
- 内容
- 属性
- 插槽
- 指令
- 函数组件
JSX 中的 slot
vm.$scopedSlots 在使用渲染函数开发一个组件时特别有用。
- 子组件中使用 this.$scopedSlots.user 来绑定一个作用域插槽
<script>
export default {
name: 'Start',
data () {
return {
a: '123',
b: '456'
}
},
mounted () {
console.log('children', this.$scopedSlots.user) // 一个函数
},
render () {
return (
<div class="abc">
<span>this is goods</span>
{this.$slots.default}
{this.$scopedSlots.user({
a: this.a
})}
</div>
)
}
}
</script>
<style scoped>
.abc {
background: rebeccapurple;
color: #fff;
}
</style>
- 父组件
父组件中没有使用 render + jsx 的方案,而是直接使用模板,应为 jsx 的babel 转换器是几年前写的,现在没有更新,对与 Vue 的 v-slot 指令并没有支持。
<template>
<div>
<hello-world />
<start v-slot:user="innerSlotData">
{{innerSlotData}}
<div>
<span>this div is test => this.$slot.default</span>
<span>123</span>
<span>{{innerSlotData.a}}</span>
</div>
</start>
</div>
</template>
<script>
import HelloWorld from '../components/HelloWorld'
import Start from '../components/Start'
export default {
name: 'Home',
components: {
HelloWorld,
Start
},
mounted () {
console.log(this.$scopedSlots.user) // undefined
}
}
</script>
<style lang="scss" scoped>
</style>