技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 的双向绑定原理与实现

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/47-vue的双向绑定原理与实现
本文目录11 个章节
  1. 一、什么是双向绑定
  2. 二、Vue 2.7.16 的响应式原理
  3. 三、Vue 3.5.39 的响应式原理
  4. 四、v-model 的实现方式
  5. 五、一个简化的 MVVM 实现
  6. 六、Vue 模板编译与 VNode
  7. 七、响应式更新队列
  8. 八、关于“数据劫持”的准确说法
  9. 九、总结
  10. 官方参考
  11. 原文出处

Vue 的双向绑定原理与实现

本文保留原文“从一个简化的 MVVM 了解 Vue 双向绑定”的主线,同时把 Vue 2.7.16 和 Vue 3.5.39 分开说明。

Vue 2.7 是 Vue 2 的最终 minor 版本,Vue 2 已于 2023 年 12 月 31 日结束官方维护;下面保留它主要是为了维护旧项目和理解历史源码。新项目应优先使用 Vue 3。

先给结论:Vue 的响应式更新本身主要是 状态到视图的单向更新v-model 再监听用户输入,把视图产生的新值写回状态,才形成开发者通常所说的“双向绑定”。不要把“响应式”“双向绑定”和“数据流方向”当成同一个概念。

一、什么是双向绑定

1.1 双向绑定与单向绑定

双向绑定通常指:

Model(状态) ──────────────> View(视图)
    ▲                            │
    └──── 用户输入/事件 ──────────┘
  • Model → View:状态变化,框架更新 DOM;
  • View → Model:用户输入触发事件,应用把新值写回状态。

单向数据流则强调状态由一个明确的数据源向下流动:

状态/props ──> 组件 ──> DOM
                  ▲
                  └── emit/action/事件通知变化

React、Vue 都可以实现表单的双向体验;Vue 并不是所有数据都双向流动。Vue 组件的 props 默认是单向下行,子组件应该通过事件通知父组件,而不是直接修改 prop。

1.2 原生元素上的 v-model

下面的写法:

<template>
  <input v-model="message" />
  <p>{{ message }}</p>
</template>

在文本输入框上可以近似理解为:

<input
  :value="message"
  @input="message = $event.target.value"
/>

这只是便于理解的展开,不是所有表单元素都使用完全相同的 property 和事件:

元素绑定的 DOM property默认事件
文本 inputtextareavalueinput
checkboxcheckedchange
radiocheckedchange
selectvaluechange

Vue 始终把 JavaScript 状态作为真实来源,不会把 HTML 上初始的 valuecheckedselected 当作之后的状态来源。

1.3 .lazy.trim.number

<input v-model.lazy="message" />
<input v-model.trim="name" />
<input v-model.number="age" />
  • .lazy:从 input 后同步改为 change 后同步;
  • .trim:去掉输入字符串两端空白;
  • .number:尝试使用 parseFloat() 转换为数字,转换失败时保留原字符串,空输入仍可能是空字符串。

type="number" 的输入在 Vue 3 中会自动采用 number 行为,但需要清晰表达意图时仍可以显式写 .number。中文、日文等 IME 输入处于 composition 阶段时,v-model 不会在每一个中间字符都更新;如果业务必须监听组合输入,应自行组合 valueinput 事件。

二、Vue 2.7.16 的响应式原理

2.1 Object.defineProperty + Dep + Watcher

Vue 2.7.16 的响应式系统仍然使用 getter/setter,而不是 Proxy:

初始化 data
   ↓
Observer 递归遍历对象
   ↓
Object.defineProperty 为属性定义 getter/setter
   ↓
渲染或 watch 读取属性,Dep 收集 Watcher
   ↓
setter 被调用,Dep.notify()
   ↓
Watcher.update() 进入调度队列
   ↓
重新渲染或执行用户回调

原文把它概括为“数据劫持 + 发布订阅”,这个说法可以保留,但需要知道它是教学层面的概括:Vue 2 还包含依赖去重、Watcher 栈、异步调度、数组方法改写和组件更新队列等机制。

原文数据代理示意图(本地化,历史资料)

2.2 Observer 与 defineReactive

下面是 Vue 2.7.16 源码思想的简化版,不是完整源码:

let uid = 0

class Dep {
  static target = null

  constructor() {
    this.id = uid++
    this.subs = []
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    const subs = this.subs.slice()
    for (const watcher of subs) {
      watcher.update()
    }
  }
}

function defineReactive(obj, key, value) {
  const dep = new Dep()
  let childOb = observe(value)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) {
        dep.depend()

        // 对象和数组还需要收集子 Observer 的依赖
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newValue) {
      if (Object.is(newValue, value)) {
        return
      }

      value = newValue
      childOb = observe(newValue)
      dep.notify()
    },
  })
}

function observe(value) {
  if (!value || typeof value !== 'object') {
    return
  }

  // 真实实现还会处理已有 Observer、不可扩展对象、数组等情况
  const ob = new Observer(value)
  return ob
}

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()

    if (Array.isArray(value)) {
      // Vue 2 会改写 push、pop、splice 等七个变异方法
      augmentArrayMethods(value)
      for (const item of value) {
        observe(item)
      }
    } else {
      for (const key of Object.keys(value)) {
        defineReactive(value, key, value[key])
      }
    }
  }
}

真正的 Vue 2.7.16 源码还会:

  • 使用 __ob__ 标记已经被观察的对象;
  • 为数组调用 dependArray,收集数组元素中嵌套对象的依赖;
  • 通过 Vue.set/this.$setVue.delete/this.$delete 处理新增、删除属性;
  • 忽略非对象、不可扩展对象和某些特殊对象;
  • 对被观察的数组改写变异方法,并在方法执行后通知依赖。

源码参考:Vue 2.7.16 observer/index.ts

2.3 Vue 2 的响应式限制

Object.defineProperty 只能拦截已经定义的属性,因此 Vue 2 不能自动发现:

this.user.age = 18
this.items[2] = 'new value'
this.items.length = 0

推荐写法:

this.$set(this.user, 'age', 18)
this.$set(this.items, 2, 'new value')
this.items.splice(0, this.items.length)

Vue 2.7 仍保留这些 API。Vue 3 使用 Proxy 后,普通对象新增和删除属性可以被追踪,但依然应该遵循清晰的状态建模,不要随意改变不属于组件的数据结构。

2.4 Dep 与 Watcher 的多对多关系

一个响应式属性 ──> 一个 Dep ──> 多个 Watcher
一个渲染表达式 ──> 一个 Watcher ──> 多个 Dep

例如模板中出现两次 name

<p>{{ name }}</p>
<span v-text="name"></span>

它们可能分别创建渲染 Watcher,但都会订阅 name 对应的 Dep。一个表达式 user.profile.name 则会在读取过程中访问多个响应式属性。

2.5 Dep.target 与依赖收集

渲染 Watcher 求值前,Vue 会把它放入全局的 Dep.target;getter 被读取时,Dep 就知道当前应该收集哪个 Watcher:

const targetStack = []

function pushTarget(watcher) {
  targetStack.push(watcher)
  Dep.target = watcher
}

function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

class Watcher {
  constructor(vm, getter, cb, options = {}) {
    this.vm = vm
    this.getter = getter
    this.cb = cb
    this.deps = new Set()
    this.lazy = !!options.lazy
    this.dirty = this.lazy
    this.value = this.lazy ? undefined : this.get()
  }

  addDep(dep) {
    if (!this.deps.has(dep)) {
      this.deps.add(dep)
      dep.subs.push(this)
    }
  }

  get() {
    pushTarget(this)
    let value
    try {
      value = this.getter.call(this.vm)
    } finally {
      popTarget()
    }
    return value
  }

  update() {
    if (this.lazy) {
      this.dirty = true
      return
    }

    queueWatcher(this)
  }

  evaluate() {
    this.value = this.get()
    this.dirty = false
    return this.value
  }
}

真实 Watcher 还需要 addDepdepIds 去重、深度遍历、用户 Watcher 错误处理和销毁逻辑。Vue 2 的 computed 使用 lazy Watcher;渲染 Watcher 和 user watcher 则会通过 scheduler 合并更新。

三、Vue 3.5.39 的响应式原理

3.1 Proxy 替代 getter/setter

Vue 3 使用 Proxy:

import { reactive, ref } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'Ada',
  },
})

state.count++
state.user.name = 'Grace'

const count = ref(0)
count.value++

reactive() 返回一个 Proxy;嵌套对象在访问时也会被转换为响应式代理。ref().value 保存值,如果值是对象,内部也会采用响应式转换。

Proxy 可以拦截属性读取、写入、删除、in、ownKeys 等操作,因此 Vue 3 不再需要 Vue 2 的 $set。但 reactive() 仍然只适合对象、数组、Map、Set 等对象类型;基本类型应使用 ref()

Vue 2.7 的 Composition API 是回移植版本,仍基于 getter/setter。下面这个差异很容易被忽略:

// Vue 2.7:reactive 会直接转换原对象
reactive(object) === object // true

// Vue 3:reactive 返回代理对象
reactive(object) === object // false

3.2 tracktrigger 和依赖表

Vue 3 响应式可以抽象为:

WeakMap<rawTarget, Map<key, Dep>>

读取时:

const targetMap = new WeakMap()

function track(target, key) {
  if (!activeEffect) {
    return
  }

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  dep.add(activeEffect)
}

写入时,Vue 会根据操作类型判断是否真的发生了变化,再触发相关 effect:

function trigger(target, key, type, newValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }

  const effects = new Set(depsMap.get(key))
  for (const effect of effects) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

这是便于理解的伪代码。Vue 3.5.39 的真实实现已经使用 DepLink、版本号和 Subscriber 等机制优化依赖关系;不要把上面的 Set 伪代码当成当前版本逐行源码。

源码参考:

3.3 响应式更新不是同步 DOM 更新

修改状态后,Vue 通常会把组件更新放入队列并批量执行:

import { nextTick, ref } from 'vue'

const count = ref(0)

async function increment() {
  count.value++
  console.log(document.querySelector('#count')?.textContent)
  // 这里可能还是旧 DOM

  await nextTick()
  console.log(document.querySelector('#count')?.textContent)
  // 这里可以读取本次更新后的 DOM
}

Vue 2 的 nextTick 会优先尝试 Promise、MutationObserver、setImmediate,最后回退到 setTimeout;Vue 3 的调度器以 Promise 微任务队列为主要实现。两者的公共语义都是“等待当前更新批次完成”,不要把它理解成固定的 0ms 定时器。

四、v-model 的实现方式

4.1 原生元素

原生 v-model 会由编译器根据元素类型生成合适的 prop 和事件:

<input v-model="text" />
<textarea v-model="text" />
<input type="checkbox" v-model="checked" />
<select v-model="selected">...</select>

Vue 3 的模板编译器会生成对应的 vModelTextvModelCheckboxvModelSelect 等运行时指令。它还要处理 IME、checkbox 数组、radio、动态 value 和 .lazy/.trim/.number 等细节,所以“v-model 等于一行 @input”只是最简单文本输入框的近似。

原文 v-model 绑定示意图(本地化,历史资料)

4.2 Vue 2 组件上的 v-model

Vue 2 默认协议是:

<CustomInput v-model="title" />

近似展开为:

<CustomInput
  :value="title"
  @input="title = $event"
/>

子组件:

<script>
export default {
  props: {
    value: String,
  },
  methods: {
    updateValue(event) {
      this.$emit('input', event.target.value)
    },
  },
}
</script>

<template>
  <input :value="value" @input="updateValue" />
</template>

Vue 2 可以使用 model 选项修改默认的 prop 和事件名:

export default {
  model: {
    prop: 'checked',
    event: 'change',
  },
  props: {
    checked: Boolean,
  },
}

4.3 Vue 3 组件上的 v-model

Vue 3 默认协议是:

<CustomInput v-model="title" />

近似展开为:

<CustomInput
  :modelValue="title"
  @update:modelValue="title = $event"
/>

手写子组件:

<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
  />
</template>

Vue 3.4+ 推荐使用 defineModel

<script setup lang="ts">
const model = defineModel<string>()
</script>

<template>
  <input v-model="model" />
</template>

多个模型:

<UserName
  v-model:first-name="firstName"
  v-model:last-name="lastName"
/>
<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')
</script>

五、一个简化的 MVVM 实现

下面的实现用于帮助理解原文中的 ObserverCompileDepWatcher 如何连接起来。它只支持少量语法,不是 Vue 的替代品,也没有处理安全、组件、列表、异步调度、表达式解析和完整错误边界。

5.1 数据代理

function proxy(vm, sourceKey, key) {
  Object.defineProperty(vm, key, {
    enumerable: true,
    configurable: true,
    get() {
      return vm[sourceKey][key]
    },
    set(value) {
      vm[sourceKey][key] = value
    },
  })
}

function proxyData(vm, data) {
  for (const key of Object.keys(data)) {
    proxy(vm, '$data', key)
  }
}

这样可以通过 vm.message 访问 vm.$data.message,但数据代理本身不负责依赖收集;依赖收集发生在被观察的数据属性 getter 中。

5.2 Dep 和 Watcher

let activeWatcher = null
const watcherStack = []

class Dep {
  constructor() {
    this.subs = new Set()
  }

  depend() {
    if (activeWatcher) {
      this.subs.add(activeWatcher)
    }
  }

  notify() {
    for (const watcher of this.subs) {
      watcher.update()
    }
  }
}

class Watcher {
  constructor(vm, getter, callback) {
    this.vm = vm
    this.getter = getter
    this.callback = callback
    this.value = this.get()
  }

  get() {
    watcherStack.push(this)
    activeWatcher = this
    try {
      return this.getter.call(this.vm)
    } finally {
      watcherStack.pop()
      activeWatcher = watcherStack[watcherStack.length - 1]
    }
  }

  update() {
    const oldValue = this.value
    const newValue = this.get()

    if (!Object.is(oldValue, newValue)) {
      this.value = newValue
      this.callback(newValue, oldValue)
    }
  }
}

5.3 递归观察数据

function observe(value) {
  if (!value || typeof value !== 'object') {
    return
  }

  for (const key of Object.keys(value)) {
    defineReactive(value, key, value[key])
  }
}

function defineReactive(object, key, initialValue) {
  const dep = new Dep()
  let value = initialValue

  observe(value)

  Object.defineProperty(object, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend()
      return value
    },
    set(nextValue) {
      if (Object.is(nextValue, value)) {
        return
      }

      value = nextValue
      observe(nextValue)
      dep.notify()
    },
  })
}

完整实现还需要处理数组变异方法、对象依赖、循环引用和重复 observe。上面的代码只能演示最核心的 getter 收集、setter 通知。

5.4 表达式读取与写入

function parsePath(path) {
  const segments = path.split('.')

  return object => {
    let value = object
    for (const segment of segments) {
      if (value == null) {
        return undefined
      }
      value = value[segment]
    }
    return value
  }
}

function setPath(object, path, value) {
  const segments = path.split('.')
  const lastKey = segments.pop()
  let target = object

  for (const segment of segments) {
    if (target[segment] == null) {
      target[segment] = {}
    }
    target = target[segment]
  }

  target[lastKey] = value
}

真实 Vue 的模板表达式不只是 split('.'):它支持运算、条件、函数调用、可选链等由编译器解析的语法。原文中“Vue 的 parsePath 不支持 abc['bbc']”只适合这个简化的路径读取函数,不能直接概括成 Vue 模板的全部能力。

原文还保留了几张调试截图,展示简化编译器从模板读取指令、解析表达式和更新节点的过程。截图中的实现是教学代码,不是 Vue 2.7.16 的完整源码:

简化编译器调试截图一(本地化,历史资料)简化编译器调试截图二(本地化,历史资料)简化编译器调试截图三(本地化,历史资料)

5.5 编译文本和 v-model

const interpolationRE = /\{\{\s*([^}]+?)\s*\}\}/g

function getTextValue(vm, template) {
  return template.replace(interpolationRE, (_, expression) => {
    const getter = parsePath(expression.trim())
    const value = getter(vm)
    return value == null ? '' : String(value)
  })
}

function compileText(node, vm) {
  const template = node.textContent
  if (!interpolationRE.test(template)) {
    return
  }

  interpolationRE.lastIndex = 0
  const expressions = [...template.matchAll(interpolationRE)]
    .map(match => match[1].trim())

  const getter = () => {
    // 为了让所有表达式参与依赖收集,逐个读取
    expressions.forEach(expression => parsePath(expression)(vm))
    return getTextValue(vm, template)
  }

  const update = () => {
    node.textContent = getter()
  }

  new Watcher(vm, getter, update)
  update()
}

function compileModel(node, vm, expression) {
  const getter = parsePath(expression)

  const update = value => {
    node.value = value == null ? '' : String(value)
  }

  new Watcher(vm, getter, update)
  update(getter(vm))

  node.addEventListener('input', event => {
    setPath(vm, expression, event.target.value)
  })
}

v-html 可以把字符串当 HTML 插入,但不要把用户可控内容直接交给 innerHTMLv-html。实际应用应优先使用文本插值,并在确实需要 HTML 时进行可信来源控制和清洗。

5.6 Compile 的基本流程

class Compile {
  constructor(root, vm) {
    this.root = typeof root === 'string'
      ? document.querySelector(root)
      : root
    this.vm = vm

    if (!this.root) {
      return
    }

    const fragment = document.createDocumentFragment()
    while (this.root.firstChild) {
      fragment.appendChild(this.root.firstChild)
    }

    this.compile(fragment)
    this.root.appendChild(fragment)
  }

  compile(node) {
    for (const child of [...node.childNodes]) {
      if (child.nodeType === Node.ELEMENT_NODE) {
        this.compileElement(child)
        this.compile(child)
      } else if (child.nodeType === Node.TEXT_NODE) {
        compileText(child, this.vm)
      }
    }
  }

  compileElement(node) {
    for (const attribute of [...node.attributes]) {
      const { name, value } = attribute

      if (name === 'v-model') {
        compileModel(node, this.vm, value)
      } else if (name.startsWith('@')) {
        const method = this.vm[value]
          || this.vm.$options.methods?.[value]
          || this.vm.$options[value]
        if (typeof method === 'function') {
          node.addEventListener(name.slice(1), method.bind(this.vm))
        }
      }
    }
  }
}

原文使用 DocumentFragment 批量把节点移到内存中,这个思路可以用来理解早期 MVVM 编译器。但现代 Vue SFC 通常在构建阶段编译模板,运行时执行的是渲染函数和 VNode patch,不会按这个简化版本逐个查找所有 DOM 指令。

原文中的事件指令、普通指令、class 更新和 watcher 更新截图如下:

简化编译器事件指令截图(本地化,历史资料)简化编译器文本与 model 工具截图(本地化,历史资料)简化编译器 class 更新截图(本地化,历史资料)简化编译器 watcher 更新截图(本地化,历史资料)

5.7 MVVM 入口

class MVVM {
  constructor(options) {
    this.$options = options
    this.$data = options.data || {}

    proxyData(this, this.$data)
    observe(this.$data)

    if (options.el) {
      new Compile(options.el, this)
    }
  }
}

const vm = new MVVM({
  el: '#app',
  data: {
    message: 'hello',
  },
  methods: {
    show() {
      window.alert(this.message)
    },
  },
})

示例模板:

<div id="app">
  <input v-model="message" />
  <p>{{ message }}</p>
  <button @click="show">显示</button>
</div>

执行链路是:

new MVVM
  ↓
proxyData + observe
  ↓
Compile 扫描 {{ message }} 和 v-model
  ↓
创建 Watcher,读取 message,触发 getter 收集 Dep
  ↓
input 事件写入 message
  ↓
setter 调用 dep.notify()
  ↓
Watcher 更新文本和 input value

六、Vue 模板编译与 VNode

6.1 Vue 2 的模板编译

Vue 2 的编译流程通常概括为:

template 字符串
   ↓ parse
AST
   ↓ optimize
标记静态节点
   ↓ generate
render 函数代码
   ↓
VNode

Vue 2 的运行时编译器会解析标签、文本、指令和表达式。生产构建一般在构建阶段完成编译,运行时构建才会包含模板编译器。

原文 MVVM 原理图(本地化,历史资料)

原文编译流程图(本地化,历史资料)

6.2 Vue 3 的编译器优化

Vue 3 仍然生成 VNode,但采用“编译器感知的 Virtual DOM”:

  • 静态节点提升(hoist static);
  • 静态子树缓存;
  • patch flags 标记只会变化的 class、style、text 等;
  • block tree 收集动态后代;
  • SSR hydration 使用 block 和 patch flag 走更快路径;
  • v-oncev-memo 可在适合的场景减少重复更新。

例如:

<div class="static">固定内容</div>
<div :class="className">{{ message }}</div>

编译器可以告诉运行时第二个节点主要需要更新 class 和文本,而不必每次完整比较所有属性。patch flag 的判断会使用位运算,但这只是实现细节,不能简单说“Vue 3 的 diff 只靠位运算”。

原文数据与视图更新流程图(本地化,历史资料)

参考:Vue Rendering Mechanism

七、响应式更新队列

7.1 为什么要异步批量更新

vm.message = 'one'
vm.message = 'two'
vm.message = 'three'

Vue 不需要为同一事件循环中的三次修改都执行完整渲染。它会对待执行的 watcher/job 去重,在更新批次中统一刷新。

this.message = 'updated'
console.log(this.$el.textContent) // 可能还是旧值

this.$nextTick(() => {
  console.log(this.$el.textContent) // 更新后的 DOM
})

“数据已变但 DOM 还没变”不是响应式失效,而是批量更新尚未 flush。需要访问更新后的 DOM 时使用 nextTick

7.2 Vue 2 与 Vue 3 调度差异

版本依赖/更新模型等待 DOM 更新
Vue 2.7Watcher、Dep、scheduler、queueWatchervm.$nextTickVue.nextTick
Vue 3.5ReactiveEffect/Subscriber、scheduler、job queuenextTick()

Vue 3 的 watch 还可以通过 flush: 'pre' | 'post' | 'sync' 选择回调时机;watchPostEffect 适合需要在 DOM 更新后执行的副作用。sync 会失去批量调度优势,应该谨慎使用。

八、关于“数据劫持”的准确说法

“劫持”是历史文章中的形象说法,更准确的表述是:

  • Vue 2:通过 Object.defineProperty 定义 getter/setter,并由 Dep/Watcher 追踪依赖;
  • Vue 3:通过 Proxy/ref 的读取和写入拦截,由依赖表触发 effect;
  • Vue 2.7:虽然有 Composition API 和 <script setup> 回移植,但底层仍不是 Vue 3 的 Proxy 响应式;
  • Vue 3 的响应式对象是代理,原始对象与代理对象不是同一个引用;
  • 非所有状态都应该做深层响应式,性能敏感的大型不可变数据可以考虑 shallowRefmarkRaw,但必须明确这些 API 会减少自动追踪。

九、总结

  1. “双向绑定”可以理解为 状态到视图的响应式更新 + 视图事件写回状态
  2. Vue 2.7.16 使用 Object.defineProperty、Observer、Dep、Watcher 和异步更新队列;
  3. Vue 2 需要用 $set/splice 处理部分新增属性和数组索引变化;
  4. Vue 3.5.39 使用 Proxy、ref 和新的依赖/调度实现,普通新增属性不再需要 $set
  5. Vue 2 组件 v-model 默认是 value/input,Vue 3 默认是 modelValue/update:modelValue
  6. Vue 3.4+ 可使用 defineModel,Vue 2.7 的 Composition API 仍基于 Vue 2 getter/setter;
  7. 简化 MVVM 中的 Observer、Compile、Dep、Watcher 适合学习原理,不能当作完整 Vue 实现;
  8. Vue 2 和 Vue 3 都会批量更新 DOM,DOM 时序问题使用 nextTick 解决;
  9. 模板编译会为运行时提供静态提升、patch flags 和 block tree 等优化信息。

官方参考

Vue 2.7.16

Vue 3.5.39

原文出处

作者:uu_Q

来源:稀土掘金

本文保留原文的 MVVM、数据代理、模板编译、Observer/Dep/Watcher 和 v-model 示例,并补充 Vue 2.7.16、Vue 3.5.39 的实现差异。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS