来自 Web前端 2020-04-07 23:17 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

Vue3数据响应系统

时间: 2019-10-05阅读: 222标签: 响应式

时间: 2019-10-06阅读: 139标签: Vue3

随着 Vue 3.0 Pre Alpha 版本的公布,我们得以一窥其源码的实现。Vue 最巧妙的特性之一是其响应式系统,而我们也能够在仓库的 packages/reactivity 模块下找到对应的实现。虽然源码的代码量不多,网上的分析文章也有一堆,但是要想清晰地理解响应式原理的具体实现过程,还是挺费脑筋的事情。经过一天的研究和整理,我把其响应式系统的原理总结成了一张图,而本文也将围绕这张图去讲述具体的实现过程。

Vue3 就是基于Proxy对其数据响应系统进行了重写,现在这部分可以作为独立的模块配合其他框架使用。数据响应可分为三个阶段:初始化阶段 -- 依赖收集阶段 -- 数据响应阶段

一个基本的例子

Proxy代理须知

Vue 3.0 的响应式系统是独立的模块,可以完全脱离 Vue 而使用,所以我们在 clone 了源码下来以后,可以直接在 packages/reactivity 模块下调试。

用Proxy做代理时,我们需要了解几个问题:

在项目根目录运行yarn dev reactivity,然后进入packages/reactivity目录找到产出的dist/reactivity.global.js文件。

1、Proxy代理是如何对其trap进行处理来实现数据响应的?也就是其get/set里面是如何做拦截处理(其实这里的trap默认行为可以通过Reflect来返回,Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。这里具体可以查看阮大神的ES6入门)

新建一个index.html,写入如下代码:

2、Proxy代理的对象只能代理到第一层,当代理的对象多层嵌套时,那么对象内部的深度监测需要如何去实现?

script src="./dist/reactivity.global.js"/scriptscriptconst { reactive, effect } = VueObserverconst origin = { count: 0}const state = reactive(origin)const fn = () = { const count = state.count console.log(`set count to ${count}`)}effect(fn)/script

3、当代理对象是数组时,比如push操作会触发多次get/set,因为push操作除了增加数组的数据项之外,也会引发数组本身其他相关属性的改变,因此会多次触发get/set,那么要如何解决呢?

在浏览器打开该文件,于控制台执行state.count++,便可看到输出set count to 1。

下面我们会稍微分析下 Vue3 针对这几个问题做了哪些优化处理。

在上述的例子中,我们使用reactive()函数把origin对象转化成了 Proxy 对象state;使用effect()函数把fn()作为响应式回调。当state.count发生变化时,便触发了fn()。接下来我们将以这个例子结合上文的流程图,来讲解这套响应式系统是怎么运行的。

初始化阶段

初始化阶段

初始化过程相对比较简单,通过reactive()方法将数据转化成Proxy对象,这里注意一个比较重要的对象targetMap,它在依赖收集阶段起着比较重要的作用,具体下面会有分析。

在初始化阶段,主要做了两件事。

export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (readonlyToRaw.has(target)) { return target } // target is explicitly marked as readonly by user if (readonlyValues.has(target)) { return readonly(target) } return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers )}...// 创建proxy对象function createReactiveObject( target: any, toProxy: WeakMapany, any, toRaw: WeakMapany, any, baseHandlers: ProxyHandlerany, collectionHandlers: ProxyHandlerany) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // target already has corresponding Proxy let observed = toProxy.get(target) if (observed !== void 0) { return observed } // target is already a Proxy if (toRaw.has(target)) { return target } // only a whitelist of value types can be observed. if (!canObserve(target)) { return target } const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed}

把origin对象转化成响应式的 Proxy 对象state。把函数fn()作为一个响应式的 effect 函数。

Vue3 如何进行深度观测的?先看下面这段代码

首先我们来分析第一件事。

let data = { x: {y: {z: 1 } } }let p = new Proxy(data, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver) console.log('get value:', key) console.log(res) return res }, set(target, key, value, receiver) { console.log('set value:', key, value) return Reflect.set(target, key, value, receiver) }})p.x.y = 2// get value: x// {y: 2}

大家都知道,Vue 3.0 使用了 Proxy 来代替之前的Object.defineProperty(),改写了对象的 getter/setter,完成依赖收集和响应触发。但是在这一阶段中,我们暂时先不管它是如何改写对象的 getter/setter 的,这个在后续的”依赖收集阶段“会详细说明。为了简单起见,我们可以把这部分的内容浓缩成一个只有两行代码的reactive()函数:

上面代码我们可以知道Proxy只会代理一层,因为这里只是触发了一次最外层属性x的get,而重新赋值的其内部属性y,此时set并没有被触发,所以改变内部属性是不会监测到的。继续看,Reflect.get返回的结果正是target的内层结构,此时p.x.y的值也已经变成2了,我们可以判断当前Reflect.get返回的值是否为object,若是则再通过reactive做代理,这样就达到了深度观测的目的了。

export function reactive(target) { const observed = new Proxy(target, handler) return observed}

Vue3实现过程具体我们可以看下面源码:

完整代码在reactive.js。这里的handler就是改造 getter/setter 的关键,我们放到后文讲解。

function createGetter(isReadonly: boolean) { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) if (typeof key === 'symbol'  builtInSymbols.has(key)) { return res } if (isRef(res)) { return res.value } track(target, OperationTypes.GET, key) // 当代理的对象是多层结构时,Reflect.get会返回对象的内层结构,我们可以拿到当前res再做判断是否为object,进而进行reactive,就达到了深度观测的目的了 return isObject(res) ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res }}

接下来我们分析第二件事。

依赖收集阶段

当一个普通的函数fn()被effect()包裹之后,就会变成一个响应式的 effect 函数,而fn()也会被立即执行一次

所谓的依赖在Vue3可简单理解为各种effect响应式函数,其中包括了属性依赖的effect,计算属性computedEffect以及组件视图的componentEffect

由于在fn()里面有引用到 Proxy 对象的属性,所以这一步会触发对象的 getter,从而启动依赖收集。

1、在视图挂载渲染时会执行一个componentEffect,触发相关数据属性getter操作来完成视图依赖收集。

除此之外,这个 effect 函数也会被压入一个名为”activeReactiveEffectStack“(此处为 effectStack)的栈中,供后续依赖收集的时候使用。

2、effect函数执行也会触发相关属性的getter操作,此时操作了某个属性的effect也会被该属性对应进行收集(注意这里的属性是可观测的)。

网上澳门金莎娱乐,来看看代码(完成代码请看effect.js):

之所以说是响应式的,是因为effect方法回调中关联了被观测的数据属性,而effect一般是立即执行的,此时触发了该属性的getter,进行依赖收集,当该属性触发setter时,便会触发执行收集的依赖。另外,这里每次effect执行时,当前的effect会被压入一个名为activeReactiveEffectStack的栈中,是在依赖收集的时候使用。

export function effect (fn) { // 构造一个 effect const effect = function effect(...args) { return run(effect, fn, args) } // 立即执行一次 effect() return effect}export function run(effect, fn, args) { if (effectStack.indexOf(effect) === -1) { try { // 往池子里放入当前 effect effectStack.push(effect) // 立即执行一遍 fn() // fn() 执行过程会完成依赖收集,会用到 effect return fn(...args) } finally { // 完成依赖收集后从池子中扔掉这个 effect effectStack.pop() } }}
export function effect( fn: Function, options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect { if ((fn as ReactiveEffect).isEffect) { fn = (fn as ReactiveEffect).raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { // effect立即执行,触发effect回调函数fn中相关响应数据属性的getter操作,从而进行依赖收集 effect() } return effect}...// 触发getter操作,进行依赖收集export function track( target: any, type: OperationTypes, key?: string | symbol) { if (!shouldTrack) { return } const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (effect) { if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } // 防止依赖重复收集 if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__  effect.onTrack) { effect.onTrack({ effect, target, type, key }) } } }}

至此,初始化阶段已经完成。接下来就是整个系统最关键的一步——依赖收集阶段。

开头说过targetMap对象在依赖收集过程中的重要作用,看源码我们大概知道了,它维护了一个依赖收集的关系表,targetMap是一个WeakMap,其key值是当前被代理的对象target,而value则是该对象所对应的depsMap,它是一个Map,key值为触发getter时的属性值,而value值则是触发过该属性值所对应的各个effect。

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:Vue3数据响应系统

关键词: