Vue—双向绑定原理

2022/03/01 Vue 共 6857 字,约 20 分钟

什么是 MVVM 数据双向绑定

MVC | MVP | MVVM

MVC(Model-View-Controller)

mvc

图中简单描述了 MVC 中 Model(数据)、View(视图)、Controller(控制器)三者的关系:

  • Model:数据模型,用来存储数据
  • View:视图界面,用来展示 UI 界面和响应用户交互
  • Controller:控制器,监听模型数据的改变和控制器视图行为,处理用户交互

注意:各部分之间的通信都是单向的。Controller 层触发 View 层时,并不会更新 View 层中的数据,View 层中的数据是通过监听 Model 层数据变化而自动更新的,与 Controller 层无关

MVC 框架主要有两个缺点:

  • 大部分逻辑都集中在 Controller 层,给 Controller 层带来很大的压力,而已经有独立处理事件能力的 View 层却没有用到
  • Controller 层与 View 层之间是一一对应的,断绝了 View 层复用的可能

MVP(Model-View-Presenter)

mvp

  • Model:数据模型,数据库接口或远程服务器 api
  • View:显示数据并且与用户交互
  • Presenter:从 Model 中获取数据并提供给 View,还负责处理后台任务

MVP 模式将 Controller 改为 Presenter,并且各部分之间的通信变成双向的。

在 MVC 中,View 层可以通过访问 Model 层来更新,但在 MVP 框架中,View 层必须通过 Presenter 层提供的接口,然后 Presenter 再去访问 Model

MVP 框架的缺点:

  • View 层和 Model 层都需要经过 Presenter 层,导致 Presenter 层比较复杂,维护起来容易出现问题
  • 因为没有绑定数据,所有数据都需要 Presenter 层“手动同步”

MVVM(Model-View-ViewModel)

mvvm

  • Model:数据层的域模型,它主要做域模型的同步。
  • View:视图模板,View 层不负责处理状态,做的是数据绑定的声明、指令的声明、事件绑定的声明
  • ViewModel:ViewModel 层把 View 层需要的数据暴露,并对 View 层的几种声明负责,也就是处理 View 层的具体业务逻辑。

MVVM 的双向数据绑定

可以这样理解:双向数据绑定是一个模板引擎,它会根据数据的变化实时渲染。

如图所示,View 层和 Model 层之间的修改都会同步到对方:

mvvm-data

MVVM 模型中数据绑定方法一般有三种:

  • 数据劫持
  • 发布-订阅模式
  • 脏值检查

Vue2.x 中的双向数据绑定

首先了解几个概念:

  • Observer:数据监听器,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者
  • Compile:指定解析器,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化
  • Watcher:订阅者,可以收到属性的变化通知并执行相应的方法,从而更新视图
  • Dep:订阅器,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理

Observer

Observer 的实质就是使用 defineProperty 重写对象属性的 getter/setter 方法。但由于 defineProperty 无法监测到对象和数组内部的变化,所以遇到子属性为对象时,会递归观察该属性直至子属性为简单数据类型。

在 vue2.x 版本中对数组的处理方法是重写 push、pop、shift、unshift、splice、sort、reverse 方法。

源码目录 src/core/observer,首先看下入口函数 observe

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 检测数据是否为 'object',如果不是则退出
  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
}

observe() 方法尝试为 value 创建观察者实例,观察成功则返回新的观察者或已有的观察者,对象被观察后会有 __ob__ 属性,用于存储观察者实例。

再来看看 Observer 类:

// can we use __proto__?
// const hasProto = '__proto__' in {} // '../util/env.js'

export class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 给 value 对象通过 defineProperty 追加 __ob__ 属性
    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)
    }
  }
}

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

Observer 类除了属性的定义,就是对数组的特殊处理了。处理的方法就是通过原型链去修改数组的方法(push、pop、shift、unshift、splice、sort、reverse),并且对数组的每个元素进行 observe(),因为数组元素也可能是对象,需要递归劫持,直到为基本类型。

至此,defineProperty 对数组内部变化的监听问题就解决了。(关于数组方法的重写,可以看目录 src/core/observer/array.js

接着继续看 walk() 函数:

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

walk() 函数遍历对象的每个属性,并侵占它们的 getter/setter,接下来就是数据劫持的重点函数 defineReactive()

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
  }
  // 获取原来的get、set
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 递归:继续监听该属性值(只有val为对象时才有childOb)
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: ...,
    set: ...
  })
}

defineProperty() 先定义了依赖中心 dep,再获取对象的原生 get/set 方法,然后递归监听该属性,因为当前属性可能是对象,最后通过 defineProperty 劫持 getter/setter 函数

看一下 get/set 方法:

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 (getter && !setter) return
  // 设置新值
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  // 劫持新值
  childOb = !shallow && observe(newVal)
  // 发送变更通知
  dep.notify()
}

到这,数据劫持就完成了。总的来看,observe()对数组对象进行了递归遍历,将每个属性的 setter/getter 进行了改造,在赋值操作时会触发订阅中心的通知事件。

Dep

Dep(订阅中心),它要做的事很简单,维护一个容器存储订阅者,源码目录 src/core/observer/dep.js

let uid = 0
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

Watcher 作为 Observer 和 Compile 之间通信的桥梁,主要做的事是:

  • 在自身实例化时往订阅中心添加自己
  • 自身必须有一个 update 方法
  • 属性变动 dep.notice() 通知时,能调用自身的 update 方法,并触发 Compile 中绑定的回调

源码目录 src/core/observer/watcher.js

let uid = 0
export default class Watcher {
  constructor (vm,expOrFn,cb,options,isRenderWatcher) {
    this.vm = vm

    vm._watchers.push(this)

    this.cb = cb
    this.id = ++uid // uid for batching

    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = 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 {
      // 指向渲染函数,会触发getter
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      // 关闭
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  addDep (dep: 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)
    }
  }
}

实例化 Watcher 的时候,调用 get() 方法,通过 pushTarget() 方法(Dep.target = target)标记订阅者是当前 watcher 实例,强行触发属性定义的 getter 方法,getter 方法执行的时候,就会在属性的 dep 添加当前 watcher 实例,从而在属性变化时,能接收到更新通知。

Watcher 的 addDep() 方法内为了防止重复添加订阅者到订阅中心,维护了一个 Set 用于存储订阅中心的 id。

Compile

待更新

Vue3.x 中的双向数据绑定

Proxy 取代 Object.defineProperty

理由:

  • 在 Vue2.x 中使用 Object.defineProperty 无法监测到数组下标的变化,导致 vue 对数组的一些方法进行了重写
  • Object.defineProperty 只能劫持对象的属性,因此需要对每个对象的每个属性进行遍历,Vue2.x 中通过递归遍历 data 对象对数据监控,对性能有很大的影响

Proxy 实现一个数据绑定和监听:

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      setBind(value)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`get '${property}' = ${target[property]}`)
})
p.a = 2 // bind 'value' to '2'
p.a // get 'a' = 2

reactive

源码目录 packages/reactivity/src/reactive.ts