Vue2源码解读(四)-Observe

前篇

上面文章讲到了Vue的声明Vue的InitVue的InitState,本篇将对Vue核心的Observe进行讲解和分析,Observe分为Dep和Watcher两部分,入口文件为src/core/observe/index.js。我们一步一步来看下源码。

正文

入口observe

Observe对外只暴露了一个函数observe,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)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

上面就是observe函数的源代码;

  • 首先检测了传进来的value,不是对象或者是VNode(虚拟dom)对象,则直接return;
  • 然后判断了一下,当前value是否是已经进行了Observe处理的对象;
  • 上面步骤为false时,进行判断是否需要进行监听,并且不是服务端渲染,并且是可监听对象,可扩展对象,不是Vue对象,则对value进行Observer初始化;
  • vmCount是ob的一个属性,初始值为0,当asRootData为true且ob不为空的时候,vmCount + 1;
  • 返回ob对象; 此函数作为监听的入口文件,对数据进行拦截判断,返回一个Observer的实例。

类Observer

Observer是一个class,有三个私有属性:value、dep、vmCount;一起来看下其构造函数:

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

构造函数干了四件事情:

  • 赋值,为私有变量赋值,value为传进来的参数value;dep为Dep的实例,后面讲Dep;vmCount默认值为0;
  • 定义ob,为当前value定义ob属性,此属性指向Observer的当前实例-this;
  • 如果value为数组,支持proto属性则执行protoAugment,否则执行copyAugment;最后调用observeArray;
  • 不为数组,则执行walk;

上面讲到的【支持proto属性】,现在主流浏览器都支持的,除了IE;

数组处理

protoAugment和copyAugment方法的参数,参数差别就是最后一个参数arrayKeys;

  • 第一个参数为当前value;
  • 第二个参数为arrayMethods
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  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':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

上面会对arrayMethods里面的值进行重新定义,也就是重写会引起数组变化的方法,以达到对数组进行监听的目的。

  • 第三个参数,其实也就是当前浏览器所支持的所有的数组的方法。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

接下来,咱们分别看下两个方法(protoAugment和copyAugment)的实现:

function protoAugment (target, src: Object) {
  target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
  • protoAugment方法直接把数组的proto指向了src,这样通过array调用arrayMethods里面的方法的时候,就是调用的重写后的方法,也就达到了对数组进行监听的目的; copyAugment方法,因为不支持proto的缘故,则需要在数组上面覆盖原生的arrayMethods里面的方法,也就达到了对数组进行监听的目的。

数组走observeArray

observeArray非常简单:

observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

循环调用上面文章开头介绍的observe方法;最终都会走到下面要说的walk方法。

非数组走walk

walk函数也是非常简单:

walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

获取到当前对象的keys后,对keys进行遍历,遍历调用defineReactive,传递两个参数,参数1为当前对象,参数2为当前遍历到的key。

defineReactive

接下来是重头戏,这才是Vue真正的核心之一。来看下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
  }

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

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      .....
    },
    set: function reactiveSetter (newVal) {
      ......
    }
  })
}

先说下调用defineReactive的地方:

  • initInjections,对依赖进行处理的时候,会对inject的key进行响应化调用;
  • initRender,对attrs和attrs和attrslisteners对象进行浅式响应化调用;
  • initState里面的initProps,会对props进行响应化调用;
  • 上面说到的walk里面会调用;
  • set函数里面会调用,包括Vue.set和原型对象上的$set里面;

接着说下defineReactive函数的参数:

  • 第一个就是要进行响应式处理的对象,obj;
  • 第二个为当前对象下面的一个属性,key;
  • 第三个为默认值,val;
  • 第四个为customSetter,是一个函数,用户设置的set函数时的回调,不过这个函数只有非线上环境才会调用;
  • 第五个参数为是否是浅式响应化,如果是浅式则不会对子对象进行监听。

不用看着defineReactive很长,其实就干了三件事情,一一来看 defineReactive源码解读:

  • 首先声明了一个dep,后面研究Dep是干啥的;
  • 接着判断了当前对象的当前属性是否是可改变,不可改变,直接返回;其实个人觉得,上面的dep声明,,可以放到这后面来,有点浪费;
  • 判断了下是否是无getter或者只有setter,且只有两个参数的时候,会把默认值val设置为obj[key];
  • 最后调用Object.defineProperty,重新声明obj对key的处理方式。

然后咱们接下来看下重新声明后get的定义:

function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
}

上面代码是重新定义的get方法,先是调用原生getter方法获取到value,然后判断是否有Dep.target,Dep.target是一个Watcher对象,然后调用收集依赖的函数dep.depend(),然后依次判断childOb,收集依赖;每次调用defineReactive,都有一个唯一的Dep实例与当前value一一对应。

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()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

上面代码是重新定义的set方法,先是调用原生getter方法获取到value,然后对newVal进行判断,如果未发生变化则直接返回; 如果只有getter没有setter则直接返回;然后调用原生setter进行赋值,后面调用dep.notify进行通知更新;notify会调用dep对象下面所有的依赖watcher对象下面的update方法进行更新操作;下面咱们会讲到Dep。

Dep

上面讲到了Dep的使用,现在咱们来看下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 () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

上面代码就是去除生产环境后的代码。

可以看到Dep对象有三个变量:一个是target,target对象是一个Watcher对象,同时是一个static的对象;id为一个数字的变量;subs则为一个Watcher对象的数组;

  • 构造函数为id和subs赋值;
  • addSub为依赖收集的函数;
  • removeSub为删除依赖的函数;
  • depend为依赖收集的函数,此处会调用Watcher实例的addDep函数(会有去重操作);
  • notify函数则是通知函数,此处会循环所有的依赖(Watcher实例),然后调用实例的update方法。 额外要讲的是,除了Dep的声明外,还有Dep.target这个static类型的变量的处理:
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

此处会对Dep.target进行赋值等操作,pushTarget和popTarget是成对出现的,有一个pushTarget则必然有一个popTarget;target则依旧是一个Watcher;暴露给外部调用,收集Watcher所使用,下次咱们会讲到Watcher。

结言

本章沿着observe函数进行了一步步的探索,从Observer到Dep,下一篇文章会进行Watcher的讲解。

原文链接:juejin.im

上一篇:Computed property “xxx” was assigned to but it has no setter
下一篇:你必须要掌握的HTTPS

相关推荐

  • 🚩Vue源码——组件是如何注册的

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 在上一篇 🚩Vue源码——组件...

    3 天前
  • 🚩Vue源码——组件如何渲染成最终的DOM

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 Vue有两个核心思想,一个是数据...

    8 天前
  • 🔥手写大厂前端知识点源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    6 个月前
  • 🔥前端面试大厂手写源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    6 个月前
  • (源码分析)为什么 Vue 中 template 有且只能一个 root ?

    引言 今年,疫情并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题,为什么每个组件 template 中有且只能一个 root? 可能...

    6 个月前
  • 面试还问redux?那我从头手撸源码吧(中间件篇)

    昨天的文章手写了一版redux的核心源码,redux库除了数据的状态管理还有一块重要的内容那就是中间件,今天我还是尝试将此部分源码完成。 中间件 react中管理数据的流程是单向的,就是说,从派发动作...

    2 年前
  • 防抖与节流(源码学习)

    最近自己撸了一个轮播图,在点击切换的时候,为了寻求更好的用户体验,引入了节流,在此记录对源码的学习过程 源码来源:underscore 防抖 函数防抖(debounce) 使用场景:现在我们需要做一个...

    2 年前
  • 阅读redux源码_compose

    先上源码: // 将(fun1,fun2,fun3)转换成fun1(fun2(fun3())) export default function compose(...funcs) { if (fu...

    2 年前
  • 阅读 is-generator-function 源码

    Function.prototype.toString 从正则表达式 /^\s*(?:function)?\*/ 可知 1: GeneratorFunction 不管书写是 function* 还是...

    2 年前
  • 速览vuex源码

    Vuex 源码不过千行,主要使用了 Store 类、ModuleCollection 类、Module 类,结构清晰,下面简单说说 Vuex 中一些主要的源码实现。推荐打开 Vuex 源码一同观看。

    5 个月前

官方社区

扫码加入 JavaScript 社区