[vue2]熬夜编写为了让你们通俗易懂的去深入理解双向绑定以及解决监听Array数组变化问题

889 阅读2分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

更多文章

[vue2]熬夜编写为了让你们通俗易懂的去深入理解nextTick原理

[vue2]熬夜编写为了让你们通俗易懂的去深入理解vue-router并手写一个

[vue2]熬夜编写为了让你们通俗易懂的去深入理解vuex并手写一个

[vue2]熬夜编写为了让你们通俗易懂的去深入理解v-model原理

熬夜不易,点个赞再走吧

双向绑定

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

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

  3. setter通知Watcher

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

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

Observe

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

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

Observer源码逐步解析:

export class Observer {
  valueany;
  depDep;
  vmCountnumber;

constructor (valueany) {
    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){

        },
        deeptrue,
        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, {
    enumerabletrue,
    configurabletrue,
    valuefunction(...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([12], 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)触发响应式