Vue2.0 与 Vue3.0 响应式实现的区别

551 阅读5分钟

一、 数据双向绑定实现的对比

实现方式的相同之处

Vue2.0 与 Vue3.0 都是采用数据劫持结合发布者-订阅者模式的方式实现的。

区别

  1. Vue2.0 实现MVVM(双向数据绑定)的原理是通过 Object.defineProperty 来劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
  2. Vue 3.0 则是通过 new Proxy() 来劫持各个属性的 setter,getter。

二、 数据劫持方式的优劣对比

Vue2.0

  1. 无法监听数组和对象变化。
  2. 由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所有属性必须在 data 对象上存在才能让 Vue 将它转换为响应式。
  3. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。深度监听需要一次性递归,对性能影响比较大。
  4. Object.defineProperty 是 ES5 中一个无法 shim 的特性,因此不能兼容 IE8 及以下的版本。

Vue3.0

  1. 基于 Proxy 和 Reflect,可以原生监听数组,可以监听对象属性的添加和删除。
  2. 不需要一次性遍历 data 的属性,可以显著提高性能。
  3. 因为 Proxy 是 ES6 新增的属性,有些浏览器还不支持,只能兼容到 IE11。

三、实现过程

Vue2.0

  1. Vue ,初始化实例时,把 data 中的成员转成 getter/setter

  2. Observer ,对数据对象的所有属性进行监听,如果变动拿到最新值并通知 Dep(发布者-目标)

  3. Watcher ,定义观察者,定义update() 更新函数,当数据发生变动,更新视图

  4. Dep ,添加观察者,当数据发生变化的时候,通知所有的观察者,执行观察者的 update() 函数

  5. Compiler ,负责编译模板,解析指令/差值表达式,负责页面的首次渲染,当数据变化后更新视图

MVVM 作为数据绑定的入口,通过 Observer 来监听 data 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到双向绑定效果。

// 1. 定义一个 vue 实例,将实例中 data 对象中的数据转成 getter/setter
class Vue {
  // 1.1 定义类属性
  constructor(options) {
    // 1.1.1 通过属性保存data中的数据
    this.options = options || Object.create(null);
    // 1.1.2 为了防止与内部变量冲突,data 必须是一个函数
    if (typeof options.data !== 'function') {
      throw ('data must be a function')
    }
    // 1.1.3 储存data数据
    this.data = options.data() || Object.create(null);
    // 1.1.4 获取挂载节点
    this.el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    // 1.1.5 把data中的成员转换成getter/setter注入到Vue实例中
    this.$proxyData(this.data);
    // 1.1.6 调用Observer对象,把data变成客观擦者,监听data数据的变化
    new Observer(this.data)
    // 1.1.7 调用Compiler对象,解析指令和差值表达式
    new Compiler(this)
  }

  // 1.2 私有成员,把data中的属性,转换成getter/setter注入到Vue实例中
  $proxyData(data) {
    // 1.2.1 如果data不是对象不再执行后续操作
    if (!data || typeof data !== 'object') {
      return;
    }
    // 1.2.2 遍历data中的所有属性
    Object.keys(data).forEach(item => {
      // 1.2.3 使用Object.defineProperty()方法,劫持数据
      Object.defineProperty(data, item, {
        configurable: true,
        enumerable: true,
        get() {
          // 1.2.4 劫持数据
          return data[item];
        },
        set(val) {
          // 1.2.5 如果data的值与变更的值不一致,则更新data的值
          if (data[item] !== val) {
            data[item] = val;
          }
        }
      })
    })
  }
}

添加一个 Observer,做数据响应式的处理,包含数据的监听/劫持

// 2. 监听data,做数据响应式处理
class Observer {
 constructor(data) {
   this.deepDate(data)
 }
 //只针对对象数据进行响应式处理
 deepDate(data) {
   if (typeof data !== 'object') {
     return
   }
   // 遍历data数据
   Object.keys(data).forEach(key => {
     this.proxyData(data, key, data[key])
   })
 }
 // 将data数据转为getter/setter
 proxyData(data, key, value) {
   // 递归监听所有数据
   this.deepDate(value);
   // 暂存this
   let that = this;
   //收集依赖,发送通知
   let dep = new Dep();
   // 数据劫持
   Object.defineProperty(data, key, {
     configurable: true,
     enumerable: true,
     get() {
       if(Dep.target && dep.addSub(Dep.target)){
         return value;
       }
       
     },
     set(val) {
       if (value == val) return;
       // 赋值最新值
       value = val;
       that.deepDate(val);
       // 数据变化,发送通知
       dep.notify(key, val)
       
     }
   })
 }
}

创建一个 Dep 发布者,收集依赖并添加观察者,当有变更时,通知所有的观察者

// 3. 定义一个发布者
class Dep {
  constructor() {
    // 3.1 定义一个数组,记录所有的(观察者/订阅者)
    this.sub_arr = [];
  }
  /**
   * 添加观察者
   * @param {*} sub -- 观察者
   */
  addSub(sub) {
    // 3.2 每一个观察者都必须包含一个update方法
    if (sub && sub.update) this.sub_arr.push(sub);
  }
  /**
   * 通知所有观察者进行更新
   * @param { String} key -- 当前变更对象的 key 值
   * @param {*} val -- 当前变更的值
   */
  notify(key, val) {
    // 3.3 遍历观察者数据,通知所有观察者进行更新
    this.sub_arr.forEach(sub => sub.update(key, val))
  }
}

创建一个 Watcher 观察者,当收到发布者通知更新时,更新视图

// 4. 创建一个订阅者-观察者
class Watcher {
  constructor(vm, key, cb) {
    // 4.1 存储当前视图
    this.vm = vm;
    // 4.2 存储当前变更的key
    this.key = key;
    // 4.3 存储当前回调
    this.cb = cb;
    // 4.4 把Watcher对象记录到Dep的静态属性target上。触发get方法,在get中会调用addSub
    Dep.target = this;
    // 4.5 当获取vm[key]的时候会执行getter,记录数据变化之前的值
    this.oldValue = vm[key];
    // 4.6 当Watcher加到subs之后,重置Dep的target属性
    Dep.target = null;
  }
  /**
   * 添加更新方法
   * @param {*} key -- 当前data的key
   * @param {*} val -- 当前变更的值
   * @returns 
   */
  update(key, val) {
    // 4.7 如果当前更新值与之前的值相同,不做更新操作
    if (val == this.oldValue) return;
    // 4.8 执行Compile中绑定的回调,更新视图
    this.cb(key, val)
    // 4.9 更新值
    this.oldValue = val;
  }
}

定义一个 compiler 编译解析器,编译、更新视图

// 5. 定义一个compiler
class Compiler {
  // 5.1 构造函数
  constructor(vm) {
    // 5.1.1 储存当前实例
    this.vm = vm;
    // 5.1.2 存储DOM对象
    this.el = vm.$el;
    // 5.1.3 遍历DOM对象所有节点,如果是文本节点,解析差值表达式。如果是元素节点,解析指令。
    this.compile(this.el)
  }
  // 5.1.4 编译模板,处理文本节点和元素节点
  compile(el) {
    // 5.1.5 存储所有节点
    let childNodes = el.childNodes;
    // 5.1.6 节点属于伪数组需要通过Array.from()转换成真实数组
    Array.from(childNodes).forEach(node => {
      if (this.isTextNode(node)) {
        // 5.1.7 处理文本节点
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 5.1.8 处理元素节点
        this.compileElement(node)
      }
      // 5.1.9 判断node节点,是否有子节点,如果有,递归深度遍历
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 5.2 编译元素节点,处理指令
  compileElement(node) {
    // 5.2.1 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.name;
       // 5.2.2 判断是否是指令
      if (this.isDirective(attrName)) {
        attrName = attrName.substr(2);
        let key = attr.value;
        // 5.2.3 如果当前元素含有指令,则需要首次渲染指令对应的内容
        this.update(node, key, attrName)
      }
    })
  }
  // 5.3 更新视图
  update(node, key, attrName) {
    let updateFn = this[attrName + 'Update'];
    updateFn && updateFn.call(this, node, this.vm[key], key);
  }
  // 5.4 针对不同的指令编译/更新视图
  domUpdate(node, value, key) {
    node.textContent = value;
    new Watcher(this.vm, key, (k, nv) => {
      console.log('创建Watcher ,当数据改变更新视图' + nv)
      node.textContent = nv;
    })
  }

  // 5.5 处理v-model 指令
  modelUpdate(node, value, key) {
    node.value = value;
    new Watcher(this.vm, key, (k, nv) => {
      console.log('创建Watcher ,当数据改变更新视图' + nv)
      node.value = nv;
    })
    // 5.5.1 设置双向绑定事件
    node.addEventListener('input', e => this.vm[key] = node.value)
  }
  // 5.6 判断元素是否是指令
  isDirective(attrName) {
    //判断属性是否是v-开头
    return attrName.startsWith('v-');
  }
  // 5.7 判断是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }
  // 5.8 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
}

Vue3.0

vue3.0 数据响应式采用 ES6 的 proxy 特性进行数据拦截

proxyData = new Proxy(obj,
{
  // 获取
  get(data, key) {
    return Reflect.get(data, key);
  },
  // 修改
  set(data, key, value) {
    return Reflect.set(data, key, value)
  },
  // 删除
  deleteProperty(data, key) {
    return Reflect.deleteProperty(data, key)
  }
})