面试官问你Vue2的响应式原理该如何回答?

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

面试官问你Vue2的响应式原理该如何回答?

清风丶   2022-12-13 我要评论

前言

可能很多小伙伴之前都了解过 Vue2实现响应式的核心是利用了ES5的Object.defineProperty 但是面对面试官时如果只知道一些模糊的概念,回答肯定是虎头蛇尾的,只有深入底层了解响应式的原理,才能在关键时刻对答如流,百毒不侵。

响应式对象

Object.defineProperty方法的官方解释是可以直接在一个对象上定义一个新属性或者修改一个对象的现有属性

let object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
});
console.log(object1);
//{property1: 42}

经过Object.defineProperty定义后,object1就有了一个property1属性

并且通过这种方式能为属性添加getset方法,

当一个对象的属性都拥有get和set方法时,就可以称这个对象为响应式对象

let object1 ={}
Object.defineProperty(object1, "name", {
  get() {
    return 1;
  },
  set(x) {
    console.log("数据变化了",1+x)
  }
});
console.log(object1)
当我们为object1添加name属性以及get和set方法时
//{
//  name:1
//  get name:function()...
//  set name:function()...
//}
console.log(object1.name)
//1
object1.name = 1
//数据变化了 2

当我们读取object1的name值会触发get方法 这里会打印出1

当我们修改object1的name值会触发set方法 这里会打印出 ”数据变化了 2“

响应式开始的地方

vue源码中在初始化data的方法initData有一句 observe(data)这就是梦开始的地方,让我们看一下observe具体实现

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{
    ob = new Observer(value)
  }
  return ob
}

首先对传入的值做了一些类型校验,如果不是引用类型或者是vNode实例就直接return返回

接下来判断对象下是否有_ob_属性,如果有直接返回否则执行new Observer(value)那么这个_ob_以及 Observer是什么东西呢?我们接着往下看

Observer

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)) {
      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])
    }
  }

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

Observer的逻辑也非常简单,首先为传入的这个引用类型new了一个dep这个dep主要是为了后续的$set方法使用暂且不看并将当前的observer实例储存在目标的_ob_里,上面说的observe方法会根据这个_ob_进行判断,这样是为了防止data里的属性相互引用导致多次生成新实例接下来判断如果是对象类型则对每个属性执行defineReactive方法

如果是数组类型则遍历数组对每个子项执行observe方法,observe方法上面我们说过,它会根据值的类型进行判断如果是数组或者对象就执行new Observer这一层的套娃实际上是对数组的层层解析,目的就是为了让数组里的对象都执行defineReactive方法

实现响应式的defineReactive

vue2的源码中是通过递归调用defineReactive方法将所有对象变为响应式对象接下来我们简单看一下defineReactive的主要逻辑

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
 ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
         
        }
      }
      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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

首先为每一个对象属性都添加的getset方法

并且为每个属性都new一个dep,这个dep接下来会介绍,值得一提的是这句let childOb = !shallow && observe(val)observe我们上面说了它会对所有对象以及数组里嵌套的对象执行defineReactive这段逻辑就是在递归调用defineReactive方法,这样不管我们对象套了多少层,它都能实现响应vue的响应式实际上的经典的观察者模式dep在get方法里实现对观察者watcher进行收集,在set方法里通知每个观察者watcher执行 update 方法,想要了解过程,接下来我们重点看一下depwatcher的定义

dep

dep.js
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充当一个中间桥梁的作用,收集以及维护观察者,在目标属性发生变化时调用自己的notify方法,对每个观察者都执行update方法通知观察者需要更新

watcher

watcher.js
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } 
      if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
     }
    }
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  addDep (dep: Dep) {
    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)
      }
    }
  }

  update () {
    if (this.computed) {
      if (this.dep.subs.length === 0) {
        this.dirty = true
      } else {
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

watcher中的逻辑不适合单独拆开解析,接下来我们结合流程分析,watcher首次创建实例的场景,这是在第一次渲染页面组件的时候,我们传入的expOrFn参数对应的是updateComponentupdateComponent是vue定义的用于重新渲染页面组件的函数,在代码中updateComponent又被赋值给了this.getter

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true )

接着往下看,由于当前不是计算属性所以this.computed是false,执行this.value = this.get()在执行get()的时候会执行到pushTarget方法

Dep.target = null
function pushTarget (_target: ?Watcher) {
  ...
  Dep.target = _target
}

这个方法实际上是将当前watcher赋值给了Dep.targetDep.target是一个全局变量,为啥要这么干呢

因为会有组件嵌套的情况,所以可能会有多个渲染watcher,但是通过这种方式就这样保证了 Dep.target指向的是最新创建的watcher,接下来执行了value = this.getter.call(vm, vm),上面说了这个this.getter就是传入的updateComponent,这个updateComponent就是页面组件重新渲染的方法,
流程分别是:生成vnode->根据vnode树生成真实的dome树->挂载到页面上,在使用rander生成vnode的时候就会读取到模版语法中的值,当访问到值时就触发了我们通过defineReactive方法添加的get方法,就触发了依赖收集过程。

依赖收集

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
 ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    ...
  })
}

刚刚我们说到Dep.target就是当前的watcher,在上面

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们