Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue2核心之Observe源码分析 #6

Open
dravenww opened this issue Feb 8, 2021 · 0 comments
Open

Vue2核心之Observe源码分析 #6

dravenww opened this issue Feb 8, 2021 · 0 comments
Labels

Comments

@dravenww
Copy link
Owner

dravenww commented Feb 8, 2021

正文

入口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和$listeners对象进行浅式响应化调用;
  • 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.

@dravenww dravenww added the vue label Feb 8, 2021
@dravenww dravenww reopened this Mar 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant