Vue3数据响应系统

更新日期: 2019-10-06阅读: 2.4k标签: Vue3

vue3 就是基于 Proxy 对其数据响应系统进行了重写,现在这部分可以作为独立的模块配合其他框架使用。数据响应可分为三个阶段: 初始化阶段 --> 依赖收集阶段 --> 数据响应阶段


Proxy代理须知

用 Proxy 做代理时,我们需要了解几个问题:

1、 Proxy 代理是如何对其 trap 进行处理来实现数据响应的?也就是其 get/set 里面是如何做拦截处理(其实这里的trap默认行为可以通过 Reflect 来返回, Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。这里具体可以查看阮大神的 ES6入门 )

2、 Proxy 代理的对象只能代理到第一层,当代理的对象多层嵌套时,那么对象内部的深度监测需要如何去实现?

3、当代理对象是数组时,比如push操作会触发多次 get/set ,因为push操作除了增加数组的数据项之外,也会引发数组本身其他相关属性的改变,因此会多次触发 get/set ,那么要如何解决呢?

下面我们会稍微分析下 Vue3 针对这几个问题做了哪些优化处理。


初始化阶段

初始化过程相对比较简单,通过 reactive() 方法将数据转化成 Proxy 对象,这里注意一个比较重要的对象 targetMap ,它在依赖收集阶段起着比较重要的作用,具体下面会有分析。

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

...

// 创建proxy对象
function createReactiveObject(
  target: any,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target already has corresponding Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

Vue3 如何进行深度观测的?先看下面这段代码

let data = { x: {y: {z: 1 } } }
let p = new Proxy(data, {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    console.log('get value:', key)
    console.log(res)
    return res
  },
  set(target, key, value, receiver) {
    console.log('set value:', key, value)
    return Reflect.set(target, key, value, receiver)
  }
})
p.x.y = 2

// get value: x
// {y: 2}

上面代码我们可以知道 Proxy 只会代理一层,因为这里只是触发了一次最外层属性 x 的 get,而重新赋值的其内部属性 y ,此时 set 并没有被触发,所以改变内部属性是不会监测到的。继续看,Reflect.get返回的结果正是 target 的内层结构,此时 p.x.y 的值也已经变成 2 了,我们可以判断当前 Reflect.get 返回的值是否为 object ,若是则再通过 reactive 做代理,这样就达到了深度观测的目的了。

Vue3实现过程具体我们可以看下面源码:

function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    const res = Reflect.get(target, key, receiver)
    if (typeof key === 'symbol' && builtInSymbols.has(key)) {
      return res
    }
    if (isRef(res)) {
      return res.value
    }
    track(target, OperationTypes.GET, key)
    // 当代理的对象是多层结构时,Reflect.get会返回对象的内层结构,我们可以拿到当前res再做判断是否为object,进而进行reactive,就达到了深度观测的目的了
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}


依赖收集阶段

所谓的依赖在Vue3可简单理解为各种 effect 响应式函数,其中包括了属性依赖的 effect ,计算属性 computedEffect 以及组件视图的 componentEffect

1、在视图挂载渲染时会执行一个 componentEffect ,触发相关数据属性getter操作来完成视图依赖收集。

2、 effect 函数执行也会触发相关属性的getter操作,此时操作了某个属性的 effect 也会被该属性对应进行收集(注意这里的属性是可观测的)。

之所以说是响应式的,是因为effect方法回调中关联了被观测的数据属性,而effect一般是立即执行的,此时触发了该属性的 getter ,进行依赖收集,当该属性触发 setter 时,便会触发执行收集的依赖。另外,这里每次effect执行时,当前的effect会被压入一个名为 activeReactiveEffectStack 的栈中,是在依赖收集的时候使用。

export function effect(
  fn: Function,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
  if ((fn as ReactiveEffect).isEffect) {
    fn = (fn as ReactiveEffect).raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // effect立即执行,触发effect回调函数fn中相关响应数据属性的getter操作,从而进行依赖收集
    effect()
  }
  return effect
}
...
// 触发getter操作,进行依赖收集
export function track(
  target: any,
  type: OperationTypes,
  key?: string | symbol
) {
  if (!shouldTrack) {
    return
  }
  const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
  if (effect) {
    if (type === OperationTypes.ITERATE) {
      key = ITERATE_KEY
    }
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key!)
    if (dep === void 0) {
      depsMap.set(key!, (dep = new Set()))
    }
    // 防止依赖重复收集
    if (!dep.has(effect)) {
      dep.add(effect)
      effect.deps.push(dep)
      if (__DEV__ && effect.onTrack) {
        effect.onTrack({
          effect,
          target,
          type,
          key
        })
      }
    }
  }
}

开头说过 targetMap 对象在依赖收集过程中的重要作用,看源码我们大概知道了,它维护了一个依赖收集的关系表, targetMap 是一个 WeakMap ,其 key 值是当前被代理的对象 target ,而 value 则是该对象所对应的 depsMap ,它是一个 Map , key 值为触发 getter 时的属性值,而 value 值则是触发过该属性值所对应的各个 effect 。

故 targetMap 的关系映射可以看成 target --> key --> effect ,可以看出 target 被观测后,其属性 key 在被触发 getter 操作时,收集了所依赖的 effect ,可以说 targetMap 是Vue3进行依赖收集的一个核心对象。


响应阶段

当触发属性 setter 时,通过 trigger 函数会执行属性对应收集的 effects ,也包括 computedEffects ,此时通过 scheduleRun 逐个调用 effect ,最后完成视图更新。

上面我们讲过监测数组的时候可能触发多次 get/set , 那么如何防止触发多次的呢?先看Vue3的源码(简写省略了部分代码):

// setter操作触发响应
function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean {
  value = toRaw(value)
  // 判断key是否为当前target自身属性
  const hadKey = hasOwn(target, key)
  // 获取旧值
  const oldValue = target[key]
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  const result = Reflect.set(target, key, value, receiver)
  ...
  if (!hadKey) {
    // 若属性不存在标记为add操作
    trigger(target, OperationTypes.ADD, key)
  } else if (value !== oldValue) {
    // 若值不相等在触发,并且标记为set操作
    trigger(target, OperationTypes.SET, key)
  }
  ...
  return result
}

export function trigger(
  target: any,
  type: OperationTypes,
  key?: string | symbol,
  extraInfo?: any
) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // never been tracked
    return
  }
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 这里遍历找出相关依赖的effect
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      // 这里当改变数组length长度时也会触发相关effect进行响应
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  // 遍历执行依赖的effect
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  computedRunners.forEach(run)
  effects.forEach(run)
}

function scheduleRun(
  effect: ReactiveEffect,
  target: any,
  type: OperationTypes,
  key: string | symbol | undefined,
  extraInfo: any
) {
  ...
  if (effect.scheduler !== void 0) {
    effect.scheduler(effect)
  } else {
    effect()
  }
}

由源码我们可以分析出:1、判断key是否为当前被代理对象target自身属性; 2、判断旧值与新值是否相等。只有这两个条件其中一个满足,才有可能执行 trigger 。

怎么理解呢,我们举个:chestnut:,可以实现一个小的 reactive 方法来做数据代理,代码如下:

function hasOwn(val, key) {
  const hasOwnProperty = Object.prototype.hasOwnProperty
  return hasOwnProperty.call(val, key)
}
function reactive(data) {
  let observed = new Proxy(data, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      return res
    },
    set(target, key, value, receiver) {
      console.log(target, key, value)
      const hadKey = hasOwn(target, key)
      const oldValue = target[key]

      const result = Reflect.set(target, key, value, receiver)
      if (!hadKey) {
        console.log('trigger add operation...')
      } else if(value !== oldValue) {
        console.log('trigger set operation...')
      }

      return result
    }
  })
  return observed
}

let data = ['a', 'b']
let state = reactive(data)
state.push('c')

// ["a", "b"] "2" "c"
// trigger add operation...
// ["a", "b", "c"] "length" 3

state.push(‘c’) 会触发两次 set ,一次是push的值 c ,一次是 length 属性设置。

1、设置值 c 时,新增了索引 key 为 2,*target 是原始的代理对象 [‘a’, ‘c’] ,这是一个add 操作, 故 hasOwn(target, key) 返回的是false,此时执行 trigger add operation… 。注意在trigger 方法中, length 没有对应的 effect ,所以就没有执行相关的 effect 。

2、当传入 key 为 length 时, length 是自身属性,故 hasOwn(target, key) 返回 true , 此时 value 是 3, 而 oldValue 即为 target[‘length’] 也是 3,故 value !== oldValue 不成立,不执行 trigger 方法

故只有当 hasOwn(target, key) 返回true或者 value !== oldValue 的时候才执行 trigger 。


总结

在分析源码之前我们先列举了用Proxy做代理实现数据响应需要解决的几个问题,并带着这些问题一步一步揭开Vue在数据响应系统处理这些问题的面纱,也让我们进一步了解了Vue源码编写有许多巧妙的地方,比如利用 Reflect.get 返回值为 target 当前触发的第一层属性 key 值对应的 value 值,从而再来判断是否为Object来进行深度观测,并且观测的值存放在一个WeakMap下,这样相比较递归Proxy,Vue的这种实现方式大大提高了数据响应的性能。


链接: https://www.fly63.com/article/detial/5872

vue3.x 新特性 - CompositionAPI

安装 vue-cli3,在使用任何 @vue/composition-api 提供的能力前,必须先通过 Vue.use() 进行安装,安装插件后,您就可以使用新的 Composition API 来开发组件了。

快速进阶Vue3.0

在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.0 正式版。新版Vue 3.0计划并已实现的主要架构改进和新功能:

Vue 3 对 Web 应用性能的改进

有关即将发布的 Vue.js 的第 3 个主要版本的信息越来越多。通过下面的讨论,虽然还不能完全确定其所有内容,但是我们可以放心地认为,它将是对当前版本(已经非常出色)的巨大改进。 Vue 团队在改进框架 API 方面做得非常出色

Vue3 中令人兴奋的新功能

用新的 Vue 3 编写的程序效果会很好,但性能并不是最重要的部分。对开发人员而言,最重要的是新版本将会怎样影响我们编写代码的方式。如你所料,Vue 3 带来了许多令人兴奋的新功能。值得庆幸的是

200 行从零实现 vue3

emmm 用半天时间捋顺了 vue3 的源码,再用半天时间写了个 mini 版……我觉得我也是没谁了,vue3 的源码未来一定会烂大街的,我们越早的去复现它,就……emm可以越早的装逼hhh

从 Proxy 到 Vue 源码,深入理解 Vue 3.0 响应系统

10 月 5 日,尤雨溪在 GitHub 开放了 Vue 3.0 处于 pre-alpha 状态的源码,这次 Vue 3.0 Updates 版本的更新,将带来五项重大改进:速度体积、可维护性、面向原生、易用性

Vue 的数据响应式(Vue2 及 Vue3)

从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。

在Vue2与Vue3中构建相同的组件

Vue 开发团队终于在今天发布了 3.0-beta.1 版本,也就是测试版。通常来说,从测试版到正式版,只会修复 bug,不会引入新功能,或者删改老功能。所以,如果你对新版本非常感兴趣,或者有新项目即将上马,不妨尝试一下新版本

Vue3中的Vue Router初探

对于大多数单页应用程序而言,管理路由是一项必不可少的功能。随着新版本的Vue Router处于Alpha阶段,我们已经可以开始查看下一个版本的Vue中它是如何工作的。

vue3对比vue2使用,代码解释最直观

对于大多数组件,Vue2和Vue3中的代码即使不完全相同,也是非常相似的。但是,Vue3支持片段,这意味着组件可以有多个根节点。这在呈现列表中组件以删除不必要的包装器div元素时特别有用。但是,在本例中,表单组件的两个版本都将只保留一个根节点

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!