vue3-11-剖析vue3 Reactivity 源代码

137 阅读6分钟

调试源代码查看reactivity是怎么实现的

github.com/vuejs/core

git clone xxx --depth 1 拉取最新的一次提交

跑源代码的时候用的是yarn

项目用的也是采用了Monorepo

element-plus 也用了monorepo

每一个组件都是一个包可以对这一个包进行打包。

scripts/dev.js 测试环境的工具

monorepo 是组织代码的方式,和具体怎么打包没有啥关系。

如果在研究源代码的时候可以修改dev.js的reactivity这样写死就只关心这一个包不需要打包别的包。

深度研究reactivity

// 把dev.js 强行改成reactivity -> 默认是vue改成 reactivity 只对这一个模块处理
// 核心的api
/**
* reactive
* effect effect中的响应式变量改变了这个effect就会重新执行 【track】收集 【trigger】触发 内部方法
* ref 
*/

// reactivity ->  [看源代码可以从单元测试入手][package.json]/main 入手

// index.js 默认用的commonjs的规范 - node的

// 如果用的import 的语法找的是package.js module 对应的入口

// 在构建工具中使用默认用的是module

// 源代码的入口 src/index.ts // 这个玩意就是把内部功能导出来。

/**
* reactive 分析
* 1、target 判断是否存在并且是否存在IS_READYONLY如果存 如果对象存在并且是只读的直接返回。 防止这么操作 reactive(readonly(target)) 
*   如果对象被readonly包装会返回proxy 因为本身就是只读的不可能被修改了。用reactive这个包一层意义就不大了。
* 2、创建响应式对象
* createReactiveObject(
*  // 目标 (array obj) 用的A (Set Map )用的B
*  // 是不是readyonly
*  // 拦截器 A new Proxy 对应的拦截器
*  // 拦截器 B collection handler
* )
* createReactiveObject
    1. 判断 target是不是对象
    2. 如果不是对象就直接返回,传递的什么就返回什么
    3. target[Reactiveflgs.RAW] // 对象被代理过会有这个RAW属性
    4. 这么访问如果target被代理过会走到get里面
    5. 观察 createGetter key === Reactiveflgs.RAW 说明这个属性已经被代理过了。 - baseHandler.ts
    6. 如果被代理过就直接返回 target
    7. if(target[Reactiveflgs.RAW]
    && 
    // 如果被readonly代理了并且被reactive没有同时代理
    !(isReadonly && target[Reactiveflgs.IS_REACTIVE])){ 所以这里可以用 reactive(readonly(target)) 是可以创建target的 这种情况是ok的
    return target;
  }
  
  8. const M = isReadonly ? readonlyMap : reactiveMap // 如果是只读的会产生只读的映射表,否则会产生reactive的映射表。  readonly放一起  reactive的放一起
  9. 这两个是做缓存的如果创建过就直接返沪了用weakMap
  10. M.get(target) 先看一下有没有代理过 // 保证对象不能被重复的代理。
  11。reactive(reactive(target)) // 这是不允许的
  12. 如果对象没有被代理过,看一看白名单 对象能不能去被代理 __v_skip
  13. getTargetType 获取目标的类型。
  14. 如果对象有个__v_skip 说明这个对象不能被代理 或者 这个对象是不可以被扩展的 !Object.isExtensible(value);
  15. 如果可以被代理 toRawType(target)  Object.prototype.toString.call({}).slice(8, -1) 其实就是获取类型的字符串 toRawType 这个玩意就是在获取类型。
  16. targetTypeMap(toRawType(target)) 取出来target是一个普通对象,还是一个集合类型。
  17. 如果返回了TargetType.INVALID 就直接返回了
  18. 如果没有问题提就new Proxy
  19. const Proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  20. M.set(target, Proxy); // 写入缓存
  21. return Proxy
  =======get=======
  22. 数组的处理
  const targetIsArray = isArray(target);
  23. arrayInstrumentations 重写 includes indexOf lastIndexOf 类似vue2的方法劫持  并且调用原有方法 可以增加自己的逻辑
  24. 为什么需要对他们三个特殊处理? ['a', 'b', 'c'].includes(dyn); dyn是个变量多种可能 因为 dyn可能是 'a'|'b'|'c' 所以需要对 a b c 进行依赖收集。
  相当于 proxy(['a','b','c']).includes(dyn) // 数组的每一项都进行以来收集
  25. ['push', 'pop', 'shift', 'unshift', 'splice'] 也重写了
  27. pauseTracking() // 暂停跟踪?可以控制是否依赖收集   resetTracking() // 重制跟踪  这一坨有啥用? 防止length被修改了如果length被修改在某些场景会无限循环。解决的bug是2137
  bug内容
  const arr = reactive([])
  effect(()=>{
    arr.push(1)
  })
  effect(()=>{
    arr.push(2)
  })
  造成的问题就是 length改变就无限执行1 2 无限的加
  // 调用push 的时候回去访问数组的push length 就忽略收集了
  28. 回到23继续往下走
  const res = Reflect.get(target, key, receiver);
  29. 
    isSymbol(key) ...... // 内置symbol 或者是原型链查找到的直接返回 // 这些就不考虑收集依赖了
    Object.getOwnPropertyNames(Symbol) // 返回的是一些symbol的内置默认属性 或者 是 __proto__ __v_isRef, __isVue都直接返回屏蔽掉了一些内置symbol 值的获取 不需要收集symbol和链的依赖
  30. if(!isReadonly){
    track(target, TrackOpTypes.Get, key) // 如果不是只读就需要收集
  }
  31. if(shallow){
    return res; 如果是浅层的就直接返回了不需要收集
  }
  32. if(isRef(res)){
    //会对ref做特殊的处理
  }
  33. if(isObject(res)){
    // 如果是对象就递归处理成reactive
  }
  34. Basehandler createSetter 
  if(!shallow) 如果不是浅的 可能我们这个对象被深层代理了
  value = toRaw(value) 如果本身是proxy 就把它拆包了
  相当于
  p = reactive({r:1})
  p.r = reactive({a:1}) // 给值有做了个reactive 
  这时候就需要把 reactive({a:1})变成 {a:1} 不需要proxy套proxy
  let proxy1 = reacitve({name:1, age:ref(11)})
  proxy1.name = reactive({str:''}) // 这里就拆包了 不担心多次设置proxy
  proxy1.age = 11; // 这个相当于改的是老的ref(11)的value
*/

  

针对上面的特性进行测试

let obj = {a:1};
let a = readonly(obj);
let b = reactive(a);

a === b; // true;

// demo2
let a = reactive({});
let b = reactive(a);
let c = reactive(b);
a == b == c;

// demo3
let obj = {a:1};
let proxy = reactive(obj);
obj == toRaw(proxy); // toRaw把proxy对象变回到普通对象 true toRaw(observed){return (observed && toRaw(observed["__v_raw"])) || observed} // 取值就走get了observed["__v_raw"] observed["__v_raw"] 这个走get就从缓存里面把对象对应的target拿出来

let obj2 = {a:1};
let proxy2 = reactive(markRaw(obj2)); // markRaw 标记obj2是一个raw对象。他会给这个对象加一个"__v_skip"属性表示这个对象不可以被代理,就不能用new proxy做处理了 define object.defineProperty定义一下 proxy2 还是 obj2 有些写好的库 并不支持 proxy 需要用markRaw包装一下不让proxy代理

let a = reactive({a:markRaw({b:1})});// b就不被代理了

// demo4
// 对数组的特定处理
let proxyArray = reactive([1,2,3,4]);
// 访问数组长度的时候
proxyArray.push(1) // 调用数组长度的时候有暂停收集功能和增加收集功能。它会访问length 自动访问 它会来两次第一次push 进来第二次length进来 你取length就暂停依赖收集 track里面有个shouldTrack表示要不要收集
proxyArray[Symbol.hasInstance] // 访问内部自定义属性不会依赖收集

// 只要用到长度的都会触发  length 所以对  includes indexOf lastIndexOf  'push', 'pop', 'shift', 'unshift', 'splice' 做了单独的处理

effect(() => {
    arr[0]xxx,arr.length//操作了arr属性才会需要收集和触发 一定要是取arr的动作
    arr.push()// 这个并没用到具体的成员也就不需要收集依赖。
})

// 考虑了ref的情况
let r = reactive({name:ref('zf')})
// 属性有可能是被ref包裹的
// 正常情况下r.name.value是不是有点2
// 它会帮着你拆包
// 走到了 上面 32
if(isRef(xxx)) {
    // 通过 __v_isRef来查看
    // 如果不是数组就直接返回res.value了把包拆了 所以 r.name = r.name.value
    // 所以reactive包了ref可以直接不用了value了
}

// demo5
let r1 = reactive([ref(1),2,3,4]);
r1[0].value // r1[0] != r1[0].value 这种情况是不能被拆包的 返回就是个ref没有拆包动作