Loading...
墨滴

jojo的奇妙前端

2021/07/30  阅读:56  主题:默认主题

[vue2]深入理解Observer,Dep,Watcher以及解决监听Array数组变化

双向绑定

1. 首先通过一次渲染操作触发Data的getter进行依赖收集

2. 在data发生变化的时候会触发它的setter

3. setter通知Watcher

4. Watcher进行回调通知组件重新渲染的函数

5. diff算法来决定是否发生视图的更新

Observe

1. 每个数据都有一个标记,防止重复绑定

2. Observer为数据加上响应式属性进行双向绑定,如果是对象,则进行深度遍历,为每一个子对象都绑定上方法,如果是数组,对每个成员进行遍历绑定方法

Observer源码逐步解析:
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

constructor (value: any) {
  this.value = value
  this.dep = new Dep() // 建立发布者
  this.vmCount = 0
  def(value, '**ob**', this)
  if (Array.isArray(value)) {
  // 是数组对每个成员进行遍历绑定方法
  
  if (hasProto) {
  // **proto**指向重写过后的原型
  protoAugment(value, arrayMethods)
  
  } else {
  //遍历 arrayMethods 把它身上的这些方法直接给 value
  copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
  } else {
  
  // 是对象,则进行深度遍历,为每一个子对象都绑定上方法
  // defineReactive 通过 Object.defineProperty 定义 getter 和 setter 收集依赖通知更新
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
  }
}

Watcher

观察者对象

1. 依赖收集后保存在deps里

2. 变动的时候deps作为发布者通知watcher watcher进行回调渲染

Dep

1. 发布者,可以订阅多个观察者

2. 收集依赖后会有一个或者多个watcher

3. 一旦有变动便通知所有watcher

Watch监听Array数组变化

监听对象:

data(){
  return {
    objVal: {
      name: 'obj',
      type'obj'
    }
  }
},
watch:{
  objVal:{
    handler(val,oldval){

        },
        deep: true,
        immediate:true
      }
    },
    methods:{
      changeObj(){
        this.objVal.name = 'newobj';
      }
    }

deep: 当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,只有data中的数据才能够监听到变化,深入监听,即监听对象里面的值的变化

immediate: watch默认当值第一次绑定的时候,不会执行监听函数,immediate的作用就是首次获取值也执行函数

以上demo是监听对象,如果换成数组的话,会出现vue不会响应数据变化而重新去渲染页面,则监听失败

解决方法:

// Vue.set
Vue.$set(vm.items, indexOfItem, newValue)

// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

原理: Object.defineProperty对数组进行响应式化是有缺陷的 Vue使用了重写原型的方案代替

  1. 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
  2. 对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
  3. 把需要被拦截的 Array 类型的数据原型指向改造后原型
const arrayProto = Array.prototype // 获取Array的原型
function def (obj, key) {
 Object.defineProperty(obj, key, {
   enumerable: true,
   configurable: true,
   value: function(...args) {
     console.log(key); // 控制台输出 push
     console.log(args); // 控制台输出 [Array(2), 7, "hello!"]
      
     // 获取原生的方法
     let original = arrayProto[key];
     // 将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变
     const result = original.apply(this, args);

     // do something 比如通知Vue视图进行更新
     console.log('我的数据被改变了,视图该更新啦');
     this.text = 'hello Vue';
     return result;
   }
 });
}
// 新的原型
let obj = {
 push() {}
}
// 重写赋值
def(obj, 'push');

let arr = [0];
// 原型的指向重写
arr.__proto__ = obj;
// 执行push
arr.push([1, 2], 7, 'hello!');
console.log(arr);

源码解析 array.js

Vue在array.js中重写了methodsToPatch中七个方法,并将重写后的原型暴露出去。

// Object.defineProperty的封装
import { def } from '../util/index'
// 获得原型上的方法
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// Vue拦截的方法
const methodsToPatch = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
];
// 1.拦截方法
// 2.将开发者的参数传给原生的方法
// 3.重写
// 4.视图更新
methodsToPatch.forEach(function (method) {
  // 原型方法进行赋值,不会去重新改写Array.prototype
  const original = arrayProto[method]
  //ob为成员唯一标识
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    //判断方法
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    //判断后observeArray为每个成员绑定方法
    if (inserted) ob.observeArray(inserted)
    // 通知视图更新
    ob.dep.notify()
    return result
  })
})

问答

问:遇到过改变对象或者数组的时候视图没有更新的情况吗?为什么?怎么解决?

场景:

1.利用索引直接设置一个项时:vm.items[indexOfItem] = newValue

2.修改数组的长度时: vm.items.length = newLength

3.Vue 不能检测到对象属性的添加或删除

原因:

1. 因为没有用被重写的方法去修改数组,导致没有响应式的监听到

2. 而vue官方文档有明确说明,Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的

解决方法:

// 1. 利用this.$set(this.object,key,value)
this.$set(this.obj,"sex","man")
// 2. 利用this.$delete(target, propertyName/index )
this.$delete(this.testData,"name")
// 3. 利用Object.assign({},this.obj)
this.obj = Object.assign({},this.obj,{"myName","jojo"})

set()通过defineReactive(ob.value, key, val)触发响应式

jojo的奇妙前端

2021/07/30  阅读:56  主题:默认主题

作者介绍

jojo的奇妙前端