vue observer 源码学习

简介: 一、版本:2.5.9 二、建议       vue最重要的应该就是响应式更新了,刚开始接触vue或多或少都能从官方文档或者其他地方知道vue响应式更新依赖于Object.defineProperty()方法,这个方法在MDN上有详细讲解,不过,如果是初学者的话,直接去看响应式更新源码还有点难度的,最好是先用项目练一遍,对vue有个相对熟悉的了解,然后可以去各大热门讲解的博客上看看人家的讲解,这样汇总一番有点底子了再去看源码实现相对轻松点。

一、版本:2.5.9

二、建议

      vue最重要的应该就是响应式更新了,刚开始接触vue或多或少都能从官方文档或者其他地方知道vue响应式更新依赖于Object.defineProperty()方法,这个方法在MDN上有详细讲解,不过,如果是初学者的话,直接去看响应式更新源码还有点难度的,最好是先用项目练一遍,对vue有个相对熟悉的了解,然后可以去各大热门讲解的博客上看看人家的讲解,这样汇总一番有点底子了再去看源码实现相对轻松点。 最低级别的监听可以看我这个库:https://github.com/lizhongzhen11/obj 参考:https://segmentfault.com/a/1190000009054946 https://segmentfault.com/a/1190000004384515

三、阅读

      从github上把vueclone下来,或者直接在github上看也行。       别的先不管,直接去src/core/observer文件夹,这个明显就是vue响应式更新源码精华所在,内部共有array.js,dep.js,index.js,scheduler.js,traverse.js,watcher.js6个文件,先看哪一个呢?第一次看没有头绪的话就先看index.js。       index.js开头import了不少文件,先不用管,往下看需要用到时再去查找不迟。而第一步就用到了arrayMethods,该对象来自array.js,下面同时列出array.js中的相关代码:



// index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

    如上所示, arrayMethods其实是一个 Array.prototype的实例,只不过中间经过 arrayProto过渡,一开始我还在纠结下方的代码(对数组 push等方法遍历添加到刚刚创建的实例 arrayMethods中,这里没有列出来),因为没看到下方代码有 export,感觉很奇怪,而且他代码是下面这样的, []前有个 ;,感觉很奇怪, vue作者是不写 ;的,这里出现一个 ;感觉很突兀。 PS:后来问了前辈,前辈解释说:在js文件合并的时候,防止前一个js文件没有;结尾导致的错误

;['push','pop','shift','unshift','splice','sort','reverse']

   接下来,go on!定义了一个“观察状态”变量,内部有一个是否可以覆盖的布尔属性。注释里面说 不想强制覆盖冻结数据结构下的嵌套值,以避免优化失败


export const observerState = {
  shouldConvert: true
}

 继续往下看,来到了重头戏: Observer类,注释中也说的明白:该类属于每个被观察的对象, observer在目标对象的属性的 getter/setters覆盖键同时搜集依赖以及分发更新。

import Dep from './dep'
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()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      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], obj[keys[i]])
    }
  }
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}



  构造函数里面第二步 this.dep = new Dep(),这个 Dep来自 dep.js,这时候,得需要去看看 dep.js里面相关的代码了:


let uid = 0
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 * dep是可观察的,可以有多个指令订阅它
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  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() // 更新 Watcher 数组中的数据
    }
  }
}


  Dep内部用到了Watcher,而Watcher又来自watcher.js。先说Dep,内部主要对Watcher类型的数组进行增加删除以及更新维护,自己内部没有什么太多复杂的逻辑,主要还是在watcher.js中。接下来列出watcher.js相关代码:


let uid = 0
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  // 先看构造函数,内部变量不列出来了,太多了
  constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this // 直接在vue 页面里打印 this 可以找到_watcher属性
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep // 这里可能是怕万一 options 对象里没有 deep 等属性,所以用了 !! 来强转成布尔型
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set() // es6语法,类似java Set集合,不会添加重复数据
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy ? undefined : this.get()
  }


上面构造函数第一个参数 vm是什么?如果一直用 vue-cli构建工具开发的话,可能没怎么注意过,**其实 vm就是 vue的一个实例!!!**第二个参数 expOrFn暂时还不清楚,如果是函数的话直接赋给 this.getter,否则 this.getter直接指向一个空函数,同时还发出警报,需要传递一个函数。最后,判断 this.lazy,为 true的话调用 this.get()方法:


import Dep, { pushTarget, popTarget } from './dep'
/**
   * Evaluate the getter, and re-collect dependencies.
   * 对 getter 求值,并重新收集依赖
   */
  get () {
    pushTarget(this) // 相当于 Dep.target = this,
    let value
    const vm = this.vm
    try {
      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() // 清理deps,为了依赖收集
    }
    return value
  }
  // dep.js
  export function pushTarget (_target: Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
  }
  export function popTarget () {
    Dep.target = targetStack.pop()
  }

   get()中最终会判断 cthis.deep是否为 true,如果是调用 traverse(value),而 traverse()来自 traverse.js,其目的是把 dep.id加进去; popTarget()是为了将之前 pushTarget(this)target移除。


/**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear() // newDepIds 是Set类型,可以通过clear()清空
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  cleanupDeps()方法将旧的依赖编号与新的依赖集合编号进行对比,如果旧依赖数组中存在的编号,而新依赖集合编号中不存在,就需要删除对应编号的依赖;接下来交换新旧依赖集合编号,然后清空 this.newDepIds(其实此时该集合内保存的是旧有的依赖集合编号);随后交换新旧依赖数组,然后来了一步骚操作: this.newDeps.length = 0,将 this.newDeps清空,比较骚。


    也就是说,利用get()方法求值后会清理依赖收集。       到了get()可以先暂停回顾一下。这里是在Watcher构造函数中调用的,也就是说,当new Watcher()时就会走遍上述代码,包括调用get()来取值。


这时候如果继续强行看完 Watcher下面的源码,会发现没什么头绪,所以依然回到 index.js中。继续研究 Observer类的构造函数。


constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    const augment = hasProto ? protoAugment : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}

构造函数中紧跟着调用了 def(value, '__ob__', this),这个方法是干嘛的?在哪里?       通过查找发现 def方法位于 util/lang.js内,下面贴出源码:


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




 def内部调用了Object.defineProperty(),结合Observer构造函数的传参,可知这里给每个对象定义了一个__ob__属性,在日常开发中,当我们打印输出时经常能看到__ob__。
      接下来进一步判断value是不是数组,如果不是的话调用walk(),当然要确保参数是Object,然后遍历对象的key并且每个调用defineReactive(obj, keys[i], obj[keys[i]])。

看看 defineReactive()方法内部实现:

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
  const getter = property && property.get // 缓存对象属性内的get方法
  const setter = property && property.set // 缓存对象属性内的set方法
  let childOb = !shallow && observe(val)  // observe(val)尝试返回一个 observer实例,如果 !shallow === true 那么 childOb === ob
                                          // 其实也可以理解为, childOb === val.__ob__
  Object.defineProperty(obj, key, {       // 这里开始是真正的核心所在,其实就是重新对象的get、set方法,方便监听
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val // getter 存在的话就调用原生的 get 方法取值,否则用传进来的值
      if (Dep.target) {
        dep.depend()                                // 增加依赖
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)                      // 递归调用收集数组依赖
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      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
      }
      childOb = !shallow && observe(newVal) // childOb === newVal.__ob__
      dep.notify() // 内部调用了 watcher.js 里面的 uodate(),内部又调用了 run(),run()里面设置值,其中还用到了watcher队列
    }
  })
}


   响应式更新的重中之重就是首先得监听到对象属性值的改变,vue通过defineReactive()内部重写传入的对象属性中的set以及get方法,其中,js原生的call()也有很大的功劳。


总结

      再一次看vue源码明显比第一次看好多了,但是不断地调用其它方法,理解上还是有一定的难度,这一次阅读源码更多的就是做个笔记,写得并不好,但是留个印象,方便下次再看。



原文发布时间为:2018年06月28日
原文作者: pro-xiaoy
本文来源: 掘金 如需转载请联系原作者




目录
相关文章
|
10天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
10天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
9天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
15天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
15天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
15天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
15天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
15天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
15天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
16天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。