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

显示模式

登录
ARCHIVE DOCUMENTVUE

0 到 1 掌握:Vue 核心之数据双向绑定

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/01-0 到 1 掌握:Vue 核心之数据双向绑定
本文目录12 个章节
  1. 前言
  2. 一、什么是 MVVM 数据双向绑定
  3. 二、Vue 2 的监听器 Observer
  4. 三、Dep:依赖管理
  5. 四、Watcher:收集依赖并更新
  6. 五、Compile 与真正的 v-model
  7. 六、Vue 2 源码主链路
  8. 七、原文 Vue 2 历史源码走读
  9. 八、原文 Compile 到完整演示(Vue 2 风格教学实现)
  10. 九、Vue 3:Proxy、ref 与 effect
  11. 十、总结
  12. 官方参考

0 到 1 掌握:Vue 核心之数据双向绑定

Category(分类): Vue Status: 已校订(Vue 2 原理 + Vue 3 更新)

前言

当被问到 Vue 数据双向绑定原理时,很多人会脱口而出:Vue 通过 Object.definePropertydata 属性转换成 getter/setter,数据变化时通知视图更新。这句话描述的是 Vue 2 的响应式基础,而不是所有版本的 Vue,也还没有覆盖完整的“双向绑定”。

  • 响应式更新(Data → View):读取状态时收集依赖,写入状态时通知渲染更新。
  • 视图写回(View → Data):由 DOM 事件或组件事件完成,v-model 只是把属性绑定与事件监听组合起来。

本文保留原文 Observer、Dep、Watcher、Compile 的 Vue 2 教学思路,并补充 Vue 3 在 2026 年仍适用的机制。Vue 2 已于 2023-12-31 EOL,新项目应优先使用 Vue 3。

原作者简化 MVVM 完整演示效果(01-01)

原作者项目汇总:github.com/fengshi123/blog

一、什么是 MVVM 数据双向绑定

MVVM 中常说的双向绑定包含两条方向不同的链路:

原文配图 01-02

  1. 输入框内容变化时,事件处理器把值写回状态,即 View → Data。
  2. 状态变化后,响应式系统触发组件重新渲染,即 Data → View。

以文本输入框为例,下面两种写法表达了同一个核心意图:

<input v-model="name">

<!-- 概念上的展开形式;不同表单控件使用的属性和事件并不完全相同 -->
<input :value="name" @input="name = $event.target.value">

Vue 3 组件上的 v-model 默认对应 modelValue prop 和 update:modelValue 事件;Vue 3.4+ 还可使用 defineModel()。因此,getter/setter 或 Proxy 主要解决 Data → View,事件才补上 View → Data。

原文的教学实现可拆成四个角色:

  1. Observer:拦截数据的读写;
  2. Dep:管理依赖;
  3. Watcher:读取数据以建立依赖,并在变化后执行更新;
  4. Compile:解析模板、创建绑定,并为 v-model 等指令注册事件。

原文配图 01-03

原始演示源码:mvvm_example。需要注意,该教学项目不是完整 Vue 源码。

二、Vue 2 的监听器 Observer

2.1 Object.defineProperty()

Object.defineProperty(obj, prop, descriptor) 可以定义或修改对象属性,并返回传入的对象。obj 是目标对象,prop 是属性名,descriptor 是属性描述符。

描述符分为两类,不能把 value / writableget / set 混用:

  • 两类共有:configurable 决定描述符能否再次修改以及属性能否删除,enumerable 决定属性能否出现在枚举中;二者默认都是 false
  • 数据描述符:value 是属性值,默认 undefinedwritable 决定能否用赋值改变值,默认 false
  • 存取描述符:get 在读取时执行,默认 undefinedset 在赋值时接收新值,默认 undefined。访问器中的 this 取决于实际接收者,并不总是最初定义属性的对象。

教学代码若遗漏描述符选项,可能意外改变原字段的枚举、配置行为。一个最小示例:

let value = 'tom'
const person = {}

Object.defineProperty(person, 'name', {
  enumerable: true,
  configurable: true,
  get() {
    console.log('name 属性被读取了')
    return value
  },
  set(newValue) {
    console.log('name 属性被修改了')
    value = newValue
  },
})

原文配图 01-04

2.2 浅层教学实现

下面的代码仅演示“遍历已有属性并拦截读写”,不是 Vue 2 的等价实现:

function observable(obj) {
  if (!obj || typeof obj !== 'object') return obj

  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key])
  })
  return obj
}

function defineReactive(obj, key, initialValue) {
  let value = initialValue

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`${key} 属性被读取了`)
      return value
    },
    set(newValue) {
      if (Object.is(newValue, value)) return
      value = newValue
      console.log(`${key} 属性被修改了`)
    },
  })
}

const person = observable({ name: 'tom', age: 15 })

真实 Vue 2 还会递归观测初始化时存在的对象、增强数组变异方法、处理原 accessor,并在赋入新对象时继续观测。它也有明确限制:任意已经观测的普通对象,直接新增或删除属性都不会被现有属性的 getter/setter 捕获。边界要分开看:

  • 对嵌套响应式对象,可用 Vue.set(object, key, value) / vm.$set 新增响应式属性,用 Vue.delete(object, key) 删除并通知依赖;
  • 对 Vue 实例本身或根 $data,运行时不能用 Vue.set / Vue.delete 动态增删根级响应式属性,应在初始化时声明;若需要动态字段,预先放进一个嵌套对象;
  • 数组索引赋值和直接修改 length 也有检测限制,使用 Vue.set(items, index, value)splice()

这些都是 Vue 2 历史限制;Vue 3 的 Proxy 机制不同。

三、Dep:依赖管理

可把 Vue 2 的依赖系统类比成观察者/发布订阅模式:属性在 getter 中登记当前 watcher,在 setter 中通知依赖它的 watcher。严格说,Vue 2 的 Dep 更接近直接维护观察者依赖,并不等于带事件中心的通用发布订阅系统;这个类比仍有助于理解一对多通知。

原作者用售楼处说明这一点:若每位购房者每天打电话询问尾盘,双方高度耦合;改为购房者留下号码(订阅),楼盘推出时售楼处遍历名单发消息(通知),发布者无需逐个硬编码调用购买者。它的教学优点是:

  • 发布者只负责在状态变化时通知约定的订阅者,减少对象之间的硬编码调用;
  • 新增订阅者通常不要求修改发布者,双方只依赖约定的通知机制;
  • 在异步编程里,调用者可只关注成功、失败等事件,而不必持续查询对象内部状态。

类比不能替代源码事实:Vue 的 getter 依赖收集会记录“本次求值实际读取了什么”,并在后续求值中清理失效依赖。

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

  depend() {
    if (Dep.target) this.subs.add(Dep.target)
  }

  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}

Dep.target = null

真实源码不是简单地把一个 watcher 永久放在全局变量中。Dep.target 表示“当前正在求值的 watcher”,嵌套求值时通过 pushTarget()/popTarget() 栈恢复;Watcher 还会去重并清理已失效依赖。

把 Dep 放进简化版属性拦截:

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

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend()
      return value
    },
    set(newValue) {
      if (Object.is(newValue, value)) return
      value = newValue
      dep.notify()
    },
  })
}

四、Watcher:收集依赖并更新

Watcher 初始化时读取目标属性,从而触发 getter 和 dep.depend()

const targetStack = []

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

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

class Watcher {
  constructor(data, key, callback) {
    this.data = data
    this.key = key
    this.callback = callback
    this.value = this.get()
  }

  get() {
    pushTarget(this)
    try {
      return this.data[this.key]
    } finally {
      popTarget()
    }
  }

  update() {
    const value = this.data[this.key]
    if (!Object.is(value, this.value)) {
      const oldValue = this.value
      this.value = value
      this.callback(value, oldValue)
    }
  }
}

这仍是教学简化版:只支持顶层单字段,没有依赖清理、computed、deep/lazy/user watcher,也没有调度队列。真实 Vue 2 的普通更新会进入 queueWatcher 批处理,而不是在 setter 中同步完成 DOM 更新。

五、Compile 与真正的 v-model

Compile 会初始化文本节点并创建 Watcher:

function bindText(node, data, key) {
  node.textContent = String(data[key] ?? '')
  new Watcher(data, key, (value) => {
    node.textContent = String(value ?? '')
  })
}

textContent 与 Vue 插值的纯文本语义一致;不要把不可信数据直接交给 innerHTML。如果要实现 v-model,还必须增加 View → Data 的事件回路:

function bindTextInput(input, data, key) {
  input.value = data[key] ?? ''
  input.addEventListener('input', (event) => {
    data[key] = event.target.value
  })

  new Watcher(data, key, (value) => {
    input.value = value ?? ''
  })
}

因此,仅有 Observer、Dep、Watcher 和插值替换只能称为“响应式渲染”;加入事件监听后才构成这个简化文本输入场景的双向绑定。

六、Vue 2 源码主链路

以 Vue 2.6.14 为例:

  1. initState() 调用 initData()
  2. initData()vm._data.xxx 代理为 vm.xxx,随后调用 observe(data)
  3. Observer 遍历对象 key 并调用 defineReactive(),数组则增强变异方法并观测数组元素;
  4. 渲染 watcher 执行 getter,本次 render 实际读取到的字段触发依赖收集;
  5. setter 调用 dep.notify(),watcher 通常进入异步更新队列;
  6. 下一轮 flush 执行 render 和 patch,更新 DOM。

真实链路是 getter → dep.depend() → Dep.target.addDep(dep)Watcher.addDep() 使用集合去重,并会在求值后清理不再使用的依赖。不能把简化版的 dep.addSub(Dep.target) 当作完整源码。

原文配图 01-05

七、原文 Vue 2 历史源码走读

版本提示: 本节固定在 Vue 2.6.14 的历史实现。为恢复原文教学链路,保留 Flow 类型源码的关键完整函数,并修正原抓取中的 xport、粘连注释等问题。Vue 2 已 EOL;这些内部 API 也不是业务代码可依赖的公共接口。

7.1 从 initStateinitData

组件初始化时,initState() 按 props、methods、data、computed、watch 的顺序初始化状态:

export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe((vm._data = {}), true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initData() 先取得 data 对象,检查与 methods/props 的重名,再把 vm._data.xxx 代理为 vm.xxx,最后观测整个 data:

function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm,
    )
  }

  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        'Use prop default value instead.',
        vm,
      )
    } else if (!isReserved(key)) {
      proxy(vm, '_data', key)
    }
  }
  observe(data, true /* asRootData */)
}

7.2 observeObserverdefineReactive

observe() 会复用已有 __ob__,只在满足条件时创建 Observer

export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) return

  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) ob.vmCount++
  return ob
}

Observer 为对象遍历已有 key;数组则增强变异方法并递归观测数组项:

export class Observer {
  value: any
  dep: Dep
  vmCount: number

  constructor(value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

每个属性各自持有一个 Dep。getter 保留原 accessor、收集当前 watcher,并为子对象/数组补充依赖;setter 比较值、调用原 setter 或保存新值、观测新对象后通知:

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean,
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) return

  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) val = obj[key]

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) return
      if (process.env.NODE_ENV !== 'production' && customSetter) customSetter()
      if (getter && !setter) return
      if (setter) setter.call(obj, newVal)
      else val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify()
    },
  })
}

7.3 DepWatcher

原文 Dep.target “全局唯一”的说法要结合 target 栈理解:任一时刻只有当前求值 watcher 是 target,但嵌套求值会通过 pushTarget / popTarget 保存和恢复,而不是求值后永远简单置空。

export default class Dep {
  static target: ?Watcher
  id: number
  subs: Array<Watcher>

  constructor() {
    this.id = uid++
    this.subs = []
  }
  addSub(sub: Watcher) { this.subs.push(sub) }
  removeSub(sub: Watcher) { remove(this.subs, sub) }
  depend() { if (Dep.target) Dep.target.addDep(this) }
  notify() {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) subs[i].update()
  }
}
Dep.target = null

Watcher 构造函数会把表达式转换成 getter,并在非 lazy 时立即 get();真实类还有 teardown、deep traverse、依赖清理等代码:

export default class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    if (isRenderWatcher) vm._watcher = this
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid
    this.active = true
    this.dirty = this.lazy
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn)
    if (!this.getter) this.getter = noop
    this.value = this.lazy ? undefined : this.get()
  }

  get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } finally {
      if (this.deep) traverse(value)
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  addDep(dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) dep.addSub(this)
    }
  }

  update() {
    if (this.lazy) this.dirty = true
    else if (this.sync) this.run()
    else queueWatcher(this)
  }
}

因此完整路径是:render watcher 求值 → 实际读取字段 getter → dep.depend()watcher.addDep(dep);赋值触发 setter → dep.notify() → watcher 进入队列 → 下一轮 flush 重新 render/patch。

上文 01-05 原理图概括了这条 Vue 2 数据响应主链路。

八、原文 Compile 到完整演示(Vue 2 风格教学实现)

下例恢复原文四角色闭环,但仍是历史教学代码,不是 Vue 2 源码。它只支持简单 {{ key }}v-modelv-on:click,不处理嵌套路径、组件、虚拟 DOM、指令修饰符和销毁清理。

function Compile(el, vm) {
  this.vm = vm
  this.el = document.querySelector(el)
  this.compile(this.el)
}

Compile.prototype.compile = function (node) {
  Array.from(node.childNodes).forEach((child) => {
    if (child.nodeType === 3) {
      const match = child.textContent.match(/\{\{\s*([\w$]+)\s*\}\}/)
      if (match) this.compileText(child, match[1])
    } else if (child.nodeType === 1) {
      Array.from(child.attributes).forEach((attr) => {
        if (attr.name === 'v-model') {
          const key = attr.value
          child.value = this.vm[key]
          child.addEventListener('input', (event) => {
            this.vm[key] = event.target.value
          })
          new Watcher(this.vm, key, (value) => { child.value = value })
        }
        if (attr.name === 'v-on:click') {
          child.addEventListener('click', this.vm.$methods[attr.value].bind(this.vm))
        }
      })
      this.compile(child)
    }
  })
}

Compile.prototype.compileText = function (node, key) {
  node.textContent = this.vm[key]
  new Watcher(this.vm, key, (value) => { node.textContent = value })
}

完整页面把 Observer、Dep、Watcher、Compile 串联起来:

<!doctype html>
<html lang="zh-CN">
  <body>
    <div id="mvvm-app">
      <input v-model="title">
      <h2>{{ title }}</h2>
      <button v-on:click="clickBtn">数据初始化</button>
    </div>

    <!-- 原作者项目构建出的 bundle 包含 Observer、Dep、Watcher、Compile 与 MVVM -->
    <script src="../dist/bundle.js"></script>
    <script>
      const vm = new MVVM({
        el: '#mvvm-app',
        data: { title: 'hello world' },
        methods: {
          clickBtn() { this.title = 'hello world' },
        },
      })
    </script>
  </body>
</html>

输入事件完成 View → Data,setter/Watcher 完成 Data → View,按钮则演示方法再次写状态。文首的原文效果图 01-01 展示的正是这一简化双向绑定演示结果。

九、Vue 3:Proxy、ref 与 effect

Vue 3 不再以 Vue 2 的遍历 accessor 作为主要对象响应式机制:

维度Vue 2Vue 3
对象拦截遍历已有 key,使用 Object.definePropertyreactive() 返回 Proxy
基本值容器非核心对象模型ref() 通过 .value getter/setter 跟踪和触发
依赖主体Dep / Watchertrack / trigger / ReactiveEffect
新增、删除有检测限制,常需 Vue.set/Vue.delete通过 Proxy 的 set/delete 等 trap 处理
数组、集合数组方法增强;索引与 length 有限制支持数组,并为 Map/Set 等集合提供 handler

官方概念模型可以简化为:

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    },
  })
}

function ref(value) {
  return {
    get value() {
      track(this, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(this, 'value')
    },
  }
}

这也是概念伪代码。真实实现会处理缓存、只读、浅层、数组和集合、相同值判断、effect 调度等大量边界。reactive(raw) !== raw,应持续使用返回的 Proxy;普通 ref 包裹对象时,内部对象会转为深层 reactive。

十、总结

  • 响应式系统负责 Data → View;v-model 用事件补齐 View → Data。
  • Object.defineProperty 是 Vue 2 的核心实现,Vue 3 的 reactive 使用 Proxy,ref.value 使用 getter/setter。
  • Observer、Dep、Watcher、Compile 适合解释 Vue 2 的主链路,但教学代码不等于 Vue 源码。
  • Vue 2 已 EOL;维护旧项目时要牢记新增属性、数组操作和异步更新队列等限制,新项目应以 Vue 3 官方文档为准。

官方参考

作者:随风而逝_风逝 原文链接:https://juejin.cn/post/6844903903822086151 来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS