vue的响应式系统和依赖收集

前言

在掘金上看染陌同学《剖析 Vue.js 内部运行机制》的掘金小册时,发现自己一个极大问题,基础知识掌握的不够牢靠,导致中间有时候出现一些错误,无法理解,所以在这里写下这个笔记,加深自己的印象。

Object.defineProperty

在记录vue的响应式系统前,一定要对Object.defineProperty的用法掌握,这是实现vue数据双向绑定的基础,但是vue的作者宣布将会在下个版本使用Proxy代替Object.defineProperty,这不重要,这里依然来说Object.defineProperty。这个对象的扩展方法是干什么的?简单的说就是用来劫持对象属性的,已达到对象在改变数据之前可以对对象进行一系列操作,这就是js中数据劫持的一个基本原理。

Object.defineProperty有三个参数,分别为obj, prop和descriptor

obj:要在其上定义属性的对象。

prop:要定义或修改的属性的名称。

descriptor:将被定义或修改的属性描述符。

而整个对象的操作都在descriptor里进行,他接受一个对象参数,对象参数支持六个属性,这六个属性在这里我们只需要使用四个,如下

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function () {},
  set: function () {}
})

enumerable: 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

configurable: 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。

get: 当读取属性时调用

set: 当属性值改变时调用

此处使用MDN的例子说明Object.defineProperty的使用

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

更多详细介绍请查看MDN

vue的响应式系统简析

vue的响应式系统在vue整个框架里有什么作用?或者更详细的说,vue的数据双向绑定是怎么实现的?这里就可以说Object.defineProperty是其关键所在,我先撸代码,然后再详细说

function cb(val) {
    console.log("视图更新了!!!")
}

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function () {
        return val
    },
    set: function (newVal) {
      if (val === newVal) return
      val = newVal
      cb(val)
    }
  })
}

function observer(obj) {
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

class Vue {
  constructor(options) {
    this.data = options.data
    observer(this.data)
  }
}

let o = new Vue({
  data: {
    text: "hello world!!!"
  }
})

o.data.text = "hello Tom" // 视图更新了!!!

从上面代码我们可以看出,当我们改变text的值时,就会触发cb函数,而整个过程中我们是通过Object.defineProperty的set实现的,当属性值改变时,就会触发set,并把新值当做参数,这里就完成了简单的响应系统,这里并没有对传入的参数做判断,并且并不支持数组,但是vue里是实现了对数组的支持的

依赖收集

为什么要依赖收集?依赖收集发挥着怎样的作用?

在使用vue时,我们经常会遇到data里有多个属性,然而有的属性并没有在template里展示,但是我们即将修改这个属性的值,那么就会造成视图更新,这是没有必要的,如下

new Vue({
  template: `
    <div>{{text1}}</div>
  `,
  data: {
    text1: '123',
    text2: '456'
  },
  mounted() {
    this.text2 = '789'
  }
})

这里我们改变了text2的值,但是我们并没有在template里展示这个值,只是vue的内部使用,所以并不需要通知视图,进行更新,所以这里我们就需要进行依赖收集,避免不必要的视图更新。

订阅发布模式/观察者模式

在说依赖收集之前,我先说在程序设计中经常使用的两个设计模式订阅发布模式和观察者模式,这是我们实现依赖收集的设计模式,了解到这些模式,会更容易理解如何进行的依赖收集。

先写个例子

class EventBus {
  constructor() {
    this._event = new Map()
  }
  addListener(type, fn) {
    const handler = this._event.get(type)
    if (!handler) {
      this._event.set(type, fn)
    }
  }
  emit(type, ...args) {
    const handler = this._event.get(type)
    if (handler && typeof handler === 'function') {
      if (args.length > 0) {
        handler.apply(this, args)
      } else {
        handler.call(this)
      }
    }
  }
}

var emitter = new EventBus()

emitter.addListener('put', function(name) {
    console.log("my name is " + name)
})

emitter.addListener('put', function(name) {
    console.log("your name is " + name)
})

emitter.emit('put', 'Lucy')    //my name is Lucy

这是一个模拟事件池的代码,先给emitter添加一个事件,用emit触发事件,但是在这里,我们给同一个事件绑定了多个函数,当emit时,希望可以通知绑定到这个事件的所有函数,然而这里只是通知了第一个,当我们希望这种一对多的依赖关系时,就可以用发布订阅模式去描述。可以用微信公众号来形象的说明这个模式,公众号就是发布者,而用户就是订阅者,当文章更新时,就会通知每一个订阅者用户,这样一个发布者维护多个订阅者,就是发布订阅模式。那么什么是观察者模式?其实在很多文章中,这两个模式很难有什么区别,而在百度时,他们也是会成对出现的,这里我就不详细讨论他们的区别了,暂且当做一个模式来看。

那么现在我来给这个EventBus进行升级

class EventBus {
  constructor() {
    this._event = new Map()
  }
  addListener(type, fn) {
    const handler = this._event.get(type)
    if (!handler) {
      this._event.set(type, fn)
    } else if(handler && typeof handler === 'function') {
      this._event.set(type, [handler, fn])
    } else {
      this._event.set(type, handler.push(fn))
    }
  }
  emit(type, ...args) {
    const handler = this._event.get(type)
    if (handler && Array.isArray(handler)) {
      handler.forEach(fn => {
        if (args.length > 0) {
          fn.apply(this, args)
        } else {
          fn.call(this)
        }
      })
    } else {
       if (args.length > 0) {
         handler.apply(this, args)
       } else {
         handler.call(this)
       }
    }
  }
}

var emitter = new EventBus()

emitter.addListener('put', function(name) {
    console.log("my name is " + name)
})

emitter.addListener('put', function(name) {
    console.log("your name is " + name)
})

emitter.emit('put', 'Lucy')   
// my name is Lucy
// your name is Lucy

在这里实际上就是监听了所有绑定在listener上的函数,也就是订阅者绑定在发布者上,当emit触发时,就通知所有的订阅者,发布更新了

依赖收集

现在我们对vue的响应式系统进行升级,先撸为敬

class Dep {
  constructor() {
    this.subs = [] 
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  // 通知所有的订阅者sub更新
  notify(val) {
    this.subs.forEach(sub => {
      sub.update(val)
    })
  }
}
// 管理订阅者的watcher
class Watcher {
  constructor() {
    Dep.target = this
  }
  update(val) {
    console.log("视图更新了!!!!")
  }
}
Dep.target = null

function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function () {
        dep.addSub(Dep.target)
        return val
    },
    set: function (newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify(val)
    }
  })
}

function observer(obj) {
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

class Vue {
  constructor(options) {
    this.data = options.data
    observer(this.data)
    new Watcher()
    /**
    * 这里的console.log是模拟使用this.data的属性
    * 以触发defineProperty的get,这样就会对当属性
    * 改变时,视图需要更新的属性进行了收集,而未在
    * template里使用的进行剔除
    */
    console.log(this.data.text)
  }
}

var vue = new Vue({
  data: {
    text: '123',
    text1: '456'
  }
})

vue.data.text = '456' // 视图更新了!!!!
vue.data.text1 = '789'

改造好的vue响应式系统,基本具有了数据变化,视图更新的流程,并能进行依赖收集。

有错误之处,望请指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容