Vue3响应式源码分析 - reactive篇

更新日期: 2022-06-23阅读: 1k标签: 源码作者: 周小羊

最近一阶段在学习vue3,Vue3中用 reactive、ref 等方法将数据转化为响应式数据,在获取时使用 track 往 effect 中收集依赖,在值改变时,使用 trigger 触发依赖,执行对应的监听函数,这次就先来看一下 reactive 的源码。

reactive的源码在官方源码的packages/reactivity/src/reactive.ts文件中,源码中提供了四个api来创建reactive类对象:

reactive:创建可深入响应的可读写对象
readonly:创建可深入响应的只读对象
shallowReactive:创建只有第一层响应的浅可读写对象(其他层,值改变视图不更新)
shallowReadonly:创建只有一层响应的浅只读对象

它们都是调用createReactiveObject方法来创建响应式对象,区别在于传入不同的参数,本文只讲reactive,其他几个大同小异:

export function reactive(target: object) {
  // 如果是只读的话直接返回
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    // 目标对象
    target,
    // 标识是否是只读
    false,
    // 常用类型拦截器
    mutableHandlers,
    // 集合类型拦截器
    mutableCollectionHandlers,
    // 储了每个对象与代理的map关系
    reactiveMap
  )
}

export const reactiveMap = new WeakMap<Target, any>()

createReactiveObject代码如下:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果代理的数据不是对象,则直接返回原对象
  if (!isObject(target)) {
    return target
  }

  // 如果传入的已经是代理了 并且 不是readonly 转换 reactive的直接返回
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 查看当前代理对象之前是不是创建过当前代理,如果创建过直接返回之前缓存的代理对象
  // proxyMap 是一个全局的缓存WeakMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 如果当前对象无法创建代理,则直接返回源对象
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  //  根据targetType 选择集合拦截器还是基础拦截器
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )

  // 向全局缓存Map里存储
  proxyMap.set(target, proxy)
  return proxy
}

其中有个方法是 getTargetType,用来获取传入target的类型:

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

export const enum ReactiveFlags {
  SKIP = '__v_skip',              // 标记阻止成为代理对象
  IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
  IS_READONLY = '__v_isReadonly', // 标记一个只读对象
  IS_SHALLOW = '__v_isShallow',   // 标记只有一层响应的浅可读写对象
  RAW = '__v_raw'                 // 标记获取原始值
}

const enum TargetType {
  // 无效的 比如基础数据类型
  INVALID = 0,
  // 常见的 比如object Array
  COMMON = 1,
  // 集合类型比如 map set
  COLLECTION = 2
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

当target被标记为 ReactiveFlags.SKIP 或是 不可拓展的,则会返回 TargetType.INVALID,无法创建代理,因为Vue需要对Target代理附加很多东西,如果是不可拓展的则会附加失败;或是用户主动调用 markRaw 等方法将数据标记为非响应式数据,那么也无法创建代理。

export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

看完了入口函数,接下来就是创建Proxy对象的过程了,Vue3会根据getTargetType返回的数据类型来选择是使用collectionHandlers集合拦截器还是baseHandlers常用拦截器,原因下面讲到集合拦截器的时候再说。

常用拦截器baseHandlers:

get 拦截器:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
 if (key === ReactiveFlags.IS_REACTIVE) { // 获取当前是否是reactive
   return !isReadonly
 } else if (key === ReactiveFlags.IS_READONLY) { // 获取当前是否是readonly
   return isReadonly
 } else if (key === ReactiveFlags.IS_SHALLOW) { // 获取当前是否是shallow
   return shallow
 } else if (
   // 如果获取源对象,在全局缓存WeakMap中获取是否有被创建过,如果创建过直接返回被代理对象
   key === ReactiveFlags.RAW &&
   receiver ===
     (isReadonly
       ? shallow
         ? shallowReadonlyMap
         : readonlyMap
       : shallow
       ? shallowReactiveMap
       : reactiveMap
     ).get(target)
 ) {
   return target
 }

 // 是否是数组
 const targetIsArray = isArray(target)

 // arrayInstrumentations相当于一个改造器,里面定义了数组需要改造的方法,进行一些依赖收集等操作
 // 如果是数组,并且访问的方法在改造器中,则使用改造器获取
 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
   return Reflect.get(arrayInstrumentations, key, receiver)
 }

 // 获取结果
 const res = Reflect.get(target, key, receiver)

 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
   return res
 }

 // 如果不是只读则收集依赖,Vue3中用track收集依赖
 if (!isReadonly) {
   track(target, TrackOpTypes.GET, key)
 }

 // shallow只有表层响应式,不需要下面去深度创建响应了
 if (shallow) {
   return res
 }

 // 如果获取的值是ref类型
 if (isRef(res)) {
   // 如果是数组 并且 是int类型的 key,则返回,否则返回.value属性
   return targetIsArray && isIntegerKey(key) ? res : res.value
 }

 if (isObject(res)) {
   // *获取时才创建相对应类型的代理,将访问值也转化为reactive,不是一开始就将所有子数据转换
   return isReadonly ? readonly(res) : reactive(res)
 }

 return res
  }
}

注意点是当代理类型是 readonly 时,不会收集依赖。
Vue3对于深层次的对象是使用时才创建的,还有如果结果是ref类型,则需要判断是否要获取它的.value类型,举个例子:

const Name = ref('张三')
const Array = ref([1])

const data = reactive({
  name: Name,
  array: Array
})

console.log(Name)          // RefImpl类型
console.log(data.name)     // 张三
console.log(data.array[0]) // 1

Vue3中使用 arrayInstrumentations对数组的部分方法做了处理,为什么要这么做呢? 对于 push、pop、 shift、 unshift、 splice 这些方法,写入和删除时底层会获取当前数组的length属性,如果我们在effect中使用的话,会收集length属性的依赖,当使用这些api是也会更改length,就会造成死循环:

 let arr = []
 let proxy = new Proxy(arr, {
get: function(target, key, receiver) {
  console.log(key)
  return Reflect.get(target, key, receiver)
}
 })
 proxy.push(1)
 /* 打印 */
 // push
 // length
// 当把这个代码注释掉时
// if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
//     return Reflect.get(arrayInstrumentations, key, receiver);
// }

const arr = reactive([])

watchEffect(() => {
 arr.push(1)
})

watchEffect(() => {
 arr.push(2)    
 // 上面的effect里收集了对length的依赖,push又改变了length,所以上面的又会触发,以此类推,死循环
})

// [1,2,1,2 ...] 死循环
console.log(arr)

对于 includes、 indexOf、 lastIndexOf,内部会去获取每一个的值,上面讲到如果获取出来的结果是Obejct,会自动转换为reactive对象:

let target = {name: '张三'}

const arr = reactive([target])

console.log(arr.indexOf(target)) // -1

因为实际上是 reactive(target) 和 target 在对比,当然查不到。

set 拦截器

function createSetter(shallow = false) {
 return function set(target, key, value, receiver) {
     // 获取旧数据
     let oldValue = target[key];
     if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
         return false;
     }
     // 如果当前不是shallow并且不是只读的
     if (!shallow && !isReadonly(value)) {
         if (!isShallow(value)) {
             // 如果新value本身是响应对象,就把他变成普通对象
             // 在get中讲到过如果取到的值是对象,才转换为响应式
             // vue3在代理的时候,只代理第一层,在使用到的时候才会代理第二层
             value = toRaw(value);
             oldValue = toRaw(oldValue);
         }
         // 如果旧的值是ref对象,新值不是,则直接赋值给ref对象的value属性
         if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
             // 这里不触发trigger是因为,ref对象在value被赋值的时候会触发写操作,也会触发依赖更新
             oldValue.value = value;
             return true;
         }
     }
     const hadKey = isArray(target) && isIntegerKey(key)
         ? Number(key) < target.length
         : hasOwn(target, key);
     const result = Reflect.set(target, key, value, receiver);
     // 这个判断主要是为了处理代理对象的原型也是代理对象的情况
     if (target === toRaw(receiver)) {
         if (!hadKey) {
             // key不存在就 触发add类型的依赖更新
             trigger(target, "add" /* ADD */, key, value);
         }
         else if (hasChanged(value, oldValue)) {
             // key存在就触发set类型依赖更新
             trigger(target, "set" /* SET */, key, value, oldValue);
         }
     }
     return result;
 };
}

set中还有一个要注意的地方就是 target === toRaw(receiver),这主要是为了处理代理对象的原型也是代理对象的情况:

const child = reactive({})

let parentName = ''
const parent = reactive({
  set name(value) {
     parentName = value
  },
  get name() {
     return parentName
  }
})

Object.setPrototypeOf(child, parent)

child.name = '张三'

console.log(toRaw(child)) // {name: 张三}
console.log(parentName) // 张三

当这种时候,如果不加上这个判断,由于子代理没有name这个属性,会触发原型父代理的set,加上这个判断避免父代理也触发更新。

集合拦截器collectionHandlers:

集合类型的数据比较特殊,其相关实例方法Proxy没有提供相关的捕获器,但是因为方法调用属于属性获取操作,所以都可以通过捕获get操作来实现,所以Vue3也只定义了get拦截:

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    // 注意这里
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

之前的文章《代理具有内部插槽的内建对象》中说过Proxy代理具有内部插槽的内建对象,访问Proxy上的属性会发生错误。Vue3中是如何解决的呢?

Vue3中新创建了一个和集合对象具有相同属性和方法的普通对象,在集合对象 get 操作时将 target 对象换成新创建的普通对象。这样,当调用 get 操作时 Reflect 反射到这个新对象上,当调用 set 方法时就直接调用新对象上可以触发响应的方法,这样访问的就不是Proxy上的方法,是这个新对象上的方法:

function createInstrumentations() {
  const mutableInstrumentations: Record<string, Function> = {
    get(key: unknown) {
      return get(this, key)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, false)
  }
  
  const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {
    mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
  })

  return [
    mutableInstrumentations
  ]
}

接下来看一看几个具体的拦截器:

get 拦截器:

function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // 如果出现readonly(reactive())这种嵌套的情况,在readonly代理中获取到reactive()
  // 确保get时也要经过reactive代理
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 确保 包装后的key 和 没包装的key 都能访问得到
  if (!isReadonly) {
     if (key !== rawKey) {
       track(rawTarget, TrackOpTypes.GET, key)
     }
     track(rawTarget, TrackOpTypes.GET, rawKey)
  }
  const { has } = getProto(rawTarget)
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  if (has.call(rawTarget, key)) {
     return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
     return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {
     target.get(key)
  }
}

集合拦截器里把 key 和 rawKey 都做了处理,保证都能取到数据:

let child = {
  name: 'child'
}

const childProxy = reactive(child)

const map = reactive(new Map())

map.set(childProxy, 1234)

console.log(map.get(child)) // 1234
console.log(map.get(childProxy)) // 1234

set 拦截器:

// Map set拦截器
function set(this: MapTypes, key: unknown, value: unknown) {
  // 存origin value
  value = toRaw(value);
  // 获取origin target
  const target = toRaw(this);
  const { has, get } = getProto(target);

  // 查看当前key是否存在
  let hadKey = has.call(target, key);
  // 如果不存在则获取 origin
  if (!hadKey) {
     key = toRaw(key);
     hadKey = has.call(target, key);
  } else if (__DEV__) {
     // 检查当前是否包含原始版本 和响应版本在target中,有的话发出警告
     checkIdentityKeys(target, has, key);
  }

  // 获取旧的value
  const oldValue = get.call(target, key);
  // 设置新值
  target.set(key, value);
  if (!hadKey) {
     trigger(target, TriggerOpTypes.ADD, key, value);
  } else if (hasChanged(value, oldValue)) {
     trigger(target, TriggerOpTypes.SET, key, value, oldValue);
  }
  return this;
}

has 拦截器:

function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  // 获取代理前数据
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 如果key是响应式的都收集一遍
  if (key !== rawKey) {
     !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)

  // 如果key是Proxy 那么先访问 proxyKey 在访问 原始key 获取结果
  return key === rawKey
 ? target.has(key)
 : target.has(key) || target.has(rawKey)
}

forEach 拦截器:

function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
     this: IterableCollections,
     callback: Function,
     thisArg?: unknown
  ) {
 const observed = this as any
 const target = observed[ReactiveFlags.RAW]
 const rawTarget = toRaw(target)
 const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
 !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
 // 劫持传递进来的callback,让传入callback的数据转换成响应式数据
 return target.forEach((value: unknown, key: unknown) => {
   // 确保拿到的值是响应式的
   return callback.call(thisArg, wrap(value), wrap(key), observed)
 })
  }
}

来自:https://segmentfault.com/a/1190000042021408

链接: https://fly63.com/article/detial/11786

Js中的 forEach 源码

在日常 Coding 中,码农们肯定少不了对数组的操作,其中很常用的一个操作就是对数组进行遍历,查看数组中的元素,然后一顿操作猛如虎。今天暂且简单地说说在 JavaScript 中 forEach。

微信小程序代码源码案例大全

克隆项目代码到本地(git应该都要会哈,现在源码几乎都会放github上,会git才方便,不会的可以自学一下哦,不会的也没关系,gitHub上也提供直接下载的链接);打开微信开发者工具;

Node 集群源码初探

随着这些模块逐渐完善, Nodejs 在服务端的使用场景也越来越丰富,如果你仅仅是因为JS 这个后缀而注意到它的话, 那么我希望你能暂停脚步,好好了解一下这门年轻的语言,相信它会给你带来惊喜

Vue源码之实例方法

在 Vue 内部,有一段这样的代码:上面5个函数的作用是在Vue的原型上面挂载方法。initMixin 函数;可以看到在 initMixin 方法中,实现了一系列的初始化操作,包括生命周期流程以及响应式系统流程的启动

vue源码解析:nextTick

nextTick的使用:vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom,我们看下面的代码。

React源码解析之ReactDOM.render()

React更新的方式有三种:(1)ReactDOM.render() || hydrate(ReactDOMServer渲染)(2)setState(3)forceUpdate;接下来,我们就来看下ReactDOM.render()源码

React源码解析之ExpirationTime

在React中,为防止某个update因为优先级的原因一直被打断而未能执行。React会设置一个ExpirationTime,当时间到了ExpirationTime的时候,如果某个update还未执行的话,React将会强制执行该update,这就是ExpirationTime的作用。

扒开V8引擎的源码,我找到了你们想要的前端算法

算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索 Array.prototype.sort 函数下的算法实现。来,先把你用过的和听说过的排序算法都列出来:

jQuery源码之extend的实现

extend是jQuery中一个比较核心的代码,如果有查看jQuery的源码的话,就会发现jQuery在多处调用了extend方法。作用:对任意对象进行扩;’扩展某个实例对象

vuex源码:state及strict属性

state也就是vuex里的值,也即是整个vuex的状态,而strict和state的设置有关,如果设置strict为true,那么不能直接修改state里的值,只能通过mutation来设置

点击更多...

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