最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [Vue源码]Vue是怎么对数组的变更方法进行增强的

    正文概述 掘金(村上小树)   2021-02-11   577

    前言

    Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()

    已知Vue通过data中的数据进行递归遍历,然后用Object.defineProperty对其设置存取描述符从而达到响应式。然而其实在上述步骤时,是针对数据类型是对象的变量时采取的方式。

    在针对数据类型为数组的数据,Vue会采用另一种处理方式,在给数组中的元素设置响应式的同时,给该数组的变更方法进行响应式增强。下面来分析一下,Vue是如何对数组进行处理的:

    Observer类

    首先要知道,Vue针对每个data都用Observer类进行处理,我们先从Vue的构造函数出发,分析流程是如何走到Obeserver这一步的。

    src\core\instance\index.js

    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    // 注册vm的_init()方法
    initMixin(Vue)
    // 注册vm的$data/$props/$set/$delete/$watch
    stateMixin(Vue)
    // 事件相关 $on/$once/$off/$emit
    eventsMixin(Vue)
    // 初始化声明周期相关的混入方法
    lifecycleMixin(Vue)
    // $nextTick/_render
    renderMixin(Vue)
    
    export default Vue
    

    构造函数可知,初始化时会调用实例的_init方法,而该方法在initMixin函数中设置的,接下来看initMixin函数。

    src\core\instance\init.js

    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
    	// ... 省略
        // vm状态的初始化
        initState(vm)
    	// ... 省略
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    这个函数代码过多我直接省略不涉及到响应式处理的,initState方法用于处理vm.$options的属性,就是props,data,methods那些,接下来看一下initState方法的内部逻辑。

    src\core\instance\state.js

    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      // 处理props
      if (opts.props) initProps(vm, opts.props)
      // 处理methods
      if (opts.methods) initMethods(vm, opts.methods)
      // 处理data
      if (opts.data) {
        // 把组件中data的成员注入到实例中,且转换成响应式
        initData(vm)
      } else {
        // 初始化vm._data且把其转换为响应式,由此看出observe函数为响应式处理函数
        observe(vm._data = {}, true /* asRootData */)
      }
      // 处理computed
      if (opts.computed) initComputed(vm, opts.computed)
      // 处理watch
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    

    从上可知,处理data时,如果vm.$options.data不为空,则改用initData方法处理。否则把vm._data置为空对象且调用observe方法,该方法用于把传入的形参置为响应式。其实在initData函数的逻辑里,到最后也是调用observe方法。可见observe方法就是响应式处理函数。接下来再看observe方法的内部逻辑。

    src\core\observer\index.js

    export function observe (value: any, asRootData: ?boolean): Observer | void {
      // 如果传入的value不是一个对象或者是VNode的实例,则直接返回
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      // ob用于存放observer变量
      let ob: Observer | void
      // 如果value中有__ob__属性(observer对象)且该属性为Observer的实例
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else if ( // 判断是否可以进行响应式处理
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }
    

    可见,经过条件判断后初始化Observer实例并把value传入到构造函数中。

    到目前为止,从初始化Vue实例到初始化Observer实例的整个过程可以总结为下:

    [Vue源码]Vue是怎么对数组的变更方法进行增强的

    如何做到数组增强

    终于到文章的核心了,现在从Observer类的内部逻辑进行分析:

    src\core\observer\index.js

    import { arrayMethods } from './array'
    
    const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
    
    export class Observer {
      // 要置为响应式的数据
      value: any;
      // 依赖对象
      dep: Dep;
      // 实例计数器
      vmCount: number; // number of vms that have this object as root $data
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        // 通过Object.defineProperty把实例挂载到value.__ob__中
        def(value, '__ob__', this)
        if (Array.isArray(value)) { // value为数组的情况下的处理
          // 当浏览器支持访问__proto__属性时
          if (hasProto) {
            // 通过原型继承改变在数组的原型属性,让其__proto__指向arrayMethods
            protoAugment(value, arrayMethods)
          } else {
            // 利用Object.defineProperty覆盖数组中的方法
            copyAugment(value, arrayMethods, arrayKeys)
          }
          this.observeArray(value)
        } else { // value为对象的情况下的处理
          // 遍历对象中的属性,将其添加存取描述符(get/set)
          this.walk(value)
        }
      }
    
      // 把obj以及obj中的属性设置为响应式,非文章重点不展示细节
      walk (obj: Object) {}
    
      // 把数组中的元素通过observe方法置为响应式
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    

    处理数组用到的两个方法protoAugmentcopyAugment都用到外部引入的arrayMethods。这里先不看protoAugmentcopyAugment的内部逻辑,先分析arrayMethods涉及到的文件的源码:

    src\core\observer\array.js

    const arrayProto = Array.prototype
    // 使用数组的原型创建一个新的对象
    export const arrayMethods = Object.create(arrayProto)
    // 会修改数组的方法,arrayMethods中的原型会对这些方法进行增强,从而达到响应式的效果
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    
    methodsToPatch.forEach(function (method) {
      // cache original method
      // 根据方法名获取Array.prototype中的数组原方法且暂存下来
      const original = arrayProto[method]
      // 调用Object.defineProperty重新定义修改数组的方法
      def(arrayMethods, method, function mutator (...args) {
        // 原始方法
        const result = original.apply(this, args)
        // 获取数组的observer
        const ob = this.__ob__
        // 存储部分新增元素的方法的形参中传进来的新增的元素
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        // 对插入的新元素,重新遍历数组元素设置为响应式数据
        if (inserted) ob.observeArray(inserted)
        // notify change
        // 调用了修改数组的方法后,通知dep触发依赖
        ob.dep.notify()
        return result
      })
    })
    

    其中,def方法代码如下:

    /**
     * Define a property.
     */
    export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
      })
    }
    

    从上可知,通过遍历methodsToPatch中的字符串元素,把这些字符串作为属性名通过def方法定义到arrayMethods中,传入数据描述符value统一为mutator方法。mutator方法主要做了三件事:

    1. 先执行数组原方法
    2. 若是新增元素,则把新增的元素置为响应式
    3. 通知dep触发页面更新

    回到Observer类中继续分析,如果hasProto为真,即'__proto__' in {}为真,则使用protoAugment方法使value__proto__指向arrayMethodsprotoAugment代码如下:

    function protoAugment (target, src: Object) {
      target.__proto__ = src
    }
    

    如果hasProto为假,则使用copyAugmentarrayMethods中的方法都定义到value上。copyAugment代码如下:

    function copyAugment (target: Object, src: Object, keys: Array<string>) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
      }
    }
    

    看到这里,应该基本清楚数组增强是一个怎样的过程,把上面的流程总结为一张图,如下:

    [Vue源码]Vue是怎么对数组的变更方法进行增强的

    后记

    本文持续未完,等我看到Vue3的源码后,会把Vue3中的响应式处理与Vue2的作对比,把这篇文章继续拓展下去。


    起源地下载网 » [Vue源码]Vue是怎么对数组的变更方法进行增强的

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元