Vue3 源码解读系列(五)——响应式

news2025/1/17 2:51:18

响应式

响应式的本质是当数据变化后会自动执行某个函数

映射到组件的实现就是,当数据变化后,会自动触发组件的重新渲染。

响应式的两个核心流程:

  1. 依赖收集
  2. 派发通知

Vue2

在这里插入图片描述

Vue2 中只有 data 中定义的数据才是响应式的,因为 data 中的数据会通过 Object.defineProperty 劫持后再挂载到 this 上,这是一个相对黑盒的行为。

/**
 * 观察某个对象的所有属性
 */
function observe(obj) {
  for(const key in obj) {
    let internalValue = obj[key];
    let funcs = new Set(); // 依赖该属性的函数(注意:要用 Set 而非 Array,因为一个函数可能在多个地方都有用到该属性)
    Object.defineProperty(obj, key, {
      get: function() {
        // 依赖收集,记录:是哪个函数在用我
        if(window.__func) {
          funcs.add(window.__func)
        }
        return internalValue;
      },
      set: function(val) {
        internalValue = val;
        // 派发更新,运行:执行用我的函数
        for(let i = 0; i < funcs.length; i++) {
          funcs[i]();
        }
      }
    })
  }
}

/**
 * 自动运行
 * @param {Function} fn - 自动运行的函数
 */
function autorun(fn) {
  window.__func = fn;
  fn();
  window.__func = null;
}

Object.defineProperty 的缺点:

  1. 不能监听对象属性的新增删除
  2. 初始化阶段递归执行 Object.defineProperty 带来的性能负担

Vue3

在这里插入图片描述

reactive

创建 reactive 对象

创建 reactive 对象主要做了 6 件事:

  1. 判断 target 不是对象或数组类型,直接返回
  2. 判断 target 已经是响应式对象,直接返回
  3. 判断 target 已经有 readonly 或者 reactive,返回该响应式对象
  4. 判断 target 不能被监听,直接返回
  5. 通过 Proxy 劫持 target 对象,把它变成响应式
  6. 给原始数据打个标记,说明它已经存在响应式对象了
/**
 * 响应式函数 - reactive
 */
function reactive(target) {
  // 如果尝试把一个 readonly 变成 reactive,直接返回这个 readonly
  if (target && target.__v_isReadonly) {
    return target
  }

  // 否则创建 reactive 对象
  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}

/**
 * 创建 reactive 对象
 * createReactiveObject 主要做了 6 件事:
 * 1、判断 target 不是对象或数组类型,直接返回
 * 2、判断 target 已经是响应式对象,直接返回
 * 3、判断 target 已经有 readonly 或者 reactive,返回该响应式对象
 * 4、判断 target 不能被监听,直接返回
 * 5、通过 Proxy 劫持 target 对象,把它变成响应式
 * 6、给原始数据打个标记,说明它已经存在响应式对象了
 */
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
  // 1、判断 target 不是对象或数组类型,直接返回 - 限制目标对象必须为对象或数组
  if (!isObject(target)) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(/* ... */)
    }
    return target
  }

  // 2、判断 target 已经是响应式对象,直接返回
  // 有个例外,如果是 readonly 作用于一个响应式对象,则继续
  if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
    return target
  }

  // 3、判断 target 已经有 readonly 或者 reactive,返回该响应式对象 - 同一原始数据生成的响应式对象只有一个
  if (hasOwn(target, isReadonly ? "__v_readonly"/* readonly */ : "__v_reactive"/* reactive */)) {
    return isReadonly ? target.__v_readonly : target.__v_reactive
  }

  // 4、判断 target 不能被监听,直接返回
  if (!canObserve(target)) {
    return target
  }

  // 5、通过 Proxy 劫持 target 对象,把它变成响应式
  const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)

  // 6、给原始数据打个标记,说明它已经存在响应式对象了
  def(target, isReadonly ? "__v_readonly"/* readonly */ : "__v_reactive"/* reactive */, observed)
  return observed
}

/**
 * 能否被监听
 * 同时满足以下三点:
 * 1、不存在 __v_skip 属性
 * 2、是可以被监听的数据类型
 * 3、没有被冻结
 */
function canObserv(value) {
  // 判断是否是可以被监听的数据类型
  const isObservableType = makeMap('Object,Array,Map,Set,WeakMap,WeakSet')

  return (!value.__v_skip && isObservableType(toRawType(value)) && !Object.isFrozen(value))
}

// 处理器对象
const mutableHandlers = {
  get, // 访问对象属性会触发 get 函数
  set, // 设置对象属性会触发 set 函数
  deleteProperty, // 删除对象属性会触发 deleteProperty 函数
  has, // in 操作符会触发 has 函数
  ownKeys // Object.getOwnPropertyNames 触发 ownKeys 函数
}
get - 依赖收集
/**
 * 创建 getter - 依赖收集
 * get 主要做了 4 件事:
 * 1、对特殊的 key 做代理
 * 2、通过 Reflect.get 求值
 * 3、执行 track 函数进行依赖收集
 * 4、判断 res 为对象或者数组,递归执行 reactive 函数把 res 变成响应式(在对象属性被访问时递归 - 懒响应式)
 */
function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    // 1、对特殊的 key 做代理
    // 代理 observed.__v_isReactive
    if (key === "__v_isReactive") {
      return !isReadonly
    }
    // 代理 observed.__v_isReadonly
    else if (key === "__v_isReadonly") {
      return isReadonly
    }
    // 代理 observed.__v_raw
    else if (key === "__v_raw") {
      return target
    }

    // 2、通过 Reflect.get 求值
    const targetIsArray = isArray(target) // 判断目标对象是否是数组
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    const res = Reflect.get(target, key, receiver)
    // 内置 Symbol key 不需要依赖收集
    if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
      return res
    }

    // 3、执行 track 函数进行依赖收集
    !isReadonly && track(target, "get", key)

    // 4、判断 res 为对象或者数组,递归执行 reactive 函数把 res 变成响应式(在对象属性被访问时递归 - 懒响应式)
    return isObject(res) ? isReadonly ? readonly(res) : reactive(res) : res
  }
}

// arrayInstrumentations - 包含对数组一些方法修改的函数
const arrayInstrumentations = {}

['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function (...args) {
    // toRaw - 把响应式对象转化为原始数据
    const arr = toRaw(this)
    for (let i = 0, l = this.length; i < l; i++) {
      // 依赖收集 - get
      track(arr, "get", i + '')
    }

    // 先尝试用参数本身,可能是响应式数据
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // 如果失败,再尝试把参数转成原始数据
      return arr[key](...args.map(toRaw))
    } else {
      return res
    }
  }
})

依赖收集

在这里插入图片描述

let shouldTrack = true // 是否应该收集依赖
let activeEffect // 当前激活的 effect
const targetMap = new WeakMap() // 原始数据对象 map

/**
 * 收集依赖函数
 */
function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) return

  // 每个 target 对应一个 depsMap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  // 每个 key 对应一个 dep 集合
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // 收集当前激活的 effect 作为依赖
    dep.add(activeEffect)
    // 当前激活的 effect 收集 dep 集合作为依赖
    activeEffect.deps.push(dep)
  }
}
set - 派发通知
/**
 * 创建 setter - 事件派发
 * set 主要做了 2 件事:
 * 1、通过 Reflect.set 求值
 * 2、通过 trigger 函数派发通知
 */
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    value = toRaw(value)

    // 1、通过 Reflect.set 求值
    const result = Reflect.set(target, key, value, receiver)

    // 2、通过 trigger 函数派发通知
    // 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
    if (target === toRaw(receiver)) {
      // 根据 key 是否存在于目标对象上判断是 新增属性 还是 修改属性,然后通过 trigger 函数派发通知
      const hadKey = hasOwn(target, key)
      if (!hadKey) {
        trigger(target, "add"/* ADD */, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, "set"/* SET */, key, value, oldValue)
      }
    }
    return result
  }
}

派发通知

const targetMap = new WeakMap() // 原始数据对象 map
/**
 * 派发通知 - 根据 target 和 key 从 targetMap 中找到相关的所有副作用函数遍历执行一次
 * trigger 主要做了 4 件事:
 * 1、通过 targetMap 拿到 target 对应的依赖集合 depsMap
 * 2、创建运行的 effects 集合
 * 3、根据 key 从 depsMap 中找到对应的 effects(SET|ADD|DELETE),添加到 effects 集合
 * 4、遍历 effects,执行相关的副作用函数
 */
function trigger(target, type, key, newValue) {
  // 1、通过 targetMap 拿到 target 对应的依赖集合 depsMap
  const depsMap = targetMap.get(target)

  // 没有依赖,直接返回
  if (!depsMap) return

  // 2、创建 effects 集合
  const effects = new Set()

  // 添加 effects 的函数
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }

  // 3、根据 key 从 depsMap 中找到对应的 effects(SET|ADD|DELETE),添加到 effects 集合
  if (key !== void 0) {
    add(depsMap.get(key))
  }

  // 执行副作用函数的函数
  const run = (effect) => {
    // 调度执行
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    }
    // 直接运行
    else {
      effect()
    }
  }

  // 4、遍历 effects,执行相关的副作用函数
  effects.forEach(run)
}

const effectStack = [] // 全局 effect 栈
let activeEffect // 当前激活 effect
/**
 * 副作用函数
 */
function effect(fn, options = EMPTY_OBJ) {
  // 如果 fn 已经是一个 effect 函数了,则指向原始函数
  if (isEffect(fn)) {
    fn = fn.raw
  }

  // 通过 createReactiveEffect 创建一个新的 effect,它是一个响应式的副作用的函数
  const effect = createReactiveEffect(fn, options)

  // lazy 配置,计算属性会用到,非 lazy 则直接执行一次
  if (!options.lazy) {
    effect()
  }
  return effect
}

/**
 * 创建响应式副作用函数,并且添加一些额外的属性
 */
function createReactiveEffect(fn, options) {
  /**
   * 响应式副作用函数
   * reactiveEffect 主要做了 2 件事:
   * 1、把全局的 activeEffect 指向它
   * 2、执行被包装的原始函数 fn 即可
   */
  const effect = function reactiveEffect(...args) {
    // 非激活状态,则判断如果非调度执行,则直接执行原始函数
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
      // 入栈前执行 cleanup 清空 reactiveEffect 对应的依赖(防止添加 render 函数导致组件重新渲染,例如 v-show、v-if 的情况)
      cleanup(effect)
      try {
        // 开启全局 shouldTrack,允许依赖收集
        enableTracking()
        // 压栈
        effectStack.push(effect)

        // 1、把全局的 activeEffect 指向它
        activeEffect = effect
        // 2、执行被包装的原始函数 fn 即可
        return fn(...args)
      } finally {
        // 出栈
        effectStack.pop()
        // 恢复 shouldTrack 开启之前的状态
        resetTracking()
        // 指向栈最后一个 effect(目的是解决 effect 嵌套的问题)
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++ // 唯一标识
  effect.isEffect = true // 标识是一个 effect 函数
  effect.active = true // effect 自身的状态
  effect.raw = fn // 包装的原始函数
  effect.deps = [] // effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
  effect.options = options // effect 的相关配置
  return effect
}

/**
 * 清空 effect 引用的依赖
 */
function cleanup(effect) {
  const { deps } = effect
  if (deps.length) {
    for (leti = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

readonly

/**
 * 只读响应式对象
 */
function readonly(target) {
  // 和 reactive 一样都是通过 createReactiveObject 来创建响应式对象,区别在于第二个参数 isReadonly 不同
  return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers)
}

/**
 * 只读响应式对象处理对象
 * readonlyHandlers 和 mutableHandlers 的区别主要在于 get、set、deleteProperty 三个函数上
 */
const readonlyHandlers = {
  get: readonlyGet, // createGetter 的返回值
  has,
  ownKeys,
  set(target, key) {
    // 只读对象不允许修改,所以直接警告并返回
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(/* ... */)
    }
    return true
  },
  deleteProperty(target, key) {
    // 只读对象不允许删除,所以直接警告并返回
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(/* ... */)
    }
    return true
  }
}

/**
 * readonlyGet 的实现
 */
function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    // ...

    // 是 readonly 响应式对象,则不需要依赖收集,节省性能
    !isReadonly && track(target, "get"/* GET */, key)

    // 如果 res 是个对象或者数组类型,则递归执行 readonly 函数把 res 只读化
    return isObject(res) ? isReadonly ? readonly(res) : reactive(res) : res
  }
}

ref

 /**
 * ref 的实现
 */
function ref(value) {
  return createRef(value)
}

const convert = (val) => isObject(val) ? reactive(val) : val

/**
 * 创建 ref 对象
 */
function createRef(rawValue) {
  // 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况
  if (isRef(rawValue)) {
    return rawValue
  }

  // 如果是对象或者数组类型,则转换一个 reactive 对象
  let value = convert(rawValue)

  const r = {
    _v_isRef: true,
    get value() { // getter
      // 依赖收集,key 为固定的 value
      track(r, "get"/* GET */, 'value')
      return value
    },
    set value(newVal) { // setter,只处理 value 属性的修改
      // 判断有变化后
      if (hasChanged(toRaw(newVal), rawValue)) {
        // 1、更新值
        rawValue = newVal
        value = convert(newVal)
        // 2、派发通知
        trigger(r, "set"/* SET */, 'value', void 0)
      }
    }
  }
  return r
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1225236.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从底层认识哈希表【C++】

目录 一. unordered系列关联式容器 二. unordered_map的文档介绍 接口使用 三. 底层实现 &#xff08;1&#xff09;哈希概念 例&#xff1a; &#xff08;2&#xff09;哈希冲突 &#xff08;3&#xff09;冲突解决 1.闭散列​​​​​​​ 闭散列框架 插入 查找 删除 2.开散…

中国净初级生产力年度合成产品NPP(MYD17A3H.006)

中国净初级生产力年度合成产品NPP&#xff08;MYD17A3H.006&#xff09;由航天宏图实验室提供&#xff0c;根据NASA MODIS数据&#xff08;MYD17A3H.006&#xff09;通过航天宏图 Smoother计算得到的平滑后NPP产品&#xff0c;解决了影像云雾覆盖、像元异常值等问题。对处理后的…

黑群晖断电导致存储空间已损毁修复记录

黑群晖断电2次&#xff0c;担心的事情还是发生了&#xff0c;登录后提示存储空间已损毁...... 开干&#xff01;&#xff01; 修复方式&#xff1a; 1.使用SSH登录到群晖&#xff0c;查看相关信息 # 登录后先获取最高权限 rootDiskStation:~# sudo -i # 检测存储池状态 root…

2、LeetCode之两数相加

给你两个非空的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照逆序的方式存储的&#xff0c;并且每个节点只能存储一位数字。请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。你可以假设除了数字0之外&#xff0c;这两个数都不会以0开头。 输入&am…

循环队列(出队、入队、判空、长度、遍历、取头)(数据结构与算法)

循环队列 涉及到移动、赋值原队列参数的函数参数列表如front&#xff0c;rear&#xff0c;都最好别用&引用&#xff0c;否则会修改原队列中的地址和数值如&#xff1a;SqQueue &Q 使用SqQueue Q作参数列表时&#xff0c;函数引入的只是一份副本&#xff0c;不会修改原队…

ImportError: DLL load failed while importing _iterative: %1 不是有效的 Win32 应用程序。

问题&#xff1a;这个错误是由于导入的模块 _iterative 找不到有效的 Win32 应用程序导致的。可能是由于你的环境中缺少了某个依赖库或者是版本不匹配的问题。 解决方法&#xff1a; 可以尝试以下几种&#xff1a; 确保你的环境中已经安装了所有需要的依赖库&#xff0c;并且…

分享 | 软件测试的基本流程是什么?软件测试流程详细介绍

软件测试 软件测试和软件开发一样&#xff0c;是一个比较复杂的工作过程&#xff0c;如果无章法可循&#xff0c;随意进行测试势必会造成测试工作的混乱。为了使测试工作标准化、规范化&#xff0c;并且快速、高效、高质量地完成测试工作&#xff0c;需要制订完整且具体的测试…

JAVAEE 初阶 多线程基础(一)

多线程基础 一.线程的概念二.为什么要有线程三.进程和线程的区别和关系四.JAVA的线程和操作系统线程的关系五.第一个多线程程序1.继承Thread类 一.线程的概念 一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码 同…

Leetcode 剑指 Offer II 053. 二叉搜索树中的中序后继

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定一棵二叉搜索树和其中的一个节点 p &#xff0c;找到该节点在…

亚马逊云科技AI创新应用下的托管在AWS上的数据可视化工具—— Amazon QuickSight

目录 Amazon QuickSight简介 Amazon QuickSight的独特之处 Amazon QuickSight注册 Amazon QuickSight使用 Redshift和Amazon QuickSightt平台构建数据可视化应用程序 构建数据仓库 数据可视化 Amazon QuickSight简介 亚马逊QuickSight是一项可用于交付的云级商业智能 (BI…

AI智剪:批量剪辑实战,技巧与实例

随着人工智能技术的不断发展&#xff0c;越来越多的领域开始应用AI技术提升工作效率和质量。其中&#xff0c;AI智剪技术在视频剪辑领域的应用也越来越广泛。AI智剪是一种基于人工智能技术的视频剪辑方法&#xff0c;通过机器学习算法对视频进行自动分析和处理&#xff0c;实现…

VBA技术资料MF84:判断文件夹是否存在并创建

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

Ant Design for Figma设计系统组件库 支持变量 非社区版

Ant Design for Figma 是基于 Ant Design 设计系统的 Figma 组件库&#xff0c;提供丰富的 UI 组件和交互功能&#xff0c;帮助设计师快速构建高质量的 Figma 设计稿。 Ant Design for Figma 继承了 Ant Design 的设计理念和风格&#xff0c;提供丰富的 UI 组件和交互功能&…

在Go编程中调用外部命令的几种场景

1.摘要 在很多场合, 使用Go语言需要调用外部命令来完成一些特定的任务, 例如: 使用Go语言调用Linux命令来获取执行的结果,又或者调用第三方程序执行来完成额外的任务。在go的标准库中, 专门提供了os/exec包来对调用外部程序提供支持, 本文将对调用外部命令的几种使用方法进行总…

NET8 BlazorAuto渲染模式

.NET8发布后&#xff0c;Blazor支持四种渲染方式 静态渲染&#xff0c;这种页面只可显示&#xff0c;不提供交互&#xff0c;可用于网页内容展示使用Blazor Server托管的通过Server交互方式使用WebAssembly托管的在浏览器端交互方式使用Auto自动交互方式&#xff0c;最初使用 …

【项目设计】网络版五子棋游戏

文章目录 一、项目介绍1. 项目简介2. 开发环境3. 核心技术4. 开发阶段 二、环境搭建1. 安装 wget 工具2. 更换 yum 源3. 安装 lrzsz 传输工具4. 安装⾼版本 gcc/g 编译器5. 安装 gdb 调试器6. 安装分布式版本控制工具 git7. 安装 cmake8. 安装 boost 库9. 安装 Jsoncpp 库10. 安…

Python使用大连理工情感本体提取文本的情感倾向

import pandas as pd # 导入词典 df pd.read_excel(Sentiment_dictionary\大连理工情感词汇本体\情感词汇本体.xlsx) # 我们暂时只使用 [词语,词性种类,词义数,词义序号,情感分类,强度,极性] df df[[词语, 词性种类, 词义数, 词义序号, 情感分类, 强度, 极性]] df.head()# 按…

任你五花八门预训练方法,我自监督学习依然能打!

长时间没看论文&#xff0c;外面已经发展成这样了&#xff1f; 以下都是新paper&#xff0c;挑了几个感兴趣的&#xff0c;一起粗略看看吧~ Battle of the Backbones: A Large-Scale Comparison of Pretrained Models across Computer Vision Tasks GitHub | https://github.…

ajax,axios,fetch

文章目录 ajax工作原理ajax发请求四个步骤创建xmlhttprequest对象设置请求方式设置回调函数发送请求 自封装ajax axiosaxios 特性如何用配置拦截器fetch 三者区别 ajax 工作原理 Ajax的工作原理相当于在用户和服务器之间加了—个中间层(AJAX引擎)&#xff0c;使用户操作与服务…