响应式详细流程

如何把data里面的数据做成响应式的,能监听的到数据的变化,能通知到数据变化后相应模块的变化

1. Observe意思,作用

响应式对象,意思是把这个对象做成响应式的对象,不是针对某一个值,而是这个对象里面的所有。所以一个对象/数组就会创建一个Observe实例

作用(干什么的):

把一个对象本身以及对象里面所有的值做成响应式的对象

2. 代码执行流程

1) function Vue
2) this._init(options)
3) initState(vm)

状态初始化,props,data,methods,conputed等里面的状态初始化。我们主要看针对data的initData

4) initData(vm)

获取data,设置代理,判重,启动响应式

5) observe(data, true /* asRootData */)

判断不是对象或者是vnode则直接return,否则new一个Observe观察者

6) ob = new Observer(value)

带上自己的dep:this.dep = new Dep()

给对象加上响应式标志__ob__,标记为已经做过响应式的对象

改变数组原型链指向,使数组的push等方法重写,能被响应式

数组需要额外判断一遍数组内部是否有对象需要响应式

执行this.walk(value)

7) this.walk(value)

循环把对象里面的值做成响应式

8) defineReactive(obj, keys[i])

Object.defineProperty里面定义getter和setter函数

getter:依赖收集dep.depend(),Dep.target指向的是当前的watcher,depend就是收集这个watcher的,收集完以后tager立刻销毁

setter:派发更新dep.notify(),执行watcher里面的需要更新的函数

9) Dep通知的相关逻辑

接下去就是dep的事情,Observer观察者的任务到此结束

3. 具体详细代码逻辑步骤

1. initState(vm)函数

状态初始化,props,methods,conputed等里面的状态初始化

scss 复制代码
// 初始化props,data等
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) // 重点
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
2. initData函数
initData

主要作用:获取data,设置代理,判重,启动响应式

kotlin 复制代码
// 把data赋值给vm
function initData (vm: Component) {
  // 为什么在data里面定义了属性,可以通过this拿到,就是在这里做的
  let data = vm.$options.data
  // 因为对象是一个引用数据类型,如果data是一个对象的情况下会造成所有组件共用一个data。
  // 而当data是一个函数的情况下,每次函数执行完毕后都会返回一个新的对象,这样的话每个组件都会维护一份独立的对象(data)
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  if (!isPlainObject(data)) {
      // todo..开发环境不重要逻辑
  }
  // proxy data on instance 代理数据到实例上
  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') {
        // todo 去重methods
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        // todo 去重props
    } else if (!isReserved(key)) {
      // 把data上的东西代理到vm实例上,这一行解释了为什么可以通过this访问data上属性
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 响应式操作的地方
  observe(data, true /* asRootData */)
}
proxy(vm, _data, key)

把data上的东西代理到vm实例上,解释了为什么可以通过this访问data上属性

vbnet 复制代码
// 代理函数,this.xx访问为this._data.xx
export function proxy (target: Object, sourceKey: string, key: string) {
  // 通过这个函数,最终实际上访问的是vm._data.key
  sharedPropertyDefinition.get = function proxyGetter () {
    // this[sourceKey][key]其实是vm.$options.data[key]
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
3. observe(data, true /* asRootData */)

文件地址:src\core\observer\index.js

observe文件夹是MVVM框架最重要的文件夹,因为mvvm框架最重要的就是响应式

observe:观察者,把数据做成响应式的,我们看到的__ob__这个属性,就是Observer的实例

里面执行ob = new Observer(value)

typescript 复制代码
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 必须是对象/数组,且不能是vnode,vnode不需要观察
  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)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创建新的实例
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
4. Observer类

每一个响应式对象,都会有一个Observer类的实例__ob__

必须是对象或者数组才有,普通的原始值不会有

observe的作用是把这个对象里面的所有数据都变成响应式的,里面执行this.walk(value)

scss 复制代码
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
    // 为什么给每一个Observer设置一个dep?
    // object有新增或者删除属性,array中便跟方法
    // 那么都必须通过dep去通知
    this.dep = new Dep()
    this.vmCount = 0
    // 设置一个__ob__的属性,def的意思:Define a property
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        // 改写数组原型的指向,把__proto__指向了一个自己创建的对象,这个对象里面重新实现了push,pop等7个方法,剩余属性和数组的原型合并,这里就是为什么调用数组方法也能实现响应式的原因
        protoAugment(value, arrayMethods)
      } else {
        // 兼容性,没有原型则直接强硬覆盖(比如垃圾ie)
        copyAugment(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])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
5. defineReactive(obj, keys[i])

定义getter和setter做的事情

vbnet 复制代码
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 和key一一对应
  const dep = new Dep()
  // 如果数据是不可改变的则直接return,节约性能(vue性能优化点)
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.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) // val仍然为对象时候才有值,数组也算
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 这个dep是一个类,主要作用是建立起数据和watch之间的一个桥梁
      // target就是watcher,同一时间只有一个watch能被计算
      if (Dep.target) {
        // 依赖收集
        dep.depend()
        // 如果有子对象,则子对象也收集这个依赖(watcher)
        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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是一个对象,那么重新把它变成响应式对象
      childOb = !shallow && observe(newVal)
      // 这个函数就是派发更新的过程
      dep.notify()
    }
  })
}

4. 后续步骤(数据何时收集到watcher)

  1. 以上所有的步骤都是在initState(vm)函数里面执行,执行完后,此时知识初始化完数据,把数据变成响应式的,created的都还没有执行。创建了一大堆dep(每个组件里面的data都是一个数据对应一个dep),但是dep里面全都是空的,没有管理任何watcher。

  2. watcher在挂在的时候才会创建,是在vm. <math xmlns="http://www.w3.org/1998/Math/MathML"> m o u n t ( v m . mount(vm. </math>mount(vm.options.el)里面执行,在执行挂在的时候,会创建watcher,创建后target有值,然后会立即真正渲染一次(updateComponent),此时会访问当data里面的值,触发响应式getter,watcher被收集到了dep里面,然后target置为null,此为依赖收集过程。

  3. 全程没有触发过setter,setter是在数据二次变成时候才触发,初始化不触发,后续值二次更新才触发

挂载在初始化数据之后执行

scss 复制代码
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else { // new Vue时候触发
      // 把传入的options merge到$options上
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {}, // 这个option是new Vue传入的参数
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化props,data等
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */

    if (vm.$options.el) { // 判断有没有传el,然后挂载
      // 没传就不进行下一步操作,子组件也都会执行这些初始化,所以子组件就走不到这个逻辑,子组件内部会手动调用$mount
      vm.$mount(vm.$options.el)
    }
  }
}

挂载简易步骤

vm. <math xmlns="http://www.w3.org/1998/Math/MathML"> m o u n t ( v m . mount(vm. </math>mount(vm.options.el) = 》

return mountComponent(this, el, hydrating) =》

updateComponent =》

vm._update(vm._render(), hydrating)

patch等渲染步骤...

5. 问题知识点问答

1) 问:Observer类里为什么给每一个对象都设置一个dep

this.dep = new Dep()

答:

  1. 对象本身变化需要通知

dep的作用是通知更新,dep和key是一一对应的,这个对象本身也可以被重新赋值的(this.obj = 33),变化后也需要通知

2) 对象里面的数据的增加和删除需要通知,给$.set()使用。

对象的新增和删除obj.a = 11,以及数组的新增和删除是监听不到的,(Object.defineProperty监听不到删除事件,而新增监听不到是因为还没有被监听,修改某一个值能监听得到,因为修改的那个值已经被监听了)

所以要新增和删除属性要用 <math xmlns="http://www.w3.org/1998/Math/MathML"> . s e t ( ) 方法,可是 .set()方法,可是 </math>.set()方法,可是.set()方法怎么知道具体通知哪些个组件去跟新,所以在这个对象/数组上挂了dep,增加和删除属性仍然表示这个对象本身有变化,而不是对象里面的某个值有变化,所以触发这个对象上dep通知更新(具体更新啥由diff去计算,不会整个组件重新渲染一遍)

3) 问:Vue 中 Observer 的用处是什么?

详细:在 state.js 的 initData() 函数中,使用的是 observe() 方法来为数据对象绑定一个Observer对象,Observer对象执行 defineReactive() 方法为数据对象设置 setter 和 getter。

而在 initProps() 函数中,通过遍历 props 选项直接对数据执行了 defineReactive() 方法来设置 setter 和 getter。

那么,同样是为数据设置 setter 和 getter,为什么 initData() 比 initProps() 多一个 Observer 类,这个 Observer 类的功能到底是什么?

答案:

站在component的角度,props是immutable的,而data是mutable的

所以相对于props,data需要对对象的子对象以及数组内的元素都设置setter 和 getter

而props不用

所以Observer的功能,应该就是对数组元素的遍历执行defineReactive(),以及深度遍历Object为每一个子对象都执行defineReactive()

对应的就是Observer.observeArray和Observer.walk这两个方法

4) 问:dep和watcher的关系

总:Dep时管理watcher的。

Dep管理一组watcher,Dep关联的值更新时通知其管理watcher更新。Dep和watcher是多堆垛的关系,一个dep可能管理多个watch,因为在一个组件中,除了渲染watcher以外,还可能有多个用户watch,比如watch选项,或者是调用了$watch方法,或者是computed,此时就是一个key关联了多个watcher。watcher对应多个dep,因为一个组件当然可以绑定多个data中的key,

5) 问:依赖收集什么时候发生的(代码展示)

每一个组件都会有唯一对应的watcher。当watcher中的_render函数执行的时候,render函数会对界面中所有涉及到的数值进行访问,此时触发在defineReactive中定义的getter,于是依赖收集就发生了

6) vue2 中响应式的缺点

递归遍历,性能受影响(vue3的proxy可以解决)

api不统一,$set的数组和对象api不一致(vue3proxy可以解决)

相关推荐
熊的猫41 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
mosen8681 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
别拿曾经看以后~2 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel