You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
目前vue3还没完全稳定下来,许多rfcs都有变化的可能。本文基于目前最新(2019-11-07)fork的 vue源码进行原理分析。官方提供了在Vue2.x尝试最新Vue3功能的插件库:Vue Composition API (以前该库叫vue-function-api,现在叫composition-api)。
如果较少关注vue3征求意见稿vue rfcs,可能大部分人对vue3还停留在Vue Function API。作者尤雨溪专门为这重大改变的API做过详细的叙述,并特意翻译了中文Vue Function-based API RFC。目前Vue 官方发布了最新的3.0 API 修改草案,并在充分采纳社区的意见后,将Vue Function API 更正为 Vue Composition API.
exportfunctionreactive(target: object){// if trying to observe a readonly proxy, return the readonly version.if(readonlyToRaw.has(target)){returntarget}// target is explicitly marked as readonly by userif(readonlyValues.has(target)){returnreadonly(target)}// 给普通对象创建响应式对象returncreateReactiveObject(target,rawToReactive,reactiveToRaw,mutableHandlers,mutableCollectionHandlers)}
如下面注释解释,大部分代码都是为了做边界和重复处理。最重要的还是创建proxy对象: observed = new Proxy(target, mutableHandlers)。
functioncreateReactiveObject(target: unknown,// 原始对象toProxy: WeakMap<any,any>,// 全局rawToReactivetoRaw: WeakMap<any,any>,// 全局reactiveToRawbaseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>){// 必须是对象if(!isObject(target)){if(__DEV__){console.warn(`value cannot be made reactive: ${String(target)}`)}returntarget}// 重复的对象引用,最终都返回初始的监听对象,这就是创建全局store的原因之一// target already has corresponding Proxyletobserved=toProxy.get(target)if(observed!==void0){returnobserved}// target is already a Proxyif(toRaw.has(target)){returntarget}// vue对象、vnode对象等不能被创建为响应式// only a whitelist of value types can be observed.if(!canObserve(target)){returntarget}// 真正创建代理Proxy对象并返回consthandlers=collectionTypes.has(target.constructor)
? collectionHandlers// [Set, Map, WeakMap, WeakSet]对象走这个handles
: baseHandlers// 大部分走baseHandleobserved=newProxy(target,handlers)// 创建完马上全局缓存toProxy.set(target,observed)toRaw.set(observed,target)if(!targetMap.has(target)){targetMap.set(target,newMap())}returnobserved}
functionset(target: object,key: string|symbol,value: unknown,receiver: object): boolean{value=toRaw(value)constoldValue=(targetasany)[key]if(isRef(oldValue)&&!isRef(value)){oldValue.value=valuereturntrue}// prxoyconsthadKey=hasOwn(target,key)// 是否target本来旧有key属性,等价于:key in targetconstresult=Reflect.set(target,key,value,receiver)// don't trigger if target is something up in the prototype chain of originalif(target===toRaw(receiver)){if(!hadKey){trigger(target,OperationTypes.ADD,key)// 触发新增}elseif(hasChanged(value,oldValue)){trigger(target,OperationTypes.SET,key)// 触发修改}}returnresult}
// The main WeakMap that stores {target -> key -> dep} connections.// Conceptually, it's easier to think of a dependency as a Dep class// which maintains a Set of subscribers, but we simply store them as// raw Sets to reduce memory overhead.exporttypeDep=Set<ReactiveEffect>exporttypeKeyToDepMap=Map<any,Dep>// 原始对象: new Map({key1: new Set([effect1, effect2,...])}, {key2: Set2}, ...)// key是原始对象里的属性, 值为该key改变后会触发的一系列的函数, 比如渲染、computedexportconsttargetMap=newWeakMap<any,KeyToDepMap>()
// set: trigger(target, OperationTypes.SET, key)exportfunctiontrigger(target: object,type: OperationTypes,key?: unknown,extraInfo?: DebuggerEventExtraInfo){constdepsMap=targetMap.get(target)if(depsMap===void0){// never been trackedreturn}// 把拿到的depsMap.get(key),二选一放进effects或computedRunners中。consteffects=newSet<ReactiveEffect>()constcomputedRunners=newSet<ReactiveEffect>()// 根据不同的OperationTypes,把effect=depsMap.get(key)放进runners中if(type===OperationTypes.CLEAR){// collection being cleared, trigger all effects for targetdepsMap.forEach(dep=>{addRunners(effects,computedRunners,dep)})}else{// schedule runs for SET | ADD | DELETEif(key!==void0){addRunners(effects,computedRunners,depsMap.get(key))}// also run for iteration key on ADD | DELETEif(type===OperationTypes.ADD||type===OperationTypes.DELETE){constiterationKey=Array.isArray(target) ? 'length' : ITERATE_KEYaddRunners(effects,computedRunners,depsMap.get(iterationKey))}}// 执行runners,即执行effectsconstrun=(effect: ReactiveEffect)=>{scheduleRun(effect,target,type,key,extraInfo)}// Important: computed effects must be run first so that computed getters// can be invalidated before any normal effects that depend on them are run.computedRunners.forEach(run)effects.forEach(run)}// 添加runner时,二选一functionaddRunners(effects: Set<ReactiveEffect>,computedRunners: Set<ReactiveEffect>,effectsToAdd: Set<ReactiveEffect>|undefined){if(effectsToAdd!==void0){effectsToAdd.forEach(effect=>{if(effect.computed){computedRunners.add(effect)}else{effects.add(effect)}})}}
Vue3 响应式原理 - Ref/Reactive/Effect源码分析
众所周知,Vue3使用ES6 Proxy替代ES5 Object.defineProperty实现数据响应式,这也是Vue最为核心的功能之一。Vue3相比Vue2.x,API变化很大,提出了Vue Composition API。但在响应式原理实现方面,源码依然还是依赖收集 + 执行回调,只不过api变化后,形式也有点变化。想了解vue 2.x实现方式,可以看下笔者以前写的 Vue2.x源码分析 - 响应式原理。
你必须知道的Vue3 RFCS ChangeLog
如果较少关注vue3征求意见稿vue rfcs,可能大部分人对vue3还停留在Vue Function API。作者尤雨溪专门为这重大改变的API做过详细的叙述,并特意翻译了中文Vue Function-based API RFC。目前Vue 官方发布了最新的3.0 API 修改草案,并在充分采纳社区的意见后,将Vue Function API 更正为 Vue Composition API.
1. 重大变化点
2. 了解Vue Composition API
3. 建议阅读资料
源码解析
1. ref
先从入口ref看起,ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive。
ref本质上是把js 基本类型(string/number/bool)包装为引用对象,使得具有响应式特性。
同时ref支持把reactive转为refs对象 - toRefs:
2. reactive
再来看下vue3的响应式reactive源码:
先认识下以下4个全局存储,使用weakmap存储起普通对象和生成的响应式对象,因为很多地方都需要用到判断以及取值。其中rawToReactive和reactiveToRaw是一组,只不过key和value互相对调。
下面是reactive入口,如果传入参数是只读响应式,或者是用户设置的只读类型,返回处理。大部分都会走createReactiveObject方法:
如下面注释解释,大部分代码都是为了做边界和重复处理。最重要的还是创建proxy对象:
observed = new Proxy(target, mutableHandlers)。
所以还是看代理对象mutableHandlers中的处理:
get、has、deleteProperty、ownKeys代理方法中,都调用了track函数,用来收集依赖,这个下文讲;而set调用了trigger函数,当响应式数据变化时,收集的依赖被执行回调。从原理看,这跟vue2.x是一致的。
看下最常用的get、set。get中除常规边界处理外,最重要是根据代理值的类型,对object类型进行递归调用reactive。
set函数里除了代理set方法外,最重要的莫过于当值改变时,触发trigger方法,下文详细讲述该函数。
3. track/trigger
这里是vue3响应式源码的难点。但原理跟vue2.x基本一致,只不过实现方式上有些不同。
track用于收集依赖deps(依赖一般收集effect/computed/watch的回调函数),trigger 用于通知deps,通知依赖这一状态的对象更新。
3.1 举个例子解释
如下代码,使用effect或computed api时,里面使用了count.num,意味着这个effect依赖于count.num。当count.num set改变值时,需要通知该effect去执行。那什么时候count.num收集到effect这个依赖呢?
答案是创建effect时的回调函数。如果回调函数中用到响应式数据(意味着会去执行get函数),则同步这个effect到响应式数据(这里是count.num)的依赖集中。
其流程是(全文重点):1. effect/computed函数执行 -> 2. 代码有书写响应式数据,调用到get,依赖收集 -> 3. 当有set时,依赖集更新。
3.2 对应源码解释
理解了上面这个案例,源码阅读就能顺畅的多。
先挑effect实现过程,再来看依赖收集track函数和执行依赖函数trigger。effect api主要用effect包装了回调函数fn,并默认执行fn回调函数,最终执行run(effect, fn, args)。
再看run函数内容。其实就是执行回调函数时,先对effect入栈,使得当前effectStack有值。这个就非常巧妙,当执行fn回调时,回调函数的代码中又会去访问响应式数据(reactive),这样又会执行响应数据的get方法,get方法又会去执行后文讲的trick方法,trick进行依赖收集。
依赖收集哪些东西呢?就是收集当前的effect回调函数。这个回调函数(被effect包装)不就是刚被存储在effectStack么,所以在后续trick函数中可以看到使用effectStack栈。当执行完回调函数,再进行出栈。
通过使用栈数据结构,以及对代码执行的时机,非常巧妙的就把当前effect传递过去,最终被响应式数据收集到依赖集中。
再来看看依赖收集trick/trigger具体实现细节。
先来看下几个存储变量,主要是依赖收集时用到的:
track函数进行数据依赖采集, 以便于后面数据更改能够触发对应的函数。
trigger,将track收集到的effect函数集合,添加到runners中(二选一放进effects或computedRunners中),并通过scheduleRun执行effect:
总结
响应式数据,就是当数据对象改变时(set),有用到数据对象的地方,都会自动执行响应的逻辑。比如effect/computed/watch等js api用到数据对象,则执行对应的回调函数。而视图view用到数据对象时,则重新vnode diff,最后自动进行dom更新(即视图更新)。
而Vue3响应式源码跟Vue2.x源码流程基本一致,依然是利用在使用响应式数据时,执行数据的get方法,收集相关的依赖(依赖可以是回调函数,如effect/computed,也可以是视图自动更新);在数据进行变化的时候,执行数据的set方法,把收集的依赖都依次执行。
The text was updated successfully, but these errors were encountered: