如何侦听网线数据_vue3Reactive数据响应原理

vue3已经推出有一段时间了,相信大家都想上手并实践vue3到底带来哪些了新特性和新的设计方式。本篇是vue系列分享的第一篇,将带领大家熟悉vue3中reactive数据响应原理。

搭建vue3 项目环境

首先,我们看下如何搭建一个vue3的开发项目。官方文档建议以下方式:

1、通过 CDN:

2、通过 codepen 的浏览器 playground

3、通过脚手架vite:

npm init vite-app first-vue3

4、通过脚手架vue-ci:

npm install -g @vue/cli

vue create first-vue3

创建测试文件:

响应式计数:{{count}}

@click="addCount">add

import {reactive, toRefs} from 'vue';

export default {

setup() {

const state = reactive({ count: 0 })

const addCount = () =>{

state.count++;

}

return {

...toRefs(state),

addCount

}

}

}

我是通过vue-ci安装,直接使用运行 npm run dev,启动一个开发服务,访问本地 http://172.22.157.24:3000/ 即可查看

02299e8bb80510c962a8cd39284a8bef.png

点击add,可以看到数字在同步递增,那我们通过 reactive({count:0})挂载的count变量是如何响应的呢?我们接着看。

vue2响应式原理

我们先来回顾一下vue2的响应式原理:1、vue2的对象响应是遍历每个key,通过 Object.defineProperty API定义getter,setter

// 伪代码

function observe(){

if(typeof obj !='object' || obj == null){

return

}

if(Array.isArray(obj)){

Object.setPrototypeOf(obj,arrayProto)

}else{

const keys = Object.keys()

for(let i=0;i

const key = keys[i]

defineReactive(obj,key,obj[key])

}

}

}

function defineReactive(target, key, val){

observe(val)

Object.defineProperty(obj, key, {

get(){

// 依赖收集

dep.depend()

return val

},

set(newVal){

if(newVal !== val){

observe(newVal)

val = newVal

// 通知更新

dep.notify()

}

}

})

}

2、vue2数组实现响应是覆盖数组的原型方法,增加通知变更的逻辑

const originalProto = Array.prototype

const arrayProto = Object.create(originalProto)

['push','pop','shift','unshift','splice','reverse','sort'].forEach(key=>{

arrayProto[key] = function(){

originalProto[key].apply(this.arguments)

notifyUpdate()

}

})

vue2在实现响应式的同时也带来了相应的问题:

1、递归,消耗大 2、新增/删除属性,需要额外实现单独的API 3、数组,需要额外实现 4、Map Set Class等数据类型,无法响应式

vue3响应式原理

为解决vue2响应式带来的问题,vue3采取ES6的 Proxy 进行数据响应化,解决痛点。Proxy可以在目标对象上加一层拦截/代理,外界对目标对象的操作,都会经过这层拦截,相比 Object.defineProperty ,Proxy支持的对象操作十分全面:get、set、has、deleteProperty、ownKeys、defineProperty等等。并且在 Vue 3 版本也使用了 Object.defineProperty 来支持 IE 浏览器。两者具有相同的 Surface API,但是 Proxy 版本更精简,同时提升了性能。

// 伪代码

function reactice(obj){

return new Proxy(obj,{

get(target, key, receiver){

const ret = Reflect.get(target, key, receiver)

return isObject(ret) ? reactice(ret) : ret

},

set(target, key, val, receiver){

const ret = Reflect.set(target, key, val, receiver)

return ret

},

deleteProperty(target, key){

const ret = Reflect.deleteProperty(target, key)

return ret

},

})

}

proxy

vue3的响应式原理是依据Proxy实现的,我们先来熟悉下其原理和使用。Proxy 是一个包含另一个对象或函数并允许你对其进行拦截的对象。我们是这样使用它的:newProxy(target,handler)

const person = {

name: 'jack'

}

const handler = {

get(target, prop) {

return target[prop]

}

}

const proxy = new Proxy(person, handler)

console.log(proxy.name)

// jack

可以看到这里我们把对象包装在 Proxy 里的同时可以对其进行拦截。这种拦截被称为陷阱。此外,Proxy 还提供了另一个特性。我们不必像这样返回值:target[prop],而是可以进一步使用一个名为 Reflect 的方法,它允许我们正确地执行 this 绑定,就像这样

const person = {

name: 'jack'

}

const handler = {

get(target, prop) {

return Reflect.get(...arguments)

}

}

const proxy = new Proxy(person, handler)

console.log(proxy.name)

// jack

为了有一个 API 能够在某些内容发生变化时更新最终值,我们必须在内容发生变化时设置新的值。我们在处理器,一个名为 track 的函数中执行此操作,该函数可以传入 target 和 key两个参数。

const person = {

name: 'jack'

}

const handler = {

get(target, prop) {

track(target, prop)

return Reflect.get(...arguments)

},

set(target, key, value, receiver) {

trigger(target, key)

return Reflect.set(...arguments)

}

}

const proxy = new Proxy(person, handler)

console.log(proxy.name)

// jack

所以vue3在处理变化时候做了三件事:

1、当某个值发生变化时进行检测:vue3不再需要这样做,因为 Proxy 允许我们拦截它 2、跟踪更改它的函数:我们在 Proxy 中的 getter 中执行此操作,称为 effect

3、触发函数以便它可以更新最终值:我们在 Proxy 中的 setter 中进行该操作,名为 trigger

侦听器

每个组件实例都有一个相应的侦听器实例,该实例将在组件渲染期间把"touched"的所有 property 记录为依赖项。之后,当触发依赖项的 setter 时,它会通知侦听器,从而使得组件重新渲染。

将对象作为数据传递给组件实例时,Vue 会将其转换为 Proxy。这个 Proxy 使 Vue 能够在 property 被访问或修改时执行依赖项跟踪和更改通知。每个 property 都被视为一个依赖项。

首次渲染后,组件将跟踪一组依赖列表——即在渲染过程中被访问的 property。反过来,组件就成为了其每个 property 的订阅者。当 Proxy 拦截到 set 操作时,该 property 将通知其所有订阅的组件重新渲染。

响应式API

响应式系统中主要包含了 4 个 关键API,分别是:

reactive:响应式关键入口 API,作用同 Vue2 组件的 data 选项

ref:响应式关键入口 API,作用同 reactivity,不过是用于针对基本数据类型的响应式包装,因为基本数据类型在对象结构或者函数参数传递时会丢失引用

computed:响应式计算 API,同 Vue2 的 computed 选项

effect:作用同 Vue2 的 watcher,是用来进行依赖收集的 API,computed 和 后面的 watch API都是基于 effect 的 他们的关系简单如下:

5e9241022e9927f9ec86b6b805afcd23.png

声明响应式状态

要为 JavaScript 对象创建响应式状态,可以使用 reactive 方法:

import { reactive } from 'vue'

// 响应式状态

const state = reactive({

count: 0

})

reactive 相当于 Vue 2.x 中的 Vue.observable() API ,为避免与 RxJS 中的 observables 混淆因此对其重命名。该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响嵌套对象传递的所有 property。

我们可以在渲染期间使用Vue 中reactive state的基本用法。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。

reactive源码解析

入口文件

从官网下载vue-next源码:https://github.com/vuejs/vue-next 解压下载包,打开vue-next/packages/reactivity/src/reactive.ts,首先可以找到reactive函数如下:

export function reactive extends object>(target: T): UnwrapNestedRefs

export function reactive(target: object) {

// if trying to observe a readonly proxy, return the readonly version.

// 如果是readonly对象的代理,那么这个对象是不可观察的,直接返回readonly对象的代理

if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {

return target

}

// 调用createReactiveObject创建reactive对象

return createReactiveObject(

target,

false,

mutableHandlers,

mutableCollectionHandlers

)

}

上面的代码很好理解,调用reactive,首先判断是否是 readonly 对象的判断,然后调用createReactiveObject创建响应式对象。

辅助函数和常量

我们先来看头部的一些声明的辅助函数和常量:

// 定义reactiveMap,readonlyMap

export const reactiveMap = new WeakMap, any>()

export const readonlyMap = new WeakMap, any>()

// 定义枚举类型

export const enum ReactiveFlags {

SKIP = '__v_skip',

IS_REACTIVE = '__v_isReactive',

IS_READONLY = '__v_isReadonly',

RAW = '__v_raw'

}

// 定义目标对象的类型map

function targetTypeMap(rawType: string) {

switch (rawType) {

case 'Object':

case 'Array':

return TargetType.COMMON

case 'Map':

case 'Set':

case 'WeakMap':

case 'WeakSet':

return TargetType.COLLECTION

default:

return TargetType.INVALID

}

}

// 查找返回目标对象的类型

function getTargetType(value: Target) {

return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)

? TargetType.INVALID

: targetTypeMap(toRawType(value))

}

createReactiveObject函数

我们再来看下createReactiveObject函数:createReactiveObject传递的四个参数分别是:目标对象、是否是isReadonly、响应式数据的代理handler(基础handles),一般是Object和Array、响应式集合的代理handler,一般是Set、Map、WeakMap、WeakSet等

function createReactiveObject(

target: Target,

isReadonly: boolean,

baseHandlers: ProxyHandler,

collectionHandlers: ProxyHandler

) {

// 如果不是对象,直接返回,开发环境下会给警告

if (!isObject(target)) {

if (__DEV__) {

console.warn(`value cannot be made reactive: ${String(target)}`)

}

return target

}

// 目标对象本身是一个Proxy,将其返回,ReactiveFlags是用来标识是否是代理对象

if (

target[ReactiveFlags.RAW] &&

!(isReadonly && target[ReactiveFlags.IS_REACTIVE])

) {

return target

}

// 目标对象已经存在一个代理便将其代理,返回existingProxy

const proxyMap = isReadonly ? readonlyMap : reactiveMap

const existingProxy = proxyMap.get(target)

if (existingProxy) {

return existingProxy

}

// 只有在白名单里的(getTargetType)类型值可被观察

const targetType = getTargetType(target)

if (targetType === TargetType.INVALID) {

return target

}

// 创建Proxy

const proxy = new Proxy(

target,

targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers

)

// 设置目标代理

proxyMap.set(target, proxy)

return proxy

}

看了上面的代码,我们知道createReactiveObject用于创建响应式代理对象:

1、首先判断target是否是对象类型,如果不是对象,直接返回,开发环境下会给警告 2、然后判断目标对象是否已经是响应式Proxy,如果是,直接返回响应式Proxy,目标对象已经存在一个代理便将其代理,返回existingProxy。3、然后判断目标对象是否已经是可观察的,如果是,直接返回已创建的响应式Proxy 4、接着创建响应式代理,对于Set、Map、WeakMap、WeakSet的响应式对象handler与Object和Array的响应式对象handler不同,区分判断。5、最后使用WeakMap类型set方法设置更新目标代理。

响应式代理陷阱

Object和Array的代理

再上面创建ReactiveObject函数最后我们可以看到创建proxy的时候, 传入了collectionHandlers和baseHandlers两个处理函数,下面我们分析一下这两个函数做了什么事情

const proxy = new Proxy(

target,

targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers

)

打开vue-next/packages/reactivity/src/baseHandlers.ts,这个handler用于创建Object类型和Array类型的响应式Proxy使用:

export const mutableHandlers: ProxyHandler = {

get: createGetter(false),

set,

deleteProperty,

has,

ownKeys

}

首先来看get陷阱

function createGetter(isReadonly = false, shallow = false) {

return function get(target: Target, key: string | symbol, receiver: object) {

// 根据标识判断target是否是只读对象

if (key === ReactiveFlags.IS_REACTIVE) {

return !isReadonly

} else if (key === ReactiveFlags.IS_READONLY) {

return isReadonly

} else if (

key === ReactiveFlags.RAW &&

receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)

) {

return target

}

// 判断目标对象是否是数组

const targetIsArray = isArray(target)

if (targetIsArray && hasOwn(arrayInstrumentations, key)) {

return Reflect.get(arrayInstrumentations, key, receiver)

}

// 通过Reflect拿到原始的get行为

const res = Reflect.get(target, key, receiver)

// 如果是内置方法,不需要另外进行代理

if (

isSymbol(key)

? builtInSymbols.has(key as symbol)

: key === `__proto__` || key === `__v_isRef`

) {

return res

}

// track用于收集依赖

if (!isReadonly) {

track(target, TrackOpTypes.GET, key)

}

if (shallow) {

return res

}

// 如果是ref对象,代理到ref.value

if (isRef(res)) {

// ref展开不适用于Array和inter类型

const shouldUnwrap = !targetIsArray || !isIntegerKey(key)

return shouldUnwrap ? res.value : res

}

// 判断是嵌套对象,如果是嵌套对象,需要另外处理

// 如果是基本类型,直接返回代理的值

if (isObject(res)) {

// 这里createGetter是创建响应式对象的,传入的isReadonly是false

// 如果是嵌套对象的情况,通过递归调用reactive拿到结果

// 这里用来避免无效值警告和避免循环依赖,另外需要惰性只读访问

return isReadonly ? readonly(res) : reactive(res)

}

return res

}

}

1、get 陷阱首先通过Reflect.get,拿到原始的get行为 2、然后判断如果是内置方法,不需要另外进行代理 3、接着通过track来收集依赖 4、然后判断如果是ref对象,代理到ref.value 5、最后判断拿到的res结果是否是对象类型,如果是对象类型,再次调用reactive(res)来拿到结果,避免循环引用的情况

下面来看set陷阱:

function createSetter(shallow = false) {

return function set(

target: object,

key: string | symbol,

value: unknown,

receiver: object

): boolean {

// 首先拿到原始值oldValue

const oldValue = (target as any)[key]

// 如果原始值是ref对象,新赋值不是ref对象,直接修改ref包装对象的value属性

if (!shallow) {

value = toRaw(value)

if (!isArray(target) && isRef(oldValue) && !isRef(value)) {

oldValue.value = value

return true

}

} else {

// in shallow mode, objects are set as-is regardless of reactive or not

// 在浅模式下,无论反应与否,对象都按原样设置

}

// 标识原始对象里是否有新赋值的这个key

const hadKey =

isArray(target) && isIntegerKey(key)

? Number(key) < target.length

: hasOwn(target, key)

// 通过Reflect拿到原始的set行为

const result = Reflect.set(target, key, value, receiver)

// don't trigger if target is something up in the prototype chain of original

// 操作原型链的数据,不做任何触发监听函数的行为

if (target === toRaw(receiver)) {

// 没有这个key,则是添加属性

// 否则是给原始属性赋值

// trigger 用于通知deps,通知依赖这一状态的对象更新

if (!hadKey) {

trigger(target, TriggerOpTypes.ADD, key, value)

} else if (hasChanged(value, oldValue)) {

trigger(target, TriggerOpTypes.SET, key, value, oldValue)

}

}

return result

}

}

总结一下:

1、set 陷阱首先拿到原始值oldValue 2、然后进行判断,如果原始值是ref对象,新赋值不是ref对象,直接修改ref包装对象的value属性 3、然后通过Reflect拿到原始的set行为,如果原始对象里是否有新赋值的这个key,没有这个key,则是添加属性,否则是给原始属性赋值 4、进行对应的修改和添加属性操作,通过调用trigger通知deps更新,通知依赖这一状态的对象更新

Set、Map、WeakMap、WeakSet的代理

针对Set、Map、WeakMap、WeakSet的代理vue3在collectionHandlers里做了详细的处理,同 Object和Array代理,可以分析 vue-next/packages/reactivity/src/collectionHandlers.ts文件,源码就不再赘述了。

但是值得注意的是会发现源码mutableCollectionHandlers函数下只有一个get陷阱,这是为什么呢?

因为对于Set、Map、WeakMap、WeakSet的内部机制的限制,其修改、删除属性的操作通过set、add、delete等方法来完成,是不能通过Proxy设置set陷阱来监听的,类似于 Vue 2.x 数组的变异方法的实现,通过监听get陷阱里的get、has、add、set、delete、clear、forEach的方法调用,并拦截这个方法调用来实现响应式。

最后

本篇文章回顾了vue2响应式原理并和和vue3响应式原理做了对比,介绍了proxy代理,响应式api基本知识。带领大家分析了reactive源码和实现过程。希望对大家理解和阅读vue3新的特性带来帮助和指导。后续文章将会介绍watch、computed等核心 API,以及将响应式系统流程串联起来,大家尽请期待!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值