• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

编写Vue3.0源码-响应式数据原理

武飞扬头像
zxl樑樑
帮助23

“TM的现在也太卷了,上来就问小明:你能说下reactive的实现原理吗?”

小明哭丧着脸!Vue2还没搞明白呢,现在给我整vue3,有没有搞错哦!

现在面试原理很正常啊,都2023年了不会还没关注源码系列内容吧?

小明又挠了挠头,关注是关注了,一个文件几千行的代码表示看不懂了。

正文

1. Composition API

大家都知道Vue2.0的响应式是通过Object.defineProperty(),但是只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历,并且无法检测数组的变化。而进入3时代尤大大居然想到了使用Es6语法中的Proxy,从而大大优化了上述问题。

import { reactive } from 'Vue';

const state = reactive({
    name: '小明',
    age: 108, // 虽然是90后,但是心智已过百  卷...
})

使用过Vue3的这段代码想必是非常熟悉了,不在是老套的将响应式数据写在data中了,类似React中的Hooks,使用更加灵活。并且同时它还给我们提供了其他几个带有优化功能的Api。

名称 功能
reactive 定义响应式变量,仅支持对象、数组、Map、Set等集合类型有效。对String、number、boolean、等原始类型无效
shallowReactive 与reactive的区别就是该Api只对对象第一层数据进行响应式
readonly 入参和reactive相同,整个对象只读无法进行修改
shallowReadonly 只是第一层是只读的

2. 暴露出响应式API

// reactivity/reactive.js
export function reactive(target) { // target为目标对象
  return createReactiveObj(target, false, reactiveHandlers)
}

export function shallowReactive(target) { // target为目标对象
  return createReactiveObj(target, false, shallowReactiveHandlers)
}

export function readonly(target) { // target为目标对象
  return createReactiveObj(target, true, readonlyHandlers)
}

export function shallowReadonly(target) { // target为目标对象
  return createReactiveObj(target, true, shallowReadonlyHandlers)
}

在源码中采用的是高阶函数柯里化,因为这四个API的功能实现上大差不差,柯里化可以通过不同的参数来进行不同的处理,提供公共的方法。所以我这里我们只需要考虑两点:

  1. 是不是只读的(readonly)
  2. 是不是浅层响应式数据(shallow)
// reactivity/reactive.js
// 用来存储已经响应式的数据,防止重复代理
const reactiveMap = new WeakMap();
const readonlyMap = new WeakMap();

// target目标对象  isReadonly是不是只读的  baseHandlers为proxy的配置属性参数
function createReactiveObj(target, isReadonly, baseHandlers) { 

  // target必须是一个对象
  if (!isObject(target)) { 
    return target;
  }
  // 优化已经被代理的对象
  const proxymap = isReadonly ? readonlyMap : reactiveMap;
  const proxyEs = proxymap[target];
  
  // 已存在直接返回代理数据
  if (proxyEs) {
    return proxyEs;
  }
  
  // 使用proxy对目标对象进行代理
  const proxy = new Proxy(target, baseHandlers); 
  proxymap.set(target, proxy);
  return proxy;
}

小明很开心😀,这段代码我看懂了:“ 这里是通过isReadonly参数的不同来创建代理对象proxy。如果传入的目标对象已经被代理过就会被缓存 ”

面试官: ” 那为什么这里要使用WeakMap而不是用Map

小明又暗自窃喜🤭,之前刚好看过这个。因为WeakMap数据结构的key可以是一个引用类型的对象,并且可以被自动垃圾回收。

// reactivity/baseHandlers.js
// 这里是上文中各个不同API的proxy的配置对象
export const reactiveHandlers = {
  get: get,
  set: set
}
export const shallowReactiveHandlers = {
  get: shallowGet,
  set: shallowSet
}
export const readonlyHandlers = { // readonly的不可进行修改,所以这里直接给出警告
  get: readonlyGet,
  set: (target, key) => {
    console.warn('is readonly');
    return true;
  }
}
export const shallowReadonlyHandlers = { // readonly的不可进行修改,所以这里直接给出警告
  get: shallowReadonlyGet,
  set: (target, key) => {
    console.warn('is readonly');
    return true;
  }
}
// reactive/baseHandlers.js
const get = createGetter(); // 不是只读也不是前层次的
const shallowGet = createGetter(false, true); // 不是只读 是浅层次的
const readonlyGet = createGetter(true); // 只读 深的
const shallowReadonlyGet = createGetter(true, true); // 只读 浅层次的
const set = createSet();
const shallowSet = createSet(true); // 浅层次的

小明:“ 哦!这里又和上面一样吧,又是区分参数来实现函数柯里化吧! 通过是不是只读的和是不是浅层次的来创建不同的get函数set函数

// reactivity/baseHandlers.js

// 创建get函数来进行依赖来进行依赖收集
// isReadonly只读的 shallow是不是浅层次
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    if (shallow) { // 浅层次的直接返回,不做深层次的递归
      return res;
    }
    if (isObject(res)) { // vue3性能优化 懒代理 只有访问了才进行递归深层次的代理
      return isReadonly ? readonly(res) : reactive(res)
    }
    if (!isReadonly) {
      // 收集依赖
      Track(target, TrackOpTypes.GET, key);
    }
    return res;
  }
}

 // 拦截设置功能
function createSet(shallow = false) {
  return function set(target, key, value, receiver) { // target目标对象 key属性 value修改的新值
    // 获取到老值
    const oldValue = target[key];
    // 1.数组还是对象 2.添加值 还是 修改值
    const hadKey = isArray(target) && (isIntergetKey(key) ? Number(key) < target.length : hasOwn(target, key));
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) { // 新增
      // 触发更新
      trigger(target, TriggerOpTypes.ADD, key, value);
    } else if (hasChange(value, oldValue)) { // 修改
      // 触发更新
      trigger(target, TriggerOpTypes.SET, key, value, oldValue);
    }
    return result;
  }
}

createGetter: 函数主要是完成了对浅层次的剔除,不做深一层的递归代理。同时如果是只读类型的,函数就递归又调用了一边readonlyApi,对响应式的数据使用reactive递归代理。

面试官问小明:“ 这段代码里面有什么优化的操作吗? ”

小明: “ 额.....没看出来😅 ”

面试官:“ 让我来告诉你吧!这里就是proxyObject.defineProperty好的地方。之前在Vue2.0中一上来就对data中属性进行递归遍历,不管是用到的还是没用到的。而proxy是对对象进行代理,如果我们没有用到深层次的数据,他就不会进行递归代理,只有我们用到了才进行递归,明白了吗? ”

小明:“ 那是不是就是Object.freeze()的意思啊? ”

面试官:“ 666 ”

3. Effect观察者

在讲依赖收集之前,大家要先知道一个effect概念。effect有点类似vue2.0中的watcher监听者,为了解决vue2的问题,依赖收集(即添加观察者/通知观察者)模块单独出来,就是现在的effect。我们先使用一下effect。

import { effect } from 'Vue';

<script>
export default{
    setup(){
        // effect接受两个参数 一个是函数,一个配置对象
        effect(() => {
            console.log('小明今年高寿啊!') // 哈哈哈...
        })
    }
}
</script>

我们发现页面一进入就打印出了“ 小明今年高寿啊! ”。当然effect也可以不立即执行,只需要添加配置参数lazy: true

// reactivity/effect.js

// effect函数接口一个函数fn和配置对象options
export function effect(fn, options = {}) {
  const effect = createReactiveEffect(fn, options);
  if (!options.lazy) { // 如果lazy不为true就立即执行
    effect();
  }
  return effect;
}

let uid = 0; // 创建effect的自增uid,类似于vue2.0中的每个watcher都有一个id
let activeEffect; // 保存当前的effect,类似于vue2.0中的Dep.target
const effectStack = []; // effect存储栈

function createReactiveEffect(fn, options = {}) {
  const effect = function reactiveEffect() {
    if (!effectStack.includes(effect)) {
      try {
        activeEffect = effect; // 当前依赖收集的effect
        effectStack.push(activeEffect); // 入栈
        return fn(); // 执行用户的方法并返回值
      } finally {
        // 出栈
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1]; // 重置当前effect
      }
    }
  }
  effect.id = uid  ; // 用于区别effect
  effect._isEffect = true; // 用户区分我们effect是不是响应式的
  effect.raw = fn; // 保存用户的方法
  effect.options = options; // 保存用户的属性
  return effect;
}

effect: 函数主要用来生成/处理/追踪reactiveEffect数据,主要是收集数据依赖(观察者),通知收集的依赖(观察者)。

小明:“ 我去官网看了下,好像说是这个effect不是给开发者用的 ”

面试官::“ 是的呢!这个函数主要是给作者用的。 ”

4. Track依赖收集

上文中在proxyget方法中使用了Track进行依赖收集,依赖收集的数据一个是keytarget目标对象,value是一个key为目标对象的属性value为收集到的effect的Set

数据结构:

WeakMap(target, Map(key, Set(effect)))

// reactivity/effect.js

// 收集effect
let targetMap = new WeakMap(); // 用于存放effect的map
export function Track(target, type, key) {
  if (!activeEffect) return // 如果当前没有effect直接结束
  let depMap = targetMap.get(target);
  if (!depMap) {
    targetMap.set(target, (depMap = new Map())); // 根据当前target获取depMap,如果没有就新增一个Map
  }
  let dep = depMap.get(key);
  if (!dep) {
    depMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect); // 收集effect
  }
}

5. trigger派发更新

当我们修改一个对象中的key时,就去刚刚的targetMap中去查找依赖的effect并执行。trigger主要功能是通知target[key](将观察者队列函数一一取出来执行)。

// reactivity/effect.js

// 触发更新
// target 目标对象 type可以为SET ADD DELETE  key:属性key
export function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  let effects = new Set(); // 触发的effect 需要进行去除
  const add = (effectAdd) => {
    if (effectAdd) {
      effectAdd.forEach(effect => {
        effects.add(effect);
      })
    }
  }
  add(depsMap.get(key)); // 获取当前属性的effect
  if (key === 'length' && isArray(target)) { // 如果修改数组的长度length,需要做处理
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newValue) {
        add(dep)
      }
    })
  } else {
    if (key !== undefined) {
      add(depsMap.get(key));
    }
    switch (type) {
      case TriggerOpTypes.ADD: {
        if (isArray(target) && isIntergetKey(key)) { // 如果数组新增索引需要处理数组length
          add(depsMap.get('length'))
        }
        break;
      }
    }
  }
  effects.forEach(effect => { // 遍历执行effect栈
    if (effect.options.scheduler) {
      effect.options.scheduler(effect);
    } else {
      effect(); // 执行effect
    }
  })
}

小明:“ 相比之下这种里面Map来存储关联的effectvue2.0中在目标对象上添加一个_ob_来实现容易理解的多。 ”

6. 使用effect

面试官:“ 到此为止,我们的Vue3.0的响应式源码已经完毕,让我们来测试下其功能。 ”

小明: “哇,”

<div id="app"></div>
<button id="age">长大了</button>
import { effect, reactive } from './reactivity/index';

let state = reactive({
  name: '小明',
  age: 108,
});

effect(() => { // 这里代替页面上使用了state.age值
    document.getElementById("app").innerHTML = state.age;
})

document.getElementById("age").onclick = function () {
  state.age  
}

7. 目录结构

这里时我写源码的基本目录结构:

  • reactivity
    • reactive.js // 这里列出了常用api(relative, shallowRealtive, readonly)
    • effect.js // 这里列出了依赖收集和派发更新方法
    • baseHandlers.js // baseHandlers 中主要包含四种proxy的配置对象
    • operations.js // 这里列出了一项枚举值
  • shared
    • index.js // 这里时工具方法,比如:isObject, isArray

小结

至此Vue3.0的响应式数据原理已经完结 大家可以试着自己动手写一遍核心代码哈,本文主要列出了一个核心功能点,其中不乏出错的地方,望请见谅!至于修改数据如何更改试图,后续我还会更新,目前只是用了个小案例测试一下。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanffaaa
系列文章
更多 icon
同类精品
更多 icon
继续加载