Vue3.0数据响应式原理详解

基于Vue3.0发布在GitHub上的第一版源码(2019.10.05)整理

预备知识

  1. ES6 Proxy,整个响应式系统的基础。
  2. 新的composition-API的基本使用,目前还没有中文文档,可以先通过这个仓库(composition-api-rfc)了解,里面也有对应的在线文档。

先把Vue3.0跑起来

先把vue-next仓库的代码clone下来,安装依赖然后构建一下,vue的package下的dist目录下找到构建的脚本,引入脚本即可。
下面一个简单计数器的DEMO:

<!DOCTYPE html>
<html lang="en">
<body>
 <div id='app'></div>
</body>
<script src="./dist/vue.global.js"></script>
<script>
const { createApp, reactive, computed } = Vue;

const RootComponent = {
 template: `
  <button @click="increment">
   Count is: {{ state.count }}
  </button>
 `,
 setup() {
  const state = reactive({
   count: 0,
  })

  function increment() {
   state.count++
  }

  return {
   state,
   increment
  }
 }
}

createApp().mount(RootComponent, '#app')
</script>
</html>

template和之前一样,同样Vue3也支持手写render的写法,template和render同时存在的情况,优先render。

setup选项是新增的主要变动,顾名思义,setup函数会在组件挂载前(beforeCreate和created生命周期之间)运行一次,类似组件初始化的作用,setup需要返回一个对象或者函数。返回对象会被赋值给组件实例的renderContext,在组件的模板作用域可以被访问到,类似data的返回值。返回函数会被当做是组件的render。具体可以细看文档。

reactive的作用是将对象包装成响应式对象,通过Proxy代理后的对象。

上面的计数器的例子,在组件的setup函数中,创建了一个响应式对象state包含一个count属性。然后创建了一个increment递增的函数,最后将state和increment返回给作用域,这样template里的button按钮就能访问到increment函数绑定到点击的回调,count也能显示在按钮上。我们点击按钮,按钮上的数值就能跟着递增。

下面切入正题,我们就来探究下按钮上count值跟着响应式更新的原理

数据结构

首先列一下主要的一些数据结构,先列在这里,后面提到可以翻回来看看。

ReactiveEffect 一个Function对象,用于执行组件的挂载和更新。

interface ReactiveEffect {
 (): any
 isEffect: true
 active: boolean
 raw: Function // 具体执行的函数
 deps: Array<Dep>
 computed?: boolean
 scheduler?: (run: Function) => void
 onTrack?: (event: DebuggerEvent) => void
 onTrigger?: (event: DebuggerEvent) => void
 onStop?: () => void
}

targetMap 类似 {target -> key -> dep}的一个Map结构,用于缓存所有响应式对象和依赖收集。

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()

Proxy代理拦截

reactive函数执行,会将传入的target对象通过Proxy包装,拦截它的get,set等,并将代理的target缓存到targetMap,targetMap.set(target, new Map())。

代理的get的时候会调用一个track函数,而set会调用一个triger函数。分别对应依赖收集和触发更新。

// Proxy get 简化
function get(target: any, key: string | symbol, receiver: any) {
 // 通过key拿到原始值res
 const res = Reflect.get(target, key, receiver)
 // 过滤不需要代理的情况
 // ...
 // 依赖收集
 track(target, OperationTypes.GET, key)
 // 如果取到的值是个对象,将对象再代理包装一下
 // Proxy只能代理对象第一层级
 return isObject(res) ? reactive(res) : res
}

// Proxy set 简化
function set(
 target: any,
 key: string | symbol,
 value: any,
 receiver: any
): boolean {
 // 一些不需要代理设置的场景
 // ...

 // 设置原始对象的值
 const result = Reflect.set(target, key, value, receiver)
 // 避免重复trigger的逻辑
 // ...
 // 触发通知更新
 trigger(target, '更新的类型, 新增key或更新key', key)
 return result
}

依赖收集和触发更新

组件在render阶段,视图会读取数据对象上的值进行渲染,此时便触发了Proxy的get,由此触发对应的track函数,记录下了对应的ReactiveEffect,也就是常说的依赖收集。

ReactiveEffect其实就可以看作是组件的更新(mount是特殊的update),数据的变更触发trigger,trigger遍历调用track收集的对应的数据的ReactiveEffect,也就是对应有关联的组件的更新。

trigger触发的组件的更新,在render阶段又触发了新一轮的track依赖收集,更新依赖。

// 简化的 track
function track(
 target: any,
 type: OperationTypes,
 key?: string | symbol
) {
 // 只有在依赖收集阶段才进行依赖收集
 // 除了render,其他场景也可能会触发Proxy的get,但不需要进行依赖收集
 // activeReactiveEffectStack栈顶包装了当前render的组件的mount和update的逻辑
 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
 // 如果effect为空,说明当前不在render阶段
 if (effect) {
  // ...
  // =====>初始化对应{target -> key -> dep}的结构
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
   targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key as string | symbol)
  if (!dep) {
   depsMap.set(key as string | symbol, (dep = new Set()))
  }
  // <=====初始化对应{target -> key -> dep}的结构
  // 依赖列表里如果没有,add
  if (!dep.has(effect)) {
   // 这里将effect作为依赖,缓存到依赖列表
   dep.add(effect)
   effect.deps.push(dep)
  }
 }
}

// 简化的trigger
function trigger(
 target: any,
 type: OperationTypes,
 key?: string | symbol,
 extraInfo?: any
) {
 // 获取对应target在track过程中缓存的依赖
 const depsMap = targetMap.get(target)

 const effects: Set<ReactiveEffect> = new Set()
 // 省略分类逻辑
 depsMap.forEach(dep => {
  // 将effect分类过滤添加到effects
 })

 const run = (effect: ReactiveEffect) => {
  // 有个异步调度的过程,nextTick
  scheduleRun(effect, target, type, key, extraInfo)
 }

 effects.forEach(run)
}

大致流程:

总结

现在的代码只有新特性的实现,而且ES6+TS的组合可读性大大提高,编辑器支持也很好,所以相对会好读很多。这里只是简单的理了一下vue 3.0 reactive的整体流程,细节还有很多地方值得学习,继续加油。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈Vue 数据响应式原理

    前言 Vue的数据响应主要是依赖了Object.defineProperty(),那么整个过程是怎么样的呢?以我们自己的想法来走Vue的道路,其实也就是以Vue的原理为终点,我们来逆推一下实现过程. 本文代码皆为低配版本,很多地方都不严谨,比如 if(typeof obj === 'object')这是在判断obj是否为为一个对象,虽然obj也有可能是数组等其他类型的数据,但是本文为了简便,就直接这样写来表示判断对象,对于数组使用Array.isArray(). 改造数据 我们先来尝试写一个函数

  • Vue3.0数据响应式原理详解

    基于Vue3.0发布在GitHub上的第一版源码(2019.10.05)整理 预备知识 ES6 Proxy,整个响应式系统的基础. 新的composition-API的基本使用,目前还没有中文文档,可以先通过这个仓库(composition-api-rfc)了解,里面也有对应的在线文档. 先把Vue3.0跑起来 先把vue-next仓库的代码clone下来,安装依赖然后构建一下,vue的package下的dist目录下找到构建的脚本,引入脚本即可. 下面一个简单计数器的DEMO: <!DOCTY

  • Vue响应式原理详解

    Vue 嘴显著的特性之一便是响应式系统(reactivity system),模型层(model)只是普通JavaScript对象,修改它则更新视图(view). Vue 响应式系统的底层细节 如何追踪变化 把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象的所有属性,并使用Object.defineProperty 把这些属性全部转为 getter/setter.Object.defineProperty是仅ES5支持,并无法shim的特性,这也就是为什么Vu

  • vue数据响应式原理知识点总结

    vue2.0数据响应式原理 对象 Obect.defineproperty 定义对象的属性mjm defineproperty 其实不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,设置一系列操作权限,只不过属性里的get和set实现了响应式 var ob = { a: 1, b: 2 } //1-对象 2-属性 3-对于属性的一系列配置 Object.defineProperty(ob, 'a' , { //a对象则是ob的绝对私有属性,,默认都是true writable: fal

  • vue2.0/3.0的响应式原理及区别浅析

    前言 自从vue3.0正式上线以来,好多小伙伴都转战vue3.0了,这里把我自己总结的3.0和2.0的原理以及他俩的区别写出来,方便自己学习. step 一,vue2.0的响应式原理 先看看官网的解释: 当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty把这些属性全部转为 getter/setter.Object.defineProperty 是 ES5 中一个无法 shim 的特性

  • 详解实现vue的数据响应式原理

    这篇文章主要是给不了解或者没接触过 vue 响应式源码的小伙伴们看的,其主要目的在于能对 vue 的响应式原理有个基本的认识和了解,如果在面试中被问到此类问题,能够知道面试官想让你回答的是什么?「PS:文中如有不对的地方,欢迎小伙伴们指正」 响应式的理解 响应式顾名思义就是数据变化,会引起视图的更新.这篇文章主要分析 vue2.0 中对象和数组响应式原理的实现,依赖收集和视图更新我们留在下一篇文章分析. 在 vue 中,我们所说的响应式数据,一般指的是数组类型和对象类型的数据.vue 内部通过

  • Vue2.X和Vue3.0数据响应原理变化的区别

    defineProperty 定义对象的属性,只不过属性里的get和set实现了响应式. 常用: value属性值 get set writeable 是否可写 enumrable 可遍历 Vue从改变一个数据到发生改变的过程  Vue2.X数据响应原理 创建页面,实现延时2s修改对象的值. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>LearnVue

  • Vue深入讲解数据响应式原理

    目录 响应式是什么 如何实现数据响应式 实现对象属性拦截 通用的劫持方案 总结 响应式是什么 简而言之就是数据变页面变 如何实现数据响应式 在Javascript里实现数据响应式一般有俩种方案,分别对应着vue2.x 和 vue3.x使用的方式,他们分别是: 对象属性拦截 (vue2.x) Object.defineProperty 对象整体代理 (vue3.x) Proxy 其中对象属性拦截,道理都是相通的 实现对象属性拦截 字面量对象定义 let data = { name:'小兰同学' }

  • vue.js数据响应式原理解析

    目录 Object.defineProperty() 定义 defineReactive 函数 递归侦测对象的全部属性 流程分析 observe 函数 Observer 类 完善 defineReactive 函数 One More Thing Object.defineProperty() 得力于 Object.defineProperty() 的特性,vue 的数据变化有别于 react 和小程序,是非侵入式的.详细介绍可以看 MDN 文档,这里特别说明几点: get / set 属性是函数

  • vue3中defineProps传值使用ref响应式失效详解

    子组件接收父组件的传参. 父组件: <template> <Son :data="data"/> </template> <script setup> import { ref } from "vue"; let data = ref('hello') setTimeout(() => { data.value = 'how are you doing' }, 2000) </script> 子组件:

随机推荐