In-depth analysis of the principle of Vue2 responsive data

This article will take you through the Vue data responsive principle quickly, analyze the source code, learn design ideas, step by step.

Data initialization

_heat

When we perform the new Vuecreation of an instance, the following constructor is called, which is called inside this function 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); // 定义原型方法_init
stateMixin(Vue)  //定义 $set $get $delete $watch 等
eventsMixin(Vue) // 定义事件  $on  $once $off $emit
lifecycleMixin(Vue) // 定义 _update  $forceUpdate  $destroy
renderMixin(Vue) // 定义 _render 返回虚拟dom 

export default Vue;
复制代码

initMixinThe prototype method is defined in the function _init, and other methods are _initcalled , and a lot of initialization work is done in it. We focus oninitState(vm)_initinitState

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

The data initialized by the initState function in order $options, the order isprop>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

What does initData do?

  1. assign vm.$options.datatovm._data

    Here is a detail. The vue component data recommends using functions to prevent data from being shared between components. The reason is that if the data you define is an object, the data of all component instances will refer to this object, and a component changes the data. The components also change, and their data points to the same memory address.

  2. Determine whether methods and properties have the same name, and whether there are reserved properties

  3. There is no problem, proxy()by proxying each attribute in data to the current instance, it can be this.xxaccessed through

  4. Finally, call to observelisten the entire data, and the observe method is used to create a listener

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 data proxy

The proxy function calls to proxy Object.definePropertyeach _dataproperty in the vm to the vm. The effect is that you can access vm._data.xx through vm.xx. When you access vm.a, you actually access 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 data hijacking

observe

This method is used to create a listener instance

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

Listener class to convert data to reactive data

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])
    }
  }
}

复制代码

There are different processing for arrays and objects, let's first look at the method of processing object responsiveness, walk.

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的做法是修改原生操作数组的方法,并且跟用户约定修改数组要用这些方法去操作。

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

image.png

数组的观测

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

记得我们在讲寄生式继承时说的么,寄生式继承的核心:使用原型式继承Object.create(parent)可以获得一份目标对象的浅拷贝,在这个浅拷贝对象上进行增强,添加一些方法属性。
vue对重写数组方法的设计与寄生式继承类似,都是面向切面编程的思想(AOP),即不破坏原有功能封装的前提下,动态的扩展功能

import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'

const arrayProto = Array.prototype // 用Array的原型创建一个新对象,arrayMethods.__proto__ === arrayProto,免得污染原生Array
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) // 先调用原生方法
    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来看,这段后续接着说,一点点地来消化。

整个流程如下图所示: image.png

Guess you like

Origin juejin.im/post/7134183965824385032