当前位置: 首页 > article >正文

根据源码解析Vue2中对于对象的变化侦测

UI = render(state)

VUE的特点是数据驱动视图,在这里可以把数据理解为状态,而视图就是用户可以看到的页面,页面是动态变化的,而他的变化或是用户操作引起,或是后端数据变化引起,这些都可以说是数据的状态变了,导致页面随之变化。

把顶部公式拆为三部分:UI、render、state。state和UI都是用户定的,不变的是render,所以VUE就担任了这个角色,当VUE发现state变化后,就通过一系列操作,把他反应在了UI上。

那么VUE是如何发现state变化的?引出了VUE的变化侦测:

JS为我们提供了object.defineProperty()方法,通过此方法我们可以知道数据什么时候发生了变化。

1.Object数据变得可观测

数据每次读写能够被我们看到,我们能知道数据什么时候发生了变化,将其称为数据的可观测。通过使用上述对象方法,可以实现。如下

let person = {}
let nameval = 'lwh'
Object.defineProperty(person,name,{
    get() {
        console.log('name被读取了')
        return nameval
    }
    set(newval) {
        console.log('name被修改了')
        nameval = newval
}
})

通过Object.defineProperty()方法定义了person的Name属性,并把这个属性的读写分别使用get和set方法进行拦截,每当该属性进行读或写操作时候就会触发这两个方法,意味着person的数据对象已经是可观测的了

但是为了将此对象的所有属性都变得可观测,可以编写以下代码: 

export class Observe {
    constructor(value) {
        this.value = value
        //给value新增一个__ob__属性,值为该value的observe实例
        //相当于给value打上标记,表示他已经转化为响应式了
        def(value,'__ob__',this)
        if (Array.isArray(value)) {
        //为数组的逻辑
        } else {
            this.walk(value)
        }
    }
    
    walk(obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj,key[i])
        }
    }
}
​
function defineReactive(obj, key, val) {
    if(typeof val === 'object') {
    new Observable(val)
    }
    object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log(`${key}属性被读取`)
            return val;
        },
        set(newval) {
            if(val === newval) {
                return
            }
            console.log(`${key}属性被修改了`)
            val = newval
        }
    })
}

上面的代码中,我们定义了observe类,用来将一个普通的Object转化为可观测的object。并且给value新增一个ob属性,表示这个已经被转化成响应式了。

然后判断数据类型,只有Object 类型才会调用walk方法将每一个属性都转换成getter/setter形式来侦测变化,最后在defineReactive中当传入的属性值还是一个object时使用new observe(val)来递归子属性,这样我们就可以把obj中所有属性都变为可观测型的,只要将object传入observe中,那么这个object就会变成可观测,响应式的Object.

observer类位于Vue2源码的src/core/observer/index.ts中。

2.依赖收集

当Object变得可观测后,我们就能知道数据什么时候发生了变化,发生了什么变化,这样就可以通知视图更新。但是问题来了,我们怎么知道哪些视图需要更新?总不能一个数据变化全部视图全部更新一次吧。所以,应该是视图里哪个使用到了这个数据的就更新。

视图里谁用到了这个数据谁更新,说官方一点就是谁依赖了这个数据谁更新。我们建立一个依赖数组(因为一个数据可能被多个地方使用,所以需要建立数组),谁依赖了这个数据就把谁放进这个数组。当数据变化时,就根据它的依赖数组去通知哪些视图需要更新。这个过程就是依赖收集。

那么我们该如何进行依赖收集,如何知道哪些依赖这个数据?

谁用了这个数据,那么当这个数据变化时就通知谁,所谓谁用了这个数据,就是谁获取了这个数据。根据上文可观测数据在被获取时会调用get方法,那么我们就可以在get中收集这个依赖。同样,数据变化会触发Set,所以,我们就在set中通知更新。

所以,我们应该给每个数据都配置一个依赖数组,谁依赖了它就放入谁。不过,单单使用数组的话,功能可能有点欠缺且代码有点耦合。所以拓展一下的话,我们给每一个数据建立一个类,用作依赖管理。所以在vue2源码中有了这个依赖管理器Dep类,代码如下:

// 源码位置:src/core/observer/dep.ts    
export default class Dep {
  constructor () {
    this.subs = []
  }
  
  addSub (sub) {
    this.subs.push(sub)
  }
  // 删除一个依赖
  removeSub (sub) {
    remove(this.subs, sub)
  }
  // 添加一个依赖
  depend () {
    if (window.target) {
      this.addDep(window.target)
    }
  }
  // 通知所有依赖更新
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
​
/**
 * Remove an item from an array
 */
export function remove (arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

在上面依赖管理器中,初始化了一个sub数组用来存放依赖,并且定义了几个附加方法。

有了依赖管理器后,我们就可以在getter中收集依赖,setter中通知更新了

function defineReactive (obj,key,val) {
  if(typeof val === 'object'){
    new Observable(val)
  }
  const dep = new Dep()  //实例化一个依赖管理器,生成一个依赖管理数组dep
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      dep.depend()    // 在getter中收集依赖
      return val;
    },
    set(newVal){
      if(val === newVal){
          return
      }
      val = newVal;
      dep.notify()   // 在setter中通知依赖更新
    }
  })
}

接下来需要知道的是,我们收集的依赖到底是谁?

谁用到了这个数据谁就是依赖,那代码上该如何描述这个谁呢?

在Vue中还实现了一个叫做Watcher的类,这个类的实例就是我们上面说的那个谁。换句话说,谁用到了数据,我们就为谁创建一个Watcher实例。在之后数据变化时,我们不直接去通知依赖更新,而是去通知依赖对应的watch实例,由watcher实例去通知真正的视图

类的具体实现如下:

export default class Watcher {
  constructor (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get () {
    window.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    window.target = undefined;
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, value, oldValue)
  }
}
​
/**
 * Parse simple path.
 * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
 * 例如:
 * data = {a:{b:{c:2}}}
 * parsePath('a.b.c')(data)  // 2
 */
const bailRE = /[^\w.$]/
export function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

分析代码:

1.实例化Watcher类时,会先执行其构造函数;

2.在构造函数中调用了this.get()实例方法;

3.在get()方法中,首先通过window.target = this 把实例自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm,vm)获取一下被依赖的数据,这里的目的是触发该数据的getter,在上文说过,调用getter会随之调用dep.depend()方法收集依赖,而在dep.depend()上取到挂载到window.target的值并将其存到依赖数组中,在get()方法最后需要将window.target释放掉

4.当数据变化时,会触发setter,在其中执行了dep.notify()方法,在这个方法中,遍历所有的依赖(即watcher实例),执行依赖的update()方法,在方法中调用数据变化的更新回调函数,从而更新视图。

3.不足之处

通过defineProperty方法实现了对Object数据的观测,但是这个方法仅仅只能观测到其中数据及设置值,如果向Object中添加或删除值时,他是无法观察到的,导致添加或者删除值时,无法通知依赖,无法驱动视图更新。vue也注意到了这一点,所以为了解决这个问题,vue添加了两个全局api:$set $delete,在后面学习全局API时会说到。

4.总结

通过Object.defineProperty方法实现了对object数据的可观测,并且封装了Observer类,让我们能够方便的把object数据中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。

接着,知道了在getter中收集依赖,在setter中通知依赖更新,以及封装了依赖管理器Dep,用于存储收集到的依赖。

最后,我们为每一个依赖都创建了一个Wtcher实例,当数据发生变化时,通知Watcher实例,由Watcher实例去做真实的更新操作。

其整个流程大致如下:

  1. Data通过observer转换成了getter/setter的形式来追踪变化。

  2. 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。

  3. 当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。

  4. Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。


http://www.kler.cn/news/319163.html

相关文章:

  • 搭建HAproxy----7层负载均衡集群
  • FDA辅料数据库在线免费查询-药用辅料
  • 灵当CRM multipleUpload.php 文件上传致RCE漏洞复现
  • 双11好物推荐有哪些?五大双十一好货推荐!
  • PHP如何从字符串中删除转义字符
  • 抽奖拼团卷轴模式系统开发小程序源代码解析
  • Flask 第十二课 -- 错误处理
  • 下水道内缺陷识别检测数据集 yolo数据集 共2300张
  • LeetCode2207解题思路
  • 双十一买什么好?五款数码好物推荐!
  • 毕业设计选题:基于ssm+vue+uniapp的面向企事业单位的项目申报小程序
  • 1.3 MySql的用户管理
  • 电脑如何录屏?无水印、高清晰度电脑录屏教程
  • 『功能项目』QFrameWork道具栏物品生成【64】
  • thinkphp8 从入门到放弃(后面会完善用到哪里写到哪)
  • C#图像爬虫实战:从Walmart网站下载图片
  • python常见的魔术方法
  • 对FPGA加载过程中不同寄存器初始化方式现象的分析
  • 基于PHP的CRM管理系统源码/客户关系管理CRM系统源码/php源码/附安装教程
  • 免费分享必看!AI合规常见问题解答(二)
  • java之斗地主部分功能的实现
  • 修改Linux服务器系统语言
  • 深入解析Debian与Ubuntu:技术特点与用户使用指南
  • Git 详细安装教程(详解 Git 安装过程的每一个步骤)
  • Python 课程19-FastAPI
  • 开源 AI 智能名片与 S2B2C 商城小程序:嫁接权威实现信任与增长
  • 深入解析:HTTP 和 HTTPS 的区别
  • 51单片机开关电路+限位+舵机
  • 【玉米田】
  • Springboot多种请求参数