小丸子看vue如何响应式

时间:2022-01-06 21:24:08

小丸子看vue如何响应式

几个月跟着大佬们投入在vue项目里,对vue算是从0开始啦。

相比较之前使用的regular和angular的响应式的脏检查机制,发现vue的响应式是命中式的。使用angular或regular的时候,发现更新一个值,框架会检查许多无关的值,从中挑出哪个数据脏了,可以更新视图等等。而以vue的computed为例,发现vue只会检查改变的数据的对应的方法,其他无关的数据相关的方法,并不会打扰。

本文也以vue的computed计算属性实现举例,揭秘vue的响应式实现。

目录

1. 举个栗子

在使用vue的时候,经常用到computed来动态获取属性的值。

举个栗子:

<div id="example"> <input v-model="message1"/> <input v-model="message2"/> <p>Computed reversed message: "{{ reversedMessage }}"</p> </div>
var vm = new Vue({
  el: '#example',
  data: {
    message1: 'Hello',
    message2: 'world'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      console.log('改啦改啦');
      return this.message1.split('').reverse().join('')
    }
  }
})

上述代码在页面显示了两个输入框,一行展示文字。

左侧输入框双向绑定this.message1变量,右侧输入框双向绑定this.message2变量, 下方的展示框双向绑定this.reversedMessage变量,并且由计算属性获得,定义为this.message1.split('').reverse().join('')左侧输入框字符串的翻转。

运行发现左边的输入框变化的时候,下面的展示文字立即得到响应,更新为当前左边输入框的文字的翻转,并且控制台打印出改啦改啦。而右侧的输入框无论怎么变化,下面的展示框和控制台都没有变化。
小丸子看vue如何响应式

我们发现,vue的计算属性可以自动地区分出来谁变化的时候,才去更新依赖的值。当这个属性发生变化的时候,函数仿佛可以嗅探到这个变化,并自动重新执行。

2. 原理解析

我们知道Vue的响应式原理是通过Object.defineProperty中的gettersetter方法,在getter方法中进行依赖收集,在setter方法中进行响应通知。

我们大胆猜测,作者在computed的设计中,在计算属性定义之时,对计算属性的数据b和被依赖的数据a 两者之间关联了响应式的依赖,实现在被依赖项asetter被触发的时候,依赖a的数据bcomputed方法被响应通知。
小丸子看vue如何响应式

下面通过源码来揭秘吧~

小丸子看vue如何响应式

2.1 Mixin

Vue中的特性,通过mixin注入

//  ... 
initMixin(Vue)   
stateMixin(Vue)   // 注入Watcher
eventMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
// ...

假设,官方的栗子:

var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 `question` 发生改变,这个函数就会运行
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.getAnswer()
    }
  }
 });

在Vue注入了Watcher之后,才能在vm.$optionswatch方法调用的时候,具有Watch功能。

在Vue注入了Init之后,Vue具有init功能。blabla

2.2 initData(vm)

现在Vue有了init功能。

官方的Vue生命周期图中,new Vue()之后,会进行observe data过程,让我们来看一下,这个阶段做了什么吧。

小丸子看vue如何响应式

手画initData过程图

小丸子看vue如何响应式

首先,vue把vm.$options.data转移到vm下。
然后,对data里的数据及其属性进行响应式转化。
源码如下:

Vue.prototype._initData = function () { 

    this.$options=options;
    // 把options下的data转移到vm下
    let data = this._data=this.$options.data;
    // 遍历所有的data,添加响应式
    Object.keys(data).forEach(key=>this._proxy(key));
    // 遍历所有的data及其属性,添加响应式
    observe(data,this);
  }

// vue的响应式原理,不说了
Vue.prototype._proxy = function (key) {
    var self = this;
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return self._data[key];
      },
      set: function proxySetter (val) {
        self._data[key] = val;
      }
    })
  }
}

// 遍历所有的data及其属性,添加响应式
export function observe (value, vm) { 
  if (!value || typeof value !== 'object') { return; } 

  var ob; 
  if ( hasOwn(value, '__ob__') && value.__ob__ instanceof Observer ) { 
     ob = value.__ob__ ;
  } else if ( 
  shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { 
    ob = new Observer(value);    // 保证每个属性都有一个__ob__属性,它是一个Objserver对象,也就是这个东西添加的响应式
  } 
  if (ob && vm) { 
     ob.addVm(vm);
  } 

  return ob;
}

看一下Observer类的实现

export default class  Observer{
  constructor(value) {
    this.value = value
    this.walk(value)
  }
  //递归。。让每个属性可以observe
  walk(value){
    Object.keys(value).forEach(key=>this.convert(key,value[key]))
  }
  convert(key, val){
    defineReactive(this.value, key, val)
  }
}

// 此函数看不懂Dep.target没关系,后面说,先知道是创建了响应式
export function defineReactive (obj, key, val) {
  var dep = new Dep()
  var childOb = observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: ()=>{
      if(Dep.target){
        dep.addSub(Dep.target)
      }
      return val
    },
    set:newVal=> {
      var value =  val
      if (newVal === value) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      dep.notify()
    }
  })
}


export function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }
  return new Observer(value)
}

说白了,给每个属性添加Observer实例作为属性的目的就是实现给每个属性添加gettersetter,以保证响应式。

2.3 Dep: 消息订阅器

之前有没有亲手实践过javascript的消息通知订阅设计模式? Dep的设计就是一个消息订阅器。

export default class Dep {
  constructor() {
    this.subs = []
  }
  // 事件订阅
  addSub(sub){
    this.subs.push(sub) } // 事件通知 notify(){
    this.subs.forEach(sub=>sub.update()) } }

一个简单的事件通知订阅模型。

在看之前的defineReactive函数:

// 此函数看不懂Dep.target没关系,还是后面说
export function defineReactive (obj, key, val) {
  var dep = new Dep()
  var childOb = observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: ()=>{
      if(Dep.target){
        dep.addSub(Dep.target)   // 订阅
      }
      return val
    },
    set:newVal=> {
      var value =  val
      if (newVal === value) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      dep.notify()   // 发布
    }
  })
}

手画了一张图:

小丸子看vue如何响应式

每一个data及其属性都有一个Observer,它赋予data响应式,它依赖全局的Dep消息订阅器,getter的时候去订阅消息,由Dep实例存储实例队列,setter的时候去触发消息,通知队列里的watcher实例,运行用户想要的事件。

现在实现了每个data及其属性的Observer属性成功通知和订阅了Dep实例,那么,Watcher加入Dep实例队列具体是怎么做的呢?

2.4 this.$watch

手画this.$watch过程图(包括上一节的initData过程)

小丸子看vue如何响应式

Wacher类的实现:

export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.cb = cb
    this.vm = vm
    this.expOrFn = expOrFn
    // 2. 关键的地方来啦,此处调用了data/property的get方法
    this.value = this.get()
  }
  update(){
    this.run()
  }
  run(){
    const  value = this.get()
    if(value !==this.value){
      this.value = value
      this.cb.call(this.vm)
    }
  }
  get(){
    // 1. 重点来了: 把全局变量Dep.target只想当前的watcher实例
    Dep.target = this
    const value = this.vm._data[this.expOrFn]
    Dep.target = null
    return value
  }
}

其实Watcher做了什么呢?

第一步:Dep.target = this,为了第二步,执行data的get方法时,告诉data,是watcher触发的get方法,不是别人哦。
第二步:this.value = this.get(),触发data的get方法。

data的get方法做了什么呢?之前贴过代码。

回顾一下:

get: ()=>{
      if(Dep.target){
        dep.addSub(Dep.target)
      }
      return val
    }

get方法判断Dep.target有值(即是watcher触发的),就进行依赖收集。把当前闯进来的watcher加入自身dep实例的依赖队列里。

到此实现了vue的响应式过程。把下图三者串联了起来。

小丸子看vue如何响应式

3. 小结

结束。

小丸子看vue如何响应式


参考:
https://segmentfault.com/a/1190000010408657
https://www.imooc.com/article/14466