Skip to content

Vue2的响应式原理 -- 源码剖析 ​

vue2的响应式原理一直是面试基本必考的,今天我们通过手写来剖析响应式原理的实现

手写响应式原理 ​

  1. vue首先把所有data中的属性和方法耦合到了一起
js
     //响应式的数据变化,数据变化了我可以监控到数据的变化
      //数据的取值 和 更改值我们要监控到
      const vm = new Vue({
        data() {
          return { //代理数据
            name: 'zf',
            age: 20,
            address: {
              content: 'DDD',
              num: 56
            },
            hobby: ['eat', 'slipp', {
              a: 1
            }]
          }
        }
      })


    //将所有的方法都耦合到一起
      function Vue(options){//options就是用户的选项
        this._init(options) //默认就调用了init
      }
      initMixin(Vue) //扩展了init方法
  1. 将用户选项挂载到vm实例上
js
    function initMixin(Vue){
      Vue.prototype._init = function(options){//用于初始化操作
        //vue  vm.$options 就是获取用户的配置
        //我们使用的vue的时候 $nextTick $data $attr.....
        const vm = this 
        vm.$options = options//将用户的选项挂载到实例上   
        //初始化状态
        initState(vm);
      } 
    }
  1. 对属性进行劫持
js
    function initState(vm){
      const opts = vm.$options //获取所有的选项
      if(opts.data){
        initData(vm)
      }
    }  

    function initData(vm){
      let data = vm.$options.data // 可能是函数或对象
      data = typeof data === 'function' ? data.call(vm) : data
      
        vm._data = data  //我将返回的对象放到了_data上
        //对数据进行劫持 vue2里采用了一个api defineProperty
        observe(data)
        //将vm._data用vm来代理就可以了
        for(let key in data){
          proxy(vm,'_data',key)
        }    
    }
  1. 为了获取属性方便,进行数据代理
js
    function proxy(vm,target,key){
     //把_data中的属性再vm上在添加一遍,这样我们就可以直接this.xxx获取数据,不用this._data.xxx来获取数据了   
      Object.defineProperty(vm,key,{
        get(){
          return vm[target][key]
        }
      })
    }
  1. Observe观测者 进行递归,将所有对象属性加上了get和set
js
    class Observer {
      constructor(data) {
        this.dep = new Dep(); // 此时的dep挂在被observe的数据的__ obj__属性上,在对象或数组增删元素时调用$set等api时发布更新的;
        this.vmCount = 0 //记录实例使用次数 
        //Object.defineProperty只能劫持已经存在的属性(vue2里会为此单独写一些api $set $delete)
        Object.defineProperty(data, '__ob__', {
          value: this,
          enumerable: false //将__ob__变成不可枚举(循环时无法获取,防止死循环)
        })
        data.__ob__ = this //给数据加了一个标识
        if (Array.isArray(data)) {
          //重写数组的方法,7个变异方法是可以修改数组本身的
          data.__proto__ = newArrayProto //需要保留原数组原有的特性,并且重写部分方法
          this.observeArray(data) //如果数组中放的是对象,可以监控到对象的变化
        } else {
          this.walk(data)
        }
      }
      walk(data) { //循环对象 对属性依次劫持
        //"重新定义"属性  性能差
        Object.keys(data).forEach(key => defineReactive(data, key, data[key])) //会根据__ob__来判断对象是否已经被劫持了
      }
      observeArray(data) {
          for (let i = 0, l = data.length; i < l; i++) {
            observe(data[i]); // 逐项进行observe
          }
      }
    }
    export function defineReactive(target, key, value) { //闭包 //外层函数包裹,避免使用全局变量接收newVal的问题
      const dep = new Dep();//存在在getter/setter闭包函数的作用域链上,是为对象属性服务的。在Watcher获取属性的值的时候收集订阅者,在设置属性值的时候发布更新。
      let childOb = observe(value) //子元素要进行observe,递归调用深度监听
      Object.defineProperty(target, key, {
        get() { 
          if (Dep.target) { // 如果现在处于依赖收集阶段,render函数/computed/watcher创建的阶段
            dep.depend();
            if (childOb) {
                childOb.dep.depend();
            }
            if(Array.isArray(value)){
              dependArray(value)
            }
          }
          return value    //不是依赖收集阶段 直接返回值
        },
        set(newValue) {
          // value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
          if (newValue === value) return
          observe(newVal) //若赋值成为对象,也要进行深度监听
          value = newValue
          dep.notify() // 发布订阅模式,通知dep去告诉watcher触发更新
        }
      })
    }
    export function observe(data) {
      if (typeof data !== 'object' || data == null) {
        return //只有对象需要劫持
      }
      //如果一个对象被劫持过了,那就不需要再被劫持了(要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)
      if (data.__ob__ instanceof Observer) {
        return data.__ob__ //说明这个对象被代理过了
      }
      return new Observer(data)
    }
  1. 重写数组中的部分方法
js
    let oldArrayProto = Array.prototype // 获取数组的原型
    // 创建新对象,原型指向oldArrayProperty
    let newArrayProto =  Object.create(oldArrayProto)
    let methods = [
      //找到所有的变异方法
      'push',
      'pop',
      'shift',
      'unshift',
      'reverse',
      'sort',
      'splice'
    ] // concat slice 不会改变原数组
    methods.forEach(methods=>{
      newArrayProto[method] = function(...args){//这里重写了数组的方法
      const result =  oldArrayProto[method].call(this,...args)//内部调用原来的方法,函数的劫持,切片编程
      //我们需要对新增的数据再次进行劫持
      let inserted
      let ob = this.__ob__
      switch(method){
        case 'push':
        case'unshift':
            inserted =args;
            break
        case 'splice':
            inserted = args.slice(2)
        default:
            break
      }
      console.log(inserted) //新增的内容
      if(inserted){
        //对新增的内容再次进行观测
        ob.observeArray(inserted)
      }
      return result
      }
    })
  1. dep
js
    let id = 0
    class Dep {
      constructor() {
        this.id = id++
        // 依赖收集,收集当前属性对应的观察者 watcher
        this.subs = []
      }
      // 通知 watcher 收集 dep
      depend() {
        Dep.target.addDep(this)
      }
      // 让当前的 dep收集 watcher  
      addSub(watcher) {
        this.subs.push(watcher)
      }
      // 通知subs 中的所有 watcher 去更新
      notify() {
        const subs = this.subs.slice(); // 浅克隆一份
        this.subs.forEach(watcher => watcher.update())
      }
    }
    // 当前渲染的 watcher,静态变量
    Dep.target = null //渲染/更新完毕后我们会立即清空 Dep.target  保证了只有在模版渲染/更新阶段的取值操作 才会进行依赖收集。之后我们手动进行数据访问时,不会触发依赖收集,因为此时 Dep.target 已经重置为 null   
    export default Dep
  1. Watcher
js
      import Dep from './dep'
      let id = 0
      class Watcher {
        constructor(vm, fn) {
          this.id = id++
          this.getter = fn
          this.deps = []  // 收集当前 watcher 对应被观察者属性的 dep
          this.depsId = new Set()
          this.get()
        }
        // 收集 dep
        addDep(dep) {
          let id = dep.id
          // 去重,一个组件 可对应 多个属性 重复的属性不用再次记录
          if (!this.depsId.has(id)) {
            this.deps.push(dep)
            this.depsId.add(id)
            dep.addSub(this) // watcher已经收集了去重后的 dep,同时让 dep也收集 watcher
          }
        }
        // 执行 watcher 回调
        get() {
          Dep.target = this // Dep.target 是一个静态属性
          this.getter() // 执行vm._render时,会劫持到数据访问,调用 dep.depend() 进行依赖收集
          Dep.target = null // 渲染完毕置空,保证了只有在模版渲染阶段的取值操作才会进行依赖收集
        }
        // 重新渲染
        update() {
          this.get()
        }
      }
  1. 总结 :
    1. vue2中对每一个属性都进行"重新定义"属性,这也是相对性能差的原因,
    2. 数组如果数据量过大,那么将会有溢出栈的问题,所以对数组部分方法进行改造,
    3. __ob__
      1. 我们给数组/对象的实例都增加一个 dep。可以通过响应式数据的__ob__获取到实例,进而访问实例上的属性和方法,可以为重写数组方法中拿到class类中的observeArray方法
      2. 又可以给检测过的属性加上标识,防止重复监测的问题.
    4. 将__ob__变成不可枚举(循环时无法获取,防止死循环)
    5. Dep有一个静态属性 target,全局唯一,Dep.target 是当前正在执行的 watcher 实例,这是一个非常巧妙的设计!因为在同一时间只能有一个全局的 watcher
    6. 实例化 dep 的时机
      1. Observer类:在 Observer 类中实例化 dep,可以给每个数组/对象的实例都增加一个 dep
      2. defineReactive函数:在 defineReactive 方法中实例化 dep,可以让每个被劫持的属性都拥有一个 dep,这个 dep 是被闭包读取的局部变量,会驻留到内存中且不会污染全局