Skip to content
一、vue2.x 响应式系统的实现原理

MVVM 模式最核心的特性就是数据双向绑定,Vue 构建了一套响应式系统,可以实现侦测对象的变化,从而在数据发生变化时自动渲染视图。

vue 2.x 响应式实现原理 利用 Object.defineProperty() 方法作为对象添加 get() 和 set() 方法来侦测对象的变化,当获取对象属性值时会调用 get() 方法,当修改对象属性值时会调用 set(),于是可以 在 get() 和 set() 方法中添加代码,实现数据与视图的双向绑定。

  • 针对简单地侦测对象的属性变化

    js
    /**
     * 模拟视图更新
     */
    const updateView = () => {
      console.log('视图更新')
    }
    
    /**
     * 对 Object.defineProperty() 方法进行封装
     */
    const defineReactive = (obj, key, value) => {
      Object.defineProperty(obj, key, {
        get() {
          return value
        },
        set(newValue) {
          if (newValue !== value) {
            // 在 set()  方法中触发视图更新
            updateView();
            value = newValue
          }
        }
      })
    }
    
    /**
     * 对一个对象中所有属性的变化进行侦测
     */
    const observer = (target) => {
      // 如果不是对象数据类型,直接返回
      if (typeof target !== 'object') {
        return target
      }
      // 循环遍历对象的所有属性,并将它们转换为 getter 和 setter 形式
      for (const key in target) {
        defineReactive(target, key, target[key])
      }
    }
    
    // 调用
    let user = { name: '张三' };
    // 对 user 对象的所有属性变化进行侦测
    observer(user)
    user.name = '李四'

    存在一个问题,假如属性的值也是对象,更改这个对象属性的值,就监听不到了。

  • 当对象的属性也是对象类型,单纯针对这个值对象的属性进行改变

    js
    /**
     * 模拟视图更新
     */
    const updateView = () => {
      console.log('视图更新')
    }
    
    /**
     * 对 Object.defineProperty() 方法进行封装
     */
    const defineReactive = (obj, key, value) => {
      // 通过递归调用解决多层对象嵌套的属性侦测问题
      observer(value)
    
      Object.defineProperty(obj, key, {
        get() {
          return value
        },
        set(newValue) {
          if (newValue !== value) {
            // 在 set()  方法中触发视图更新
            updateView();
            value = newValue
          }
        }
      })
    }
    
    /**
     * 对一个对象中所有属性的变化进行侦测
     */
    const observer = (target) => {
      // 如果不是对象数据类型,直接返回
      if (typeof target !== 'object') {
        return target
      }
      // 循环遍历对象的所有属性,并将它们转换为 getter 和 setter 形式
      for (const key in target) {
        defineReactive(target, key, target[key])
      }
    }
    
    // 调用
    let user = { name: '张三', company: { companyName: '亚信科技' } };
    // 对 user 对象的所有属性变化进行侦测
    observer(user)
    user.company.companyName = '阿里巴巴'

    通过在 defineReactive 方法中 递归调用 observer 方法

  • 当对象的属性也是对象类型,赋值新的对象,再改变新对象的属性值

    js
    /**
     * 模拟视图更新
     */
    const updateView = () => {
      console.log('视图更新')
    }
    
    /**
     * 对 Object.defineProperty() 方法进行封装
     */
    const defineReactive = (obj, key, value) => {
      // 通过递归调用解决多层对象嵌套的属性侦测问题
      observer(value)
    
      Object.defineProperty(obj, key, {
        get() {
          return value
        },
        set(newValue) {
          if (newValue !== value) {
            // 如果 newValue 是对象类型,则继续侦测该对象的所有属性变化
            observer(newValue)
            // 在 set()  方法中触发视图更新
            updateView();
            value = newValue
          }
        }
      })
    }
    
    /**
     * 对一个对象中所有属性的变化进行侦测
     */
    const observer = (target) => {
      // 如果不是对象数据类型,直接返回
      if (typeof target !== 'object') {
        return target
      }
      // 循环遍历对象的所有属性,并将它们转换为 getter 和 setter 形式
      for (const key in target) {
        defineReactive(target, key, target[key])
      }
    }
    
    // 调用
    let user = { name: '张三', company: { companyName: '亚信科技' } };
    // 对 user 对象的所有属性变化进行侦测
    observer(user)
    user.company = { companyName: '阿里巴巴' }
    user.company.companyName = '京东'
  • 数组侦测

    • 现象

      js
      // 调用
      let user = { name: '张三', company: { companyName: '亚信科技' }, emails: ['163@qq.com'] };
      // 对 user 对象的所有属性变化进行侦测
      observer(user)
      user.emails.push('zhangsan@qq.com')

      emails 属性是数组类型,当通过 push() 方法改变数组内容时,并不会触发对象的 set() 方法的调用。

    • 解决方案

      替换数组的原型对象

      js
      const arrayPrototype = Array.prototype
      // 使用数组的原型对象创建一个新对象
      const proto = Object.create(arrayPrototype)
      
      /**
       * 模拟视图更新
       */
      const updateView = () => {
        console.log('视图更新')
      }
      
      // 改变数组自身内容的方法只有如下7个,对它们进行拦截
      ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
        Object.defineProperty(proto, method, {
          get() {
            // 在 get()  方法中触发视图更新
            updateView();
            return arrayPrototype[method]
          },
        })
      })
      
      /**
       * 对 Object.defineProperty() 方法进行封装
       */
      const defineReactive = (obj, key, value) => {
        // 通过递归调用解决多层对象嵌套的属性侦测问题
        observer(value)
      
        Object.defineProperty(obj, key, {
          get() {
            return value
          },
          set(newValue) {
            if (newValue !== value) {
              // 如果 newValue 是对象类型,则继续侦测该对象的所有属性变化
              observer(newValue)
              // 在 set()  方法中触发视图更新
              updateView();
              value = newValue
            }
          }
        })
      }
      
      /**
       * 对一个对象中所有属性的变化进行侦测
       */
      const observer = (target) => {
        // 如果不是对象数据类型,直接返回
        if (typeof target !== 'object') {
          return target
        }
      
        if (Array.isArray(target)) {
          // 如果 target 是数组,则将数组的原型对象设置为 proto
          Object.setPrototypeOf(target, proto);
          // 对数组中的元素进行侦测
          for (let i = 0, len = target.length; i < len; i++) {
            observer(target[i])
          }
          return
        }
      
        // 循环遍历对象的所有属性,并将它们转换为 getter 和 setter 形式
        for (const key in target) {
          defineReactive(target, key, target[key])
        }
      }
      
      // 调用
      let user = { name: '张三', company: { companyName: '亚信科技' }, emails: ['163@qq.com'] };
      // 对 user 对象的所有属性变化进行侦测
      observer(user)
      user.emails.push('zhangsan@qq.com')

Released under the MIT License.