学习Vue的数据响应式原理

    这几天在B站大学上跟着尚硅谷的老师学习Vue2的源码解析,在今天终于是把数据的响应式看完了,就做一个总结吧!

    老师的视频连接:https://www.bilibili.com/video/BV1G54y1s7xV?p=6&vd_source=a81826692f4afea80764f4048dc1ae0a

    我的代码地址:Vue2的数据响应式: 在B站大学跟着视频一点点手敲的

     这个文章还参考了某金上叫争霸爱好者的作者的文章

  • Vue的数据响应式是什么?

    当我们在使用Vue2时,对数据进行修改之后,视图就会相应的更新,这就是数据的响应式

  • 那么应该怎么做才可以将数据变为响应式的呢

        通过Object.defineProperty(),这个方法是干什么用的呢?在MDN中是这么定义的

        语法:

        obj:要定义属性的对象。

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

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

        尝试一下:

const obj = {}
    Object.defineProperty(obj, 'a', {
      value: 66
    })
    console.log(obj.a);// 打印出来66

       此方法中有两个属性,用过这两个属性的函数我们可以做到对数据进行劫持:

  •  什么是数据劫持?

        举个栗子:

const obj = {}
    let value = 1
    Object.defineProperty(obj, 'a', {
      get() {
        console.log('你要访问a属性')
        return value
      },
      set(newVal) {
        if (newVal === value) return
        console.log('你要更改a属性');
        value = newVal
      }
    })
    obj.a = 2
    console.log(obj.a); 

         当我们修改obj.a时,就会触发set,当我们打印obj.a时,就会触发get,这样我们就可以侦测到对属性a访问和修改的操作,也叫数据劫持

          由于上面的方法需要定义一个全局变量,所以可以将这个方法进行改进,封装成一个闭包:

const obj = {}
    function defineReactive(data, key, value) {
      // 进行改进,如果没有value,令value等于原值
      if (arguments.length <= 2) {
        value = data[key]
      }
      Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可被删除
        configurable: true,
        get() {
          return value
        },
        set(newValue) {
          if (newValue === value) return
          value = newValue
        }
      })
    }
    defineReactive(obj, 'a',1)
    console.log(obj);

        每当我们从data数据中的key读取数据的时候,get()函数就会触发,对data数据的内容进行修改的时候,set()就会触发,他们会对value进行操作,因此形成了一个闭包

  • Observer类

        功能:将每一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object

const obj = {
      a: 10,
      b: 20,
      c: 30
    }

       如果obj有多个属性的话,我们可以创建一个Observer类对obj进行遍历:

class Observer {
      constructor(value) {
        this.walk(value)
      }
      // 遍历
      walk(value) {
        for (let k in value) {
          defineReactive(value, k)
        }
      }
    }
    
const obj = {
      a: 10,
      b: 20,
      c: 30
    }

    new Observer(obj)

        现在我们的obj有多层属性的嵌套 :

const obj = {
      a: {
        m: {
          n: 10
        }
      },
      b: 20,
      c: 30
    }

        所以我们需要对Observer类进行一些改进:

// 入口函数observe
    // 现在的__ob__就是给遍历过的对象添加的标识(后面会用来存储东西)
    function observe(value) {
      // 如果value不是对象,就什么都不做
      if (typeof value != 'object') return
      var ob
      if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__
      } else {
        ob = new Observer(value)
      }
      return ob
    }
    // Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
    class Observer {
      constructor(value) {
        // 给实例(构造器中的this不是表示类本身,而是表示实例)添加__ob__属性,值是这次new的实例
        this.def(value, '__ob__', this)
        this.walk(value)
      }
      // 遍历
      walk(value) {
        for (let k in value) {
          defineReactive(value, k)
        }
      }
      // 定义obj对象key的属性
      def(obj, key, value) {

        Object.defineProperty(obj, key, {
          value,
          // 添加一些配置项
          writable: true,
          configurable: true
        })
      }
    }
    function defineReactive(data, key, value) {
      // 进行改进,如果没有value,令value等于原值
      if (arguments.length == 2) {
        value = data[key]
      }

      // 子元素要进行observe,至此形成递归
      let childOb = observe(value)

      Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可被删除
        configurable: true,
        get() {
          return value
        },
        set(newValue) {
          if (newValue === value) return
          value = newValue
          // 当我们向obj中添加新的属性时,这个属性也有可能是一个对象  
          childOb = observe(newValue)
        }
      })
    }
    const obj = {
      a: {
        m: {
          n: 10
        }
      },
      b: 20,
      c: 30
    }
    observe(obj)

        我们通过三个函数的循环调用形成了一个递归,这样我们就将obj中的每一个属性都转换成了响应式的数据,并且为他们打上了__ob__的标记,可能有的同学看到递归会有些懵,可以通过下面的的流程图来帮助理解:

 图片来自尚硅谷的老师!!!

  • 数组的响应式处理

        当我们通过数组的方法对属性进行操作时,上面的代码是不起作用的,在Vue中,尤雨溪对数组的七个方法进行了改写:

        在Vue中,尤雨溪通过原型拦截的方法对着其中数组进行了改写: 

  1. 将Observer类中的def方法抽离出来封装成utils.JS文件,避免Observer过于臃肿
// 遍历工具函数
export const def = function (obj, key, value) {
  // defineProperty即给对象定义属性
  Object.defineProperty(obj, key, {
    value,
    writable: true,
    configurable: true
  })
}

       2. 对Observer类进行修改

    // 引入def
    import { def } from './utils'
    // 引入数组方法
    import { arrayMethods } from './array'
    // Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
    class Observer {
      constructor(value) {
        // 给实例(构造器中的this不是表示类本身,而是表示实例)添加__ob__属性,值是这次new的实例
        def(value, '__ob__', this)
        // 检查他是数组还是对象
        if (Array.isArray(value)) {
          // 如果是数组:将这个数组的原型指向arrayMethods
          Object.setPrototypeOf(value, arrayMethods)
          // 遍历数组
          this.observeArray(value)
        } else {
          this.walk(value)
        }
      }
      // 数组的特殊遍历
      observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
          // 逐项进行递归
          observe(arr[i])
        }
      }
      // 遍历
      walk(value) {
        for (let k in value) {
          defineReactive(value, k)
        }
      }
    }

        3. 封装转换数组方法的JS文件

    // 引入def
    import { def } from './utils'
    // 得到Array.prototype
    const arrayPrototype = Array.prototype

    // 以Array.prototype为原型创建arrayMethods对象,将他导出
    export const arrayMethods = Object.create(arrayPrototype)

    // 要被改写的七个数组方法
    const methodsNeedChange = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

    // 遍历上述数组方法名
    methodsNeedChange.forEach(methodsName => {
      // 备份原来的方法(因为基本功能还是需要使用原来的数组方法,只是我们需要添加响应式的功能)
      const original = arrayPrototype[methodsName]
      // 将重写后的方法定义到arrayMethods对象上,函数就是重写后的方法
      def(
        arrayMethods,
        methodsName,
        function () {
          // 要有数组原来方法的功能
          const result = original.apply(this, arguments)
          let args = Array.from(arguments)

          // 在这个数组中,__ob__已经被添加过,可以将__ob__取出来
          const ob = this.__ob__

          // 由于push,unshift,splice可以往数组中添加项,所以也需要对这些项进行遍历(防止插入的项是对象)
          let inserted = []
          switch (methodsName) {
            case 'push':
            case 'unshift':
              inserted = arguments
              break
            case 'splice':
              // slice(2)从下标为二的项开始
              // 因为splice格式(下标,数量,插入新的项)
              inserted = args.slice(2)
              break
          }
          // 判断有没有要插入的新项,如果有就让新项也变为响应式的
          if (inserted.length > 0) {
            ob.observeArray(inserted)
          }
          return result
        }
      )
    })

        至此,数组也被变成了响应式的数据

        我们通过defineReactive方法将数据进行响应式后,虽然可以监听到数据的变化了,那我们怎么处理通知视图更新呢?这就需要用到依赖

  • 依赖

        需要用到数据的地方,称为依赖,在Vue中,依赖就是Watcher,他的只要作用就是观察Vue中的属性,当属性更新时做出相应操作,也就是在实例化Watcher时传入的回调函数 :

         在实例化Watcher类时我们传入三个参数(要监听的数据,表达式,回调函数),Watcher类我们放到下面定义,现在只需要知道回调函数是什么就好了

  • 收集依赖与派发更新

        在Vue中,依赖的收集与派发采用了发布-订阅模式 :

        发布订阅模式是对象间的一种一对多的关系,由三部分组成 :

                发布者(observe) : 增加了订阅者,并且在改变时通知了订阅者

                订阅者(watcher) : 每一个使用到数据的地方,所以每一个使用到数据的地方都有一个Watcher

                事件中心(Dep) : 存储Weacher类的地方

                当一个对象发生改变时,所有依赖于它的对象都将得到通知。

        对数据的操作都会在getter和setter中被拦截,所以在getter中收集依赖,在setter中派发更新

        我们先定义Dep类用于存储Watcher

var uid = 0
export default class Dep {
  constructor() {
    // 在Vue中,每一个组件都已一个自己的ID
    this.id = uid++
    // 用数组存储自己的订阅者.
    // 这个数组存放Watcher的实例
    this.subs = []
  }
  // 添加订阅
  addSub(sub) {
    this.subs.push(sub)
  }
  // 添加依赖
  depend() {
    // Dep.target就是Watcher
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  // 通知更新
  notify() {
    // 浅克隆,防止改变原始subs
    const subs = [...this.subs]
    // 遍历
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 用于同时Weather更新,Weather会放到后面定义
    }
  }
}

       为了方便理解,我们把Watcher类也先定义出来:

import Dep from './Dep'

var uid = 0

export default class Watcher {
  constructor(target, expression, callback) {
    // target: 你要监听的数据对象
    // expression:表达式,如a.m.n
    // callback:依赖变化时触发的回调
    this.id = uid++
    // this指向window
    this.target = target
    // 对表达式进行拆分
    this.getter = parsePath(expression)
    this.callback = callback
    //  get方法会读取数据的值,从而触发了数据的getter
    this.value = this.get()
  }
  // 当收到数据变化的消息时执行该方法
  update() {
    this.run()
  }
  // get方法的作用就是获取自己依赖的数据
  get() {
    // 进入依赖收集阶段,将全局的Dep.target设置为Watcher本身
    // this就是Watcher
    Dep.target = this
    const obj = this.target

    var value

    // 只要能找到,就一直找
    try {
      value = this.getter(obj)
    } finally {
      Dep.target = null
    }
    return value
  }
  // 当数据更新时会调用run
  run() {
    // 得到并且唤起
    this.getAndInvoke(this.callback)
  }
  getAndInvoke(cb) {
    const value = this.get()
    if (value !== this.value || typeof value == 'object') {
      const oldValue = this.value
      this.value = value
      cb.call(this.target, value, oldValue)
    }
  }
}
function parsePath(str) {
  // 将表达式按.进行拆分
  var segments = str.split('.')
  return obj => {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

        在Observer中引入Dep类并创建实例

import Dep from './Dep'
class Observer {
  constructor(value) {
    this.dep = new Dep()
    // 其他代码省略
 }
}

        为什么要在Observer中创建Dep的实例 :

        由于数据在转换的过程中,每一层属性都会经过Observe类,所以每一个响应式的对象都会有一个自己的依赖存储器,并且__ob__指向的是这个Observer类的实例所以__ob__上会有Dep属性,相当于将Dep这个存储桶放到了一开始为每个响应式对象创建的标识上

        由于要在Object.defineProperty()中收集和触发依赖,所以我们在闭包里面也创建一个Dep的实例并且改造getter和setter:

function defineReactive(data, key, value) {
  const dep = new Dep()
 // 其他代码省略
    
    Object.defineProperty(data, key, {
    // 可枚举
    enumerable: true,
    // 可以被配置,比如可以被delete
    configurable: true,
    get() {
      // 如果现在处于依赖的收集阶段
      if (Dep.target) {
        // 把当前的watcher添加到dep数组中
        // console.log(1213213)
        dep.depend()
        // 如果有子元素,还要让子元素添加依赖
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newValue) {
      if (value === newValue) {
        return
      }
      value = newValue
      childOb = observe(newValue)
      // 修改数据是触发notify
      dep.notify()
    }
  })
}

       在get()中Dep.target就是触发数据响应的Watcher

        当数组被更改,也需要通知Dep:

// 遍历上述数组方法名
methodsNeedChange.forEach(methodsName => {
  // 备份原来的方法(因为基本功能还是需要使用原来的数组方法,只是我们需要添加响应式的功能)
  const original = arrayPrototype[methodsName]
  // 将重写后的方法定义到arrayMethods对象上,函数就是重写后的方法
  def(arrayMethods, methodsName, function () {
    // 要有数组原来方法的功能
    const result = original.apply(this, arguments)
    let args = Array.from(arguments)

    // 在这个数组中,__ob__已经被添加过,可以将__ob__取出来
    const ob = this.__ob__

    // 由于push,unshift,splice可以往数组中添加项,所以也需要对这些项进行遍历(防止插入的项是对象)
    let inserted = []
    switch (methodsName) {
      case 'push':
      case 'unshift':
        inserted = arguments
        break
      case 'splice':
        // slice(2)从下标为二的项开始
        // 因为splice格式(下标,数量,插入新的项)
        inserted = args.slice(2)
        break
    }
    // 判断有没有要插入的新项,如果有就让新项也变为响应式的
    if (inserted.length > 0) {
      ob.observeArray(inserted)
    }
    ob.dep.notify() // 修改:通知Dep
    return result
  })
})
  • 总结       

        现在代码写完了,让我们总结一下 :

 

         我们在转化响应式的过程中,通过循环递归使每一个Observer的实例中都有一个Dep的实例,当模板引擎使用数据的时候,我们就会在getter中通过dep.depend()方法收集依赖,当修改数据的时候,就会触发setter,通过dep.notify()通知Watcher进行更新,最后通过Watcher触发视图的更新

  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值