likes
comments
collection
share

我的源码学习之路(四)---vue-2.6.14

作者站长头像
站长
· 阅读数 17

前言

最近的工作安排几乎没有,工资也没发,我不知道能待多久。我感觉我自己闲的烦死了,我现在给自己的规定就是每天写一篇文章.希望能过完年之后有一个底气出去找一个能正常发工资的公司。毕竟打工人每个月都是靠着这点点收入生活

这是第五篇文章,前面四篇主要根据源码讲述一个初始化的过程。对于vue源码来说。我看源码就是为了解决面试官提出的vue的面试的所有问题。也是比较没有追求的一种方式,是的,我没啥追求。


回顾

涉及的前端小知识【遇到就记录一下】

Object.getOwnPropertyDescriptor()

Vue源码中响应式功能实现过程中defineReactive()方法中使用了一个对象的方法 Object.getOwnPropertyDescriptor(obj, key)

Object.getOwnPropertyDescriptor(obj,prop):返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

  • obj:需要查找的目标对象
  • prop:目标对象内属性名称
  • 返回值:如果指定的属性存在于对象上,则返回其属性描述符对象,否则返回undefined

我的源码学习之路(四)---vue-2.6.14

我的源码学习之路(四)---vue-2.6.14

而这个返回值打印出来的结果由下面几种组成:

  • value: 该对象属性的值
  • writable: 当且仅当属性的值可以被改变时为true
  • get: 获取该属性的访问器函数(getter),如果没有访问器,返回undefined
  • set: 获取该属性的设置器函数(setter), 如果没有设置器,该值为undefined
  • configurable: 当且仅当指定对象的属性描述可以被改变或者属性可被删除时,为true
  • enumerable: 当且仅当指定对象的属性可以被枚举出时,为true

Object.keys()

Object.keys():返回一个由一个给定对象的自身枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致。

我的源码学习之路(四)---vue-2.6.14

Object.defineProperty(obj,prop,descriptor)

Object.defineProperty:会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

  • obj:要定义属性的对象
  • prop: 要定义或修改的属性的名称或Symbol
  • descriptor:要定义或修改的属性描述符
  • 返回值:被传递的函数的对象

我的源码学习之路(四)---vue-2.6.14

我的源码学习之路(四)---vue-2.6.14

const o = {}
Object.defineProperty(o, "key", {
  enumerable: true, // 默认为false 。为true,定义的属性可以在for...in和Object.keys()中枚举
  configurable: true,// 默认为false。表示对象的属性是否可以删除,以及除value和writable特性外的其他特性是否可以修改
  writable: true, // 默认为false,不能重新赋值
  value: 12
});

enumerable 我的源码学习之路(四)---vue-2.6.14

configurable 我的源码学习之路(四)---vue-2.6.14

writable:为false,configurable也要设置为false

我的源码学习之路(四)---vue-2.6.14

我的源码学习之路(四)---vue-2.6.14

Object.isExtensible()

Object.isExtensible(obj): 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)

  • obj:需要检测的对象
  • 返回值: 返回一个Boolean[true/false]

我的源码学习之路(四)---vue-2.6.14

冻结的对象是不能扩展的 我的源码学习之路(四)---vue-2.6.14

密封对象是不可以扩展的 我的源码学习之路(四)---vue-2.6.14


我觉得通篇去记录源码的一个方式不太利于我面试的时候去应对面试官的提问,追问。所以接下来的六篇文章,我准备以vue的面试题的问题方式在源码中寻找答案。也是为了我明年的一个面试打一个基础。

vue常见的一些面试问题

vue是如何实现响应式的

 这是vue的一个核心点。因为vue主要就是数据驱动页面的方式。

下图来自vue官网

我的源码学习之路(四)---vue-2.6.14

这个问题下扩展一些问题:

  • Vue是如何收集依赖的

  • Vue是如何通知更新的?

  • 对象和数组分别是如何进行深层响应式处理的?

  • 嵌套对象是如何进行浅层响应式处理的?

    创建响应式数据

    实现数据的双向绑定,原理就是重写了data中的每项数据的getter和setter(利用Object.definePrototype())每次取值时候收集依赖,改值的时候通知notify: 而我们的数据的传输的路径如下:

  1. initState(vm)[src/core/instance/init.js(57)]
  2. initData(vm)[src/core/instance/state.js(54-57)(113)]----->proxy()--->observe()
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function' //判断data是否是一个函数
    ? getData(data, vm) // 是函数的话执行getData()
    : data || {} 
  if (!isPlainObject(data)) { // return _toString.call(obj) === '[object Object]'判断是不是对象
    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
  const keys = Object.keys(data) // 按顺序列举出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)) { // isReserved(key):检查字符串(key)是否以$或_开头
      proxy(vm, `_data`, key) // 在vm上设置对于_data中key的代理,这样可以通过this[key],本质上访问的是this[_data][key]
      // proxy:代理,封装一层Object.defineProperty
      //  const sharedPropertyDefinition = {
      //    enumerable: true,
       //   configurable: true,
       //    get: noop,
       //   set: noop
       //   }

       // export function proxy (target: Object, sourceKey: string, key: string) {
       //   sharedPropertyDefinition.get = function proxyGetter () { //重新定义get
       //     return this[sourceKey][key]
       //    }
       //  sharedPropertyDefinition.set = function proxySetter (val) {//重新定义set
       //    this[sourceKey][key] = val
       //  }
       //   Object.defineProperty(target, key, sharedPropertyDefinition)
       // }
    }
  }
  // observe data
  observe(data, true /* asRootData */)// 接下来观察处理
}
  1. observe()[src/core/observer/index.js(110)] ----> ob = new Observer(value)
function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 如果属性中有__ob__,说明已经被观察过了,无需再
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() && // 非服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && // vue只对数组和普通对象进行观察
    Object.isExtensible(value) && // 判断一个对象是否可以扩展
    !value._isVue // /_isVue属性是在_init方法中挂载到vm上的,说明不对vue实例进行observe
  ) {
    ob = new Observer(value) // Observer构造函数
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
  1. new Observer[src/core/observer/index.js(37)]
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value // 要被观察的对象或数组
    this.dep = new Dep() // Dep()用于收集依赖
    this.vmCount = 0 // 用于计数
    def(value, '__ob__', this)
    if (Array.isArray(value)) {//判断value是不是数组
      if (hasProto) {//判断是否有__proto__
        protoAugment(value, arrayMethods)// 如果有吧arrayMethods重新赋值给value的__proto__
      } else {
        copyAugment(value, arrayMethods, arrayKeys) // 一直都知道vue中重写了数组的方法,(arrayMethods[src/core/observer/array.js])
      }
      this.observeArray(value) // 监控数组内的每一项
    } else {
      this.walk(value) // 执行响应式操作
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj) // 返回对象属性的数组
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) // 将所有的属性都执行defineReactive
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

arrayMethods

我的源码学习之路(四)---vue-2.6.14

  1. walk()---->defineReactive()[src/core/observer/index.js(135)]
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) { // 如果key的configurable为false不处理
    return
  }

  // cater for pre-defined getter/setters
  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, // 每个key设置可以枚举
    configurable: true, // 每个key设置可以删除
    get: function reactiveGetter () { // 收集获取值
        const value = getter ? getter.call(obj) : val
       if(Dep.target) {
          dep.depend() //操作:观察者dep 关联 被观察者watcher
          if (childOb) {
            childOb.dep.depend()
            if (Array.isArray(value)) {
              dependArray(value)
            }
          }
        }
        return value
    },
    set: function reactiveSetter (newVal) { // 修改值
     ....
      dep.notify() // 触发watcher.update()
    }
  })
}

这个时候只是初始化过程中,目前也只是执行到beforeCreate之后,created之前

vue是如何进行依赖收集

在上一篇文章里watcher中谈到了收集依赖,在上面响应式代码中也谈到了要收集依赖 收集依赖的代码就是dep:new Dep()[src/core/observer/dep.js]。可见,收集依赖是一个非常重要的步骤!

在记录这一块内容的时候,看到了一些资料和一些问题,我也不是很明白。现在一边看一遍记录一下,主要有:

  • 为什么要收集依赖?
  • 什么时候进行依赖收集
  • 如何收集依赖的
  • defineReactive中有两种dep在收集依赖,为什么?

为什么要收集依赖?

所谓依赖收集,就是把一个数据用到的地方收集起来[就是把watcher实例存放到对应的Dep对象中去]。vue中当数据发生改变的时候,统一通知做处理

看到一个形容:dep就是引用计数,就像是谁借了我的钱,都记在本本(subs[])上,以后只要发生变化,就通知他们

原因:目的在于我们观察数据的属性值改变的时候,可以通知哪些视图层使用了该数据。

而我们收集的依赖就是Watcher。

什么时候进行依赖收集

data中项被取值的时候(get)

取值:在模板取值的时候就会进行 依赖收集,执行dep.depend()[src/core/observer/index.js(163)]

改值:在值发生改变的时候,会触发dep.notify()[src/core/observer/index.js(191)]

如何收集依赖的

主要过程:在getter中收集依赖,在setter中触发依赖。当属性改变的时候,把之前收集好的依赖触发一遍

这里涉及到我们一个设计模式:订阅观察者模式。

订阅者Dep类:用于解耦属性的依赖收集和派发更新操作,主要用来存放Watcher 观察者Watcher: 数据只要发生变化就通知他,他再通知其他地方

Dep

export default class Dep {
  static target: ?Watcher; // 一个静态属性。静态属性特点:全局唯一,随着类的加载而加载的。
  id: number;
  subs: Array<Watcher>; 存放watcher数组

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

  addSub (sub: Watcher) { // watcher调用将其实例反传当前dep存放在subs[]中
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  // Dep.target是一个全局变量,将Watcher和Dep进行互相绑定
  depend () { // 将this传给watcher实例,用以在wacher中记录dep实例和去重watcher
    if (Dep.target) { 
      Dep.target.addDep(this)
    }
  }

  notify () { // 执行subs中所有的watcher.update()
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Watcher

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  )
    ....
  get () { // 主要执行getter()
    pushTarget(this) // 将变量对应的唯一的watcher赋值给Dep.target
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    }
    ...
  }
  addDep (dep: Dep) { // 去掉重复的dep并记录,后调用dep.addSub(this)
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  cleanupDeps () {
   ...
  }
  update () { // 去重watcher防抖,
    if (this.lazy) {// 懒加载
      this.dirty = true
    } else if (this.sync) {//异步执行run()
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  run () { // 执行get()及侦听器中用户的回调
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.value = value
        ...
      }
    }
  }
    evaluate () {// 缓存计算属性 执行get
      this.value = this.get()
      this.dirty = false
    }
  depend () { // 调用所有deps[]中的dep.depend()
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

关系

Watcher和Dep是多对多的关系:

  • Watcher中有一个deps

  • Dep中subs

我的源码学习之路(四)---vue-2.6.14

defineReactive中有两种dep在收集依赖,为什么?

defineReactive中的get方法中出现了一段代码:

let childOb = !shallow && observe(val)
...
 get: function reactiveGetter () { // 收集获取值
   const value = getter ? getter.call(obj) : val
   if(Dep.target) { // 就是当前正在运行的watcher:Dep.target.addDep(this)【这个addDep是watcher中的方法】
      dep.depend() //操作:观察者dep 关联 被观察者watcher
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
}
// 接下来进入watcher.addDep()
    addDep (dep: Dep) {
      const id = dep.id
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id) // 将Dep实例保存到实例属性this.newDepIds中
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this)//传入当前watcher的实例,之后进入到dep.addSub
        }
      }
    }
// Dep中的addSub
    addSub (sub: Watcher) {
      this.subs.push(sub) // 这个就是吧dep收集到的watcher全部放入this.subs这个梳理里了
    }

其中:dep.depend()childOb.dep.depend()可以看出来是同一种操作。那为什么要出现两次呢?

看了一些资料:

childOb.dep.depend() 作用:是给Vue.set()方法和数组劫持用的。

代码中要存在childOb才会执行childOb.dep.depend(),那先看一下childOb是什么?

   let childOb = !shallow && observe(val)// childOb是Observe的实例,
   在Observe构造函数中添加了dep属性

我的源码学习之路(四)---vue-2.6.14

childOb是$data中的对象添加响应式后的结果,指的就是该对象的__ob__属性,就是将整个对象结构也加入到了依赖中,可以随时监控a.b.c有没有被修改或者删除。这样就可以理解上面说的作用的意思了

我的源码学习之路(四)---vue-2.6.14

后记

这个dep真的弄了好久才理解好。我感觉我应该是理解了。

希望以后面试的时候:我能我会我可以

本文仅作为自己一个阅读记录,具体还是要看大佬们的文章。

下一篇:我的源码学习之路(五)---vue-2.6.14

转载自:https://juejin.cn/post/7179541008122642491
评论
请登录