最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 重学Vue【响应式对象】

    正文概述 掘金(道道里)   2021-03-10   504

    重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。

    正文

    Vue.js实现响应式原理的核心是利用ES5的 Object.defineProperty,而 IE8 以下是没有这个东西的,所以这也就是为什么Vue.js不能兼容IE8及以下的原因。

    Object.defineProperty

    Object.defineProperty 会在一个对象上定义一个属性,或者修改一个现有属性,并返回这个对象,它的用法如下:

    Object.defineProperty(obj, prop, descriptor)
    

    Obj 参数是要定义属性的对象,prop 是定义或修改的属性名称,descriptor 是将被定义或修改的描述符。

    使用这种方式来操作对象的时候,最关键的就是 getsetget 是给一个属性提供的 getter 方法,在访问对象的属性的时候使用,set 是给一个属性提供的 setter 方法,在修改对象的属性的时候使用(这块可以看重学JavaScript【对象的结构、创建和继承关系】)。

    一旦对象有了 gettersetter,就可以简单的把该对象理解为 响应式对象,在Vue.js里被定义成响应式对象的对象,有 initStateinitPropsinitData

    initState

    在Vue初始化的时候有一个 _init 方法,里面有一个 initState

    Vue.prototype._init = function (options?: Object) {
      // ...
      iniitState(vm)
      // ...
    }
    

    这个方法的作用是初始化了 propsdatamethodscomputedwatcher 等,它的定义在 src/core/instance/state.js 里:

    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    

    这里重点关注一下 propsdata

    initProps

    initProps 的定义也在 src/core/instance/state.js 里:

    function initProps (vm: Component, propsOptions: Object) {
      const propsData = vm.$options.propsData || {}
      const props = vm._props = {}
      // cache prop keys so that future props updates can iterate using Array
      // instead of dynamic object key enumeration.
      const keys = vm.$options._propKeys = []
      const isRoot = !vm.$parent
      // root instance props should be converted
      if (!isRoot) {
        toggleObserving(false)
      }
      for (const key in propsOptions) {
        keys.push(key)
        const value = validateProp(key, propsOptions, propsData, vm)
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          const hyphenatedKey = hyphenate(key)
          if (isReservedAttribute(hyphenatedKey) ||
              config.isReservedAttr(hyphenatedKey)) {
            warn(
              `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
              vm
            )
          }
          defineReactive(props, key, value, () => {
            if (vm.$parent && !isUpdatingChildComponent) {
              warn(
                `Avoid mutating a prop directly since the value will be ` +
                `overwritten whenever the parent component re-renders. ` +
                `Instead, use a data or computed property based on the prop's ` +
                `value. Prop being mutated: "${key}"`,
                vm
              )
            }
          })
        } else {
          defineReactive(props, key, value)
        }
        // static props are already proxied on the component's prototype
        // during Vue.extend(). We only need to proxy props defined at
        // instantiation here.
        if (!(key in vm)) {
          proxy(vm, `_props`, key)
        }
      }
      toggleObserving(true)
    }
    

    props 的初始化过程,主要就是遍历定义的 props 配置,在遍历期间调用了一个 defineReactive 函数,这个函数就是把传入的 props 对象上的 key 变成一个响应式的,然后通过 vm._props.xxx 就可以访问到定义 props 中对应的属性,该方法在下面有分析。在下面还使用了一个 proxy,这个 proxy 之前也分析过,这里就可以通过 proxyvm._props.xxx 的访问代理到 vm.xxx 上,下面还会再分析一下它。

    initData

    initData 的定义也在 src/core/instance/state.js 里:

    function initData (vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      const keys = Object.keys(data)
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else if (!isReserved(key)) {
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      observe(data, true /* asRootData */)
    }
    

    data 的初始化也是做两件事,首先对定义 data 函数返回的对象进行一次遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性。

    不管是 props 还是 data,它们的初始化都是把它们变成一个响应式对象,在这个过程中会走几个函数,下面来具体分析一下。

    proxy

    在new Vue发生了什么事情文章里有分析过它的作用,这里再提一下: proxy 定义了 getset,通过 Object.defineProperty 在参数 target(就是vm) 上定义了 _data 属性,从而把我们常写的 this.xx 代理到 this._data.xx 上(也就是代理到实例上,可以理解为 vm._data.xx),这样就可以在 data 或者 methods 里拿到并且使用 xx

    上面是对 data 的,对于 props 而言也一样,对 vm._props.xxx 的读写就变成了 vm.xxx 的读写,而对于 vm._props.xxx 我们可以访问到定义在 props 中的属性,所以我们就可以通过 vm.xxx 访问到定义在 props 中的 xxx 属性了。

    observe

    observe 的功能就是用来监测数据变化的,它的定义在 src/core/observer/index.js 中:

    /**
     * Attempt to create an observer instance for a value,
     * returns the new observer if successfully observed,
     * or the existing observer if the value already has one.
     */
    export function observe (value: any, asRootData: ?boolean): Observer | void {
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob: Observer | void
      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
    }
    

    如果 value 不是一个对象,并且是一个VNode,就直接返回。接着判断 valu e有没有 __ob__ 属性,并且它是一个 Observer 的实例的话,就返回这个 __ob__

    下一个判断有一个 shouldObserve 布尔值,它有一个改变值的方法:

    export let shouldObserve: boolean = true
    
    export function toggleObserving (value: boolean) {
      shouldObserve = value
    }
    

    这个方法在上面的 initProps 上调用了一次:

    // root instance props should be converted
    if (!isRoot) {
      toggleObserving(false)
    }
    

    注释上说:根的props应该需要观测,所以它的逻辑里,如果不是root就设置为false,那也就走不到 ob = new Observer(value) 这个逻辑了,这样就决定了:非根props是不会执行 new Observer 的,也就不会变成 Observer 的实例,所以这个 shouldObserve 就是控制要不要变成 Observer 实例的。

    整体来看的话, observe 的作用就是给非VNode的对象数据添加一个 Observer,如果已经添加过就直接返回,否则满足一些条件的话,就实例化一个 Observer 对象实例。

    Observer

    Observer 的定义是这样的:

    
    /**
     * Observer class that is attached to each observed
     * object. Once attached, the observer converts the target
     * object's property keys into getter/setters that
     * collect dependencies and dispatch updates.
     */
    export class Observer {
      value: any;
      dep: Dep;
      vmCount: number; // number of vms that has this object as root $data
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value)
        } else {
          this.walk(value)
        }
      }
    
      /**
       * Walk through each property and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
    
      /**
       * Observe a list of Array items.
       */
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    

    它可以理解为定义了一个观察者的类,在每次 new 它的时候,都会有一个 value 值,会实例化一个 dep,会有一个计数的 vmCount 等等,然后会调用 def 函数,这个 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
      })
    }
    

    也就是封装了一下 Object.defineProperty

    这里调用 def 的目的是,给 value 添加一个 __ob__ 属性,并且这个属性指向了当前实例,目的是第一次定义了它之后,在接下来后面调用 observe 的话,进行到 hasOwn(value, '__ob__') 判断的时候,可以直接返回当前实例。

    接着判断 value 是数组的话就执行 observeArray 方法(递归数组元素,观察每一个元素),否则就是对象,就执行 walk 方法(遍历每一个键,从而观察它的值)。

    这里再分析一下在 Observerconstructor 里,为什么它要调用 def__ob__ 指向 this,而不是直接 value.__ob__ = this

    因为如果 value 是一个对象,就会走 walk,如果用直接赋值的方式(就是 value.__ob__ = this)的话,那 walk 就会遍历这个 __ob__,然后执行 defineReactive,而我们不希望它走这一步(因为没必要,我们也不会手动去修改这个 __ob__),所以使用了 def 方法,然后传的最后一个参数 enumerable 没传,也就是false,也就是不可枚举,这样就不会遍历 __ob__ 属性了。

    defineReactive

    最后再来分析一下 defineReactive 是如何把参数 obj 变成响应式的,它的定义在 src/core/observer/index.js 中:

    /**
     * Define a reactive property on an Object.
     */
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
    
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
      })
    }
    

    通过 Object.getOwnPropertyDescriptor 拿到属性的定义,如果该属性的 configurable 是false,就什么都不做。然后尝试拿到该属性的原生 getset,如果没有 get,有 set,并且传入了2个参数(其实就是通过walk调用的话),就直接拿默认值。接着如果对象的值是一个对象的话,就递归调用 observe,然后把该对象重写 getsetget 主要做的就是依赖收集, set 主要做的就是派发更新。这两个概念在后两篇会详细说一下。


    起源地下载网 » 重学Vue【响应式对象】

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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