文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2022-12-14 06:03

关注

前言

可能很多小伙伴之前都了解过 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
      
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      
      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,在上面??看dep的定义中,dep.depend就是调用当前watcheraddDep方法,将当前的watcher观察者收集到dep的subs数组,这句childOb.dep.depend()是为$set量身定做的,事先将当前的watcher收集到Observer实例里的dep的subs数组中后续当我们使用$set方法时,只需要调用引用对象保存在_ob_的Observer实例中的dep的notify方法就能实现手动派发更新

 watcher.js
  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)
      }
    }
  }

this在这里是指向当前创建的watcher实例,这样这个watcher就被收集到该属性的dep对象的subs数组中了,到这里一依赖收集的主干流程就完成了

派发更新

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,
    ...
    set: 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 (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

触发set方法时就会进行派发更新,在set方法里我们首先对修改后的值和原来的值进行对比,如果相同就return,如果不相同就继续执行下面的逻辑,将新值赋值给旧值,childOb = !shallow && observe(newVal)这一段的逻辑是如果新值也是对象的话就对新值执行defineReactive将新值变为响应式对象,接下来我们看一下dep.notify()的定义

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

notify方法会遍历当前收集的watcher,调用每个watcher的update方法

update () {
    
    if (this.computed) {
      ...
    } else if (this.sync) {
      ...
    } else {
      queueWatcher(this)
    }
  }

update方法分别判断了是否是计算属性以及是否是同步的watcher,在当前执行的环境下都为false,所以执行了queueWatcher(this)

const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

因为多个dep有可能收集的是同一个watcher,当多个dep收集的同一个watcher时这个watcher只需要更新一次,所以通过这种方式去除重复watcher,由于flushingwaiting初始值都为false,所以会将经过过滤watcherpush进queue数组,然后会在下一个tick内执行flushSchedulerQueue

function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  queue.sort((a, b) => a.id - b.id)
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    ...

}

首先会根据id进行一次排序因为如果有嵌套组件的情况出现的话,是先更新父组件再更新子组件,所以进行了一次由小到大的排序,后续对每个watcher都调用了run方法

  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

active的默认值是true所以run方法里调用了getAndInvoke

getAndInvoke (cb: Function) {
    const value = this.get()
   ....
  }

绕了这么大一圈最后还是执行的watcher中的get方法,get方法在上面讲解watcher的时候就已经说过,这个get会执行this.getter而这个this.getter是创建watcher时传入的updateComponentupdateComponent的执行会导致重新渲染页面组件,也就是在属性被修改时触发了set方法,而这个set方法会将依赖当前属性的页面组件重新渲染,从而达到数据驱动的效果。

总结

vue用Object.defineProperty重写属性的get和set方法

以上就是面试官问你Vue2的响应式原理该如何回答?的详细内容,更多关于Vue2响应式原理的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯