vue2 响应式原理保姆级别

Posted twinkle||cll

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue2 响应式原理保姆级别相关的知识,希望对你有一定的参考价值。

面试题:请阐述vue2响应式原理

vue官方阐述:cn.vuejs.org/v2/guide/re…

响应式数据的最终目标,是当对象本身或对象属性发生变化时,将会运行一些函数,最常见的就是render函数。

在具体实现上,vue用到了几个核心部件

  1. Observer:
  2. Dep
  3. Watcher
  4. Scheduler

Observer

Observer要实现的目标非常简单,就是把一个普通的对象转换为响应式的对象

为了实现这一点,Observer把对象的每个属性通过Object.defineProperty转换为带有gettersetter的属性,这样一来,当访问或设置属性时,vue就有机会做一些别的事情。

代码实现响应式

/**
 * Define a reactive property on an Object.
 * 定义一个响应式数据
 */
export function defineReactive (
  obj: Object, // 传入的对象
  key: string, // 对象属性名
  val: any,    // 对象属性的值
  customSetter?: ?Function, // 自定义的setter
  shallow?: boolean // 不进行深度响应式
) 
  // 创建一个依赖实例对象
  const dep = new Dep()
  //  获取当前属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果对象不可以进行配置,直接返回
  if (property && property.configurable === false) 
    return
  

  // cater for pre-defined getter/setters 满足预定义的getter/setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) 
    val = obj[key]
  
  // 深度响应式的话,调用observe方法
  let childOb = !shallow && observe(val);
  // 使用Object.defineProperty来进行setter和getter,这样就能进行
  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()
          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()
    
  )


复制代码

上面列举是是单个对象的响应式,实际上如果是对象里面嵌套对象,需要进行递归遍历对象的所有属性,以完成深度的属性转换

Observer是vue内部的构造器,在vue2.6以后,我们可以通过Vue提供的静态方法Vue.observable( object )间接的使用该功能。 api的具体实现

// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => 
  // 将数据进行响应式后直接返回数据,由于对象是引用传递,所以会有以下代码
  observe(obj)
  return obj

复制代码

observe具体实现

/**
 * 尝试为值创建观察者实例,如果成功观察,则返回新的观察者,如果该值已有一个观察者,则返回现有的观察者。
 */
export function observe (value: any, asRootData: ?boolean): Observer | void 
  // 如果传入的数据不是对象或者是vue虚拟节点,直接返回
  if (!isObject(value) || value instanceof VNode) 
    return
  
  
  let ob: Observer | void
  // 判断传入的数据是否有 __ob__的原型并且,value的原型是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
  ) 
  // 为value创建一个value实例对象,Observer(观察者)将目标对象的属性键转换为getter/setter,用于收集依赖项并发送更新
    ob = new Observer(value)
  
  if (asRootData && ob) 
    ob.vmCount++
  
  return ob

复制代码

在组件生命周期中,这件事发生在beforeCreate之后,created之前。

由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加删除的属性,因此vue提供了$set$delete两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。 $set的实现方法

/**
 * 设置对象的属性。添加新属性并在该属性不存在时触发更改通知。
 */
export function set (target: Array<any> | Object, key: any, val: any): any 
// 判断目标对象
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) 
    warn(`Cannot set reactive property on undefined, null, or primitive value: $(target: any)`)
  
  // 如果对象是数组并且并且下标是一个数组有效的索引(数字)
  if (Array.isArray(target) && isValidArrayIndex(key)) 
   // 扩大数组长度
    target.length = Math.max(target.length, key)
    // 放入数据 
    target.splice(key, 1, val)
    return val
  
  // 如果属性存在目标对象中,但是不存在于超类Object的原型对象上
  if (key in target && !(key in Object.prototype)) 
    target[key] = val
    return val
  
  const ob = (target: any).__ob__;
  // 对象不能是 Vue 实例,或者 Vue 实例的根数据对象
  if (target._isVue || (ob && ob.vmCount)) 
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  
  // ob 不存在,直接往对象中添加属性
  if (!ob) 
    target[key] = val
    return val
  
  // 把属性变为响应式的属性
  defineReactive(ob.value, key, val)
  // 依赖通知用到该对象的进行render更新
  ob.dep.notify()
  return val

复制代码

$del的实现

/**
 * 删除属性并在必要时触发更新。
 */
export function del (target: Array<any> | Object, key: any) 
// 和set一样,判断目标是否是引用值
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) 
    warn(`Cannot delete reactive property on undefined, null, or primitive value: $(target: any)`)
  
  // 判断目标是否是数组,并且判断key是否是一个数字
  if (Array.isArray(target) && isValidArrayIndex(key)) 
    target.splice(key, 1)
    return
  
  const ob = (target: any).__ob__;
  // 对象不能是 Vue 实例,或者 Vue 实例的根数据对象
  if (target._isVue || (ob && ob.vmCount)) 
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  
  // 如果target不存在key,直接返回
  if (!hasOwn(target, key)) 
    return
  
  // 存在进行删除
  delete target[key]
  if (!ob) 
    return
  
  // 进行通知,render进行渲染
  ob.dep.notify()

复制代码

✨✨✨注意:通过$set, $del, 我们会发现,这两个方法虽然是官方提供的方法,但是尽量少用,毕竟需要进行好多的判断,然后来进行通知render

对于数组,vue会更改它的隐式原型,之所以这样做,是因为vue需要监听那些可能改变数组内容的方法

将数组变为响应式的关键代码

// 判断是否有对象原型
if (hasProto) 
  // 通过使用__proto__拦截原型链来扩充目标数组
  protoAugment(value, arrayMethods) 
  上面这句话等于  value.__proto__ = arrayMethods
 else 
// 通过定义隐藏属性来扩充目标对象或数组。
  copyAugment(value, arrayMethods, arrayKeys)

// 将数组变成响应式
this.observeArray(value)
复制代码

总之,Observer的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被vue感知到。使得Vue能够在数据改变,来做一些事情。。

Dep

这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠Dep来解决。

Dep的含义是Dependency,表示依赖的意思。

Vue会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep实例,每个Dep实例都有能力做以下两件事:

  • 记录依赖:当读取响应式对象的某个属性时,它会进行依赖收集
  • 派发更新:当改变某个属性时,它会派发更新

响应式对象创建Dep核心代码

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
    // 创建一个dep实例,每一个响应对象都会有一个Dep实例哦
    this.dep = new Dep()
    this.vmCount = 0
   // …… 把数据变成响应式的数据
  
复制代码

Dep本是一个发布订阅模式

/**
 * dep是一个可观察对象,可以有多个指令订阅它
 */
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 () 
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) 
     // 这里交给update去更新依赖
      subs[i].update()
    
  

复制代码

Watcher

这里又出现一个问题,就是Dep如何知道是谁在用我?

要解决这个问题,需要依靠另一个东西,就是Watcher

当某个函数执行的过程中,用到了响应式数据,响应式数据是无法知道是哪个函数在用自己的,vue把函数交给一个叫做watcher的东西去执行,watcher是一个对象,每个这样的函数执行时都应该创建一个watcher,通过watcher去执行. watch 简化版

观察者解析表达式,收集依赖项,并在表达式值更改时触发回调。这用于$watch()api和指令

export default class Watcher 
    constructor(
        vm: Component,
        expOrFn: string | Function,   // 要 watch 的属性名称
        cb: Function,    // 回调函数
        options?: ?Object,   // 配置参数
        isRenderWatcher?: boolean  // 是否是渲染函数观察者,Vue 初始化时,这个参数被设为 true
    ) 
        // 省略部分代码... 这里代码的作用是初始化一些变量
        // expOrFn 可以是 字符串 或者 函数
        // 什么时候会是字符串,例如我们正常使用的时候,watch:  x: fn , Vue内部会将 `x` 这个key 转化为字符串
        // 什么时候会是函数,其实 Vue 初始化时,就是传入的渲染函数 new Watcher(vm, updateComponent, ...);
        
        if (typeof expOrFn === 'function') 
            this.getter = expOrFn
         else 
        
            // 当 expOrFn 不为函数时,可能是这种描述方式:watch: 'a.x'() //do  ,具体到了某个对象的属性
            // 这个时候,就需要通过 parsePath 方法,parsePath 方法返回一个函数
            // 函数内部会去获取 'a.x' 这个属性的值了
            
            this.getter = parsePath(expOrFn)
            
            // 省略部分代码...
        
        
        // 这里调用了 this.get,也就意味着 new Watcher 时会调用 this.get
        // this.lazy 是修饰符,除非用户自己传入,不然都是 false。可以先不管它
        
        this.value = this.lazy? undefined: this.get()
    
    
    get () 
        // 将 当前 watcher 实例,赋值给 Dep.target 静态属性
        // 也就是说 执行了这行代码,Dep.target 的值就是 当前 watcher 实例
        // 并将 Dep.target 入栈 ,存入 targetStack 数组中
        
        pushTarget(this)
        
        // 省略部分代码...
        
        try 
        
            // 这里执行了 this.getter,获取到属性的初始值
            // 如果是初始化时 传入的 updateComponent 函数,这个时候会返回 udnefined
            
            value = this.getter.call(vm, vm)
            
         catch (e) 
            // 省略部分代码...
         finally 
            // 省略部分代码...
            
            // 出栈
            popTarget()
            
            // 省略部分代码...
        
        
        // 返回属性的值
        return value
    
    
    // 这里再回顾一下
    // dep.depend 方法,会执行 Dep.target.addDep(dep) 其实也就是 watcher.addDep(dep)
    // watcher.addDep(dep) 会执行 dep.addSub(watcher)
    // 将当前 watcher 实例 添加到 dep 的 subs 数组 中,也就是收集依赖
    // dep.depend 和 这个 addDep 方法,有好几个 this, 可能有点绕。
    
    addDep (dep: Dep) 
        const id = dep.id
        
        // 下面两个 if 条件都是去重的作用,我们可以暂时不考虑它们
        // 只需要知道,这个方法 执行 了 dep.addSub(this)
        
        if (!this.newDepIds.has(id)) 
            this.newDepIds.add(id)
            this.newDeps.push(dep)
            if (!this.depIds.has(id)) 
            
                // 将当前 watcher 实例添加到 dep 的 subs 数组中
                
                dep.addSub(this)
            
        
    
    
    // 派发更新
    
    update () 
    
        // 如果用户定义了 lazy ,this.lazy 是描述符,我们这里可以先不管它
        
        if (this.lazy) 
            this.dirty = true
            
        // this.sync 表示是否改变了值之后立即触发回调。如果用户定义为true,则立即执行 this.run
         else if (this.sync) 
        
            this.run()
            
        // queueWatcher 内部也是执行的 watcher实例的 run 方法,只不过内部调用了 nextTick 做性能优化。
        // 它会将当前 watcher 实例放入一个队列,在下一次事件循环时,遍历队列并执行每个 watcher实例的run() 方法
        
         else 
            queueWatcher(this)
        
    
    
    run () 
        if (this.active) 
            // 获取新的属性值
            const value = this.get()
            if (
                // 如果新值不等于旧值
                
                value !== this.value ||
                
                // 如果新值是一个 引用 类型,那么一定要触发回调
                // 举个例子,如果旧值本来就是一个对象,
                // 在新值内,我们只改变对象内的某个属性值,那新值和旧值本身还是相等的
                // 也就是说,如果 this.get 返回的是一个引用类型,那么一定要触发回调
                
                isObject(value) ||
                
                // 是否深度 watch 
                this.deep
            ) 
                // set new value
                const oldValue = this.value
                this.value = value
                // this.user 是一个标志符,如果开发者添加的 watch 选项,这个值默认为 true
                // 如果是用户自己添加的 watch ,就加一个 try catch。方便用户调试。否则直接执行回调。
                
                if (this.user) 
                    try 
                    
                        // 触发回调,并将 新值和旧值 作为参数
                        // 这也就是为什么,我们写 watch 时,可以这样写: function (newVal, oldVal)  // do 
                        
                        this.cb.call(this.vm, value, oldValue)
                        
                     catch (e) 
                        handleError(e, this.vm, `callback for watcher "$this.expression"`)
                    
                 else 
                    this.cb.call(this.vm, value, oldValue)
                
            
        
    
    
    // 省略部分代码...
    

复制代码

watcher会设置一个全局变量,让全局变量记录当前负责执行的watcher等于自己,然后再去执行函数,在函数的执行过程中,如果发生了依赖记录dep.depend(),那么Dep就会把这个全局变量记录下来,当Dep进行派发更新时,它会通知之前记录的所有watcher进行更新,执行run函数

  • 每一个vue组件实例,都至少对应一个watcher,该watcher中记录了该组件的render函数。

  • watcher首先会把render函数运行一次以收集依赖,于是那些在render中用到的响应式数据就会记录这个watcher。

  • 当数据变化时,dep就会通知该watcher,而watcher将重新运行render函数,从而让界面重新渲染同时重新记录当前的依赖。

Scheduler

现在还剩下最后一个问题,就是Dep通知watcher之后,如果watcher执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下

试想,如果一个交给watcher的函数,它里面用到了属性a、b、c、d,那么a、b、c、d属性都会记录依赖,于是下面的代码将触发4次更新:

state.a = "new data";
state.b = "new data";
state.c = "new data";
state.d = "new data";
复制代码

这样显然是不合适的,因此,watcher收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给一个叫调度器的东西

调度器核心代码

export function queueWatcher (watcher: Watcher) 
  const id = watcher.id
  if (has[id] == null) 
    has[id] = true
    // 判断一个极限的情况,是否正在入队
    if (!flushing) 
      queue.push(watcher)
     else 
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) 
        i--
      
      // 进行出队
      queue.splice(i + 1, 0, watcher)
    
    // queue the flush 队列正在刷新
    if (!waiting) 
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) 
        flushSchedulerQueue()
        return
      
      // 放入nextick来进行微队列进行执行
      nextTick(flushSchedulerQueue)
    
  

复制代码

调度器维护一个执行队列,该队列同一个watcher仅会存在一次,队列中的watcher不是立即执行,它会通过一个叫做nextTick的工具方法,把这些需要执行的watcher放入到事件循环的微队列中,nextTick的具体做法是通过Promise完成的

nextTick 通过 this.$nextTick 暴露给开发者

nextTick 的具体处理方式见:cn.vuejs.org/v2/guide/re…

也就是说,当响应式数据变化时,render函数的执行是异步的,并且在微队列中

nextick 核心方法

export function nextTick (cb?: Function, ctx?: Object) 
  let _resolve
  callbacks.push(() => 
    // 在callbacks这个栈种维护函数
    if (cb) 
      try 
      // 改变cb的上下文
        cb.call(ctx)
       catch (e) 
        handleError(e, ctx, 'nextTick')
      
     else if (_resolve) 
      _resolve(ctx)
    
  )
  // 当不在等着中
  if (!pending) 
    pending = true
    // 执行timerFuc函数,这个函数的实现会根据当前的环境来决定。
    // Vue 在内部对异步队列尝试使用原生的 `Promise.then`、`MutationObserver` 和 `setImmediate`,
    // 如果执行环境都不支持,则会采用 `setTimeout(fn, 0)` 代替。
    
    timerFunc()
  
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') 
    return new Promise(resolve => 
      _resolve = resolve
    )
  

复制代码

总体流程

以上是关于vue2 响应式原理保姆级别的主要内容,如果未能解决你的问题,请参考以下文章

vue2 响应式原理保姆级别

Vue2/Vue3 响应式原理

vue2数据响应式原理

vue2响应式原理总结

vue2与vue3响应式原理

vue2响应式原理