libin1991 / libin_Blog

爬虫-博客大全
https://libin.netlify.com/
124 stars 17 forks source link

Vue.js 源码解析响应式原理 #535

Open libin1991 opened 6 years ago

libin1991 commented 6 years ago

Vue 实现响应式的机制简单来说就是 Object.defineProperty 实现的访问拦截观察者模式. 其他关键词包括: Observer Dep Watcher 和依赖收集. 这篇文章将会分析 Vue.js 的源码以解释这些概念, 讲解响应式原理, 还会给出一个简单的例子以在 Chrome 开发工具中验证这篇文章的内容.

你可以在 lets-read-vue 中找到注释后的源码以及文末例子的源码.


Vue.js 项目的结构如下:

├── src
│   ├── compiler // template 编译
│   │   ├── codegen
│   │   ├── create-compiler.js
│   │   ├── directives
│   │   ├── error-detector.js
│   │   ├── helpers.js
│   │   ├── index.js
│   │   ├── optimizer.js
│   │   ├── parser
│   │   └── to-function.js
│   ├── core // 所有的核心代码, 重中之重
│   │   ├── components // 主要是 keep-alive 抽象组件
│   │   ├── config.js
│   │   ├── global-api
│   │   ├── index.js
│   │   ├── instance // 主要模块, 实现生命周期, 状态, 事件, 渲染等等
│   │   ├── observer // 响应式核心代码
│   │   ├── util
│   │   └── vdom // Virual DOM
│   ├── platforms
│   │   ├── web
│   │   └── weex
│   ├── server // 服务端渲染相关
│   │   ├── bundle-renderer
│   │   ├── create-basic-renderer.js
│   │   ├── create-renderer.js
│   │   ├── optimizing-compiler
│   │   ├── render-context.js
│   │   ├── render-stream.js
│   │   ├── render.js
│   │   ├── template-renderer
│   │   ├── util.js
│   │   ├── webpack-plugin
│   │   └── write.js
│   ├── sfc
│   │   └── parser.js
│   └── shared
│       ├── constants.js
│       └── util.js

这篇文章相关的代码都在 src/core 底下.

响应式模型

先给出一个 Vue.js 的响应式原理抽象成的模型.

responsive

接下来我们深入代码来讲解这个模型.

响应式初始化

当我们通过 new Vue({}) 创建 Vue 实例时, 构造函数会调用 Vue._init 方法, 其中会调用 initState, 而在这个方法会按序初始化 props methods data computed watch, 响应式初始化就发生在这里. 我们会着重讲解 datacomputed 的初始化过程. computed 依赖 props 或者 data, 所以是订阅者, 想要知道某个被订阅者的变化, 正好构成一个响应式关系!

initData

该方法将 data 变为响应式的, 它做了以下这些事情:

  1. data 函数中获取返回值作为 data, 这就是为什么在 Vue 中 data 应当是一个返回对象的函数
  2. 检查 data 中的属性有没有和 props 重名的
  3. data 中的属性全部代理到 Vue 实例上以进行访问
  4. 观察 data 对象
function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  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 中所有的属性
  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)) {
      // 将 data 中的属性全部代理到 Vue 实例上以进行访问
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 使得 data 变为响应式的, 由于 asRootData 为 true, 可以想象有个 Observer 的 vmCount 会 + 1
  observe(data, true /* asRootData */)
}

observe Observer defineReactive Dep

observe 尝试为一个对象创建 Observer, 或者返回已有的 Observer.

export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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)
  }
  // 如果作为根数据要加上 vmCount
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer 被附加到被观察的对象上, 一旦添加, 就会尝试将该对象的属性全部转化为 get/set 以实现依赖收集和触发更新.

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor(value: any) {
    this.value = value
    this.dep = new Dep() // 创建 Dep, 这个 Dep 是对象自己而非它的属性的 Dep
    this.vmCount = 0
    def(value, '__ob__', this)
    // 如果对象是一个数列, 用 Vue 更新后的数组方法实现响应式, 这就是为什么在 Vue 中用数组下标访问无法实现响应式的效果
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      // 如果是一个对象, 就转化 get/set
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   * 
   * 这个方法遍历所有属性值, 并将它们变成响应式的
   */
  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   * 
   * 如果对象是一个数组, 就 observe 数组中的每一个元素
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Dep 对象是什么? 它是一个用来记录A 对 B 的变化感兴趣数据结构, 其中 B 是某个对象或对象的某个属性, 而 A 是一个 Watcher. 当 A 需要在 B 的数据的变化时收到通知, 就会在 B 的 Dep 中注册自己, 当 B 发现数据更新的时候, 就会通知所有感兴趣的 A. 这就是观察者模式.

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor() {
    this.id = uid++
    this.subs = []
  }

  // 将会被某个 Watcher 调用, 修改自己的订阅者数组
  addSub(sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub(sub: Watcher) {
    remove(this.subs, sub)
  }

  // 将会被某个 getter 调用, 收集 Dep.target 指向的 Watcher
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

defineReactive 通过 Object.defineProperty 方法设置了某个属性的 get/set, 并在自己的作用域中创建了一个 Dep 对象. 它可以把某个属性变为响应式的, 原理就是 Object.definePropery 提供的 get 和 set. 当 Watcher 访问这个属性的时候, 首先会把自己标记为依赖收集的目标, 然后触发 get, get 会让自己闭包内保存的 Dep 进行依赖收集. 当这个属性被修改的时候, 会触发 set, set 会通知 Dep 让它去更新所有对它感兴趣的 Watcher.

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 每个属性都会有一个依赖者对象
  const dep = new Dep()

  // 如果属性值已经被设置为不可配置, 就直接返回, 什么都不做
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 如果开发者定义的属性原本就有 setter/getter, 要对它们予以保留
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 如果不是浅观察, 而且被观察值是一个对象的话, 就会返回一个 Observer 对象
  let childOb = !shallow && observe(val)
  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) {
          // 对该对象的属性也要进行依赖搜集, 因为这个 watcher 很可能就是对这些属性有依赖
          // 问题在于: Vue 会为属性的属性的属性实现响应式吗?
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value) // 如果值是数组, 递归进行数组的依赖搜集
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      // 如果没有改变, 就不要 set
      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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 要观察一下对象, 因为这里的 setter 是整个对象被替换掉了
      childOb = !shallow && observe(newVal)
      // 通知该属性的 Dep 属性值已经改变, 对应的 Watcher 应该收到通知
      dep.notify()
    }
  })
}

到这里, 被观察的一侧 (Dep) 需要做的工作就做好了.

initComputed

这个函数主要做了如下事情:

  1. 为每一个计算属性创建 Watcher 对象并添加到 _watchers 数组中
  2. 在 Vue 实例上代理访问计算属性
const computedWatcherOptions = { computed: true }

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // computed 可以是一个有 get 和 set 两个函数的对象, 这里找到正确的 getter
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 为计算属性创建 Watcher, 并且在创建的时候特别声明为计算属性而创建
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      // 在 Vue 实例上代理访问计算属性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

相比于被观察者 Dep, 观察者 Watcher 要复杂得多! 所以我们就不把代码贴在这里了, 请去 lets-read-vue 中查看. 我们这里就讲对响应式来说很重要的几个方法.

  // 这个方法用来对 computed 实际求值
  get() {
    pushTarget(this) // 先将自己设置为依赖搜集的对象
    let value
    const vm = this.vm
    try {
      // 这里调用了 getter 实现了依赖收集! 因为 getter 里面必然访问了某个对象的属性, 看 defineReactive
      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)
      }
      popTarget() // 自己依赖搜集完毕, 让出位置
      this.cleanupDeps()
    }
    return value
  }

  // 这个方法和 Dep 中的方法协作, 将会被一个 Dep 调用, Dep 会把自己传过来
  // 更新 Dep 的过程, 是记录这一次更新过程中自己需要的依赖, 与上一次更新的依赖作比较
  // 订阅新的依赖, 将不再需要的依赖剔除掉 (通过 cleanupDeps 方法)
  addDep(dep: Dep) {
    const id = dep.id
    // 记录新的依赖
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // 如果自己没有订阅过这个 Dep, 就订阅
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }

  // 当 setter 被触发的时候, 就会调用 Dep 的 update, Dep 再来调用 update 方法
  update() {
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      // 如果 Watcher 作为计算属性的 Watcher, 那么它会有两种模式, 当它没有订阅者的时候就是 lazy
      // 模式, 仅仅将 Watcher 设置为 dirty, 然后当计算属性被访问的时候, 才会重新计算
      // 如果有订阅者的时候, 就是 activated 模式, 立即计算新值, 但只有在值真的发生变化的时候
      // 才去通知自己的订阅者
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      // 如果是渲染函数指令中的 Watcher 且有 .sync 修饰符, 就立即更新, 以后再讲
      this.run()
    } else {
      // 否则进入更新队列, 之后在讲 Vue 的异步更新策略的时候会讲
      queueWatcher(this)
    }
  }

例子

接下来我们根据一个非常简单的例子来串讲我们之前覆盖的内容, 代码可以在 lets-read-vueplayground/responsive-demo.html 中找到.

<body>
  <div id="app">
  </div>
  <script src="https://vuejs.org/js/vue.js"></script>
  <script>
    var vm = new Vue({
      data: () => ({
        message: 'Wendell'
      }),
      computed: {
        helloMessage() {
          return 'Hello ' + this.message
        }
      },
      el: '#app'
    })
  </script>
</body>

responsive

在 Vue 实例初始化的时候, 先处理 data. initData 调用 observe 方法为 data 对象创建 Observer, 然后 Observer 调用自己的 walk 方法, walk 对每一个属性调用 defineReactivemessage 变成响应式的. 现在 $datamessage 都有自己的一个 Dep. 然后处理 computed, 为 computed 创建了一个 Watcher 并添加到 _watchers 数组中.

2018-04-13 13 23 53

message 属性的 Dep 保存在 defineReactive 函数调用时构成的闭包内, id 为 3.

2018-04-13 13 25 43

helloMessageWatcher 被创建, 但它没有依赖, 因为我们还没对它求值, 因为它也没有触发 messageget.

当我们在 console 访问 vm.$data.helloMessage 的时候, Watcherget 将会被调用, 这时候就通过触发 messageget 实现依赖收集, messageDepsubs 就有了 helloMessageWatcher, 与之对应 helloMessageWatcher 也会记录 messageDep.

2018-04-13 13 25 01

再次放上模型以供你温习.

2018-04-13 13 25 43

当我们修改 message 的时候, 就会触发 messageset, 此时 messageDep 就会去更新依赖, 调用 Watcherupdate 方法. 而 Watcher 如果属于某个计算属性, 仅仅会把自己设置为脏值, 仅有计算属性重新被访问的时候才会去实际求值 (这一点之前没有讲).

其他

当然了, 实现响应式的方式并不只有 datacomputed, 还有模板中的表达式, computed 相互的依赖和 watch 等等, 但原理都是如此, 就不再赘述了, 请自己阅读源码吧.