Vue之深入响应式原理(一)

一、深入响应式原理

Vue 的数据驱动除了数据渲染 DOM 之外,还有一个很重要的体现就是数据的变更会触发 DOM 的变化。
下面我们就分析一下用户交互或者是其它方面导致数据发生变化重新对页面渲染的原理。

Vue的响应式

如下示例,说明当数据变更,会自动触发 DOM 重新渲染:

<div id="app"></div>
// 这里的Vue是自己实现简版的Vue,render函数这里并没有返回虚拟DOM

const vm = new Vue({
  el: '#app',
  data: {
    message: 'vue-1'
  },
  render() {
    return this.message
  }
})

setTimeout(() => {
  vm.message = 'vue-' + Math.floor(Math.random() * 100)
}, 1000)

当我们去修改 vm.message 的时候,div容器内容也会渲染成新的数据,那么这一切是怎么做到的呢?

Vue背后帮我们做了哪些事情?

在分析前,我们先直观的想一下,如果不用 Vue 的话,我们会通过最简单的方法实现这个需求:定时器过1000ms之后,数据被修改,然后手动操作 DOM 重新渲染。这个过程和使用 Vue 的最大区别就是多了一步“手动操作 DOM 重新渲染”。这一步看上去并不多,但它背后又潜在的几个要处理的问题:

  1. 我需要修改哪块的 DOM?

  2. 我的修改效率和性能是不是最优的?

  3. 我需要对数据每一次的修改都去操作 DOM 吗?

  4. 我需要 case by case 去写修改 DOM 的逻辑吗?

如果我们使用了 Vue,那么上面几个问题 Vue 内部就帮你做了,那么 Vue 是如何在我们对数据修改后自动做这些事情呢,接下来我们将进入一些 Vue 响应式系统的底层的细节。

二、响应式对象

可能很多小伙伴之前都了解过 Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty,这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因,我们先来对它有个直观的认识。

Object.defineProperty

Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,先来看一下它的语法:

Object.defineProperty(obj, prop, descriptor)

obj 是要在其上定义属性的对象;prop 是要定义或修改的属性的名称;descriptor 是将被定义或修改的属性描述符。

比较核心的是 descriptor,它有很多可选键值,具体的可以去参阅它的文档。这里我们最关心的是 getsetget 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。

一旦对象拥有了 gettersetter,我们可以简单地把这个对象称为响应式对象。那么 Vue.js 把哪些对象变成了响应式对象了呢,接下来我们从源码层面分析。

简版Vue

先看下简版Vue的实现:

import { Watcher } from './Watcher'
import { observe } from './observe'

export default class Vue {

  constructor(options = {}) {
    this._init(options)
  }

  _init(options) {
    this.$options = options

    this._initState()

    if (this.$options.el) {
      this.$mount(this.$options.el)
    }
  }

  _initState() {
    this._watchers = []

    if (this.$options.data) {
      this._initData()
    }
  }

  _initData() {
    let data = this.$options.data

    try {
      data = this._data = typeof data === 'function'
        ? data.call(this, this)
        : data || {}
    } catch (e) {
      data = {}
    } finally { }

    const keys = Object.keys(data)

    let i = keys.length

    while (i--) {
      const key = keys[i]
      proxy(this, '_data', key)
    }

    // observe data
    observe(data, true)/* asRootData */
  }

  $mount(el) {
    this.$el = el && document.querySelector(el)

    let updateComponent = () => {
      this._update(this._render())
    }

    new Watcher(this, updateComponent, () => { }, {}, true)

    return this
  }

  _render() {
    const { render } = this.$options
    let val
    try {
      val = render.call(this)
    } catch (e) { } finally { }
    return val
  }

  _update(val) {
    this.$el = this.__patch__(this.$el, val)
  }

  __patch__($el, val) {
    // TODO
    $el.innerText = val
    return $el
  }

}

_initState

在 Vue 的初始化阶段,_init 方法执行的时候,会执行 _initState() 方法。

源码 initState 方法主要是对 propsmethodsdatacomputedwathcer 等属性做了初始化操作。这里我们主要分析 data,对于其它属性的初始化我们之后再详细分析。

_initData

data 的初始化主要过程做两件事,一个是对传入 data 对象挂载到 vm._data 下,然后将其进行属性遍历,通过 proxy 函数把每一个属性 vm._data.xxx 都代理到 vm.xxx另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式

proxy

首先介绍一下代理,代理的作用是把 data 上的属性代理到 vm 实例上,这也就是为什么我们定义了如下 data,却可以通过 vm 实例访问到它。

const vm = new Vue({
  el: '#app',
  data: {
    message: 'vue-1'
  },
  render() {
    return this.message
  }
})

console.log('message:', vm.message)

我们可以通过 vm.message 访问到我们定义在 data 中的 message,这个过程发生在 proxy 阶段:

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

proxy 方法的实现很简单,通过 Object.definePropertytarget[sourceKey][key] 的读写变成了对 target[key] 的读写。所以对于 data 而言,对 vm._data.xxx 的读写变成了对 vm.xxx 的读写,而 data 对象就是挂载到 vm._data 下,反之我们通过 vm.xxx 就可以访问到定义在 data 对象中的 xxx 属性了。

observe

observe 的功能就是用来监测数据的变化。

/**
 * 尝试为某个值创建一个观察者实例,
 * 如果成功观察到该观察者,则返回新观察者,
 * 如果该值已经包含一个观察者,则返回现有观察者。
 * @param value 
 * @param asRootData 
 */
export function observe(value, asRootData) {
  if (!isObject(value)) {
    return
  }

  let ob

  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    Array.isArray(value) ||
    isPlainObject(value)
  ) {
    ob = new Observer(value)
  }

  if (asRootData && ob) {
    ob.vmCount++
  }

  return ob
}

observe 方法的作用就是给对象类型数据 value 添加一个 Observer 实例对象 ob,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 实例对象。接下来我们来看一下 Observer 的作用。

Observer

Observer 是一个类,它的作用是给数据对象 value 添加 Observer 的实例对象,即保存到 value__ob__ 不可枚举属性下,还遍历该数据对象 value 的属性来添加 gettersetter,用于依赖收集和派发更新:

/**
 * 附加到每个观察对象的Observer类。
 * 附加后,观察者将目标对象的属性转换为
 * 用于收集依赖关系并调度更新的getter/setter
 */
export class Observer {

  constructor(value) {
    this.value = value
    this.vmCount = 0

    // 缓存当前实例到 value.__ob__ 下,并且不作枚举
    def(value, '__ob__', this)

    if (Array.isArray(value)) {
      // 数组 TODO
    } else {
      this.walk(value)
    }
  }

  /**
   * 遍历所有属性并将它们转换为getter/setter。
   * 仅当值类型为Object时才应调用此方法
   * @param obj 
   */
  walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

}

Observer 的构造函数逻辑很简单,通过执行 def 函数把自身实例添加到数据对象 value__ob__ 属性上,def 的定义如下:

/**
 * Define a property.
 */
function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def 函数是一个非常简单的Object.defineProperty 的封装,这就是为什么我在开发中输出 data 上对象类型的数据,会发现该对象多了一个 __ob__ 的属性。

回到 Observer 的构造函数,接下来会对 value 做判断,对于数组这里暂不分析,对于纯对象,则调用 walk 方法,即遍历对象的 key 调用 defineReactive 方法,那么我们来看一下这个方法是做什么的。

在这里插入图片描述

defineReactive

defineReactive 的功能就是定义一个响应式对象,给数据对象动态添加 gettersetter,此时这两个方法是不会被调用的:

/**
 * 在对象上定义反应性属性。
 * @param obj 
 * @param key 
 * @param val 
 * @param constructor 
 * @param shallow 是否深度观测
 */
export function defineReactive(obj, key, val, customSetter, shallow) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 递归观测,当然前提val不是基础类型,observe中已作判断
  let childOb = !shallow && observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      console.log('getter:', value)

      /***********************依赖收集*************************/
      if (Dep.target) {
        dep.depend()
      }
      /***********************依赖收集*************************/

      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      console.log('setter:', newVal)

      // 新旧的值没有发生改变
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }

      if (customSetter) {
        customSetter()
      }

      // 用于没有setter的访问器属性
      if (getter && !setter) return

      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }

      // 递归观测,当然前提val不是基础类型,observe中已作判断
      childOb = !shallow && observe(newVal)

      /***********************派发更新*************************/
      dep.notify()
      /***********************派发更新*************************/
    }
  })
}

defineReactive 函数拿到数据对象 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 gettersetter。而关于 gettersetter 的具体实现,我们会在之后介绍。

总结

这一节我们介绍了Vue会把数据对象 data 等变成响应式对象,在创建过程中,如果发现子属性也是对象,则将递归地把该对象变成响应式的。还介绍了响应式对象,核心就是利用 Object.defineProperty 给对象的属性添加了 gettersetter,目的就是为了在我们访问数据或者修改数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新,接下来的我们会重点对这两个过程分析。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值