• 【Vue2.x源码系列01】响应式原理


    Vue.js 基本上遵循 MVVM(Model–View–ViewModel)架构模式,数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。 本文讲解一下 Vue 响应式系统的底层细节。

    检测变化注意事项

    Vue 2.0中,是基于 Object.defineProperty 实现的响应式系统 (这个方法是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因)
    vue3 中,是基于 Proxy/Reflect 来实现的

    1. 由于 JavaScript 的限制,这个 Object.defineProperty() api 没办法监听数组长度的变化,也不能检测数组和对象的新增变化。
    2. Vue 无法检测通过数组索引直接改变数组项的操作,这不是 Object.defineProperty() api 的原因,而是尤大认为性能消耗与带来的用户体验不成正比。对数组进行响应式检测会带来很大的性能消耗,因为数组项可能会大,比如1000条、10000条。

    响应式原理

    响应式基本原理就是,在 Vue 的构造函数中,对 options 的 data 进行处理。即在初始化vue实例的时候,对data、props等对象的每一个属性都通过 Object.defineProperty 定义一次,在数据被set的时候,做一些操作,改变相应的视图。

    数据观测

    让我们基于 Object.defineProperty 来实现一下对数组和对象的劫持。

    import { newArrayProto } from './array'
    
    class Observer {
      constructor(data){
        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]))
      }
      // 观测数组
      observeArray(data) {
        data.forEach(item => observe(item))
      }
    }
    
    function defineReactive(data,key,value){
      observe(value)  // 深度属性劫持,对所有的对象都进行属性劫持
    
      Object.defineProperty(data,key,{
        get(){
          return value
        },
        set(newValue){
          if(newValue == value) return
          observe(newValue) // 修改属性之后重新观测,目的:新值为对象或数组的话,可以劫持其数据
          value = newValue
        }
      })
    }
    
    export function observe(data) {
      // 只对对象进行劫持
      if(typeof data !== 'object' || data == null){
        return
      }
      return new Observer(data)
    }

    重写数组7个变异方法

    7个方法是指:push、pop、shift、unshift、sort、reverse、splice。(这七个都是会改变原数组的)

    实现思路:面向切片编程!!!

    不是直接粗暴重写 Array.prototype 上的方法,而是通过原型链继承与函数劫持进行的移花接木。

    利用 Object.create(Array.prototype) 生成一个新的对象 newArrayProto,该对象的 __proto__指向 Array.prototype,然后将我们数组的 __proto__指向拥有重写方法的新对象 newArrayProto,这样就保证了 newArrayProto 和 Array.prototype 都在数组的原型链上。

    arr.__proto__ === newArrayProto;newArrayProto.__proto__ === Array.prototype

    然后在重写方法的内部使用 Array.prototype.push.call 调用原来的方法,并对新增数据进行劫持观测。

    let oldArrayProto = Array.prototype // 获取数组的原型
    
    export let newArrayProto = Object.create(oldArrayProto)
    
    // 找到所有的变异方法
    let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
    
    methods.forEach(method => {
      // 这里重写了数组的方法
      newArrayProto[method] = function (...args) {
        // args reset参数收集,args为真正数组,arguments为伪数组
        const result = oldArrayProto[method].call(this, ...args) // 内部调用原来的方法,函数的劫持,切片编程
    
        // 我们需要对新增的数据再次进行劫持
        let inserted
        let ob = this.__ob__
    
        switch (method) {
          case 'push':
          case 'unshift': // arr.unshift(1,2,3)
            inserted = args
            break
          case 'splice': // arr.splice(0,1,{a:1},{a:1})
            inserted = args.slice(2)
          default:
            break
        }
    
        if (inserted) {
          // 对新增的内容再次进行观测
          ob.observeArray(inserted)
        }
        return result
      }
    })

    增加__ob__属性

    这是一个恶心又巧妙的属性,我们在 Observer 类内部,把 this 实例添加到了响应式数据上。相当于给所有响应式数据增加了一个标识,并且可以在响应式数据上获取 Observer 实例上的方法

    class Observer {
      constructor(data) {
        // data.__ob__ = this // 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了
        Object.defineProperty(data, '__ob__', {
          value: this,
          enumerable: false, // 将__ob__ 变成不可枚举 (循环的时候无法获取到,防止栈溢出)
        })
    
        if (Array.isArray(data)) {
          // 这里我们可以重写可以修改数组本身的方法 7个方法,切片编程:需要保留数组原有的特性,并且可以重写部分方法
          data.__proto__ = newArrayProto
          this.observeArray(data) // 如果数组中放的是对象 可以监控到对象的变化
        } else {
          this.walk(data)
        }
      }
    
    }

    __ob__有两大用处:

    1. 如果一个对象被劫持过了,那就不需要再被劫持了,要判断一个对象是否被劫持过,可以通过__ob__来判断
    // 数据观测
    export function observe(data) {
      // 只对对象进行劫持
      if (typeof data !== 'object' || data == null) {
        return
      }
    
      // 如果一个对象被劫持过了,那就不需要再被劫持了 (要判断一个对象是否被劫持过,可以在对象上增添一个实例,用实例的原型链来判断是否被劫持过)
      if (data.__ob__ instanceof Observer) {
        return data.__ob__
      }
    
      return new Observer(data)
    }
    1. 我们重写了数组的7个变异方法,其中 push、unshift、splice 这三个方法会给数组新增成员。此时需要对新增的成员再次进行观测,可以通过__ob__调用 Observer 实例上的 observeArray 方法

    __EOF__

  • 本文作者: 柏成
  • 本文链接: https://www.cnblogs.com/burc/p/17251352.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    Intel确认酷睿第14代是最后一代酷睿i系列, 以后是酷睿Ultra的时代
    3、传统的Synchronized锁
    (附源码)计算机毕业设计SSM基于的楼盘销售系统
    带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法
    L44.linux命令每日一练 -- 第七章 Linux用户管理及用户信息查询命令 -- su和visudo
    热点探测技术架构设计与实践
    一文讲清楚Java面向对象的继承关系
    mysql学习笔记2:如何避免数据库乱码
    Springboot监控
    python写一个文本处理器
  • 原文地址:https://www.cnblogs.com/burc/p/17251352.html