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/
即可查看
点击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 的 他们的关系简单如下:
声明响应式状态
要为 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,以及将响应式系统流程串联起来,大家尽请期待!