聊一聊 Vue3 中响应式原理

聊一聊 Vue3 中响应式原理,第1张

Vuejs 30 "One Piece" 正式发布已经有一段时间了,真可谓是千呼万唤始出来啊!

相比于 Vue2x , Vue30 在新的版本中提供了更好的性能、更小的捆绑包体积、更好的 TypeScript 集成、用于处理大规模用例的新 API 。

在发布之前,尤大大就已经声明了响应式方面将采用 Proxy 对于之前的 ObjectdefineProperty 进行改写。其主要目的就是弥补 ObjectdefineProperty 自身的一些缺陷,例如无法检测到对象属性的新增或者删除,不能监听数组的变化等。

而 Vue3 采用了新的 Proxy 实现数据读取和设置拦截,不仅弥补了之前 Vue2 中 ObjectdefineProperty 的缺陷,同时也带来了性能上的提升。

今天,我们就来盘一盘它,看看 Vue3 中响应式是如何实现的。

The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object MDN

Proxy - 代理,顾名思义,就是在要访问的对象之前增加一个中间层,这样就不直接访问对象,而是通过中间层做一个中转,通过 *** 作代理对象,来实现修改目标对象。

关于 Proxy 的更多的知识,可以参考我之前的一篇文章 —— 初探 Vue30 中的一大亮点——Proxy ! ,这里我就不在赘述。

Vue3 中响应式核心方法就是 reactive 和 effect , 其中 reactive 方法是负责将数据变成响应式, effect 方法的作用是根据数据变化去更新视图或调用函数,与 react 中的 useEffect 有点类似~

其大概用法如下:

默认会执行一次,打印 Hello , 之后更改了 dataname 的值后,会在触发执行一次,打印 World 。

我们先看看 reactive 方法的实现~

reactivejs

首先应该明确,我们应该导出一个 reactive 方法,该方法有一个参数 target ,目的就是将 target 变成响应式对象,因此返回值就是一个响应式对象。

reactive 方法基本结构就是如此,给定一个对象,返回一个响应式对象。

其中 isObject 方法用于判断是否是对象,不是对象不需要代理,直接返回即可。

reactive 方法的重点是 Proxy 的第二个参数 handler ,它承载监控对象变化,依赖收集,视图更新等各项重大责任,我们重点来研究这个对象。

handlerjs

在 Vue3 中 Proxy 的 handler 主要设置了 get , set , deleteProperty , has , ownKeys 这些属性,即拦截了对象的读取,设置,删除, in 以及 ObjectgetOwnPropertyNames 方法和 ObjectgetOwnPropertySymbols 方法。

这里我们偷个懒,暂时就考虑 set 和 get *** 作。

handlerget()

get 获取属性比较简单,我们先来看看这个,这里我们用一个方法创建 getHanlder 。

这里推荐使用了 Reflectget 而并非 target[key] 。

可以发现, Vue3 是在取值的时候才去递归遍历属性的,而非 Vue2 中一开始就递归 data 给每个属性添加 Watcher ,这也是 Vue3 性能提升之一。

handlerset()

同理 set *** 作,我们也是用一个方法创建 setHandler 。

Reflectset 会返回一个 Boolean 值,用于判断属性是否设置成功。

完事后将 handler 导出,然后在 reactive 中引入即可。

测试几组对象貌似没啥问题,其实是有一个坑,这个坑也跟数组有关。

如上例子,如果我们选择代理数组,在 setHandler 中打印其 key 和 value 的话会得到 3 4 , length 4 这两组值:

如果不作处理,那么会导致如果更新视图的话,则会触发两次,这肯定是不允许的,因此,我们需要将区分新增和修改这两种 *** 作。

Vue3 中是通过判断 target 是否存在该属性来区分是新增还是修改 *** 作,需要借助一个工具方法 —— hasOwnProperty 。

这里我们将上述的 createSetter 方法修改如下:

如此一来,我们调 push 方法的时候,就只会触发一次更新了,非常巧妙的避免了无意义的更新 *** 作。

effectjs

光上述构造响应式对象并不能完成响应式的 *** 作,我们还需要一个非常重要的方法 effect ,它会在初始化执行的时候存储跟其有关的数据依赖,当依赖数据发生变化的时候,则会再次触发 effect 传递的函数。

其基本雏形如下,入参是一个函数,还有个可选参数 options 方便后面计算属性等使用,暂时不考虑:

createReactiveEffect 就是为了将 fn 变成响应式函数,监控数据变化,执行 fn 函数,因此该函数是一个高阶函数。

createReactiveEffect 将原来的 fn 转变成一个 reactvieEffect , 并将当前的 effect 挂到全局的 activeEffect 上,目的是为了一会与当前所依赖的属性做好对应关系。

我们必须要将依赖属性构造成 { prop : [effect,effect] } 这种结构,才能保证依赖属性变化的时候,依次去触发与之相关的 effect ,因此,需要在 get 属性的时候,做属性的依赖收集,将属性与 effect 关联起来。

依赖收集 —— track

在获取对象的属性时,会触发 getHandler ,再次做属性的依赖收集,即 Vue2 中的发布订阅。

在 setHandler 中获取属性的时候,做一次 track(target, key) *** 作。

整个 track 的数据结构大概是这样

目的就是将 target , key , effect 之间做好对应的关系映射。

打印 targetMap 的结构如下:

触发更新 —— trigger

上述已经完成了依赖收集,剩下就是监控数据变化,触发更新 *** 作,即在 setHandler 中添加 trigger 触发 *** 作。

这样一来,获取数据的时候通过 track 进行依赖收集,更新数据的时候再通过 trigger 进行更新,就完成了整个数据的响应式 *** 作。

再回头看看我们先前提到的例子:

控制台会依次打印 Hello effect 以及 World effect , 分别是首次渲染触发跟更新数据重渲染触发,至此功能实现!

整体来说, Vue3 相比于 Vue2 在很多方面都做了调整,数据的响应式只是冰山一角,但是可以看出尤大团队非常巧妙的利用了 Proxy 的特点以及 es6 的数据结构和方法。另外, Composition API 的模式跟 React 在某些程度上有异曲同工之妙,这种设计模式让我们在实际开发使用中更加的方法快捷,值得我们去学习,加油!

最后附上仓库地址 github ,欢迎各位大佬批评斧正~

您好,vue3,经过数组重组,vant3页面拿不到值,vue3 使用proxy,对于对象和数组都不能直接。 具体原因:reactive声明的响应式对象被 arr 代理, *** 作代理对象需要有代理对象的前缀

返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文。

创建组件

或者是一个 setup 函数,函数名称将作为组件名称来使用

创建一个异步加载组件

// 高阶组件

在当前应用中查找组件, 只能在 render 或 setup 函数中使用。

查找组件

在当前应用中查找指令, 只能在 render 或 setup 函数中使用。

将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。

vue2x的生命周期

vue3的生命周期

与 2x 版本生命周期相对应的组合式 API

新增的钩子函数

组合式 API 还提供了以下调试钩子函数:

把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref

应用: 当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用

问题: reactive 对象取出的所有属性值都是非响应式的

解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性

利用ref函数获取组件中的标签元素

功能需求: 让输入框自动获取焦点

一般来说, ref 被用来定义简单的字符串或者数值,而reactive被用来定义对象数组等
ref 定义对象时, value 返回的是 proxy , reactive 定义对象时返回的也是 proxy ,而这确实存在一些联系
ref 来定义数据时,会对里面的数据类型进行一层判断,当遇到复杂的引用类型时,还是会使用 reactive 来进行处理
那么能否使用 reactive 来定义普通类型?答案是不行会出现警告,因此 reactive 只能被用来定义对象

如果非要用 reactive 定义基本数据类型的话,我们需要在 reactive 中将数据包装一下

但是使用 reactive 定义的数据和ref定义的数据打印结果有一些差异
注意 ref 定义的数据需要用 value 获取,但 template 模板中不需要直接写入 return 出去的值即可

Vue3提供了一个新的API: toRef/toRefs ,它可以将一个响应型对象( reactive object ) 转化为普通对象( plain object ),同时又把该对象中的每一个属性转化成对应的响应式属性( ref )。说白了就是放弃该对象( Object )本身的响应式特性( reactivity ),转而给对象里的属性赋予响应式特性( reactivity )。
其实这个不是和 ref 一个意思吗
setup 的执行时组件对象还没有创建,此时不能使用this来访问 setup 里的数据或者 *** 作router和vuex等
我们可以通过 getCurrentInstance 这个函数来返回当前组件的实例对象,也就是当前 Vue 这个实例对象,从而去 *** 作一些数据与方法, getCurrentInstance 函数里的 ctx /proxy 就相当于 this

这篇笔记用于自己复习,知识点很散,不适合别人看

应用实例 const app = VuecreateApp(RootOption)

根组件选项 const RootOption = {}
根组件实例 vm = appmount('#app')

组件选项:与根组件的配置选项相同
组件实例:每个组件都有一个组件实例,vm 通常代表组件实例

将 provide/inject 看做是长距离的 props
默认情况下,provide/inject 绑定并不是响应式的

单向下行绑定,父组件中更新 props 属性值,则子组件中的值自动更新,然后自动更新视图
不要在子组件中更改 props 属性值
prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

data 是个函数,返回一个对象,该对象作为 vm datacount 一样,可读写
vmfoo 动态添加的属性不具备响应式,该属性也不会保存在 vm datafoo undefined
相同组件的多个实例拥有各自不同的 $data 属性,类似对象的实例属性

依赖的 data 发生更新,则 computed 自动更新,对应的视图自动更新

computed vs watch

这两个都可以根据一个属性的变化来改变另一个属性,大多数需要用到 computed,因为他可以缓存。
只有在某个属性改变之后需要进行异步 *** 作或者开销较大的复杂 *** 作的时候,需要使用 watch

可以将 methods 方法作为事件的处理函数 v-on:click="increment"
可以从模板中直接调用 methods 中定义的方法 v-bind:title="getTitle()" {{ getMessage() }}
从模板中调用方法,如果方法访问了任何响应式数据,则将其作为渲染依赖项进行跟踪
从模板调用的方法不应该有任何副作用,比如更改数据或触发异步进程
methods 中的方法被相同组件的多个实例共享,类似对象的原型方法

beforeCreated:已经通过 vm$on() 为组件监听了所有事件,包括生命周期钩子
created:可以访问组件的所有注入属性以及响应式属性
beforeMounted: 已经将组件的模板字符串编译成了 render() 函数。
mounted: 组件的模板已经生成 DOM 并且插入到文档中了。
beforeUpdated: 响应式 data 已经发生了变化
updated: 组件的 DOM 视图已经发生了改变
beforeUnmounted: 已经调用了 vmunmount()
unmounted: 组件 DOM 结构已经从文档中删除,组件响应式数据已经删除,组件事件监听器已经删除

不管是哪种属性,都能够在该组件作用域的模板中直接访问

vmcount

vm attr
vm on

vm 实例创建之后动态添加的属性不能够响应式

vmfoo = 100
vm$datafoo //undefined

appcomponent('my-component', {})

{
component: {
'my-component': {}
}
}

自定义特性
class
style
事件

都会传递到子组件的根元素上,class style 与根元素的原有 class style 合并到一起。
如果子组件根元素不支持该事件,则无事发生 elementaddEventListener('no-suposed-event', function () {}),则不会触发任何监听器。

通过 inheritAttrs: false 配合 v-bind=" attrs 包括 html特性、class、style、事件,与 vue2 不同,vue3 listens 属性了

emits: ['myEvent', 'click']

当在 emits 选项中定义了原生事件 (如 click) 时,将使用组件中的自定义事件替代原生事件侦听器。
凡是在子组件中 $emit() 的事件,最好在 emits 属性定义该事件

props属性验证失败,在开发模式下,只是在浏览器控制台警告,不影响程序执行
自定义事件的验证如果失败,在开发模式下,只是在浏览器控制台警告,不影响程序执行,不影响发送事件以及处理事件

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件

<MyComponent v-model="user"></MyComponent>
<MyComponent :modelValue="user" @update:modelValue="user=$event"></MyComponent>

可以给 v-model 添加参数,来改变默认值,使用 person 作为 prop,使用 update:person 作为事件

<MyComponent v-model:person="user"></MyComponent>
<MyComponent :person="user" @update:person="user=$event"></MyComponent>

<input ref="foo">
<my-component ref="bar"></my-component>

this refsbar 指向子组件实例

router-link 和 router-view 可以放在任何组件的任何位置,不是非要放在根组件里面,这两个甚至可以分开放在不同组件里。

router-link 本质上就是渲染 a 标签,放在哪个组件里都可以
router-view 就是渲染与 path 对应的组件,在哪渲染都可以

routes 是一个数组,每个元素是一个路由记录 RouteRecord 对象,定义路由与组件的映射关系

router 实例,定义了路由的方法

router 同时还是个插件,appuse(router) 可以插入全局实例属性 this router

vue-router 最核心的工作就是编写 routes 映射关系

URL:

顶级 <router-view> 匹配顶层 path 渲染,
组件 User 中的 <router-view> 匹配 User Route 的 children 中的 path 渲染

一个路由映射一组组件 { path: '', components: {} }

为一个匹配路径提供多个 <router-view>,来渲染多个组件,通过 <router-view name=""> name 属性来指定对应的 <router-view> 应该渲染哪个组件

如果路由有参数,绝对路径的别名必须带着参数

{{ username }} 取代 {{ $routeparamsusername }}
{ path: '/user/:username', component: User, props: true }

props 为 true,routeparams 将被设置为组件 User 的 props
props 可以是对象 { title: 'hello', id: 20 },传递静态 props 给组件 User

{ path: '', component: {}, meta: { foo: 100, bar: 'hello' } }

tometa
tomatchedsome(record => recordmetafoo === 100)

导航完成后,在组件的 created 钩子函数中从服务器获取数据
导航完成前,在组件的 beforeRouteEnter 导航守卫中从服务器获取数据

<Transition> 是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。
<Transition> 仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。
在位于 <Transition></Transition> 之间的单个元素或组件进入或者离开 DOM 时候,会为该元素或组件应用动画

进入或离开可以由以下的条件之一触发:

当一个 <Transition> 组件中的元素被插入或移除时,会发生下面这些事情:

v-enter-from
v-enter-active
v-enter-to

v-leave-from
v-leave-active
v-leave-to

<Transition name="fade"></Transition>

fade-enter-actvie
fade-leave-active


可以配合 Animatecss 使用

v-enter-active {
transition: all 03s ease-out;
}
v-leave-active {
transition: all 08s cubic-bezier(1, 05, 08, 1);
}

v-enter-from,
v-leave-to {
transform: translateX(20px);
opacity: 0;
}

v-enter-active {
animation: bounce-in 05s;
}
v-leave-active {
animation: bounce-in 05s reverse;
}

@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(125);
}
100% {
transform: scale(1);
}
}

<Transition> 组件会通过监听过渡根元素上的第一个 transitionend 或者 animationend 事件来尝试自动判断过渡何时结束。如果根元素上同时使用了 css 过渡和 css 动画,则通过 type 指定监听哪个事件。duration 属性可以显式指定过渡的持续时间 (以毫秒为单位),而不是通过监听 transitionend 或者 animationend 来决定过渡何时结束。

多用 transform 和 opacity,不要使用 margin height 这类会影响 CSS 布局导致 DOM 重绘的属性。

对 <Transition> 的 prop 的值进行动态绑定 <Transition :name="transitionName" :duration="transitionDuration">,根据当前组件的 data 的值来决定过渡行为

<TransitionGroup> 是一个内置组件,设计用于呈现一个列表中的元素或组件的插入、移除和顺序改变的动画效果。

v-move {
transition: all 05s ease;
}


欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/zz/13147445.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-06-11
下一篇 2023-06-11

发表评论

登录后才能评论

评论列表(0条)

保存