小丸子看vue如何响应式

Posted 小章鱼哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小丸子看vue如何响应式相关的知识,希望对你有一定的参考价值。

小丸子看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的计算属性可以自动地区分出来谁变化的时候,才去更新依赖的值。当这个属性发生变化的时候,函数仿佛可以嗅探到这个变化,并自动重新执行。

2. 原理解析

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

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

下面通过源码来揭秘吧~

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过程,让我们来看一下,这个阶段做了什么吧。

手画initData过程图

首先,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()   // 发布
    
  )

手画了一张图:

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

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

2.4 this.$watch

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

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的响应式过程。把下图三者串联了起来。

3. 小结

结束。


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

以上是关于小丸子看vue如何响应式的主要内容,如果未能解决你的问题,请参考以下文章

vue源码之响应式数据

vue响应式原理

vue系列---响应式原理实现及Observer源码解析

学习 Vue 原理:响应式

vue2.0 是如何实现响应式的?

vue2.0源码分析之理解响应式架构