Vue2.x双向数据绑定原理-Object篇

前言

相信每位前端人都被问过Vue双向数据绑定的原理是什么吧?应该也很快能答出来是经过Object.defineProperty让数据的每一个属性变成getter/setter实现的,但这仅仅只回答了一半,由于ObjectArray的实现方式是不同的,这也是为何标题是Object篇的缘由。(建议先看总结,再一步步看实现过程)javascript

基础知识

首先了解一下下面的概念:前端

声明式编程和命令式编程

这个概念就通俗点说了,想详细了解的可自行查阅资料java

  • 命令式:命令计算机如何去作事,严格按照咱们的命令去实现,无论咱们想要的结果是什么。
  • 声明式:咱们只须要告诉计算机想要什么,让它本身去想办法按它的思路去作。 这里用一个简单的例子去对比二者的区别,
// 给定一个数组 arr = [1, 2, 3], 想要一个新的数组每一项都加一
  const arr = [1, 2, 3];
  // 命令式 告诉浏览器循环数组,每个元素+1,而后push进新数组
  let newArr1 = [];
  for (let i = 0; i < arr.length; i++) {
    newArr1.push(arr[i]+1);
  }
  console.log(newArr1) // 拿到新数组

  // 声明式 告诉浏览器新数组的每一项是旧数组对应的每一项加一
  let result = arr.map(item => {
    return item + 1;
  })
  console.log(result) // 新的数组
复制代码

为何要说这个呢?由于Vue.js是声明式的,按API文档的要求来写Vue就知道要作什么。 (回头看好像偏题了,无论了就当巩固知识吧😂)编程

Object.defineProperty

在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象, Vue.js是利用这个方法修改data对象的属性。数组

let name = 'test';
  let obj = {};
  Object.defineProperty(obj, 'name', {
    configurable: true, // 可修改,可删除
    enumerable: true, //可枚举
    get: function() { // 读值触发
      console.log('读取数据');
      return name;
    },
    set: function(newVal) { // 赋值触发
      if(name === newVal){
        return;
      }
      console.log('从新赋值');
      name = newVal;
    }
  })
  console.log(obj.name);
  obj.name = '赋值';
  //打印出
  // 读取数据
  // test
  // 从新赋值
  // 赋值
复制代码

到这里已经算是Vue的Object双向数据绑定原理了。浏览器

实现完整的Object对象的双向数据绑定,Vue作了那些操做呢?函数

数据监控:“用”和“变”

经过上面的概念介绍就知道Object.defineProperty是作数据监控的,获取值的时候get被触发进行相应操做,设置数据时,set被触发这时就能知道数据是否被改变。那咱们是否是就很清楚知道能够在数据被调用触发get函数的时候,去收集那些地方使用了对应的数据了呢?而后在设置的时候,触发set函数去通知get收集好的依赖进行相应的操做呢?好了,下面就针对目前这个理解,对Object.defineProperty进行封装工具

defineReactive

function defineReactive(data, key, val) {
  //let dep = [];
  let dep = new Dep() // 修改
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      // 收集依赖
      // dep.push(window.target) // window.target后面会定义,很6的操做,期待一下
      dep.depend() // 修改
      return val
    },
    set: function(newVal){
      if(val === newVal){
        return
      }
      // 触发依赖
      // for(let i=0; i<dep.length; i++){
      // dep[i](newVal, val);
      // }
      dep.notify() // 修改
      val = newVal
    }
  })
}
复制代码

这里就实现了在get的时候,收集依赖保存到dep这个数组中,当触发set的时候,就把dep中的每一个依赖触发。在源码里是把dep封装成一个类,来管理依赖的,下面就实现一下Dep这个类吧。ui

Dep类

export default class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  removeSub(sub) {
    remove(this.subs, sub)
  }
  depend() {
    if(window.target){
      this.addSub(window.target) // window.target是什么?
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // window.target的update方法
    }
  }
}

function remove (arr, item) {
  if(arr.length){
    const index = arr.indexOf(item)
    if(index > -1){
      return arr.splice(index, 1)
    }
  }
}
复制代码

这样咱们封装的Dep类就能够收集依赖、删除依赖、通知依赖,那咱们就要把这个Dep类用上,对上面的defineReactive进行修改一下。Dep收集到的依赖看代码都知道是window.target,当数据发生变化的时候,调用window.targetupdate方法进行响应更新。this

Watcher类

源码里有个Watcher类,它的实例就是咱们收集的window.target,下面先来看看Vue中的一个用法

vm.$watch('user.name', function(newVal, oldVal){
  console.log('个人新名叫' + newVal); // 就是update函数
})

复制代码

当Vue实例中的data.user.name被修改时,会触发function的执行,也就是说须要把这个函数添加到data.user.name的依赖中,怎么收集呢?是否是调一下data.user.nameget方法就能够了。那么Watcher要作的就是把本身的实例添加到对应属性的Dep中,同时也有通知去更新的能力,下面写下Watcher

export default class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get() // 获取初始值
  }
  get() {
    window.target = this // 把当前实例暴露给Dep,Dep就知道依赖是谁了
    let value = this.getter.call(this.vm, this.vm) // 取一下值,触发vm实例上对应属性的get方法收集依赖
    window.target = undefined // 用完给别人用
    return value
  }
  update() {
    const oldValue = this.value // 旧值
    this.value = this.get() // 获取新值
    this.cb.call(this.vm, this.value, oldValue)
  }
}
复制代码

到这里再回顾一下上面写好的几个程序,你会发现之间都是很巧妙的结合了,特别是Watcher的实例,把本身给添加到Dep中了,反正我本身是以为这操做特6。这里也说明了Vue中de$watch是经过Watcher实现的。

固然parsePath还没说是什么,结合上面的例子和Watcher应该知道parsePath返回的是一个方法,而且被调用后返回一个值,也就是获取值的功能,下面来实现一下

const bailRE = /[^\w.$]/
export function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.spilt('.')
  return function(obj){
    for(let i = 0; i < segments.length; i++){
      if(!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
复制代码

Watcher中的this.getter.call(this.vm, this.vm)parsePath的返回的函数指向this.vm,并把this.vm当参数传过去取值。

Observer类

Vue中data的每个属性都会被监测到,实际上咱们使用defineReactive就能够监测,若是一个data有不少属性,那是否是要调用不少次呢,那么就有了Observer这个工具类把每一个属性变成getter/setter,来码上

export class Observer {
  constructor(value) {
    this.value = value
    if(!Array.isArray(value)){
      this.walk(value)
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
复制代码

那么new Observer(obj)就能把obj下的属性都变成getter/setter了,若是obj[key]依然是一个对象呢?是否是要继续new Observer(obj[key])呀,那么就是defineReactive拿到obj[key]时,须要进行判断是否是对象,是的话就进行递归,那么加上这一步操做

function defineReactive(data, key, val) {
  if(typeof val === 'object') {
    new Observer(val)
  }
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.depend()
      return val
    },
    set: function(newVal){
      if(val === newVal){
        return
      }
      dep.notify()
      val = newVal
    }
  })
}
复制代码

到这里Vue中Object是数据响应已经完成了,可是有缺陷你们都很清楚,就是给data新增属性或者删除属性时,没法监测,上面的实现过程都是依赖现有属性进行的,可是Vue提供$set$delete去实现这两个功能,相信弄懂上面的代码,这两个的实现就不难了。

总结

对Vue中Object的数据响应,我总结的一句话就是“定义getter/setter备用,“用”:收集依赖,“变”:触发依赖

  • “备用”: 经过ObserverdefineReactive把属性变成getter/setter
  • “用”: 经过Watchergetter中把依赖收集到dep
  • “变”: 经过setter告诉dep数据变化了,dep通知Watcher去更新;
相关文章
相关标签/搜索