vue2响应式原理

718 阅读3分钟

主要做了这么几件事:数据劫持、收集依赖、派发更新

1.数据劫持:new Vue的时候遍历data对象,用Object.defineProperty给所有属性加上了getter和setter

2.依赖的收集:render的过程,new Dep()依赖收集类 会触发数据的getter,在getter的时候把当前的watcher对象收集起来

3.派发更新:setter的时候,遍历这个数据的依赖对象(watcher对象),进行更新

数据劫持

通过Object.defineProperty劫持对象属性,让数据变为是可观察的

class Vue {
    constructor(options) {
        observer(options.data);
    }
    observer(obj) {
        if (!obj || (typeof obj !== 'object')) {
            return;
        }
        // 遍历data对象 逐个加上getter setter 
        // 1.获取对象中的所有key
        // 2.遍历key组成的数组 处理函数是 传递 本对象 本key 本key对应的value
        Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
    }
    
    defineReactive(obj, key, val) {
        const dep = new Dep(); // Dep是依赖收集类
        Object.defineProperty(obj, key, {
            enumerable: true, // 属性可枚举
            configurable: true  //属性可被修改或删除
            get() {
                dep.addSub(Dep.target); // 依赖采集
                return val;
            },
            set(newVal) {
                if (newVal === val) return;
                dep.notify(newVal); // 派发更新
            }
        })
    }
}
可以看到主要就是这个defineReactive函数,他利用Object.defineProperty实现了数据劫持

依赖收集&派发更新

vue是怎么知道当数据改变的时候都要去通知谁呢?它用了一个订阅者Dep,用来存放我们的观察者对象,当数据发生改变,就通知观察者,观察者通过调用自己的update方法完成更新。

<!--订阅者Dep类-->

class Dep {
    constructor () {
        this.newDeps = [] // 用来存放我们的依赖对象(也即观察者)
    }
    addDep (watcher) {
        this.newDeps.push(watcher) // 向队列里新加一下watcher对象
    }
    update () {
        this.newDeps.forEach((sub) => {
            sub.update(); // 遍历watcher进行更新
        })
    }
}

<!--观察者Watcher类-->
class Watcher {
    constructor  () {
        Dep.target = this  // new Watcher的时候把观察者存放到Dep.target里面
    }
    update () {
        console.log("视图更新啦~"); // 更新视图
        <!--queueWatcher(this) 异步更新策略 后面再写一篇介绍 -->
    }
}

new一个Watcher对象,此时它会指向Dep.target,在render过程触发getter,把Dep.target添加到依赖队列。这样便完成了依赖的收集。数据改变,通知依赖进行update操作。

  • 简易版
// 科隆数组原型
const arrYuanXing = Array.prototype
   // 深拷贝一份
const keLongYuanXing = Object.create(arrYuanXing)

const methodsArr = ['push', 'pop', 'shift', 'unshift', 'reverse', ]

// 遍历修改克隆出来的数组原型方法 (不能直接修改 Array.prototype 上面的额方法)
methodsArr.forEach(method => {
   keLongYuanXing[method] = function() {
      //  渲染UI
      renderView()
         //method 是 methodsArr 中的item   方法名  
      arrYuanXing[method].call(this, ...arguments) // 等价与调用了 obj.push()
   }
})

function renderView() {
   console.log('view发生了改变');
}

// 1.怎么个知道obj.name 发生改变了?
// 答案: 我们要监听这个obj
// 
function observe(target) {
   // target 监听目标 可能是 null  array  object

   // 判断 target是什么
   // 1. 如果是string number null 就不监听了直接return
   if (typeof target !== 'object' || target == 'null') {
      return target
   }
   // 2.  如果是数组
   if (Array.isArray(target)) {
      //  让数组的原型 指向 克隆出来的原型
      target.__proto__ = keLongYuanXing
   }
   //  3. 如果是object (对象中的每一个key都是响应式的 需要遍历监听)
   for (const key in target) {
      //  监听方法 监听对象的每一个key 和 value
      let value = target[key]
         /* 这里要继续监听value 是否是个对象  是的话就递归  但是假如有10000层  那就卡死了递归*/
      observe(value)
         /* vue 3.0 就改的是这里   但是2.0还是用的递归*/
         //  重点:采用的是es5中的方法  object.defineProperty
      Object.defineProperty(target, key, {
         // 赋值
         get() {
            return value
         },
         // 赋新的值 (obj.key 只要改变就触发这个方法  我们刚好在这个方法里面更新ui)
         set(newValue) {
            observe(value)
            renderView()
            value = newValue
         }
      })
   }
}

var obj = {
      name: '小明',
      arr: [1, 2, 3],
      sifangqian: {
         money: 100
      }
   }
   // 调用监听
observe(obj)


// 响应式:obj.name 发生改变了就去更新ui
// obj.name = '中国加油'

// obj.sifangqian.money = '9994'
obj.arr.push(4)


// ! 总结 重点就是 object.defineProperty 对数据劫持(发生变化之前 先做一些事情)