本文将带大家疾速过一遍Vue数据响应式原理,解析源码,学习设计思路,循序渐进。

数据初始化

_init

在咱们执行new Vue创立实例时,会调用如下构造函数,在该函数外部调用this._init(options)

import { initMixin } from "./init.js";// 先创立一个Vue类,Vue就是一个构造函数(类) 通过new关键字进行实例化function Vue(options) {  // 这里开始进行Vue初始化工作  this._init(options);}// _init办法是挂载在Vue原型的办法,每一个new 实例能够调用, 由initMixin办法挂载// 将不同的操作拆分成不同的模块,导入后对Vue类做一些解决,此做法更利于保护initMixin(Vue); // 定义原型办法_initstateMixin(Vue)  //定义 $set $get $delete $watch 等eventsMixin(Vue) // 定义事件  $on  $once $off $emitlifecycleMixin(Vue) // 定义 _update  $forceUpdate  $destroyrenderMixin(Vue) // 定义 _render 返回虚构dom export default Vue;

initMixin函数外面定义了原型办法_init_init调用了initState(vm)等办法,_init里做了很多初始化工作,咱们重点关注initState

import { initState } from "./state";export function initMixin(Vue) {  Vue.prototype._init = function (options) {    const vm = this; // 这里的this指向调用_init办法的对象(即 new的实例)    //  this.$options就是用户new Vue的时候传入的属性    vm.$options = options;    ...    initLifecycle(vm);    initEvents(vm);    initRender(vm);    callHook(vm, 'beforeCreate');    initInjections(vm); // resolve injections before data/props    // 初始化状态,在beforeCreate之前,created之后    initState(vm);    initProvide(vm); // resolve provide after data/props    callHook(vm, 'created');    ...  };}

initState

initState函数按程序初始化$options的数据,程序为 prop>methods>data>computed>watch

import { observe } from "./observer/index.js";function initState (vm) {    vm._watchers = [];    const opts = vm.$options;      // 按程序初始化 prop>methods>data>computed>watch    if (opts.props) { initProps(vm, opts.props); }     if (opts.methods) { initMethods(vm, opts.methods); }    if (opts.data) { // 初始化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);    }  }

initData

initData做了什么事?

  1. vm.$options.data 赋值给vm._data

    此处有个细节,vue组件data举荐应用函数,避免数据在组件之间共享,起因是如果你定义的data是个对象的话,那所有的组件实例的data都会援用这个对象,一个组件更改了data别的组件也会发生变化,他们的data指向同一个内存地址。
  2. 判断办法和属性是否重名,以及是否有保留属性
  3. 没有问题就通过 proxy() 把 data 里的每一个属性都代理到以后实例上,就能够通过 this.xx 拜访了
  4. 最初再调用 observe 监听整个 data,observe办法用于创立监听器
import { observe } from "./observer/index.js";function initState (vm) {    ...    initData(vm);}function initData (vm: Component) {  // 获取以后实例的 data   let data = vm.$options.data  // 判断 data 的类型  data = vm._data = typeof data === 'function'    ? getData(data, vm)    : data || {}  if (!isPlainObject(data)) {    data = {}    process.env.NODE_ENV !== 'production' && warn(`数据函数应该返回一个对象`)  }  // 获取以后实例的 data 属性名汇合  const keys = Object.keys(data)  // 获取以后实例的 props   const props = vm.$options.props  // 获取以后实例的 methods 对象  const methods = vm.$options.methods  let i = keys.length  while (i--) {    const key = keys[i]    // 非生产环境下判断 methods 里的办法是否存在于 props 中    if (process.env.NODE_ENV !== 'production') {      if (methods && hasOwn(methods, key)) {        warn(`Method 办法不能反复申明`)      }    }    // 非生产环境下判断 data 里的属性是否存在于 props 中    if (props && hasOwn(props, key)) {      process.env.NODE_ENV !== 'production' && warn(`属性不能反复申明`)    } else if (!isReserved(key)) {      // 都不重名的状况下,代理到 vm 上,能够让 vm._data.xx 通过 vm.xx 拜访      proxy(vm, `_data`, key)    }  }  // 监听 data  observe(data, true /* asRootData */)}

proxy 数据代理

proxy函数中调用了Object.defineProperty_data中的每个property代理到了vm身上,作用就是,能够vm._data.xx 通过 vm.xx 拜访,当你拜访vm.a的时候实际上是拜访的vm._data.a。

function proxy (target, sourceKey, key) {    sharedPropertyDefinition.get = function proxyGetter () {      return this[sourceKey][key]    };    sharedPropertyDefinition.set = function proxySetter (val) {      this[sourceKey][key] = val;    };    Object.defineProperty(target, key, sharedPropertyDefinition);  }

observe 数据劫持

observe

该办法用于创立监听器实例

export function observe (value: any, asRootData: ?boolean): Observer | void {  // 如果不是'object'类型 或者是 vnode 的对象类型就间接返回  if (!isObject(value) || value instanceof VNode) {    return  }  let ob: Observer | void  // __ob__是监听器对象,如果存在的话阐明曾经被监听过,防止反复监听  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

监听器类,将数据转换为响应式数据

export class Observer {  value: any;  dep: Dep;  vmCount: number; // 根对象上的 vm 数量  constructor (value: any) {    this.value = value    this.dep = new Dep(); // 事后实例化一个dep,用于保留数组的依赖    this.vmCount = 0    // 给 value 增加 __ob__ 属性,值为为以后value 创立的 Observe 实例    // 示意曾经变成响应式了,目标是对象遍历时就间接跳过,防止反复监听    def(value, '__ob__', this)    // 类型判断    if (Array.isArray(value)) {      // 判断数组是否有__proto__      if (hasProto) {        // 如果有就把它的原型设置为arrayMethods,arrayMethods对象领有变异后的七个数组办法并且原型是原生数组Array的原型        protoAugment(value, arrayMethods); // 原型加强      } else {        // 没有就通过 def,也就是Object.defineProperty 去定义属性值        copyAugment(value, arrayMethods, arrayKeys)      }      this.observeArray(value)    } else {      this.walk(value)    }  }  // 如果是对象类型  walk (obj: Object) {    const keys = Object.keys(obj)    // 遍历对象所有属性,转为响应式对象,也是动静增加 getter 和 setter,实现双向绑定    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])    }  }}

对于数组和对象有不同的解决,咱们先来看解决对象响应式的办法,walk

参考vue实战视频解说:进入学习

walk

遍历对象所有属性,调用defineReactive办法转为响应式对象,

  walk (obj: Object) {    const keys = Object.keys(obj)    // 遍历对象所有属性,转为响应式对象,也是动静增加 getter 和 setter,实现双向绑定    for (let i = 0; i < keys.length; i++) {      defineReactive(obj, keys[i])    }  }

defineReactive

定义响应式对象,getter时收集依赖,setter时触发依赖

export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: ?Function,  shallow?: boolean) {  // 创立 dep 实例,保留属性的依赖,getter时增加依赖,setter时触发依赖  const dep = new Dep(); 这个是对象的依赖  // 拿到对象的属性描述符  const property = Object.getOwnPropertyDescriptor(obj, key)  if (property && property.configurable === false) {    return  }  // 获取自定义的 getter 和 setter  const getter = property && property.get  const setter = property && property.set  if ((!getter || setter) && arguments.length === 2) {    val = obj[key]  }  // 如果 val 是对象的话就递归监听  // 递归监听子属性,如果value还是一个对象会持续走一遍defineReactive 层层遍历始终到value不是对象才进行,所以如果对象层级过深,对性能会有影响  let childOb = !shallow && observe(val) // data = {a: {b: 3}, c: [1, 2]} 属性值如果是对象或数组会返回Observer实例  // 截持对象属性的 getter 和 setter  Object.defineProperty(obj, key, { // 例如监听data.a,那val就是{b: 3}    enumerable: true,    configurable: true,    // 拦挡 getter,当取值时会触发该函数    get: function reactiveGetter () {      const value = getter ? getter.call(obj) : val      // 开始依赖收集 (在get中会收集属性的依赖,以及其属性值的依赖)      // 初始化渲染 watcher 时拜访到曾经被增加响应式的对象,从而触发 get 函数        if (Dep.target) { // 如果当初处于依赖收集阶段          dep.depend(); // 增加以后属性的依赖          if (childOb) { // 数组会在此收集依赖,在数组被push等操作时调用保留的Observer实例触发依赖;对象会收集两次依赖,然而对象的第二次收集不会被setter触发            // childOb.dep 就是Observer 中 this.dep = new Dep()            childOb.dep.depend(); // 父属性蕴含子属性,即拜访了this.a,实际上也拜访了this.a.b,this.a.b变了,this.a就变了,所以子属性也要收集依赖          if (Array.isArray(value)) {            dependArray(value)          }        }      }      return value    },    // 拦挡 setter,当值扭转时会触发该函数    set: function reactiveSetter (newVal) {      const value = getter ? getter.call(obj) : val      // 判断是否发生变化      if (newVal === value || (newVal !== newVal && value !== value)) {        return      }      if (process.env.NODE_ENV !== 'production' && customSetter) {        customSetter()      }      // 没有 setter 的拜访器属性      if (getter && !setter) return      if (setter) {        setter.call(obj, newVal)      } else {        val = newVal      }      // 如果新值是对象的话递归监听      childOb = !shallow && observe(newVal)      // 遍历告诉贮存在Dep实例中的所有依赖      dep.notify()    }  })}

Object.defineProperty定义响应式对象的毛病

  1. 监听嵌套层级过深的对象会影响性能
  2. 对象新增或者删除的属性无奈被set 监听到 只有对象自身存在的属性批改才会被劫持,所以Vue设计了$set$delete办法,更新数据的同时手动触发告诉依赖
  3. 如果用其来监听数组的话,无奈监听数组长度动态变化,并且只能监听通过对已有元素下标的拜访进行的批改,即arr[已有元素下标] = val

咱们本人手写一个递归设置响应式的办法来试一下:

function defineProperty(obj, key, val){  observer(val);  Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get() {        // 读取办法        console.log('读取', key, '胜利')        return val      },      set(newval) {        // 赋值监听办法        if (newval === val) return        observer(newval)        console.log('监听赋值胜利', newval)        val = newval      }    })}function observer(obj) {  if (typeof obj !== 'object' || obj == null) {    return  }  for (const key of Object.keys(obj)) {    // 给对象中的每一个办法都设置响应式    defineProperty(obj, key, obj[key])  }}const arr = [{a:3}, 66, [4,5]];const obj = {a:1, b: [2]};arr.length = 33; // 无奈监听数组长度动态变化arr[2].push(22) // 只能监听通过对已有元素下标的拜访进行的批改arr[2][0] = 5; // 拜访已有元素的下标能够监听批改obj.c = 6; // 无奈监听新增加的属性delete obj.b // 无奈监听属性被删除obj.b = 66; // 被删除后就失去响应式了

尽管defineProperty能够监听通过对已有元素下标拜访的批改,然而出于性能思考,vue并没有应用这一性能来使数组实现响应式,因为数组元素太多时消耗肯定性能,要挨个遍历监听一遍数组的每一个属性,属性可能还会蕴含本人的嵌套属性,所以vue的做法是批改原生操作数组的办法,并且跟用户约定批改数组要用这些办法去操作。

尤大也做出了官网的解释:

数组的观测

数组元素增加或删除操作的观测通过创立一个以原生Array的原型为原型的新对象,为新对象增加数组的变异办法,将察看的对象的原型设置为这个新对象,被察看的对象调用数组办法时就会应用被重写后的办法。

记得咱们在讲寄生式继承时说的么,寄生式继承的外围:应用原型式继承Object.create(parent)能够取得一份指标对象的浅拷贝,在这个浅拷贝对象上进行加强,增加一些办法属性。
vue对重写数组办法的设计与寄生式继承相似,都是面向切面编程的思维(AOP),即不毁坏原有性能封装的前提下,动静的扩大性能
import { TriggerOpTypes } from '../../v3'import { def } from '../util/index'const arrayProto = Array.prototype // 用Array的原型创立一个新对象,arrayMethods.__proto__ === arrayProto,省得净化原生Arrayexport 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) // 先调用原生办法    const ob = this.__ob__    let inserted; //  新增加的元素    switch (method) {      case 'push':      case 'unshift':        inserted = args        break      case 'splice': // 能够监测数组长度变动        //splice格局是splice(下标,数量,插入的新项)        inserted = args.slice(2); // 获取插入的新项        break    }    if (inserted) ob.observeArray(inserted)    // notify change    if (__DEV__) {      ob.dep.notify({        type: TriggerOpTypes.ARRAY_MUTATION,        target: this,        key: method      })    } else {      ob.dep.notify()    }    return result  })})
因为出于性能思考,vue没有应用defineProperty劫持数组,所以要通过索引批改数组,咱们须要应用$set

总结

以上就是Vue2的响应式数据原理,讲述了如何对数据进行响应式观测,外围就是通过Object.defineProperty对数据进行劫持,在getter中收集依赖,setter中派发依赖,残缺的响应式原理,如批改数据后视图是如何更新视图的还须要联合Dep和Watcher来看,这段后续接着说,一点点地来消化。