当前位置: 首页 > news >正文

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

响应式

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

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

响应式的两个核心流程:

  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,直接返回这个 readonlyif (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_isReactiveif (key === "__v_isReactive") {return !isReadonly}// 代理 observed.__v_isReadonlyelse if (key === "__v_isReadonly") {return isReadonly}// 代理 observed.__v_rawelse 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++) {// 依赖收集 - gettrack(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 对应一个 depsMaplet 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 对应的依赖集合 depsMapconst 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 } = effectif (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 为固定的 valuetrack(r, "get"/* GET */, 'value')return value},set value(newVal) { // setter,只处理 value 属性的修改// 判断有变化后if (hasChanged(toRaw(newVal), rawValue)) {// 1、更新值rawValue = newValvalue = convert(newVal)// 2、派发通知trigger(r, "set"/* SET */, 'value', void 0)}}}return r
}

相关文章:

  • [Socket]Unix socket 运行权限问题
  • 关于跨域问题的个人理解
  • Vue 3.0 + vite + axios+PHP跨域问题的解决办法
  • 【数据结构】顺序表 | 详细讲解
  • 17. 机器学习——SVM
  • 专业的SRM系统全流程管理服务
  • iText v1.8.1(OCR截图文字识别工具)
  • centralwidget 不能布局
  • HarmonyOS 高级特性
  • SpringCloud Alibaba(上):注册中心-nacos、负载均衡-ribbon、远程调用-feign
  • 「Verilog学习笔记」用优先编码器①实现键盘编码电路
  • element-ui el-tree 文本超出显示省略号 添加移入提示
  • 【ARM Trace32(劳特巴赫) 使用介绍 4 - Trace32 Discovery 详细介绍】
  • outlook群发邮件
  • 基于安卓android微信小程序的快递取件及上门服务系统
  • JS 中的深拷贝与浅拷贝
  • CentOS 7 修改主机名
  • IDEA 插件开发入门教程
  • java多线程
  • Laravel深入学习6 - 应用体系结构:解耦事件处理器
  • MaxCompute访问TableStore(OTS) 数据
  • Mocha测试初探
  • VuePress 静态网站生成
  • 聊聊flink的TableFactory
  • 思否第一天
  • 通过git安装npm私有模块
  • 一道闭包题引发的思考
  • Java总结 - String - 这篇请使劲喷我
  • ​软考-高级-信息系统项目管理师教程 第四版【第19章-配置与变更管理-思维导图】​
  • #{} 和 ${}区别
  • #android不同版本废弃api,新api。
  • #define与typedef区别
  • #预处理和函数的对比以及条件编译
  • (30)数组元素和与数字和的绝对差
  • (八)五种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (一)python发送HTTP 请求的两种方式(get和post )
  • .Net 6.0 处理跨域的方式
  • .Net Web项目创建比较不错的参考文章
  • .NET 回调、接口回调、 委托
  • .NET 中 GetProcess 相关方法的性能
  • .NET:自动将请求参数绑定到ASPX、ASHX和MVC(菜鸟必看)
  • .NET处理HTTP请求
  • .net打印*三角形
  • .net中生成excel后调整宽度
  • @Bean, @Component, @Configuration简析
  • @cacheable 是否缓存成功_让我们来学习学习SpringCache分布式缓存,为什么用?
  • [ Algorithm ] N次方算法 N Square 动态规划解决
  • [ABC294Ex] K-Coloring
  • [ActionScript][AS3]小小笔记
  • [autojs]autojs开关按钮的简单使用
  • [AutoSar]BSW_OS 02 Autosar OS_STACK
  • [C#][opencvsharp]opencvsharp sift和surf特征点匹配
  • [CareerCup] 13.1 Print Last K Lines 打印最后K行
  • [CareerCup] 17.8 Contiguous Sequence with Largest Sum 连续子序列之和最大
  • [exgcd] Jzoj P1158 荒岛野人