Vue3 响应式原理剖析

image

简述

Vue3 发布后,有一个重要的有关于响应式机制的改动,html

Vue2 的时候,采用的是 Object.defineProperty 方式,重构数据的 setget 方法,来达到监听数据变动的方法,vue

可是在 Vue3 发布后,就再也不使用 Object.defineProperty 了,而是使用了 ES6 中的 Proxy 来对数据进行一个封装,起到一个中间代理的做用来监听数据的变动,对于 Proxy 不了解的小伙伴能够看这里:Proxyreact

下面主要是对 Vue3 的响应式机制进行一个简单的实现,主要包含两个:refreactivegit

  • ref:是对基础数据进行封装监听,例如:Boolean、Numberes6

  • reactive:是对复杂数据进行封装监听,例如:github

{
    key1: 'Benson',
    key2: {
        key3: 1007,
        key4: [1, 2, 3]
    }
}
复制代码

ref 迷你版

let activeEffect // 用于保存当先须要依赖的函数

// mini 依赖中心
class Dep {
  constructor(){
    this.subs = new Set(); // 使用 Set 避免重复收集依赖
  }
  depend(){
    // 收集依赖
    if(activeEffect){
      this.subs.add(activeEffect)
    }
  }
  notofy(){
    // 数据变化,触发effect执行
    this.subs.forEach(effect=>effect())
  }
}

function effect(fn){
  activeEffect = fn; // 保存当前响应式依赖函数
  fn(); // 执行依赖函数
}

const dep = new Dep() // vue3 中就变成一个大的 map

// ref 大概的原理在这了,待会后面能够看代码
function ref(val){
  let _value = val
  // 拦截.value操做
  let state = {
    get value(){
      // 获取值,收集依赖 track
      dep.depend()
      return _value
    },
    set value(newCount){
      // 修改,通知dep,执行有这个依赖的effect函数
      // 源码这里会作判断,是否真的值发生了变化
      _value = newCount
      // trigger
      dep.notofy()
    }
  }
  return state
}

const state = ref(0)

effect(()=>{
  // 这个函数内部,依赖state的变化
  console.log(state.value)
})

setInterval(()=>{
  state.value++; // 这里进行响应式数据的值改变,触发 set 方法
},1000)
复制代码

上面的案例就是对 ref 的一个简单实现了,其实已经可以很好的表示 Vue3 在源码中对 ref 的实现逻辑了。typescript

接下来能够了解一下源码是怎么样的:segmentfault

ref 在源码中会对传入的数据进行类型判断,若是判断为对象数据类型会使用 reactive 去进行响应式分装的,否者会使用 RefImplget,set 方法去监听,这点相似于 Vue2 的 Object.definePropert。数组

在源码上为 Ref 定义了一个 interface缓存

// 生成一个惟一key
declare const RefSymbol: unique symbol

export interface Ref<T = any> {
  /**
   * value值,存放真正的数据的地方
   */
  value: T
  /**
   * Type differentiator only.
   * We need this to be in public d.ts but don't want it to show up in IDE
   * autocomplete, so we use a private Symbol instead.
   * 用此惟一 key,来作 Ref 接口的一个描述符, 让 isRef 函数作类型判断
   */
  [RefSymbol]: true
  /**
   * @internal
   */
  _shallow?: boolean
}
复制代码

接下来看看 ref 方法:

// 对于 ref 进行屡次重载
export function ref<T extends object>(
  value: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  return createRef(value)
}

// 看通常状况 ref(123),使用最后一个

function createRef(rawValue: unknown, shallow = false) {
  // 判断是否已是响应式 ref 数据了
  if (isRef(rawValue)) {
    return rawValue
  }
  // 建立响应式数据
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    // 转化数据为响应式数据
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    // track 的代码在 effect中,能猜到此处就是监听函数收集依赖的方法
    track(toRaw(this), TrackOpTypes.GET, 'value')
    // 返回数据
    return this._value
  }

  set value(newVal) {
    // 若是数据发生变化
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      // 更新数据
      this._rawValue = newVal
      // 转化数据为响应式数据
      this._value = this._shallow ? newVal : convert(newVal)
      // 能猜到此处就是触发监听函数执行的方法
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

// 数据类型不合适使用 ref,将采用 reactive
const convert = <T extends unknown>(val: T): T =>
  /**
   * isObject() 从 @vue/shared 中引入,判断一个数据是否为对象
   * 若是传递的值是个对象(包含数组/Map/Set/WeakMap/WeakSet),则使用 reactive 执行,不然返回原数据
   */
  isObject(val) ? reactive(val) : val

// 从@vue/shared中引入,判断一个数据是否为对象
// Record<any, any>表明了任意类型key,任意类型value的类型
// 为何 val is Record<any, any> 而不是 val is object 呢?能够看下这个回答:
// https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'
复制代码

以上就是 Vue3 中对 ref 的简单阅读,至于 ref 里面的各个内部方法具体逻辑,能够了解一下前面的简单例子就能大概知道了,若是要仔细了解的话,就自行一步一步去查看源码了哈~

ref 源码

reactive 迷你版

Vue3 对于比较复杂的数据,就会采用 reactive 进行响应式的封装,下面来看看如何实现一个简易版的响应式逻辑:

<!--index.html-->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
  <div id="app"></div>
  <div id="btn">click</div>
  <script src="./vue.js"></script>
  <script>
    const root = document.getElementById('app')
    const btn = document.getElementById('btn')
    // 响应式封装
    let obj = reactive({
      name: 'Benson',
      age: 24,
      num: { count: 1 },
    })
    
    // 计算属性
    let double = computed(()=>obj.age*2)
    
    // 反作用,依赖函数
    effect(()=>{
      console.log('数据变了',obj.age)
      root.innerHTML = `<h1>${obj.name}今年${obj.age}岁了,双倍${double.value}, Num: ${obj.num.count}</h1>`
    })

    btn.addEventListener('click',()=>{
      // obj.age+=1;
      obj.num.count += 1; // 测试 reactive 递归封装 Proxy 的特色
    },false)
  </script>
</body>
</html>
复制代码

在 index.html 中,对一个对象进行 reactive 响应式封装,而且还生成一个对 obj.age 的计算属性,这里其实计算属性就是一个特殊的依赖函数(反作用函数)

effect 反作用函数传入的方法会在响应式数据发生变化后执行。反作用函数执行以后,计算属性根据取值操做,也就是 get 方法会触发,这时候,就会触发 computed 的传入的 Effect 执行获取到最新值,这一点能够留意一下,下面的简易版实现逻辑:

<!--vue.js-->
const effectStack = [] // 这里存储当前响应式数据的依赖函数
let targetMap = new WeakMap() // 存储全部reactive,全部key对应的依赖
// {
//   target1: {
//     key1: [effect]
//   }
// }
// target1 其实就是使用响应式源对象做为 key,对象中的属性做为 key1 ,而后该属性对应着哪一些反作用函数整合到 [effect] 中


function track(target,key){
  // 收集依赖
  // reactive可能有多个,一个又有N个属性key
  const effect = effectStack[effectStack.length-1]
  if(effect){
    let depMap = targetMap.get(target)
    if(!depMap){
      depMap = new Map() // 相似对象类型,里面放着响应数据的属性 key 和对应 dep
      targetMap.set(target, depMap)
    }
    let dep = depMap.get(key)
    if(!dep){
      dep = new Set() // 这里使用了 Set 很重要,这里的 Set 可以防止重复保存依赖函数
      depMap.set(key,dep)
    }
    // 添加依赖
    dep.add(effect)
    effect.deps.push(dep)
  }
}

function trigger(target,key,info){
  // 触发更新
  let depMap = targetMap.get(target)
  if(!depMap){
    return 
  }
  const effects = new Set()
  const computedRunners = new Set()

  if(key){
    let deps = depMap.get(key)
    deps.forEach(effect=>{
      if(effect.computed){
        computedRunners.add(effect)
      }else{
        effects.add(effect)
      }
    })
  }
  // 计算属性传入的 `fn` 会依赖 `reactive` 对象的属性 A
  // 因此这个 `fn` 也会在属性 A 依赖集合 `deps` 进行存储,属性 A
  // 发生了变化也会执行这个 `fn`
  computedRunners.forEach(computed=>computed())
  // 这里会执行通常的函数,这里就是主要就是执行:root.innerHTML 更新视图
  effects.forEach(effect=>effect())
}

function effect(fn,options={}){
  // {lazy:false,computed:false}
  // 反作用
  // computed是一个特殊的effect
  let e = createReactiveEffect(fn,options)

  if(!options.lazy){
    // lazy决定是否是首次就执行effect
    e()
  }
  return e
}

const baseHandler = {
  get(target,key){
    const res = Reflect.get(target, key); // reflect更合理的
    // 收集依赖
    track(target,key)
    // 当使用到内部属性的时候,再进行 Proxy 封装,
	if (typeof res === 'object') {
	  return reactive(res);
	}
    return res
  },
  set(target,key,val){
    const info = {oldValue:target[key], newValue:val}
    Reflect.set(target, key, val); // Reflect.set
    // 触发更新
    trigger(target,key,info)
  }
}
function reactive(target){
  if (typeof target === 'object') {
    /*
    if (target instanceof Array) {
      // 若是是一个数组,那么取出来数组中的每个元素
      // 判断每个元素是否又是一个对象,若是又是一个对象,那么也须要包装成 Proxy
	  target.forEach((item, index) => {
	    if (typeof item === 'object') {
          target[index] = reactive(item);
        }
      });
    } else {
	  // 若是是一个对象,那么取出对象属性的值
	  // 判断对象属性的值是否又是一个对象,若是又是一个对象,那么也须要包装成 Proxy
	  for (let key in target) {
	    const item = target[key];
		if (typeof item === 'object') {
		  target[key] = reactive(item);
		}
	  }
	}
	*/
    // target变成响应式
    const observerd = new Proxy(target, baseHandler);
    return observerd;
  } else {
	console.warn('请传入 Object');
	return target;
  }
}

function createReactiveEffect(fn,options){
  const effect = function _effect(...args){
  /* 这里的 _effect 和 fn 都会由于在 run 函数中保存在 effectStack,
   * 而后执行 fn 触发数据的 get 方法,保存在 targetMap 对应响应式数据属性 key 的 dep 中,
   * 因此 _effect 和 fn 都会一直处于闭包状态,而不会消失,
   * 这时候,设置响应式数据的 set 方法时,就会触发执行 _effect 方法,
   * 而且从新执行 run 和里面的 fn,这时候 fn执行时,
   * 又会触发响应数据的 get 方法,触发收集依赖函数,
   * 此时就是由于收集依赖的是 new Set(),一所不会致使重复收集相同的依赖,流程就是这样了
   */
    return run(_effect,fn,args) 
  }
  // 为了后续清理 以及缓存
  effect.deps = []
  effect.computed = options.computed
  effect.lazy = options.lazy
  return effect
}
function run(effect,fn,args){
  if(effectStack.indexOf(effect)===-1){
    try{
      /**
       * 这里计算属性取值的时候,会调用计算属性的 fn 获得返回值,若是没有 if (!effect.computed) 这个条件,
       * 那么计算属性中所依赖的属性好比:age 就会绑定上 fn 这个依赖,而不是绑定上 root.innerHTML 这个依赖
       * 会致使更新 age 值,没法刷新视图,由于对于这种状况:
       * effect(()=>{
       *   root.innerHTML = `<h1>双倍${double.value}</h1>`
       * })
       * 没有取值 obj.age,只作了 double.value 的取值的话,就没法让计算属性中的 age 绑定正确的更新函数了
       * 固然 vue3 源码中也并非这样作的,这里只是简单了一下,待更新中...
       */
      if (!effect.computed) effectStack.push(effect)
      return fn(...args)
    }finally{
      effectStack.pop()
    }
  }
}
function computed(fn){
  // 特殊的effect
  const runner = effect(fn, {computed:true,lazy:true})
  return{
    effect:runner,
    get value(){
      return runner() // 这里计算属性取值的时候,会执行这个 runner 从而获得最新的值,这个值是依赖于计算属性传入的 fn 而来的
    }
  }
}
复制代码

上诉案例就是简单的 reactive 实现,里面还有一个特殊的计算属性的响应式实现,基本流程作了什么,都在注释上进行标识了。

有一点注意的是: reactive 会进行嵌套封装 Proxy ,但它又不是一次性的,须要用到内部属性的时候会去给内部属性也封装 Proxy,这样返回的数据进行变动的时候,也能进行代理。

// 源码
if (isObject(res)) {
  // Convert returned value into a proxy as well. we do the isObject check
  // here to avoid invalid value warning. Also need to lazy access readonly
  // and reactive here to avoid circular dependency.
  return isReadonly ? readonly(res) : reactive(res)
}
复制代码

除了 reactiveref 外,还有两个相似的 Api:shallowreactiveshallowref,这两个和前两个的区别就是不执行嵌套对象的深度响应式转换,只封装第一层 Proxy。

代码中使用到了 ES6 的 Proxy 和 Reflect,不懂的小伙伴还得须要去了解一下这几个知识点滴~

reactive.js 源码

参考文献

相关文章
相关标签/搜索