返回顶部
首页 > 资讯 > 前端开发 > JavaScript >vue3.x源码剖析之数据响应式的深入讲解
  • 735
分享到

vue3.x源码剖析之数据响应式的深入讲解

2024-04-02 19:04:59 735人浏览 独家记忆
摘要

目录前言什么是数据响应式数据响应式的大体流程Vue2.x数据响应式和3.x响应式对比大致流程图实现依赖收集代码仓库结尾前言 如果错过了秋枫和冬雪,那么春天的樱花一定会盛开吧。最近一直

前言

如果错过了秋枫和冬雪,那么春天的樱花一定会盛开吧。最近一直在准备自己的考试,考完试了,终于可以继续研究源码和写文章了,哈哈哈。学过vue的都知道,数据响应式在vue框架中极其重要,写代码也好,面试也罢,数据响应式都是核心的内容。在vue3的官网文档中,作者说如果想让数据更加响应式的话,可以把数据放在Reactive里面,官方文档在讲述这里的时候一笔带过,笔者刚开始也不是很理解。后来看了源码才知道,在vue3里面响应式已经变成了一个单独的模块,而处理响应式的模块就是reactive;

什么是数据响应式

从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。
换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。

因此实现数据响应式有两个重点问题:

  • 如何知道数据发生了变化?
  • 如何知道数据变化后哪里需要修改?

对于第一个问题,如何知道数据发生了变化,Vue3 之前使用了 ES5 的一个 api Object.defineProperty Vue3 中使用了 es6 的 Proxy,都是对需要侦测的数据进行 变化侦测 ,添加 getter 和 setter ,这样就可以知道数据何时被读取和修改。

第二个问题,如何知道数据变化后哪里需要修改,Vue 对于每个数据都收集了与之相关的 依赖 ,这里的依赖其实就是一个对象,保存有该数据的旧值及数据变化后需要执行的函数。每个响应式的数据变化时会遍历通知其对应的每个依赖,依赖收到通知后会判断一下新旧值有没有发生变化,如果变化则执行回调函数响应数据变化(比如修改 dom)。

数据响应式的大体流程

在vue3.0的响应式的部分,我们需要找的核心文件是vue3.0源码的packages里面的runtime-core下面的src里面的;我们今天研究的这条线,就是沿着render这条线走下去的;

 return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }

在该文件下找到render函数,如下所示;该函数的作用是渲染传入vnode,到指定容器中;

 const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

查看patch方法,初始化的话会走else if (shapeFlag & ShapeFlags.COMPONENT)

 const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // 初始化走这个
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
  }

接下来查看processComponent方法,接下来走我们熟悉的mountComponent

 const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // 初始化走挂载流程
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

进入mountComponent方法,其中比较重要的instance为创建组件实例,setupComponent为安装组件准备的;做选项处理用的;setupRenderEffec用于建立渲染函数副作用,在依赖收集的时候使用。

const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 创建组件实例
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    if (__DEV__ && instance.type.__hmrId) {
      reGISterHMR(instance)
    }

    if (__DEV__) {
      pushWarninGContext(initialVNode)
      startMeasure(instance, `mount`)
    }

    // inject renderer internals for keepAlive
    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }

    // resolve props and slots for setup context
    if (__DEV__) {
      startMeasure(instance, `init`)
    }
    // 安装组件:选项处理
    setupComponent(instance)
    if (__DEV__) {
      endMeasure(instance, `init`)
    }

    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

      // Give it a placeholder if this is not hydration
      // TODO handle self-defined fallback
      if (!initialVNode.el) {
        const placeholder = (instance.subTree = createVNode(Comment))
        processCommentNode(null, placeholder, container!, anchor)
      }
      return
    }

    // 建立渲染函数副作用:依赖收集
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

    if (__DEV__) {
      popWarningContext()
      endMeasure(instance, `mount`)
    }
  }

进入到setupComponent函数里面,观看setupComponent函数的内部逻辑,在这里面有属性插槽的初始化; 在这里面可以看到setupStatefulComponent方法,它就是用来处理响应式的。

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children, shapeFlag } = instance.vnode
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

进入方法setupStatefulComponent,其中const Component = instance.type as ComponentOptions用于组件配置。其中instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)用于代理,data,$等都是在这里处理的。

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // 组件配置
  const Component = instance.type as ComponentOptions

  if (__DEV__) {
    if (Component.name) {
      validateComponentName(Component.name, instance.appContext.config)
    }
    if (Component.components) {
      const names = Object.keys(Component.components)
      for (let i = 0; i < names.length; i++) {
        validateComponentName(names[i], instance.appContext.config)
      }
    }
    if (Component.directives) {
      const names = Object.keys(Component.directives)
      for (let i = 0; i < names.length; i++) {
        validateDirectiveName(names[i])
      }
    }
  }
  // 0. create render proxy property access cache
  instance.accessCache = {}
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  if (__DEV__) {
    exposePropsOnRenderContext(instance)
  }
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    pauseTracking()
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    resetTracking()
    currentInstance = null

    if (isPromise(setupResult)) {
      if (isSSR) {
        // return the promise so server-renderer can wait on it
        return setupResult.then((resolvedResult: unknown) => {
          handleSetupResult(instance, resolvedResult, isSSR)
        })
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.
        instance.asyncDep = setupResult
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        )
      }
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // 处理选项等事务
    finishComponentSetup(instance, isSSR)
  }
}

由于咱们的案例里面没有setup,所以会执行 finishComponentSetup(instance, isSSR)来处理选项式api相关的东西。进入该函数里面查看代码逻辑,会看到如下的代码,该部分的代码用于处理选项式API相关的东西,用于支持vue2.x的版本。

  // support for 2.x options
  // 支持选项API
  if (__FEATURE_OPTIONS_API__) {
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }

进入applyOptions方法里面;往下翻,会看到这几行注释,这几行注释清晰地解释了vue2.x里面各个选项的优先级,其中包括props、inject、methods、data等。

  // options initialization order (to be consistent with Vue 2):
  // - props (already done outside of this function)
  // - inject
  // - methods
  // - data (deferred since it relies on `this` access)
  // - computed
  // - watch (deferred since it relies on `this` access)

继续往下看,会看到这几行代码,我们这里面用的不是混入的形式,所以这行这一系列的代码,,其中涉及到数据相应式的代码都在resolveData方法里面。

  if (!asMixin) {
    if (deferredData.length) {
      deferredData.forEach(dataFn => resolveData(instance, dataFn, publicThis))
    }
    if (dataoptions) {
      // 数据响应式
      resolveData(instance, dataOptions, publicThis)
    }

进入resolveData里面,可以看到const data = dataFn.call(publicThis, publicThis),这一行代码用于获取数据对象。instance.data = reactive(data)这一行代码用于对data做响应式处理。其中核心的就是reactive,该方法用于做响应式的处理。选项式api也好,setup也罢,最终走的都是reactive方法,用该方法来做响应式处理。

function resolveData(
  instance: ComponentInternalInstance,
  dataFn: DataFn,
  publicThis: ComponentPublicInstance
) {
  if (__DEV__ && !isFunction(dataFn)) {
    warn(
      `The data option must be a function. ` +
        `Plain object usage is no longer supported.`
    )
  }
  // 获取数据对象
  const data = dataFn.call(publicThis, publicThis)
  if (__DEV__ && isPromise(data)) {
    warn(
      `data() returned a Promise - note data() cannot be async; If you ` +
        `intend to perfORM data fetching before component renders, use ` +
        `async setup() + <Suspense>.`
    )
  }
  if (!isObject(data)) {
    __DEV__ && warn(`data() should return an object.`)
  } else if (instance.data === EMPTY_OBJ) {
    // 对data 做响应式处理
    instance.data = reactive(data)
  } else {
    // existing data: this is a mixin or extends.
    extend(instance.data, data)
  }
}

进入到reactive里面,观察其中的代码逻辑;这里面的createReactiveObject用于对数据进行处理。其中target是最终要转化的东西。

  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )

其中mutableHandlers里面有一些get、set、deleteProperty等方法。mutableCollectionHandlers会创建依赖收集之类的操作。

vue2.x数据响应式和3.x响应式对比

到这里,我们先回顾一下vue2.x是如何处理响应式的。是用defineReactive来拦截每个key,从而可以检测数据变化,这一套处理方式是有问题的,当数据是一层嵌套一层的时候,就会进行层层递归,从而消耗大量的内存。由此来看,这一套处理方式算不上友好。Vue3里面也是用用defineReactive来拦截每个key,与此不同的是,在vue3.x里面的defineReactive里面用proxy做了一层代理,相当于加了一层关卡。Vue2.x里面需要进行递归对象所有key,速度慢。数组响应式需要额外实现。而且新增或删除属性无法监听,需要使用特殊api。而现在,直接一个new proxy直接把所有的问题都给解决了。与此同时,之前的那一套方法不知Map,Set、Class等数据结构

大致流程图

然后我们梳理一下到响应式的过程中顺序

实现依赖收集

在实现响应式的过程中,依赖收集是和其紧密相连的东西,其中setupRenderEffect函数中使用effect函数做依赖收集。进入setupRenderEffect函数内部,在上面的代码中有这个函数,这里不一一赘述,我们继续往下看。进入到该函数内部,会看到如下代码。effect可以建立一个依赖关系:传入effect的回调函数和响应式数据之间;effect就相当于的vue2里面的dep,然后vue3里面没有watcher了。

  instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance

继续往下看,会看到如下代码,subTree是当前组件vnode,其中renderComponentRoot方法用于实现渲染组件的根。

        const subTree = (instance.subTree = renderComponentRoot(instance))

到这里,vue3.0的响应式部分就算要告一段落了

代码仓库

手写vue3.0简版的实现数据响应式,已上传到个人仓库,有兴趣的可以看看。喜欢的话可以来个关注,哈哈哈。关注我,你在编程道路上就多了一个朋友。https://gitee.com/zhang-shichuang/xiangyingshi/tree/master/

结尾

vue的数据响应式在面试的过程中经常会被问到,究其原理,还是要去看源码。在读源码的时候难免也会有枯燥乏味的时候,但是坚持下来就是胜利,后期还会分享vue的编译过程,以及react相关的源码知识。

到此这篇关于vue3.x源码剖析之数据响应式的文章就介绍到这了,更多相关vue3.x数据响应式内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: vue3.x源码剖析之数据响应式的深入讲解

本文链接: https://lsjlt.com/news/163235.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • vue3.x源码剖析之数据响应式的深入讲解
    目录前言什么是数据响应式数据响应式的大体流程vue2.x数据响应式和3.x响应式对比大致流程图实现依赖收集代码仓库结尾前言 如果错过了秋枫和冬雪,那么春天的樱花一定会盛开吧。最近一直...
    99+
    2024-04-02
  • Vue解读之响应式原理源码剖析
    目录初始化initState()initProps()initData()observe()ObserverdefineReactive()依赖收集DepWatcher依赖收集过程移...
    99+
    2024-04-02
  • Vue深入讲解数据响应式原理
    目录响应式是什么如何实现数据响应式实现对象属性拦截通用的劫持方案总结响应式是什么 简而言之就是数据变页面变 如何实现数据响应式 在Javascript里实现数据响应式一般有俩种方案,...
    99+
    2024-04-02
  • 源码分析Vue3响应式核心之reactive
    目录一、Reactive源码1、reactive2、接着看工厂方法createReactiveObject二、baseHandlers1、baseHandlersvue3响应式核心文...
    99+
    2023-05-17
    Vue3响应式核心reactive Vue3响应式 reactive Vue3 reactive
  • 源码分析Vue3响应式核心之effect
    目录一、effect用法1、基本用法2、lazy属性为true3、options中包含onTrack二、源码分析1、effect方法的实现2、ReactiveEffect函数源码三、...
    99+
    2023-05-17
    Vue3响应式核心effect Vue3响应式effect Vue3 effect
  • 深度解析 Vue3 的响应式机制
    目录什么是响应式响应式原理定制响应式数据Vueuse 工具包什么是响应式 响应式一直都是 Vue 的特色功能之一;与之相比,JavaScript 里面的变量,是没...
    99+
    2024-04-02
  • 深入理解Vue的数据响应式
    目录1. ES语法的getter和setter2. ES语法的 defineProperty3. Vue对数据的代理和监听4. Vue的数据响应式1. ES语法的getter和set...
    99+
    2024-04-02
  • Vue3响应式函数toRef()对比toRefs()源码分析
    今天小编给大家分享一下Vue3响应式函数toRef()对比toRefs()源码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下...
    99+
    2023-07-05
  • vue3.x数据响应式的流程是怎样的
    本篇内容介绍了“vue3.x数据响应式的流程是怎样的”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是数据响应式从一开始使用 Vue 时,...
    99+
    2023-06-28
  • Vue源码解析之数据响应系统的示例分析
    这篇文章主要介绍Vue源码解析之数据响应系统的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!数据双向绑定的思路1. 对象先来看元素是对象的情况。假设我们有一个对象和一个监测方...
    99+
    2024-04-02
  • vue响应式原理与双向数据的深入解析
    了解object.defineProperty 实现响应式 清楚 observe/watcher/dep 具体指的是什么 了解 发布订阅模式 以及其解决的具体问题 在Javascri...
    99+
    2024-04-02
  • Java数据结构之HashMap源码深入分析
    目录基本结构get方法put方法HashMap的容量为什么总是2的n次幂HashMap是Java集合框架中常用的一种数据结构,它是一种基于哈希表实现的映射表.在JDK1.8版本中,H...
    99+
    2023-05-17
    Java HashMap原理 Java HashMap Java HashMap源码
  • 深入浅析vue处理响应式数据的方法
    本篇文章带大家学习vue,聊聊vue如何处理响应式数据?希望对大家有所帮助!关于vue的响应式数据,你可能有很多疑惑。比如为什么要改用proxy比如为什么有reactive和ref两个api比如vue是如何实现响应式的?其实这些在源码中,都...
    99+
    2023-05-14
    前端 Vue.js
  • 深入理解Python虚拟机中复数(complex)的实现原理及源码剖析
    目录复数数据结构复数的操作复数加法复数取反Repr 函数总结复数数据结构 在 cpython 当中对于复数的数据结构实现如下所示: typedef struct { doub...
    99+
    2023-03-14
    Python 虚拟机 复数 Python虚拟机 Python 复数
  • C++深入讲解对象的销毁之析构函数
    目录一、对象的销毁二、析构函数三、小结一、对象的销毁 生活中的对象都是被初始化后才上市的生活中的对象被销毁前会做一些清理工作—股而言,需要销毁的对象都应该做清理 解决方案...
    99+
    2024-04-02
  • Spring深入刨析声明式事务注解的源码
    目录1、@EnableTransactionManagement2、加载事务控制组件2.1、AutoProxyRegistrar2.2、ProxyTransactionManagem...
    99+
    2024-04-02
  • 剖析 Python 变量与数据类型:深入浅出的学习之旅
    变量是计算机程序中存储数据的容器,其类型决定了存储数据的格式和操作。在 Python 中,变量和数据类型密不可分,深入理解两者对于有效利用语言至关重要。 Python 变量 标识符:变量名称,由字母、数字或下划线组成,不能以数字开头。 ...
    99+
    2024-04-02
  • Spring Boot源码解读与原理剖析:深入探索Java开发的奥秘!
    评论区留言赠书15本 关注+点赞+评论,评论区回复“Spring Boot源码解读与原理剖析:深入探索Java开发的奥秘!” 每篇最多评论3条!!采用抽奖助手自动拉取评论区有效评论送书两本, 开奖时间:9月11号 承载着作者的厚...
    99+
    2023-09-13
    Java SpringBoot 原力计划
  • 深入剖析ASP数据类型索引在Linux系统下的应用。
    ASP数据类型索引在Linux系统下的应用 随着互联网技术的不断发展,网站的流量和数据量越来越大,对于网站的性能和稳定性要求也越来越高。ASP数据类型索引作为一种常见的优化方式,可以提高网站的查询效率,降低数据库的负载,从而提升网站的用户体...
    99+
    2023-06-18
    linux 数据类型 索引
  • vue源码之批量异步更新策略的深入解析
    vue异步更新源码中会有涉及事件循环、宏任务、微任务的概念,所以先了解一下这几个概念。 一、事件循环、宏任务、微任务 1.事件循环Event Loop:浏览器为了协调事件处理、脚本...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作