【Vue3.0】- 响应式

响应式原理

  • 响应式是 Vue.js 组件化更新渲染的一个核心机制

Vue2.x响应式实现

  • Object.defineProperty API 劫持数据的变化
  • 在数据被访问的时候收集依赖
  • 然后在数据被修改的时候通知依赖更新
  • Vue.js 2.x 中,Watcher 就是依赖,
    • 首先是依赖收集流程,组件在 render 的时候会访问模板中的数据,触发 getterrender watcher 作为依赖收集,并和数据建立联系
    • 然后是派发通知流程,当我对这些数据修改的时候,会触发 setter,通知 render watcher 更新,进而触发了组件的重新渲染
  • Object.defineProperty API 的一些缺点:
    • 不能监听对象属性新增和删除
    • 初始化阶段递归执行 Object.defineProperty 带来的性能负担

响应式对象的实现差异

  • Vue.js 2.x 中构建组件时,只要我们在 datapropscomputed 中定义数据,那么它就是响应式的
  • 到了 Vue.js 3.0 构建组件时,你可以不依赖于 Options API,而使用 Composition API 去编写
  • Composition API 更推荐用户主动定义响应式对象,而非内部的黑盒处理

Reactive API

  • reactive 函数的具体实现过程
function reactive (target) {
   // 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
  if (target && target.__v_isReadonly) {
     return target
  } 
  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
  • reactive 内部通过 createReactiveObject 函数把 target 变成了一个响应式对象
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
  if (!isObject(target)) {
    // 目标必须是对象或数组类型
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
    // target 已经是 Proxy 对象,直接返回
    // 有个例外,如果是 readonly 作用于一个响应式对象,则继续
    return target
  }
  if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
    // target 已经有对应的 Proxy 了
    return isReadonly ? target.__v_readonly : target.__v_reactive
  }
  // 只有在白名单里的数据类型才能变成响应式
  if (!canObserve(target)) {
    return target
  }
  // 利用 Proxy 创建响应式
  const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
  // 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
  def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
  return observed
}
  • 1、 函数首先判断 target 是不是数组或者对象类型,如果不是则直接返回。所以原始数据 target 必须是对象或者数组。
  • 2、通过 target.__v_raw 属性,和__v_isReactive属性来判断 target 是否已经是一个响应式对象,如果是,直接返回该对象
  • 3、使用 canObserve 函数对 target 对象做一进步限制
    • 带有 __v_skip 属性的对象、被冻结的对象,以及不在白名单内的对象如 Date 类型的对象实例是不能变成响应式的
const canObserve = (value) => {
  return (!value.__v_skip &&
   isObservableType(toRawType(value)) &&
   !Object.isFrozen(value))
}
const isObservableType = /*#__PURE__*/
makeMap('Object,Array,Map,Set,WeakMap,WeakSet')
  • 4、通过 Proxy API 劫持 target 对象,把它变成响应式
  • 5、给原始数据打个标识,target.__v_reactive = observed
def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
Proxy 处理器对象 mutableHandlers
const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
  • 劫持了我们对 observed 对象的一些操作
  • 1)访问对象属性会触发 get 函数;
  • 2)设置对象属性会触发 set 函数;
  • 3)删除对象属性会触发 deleteProperty 函数;
  • 4)in 操作符会触发 has 函数;
  • 5)通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数
依赖收集:get 函数
  • 依赖收集发生在数据访问的阶段,get执行createGetter
function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    if (key === "__v_isReactive" /* isReactive */) {
      // 代理 observed.__v_isReactive
      return !isReadonly
    }
    else if (key === "__v_isReadonly" /* isReadonly */) {
      // 代理 observed.__v_isReadonly
      return isReadonly;
    }
    else if (key === "__v_raw" /* raw */) {
      // 代理 observed.__v_raw
      return target
    }
    const targetIsArray = isArray(target)
    // arrayInstrumentations 包含对数组一些方法修改的函数
    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
    }
    // 依赖收集
    !isReadonly && track(target, "get" /* GET */, key)
    return isObject(res)
      ? isReadonly
        ?
        readonly(res)
        // 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
        : reactive(res)
      : res
  }
}
  • get 函数主要做了四件事情
  • 1)首先对特殊的 key 做了代理
  • 2)通过 Reflect.get 方法求值,如果 target 是数组且 key 命中了 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++) {
      // 依赖收集
      track(arr, "get" /* GET */, i + '')
    }
    // 先尝试用参数本身,可能是响应式数据
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // 如果失败,再尝试把参数转成原始数据
      return arr[key](...args.map(toRaw))
    }
    else {
      return res
    }
  }
})
  • 3)通过 Reflect.get 求值,然后会执行 track 函数收集依赖
  • 4)对计算的值 res 进行判断,如果它也是数组或对象,则递归执行 reactiveres 变成响应式对象,因为Proxy劫持的是对象本身,并不能劫持子对象的变化
track函数收集依赖
  • 整个 get 函数最核心的部分其实是执行 track 函数收集依赖
// 是否应该收集依赖
let shouldTrack = true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 每个 target 对应一个 depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // 每个 key 对应一个 dep 集合
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // 收集当前激活的 effect 作为依赖
    dep.add(activeEffect)
   // 当前激活的 effect 收集 dep 集合作为依赖
    activeEffect.deps.push(dep)
  }
}
  • 收集的依赖就是数据变化后执行的副作用函数
  • 创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map
  • depsMap 的键是 targetkey,值是 dep 集合
  • dep 集合中存储的是依赖的副作用函数
派发通知:set 函数
  • 派发通知发生在数据更新的阶段, set 函数的实现,它是执行 createSetter 函数的返回值
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    value = toRaw(value)
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, "add" /* ADD */, key, value)
      }
      else if (hasChanged(value, oldValue)) {
        trigger(target, "set" /* SET */, key, value, oldValue)
      }
    }
    return result
  }
}
  • 主要做两件事情:
  • 1)通过 Reflect.set 求值
  • 2)通过 trigger 函数派发通知,并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改
trigger 函数派发通知
// 原始数据对象 map
const targetMap = new WeakMap()
function trigger(target, type, key, newValue) {
  // 通过 targetMap 拿到 target 对应的依赖集合
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 没有依赖,直接返回
    return
  }
  // 创建运行的 effects 集合
  const effects = new Set()
  // 添加 effects 的函数
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }
  // 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()
    }
  }
  // 遍历执行 effects
  effects.forEach(run)
}
  • 主要做了四件事情
  • 1)通过 targetMap 拿到 target 对应的依赖集合 depsMap
  • 2)创建运行的 effects 集合
  • 3)根据 keydepsMap 中找到对应的 effects 添加到 effects 集合;
  • 4)遍历 effects 执行相关的副作用函数
副作用函数
// 全局 effect 栈
const effectStack = []
// 当前激活的 effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    // 如果 fn 已经是一个 effect 函数了,则指向原始函数
    fn = fn.raw
  }
  // 创建一个 wrapper,它是一个响应式的副作用的函数
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // lazy 配置,计算属性会用到,非 lazy 则直接执行一次
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect(...args) {
    if (!effect.active) {
      // 非激活状态,则判断如果非调度执行,则直接执行原始函数。
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
      // 清空 effect 引用的依赖
      cleanup(effect)
      try {
        // 开启全局 shouldTrack,允许依赖收集
        enableTracking()
        // 压栈
        effectStack.push(effect)
        activeEffect = effect
        // 执行原始函数
        return fn(...args)
      }
      finally {
        // 出栈
        effectStack.pop()
        // 恢复 shouldTrack 开启之前的状态
        resetTracking()
        // 指向栈最后一个 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++
  // 标识是一个 effect 函数
  effect._isEffect = true
  // effect 自身的状态
  effect.active = true
  // 包装的原始函数
  effect.raw = fn
  // effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
  effect.deps = []
  // effect 的相关配置
  effect.options = options
  return effect
}
  • effect 内部通过执行 createReactiveEffect 函数去创建一个新的 effect 函数,为了和外部的 effect 函数区分,我们把它称作 reactiveEffect 函数,并且还给它添加了一些额外属性
  • 这个 reactiveEffect 函数就是响应式的副作用函数,当执行 trigger 过程派发通知的时候,执行的 effect 就是它
  • reactiveEffect 函数只需要做两件事情
  • 1)把全局的 activeEffect 指向它
  • 2)然后执行被包装的原始函数 fn
  • effectStack维护一个栈,解决嵌套场景,activeEffect指向问题,activeEffect 指向 effectStack 最后一个元素
  • 入栈前会执行 cleanup 函数清空 reactiveEffect函数对应的依赖

readonly API

  • 创建只读对象,不能修改它的属性,也不能给这个对象添加和删除属性
function readonly(target) {
    return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers)
}
  • readonlyreactive 函数的主要区别,就是执行 createReactiveObject 函数时的参数 isReadonly 不同。
  • 首先 isReadonly 变量为 true,所以在创建过程中会给原始对象 target 打上一个 __v_readonly 的标识
  • 另外还有一个特殊情况,如果 target 已经是一个 reactive 对象,就会把它继续变成一个 readonly 响应式对象
  • 创建代理是,传入readonlyHandlers
const readonlyHandlers = {
  get: readonlyGet,
  has,
  ownKeys,
  set(target, key) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target)
    }
    return true
  },
  deleteProperty(target, key) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target)
    }
    return true
  }
}
  • readonlyHandlersmutableHandlers的区别主要在 getsetdeleteProperty 三个函数上
  • 在非生产环境下 setdeleteProperty 函数的实现都会报警告,提示用户 targetreadonly
  • readonlyGet的实现,即createGetter(true)
function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    // ...
    // isReadonly 为 true 则不需要依赖收集
    !isReadonly && track(target, "get" /* GET */, key)
    return isObject(res)
      ? isReadonly
        ?
        // 如果 res 是个对象或者数组类型,则递归执行 readonly 函数把 res readonly
        readonly(res)
        : reactive(res)
      : res
  }
}
  • reactive API 最大的区别就是不做依赖收集

ref API

  • reactive API对传入的 target 类型有限制,必须是对象或者数组类型,而对于一些基础类型(比如 StringNumberBoolean)是不支持的,因此有了ref API
  • 使用
const msg = ref('Hello World') 
msg.value = 'Hello Vue'
  • ref 的实现
function ref(value) {
  return createRef(value)
}
const convert = (val) => isObject(val) ? reactive(val) : val
function createRef(rawValue) {
  if (isRef(rawValue)) {
    // 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
    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)) {
        // 判断有变化后更新值
        rawValue = newVal
        value = convert(newVal)
        // 派发通知
        trigger(r, "set" /* SET */, 'value', void 0)
      }
    }
  }
  return r
}
  • 首先处理嵌套ref,如果传入的 rawValue 也是 ref,那么直接返回
  • 然后对rawValue 做了一层转换,如果 rawValue 是对象或者数组类型,那么把它转换成一个 reactive 对象。
  • 最后定义一个对 value 属性做 gettersetter 劫持的对象并返回
    • get 部分就是执行 track 函数做依赖收集然后返回它的值
    • set 部分就是设置新值并且执行 trigger 函数派发通知
      image.png
  • 区别于vue2.x
  • 1)劫持数据的方式改成用 Proxy 实现
  • 2)收集的依赖由 watcher 实例变成了组件副作用渲染函数
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容