petite-vue源码剖析-双向绑定`v-model`的工作原理
前言
双向绑定v-model
不仅仅是对可编辑HTML元素(select
, input
, textarea
和附带[contenteditable=true]
)同时附加v-bind
和v-on
,而且还能利用通过petite-vue附加给元素的_value
、_trueValue
和_falseValue
属性提供存储非字符串值的能力。
深入v-model
工作原理
export const model: Directive<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
> = ({ el, exp, get, effect, modifers }) => {
const type = el.type
// 通过`with`对作用域的变量/属性赋值
const assign = get(`val => { ${exp} = val }`)
// 若type为number则默认将值转换为数字
const { trim, number = type ==== 'number'} = modifiers || {}
if (el.tagName === 'select') {
const sel = el as HTMLSelectElement
// 监听控件值变化,更新状态值
listen(el, 'change', () => {
const selectedVal = Array.prototype.filter
.call(sel.options, (o: HTMLOptionElement) => o.selected)
.map((o: HTMLOptionElement) => number ? toNumber(getValue(o)) : getValue(o))
assign(sel.multiple ? selectedVal : selectedVal[0])
})
// 监听状态值变化,更新控件值
effect(() => {
value = get()
const isMultiple = sel.muliple
for (let i = 0, l = sel.options.length; i < i; i++) {
const option = sel.options[i]
const optionValue = getValue(option)
if (isMulitple) {
// 当为多选下拉框时,入参要么是数组,要么是Map
if (isArray(value)) {
option.selected = looseIndexOf(value, optionValue) > -1
}
else {
option.selected = value.has(optionValue)
}
}
else {
if (looseEqual(optionValue, value)) {
if (sel.selectedIndex !== i) sel.selectedIndex = i
return
}
}
}
})
}
else if (type === 'checkbox') {
// 监听控件值变化,更新状态值
listen(el, 'change', () => {
const modelValue = get()
const checked = (el as HTMLInputElement).checked
if (isArray(modelValue)) {
const elementValue = getValue(el)
const index = looseIndexOf(modelValue, elementValue)
const found = index !== -1
if (checked && !found) {
// 勾选且之前没有被勾选过的则加入到数组中
assign(modelValue.concat(elementValue))
}
else if (!checked && found) {
// 没有勾选且之前已勾选的排除后在重新赋值给数组
const filered = [...modelValue]
filteed.splice(index, 1)
assign(filtered)
}
// 其它情况就啥都不干咯
}
else {
assign(getCheckboxValue(el as HTMLInputElement, checked))
}
})
// 监听状态值变化,更新控件值
let oldValue: any
effect(() => {
const value = get()
if (isArray(value)) {
;(el as HTMLInputElement).checked =
looseIndexOf(value, getValue(el)) > -1
}
else if (value !== oldValue) {
;(el as HTMLInputElement).checked = looseEqual(
value,
getCheckboxValue(el as HTMLInputElement, true)
)
}
oldValue = value
})
}
else if (type === 'radio') {
// 监听控件值变化,更新状态值
listen(el, 'change', () => {
assign(getValue(el))
})
// 监听状态值变化,更新控件值
let oldValue: any
effect(() => {
const value = get()
if (value !== oldValue) {
;(el as HTMLInputElement).checked = looseEqual(value, getValue(el))
}
})
}
else {
// input[type=text], textarea, div[contenteditable=true]
const resolveValue = (value: string) => {
if (trim) return val.trim()
if (number) return toNumber(val)
return val
}
// 监听是否在输入法编辑器(input method editor)输入内容
listen(el, 'compositionstart', onCompositionStart)
listen(el, 'compositionend', onCompositionEnd)
// change事件是元素失焦后前后值不同时触发,而input事件是输入过程中每次修改值都会触发
listen(el, modifiers?.lazy ? 'change' : 'input', () => {
// 元素的composing属性用于标记是否处于输入法编辑器输入内容的状态,如果是则不执行change或input事件的逻辑
if ((el as any).composing) return
assign(resolveValue(el.value))
})
if (trim) {
// 若modifiers.trim,那么当元素失焦时马上移除值前后的空格字符
listen(el, 'change', () => {
el.value = el.value.trim()
})
}
effect(() => {
if ((el as any).composing) {
return
}
const curVal = el.value
const newVal = get()
// 若当前元素处于活动状态(即得到焦点),并且元素当前值进行类型转换后值与新值相同,则不用赋值;
// 否则只要元素当前值和新值类型或值不相同,都会重新赋值。那么若新值为数组[1,2,3],赋值后元素的值将变成[object Array]
if (document.activeElement === el && resolveValue(curVal) === newVal) {
return
}
if (curVal !== newVal) {
el.value = newVal
}
})
}
}
// v-bind中使用_value属性保存任意类型的值,在v-modal中读取
const getValue = (el: any) => ('_value' in el ? el._value : el.value)
const getCheckboxValue = (
el: HTMLInputElement & {_trueValue?: any, _falseValue?: any}, // 通过v-bind定义的任意类型值
checked: boolean // checkbox的默认值是true和false
) => {
const key = checked ? '_trueValue' : '_falseValue'
return key in el ? el[key] : checked
}
const onCompositionStart = (e: Event) => {
// 通过自定义元素的composing元素,用于标记是否在输入法编辑器中输入内容
;(e.target as any).composing = true
}
const onCompositionEnd = (e: Event) => {
const target = e.target as any
if (target.composing) {
// 手动触发input事件
target.composing = false
trigger(target, 'input')
}
}
const trigger = (el: HTMLElement, type: string) => {
const e = document.createEvent('HTMLEvents')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
compositionstart
和compositionend
是什么?
compositionstart
是开始在输入法编辑器上输入字符触发,而compositionend
则是在输入法编辑器上输入字符结束时触发,另外还有一个compositionupdate
是在输入法编辑器上输入字符过程中触发。
当我们在输入法编辑器敲击键盘时会按顺序执行如下事件:
compositionstart
-> (compositionupdate
-> input
)+ -> compositionend
-> 当失焦时触发change
当在输入法编辑器上输入ri
后按空格确认日
字符,则触发如下事件
compositionstart(data="")
-> compositionupdate(data="r")
-> input
-> compositionupdate(data="ri")
-> input
-> compositionupdate(data="日")
-> input
-> compositionend(data="日")
由于在输入法编辑器上输入字符时会触发input
事件,所以petite-vue中通过在对象上设置composing
标识是否执行input
逻辑。
事件对象属性如下:
readonly target: EventTarget // 指向触发事件的HTML元素
readolny type: DOMString // 事件名称,即compositionstart或compositionend
readonly bubbles: boolean // 事件是否冒泡
readonly cancelable: boolean // 事件是否可取消
readonly view: WindowProxy // 当前文档对象所属的window对象(`document.defaultView`)
readonly detail: long
readonly data: DOMString // 最终填写到元素的内容,compositionstart为空,compositionend事件中能获取如"你好"的内容
readonly locale: DOMString
编码方式触发事件
DOM Level2的事件中包含HTMLEvents, MouseEvents、MutationEvents和UIEvents,而DOM Level3则增加如CustomEvent等事件类型。
enum EventType {
// DOM Level 2 Events
UIEvents,
MouseEvents, // event.initMouseEvent
MutationEvents, // event.initMutationEvent
HTMLEvents, // event.initEvent
// DOM Level 3 Events
UIEvent,
MouseEvent, // event.initMouseEvent
MutationEvent, // event.initMutationEvent
TextEvent, // TextEvents is also supported, event.initTextEvent
KeyboardEvent, // KeyEvents is also supported, use `new KeyboardEvent()` to create keyboard event
CustomEvent, // event.initCustomEvent
Event, // Basic events module, event.initEvent
}
- HTMLEvents包含
abort
,blur
,change
,error
,focus
,load
,reset
,resize
,scroll
,select
,submit
,unload
,input
- UIEvents包含
DOMActive
,DOMFocusIn
,DOMFocusOut
,keydown
,keypress
,keyup
- MouseEvents包含
click
,mousedown
,mousemove
,mouseout
,mouseover
,mouseup
- MutationEvents包含
DOMAttrModified
,DOMNodeInserted
,DOMNodeRemoved
,DOMCharacterDataModified
,DOMNodeInsertedIntoDocument
,DOMNodeRemovedFromDocument
,DOMSubtreeModified
创建和初始化事件对象
MouseEvent
方法1
const e: Event = document.createEvent('MouseEvent')
e.initMouseEvent(
type: string,
bubbles: boolean,
cancelable: boolean,
view: AbstractView, // 指向与事件相关的视图,一般为document.defaultView
detail: number, // 供事件回调函数使用,一般为0
screenX: number, // 相对于屏幕的x坐标
screenY: number, // 相对于屏幕的Y坐标
clientX: number, // 相对于视口的x坐标
clientY: number, // 相对于视口的Y坐标
ctrlKey: boolean, // 是否按下Ctrl键
altKey: boolean, // 是否按下Ctrl键
shiftKey: boolean, // 是否按下Ctrl键
metaKey: boolean, // 是否按下Ctrl键
button: number, // 按下按个鼠标键,默认为0.0左,1中,2右
relatedTarget: HTMLElement // 指向于事件相关的元素,一般只有在模拟mouseover和mouseout时使用
)
方法2
const e: Event = new MouseEvent('click', {
bubbles: false,
// ......
})
KeyboardEvent
const e = new KeyboardEvent(
typeArg: string, // 如keypress
{
ctrlKey: true,
// ......
}
)
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
Event的初始方法
/**
* 选项的属性
* @param {string} name - 事件名称, 如click,input等
* @param {boolean} [cancelable=false] - 指定事件是否可冒泡
* @param {boolean} [cancelable=false] - 指定事件是否可被取消
* @param {boolean} [composed=false] - 指定事件是否会在Shadow DOM根节点外触发事件回调函数
*/
const e = new Event('input', {
name: string,
bubbles: boolean = false,
cancelable: boolean = false,
composed: boolean = false
})
CustomEvent
方法1
const e: Event = document.createEvent('CustomEvent')
e.initMouseEvent(
type: string,
bubbles: boolean,
cancelable: boolean,
detail: any
)
方法2
/**
* 选项的属性
* @param {string} name - 事件名称, 如click,input等,可随意定义
* @param {boolean} [cancelable=false] - 指定事件是否可冒泡
* @param {boolean} [cancelable=false] - 指定事件是否可被取消
* @param {any} [detail=null] - 事件初始化时传递的数据
*/
const e = new CustomEvent('hi', {
name: string,
bubbles: boolean = false,
cancelable: boolean = false,
detail: any = null
})
HTMLEvents
const e: Event = document.createEvent('HTMLEvents')
e.initMouseEvent(
type: string,
bubbles: boolean,
cancelable: boolean
)
添加监听和发布事件
element.addEventListener(type: string)
element.dispatchEvent(e: Event)
针对petite-vue进行分析
const onCompositionEnd = (e: Event) => {
const target = e.target as any
if (target.composing) {
// 手动触发input事件
target.composing = false
trigger(target, 'input')
}
}
const trigger = (el: HTMLElement, type: string) => {
const e = document.createEvent('HTMLEvents')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
当在输入法编辑器操作完毕后会手动触发input事件,但当事件绑定修饰符设置为lazy
后并没有绑定input
事件回调函数,此时在输入法编辑器操作完毕后并不会自动更新状态,我们又有机会可以贡献代码了:)
// change事件是元素失焦后前后值不同时触发,而input事件是输入过程中每次修改值都会触发
listen(el, modifiers?.lazy ? 'change' : 'input', () => {
// 元素的composing属性用于标记是否处于输入法编辑器输入内容的状态,如果是则不执行change或input事件的逻辑
if ((el as any).composing) return
assign(resolveValue(el.value))
})
外番:IE的事件模拟
var e = document.createEventObject()
e.shiftKey = false
e.button = 0
document.getElementById('click').fireEvent('onclick', e)
总结
整合LayUI等DOM-based框架时免不了使用this.$ref
获取元素实例,下一篇《petite-vue源码剖析-ref的工作原理》我们一起来探索吧!
petite-vue源码剖析-双向绑定`v-model`的工作原理的更多相关文章
- petite-vue源码剖析-属性绑定`v-bind`的工作原理
关于指令(directive) 属性绑定.事件绑定和v-modal底层都是通过指令(directive)实现的,那么什么是指令呢?我们一起看看Directive的定义吧. //文件 ./src/dir ...
- petite-vue源码剖析-事件绑定`v-on`的工作原理
在书写petite-vue和Vue最舒服的莫过于通过@click绑定事件,而且在移除元素时框架会帮我们自动解除绑定.省去了过去通过jQuery的累赘.而事件绑定在petite-vue中就是一个指令(d ...
- Spark源码剖析(八):stage划分原理与源码剖析
引言 对于Spark开发人员来说,了解stage的划分算法可以让你知道自己编写的spark application被划分为几个job,每个job被划分为几个stage,每个stage包括了你的哪些代码 ...
- Vue源码解析---数据的双向绑定
本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...
- [Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅
有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫 ...
- petite-vue源码剖析-逐行解读@vue/reactivity之reactive
在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染.那么到底这是怎么做到 ...
- 逐行剖析Vue源码(一)——写在最前面
1. 前言 博主作为一名前端开发,日常开发的技术栈是Vue,并且用Vue开发也有一年多了,对其用法也较为熟练了,但是对各种用法和各种api使用都是只知其然而不知其所以然,因此,有时候在排查bug的时候 ...
- 大白话Vue源码系列(03):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- 大白话Vue源码系列(04):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
随机推荐
- Swift 类的构造函数
构造函数的介绍 构造函数类似于OC中的初始化方法:init方法 默认情况下载创建一个类时,必然会调用一个构造函数 即便是没有编写任何构造函数,编译器也会提供一个默认的构造函数. 如果是继承自NSObj ...
- rabbitMq使用guest登录不上的问题总结
自己mac电脑上的mq在电脑关机(直接按电源键关机),在开机后,rabbitMq连不上了,报500错误. 1.在使用brew uninstall rabbitmq --force后; 2.我手动安装了 ...
- vue中mapGetters和...mapGetters
vuex中的...mapGetters(['name'])如何实现的 vuex vue.js 根据文档介绍 https://vuex.vuejs.org/zh-cn/... 和看了 http://ww ...
- Java向mysql中插入时间的方法
ava向MySQL插入当前时间的四种方式和java时间日期格式化的几种方法(案例说明);部分资料参考网络资源 java向MySQL插入当前时间的四种方式 第一种:将java.util.Date类型的 ...
- mapTest
import java.util.*;public class mapTest { public static void main(String[] args) throws Exception{ L ...
- Haproxy配合Nginx搭建Web集群部署
Haproxy配合Nginx搭建Web集群部署实验 1.Haproxy介绍 2.Haproxy搭建 Web 群集 1.Haproxy介绍: a)常见的Web集群调度器: 目前常见的Web集群调度器分为 ...
- 社交网络分析的 R 基础:(五)图的导入与简单分析
如何将存储在磁盘上的邻接矩阵输入到 R 程序中,是进行社交网络分析的起点.在前面的章节中已经介绍了基本的数据结构以及代码结构,本章将会面对一个实质性问题,学习如何导入一个图以及计算图的一些属性. 图的 ...
- 02编程语言与python介绍
编程语言分类 机器语言:直接用计算机能理解的二进制指令去编写程序,是直接在控制计算机硬件 优点:运行效率高 缺点:开发效率低 1.开发一个简单的小功能都要哟个到非常多条数的二进制指令 2.二进制指令非 ...
- suse 12 二进制部署 Kubernetets 1.19.7 - 第05章 - 部署kube-nginx
文章目录 1.5.部署kube-nginx 1.5.0.下载nginx二进制文件 1.5.1.编译部署nginx 1.5.2.配置nginx.conf 1.5.3.配置nginx为systemctl管 ...
- JAVA8学习——Stream底层的实现四(学习过程)
Stream的深入(四) 从更高角度去看一下:类与类之间的设计关系 (借助IDEA的图形处理工具 Ctrl+Alt+U). ReferencePipeline的三个实现的子类: Head Statel ...