Vue源码之Object数据劫持

目录

  • vue中如何侦测Object变化?
    • Object.defineProperty
    • 数据劫持原理
  • Object 响应式中的问题
    • vm.$set内部原理
    • vm.$delete内部原理
  • vm.$watch内部原理
    • 用法
    • watch内部实现原理
    • deep实现原理
vue中如何侦测Object变化? js中两种侦测对象变化方法:
  • 使用Object.defineProperty
  • ES6的proxy(ES6浏览器支持不理想)
Object.defineProperty 该方法定义一个响应式数据,当数据的属性发生变化的时候,通知依赖更新,即向使用到它的地方发送通知
【Vue源码之Object数据劫持】Object可以通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter
Object.defineProperty(obj, key, { get() { return val; }, set(newVal) { if (newVal !== val) { val = newVal } } })

这样,修改数据时,我们可以主动通知依赖进行更新,那么问题来了:
  1. 如何收集依赖?什么时机收集依赖?
    思路:
    1. 任何地方,只要涉及到数据读取,就会触发get函数
    2. 先收集依赖,然后等属性发生变化的时候,再把之前收集好的依赖循环触发一遍就好了
    总结:在getter中收集依赖,在setter中触发依赖
  2. 依赖收集在哪里?
    定义一个Dep类,用来存储当前key的依赖。DEP类主要负责对依赖的收集、删除、发送通知。
  3. 依赖是谁?
    依赖就是用到数据的地方,用到这个数据的地方可能是视图中对应的坑({{name}}),也可能是开发者写的一个watch,于是抽象一个类Watcher
至此,问题告一段落
  • 定义一个响应式数据
//数据劫持,添加依赖追踪 defineReactive(obj, key, val) { const dep = new Dep(); Object.defineProperty(obj, key, { get() { //只有Watcher触发的getter才会收集依赖 Dep.target && dep.depend(); return val; }, set(newVal) { if (newVal !== val) { val = newVal; // 数据发生变更通知所有的观察者 dep.notify() } } }) //递归树 this.observer(val); }

  • 定义一个Dep类,收集依赖
// Dep发布者相当于vue data 对象中的某一个属性如:name class Dep { constructor(vm, key) { this.subs = []; }depend() { if (Dep.target) { this.addSub(Dep.target) } } addSub(sub) { this.subs.push(sub); } removeSub(sub) { remove(this.subs, sub); }notify() { //只负责通知更新,具体是否更新以及更新操作由watcher做 this.subs.forEach(sub => sub.update()) } }

  • 定义依赖 – Watcher类
当new Watcher()实例时,会自动将依赖收集到Dep中
// watcher 相当于视图中对应的坑({{name}}),一个坑对应有一个观察者,监听此处的数据变化 class Watcher { constructor(vm, key, cb) { this.vm = vm; this.key = key; this.cb = cb; //这段代码会自动将Watcher添加到Dep中 //先将Watcher赋值给Dep.target,然后读取一下值,会触发get,这时依赖就被收集了,再将Dep.target置空 Dep.target = this; this.vm[this.key]; Dep.target = null; }update() { //每个watcher所做的更新大不一样,所以将具体的更新操作放到回调里面去做 this.cb.call(this.vm, this.vm[this.key]) } }; }

  1. 封装类Observer,将数据内的所有属性都转换成getter/setter形式
完整代码:
class Observer{ constructor(obj){ this.obj = obj if(!Array.isArray(obj)){ this.walk(obj) } } walk(obj){ Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } } function defineReactive(obj, key, val) { // 递归子属性 if(typeof val === 'object'){ new Observer(val) } const dep = new Dep(); Object.defineProperty(obj, key, { get() { //如果有创建了一个观察者,那么Dep.target就会被它的实例赋值,此时就会通过访问实例的值(见Watcher 构造函数)将其add到dep //同时这里的判断是为了防止重复添加 Dep.target && dep.depend(); return val; }, set(newVal) { if (newVal !== val) { val = newVal; // 数据发生变更通知所有的观察者 dep.notify() } } }) }

数据劫持原理 Vue源码之Object数据劫持
文章图片

  1. Data通过Observer转换成响应式对象
  2. 当外界通过Watcher读取数据时,会触发getter从而将watcher添加到Dep中
  3. 当数据发生变化时,会触发setter,Dep像依赖(Watcher)发送通知
  4. Watcher收到通知后,向外界发送通知,可能会触发视图更新,或者执行回调函数。
tip: 至于何时new Watcher() ,我们在模板编译的时候进行说明
Object 响应式中的问题 通过defineProperty方法定义obj之后, 对Obj新增属性和删除属性的操作,将无法追踪到这个变化。
原因: ES6之前,JS没有提供元编程能力
//新增属性 this.obj.name = 'xxx' //删除属性 delete this.obj.name

为了解决这个问题,vue.js提供的两个API——vm. s e t 和 v m . set和vm. set和vm.delete
vm.$set 用法:vm.$set(target, key, value)
import {set} from './observer/index' Vue.prototype.$set = set
代码中数组的部分可以在读了《vue源码之Array》之后再了解,源码及解读:
function set (target, key, val) { // 当target是一个数组并且key是一个有效的索引 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val } // 如果key 已经存在于对象中,直接修改值即可 if (key in target && !(key in Object.prototype)) { target[key] = val; return val }// 源码中,在Observer一个对象时,会给对象添加__ob__属性,标识这是一个响应式对象 var ob = (target).__ob__; // 如果target上并没有__ob__属性,说明它本身就不是一个响应式的对象,不做处理 if (!ob) { target[key] = val; return val } //否则,将属性转换成响应式,并向依赖发出更新通知 defineReactive(ob.value, key, val); ob.dep.notify(); return val }

vm.$delete 用法:vm.$delete(target, key)
import {del} from './observer/index' Vue.prototype.$delete = del
源码解读:
function del (target, key) { if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1); return } var ob = (target).__ob__; // 不存在此属性时,直接返回 if (!hasOwn(target, key)) { return } delete target[key]; if (!ob) { return } // 删除属性后,像依赖发送通知 ob.dep.notify(); }

vm.$watch内部原理 1. 用法
vm.$watch( expOrFn, callback, [options] )
用于观察一个表达式或computed函数的变化,来达到todo something的目的。
返回一个取消观察的函数,用于停止触发回调
var unwatch = vm.$watch('a.b.c', (newVal, oldVal) => {}) unwatch()

  • expOrFn
    1. 表达式只支持以点分隔的路径
    2. computed函数,如 function(){ return this.name + this.age}
  • options
    1. deep:true 深度监听对象内部值的变化,监听数组的变化不需要这么做
    2. immediate:true 立即以当前值触发一次回调
2. watch的实现原理
vm. w a t c h 实 际 上 是 对 W a t c h e r 的 一 种 封 装 。 无 论 是 视 图 中 的 多 个 依 赖 ( 如 n a m e 出 现 的 地 方 ) , 还 是 使 用 watch实际上是对Watcher的一种封装。无论是视图中的多个依赖(如{{name}}出现的地方),还是使用 watch实际上是对Watcher的一种封装。无论是视图中的多个依赖(如name出现的地方),还是使用watch监听值的变化,都可以看做是观察者。
实质:当用户使用vm.$watch()来监听一个数据变化时,实际跟我们在视图中新增一个坑{{name}},所做的事情是一样的。
  1. 实例化了一个Watcher
  2. 将Watcher收集到Dep中
  3. 当expOrFn对应的值发生变化时,会在原有的基础上新增一个依赖通知,触发cb调用
源码解读:
Vue.prototype.$watch = function ( expOrFn, cb, options ) { var vm = this; options = options || {}; // 创建一个依赖,并收集到Dep var watcher = new Watcher(vm, expOrFn, cb, options); if (options.immediate) { try { cb.call(vm, watcher.value); } catch (error) { handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\"")); } } return function unwatchFn () { // 依赖将自己从被收集到的所有Dep中移除,终止订阅数据变更通知 watcher.teardown(); } }; }

  • Watcher 和 Dep 多对多的关系
    1. expOrFn是一个表达式,则只会对应一个Dep
    2. expOrFn是一个函数时,当函数中使用了多个数据时,那么Watcher就要收集多个Dep
    3. 同时一个数据在多处运用,所以Dep对应多个Watcher
//Watcher会收集name和age两个的Dep // 同时这两个Dep中也会收集Watcher // 任意一个数据发生变化,Watcher都会收到通知 this.$watch(function(){ return this.name + this.age },(newVal, oldVal) => {})

3. deep实现原理
思路:
  1. 无非就是收集依赖,将数据以及数据内部的子值都触发一遍依赖收集
  2. 在Watcher类中实现
初始化实例时,判断是否deep,注意代码在之前执行
Watcher类的修改:
Dep.target = this; this.vm[this.key]; //收集到当前数据Dep // 新增 if(!!options.deep){ traverse(value) // 递归收集到当前数据子值的Dep } Dep.target = null;

添加到子值Dep
var seenObjects = new Set(); function traverse (val) { _traverse(val, seenObjects); seenObjects.clear(); }function _traverse (val, seen) { var i, keys; if (val.__ob__) { var depId = val.__ob__.dep.id; if (seen.has(depId)) { return } seen.add(depId); } // 重点看这里 // 如果值是一个对象,则递归 keys = Object.keys(val); i = keys.length; while (i--) { // 其中val[keys[i]] 会触发getter,也就是此处会进行依赖收集 _traverse(val[keys[i]], seen); } }

_traverse(val[keys[i]], seen)
其中val[keys[i]]会触发getter,也就是此处会进行依赖收集。
至此,所有值的变化,都会触发cb。
为了便于理解,文章中的代码都是源码的删减版

    推荐阅读