Vue源码解析02-数据响应式

Vue源码解析02-数据响应式

开篇之前先了解几个相关概念

MVC模式

模式简介

MVC的全称是Model(模型)-View(视图)-Controller(控制器)

MVC

  • Model:这是数据层,存储项目所需的数据。Model的作用是返回或者更新数据。在应用中常用于数据库存储数据。

  • View:视图层,用于想用户显示数据。View本身不显示任何数据,而是Controller或者Model让View显示数据。

  • Controller:控制层,MVC的核心部分。控制器相当于用户和系统的链接,接收用户的输入,处理完成后,要求Model更新数据。

MVP模式

MVP模式将Controller更名为Presenter,同时改变了通讯方向。全称是Model(模型)-Presenter(呈现器)-View(视图)。

MVP

  • MVP中的数据通信均是双向的。

  • Model:数据层。

  • View:视图层。

  • Presenter:Presenter层。Pressenter作为View和Model的中间层起到了桥梁的作用。Presenter从Model层获取数据通过接口发送给View层展示。View层将用户操作发送给Presenter,借由Presenter将数据发送给Model进行数据更新。

MVVM模式

MVVP模式将Presenter更名为View-Model(对应MVC中的C-Controller),基本上于MVP模式一致。但是MVVM采用的是双向数据绑定,View的变动自动反应到ViewModel上。

MVVM

  • Model:数据层。

  • View:视图层。

  • ViewModel:在vue中指的是vue的实例对象,是一个公开公共属性和命令的抽象view;

  • View中的变化会自动更新到ViewModel,ViewModel的变化也会自动反应到View视图中。这种自动更新是通过vue中的Observer观察者实现的。

Vue的双向数据绑定原理

数据双向绑定的意思是view的变化能反应到ViewModel中,ViewModel中的变化能同步更新View视图。

Vue双向数据绑定的原理是数据劫持+订阅发布模式实现

数据劫持

数据劫持指的是在访问或者修改某个属性时,通过Object.defineProperty()或者Proxy对象拦截这个行为,扩展额外的操作和行为然后返回结果。

  • Vue2中使用的是Object.defineProperty(),Vue3中使用的是Proxy对象的方式。

订阅发布模式

定义:对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有与该对象产生依赖关系的对象都会接收到通知。

订阅发布模式

  • 优点:耦合性低,便于维护

  • 缺点:创建订阅者可能会消耗一定时间和内存,但是订阅事件不一定会发生,订阅者则会一直存在于内存中。

Vue的双向数据绑定源码解析

源码分析入口点

在之前的源码分析中我们知道,Vue的构造函数中实现了一个_init()方法,这个方法是用来初始化一些选项的:

    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')
        }
        //这里初始化了Vue传入的选项
        this._init(options)
    }

上面的_init(options)方法的实现在src/core/instance/init.js中的initMixin(Vue)中,分析其中的源码,我们可以根据方法名称做一下简单的判断:

    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) {
            // optimize internal component instantiation
            // since dynamic options merging is pretty slow, and none of the
            // internal component options needs special treatment.
            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)
            // 初始化渲染器$slots scopedSlots、_c、$createElement
            initRender(vm)
            // 调用生命周期钩子函数
            callHook(vm, 'beforeCreate')
            // 获取注入的数据
            initInjections(vm) // resolve injections before data/props
            // 初始化状态props、methods、data、computed、watch
            initState(vm)
            // 提供数据
            initProvide(vm) // resolve provide after data/props
            // 调用生命周期钩子函数
            callHook(vm, '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)
            }

            // 如果存在el则执行$mount
            if (vm.$options.el) {
            vm.$mount(vm.$options.el)
            }
        }
    }

通过分析initMixin(Vue)我们可以得到一个大概的结果,那就是在Vue初始化的时候我们完成了生命周期、事件、渲染器、状态的初始化,同时还获取了祖先组件注入的数据,同时为后代组件的注入提供了数据。
其中initState(vm)方法则是对数据还有其他东西的一些初始化操作,跳入该方法中查看一下其内容src/core/instance/state.js

  • 我们逐行分析一下该方法的实现把:
    export function initState (vm: Component) {
        // 在当前实例中创建了一个watcher的空数组
        vm._watchers = []
        // 保存了当前Vue实例的选项options
        const opts = vm.$options
        // 如果选项中的props存在的化则初始化props
        if (opts.props) initProps(vm, opts.props)
        // 如果选项中methods存在则初始化方法
        if (opts.methods) initMethods(vm, opts.methods)
        // 如果选项中的data存在则进行data的初始化操作
        // data的处理,响应化处理
        if (opts.data) {
            // 一般情况下我们初始化Vue实例的时候都会传入data,所以大部分情况是走这个方法的
            // 初始化data
            initData(vm)
        } else {
            // 数据响应化
            observe(vm._data = {}, true /* asRootData */)
        }
        // 初始化computed
        if (opts.computed) initComputed(vm, opts.computed)
        if (opts.watch && opts.watch !== nativeWatch) {
            // 初始化watch
            initWatch(vm, opts.watch)
        }
    }

初始化数据initData

分析上面这段代码我们可以知道,在initState(vm)中是对数据进行了初始化的(先考虑传入data的情况),那么我们继续顺着代码往下看:

    function initData (vm: Component) {
        // 取出data
        let data = vm.$options.data
        // 判断data是否为方法,如果是一个方法,则处理完毕后返回
        data = vm._data = typeof data === 'function'
            ? getData(data, vm)
            : data || {}
            //如果data不是一个object则警告
        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
        // 分别取出data的key,选项中的props、methods
        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 */)
    }
  • 上面代码,我们发现了一个核心的方法:observe(),这个方法曾在initState()中出现过,所以,我们初始化数据的方法归根结底会落在observe()上面代码,我们发现了一个核心的方法:observe

observe:返回一个Observer

废话不多说,直接上代码:from src/core/observer/index.js

    //该方法,接收了vue实例中的data数据,和一个boolean,返回了一个Observer(观察者)实例
    export function observe (value: any, asRootData: ?boolean): Observer | void {
        //如果data数据不是一个对象或者是一个虚拟domVNode,直接结束
        if (!isObject(value) || value instanceof VNode) {
            return
        }
        let ob: Observer | void
        // 如果当前对象已经存在observer则返回
        if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
            // 已经存在的Observer则会保存在value.__ob__中
            ob = value.__ob__
        } else if (
            shouldObserve &&
            !isServerRendering() &&
            (Array.isArray(value) || isPlainObject(value)) &&
            Object.isExtensible(value) &&
            !value._isVue
        ) {// 否则,我们要新创建一个Observer将其返回
            ob = new Observer(value)
        }
        if (asRootData && ob) {
            ob.vmCount++
        }
        // 无论结果如何,最终都会返回一个observer
        return ob
    }
  • 通过分析上述代码我们可以得出一个结论**observe()**的作用就是返回一个Observer的实例。

Observer:判断data类型做处理

所以接下来我们要看一下Observer的构造方法完成了什么事情fromsrc/core/observer/index.js

    //Observer的构造方法
    // 接收传入的Vue中的data数据
    constructor (value: any) {
        this.value = value
        // 新建了一个Dep的实例,这个Dep是用来做依赖收集的,后面会用到
        this.dep = new Dep()
        //当前的vmCount数量
        this.vmCount = 0
        // 给当前data对象定义了一个__ob__属性
        def(value, '__ob__', this)
        // 判断当前对象是否为数组,如果是数组单独处理,
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods)
            } else {
                copyAugment(value, arrayMethods, arrayKeys)
            }
            this.observeArray(value)
        } else {// 如果不是数组的话
            //普通对象则用walk遍历
            this.walk(value)
        }
    }

    walk (obj: Object) {
        // 拿出obj(data对象)的key
        const keys = Object.keys(obj)
        // 对所有的key进行遍历
        for (let i = 0; i < keys.length; i++) {
            // 实现数据响应式
            // 传入data对象和当前key,进行响应化处理
            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])
        }
    }
  • 分析Observer的构造函数,我们知道了Observer的核心功能:判断data对象是不是一个数组,根据判断进行不同的响应化处理

当data为对象时

  • 当data为对象时,walk()方法遍历data的key,执行defineReactive()方法,下面看一下defineReactive方法的实现,在src/core/observer/index.js
export function defineReactive (
  obj: Object,// 接收一个object,就是vue实例的data
  key: string,// data的key
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 这里传进来的是data对象,所以没有一个data对象就有一个dep与之对应
  const dep = new Dep()

// 查看对象上是否有该属性,如果没有则停止执行
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

// 获取属性的getter和setter
  // 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
      // 依赖收集,这里的Dep.target是watcher的实例
      if (Dep.target) {
        dep.depend()// 追加依赖关系,简单来说就是将watcher加入到dep中,但是实际操作要复杂一点
        // 如果childOb存在,说明该属性是一个对象
        if (childOb) {
          // 继续追加依赖
          childOb.dep.depend()
          // 如果是数组,继续处理
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
        // 如果getter存在则调用getter否则返回当前val
      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
      // 如果setter存在则执行setter执行更新,否则用新值覆盖老值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        // 更新本地的val
        val = newVal
      }
      // 如果新传入的val是一个数组的化,则递归进行响应话处理observe
      childOb = !shallow && observe(newVal)
      // 通知更新
      dep.notify()
    }
  })
}
  • 简单总结一下defineReactive()
    ♣ 通过Object.defineProperty方法对data属性的set和get方法进行数据劫持
    ♣ 创建Dep实例,每有一个data属性则有一个dep与之对应
    ♣ 扩展了data属性的get方法,将Dep.target静态属性中的watcher加入到dep实例中(依赖收集过程)
    ♣ 扩展了data属性中的set方法,当数据被更新时,执行dep.notify()方法通知数据更新

  • 所以我们顺着代码看一下依赖收集的过程和通知更新的方法

依赖收集

依赖收集的过程通过**dep.depend()**完成,我们来看一下它的实现from src\core\observer\dep.js,由于代码较多,我们只粘贴关键代码

export default class Dep{
    static target: ?Watcher;// 静态属性中的watcher实例
    id: number;
    subs: Array<Watcher>;// 维护了一个watcher数组
    depend () {
      // 这里的Dep.target是watcher的实例,
    if (Dep.target) {
      // 建立和watcher之间的关系,将当前Dep实例加入watcher中
      Dep.target.addDep(this)
    }
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 通知watcher进行数据更新
      subs[i].update()
    }
  }
}
    // 将当前watcher实例赋值到Dep.target的静态属性上
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  // 将watcher实例赋值到Dep的静态属性上
  Dep.target = target
}


  • 总结dep的作用:维护了一个watcher数组,实现了watcher的增加删除和通知更新

上面代码中的Dep.target.addDep(this),是将当前的Dep实例加入到了watcher实例中,这里有一个细节:理论上是每有一个Data则有一个Dep,当同一个Data多次被调用的时候,只需要创建多个watcher对其进行监听,然后Dep进行依赖收集,通知watcher更新,所以理论上Dep和watcher是一对多的关系.
但是上面的代码是将Dep实例添加到了Wtacher中,所以这就形成了多对多的关系.出现这种情况是因为真正使用的时候,有的时候一个组件

Watcher的实现

这里的Watcher主要是讲Render Watcher,组件实例化的时候会产生一个Watcher的实例,在组件$mount过程中的mountComponent()方法中new Watcher:
这里只粘贴部分核心代码

// 定义组件更新函数
    // _render()执行可以获得虚拟dom,VNode
    // _update()将虚拟DOM转换为真实DOM
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    // 创建Watcher实例
  //vm:当前vue实例
  //updateComponent:组件更新函数
  // noop:
  // {}:回调函数
  //true:是否是浏览器的watcher

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

上面的updateComponent()函数调用了Vue._render函数,最终会调用data属性的get函数,最终完成依赖收集。

  • Watcher对象的实现:

只粘贴部分核心代码

    constructor(){
        // watcher创建的时候会执行当前watcher实例的get函数,这样会出发依赖收集的过程
    this.value = this.lazy
      ? undefined
      : this.get()
    }

    /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    //将当前watcher实例赋值到Dep.target静态属性中
    pushTarget(this)
    let value
    // 当前vue实例
    const vm = this.vm
    try {
      // getter函数是上面的updateComponent()函数,会触发依赖收集过程
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // 清空Dep.target静态属性中的watcher实例
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
   //依赖收集
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // 维护了一个depid映射关系
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // 如果当前dep里面没有watcher,则将该watcher加入到dep中建立联系
      if (!this.depIds.has(id)) {
          // dep中维护了一个watcher的数组
        // 将当前watcher加入到dep中的watcher数组中,实现dep对watcher的收集
        dep.addSub(this)
      }
    }
  }
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
   // 实现数据更新
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
        // 将当前watcher实例push到更新队列中实现数据的更新
      queueWatcher(this)
    }
  }
  • 总结watcher的作用:解析传入的updateComponent更新函数并进行依赖收集。每个组件都会有一个Watcher与之对应,数值变化会触发更新函数进行重新渲染。

当data为数组时,进行数组响应化处理

根据Observer的构造方法得知,当data为数组时

    constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 给当前对象定义一个__ob__属性
    def(value, '__ob__', this)
    // 判断当前data对象是否为数组
    if (Array.isArray(value)) {
      if (hasProto) {
          // 覆盖数组的原型方法
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 如果是数组则进行数组响应化的处理
      this.observeArray(value)
    } else {
      //普通对象则用walk遍历
      this.walk(value)
    }
  }
  // 数组响应化处理
  observeArray (items: Array<any>) {
      //遍历数组
    for (let i = 0, l = items.length; i < l; i++) {
        // 取出数组的每一项进行响应化处理
      observe(items[i])
    }
  }

上述代码的核心功能主要是,protoAugment()方法扩展了当前data数组的原型方法,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'
    ]

    /**
    * Intercept mutating methods and emit events
    */
    methodsToPatch.forEach(function (method) {
        // cache original method
        // 取出数组的原型方法
        const original = arrayProto[method]
        // 拦截,添加额外行为
        // arrayMethods:数组的原型对象,定义特殊方法
        def(arrayMethods, method, function mutator (...args) {
            // 执行原先的任务
            const result = original.apply(this, args)
            // 额外任务:通知更新
            // 从this.__ob__中取出观察者
            const ob = this.__ob__
            let inserted
            // 以下三个操作需要额外处理
            // 如果是新添加的元素,还需要额外的做响应化的处理
            switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
            }
            // 如果inserted存在说明元素是新添加的,额外响应化的处理
            if (inserted) ob.observeArray(inserted)
            // notify change
            // 核心:添加通知更新方法,每一个ob中都有一个dep和这个对象或者数组对应
            ob.dep.notify()
            return result
        })
    })
  • 总结一下数组的响应化实现:通过扩展了数组原型的七个方法,实现了数组每一项的响应化,从而实现数组的响应化。

♣ 注意,通过上面的代码我们可以看出,只有通过扩展的这七个方法才能实现数组的响应化:pop、push、shift、unshift、splice、sort、reverse

总结

因为是自己边分析源码边写的一些东西,所以可能有点乱。为了捋清思路做了张图片,聊胜于无吧:

源码加载运行流程
数据响应化

下面附上一张官方数据响应化的工作流程图:

官方
图片来源:https://vuejs.org/v2/guide/reactivity.html

响应式的基本机制:

  • 通过Object.defineProperty()进行数据劫持,扩展对象属性的set和get方法
  • watcher执行getter方法触发对象属性的get方法进行依赖收集
  • 输入写入时触发对象属性的set方法,dep发布通知,watcher进行数据更新

附上一张我理解的数据响应化流程图:
数据响应化流程

以上,根据自己对源码的理解,和网上一些大神的分析整理出来的,如有不对的地方,欢迎各位大神指正。

发布了18 篇原创文章 · 获赞 2 · 访问量 3504
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章