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

    正文概述 掘金(余居居)   2021-01-26   470

    众所周知,vue 2.x是通过Object.defineProperty来实现数据响应式的。那vue中具体是如何实现的呢?数据初始化的流程又是什么样的呢?还有数组又是如何实现响应式的呢?让我们从vue的初始化一步步分析,看看这其中到底经历了什么。

    vue的初始化

    构造函数

    源码路径:\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)
    }
    
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue
    

    上述代码为vue的构造函数,我们可以看到当我们new Vue()的时候会执行_init方法,该方法是通过initMixin方法混入进去的,我们进入到initMixin方法。

    initMixin

    源码路径:\src\core\instance\init.js

    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
    
        let startTag, endTag
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          startTag = `vue-perf-start:${vm._uid}`
          endTag = `vue-perf-end:${vm._uid}`
          mark(startTag)
        }
    
        // a flag to avoid this being observed
        vm._isVue = true
        // merge options
        if (options && options._isComponent) {
          initInternalComponent(vm, options)
        } else {
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        initLifecycle(vm) // 初始化生命周期
        initEvents(vm) // 初始化事件
        initRender(vm) // 插槽相关内容
        callHook(vm, 'beforeCreate') // 调用生命周期beforeCreate钩子
        initInjections(vm) // resolve injections before data/props
        initState(vm) // 初始化状态
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created') // 调用生命周期created钩子
    
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          vm._name = formatComponentName(vm, false)
          mark(endTag)
          measure(`vue ${vm._name} init`, startTag, endTag)
        }
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    我们可以看到这个方法只在vue构造函数的原型上增加了_init方法,而_init方法中调用了各种初始化方法,并且也能看到在生命周期钩子beforeCreate和created之间初始化provide、inject和初始化状态,我们今天要探讨的是数据响应式,我们只看数据相关的initState()就行了。

    initState

    源码路径:\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)
      }
    }
    

    initState方法从实例上获取用户传入的options,然后细分对options中的每一项进行初始化,到这里我们也终于看见了今天的目标initData()。

    initData

    源码路径:src\core\instance\state.js
    避免篇幅过长,这里我就不贴源码了,实际initData中只是判断了用户传入data的类型,如果类型是function则执行该函数得到object类型的data,否则直接返回data,然后调用observe函数传入data。这里就是响应式的部分了,我们接着往下看.

    vue数据的响应式

    observe

    源码路径:src\core\observer\index.js

    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
    }
    

    可以看到observe方法首先判断传入的value是否有__ob__属性(是否是响应式数据),是则取__ob__属性值,否则new Observer()并且传入了用户定义的data。vue中已经是响应式的数据会被添加上__ob__属性,此判断可避免重复监听。我们来看Observer类。

    源码路径:src\core\observer\index.js

    export class Observer {
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
          if (hasProto) {
            protoAugment(value, arrayMethods)
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          this.observeArray(value)
        } else {
          this.walk(value)
        }
      }
    
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
    
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    

    可以看到Observer类给传入的value定义了一个__ob__的属性,值为当前实例。并且对传入的value做了判断如果是Array调用this.observerArray进行处理,是Object的话则调用this.walk进行处理,我们先来看Object的情况。

    Object 的变化侦测

    上面的walk方法获取了object的所有key进行遍历,并且调用defineReactive传入objec和key进行变化侦测,而且通过函数名我们也知道这是定义响应式的方法。

    源码路径:src\core\observer\index.js

    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()
          }
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          //触发依赖
          dep.notify()
        }
      })
    }
    

    Tips:如果还有同学不知道Object.defineProperty方法怎么用的建议先去这里看一下。

    defineReactive方法利用了Object.defineProperty重新定义了get、set方法,并且在get中收集依赖、在set中触发依赖。通过get方法中的收集依赖逻辑可以看出:Dep.target存在则调用dep.depend方法收集依赖。而set方法中则调用dep.notify方法通知依赖更新。这样只能监听数据中的属性并不能监听子属性,因为data某个属性的值也可能是对象或数组,所以需要调用observe方法递归监听。这样我们就实现了对object的监听。我们再回过头来看数组是如何处理的。

    Array的变化侦测

    vue中数组并不是通过监听索引去实现变化侦测的,因为这样做当我们用list.push(1)向list数组中push一个1时,并不会触发get/set。而我们日常编程中会频繁使用Array原型上的方法去操作数组,所以object那种方法就不灵了。

    也正因为如此,vue中重写了数组原型上的方法,当我们使用数组原型方法改变数组时,向依赖发送通知即可。

    我们回过头来看Observe类的observerArray方法,只是遍历了传进来的数组,并且递归调用observe监听子项。而再调用observerArray方法之前则将value的原型指向了arrayMethods,我们来看下arrayMethods具体是怎么实现的。
    源码路径:src\core\observer\array.js

    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    
    const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']
    
    methodsToPatch.forEach(function (method) {
      // cache original method
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        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
        ob.dep.notify()
        return result
      })
    })
    

    首先保存了Array的原型,然后创建了arrayMethods的原型指向Array的原型,因为js中只有push、pop、shift、unshift、splice、sort、reverse这七个方法会改变原数组,所以只需要重写这七个方法就可以了。然后给arrayMethods添加这七个方法。在重写的方法里调用原来的方法对数组进行操作,然后手动通知依赖进行更新即可。因为push、unshift、splice可以给数组新增项,而新增项也可能是对象或数组所以调用observeArray方法对新增项进行变化侦测。

    上面我们频繁提到依赖、依赖收集、通知依赖更新,那什么是依赖呢?依赖又收集到哪里去呢?我们接着往下看。

    依赖收集到哪里?

    vue中封装了一个Dep类,用来管理依赖,使用这个类我们可以添加依赖、删除依赖、向依赖发送通知等,也被称为依赖收集器。
    源码路径:src\core\observer\dep.js

    export default class Dep {
      constructor () {
        this.id = uid++
        this.subs = []
      }
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
      removeSub (sub: Watcher) { // 删除依赖
        remove(this.subs, sub)
      }
      depend () { // 添加依赖
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
      notify () { // 通知依赖更新
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    

    看到这里你也就明白了,其实就是data的每一个key都有一个dep依赖收集器,依赖则收集在该dep的subs数组中。上面提到的收集依赖就是调用depend方法,但是我们通过上面代码发现貌似addSub方法才是将依赖添加到数组中的,这是怎么回事呢?因为dep和依赖是相互订阅的。depend方法中调用依赖的addDep方法将该dep保存到依赖中,而依赖的addDep方法中调用了dep.addSub将依赖保存到dep里。

    那什么是依赖呢?这里的Dep.target其实就是依赖也是watcher,watcher分为渲染watcher和用户watcher,vue是组件级别的更新,就是因为一个组件对应一个渲染watcher,它控制着当前组件的更新,但是并不会控制子组件的更新。而用户watcher就是watch选项和$watch、计算属性也是通过watcher实现的,这些我们以后再说,先来看渲染相关的watcher。

    依赖Watcher

    为了突出重点,这里代码精简过。
    源码路径:src\core\observer\watcher.js

    export default class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        if (isRenderWatcher) {
          vm._watcher = this
        }
        vm._watchers.push(this)
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.expression = process.env.NODE_ENV !== 'production'
          ? expOrFn.toString()
          : ''
        // parse expression for getter
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
          if (this.user) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
          } else {
            throw e
          }
        } finally {
          if (this.deep) {
            traverse(value)
          }
          popTarget()
          this.cleanupDeps()
        }
        return value
      }
    
      addDep (dep: Dep) {
        const id = dep.id
        if (!this.newDepIds.has(id)) {
          this.newDepIds.add(id)
          this.newDeps.push(dep)
          if (!this.depIds.has(id)) {
            dep.addSub(this)
          }
        }
      }
      
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    }
    

    当组件渲染时会执行new Watcher(),此时会调用Watcher类中的get方法,我们重点关注一下这里,首先调用了pushTarget将this传过去,这个方法就是设置Dep.target = this(watcher实例),然后执行this.getter方法,其实这里就会获取数据,从而触发data的get方法此时Dep.target存在值,依赖就会被收集。然后调用popTarget方法,就是设置Dep.target = undefined。这里还是很有意思的,在组件渲染时初始化一个Watcher实例,也就完成了对应的收集依赖。

    当数据改变时会触发set方法,而set方法中会调用该key对应的dep.notify通知watcher进行更新,通过notify方法,我们能看出就是循环subs取出其中的每一个watcher并调用其update方法。update方法将watcher推入一个队列进行异步更新。

    写在最后

    因为vue实现数据响应式的特点,也导致object、array有些操作是无法侦测到变化的。

    object的问题
    例如:我们使用obj.name='pdd'或者obj['name'] = 'pdd'向obj中新增一个属性,而vue.js无法侦测到变化,使用delete操作符删除obj一个属性时,vue.js同样无法侦测到变化。

    array的问题
    例如: 我们使用arr[0] = 2,即使用数组索引去改变数组某一项时,vue.js无法侦测到变化,使用arr.length = 0,去清空数组时也不能侦测到变化。

    VUE3采用了ES6 Proxy重写了响应式部分,不存在以上问题。但是VUE2.X为了解决这些问题提供了两个api:Vue.set 、Vue.delete。但是对于数组使用以上两个方法只能通过索引去操作,通过length去清空数组还是无法侦测到变化的。后面会单独写一篇关于常用的全局api的实现原理。

    以上就是VUE 2.X数据响应式的内容,希望小伙伴们能从本文中有所收获,三连等于学会(滑稽)。


    起源地下载网 » Vue2 响应式原理

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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