Vuejs技术内幕:数据响应式之3.x版

每个系列一本前端好书,帮你轻松学重点。

本系列来自ZOOM前端架构师,前百度、滴滴资深技术专家黄轶 所编写的 《Vue.js技术内幕》

Vuejs的响应式是其能够得以流行的核心之一,上一篇我们介绍了2.x版本的实现原理,本篇看3.x。

在Vuejs 3.x中,响应式设计的变化可谓十分明显,说是换了个框架都不为过。有专门的API,可按需调用,代码组织也做了调整,不再跟框架紧密绑定,而是可以在项目中单独使用。

就像这样:

csharp 复制代码
// 安装
npm install @vue/reactivity

// 导入使用
import { reactive, ref } from '@vue/reactivity';
 
const state = reactive({ count: 0 });
const count = ref(0);

这极大地提升了Vue相关工具使用的灵活性。

通过这段代码,已经看到了Vue.js 3.x中响应式API,就是 reactiveref ,本篇围绕 reactive 展开。

reactive API

Vue.js 3.x中,组件书写的模式变为了Compostion API,上篇文的实例代码可改写为:

xml 复制代码
<template>
  <div>
   <p>{{ state.msg }}</p>
   <button @click="random">Random msg</button>
  </div>
</template>

<script>
  import { reactive } from 'vue'
  export default {
    setup(){
      const state = reactive({
        msg:"msg reactive"
      })
      const random = function(){
        state.msg = Math.random()
      }
      return {
        random,
        state
      }
    }
  }
</script>

可以看到,Vue.js 3.x的响应数据不是一股脑地塞进data对象中,而是使用reactive按需定义。

那么对响应式的研究,就转变为对API的拆解:

javascript 复制代码
function createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers,proxyMap){
  
  // 如果不是对象类型,则直接返回
  if(!isObject(target)){
    return target
  }

  // target已经是proxy类型的对象,直接返回
  if(target.__v_.raw && !(isReadOnly && target.__v_.isReactive)){
    return target
  }

  // 使用Proxy创建响应式对象
  const proxy = new Proxy(targetType === 2 ? mutableCollectionHandlers : baseHandler);
  
  // 缓存已代理的对象
  proxyMap.set(target,proxy)
  return proxy

}

以上代码的关键自然是 new Proxy的调用,在调用之前做了一些判断,避免规则之外的行为或者做重复的工作:

  • 入参是否为对象或者数组
  • 入参是否已经被处理为响应式
  • 入参是否为应该处理的类型

你肯定好奇,targetType === 2 代表什么,这个东西隐藏在getTargetType方法中,这个方法中Object、Array等返回1,Map、Set等返回2,否则返回0,如Date、Promise等类型。

响应式三要素

知道了怎么走到Proxy,就可以进行更深层次的探究,探究之前,先对全局有个把握。

本质上,它还是在劫持和处理数据,所以,依标准三件套:依赖收集、派发更新、副作用函数

先看看处理器对象,它来告诉proxy拦截哪些行为,主要是 mutableCollectionHandlers 的部分。

arduino 复制代码
const mutableCollectionHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownkeys
}

// 就是做这个用的
new Proxy(mutableCollectionHandlers);

这个对象代表拦截了哪些操作,显然包括了get、set和delete。

接下来就分析常用的get、set函数的实现。

依赖收集

依赖收集发生在数据访问阶段,即get,看看get中做了哪些处理。

javascript 复制代码
function createGetter(isReadonly = false){
  return function get(target,key,receiver){
    if(key === '__v_isReactive'){
      return !isReadonly
    }
    // 求值
    const res = Reflect.get(target,key,receiver)
    // 依赖收集
    if(!isReadonly){
      track(target,'get',key)
    }
    return res
  }
}

代码中真正起作用的,是track方法。

需要说明的一点是,在Vue2.x中,响应式数据如果仍是对象,会递归执行,但Vue3.x中会在访问数据时才决定要不要执行,这会带来一定性能上的提升。

那么,track方法做了什么:

scss 复制代码
// 当前激活的activeEffect
let activeEffect
// 原始数据对象的map
const targetMap = new WeakMap()
function track(target, type, key){

  let depsMap = targetMap.get(target)
  // 每个target对应一个depsMap 
  if(!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)){
    // 收集当前激活的activeEffect作为依赖
    dep.add(activeEffect)
    // 当前激活的effect收集dep集合作为依赖
    activeEffect.deps.push(dep)
  }

}

track接收三个参数:target(原始数据)、type(依赖类型)、key(访问的属性)

  • targetMap,用于存储原始数据(target)
  • depsMap,用于存储依赖(dep)
  • dep,用于存储副作用函数(effect)

副作用函数,就是每次数据发生变化时所会执行的函数 ,所谓"副作用",就是使某些跟响应数据关联的状态发生变化

因此,每次执行track函数,都会把当前激活的activeEffect作为依赖,收集到与target相关的dep中,记住这一点很重要。

收集完依赖,下一步,就是在值发生改变的时候派发通知进行更新。

派发通知

派发通知发生在数据更新阶段,通过执行set来实现。

scss 复制代码
function createSetter(){
  return function set(target,key,value,receiver){
    let oldValue = toRaw(target[key])
    const hadKey = isArray(target) && isIntergerKey(key)? Number(key) < target.length : hasOwn(target,key)
    // 设置值
    const result = Reflect.set(target,key,value,receiver){
      if(target === toRaw(receiver)){
        if(!hadKey){
          trigger(target,'add',key,value)
        } else if(HashChanged(value,oldValue)){
          trigger(target,'set',key,value,oldValue)
        }
      }
    return result
  }
}

set函数主要做了两件事:通过Reflect.set实际设置属性值,并确保操作符合规范,然后通过trigger函数派发通知。

最核心的部分是trigger,我们进一步看看这个函数:

scss 复制代码
const targetMap = new WeakMap()
function trigger(target,type,key){
  const depsMap = targetMap(target)
  if(!depsMap){
    return
  }
  const effects = new Set()
  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.forEach(run)
  }
}

trigger函数主要做了四件事:

1、从targetMap中获取target对应的依赖集合depsMap

2、创建effects集合

3、根据key将depsMap中的effects添加到集合中

4、遍历effects,执行副作用函数

接下来,整个流程只剩下揭开副作用函数的庐山真面目。

副作用函数

介绍副作用之前,先清楚它是怎么工作的。

通过一个简单的示例了解:

scss 复制代码
const counter = reactive({
  num:0
})
function logCount(){
  console.log(counter.num)
}
function count(){
  counter.num++
}
logCount()
count()

这个示例中,先通过logCount的执行访问了counter.num,然后,希望在执行count函数时对num修改,同时自动执行logCount函数。

怎样实现这样的效果呢?秘诀就在之前看到过的activeEffect,它是一个来存储当前执行函数的全局变量,当get时,把这个函数设置为activeEffect,当set时,再触发这个activeEffect,就可以了。

实现原理如下:

scss 复制代码
let activeEffect;
function wrapper(fn){
  const wrapped = function(...args){
    activeEffect = fn
    fn(...args)
  }
  return wrapped
}
const wrappedLog = wrapper(logCount)
wrappedLog()

Vue源码的核心实现如下:

javascript 复制代码
const effectStack = []
function createReactiveEffect(fn,options){
  const effect = function reactivedEffect(){
    try{
      // 入栈
      effectStack.push(effect)
      activeEffect = effect
      // 执行原始函数
      return fn()
    } finally {
      // 出栈
      effectStack.pop()
      // 指向栈的最后一个effect
      activeEffect = effectStack[effectStack.length-1]
      effect.options = options
      return effect
    }
  }
}

这段代码中,当执行trigger时,真正执行的effect就是reactivedEffect。

按照前面所说,它只做两件事,让全局的activeEffect指向它,执行被包装的原始函数fn。

除此之外,有个额外的东西是effectStack,干嘛的?

这是因为,需要考虑到函数的执行是会出现嵌套的,如果仅仅是赋值,activeEffect可能出现错误,使用栈结构之后,每次执行函数入栈,执行完出栈,就不会出现这种错误。

小结

至此,Vuejs 3.x响应式实现流程介绍完毕。

我们会发现,它的机制和Vuejs 2.x是类似的,但实现上的差异不小,并不单单是用Proxy代替了Object.defineProperty,而是改变了实现的逻辑:

  • 使用惰性响应式,减少了初始化开销。
  • 对嵌套对象的处理更加高效。
  • 对数组的支持更高效。
  • 使用Map存储依赖关系,结构更高效。

这几块内容的解读,不单是让大家能弄懂Vue,更重要的是对框架设计消除陌生感,对知识点本身,对代码实践,有更好的认识,体会相对复杂项目应该是什么样子。

下一篇,我们将展示算法的魅力。

更多好文第一时间接收,可关注公众号:前端说书匠

相关推荐
德莱厄斯16 小时前
没开玩笑,全框架支持的 dialog 组件,支持响应式
前端·javascript·github
非凡ghost17 小时前
Affinity Photo(图像编辑软件) 多语便携版
前端·javascript·后端
非凡ghost17 小时前
VideoProc Converter AI(视频转换软件) 多语便携版
前端·javascript·后端
endlesskiller17 小时前
3年前我不会实现的,现在靠ai辅助实现了
前端·javascript
用户9047066835717 小时前
commonjs的本质
前端
Sailing17 小时前
5分钟搞定 DeepSeek API 配置:从配置到调用一步到位
前端·openai·ai编程
麦麦大数据17 小时前
F035 vue+neo4j中医南药药膳知识图谱可视化系统 | vue+flask
vue.js·知识图谱·neo4j·中医·中药·药膳·南药
Pa2sw0rd丶17 小时前
如何在 React 中实现键盘快捷键管理器以提升用户体验
前端·react.js
麦麦大数据17 小时前
F037 vue+neo4j 编程语言知识图谱可视化分析系统vue+flask+neo4j
vue.js·flask·知识图谱·neo4j·可视化·编程语言知识图谱
非凡ghost17 小时前
ToDoList(开源待办事项列表) 中文绿色版
前端·javascript·后端