vue2响应式原理

0 阅读4分钟

考虑如下代码:

<div id="app" @click="changeMsg">
  {{ message }}
</div>
<script>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    changeMsg() {
      this.message = 'Hello World!'
    }
  }
})
</script>

message更新对应的DOM更新怎么做到的呢?

这就不得不提到vue2内部的响应式原理了


响应式对象


首先需要将message变成响应式对象,核心原理就是Object.defineProperty

Object.defineProperty(obj, prop, descriptor)

Object.defineProperty在对象上定义一个新属性,或修改对象的现有属性,并返回这个对象

参数:

  • obj:要定义属性的对象
  • prop:属性名称
  • descriptor:属性描述符,有很多描述符,我们只关心getset描述符,就是分别定义getter方法和setter方法,读这个属性时会触发getter方法,修改这个属性时会触发setter方法

一个对象拥有了gettersetter,就可以称这个对象为响应式对象

message是怎么变成了响应式对象的呢?

vue对如下属性做了初始化操作,让其变成响应式:

  • props
  • methods
  • data
  • computed
  • wathcer

具体做法就是递归遍历里面的属性,给所有属性都添加gettersetter,因此遍历到message时就将message变成了响应式


依赖收集


message变成了响应式对象后,我们需要写getter方法的具体实现了

getter方法内部主要实现的就是依赖收集,具体怎么做呢?

首先我们要为message对象定义一个Dep 实例,部分代码如下:

function defineReactive(obj, key) {
  const dep = new Dep() // 每个属性都有自己的 Dep
  
  Object.defineProperty(obj, key, {
    get: function () {},
    set: function () {}
  })
}

那么什么是Dep

Dep

DepDependency的缩写,译为依赖集

简单来说Dep实例就是收集所有的Watcher依赖

Dep部分实现:

class Dep {
  constructor() {
    this.subs = [] // 存储 Watcher 的数组
  }

  // 收集依赖 - 将当前 Watcher 添加到 subs 中
  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target)
    }
  }
  
  // 通知所有 Watcher 更新
  notify() {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// 静态属性,指向当前正在计算的 Watcher
Dep.target = null

那么什么是Watcher

Watcher

Watcher译为观察者

Watcher部分实现:

class Watcher {
  constructor(vm) {
    this.vm = vm
    // 设置 Dep.target 为当前 Watcher
    Dep.target = this
  }
  
  update() {
    // 数据变化时执行回调
    this.vm.render()
  }
}

当新建一个Vue实例的时候,会新建一个对应的Watcher实例

class Vue {
  constructor() {    
    // 创建 Watcher
    new Watcher(this)
  }

意思是把当前Vue实例变成一个观察者,至于观察谁,现在还没有定

然后执行Watcher里的构造函数,执行Dep.target = this,将上面的Dep.target静态属性指向当前Watcher实例,为什么这么做呢?

都知道Dep.target是全局唯一的,也就是Watcher实例全局只能有一个,当编译到当前Vue实例的时候,Dep.target指向当前Vue实例下的Watcher实例,当编译到下一个Vue实例的时候,Dep.target指向下一个Vue实例下的Watcher实例

比如,当如下代码执行时:

<div id="app" @click="changeMsg">
  {{ message }}
  <ComponentA :val="message" />
</div>
<script>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    changeMsg() {
      this.message = 'Hello World!'
    }
  }
})
</script>

解析到appVue实例时,Dep.target指向app下的Watcher实例,然后解析app下的子组件ComponentADep.target就指向了ComponentA下的Watcher实例

至于为什么Watcher实例全局只能有一个,下面会讲

实现getter

现在我们来实现getter方法,其实很简单,核心就是如下代码:

if (Dep.target) {
   dep.depend()
}

还拿上面的代码来举例子:

<div id="app" @click="changeMsg">
  {{ message }}
  <ComponentA :val="message" />
</div>
<script>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    changeMsg() {
      this.message = 'Hello World!'
    }
  }
})
</script>
  1. 解析到appVue实例,Dep.target指向app下的Watcher实例
  2. 解析到{{ message }}时,触发messagegetter方法,将app下的Watcher实例收集到message下的dep实例中
  3. 解析到ComponentAVue实例,Dep.target指向ComponentA下的Watcher实例
  4. 解析到ComponentA里的message,再次触发messagegetter方法,将ComponentA下的Watcher实例收集到message下的dep实例中
  5. 此时message下的dep实例中一共有两个Watcher实例

至此完成依赖收集

现在可以解释为什么Watcher实例全局只能有一个,因为解析到哪个Vue实例全局Watcher就变成当前Vue实例的WatcherVue从父到子全部遍历一遍以后,依赖收集就完成了,Vue遍历是线性的,所以Watcher实例全局只能有一个

总的来说就是定义一个Dep依赖集,将所有Watcher观察者添加到Dep里面,至于Watcher观察的是谁,很显然就是Dep对应的响应式对象(上面的message


派发更新


实现完getter方法以后,我们来实现setter方法

实际上就是将Dep里所有收集到的Watcher,都触发它们的update方法过程

dep.notify()

message更新时,对应Dep里的所有Watcher执行update方法

update方法内部核心就是执行Vue实例里的render方法

其实Watcher和Dep就是一个非常经典的观察者设计模式的实现


总结


至此一个简单的响应式原理就完成了,总的来说就是Object.defineProperty结合Watcher和Dep的观察者模式的实现