Vue的响应式原理

184 阅读3分钟

这篇文章介绍vue的响应式原理了,尽量不贴源码,就是讲讲自己的理解,主要受众是对响应式原理一知半解或者零基础的。希望看完之后都能对响应式原理有所收获。

概要

响应式原理 其实就是渲染函数会在响应式数据发生变化的时候重新执行。

这样做的好处就是当页面中的动态数据发生变化时,我们不需要手动拿到最新的数据然后进行赋值,因为框架的响应式原理已经自动帮我们做了。

Vue2的具体实现

首先会把data属性(通常是一个函数,返回值是一个对象),比如:

export default {
    data() {
        return {
            showAlert: false,
            alertText: null,
        }
    },
}

如果函数的返回值不是对象话会进行警告

  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    data = {}
    __DEV__ &&
      warn(
        'data functions should return an object:\n' +
          'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      )
  }

总之拿到data属性的对象之后,会把这个对象赋值给 vm._data 对象,接着对所有的key进行代理,这就是为什么我们可以通过this直接访问到data返回对象的属性。

  // 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 (__DEV__) {
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      __DEV__ &&
        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)
    }
  }

可以看到这里还会检查data返回对象的属性有没有在props和methods中定义过,如果已经定义过的话会进行警告。 然后通过proxy的方式proxy(vm, _data, key),,让用户可以直接通过this拿到属性,其实我们用this._data也可以拿到。

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

最后会对data进行observe

  // observe data
  const ob = observe(data)
  ob && ob.vmCount++

在observe方法里面,主要创建了一个Observer类

new Observer(value, shallow, ssrMockReactivity)

在observe方法里面,主要创建了一个Observer类

new Observer(value, shallow, ssrMockReactivity)

然后在这个类里面会拿到data的每一个key,对key进行defineReactive方法。

  const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }

在defineReactive里面会创建一个Dep实例const dep = new Dep(),Dep是Vue2实现响应式原理的核心关键。主要作用是存储我们希望自动执行的函数,在Vue2这里就是一些Watcher(有三个Watcher,渲染Watcher,computed watcher 和user watcher,user watcher就是我们vue2配置的watcher属性。) 然后会对data,key的值进行再次observe,直到值不是对象为止。

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) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
  }

  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)

接下来就是Vue2响应式的核心了,也是vue2能对数据进行劫持的关键,就是通过Object.defineProperty对对象属性的读取操作进行劫持,大致流程就是渲染函数(或者其他,比如computed和watcher属性)读取数据的时候把自己添加到这个属性创建的dep实例上,当这个数据被修改的时候,比如点击事件,再把收集到的渲染函数(或者其他)取出来重新执行。

  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 (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      dep.notify()
    }
  })

Dep.target就是我们要收集的副作用函数,在get中被收集,然后在set中就会通过dep.notify()方法重新触发。下面再贴一下dep.depend方法和dep.notify方法。

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.filter(s => s) as DepTarget[]
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      sub.update()
    }
  }

补充说明

渲染函数其实就是将vue的模板进过一系列转换,转成最终生成vdom(虚拟dom)的函数,然后再通过patch方法转成真实dom挂载到浏览器上。

Vue3的具体实现