首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >petite-vue源码剖析-双向绑定`v-model`的工作原理

petite-vue源码剖析-双向绑定`v-model`的工作原理

原创
作者头像
PHP开发工程师
发布于 2022-03-15 04:09:15
发布于 2022-03-15 04:09:15
93000
代码可运行
举报
文章被收录于专栏:thinkphp+vuethinkphp+vue
运行总次数:0
代码可运行

前言

双向绑定v-model不仅仅是对可编辑HTML元素(select, input, textarea和附带[contenteditable=true])同时附加v-bindv-on,而且还能利用通过petite-vue附加给元素的_value_trueValue_falseValue属性提供存储非字符串值的能力。

深入v-model工作原理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)
}
复制代码

compositionstartcompositionend是什么?

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逻辑。

事件对象属性如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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等事件类型。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const e: Event = new MouseEvent('click', {
  bubbles: false,
  // ......
})
复制代码
KeyboardEvent
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const e = new KeyboardEvent(
  typeArg: string, // 如keypress
  {
    ctrlKey: true,
    // ......
  }
)
复制代码

developer.mozilla.org/en-US/docs/…

Event的初始方法
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 选项的属性
 * @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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const e: Event = document.createEvent('CustomEvent')
e.initMouseEvent(
  type: string,
  bubbles: boolean,
  cancelable: boolean,
  detail: any
)
复制代码

方法2

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 选项的属性
 * @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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const e: Event = document.createEvent('HTMLEvents')
e.initMouseEvent(
  type: string,
  bubbles: boolean,
  cancelable: boolean
)
复制代码

添加监听和发布事件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
element.addEventListener(type: string)
element.dispatchEvent(e: Event)
复制代码

针对petite-vue进行分析

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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事件回调函数,此时在输入法编辑器操作完毕后并不会自动更新状态,我们又有机会可以贡献代码了:)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// change事件是元素失焦后前后值不同时触发,而input事件是输入过程中每次修改值都会触发
    listen(el, modifiers?.lazy ? 'change' : 'input', () => {
      // 元素的composing属性用于标记是否处于输入法编辑器输入内容的状态,如果是则不执行change或input事件的逻辑
      if ((el as any).composing) return
      assign(resolveValue(el.value))
    })
复制代码

外番:IE的事件模拟

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var e = document.createEventObject()
e.shiftKey = false
e.button = 0
document.getElementById('click').fireEvent('onclick', e)
复制代码

总结

整合LayUI等DOM-based框架时免不了使用this.$ref获取元素实例

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
一文搞定 Flink Job 提交全流程
前面,我们已经分析了 一文搞定 Flink 消费消息的全流程 、写给大忙人看的 Flink Window原理 还有 一文搞定 Flink Checkpoint Barrier 全流程 等等,接下来也该回归到最初始的时候,Flink Job 是如何提交的。
shengjk1
2020/07/13
2.6K0
从头分析flink源码第五篇之提交jobGraph时各组件内部都发生了什么?
上几篇文章中我们分析了一个flink wordcount任务生成streamGraph和jobGraph的过程。接下来,我们继续从jobGraph生成后开始来分析executionGraph的生成过程及任务的提交过程,本文主要分析任务提交过程中各组件的执行逻辑,如TaskManager、ResourceManager、JobManager等。本文只涉及到本地运行wordcount时各组件的内部运行逻辑分析,不包括其他资源管理模式如yarn或Kubernetes模式下任务的提交流程(后续会专门行文来分析)。文章较长,代码较多,不喜慎入。
山行AI
2021/09/14
1.4K0
从头分析flink源码第五篇之提交jobGraph时各组件内部都发生了什么?
Flink源码解读系列 | 任务提交流程
Flink在1.10版本对整个作业提交流程有了较大改动,详情请见FLIP-73。本文基于1.10对作业提交的关键流程进行分析,不深究。 入口: 依旧是main函数最后env.execute();
大数据真好玩
2020/09/22
9450
一文搞定 Flink Job 的运行过程
之前我们知道了Flink 是如何生成 StreamGraph 以及 如何生成 job 和 如何生成Task,现在我们通过 Flink Shell 将他们串起来,这样我们就学习了从写代码开始到 Flink 运行 task 的整个过程是怎么样的。
shengjk1
2021/04/25
2.3K0
一文搞定 Flink Job 的运行过程
一文搞懂 checkpoint 全过程
前面我们讲解了 一文搞懂 Flink 处理 Barrier 全过程 和 一文搞定 Flink Checkpoint Barrier 全流程 基本上都是跟 checkpoint 相关。这次我们就具体看一下 checkpoint 是如何发生的。
shengjk1
2020/07/06
1.4K0
聊聊flink的RichParallelSourceFunction
flink-streaming-java_2.11-1.6.2-sources.jar!/org/apache/flink/streaming/api/functions/source/ParallelSourceFunction.java
code4it
2018/11/28
3.2K0
聊聊flink的RichParallelSourceFunction
追源索骥:透过源码看懂Flink核心框架的执行流程
写在最前:因为这篇博客太长,所以我把它转成了带书签的pdf格式,看起来更方便一点。想要的童鞋可以到我的公众号“老白讲互联网”后台留言flink即可获取。
老白
2018/08/01
10.3K4
追源索骥:透过源码看懂Flink核心框架的执行流程
聊聊flink的JobManagerGateway
flink-1.7.2/flink-runtime/src/main/java/org/apache/flink/runtime/webmonitor/RestfulGateway.java
code4it
2019/03/18
7970
聊聊flink的JobManagerGateway
flink的local模式启动全流程源码分析
这是一个执行WordCount的操作,我们以这个demo为入口来对整个执行流程进行分析记录。
山行AI
2020/05/22
2.1K0
flink的local模式启动全流程源码分析
从头分析flink源码第六篇之ExecutionGraph的生成
上一篇中我们梳理了jobGraph提交过程中taskmanager、jobmanager、resourcemanager各组件的启动流程,本篇我们接着上一篇中的内容来分析一下从jobGraph生成ExecutionGraph的源码执行流程。
山行AI
2021/09/14
7730
从头分析flink源码第六篇之ExecutionGraph的生成
从头分析flink源码第三篇之jobGraph的生成
上一篇中我们分析了一个简单的flink wordcount程序由DataStream的transformation列表转换成StreamGraph的过程,紧接着上文的步骤,本文我们着重分析一下从streamGraph生成jobGraph的过程。
山行AI
2021/07/01
1.9K1
[源码解析] Flink的Slot究竟是什么?(2)
Flink的Slot概念大家应该都听说过,但是可能很多朋友还不甚了解其中细节,比如具体Slot究竟代表什么?在代码中如何实现?Slot在生成执行图、调度、分配资源、部署、执行阶段分别起到什么作用?本文和上文将带领大家一起分析源码,为你揭开Slot背后的机理。
罗西的思考
2020/09/07
1.4K0
[源码解析] Flink的Slot究竟是什么?(2)
Flink1.10任务提交流程分析(二)
在Flink1.10任务提交流程分析(一)中分析了从flink run开始到任务提交到集群前的流程分析,对于不同的提交模式Flink中使用不同的PipelineExecutor,本篇基于yarn-per-job模式分析向yarn-cluster提交任务的流程。(注:基于1.10.1分析)
Flink实战剖析
2022/04/18
6950
一文搞定 Flink Task 提交执行全流程
这里创建了一个 Task 对象并启动,我们来看一下 Task 启动的时候都做了什么
shengjk1
2020/07/14
1.4K0
【万字长文】详解Flink作业提交流程
Flink 作业在开发完毕之后,需要提交到 Flink 集群执行。ClientFronted 是入口,触发用户开发的 Flink 应用 Jar 文件中的 main 方法,然后交给 PipelineExecutor(流水线执行器,在 FlinkClient 升成 JobGraph 之后,将作业提交给集群的重要环节。)#execue 方法,最终会选择一个触发一个具体的 PiplineExecutor 执行。
857技术社区
2022/05/17
2.2K0
【万字长文】详解Flink作业提交流程
一文搞懂 Flink如何移动计算
对于分布式框架来说,我们经常听到的一句话就是:移动计算,不移动数据。那么对于 Flink 来说是如何移动计算的呢?我们一起重点看一下 ExecuteGraph
shengjk1
2020/09/21
5890
一文搞懂 Flink如何移动计算
flink on yarn部分源码解析 (FLIP-6 new mode)
我们在https://www.cnblogs.com/dongxiao-yang/p/9403427.html文章里分析了flink提交single job到yarn集群上的代码,flink在1.5版
sanmutongzi
2020/03/04
9660
flink on yarn部分源码解析 (FLIP-6 new mode)
聊聊flink的KvStateRegistryGateway
flink-1.7.2/flink-runtime/src/main/java/org/apache/flink/runtime/jobmaster/KvStateRegistryGateway.java
code4it
2019/03/19
5880
聊聊flink的KvStateRegistryGateway
flink指定jobid
org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
路过君
2022/06/01
9160
聊聊flink LocalEnvironment的execute方法
flink-java-1.6.2-sources.jar!/org/apache/flink/api/java/DataSet.java
code4it
2018/11/21
1.6K0
聊聊flink LocalEnvironment的execute方法
推荐阅读
相关推荐
一文搞定 Flink Job 提交全流程
更多 >
交个朋友
加入CloudBaseAI生成专属群
AI生成式应用探索 专属技术答疑空间
加入AICoding云开发技术交流群
智能编码实践分享 聚焦AI+云开发
加入数据技术工作实战群
获取实战干货 交流技术经验
换一批
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档