vue核心面试题(vue响应式数据的原理)

响应式数据的原理?

1.首先回答对响应式的个理解,提出核心点是通过Object.defineProperty, Vue 在初始化数据时,会传入一个data对象,内部会默认的对data对象进行遍历,使用 Object.defineProperty 重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通 知相关依赖进行更新操作。 

2.为什么要使用Object.defineProperty?

    它的特点可以使数据的获取和设置增加一个拦截的功能,我们可以在获取和设置的时候增加一些我们自己的逻辑,这个逻辑叫做依赖收集。比如我们取数据的时候可以收集一些依赖,过一会数据变化了可以告诉这些收集的依赖去更新。

3.原理:在vue初始化的时候,会调用一个方法initData,用来初始化用户传入的data数据,然后new Observer,对数据进行观测,如果数据是个对象类型非数组的话,就会调一个this.walk(value)方法进行对象的处理,将对象进行遍历,然后使用defineReactive重新定义,采用的就是Object.defineProperty。

总结:

        Vue采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,vue在初始话的时候,在initState方法中会调取initData方法初始化data数据,对data对象进行遍历,在这个方法中会调observe(监听器,观察者)对用户的数据进行监听,在observe中会对对象new Observe实例化创建监听,在observe中对数据进行判断,如果是数组执行observeArray深度进行监听,继续执行observe方法,如果当前传入的是对象则执行this.walk,对对象进行循环,重新定义对象的属性,这时使用的就是defineReactive,它是vue中的一个核心方法,用来定义响应式。在defineReactive方法中实例化了一个Dep(发布者),通过Object.defineProperty对数据进行拦截,把这些 property 全部转为 getter/setter。get数据的时候,通过dep.depend触发Watcher(订阅者)的依赖收集,收集订阅者,如果数据是数组,执行dependArray,对数组中的每个值通过depend都添加到依赖。set时,会对数据进行比较,如果数据发生了变化会通过dep.notify发布通知,通知watcher,更新视图。

4.下面来看源码

文件路径:src/core/instance/state.js

源码:

(1)vue在state数据初始化的时候会掉state.js中的一个initState方法

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(初始化data)

function initData (vm: Component) {
  let data = vm.$options.data // 用户传入的数据
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    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)
  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)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */) // 调observe对用户的数据进行观测
}

(3)observe方法(文件路径:src/core/observe/index.js,专门用来控制数据响应式处理的)

         里面会去判断,看数据是否已经被观测过了,如果没又被观测过就会new Observer去观测这个对象

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) { // 不是对象不进行监听
    return
  }
  let ob: Observer | void
  // 如果存在__ob__属性,说明该对象没被observe过,不是observer类
  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)new Observer实例(监听的是两种类型,一种是数组,一种是对象)

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; 
  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) // 重新哦定义对象类型数据
    }
  }
  // 对当前传入的对象进行循环,重新定义对象的所有属性
  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])
    }
  }
}

(5)defineReactive是vue中的核心方法,用来定义响应式,该方法中使用了Object.defineProperty()

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
 // 实例化一个Dep,这个Dep存在在下面的get和set函数的作用域中,用于收集订阅数据更新的Watcher
 // 这里一个Dep与一个属性(即参数里的key)相对应,一个Dep可以有多个订阅者。
  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
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val) // 如果还是一个对象,会继续调用observe方法,递归监听
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () { // 数据的取值
      // 如果这个属性在转化之前定义过getter,那么调用该getter得到value的值,否则直接返回val。
      const value = getter ? getter.call(obj) : val
      /*
      *注意这里,这里是Dep收集订阅者的过程,只有在Dep.target存在的情况下才进行这个操作
      *在Watcher收集依赖的时候才会设置Dep.target
      *所以Watcher收集依赖的时机就是Dep收集订阅者的时机。
      *调用get的情况有两种
      *一是Watcher收集依赖的时候(此时Dep收集订阅者)
      *二是模板或js代码里用到这个值,这个时候是不需要收集依赖的,只要返回值就可以了。
      */
      if (Dep.target) {
        dep.depend() // 收集依赖 watcher,当数据变了,会通知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 */
      // 当前的值和数据的值不一样的话,就会掉一个核心的方法dep.notify()
      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() // 触发数据对应的依赖进行更新,它就会通知视图更新
    }
  })
}

(6)dependArray()方法,如果不是对象是数组调用该方法

         在调用这个函数的时候,数组已经被observe过了,且会递归observe。(看上面defineReactive函数里的这行代码:var childOb = observe(val);)
         所以正常情况下都会存在__ob__属性,这个时候就可以调用dep添加依赖了。

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend() // 添加依赖
    if (Array.isArray(e)) {
      dependArray(e) // 如果还是数据继续调用
    }
  }
}

(7)dep,(用来添加依赖,通知watcher更新数据)

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++ // 每个实例中的dep实例的id都是从0开始累加的
    this.subs = []
  }
  // 添加一个订阅者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // 删除一个订阅者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  // 追加依赖
  //让Watcher收集依赖并添加订阅者。
  //Dep.target是一个Watcher, 可以查看Watcher的addDep方法。
  //这个方法做的事情是:收集依赖后,调用了Dep的addSub方法,给Dep添加了一个订阅者
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
 // 通知watcher更新,通过调用subs里面的每个Watcher的update发布更新
  notify () {
    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文件下的update方法去更新数据
    }
  }
}

5.getter/setter方法拦截数据的不足

(1)当对象增删的时候,是监控不到的,因为在observe data的时候,会遍历已有的每个属性添加getter/setter,而后面设置的属性并没有机会设置getter/setter,所以检测不到变化,同样的,删除对象属性的时候,getter/setter会跟着属性一起被删除掉,拦截不到变化。可以通过vue.set和vue.delete去更新数据。

(2)getter/setter是针对对象的,对于数组的修改也是监控不到的。可以通过调用数组的变异方法,或对数组进行深度拷贝克隆来达到更新视图。

二、相关概念

1.双向数据绑定

M ,即 model,指的是模型,也就是数据;V 即view,指的是视图,也就是页面展现的部分。
双向数据绑定为:每当数据有变更时,会进行渲染,从而更新视图,使得视图与数据保持一致(model到view层);而另一方面,页面也会通过用户的交互,产生状态、数据的变化,这个时候,这时需要将视图对数据的更新同步到数据(view到model层)。

我们上面说的Vue的数据响应式原理其实就是实现数据到视图更新原理,而视图到数据的更新,其实就是此基础上给可表单元素(input等)添加了change等事件监听,来动态修改model和 view。

2.发布-订阅者模型

订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。
Vue中的Dep和Watcher共同实现了这个模型

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值