实例化一个vue对象时, Observer类将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。

Observer

src/core/observer/index.js

  1. export class Observer {
  2. value: any;
  3. dep: Dep;
  4. vmCount: number; // number of vms that has this object as root $data
  5. constructor (value: any) {
  6. this.value = value
  7. this.dep = new Dep()
  8. this.vmCount = 0
  9. /* 将Observer实例绑定到data的__ob__属性上面去,之前说过observe的时候会先检测是否已经有__ob__对象存放Observer实例了,def方法定义可以参考/src/core/util/lang.js*/
  10. def(value, '__ob__', this)
  11. if (Array.isArray(value)) {
  12. /*如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。*/
  13. const augment = hasProto
  14. ? protoAugment /*直接覆盖原型的方法来修改目标对象*/
  15. : copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/
  16. augment(value, arrayMethods, arrayKeys)
  17. /*如果是数组则需要遍历数组的每一个成员进行observe*/
  18. this.observeArray(value)
  19. } else {
  20. /*如果是对象则直接walk进行绑定*/
  21. this.walk(value)
  22. },
  23. walk (obj: Object) {
  24. const keys = Object.keys(obj)
  25. /*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
  26. for (let i = 0; i < keys.length; i++) {
  27. defineReactive(obj, keys[i], obj[keys[i]])
  28. }
  29. }
  30. }

1. 首先将Observer实例绑定到data的ob属性上,防止重复绑定;

2. 若data为数组,先实现对应的变异方法(不做理解),再将数组的每个对象进行Observer,使之成为响应式数据;

3. 若data为对象,直接调用walk()方法,遍历对象的所有属性,进行getter/setter的绑定,核心方法为defineReactive();

  1. export function defineReactive (
  2. obj: Object,
  3. key: string,
  4. val: any,
  5. customSetter?: Function
  6. ) {
  7. /*在闭包中定义一个dep对象*/
  8. const dep = new Dep()
  9. const property = Object.getOwnPropertyDescriptor(obj, key)
  10. if (property && property.configurable === false) {
  11. return
  12. }
  13. /*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
  14. // cater for pre-defined getter/setters
  15. const getter = property && property.get
  16. const setter = property && property.set
  17. /*对象的子对象递归进行observe并返回子节点的Observer对象*/
  18. let childOb = observe(val)
  19. Object.defineProperty(obj, key, {
  20. enumerable: true,
  21. configurable: true,
  22. get: function reactiveGetter () {
  23. /*如果原本对象拥有getter方法则执行*/
  24. const value = getter ? getter.call(obj) : val
  25. if (Dep.target) {
  26. /*进行依赖收集*/
  27. dep.depend();
  28. if (childOb) {
  29. /*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
  30. childOb.dep.depend()
  31. }
  32. if (Array.isArray(value)) {
  33. /*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
  34. dependArray(value)
  35. }
  36. }
  37. return value
  38. },
  39. set: function reactiveSetter (newVal) {
  40. /*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
  41. const value = getter ? getter.call(obj) : val
  42. /* eslint-disable no-self-compare */
  43. if (newVal === value || (newVal !== newVal && value !== value)) {
  44. return
  45. }
  46. /* eslint-enable no-self-compare */
  47. if (process.env.NODE_ENV !== 'production' && customSetter) {
  48. customSetter()
  49. }
  50. if (setter) {
  51. /*如果原本对象拥有setter方法则执行setter*/
  52. setter.call(obj, newVal)
  53. } else {
  54. val = newVal
  55. }
  56. /*新的值需要重新进行observe,保证数据响应式*/
  57. childOb = observe(newVal)
  58. /*dep对象通知所有的观察者*/
  59. dep.notify();
  60. }
  61. })
  62. }

其中getter方法:

1. 先为每一个data声明一个Dep实例对象,用于执行dep.depend()方法收集相关依赖;

2. 根据dep.target判断是否收集依赖;

那么问题来了,我们为什么要收集相关依赖呢??

  1. new Vue({
  2. template:
  3. `<div>
  4. <span>text1:</span> {{text1}}
  5. <span>text2:</span> {{text2}}
  6. <div>`,
  7. data: {
  8. text1: 'text1',
  9. text2: 'text2',
  10. text3: 'text3'
  11. }
  12. });

上述代码中,data中的text3并没有被实际使用,为了提高代码效率,我们没有必要进行响应式处理,因此,依赖收集简单点理解就是收集只在实际页面中用到的data数据,然后打上标记,这里就是标记为Dep.target。

在setter中:

1. 获取新的值并进行observer,保证数据响应式;

2. 通过dep.notify()方法通知watcher更新数据;

在defineReactive()方法中,我们可以看到在getter时,dep会收集相关依赖,即收集依赖的watcher,然后在setter操作时候通过dep去通知watcher,此时watcher就执行变化,我们用一张图描述这三者之间的关系:

从图我们可以简单理解:Dep可以看做是书店,Watcher就是书店订阅者,而Observer就是书店的书,订阅者在书店订阅书籍,就可以添加订阅者信息,一旦有新书就会通过书店给订阅者发送消息。

Watcher

Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例回调cb进行视图的更新。

src/core/observer/watcher.js

  1. export default class Watcher {
  2. constructor (
  3. vm: Component,
  4. expOrFn: string | Function,
  5. cb: Function,
  6. options?: Object
  7. ) {
  8. this.vm = vm
  9. /*_watchers存放订阅者实例*/
  10. vm._watchers.push(this)
  11. // options
  12. if (options) {
  13. this.deep = !!options.deep
  14. this.user = !!options.user
  15. this.lazy = !!options.lazy
  16. this.sync = !!options.sync
  17. } else {
  18. this.deep = this.user = this.lazy = this.sync = false
  19. }
  20. this.cb = cb
  21. this.id = ++uid // uid for batching
  22. this.active = true
  23. this.dirty = this.lazy // for lazy watchers
  24. this.deps = []
  25. this.newDeps = []
  26. this.depIds = new Set()
  27. this.newDepIds = new Set()this.value = this.lazy
  28. ? undefined
  29. : this.get()
  30. }
  31. /*获得getter的值并且重新进行依赖收集*/
  32. get () {
  33. /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
  34. pushTarget(this)
  35. let value
  36. const vm = this.vm
  37. /*执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
  38. 在将Dep.target设置为自生观察者实例以后,执行getter操作。
  39. 譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
  40. 那么在执行getter的时候就会触发a跟c两个数据的getter函数,
  41. 在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
  42. 将该观察者对象放入闭包中的Dep的subs中去。*/
  43. if (this.user) {
  44. try {
  45. value = this.getter.call(vm, vm)
  46. } catch (e) {
  47. handleError(e, vm, `getter for watcher "${this.expression}"`)
  48. }
  49. } else {
  50. value = this.getter.call(vm, vm)
  51. }
  52. /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
  53. if (this.deep) {
  54. /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
  55. traverse(value)
  56. }
  57. /*将观察者实例从target栈中取出并设置给Dep.target*/
  58. popTarget()
  59. this.cleanupDeps()
  60. return value
  61. }
  62. /*添加一个依赖关系到Deps集合中*/
  63. addDep (dep: Dep) {
  64. const id = dep.id
  65. if (!this.newDepIds.has(id)) {
  66. this.newDepIds.add(id)
  67. this.newDeps.push(dep)
  68. if (!this.depIds.has(id)) {
  69. dep.addSub(this)
  70. }
  71. }
  72. }
  73. /*清理依赖收集*/
  74. cleanupDeps () {
  75. /*移除所有观察者对象*/
  76. ...
  77. }
  78. /*
  79. 调度者接口,当依赖发生改变的时候进行回调。
  80. */
  81. update () {
  82. /* istanbul ignore else */
  83. if (this.lazy) {
  84. this.dirty = true
  85. } else if (this.sync) {
  86. /*同步则执行run直接渲染视图*/
  87. this.run()
  88. } else {
  89. /*异步推送到观察者队列中,下一个tick时调用。*/
  90. queueWatcher(this)
  91. }
  92. }
  93. /*
  94. 调度者工作接口,将被调度者回调。
  95. */
  96. run () {
  97. if (this.active) {
  98. /* get操作在获取value本身也会执行getter从而调用update更新视图 */
  99. const value = this.get()
  100. if (
  101. value !== this.value ||
  102. /*
  103. 即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
  104. */
  105. isObject(value) ||
  106. this.deep
  107. ) {
  108. // set new value
  109. const oldValue = this.value
  110. /*设置新的值*/
  111. this.value = value
  112. /*触发回调*/
  113. if (this.user) {
  114. try {
  115. this.cb.call(this.vm, value, oldValue)
  116. } catch (e) {
  117. handleError(e, this.vm, `callback for watcher "${this.expression}"`)
  118. }
  119. } else {
  120. this.cb.call(this.vm, value, oldValue)
  121. }
  122. }
  123. }
  124. }
  125. /*获取观察者的值*/
  126. evaluate () {
  127. this.value = this.get()
  128. this.dirty = false
  129. }
  130. /*收集该watcher的所有deps依赖*/
  131. depend () {
  132. let i = this.deps.length
  133. while (i--) {
  134. this.deps[i].depend()
  135. }
  136. }
  137. /*将自身从所有依赖收集订阅列表删除*/
  138. teardown () {
  139. ...
  140. }
  141. }

Dep

被Observer的data在触发 getter 时,Dep 就会收集依赖的 Watcher ,其实 Dep 就像刚才说的是一个书店,可以接受多个订阅者的订阅,当有新书时即在data变动时,就会通过 Dep 给 Watcher 发通知进行更新。

src/core/observer/dep.js

  1. export default class Dep {
  2. static target: ?Watcher;
  3. id: number;
  4. subs: Array<Watcher>;
  5. constructor () {
  6. this.id = uid++
  7. this.subs = []
  8. }
  9. /*添加一个观察者对象*/
  10. addSub (sub: Watcher) {
  11. this.subs.push(sub)
  12. }
  13. /*移除一个观察者对象*/
  14. removeSub (sub: Watcher) {
  15. remove(this.subs, sub)
  16. }
  17. /*依赖收集,当存在Dep.target的时候添加观察者对象*/
  18. depend () {
  19. if (Dep.target) {
  20. Dep.target.addDep(this)
  21. }
  22. }
  23. /*通知所有订阅者*/
  24. notify () {
  25. // stabilize the subscriber list first
  26. const subs = this.subs.slice()
  27. for (let i = 0, l = subs.length; i < l; i++) {
  28. subs[i].update()
  29. }
  30. }
  31. }

总结

  1. 在 Vue 中模板编译过程中的指令或者数据绑定都会实例化一个 Watcher 实例,实例化过程中会触发 get()将自身指向 Dep.target;
  2. data在 Observer 时执行 getter 会触发 dep.depend() 进行依赖收集;依赖收集的结果:1、data在 Observer 时闭包的dep实例的subs添加观察它的 Watcher 实例;2. Watcher 的deps中添加观察对象 Observer 时的闭包dep;
  3. 当data中被 Observer 的某个对象值变化后,执行notify()方法触发subs中观察它的watcher执行 update() 方法,最后实际上是调用watcher的回调函数cb,进而更新视图。

vue核心之响应式原理(双向绑定/数据驱动)的更多相关文章

  1. Vue数据绑定和响应式原理

    Vue数据绑定和响应式原理 当实例化一个Vue构造函数,会执行 Vue 的 init 方法,在 init 方法中主要执行三部分内容,一是初始化环境变量,而是处理 Vue 组件数据,三是解析挂载组件.以 ...

  2. Vue 2.0 与 Vue 3.0 响应式原理比较

    Vue 2.0 的响应式是基于Object.defineProperty实现的 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 prop ...

  3. 手写实现vue的MVVM响应式原理

    文中应用到的数据名词: MVVM   ------------------        视图-----模型----视图模型                三者与 Vue 的对应:view 对应 te ...

  4. vue 数据劫持 响应式原理 Observer Dep Watcher

    1.vue响应式原理流程图概览 2.具体流程 (1)vue示例初始化(源码位于instance/index.js) import { initMixin } from './init' import ...

  5. vue学习之响应式原理的demo实现

    Vue.js 核心: 1.响应式的数据绑定系统 2.组件系统. 访问器属性 访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过 defineProperty() 方法单独定义. va ...

  6. 学习 vue 源码 -- 响应式原理

    概述 由于刚开始学习 vue 源码,而且水平有限,有理解或表述的不对的地方,还请不吝指教. vue 主要通过 Watcher.Dep 和 Observer 三个类来实现响应式视图.另外还有一个 sch ...

  7. Vue 数据响应式原理

    Vue 数据响应式原理 Vue.js 的核心包括一套“响应式系统”.“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码.例如,视图渲染中使用了数据,数据改变后,视图也会自动更新. 举个简单 ...

  8. Vue.js响应式原理

      写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:answershuto/learnV ...

  9. Vue的响应式原理---(v-model中的双向绑定原理)

    Vue响应式原理 不要认为数据发生改变,界面跟着更新是理所当然. 具体代码实现:https://gitee.com/ahaMOMO/Vue-Responsive-Principle.git 看下图: ...

随机推荐

  1. 比较好的MySQL索引原理

    MySQL索引原理及慢查询优化 - 美团技术团队 https://tech.meituan.com/2014/06/30/mysql-index.html

  2. 分布式强化学习基础概念(Distributional RL )

    分布式强化学习基础概念(Distributional RL) from: https://mtomassoli.github.io/2017/12/08/distributional_rl/ 1. Q ...

  3. 【ASP.NET】 MVC下拉框联动

    这个case主要是我在做项目的时候遇到一个需要根据input控件输入的内容,动态填充dropdown list中的内容, 实现二者联动的需求.在搜索了一些资源后,这篇博客解决了我的问题,所以记录并转载 ...

  4. 【译】第8节---EF Code First中配置类

    原文:http://www.entityframeworktutorial.net/code-first/configure-classes-in-code-first.aspx 前面的章节中我们知道 ...

  5. Lintcode521-Remove Duplicate Numbers in Array-Easy

    Description Given an array of integers, remove the duplicate numbers in it. You should: Do it in pla ...

  6. springmvc后台接前端的参数,数组,集合,复杂对象等

    springmvc后台接前端的参数,数组,集合,复杂对象等 参考地址:https://blog.csdn.net/feicongcong/article/details/54705933  常用的几种 ...

  7. header 格式

    headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,* ...

  8. .NetCore Session.Redis

    首先创建ASP.NET CORE Web项目,然后按如下顺序操作.1.添加nuget程序包: Microsoft.AspNetCore.Session; Microsoft.AspNetCore.Da ...

  9. javaee开发模式

    model1模式:技术组成:jsp+javaBeanmodel1的弊端:随着业务复杂性 导致jsp页面比较混乱model2模式:技术组成:jsp+servlet+javaBeanmodel2的优点:开 ...

  10. Python统计list中各个元素出现的次数

    来自:天蝎圣诞结 利用Python字典统计 利用Python的collection包下Counter类统计 利用Python的pandas包下的value_counts类统计 字典统计 a = [1, ...