前言


本文次要比拟Vue2.0Vue3.0双向绑定的原理,以及由二者不同的原理造成的一些差别,并对该差别产生的起因进行简略的剖析。

  • Vue2.0双向绑定的次要实现原理是Object.defineProperty()办法
  • Vue3.0双向绑定的次要实现原理是ES6新增的Proxy()对象

所以本文论述的双向绑定的原理的区别简略来说就是上述两种办法对于劫持对象属性的不同。然而为了具体说分明不同原理造成的差别,咱们必须从源码说起。
Vue双向绑定利用的设计模式是公布-订阅模式,因为设计模式能更好的阐明不同类之间的用意,对于咱们了解源码有很大的帮忙(Ps:对于笔者是的) ,所以咱们将从这个设计模式说起。
公布-订阅模式在很多文章中都被认为成观察者模式,包含在《Javascript设计模式与开发实际》一书中,作者示意

公布-订阅模式又叫观察者模式

通过笔者考查二者在是实现上还是有些区别,然而能确定公布-订阅模式观察者模式的一种变体。

Vue2.0双向绑定源码

观察者模式 VS 公布-订阅模式


观察者模式

它定义对象间的一种一对多的依赖关系,当一个对象的状态产生扭转时,所有依赖于它的对象都将失去告诉。

观察者模式中次要由两类对象组成,一种是发布者(主题),一种是观察者。

  • 发布者 -- 为观察者提供注册性能,在须要的时候给观察者发送音讯
  • 观察者 -- 实现上须要监听发布者的扭转

具体实现咱们来看一下代码(TypeScript)

class Observer {  update() {    //  do something    console.log("收到告诉")  }}class Subject {  observerLists: Observer[] = []  publish() {    this.observerLists.forEach((observer) => {      observer.update()    })  }  trigger(observer: Observer) {    this.observerLists.push(observer)  }}const observer = new Observer()const subject = new Subject()subject.trigger(observer)subject.publish()  //收到告诉

这就是最简略的观察者模式的实现形式,该模式次要含有两种类型的对象,而且这两种对象之间是“相互理解”的。

公布-订阅模式

事实上,公布-订阅模式观察者模式的用意是没有太大的区别的,都是为了监听一个对象的变动,并且在这个对象扭转的时候告诉另一个对象。然而当初两个对象之间是“相互理解”(耦合)的,那么为理解耦两种对象之间的关系,咱们能够来看一下公布-订阅模式有什么新的扭转呢?

咱们再来看一下具体代码的实现(TypeScript)

class Dep {  observerLists: Observer[] = []  publish() {    this.observerLists.forEach((observer) => {      observer.update()    })  }  trigger(observer: Observer) {    this.observerLists.push(observer)  }}class Observer {  update() {    //  do something    console.log("收到告诉")  }}class Subject {  deps: Dep[] = []  change() {    this.deps.forEach((dep) => dep.publish())  }  depend(dep: Dep) {    this.deps.push(dep)  }}const observer = new Observer()const subject = new Subject()const dep = new Dep()subject.depend(dep) // 发布者关联音讯核心dep.trigger(observer) // 观察者关联音讯核心subject.change() // 收到告诉

与观察者模式相比拟,该模式减少了音讯核心的对象来做音讯的调度工作。
而咱们一会要看的Vue源码,就是通过这种形式实现的。

Vue 双向绑定原理解析

Vue 2.0 VS 3.0 有哪些不同

  • 2.0 响应式数据都要提前data外面申明
  • 2.0 响应式数据对数组的成果不现实
  • 2.0 响应式数据须要对多级对象进行深度遍历影响性能

那造成2.0这些问题的起因是什么呢?

手写 Vue 双向绑定局部源码

咱们举个简略的双向绑定的例子:页面上存在input输入框以及一个p标签,咱们要实现一个在输入框输出的内容会主动显示在p标签当中的性能。其实就是手写一个最一般的一个双向绑定。

  1. Observer作为发布者,用来做检测data数据扭转的性能
  2. Dep作为音讯核心,用来治理ObserverSubscriber之间的消息传递
  3. Subscriber作为观察者,数据有扭转时被告诉并执行update办法

代码的具体实现(html + ts)

建议您本人手写一边,最好是再用调试模式看一下执行过程。

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>    <input type="text" id="input" /><br />    <span id="p"></span>    <script src="./vue-ts.js"></script>  </body></html>
/** * 为了更好的了解博客 * (https://juejin.cn/post/6844903601416978439#heading-10) * 中所讲的Vue2.0双向绑定原理,所以本人再从新实现一下这个办法(ts) */let uid = 0class Dep {  id = uid++  subs: Subscriber[] = []  static target: Subscriber | null = null  addSub(sub: Subscriber): void {    this.subs.push(sub)  }  notify(): void {    this.subs.forEach((sub) => {      sub.update()    })  }  depend() {    if (Dep.target) {      Dep.target.addDep(this)    }  }}class Subscriber {  depIds: { [propName: string]: Dep } = {}  vm: Vue  cb: any  expOrFn: any  val: any  constructor(vm: Vue, expOrFn: any, cb: any) {    this.vm = vm    this.expOrFn = expOrFn    this.cb = cb    this.val = this.get()  }  update() {    this.run()  }  run() {    const val = this.get()    if (this.val !== val) {      this.val = val      this.cb.call(this.vm, val)    }  }  get() {    Dep.target = this    console.log(this.vm)    const val = this.vm.data[this.expOrFn]    Dep.target = null    return val  }  addDep(dep: Dep) {    if (!this.depIds.hasOwnProperty(dep.id)) {      dep.addSub(this)      this.depIds[dep.id] = dep    }  }}class Observer {  value: any  constructor(value: any) {    this.value = value    this.walk()  }  walk() {    Object.keys(this.value).forEach((key) => this.defineReactive(this.value, key, this.value[key]))  }  defineReactive(obj, key, val) {    const dep = new Dep()    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      set(newValue) {        console.log(val)        if (val === newValue) return        val = newValue        observe(newValue)        dep.notify()      },      get() {        console.log(val)        if (Dep.target) {          dep.depend()        }        return val      },    })  }}const observe = (value) => {  if (!value || typeof value !== "object") return  return new Observer(value)}class Vue {  data: any  constructor(option: { data: any }) {    this.data = option.data    Object.keys(this.data).forEach((key) => this.proxy(key))    observe(this.data)  }  $watch(expOrFn: any, callback: any) {    new Subscriber(this, expOrFn, callback)  }  proxy(key: string) {    Object.defineProperty(this, key, {      get() {        return this.data[key]      },      set(newValue: any) {        this.data[key] = newValue      },    })  }}const demo: any = new Vue({  data: { text: "" },})const input = document.getElementById("input")const p = document.getElementById("p")input.addEventListener("input", (event: any) => {  demo.text = event.target.value})demo.$watch("text", (val: string) => (p.innerHTML = val))

了解源码之后咱们来剖析一下为什么存在下面咱们说的三个问题

  • 2.0 响应式数据都要提前data外面申明;响应式数据是通过拜访器属性(getter/setter)实现的,然而咱们申明的时候申明的是对象的数据属性,是通过调用办法defineReactive()设置的响应式数据。假如咱们的Vue实例化之后在代码中申明了一个属性,那么这个属性是没有调用过defineReactive()办法的。
  • 2.0 响应式数据对数组的成果不现实;响应式数据是通过拜访器属性(getter/setter)实现的,当数组扭转的时候无奈检测到。最常见扭转数组的办法:'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'(不包含扭转数组的指向)
  • 2.0 响应式数据须要对多级对象进行深度遍历影响性能;2.0中为了能全面的对数据进行监听,所以要把多级对象进行深度遍历,为每个对象(PS:包含深度)的属性设置拜访器属性

那么Proxy如何防止以上问题呢?

const data = {  title: "userInfo",  andy: {    name: "啦啦啦",  },}const newData = new Proxy(data, {  set() {    console.log("设置data的值")    return Reflect.set(...arguments)  },})newData.title = "infoMation" // 设置data的值

Proxy只须要给整个对象做劫持就能够,不须要为每个属性减少拜访器属性

const data = [123, 123]const newData = new Proxy(data, {  set() {    console.log("设置data的值")    return Reflect.set(...arguments)  },})newData.push(456) // 设置data的值console.log(newData)  // [123,123,234]

Proxy天生就能够劫持数组的扭转

const newData = new Proxy(data, {  set() {    console.log("设置data的值")    return Reflect.set(...arguments)  },})newData.title = "infoMation" // 设置data的值newData.andy.name = "吼吼吼" //  未打印newData.andy = { name: "吼吼吼" } // 设置data的值

Proxy只能劫持代理对象的直系属性,多级属性扭转也无奈劫持,所以也须要做深层遍历劫持;因为Proxy能够代理整个对象,所以相比直线深层遍历其实“不深”

文章参考起源

  1. Vue2.0双向绑定源码
  2. 公布-订阅模式和观察者模式真的不一样?
  3. 面试官: 实现双向绑定Proxy比defineproperty优劣如何?