乐趣区

关于面试:深入Vue双向绑定原理

前言


本文次要比拟 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 = 0

class 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 优劣如何?
退出移动版