Vue.js 3.x 双向绑定原理

什么是双向绑定?

废话不多说,我们先来看一个 v-model 基本的示例:

<input type="text" v-model="search">

首先,我们要明白一点的是:v-model 的本质是指令。因此,它跟我们一般的自定义指令是一样的,需要实现 Vue.js 生命周期的钩子函数。

其次,v-model 实现了双向绑定,也就是:数据到 DOM 的单向流动DOM 到数据的单向流动

明白了上面这两点,再来看代码就清晰多了。

// packages/runtime-dom/src/directives/vModel.ts

export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  created() {},
  mounted() {},
  beforeUpdate() {}
}

打开 v-model 的源码我们可以看到,它实现了对应的 Vue.js 生命周期钩子函数,实际上它就是一个内置的自定义指令。

那么,v-model 如何实现双向绑定的呢?具体来说,数据到 DOM 的单向流动以及DOM 到数据的单向流动是如何实现的。

数据到 DOM 的单向流动

// packages/runtime-dom/src/directives/vModel.ts

export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  // set value on mounted so it's after min/max for type="range"
  mounted(el, { value }) {
    el.value = value == null ? '' : value
  }
}

数据到 DOM 的单向流动实现非常简单,一行代码就搞定了,就是把 v-model 绑定的值赋值给 el.value

DOM 到数据的单向流动

// packages/runtime-dom/src/directives/vModel.ts

export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  created(el, { modifiers: { lazy, trim, number } }, vnode) {
    el._assign = getModelAssigner(vnode)
    
    // see: https://github.com/vuejs/core/issues/3813
    const castToNumber = number || (vnode.props && vnode.props.type === 'number')
    
    // 实现 lazy 功能
    addEventListener(el, lazy ? 'change' : 'input', e => {
      // `composing=true` 时不把 DOM 的值赋值给数据
      if ((e.target as any).composing) return
      
      let domValue: string | number = el.value
      if (trim) {
        domValue = domValue.trim()
      } else if (castToNumber) {
        domValue = toNumber(domValue)
      }
      
      // DOM 的值改变时,同时改变对应的数据(即改变 v-model 上绑定的变量的值)
      el._assign(domValue)
    })
    
    // 实现 trim 功能
    if (trim) {
      addEventListener(el, 'change', () => {
        el.value = el.value.trim()
      })
    }
    
    // 不配置 lazy 时,监听的是 input 的 input 事件,它会在用户实时输入的时候触发。
    // 此外,还会多监听 compositionstart 和 compositionend 事件。
    if (!lazy) {
        // 这是因为,用户使用拼音输入法开始输入汉字时,这个事件会被触发,
        // 此时,设置 `composing=true`,在 input 事件回调里可以进行判断,避免将 DOM 的值赋值给数据,
      // 因为此时并未输入完成。
      addEventListener(el, 'compositionstart', onCompositionStart)
      
      // 当用户从输入法中确定选中了一些数据完成输入后(如中文输入法常见的按空格确认输入的文字),
      // 设置 `composing=false`,在 onCompositionEnd 中手动触发 input 事件,完成数据的赋值。
      addEventListener(el, 'compositionend', onCompositionEnd)
      
      // Safari < 10.2 & UIWebView doesn't fire compositionend when
      // switching focus before confirming composition choice
      // this also fixes the issue where some browsers e.g. iOS Chrome
      // fires "change" instead of "input" on autocomplete.
      addEventListener(el, 'change', onCompositionEnd)
    }
  }
}

function onCompositionStart(e: Event) {
  (e.target as any).composing = true
}

function onCompositionEnd(e: Event) {
  const target = e.target as any
  if (target.composing) {
    target.composing = false
    target.dispatchEvent(new Event('input'))
  }
}

const getModelAssigner = (vnode: VNode): AssignerFn => {
  const fn = vnode.props!['onUpdate:modelValue']
  return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
}

代码有点多,但原理很简单:

  • 通过自定义监听事件 addEventListener 来监听 input 元素的 inputchange 事件
  • 当用户手动输入数据时执行对应的函数,并通过 el.value 获取 input 的新值
  • 调用 el._assignonUpdate:modelValue 属性对应的函数)方法 v-model 绑定的值

而实现 DOM 到数据的单向流动,关键就在 onUpdate:modelValue。借助 Vue 3 Template Explorer,我们可以查看其编译后生成的 render 函数,可以发现它做所的事情并没有什么神奇的地方,就是帮我们自动更新 v-model 上绑定的变量的值。

<input type="text" v-model="search">

import { vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return _withDirectives((_openBlock(), _createElementBlock("input", {
    type: "text",
    
    // `onUpdate:modelValue` 所做的事,
    // 就是自动帮我们更新 `v-model` 上绑定的变量的值。
    "onUpdate:modelValue": $event => ((_ctx.search) = $event)
    
  }, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
    [_vModelText, _ctx.search]
  ])
}

除此之外,还有对 lazy 的处理、trim 的处理、数字的处理、以及解决正在输入时文本被清空的问题。

关于 onCompositionStartonCompositionEnd 两个方法的作用,详见 text added with IME to input that has v-model is gone when the view is updated #2302

一句话总结:通过使用 addEventListener 来实现 DOM 到数据的单向流动

最后是 beforeUpdate 的实现,如果数据的值和 DOM 的值不一致,则将数据更新到 DOM:

// packages/runtime-dom/src/directives/vModel.ts

beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
    el._assign = getModelAssigner(vnode)
    // avoid clearing unresolved text. #2302
    // 输入某些语言如中文,在没有输入完成时,在更新时会自动将已存在的文本清空,具体可见 issue#2302
    if ((el as any).composing) return
  
    if (document.activeElement === el) {
      if (lazy) {
        return
      }
      if (trim && el.value.trim() === value) {
        return
      }
      if ((number || el.type === 'number') && toNumber(el.value) === value) {
        return
      }
    }
    const newValue = value == null ? '' : value
    if (el.value !== newValue) {
      el.value = newValue
    }
  }

以上就是 text 类型的 input 元素双向绑定原理,当然 input 元素类型不止这个,还有诸如 radiocheckbox 等类型,大家有兴趣的话可以自己去看,但是原理都是相同的,就是实现两个功能:数据到 DOM 的单向流动DOM 到数据的单向流动

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容