Proxy 对象用于定义基本操做的自定义行为(如属性查找、赋值、枚举、函数调用等)。html
proxy是es6新特性,为了对目标的做用主要是经过handler对象中的拦截方法拦截目标对象target的某些行为(如属性查找、赋值、枚举、函数调用等)。vue
/* target: 目标对象,待要使用 Proxy 包装的目标对象(能够是任何类型的对象,包括原生数组,函数,甚至另外一个代理)。 */ /* handler: 一个一般以函数做为属性的对象,各属性中的函数分别定义了在执行各类操做时代理 proxy 的行为。 */ const proxy = new Proxy(target, handler);
** 3.0 将带来一个基于 Proxy 的 observer 实现,它能够提供覆盖语言 (JavaScript——译注) 全范围的响应式能力,消除了当前 Vue 2 系列中基于 Object.defineProperty 所存在的一些局限,这些局限包括:1 对属性的添加、删除动做的监测; 2 对数组基于下标的修改、对于 .length 修改的监测; 3 对 Map、Set、WeakMap 和 WeakSet 的支持;;
vue2.0 用 Object.defineProperty 做为响应式原理的实现,可是会有它的局限性,好比 没法监听数组基于下标的修改,不支持 Map、Set、WeakMap 和 WeakSet等缺陷 ,因此改用了proxy解决了这些问题,这也意味着vue3.0将放弃对低版本浏览器的兼容(兼容版本ie11以上)。node
vue3.0 响应式用到的捕获器(接下来会重点介绍)react
handler.has() -> in 操做符 的捕捉器。 (vue3.0 用到)
handler.get() -> 属性读取 操做的捕捉器。 (vue3.0 用到)
handler.set() -> 属性设置* 操做的捕捉器。 (vue3.0 用到)
handler.deleteProperty() -> delete 操做符 的捕捉器。(vue3.0 用到)
handler.ownKeys() -> Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。(vue3.0 用到)es6
vue3.0 响应式没用到的捕获器(有兴趣的同窗能够研究一下)api
handler.getPrototypeOf() -> Object.getPrototypeOf 方法的捕捉器。
handler.setPrototypeOf() -> Object.setPrototypeOf 方法的捕捉器。
handler.isExtensible() -> Object.isExtensible 方法的捕捉器。
handler.preventExtensions() -> Object.preventExtensions 方法的捕捉器。
handler.getOwnPropertyDescriptor() -> Object.getOwnPropertyDescriptor 方法的捕捉器。
handler.defineProperty() -> Object.defineProperty 方法的捕捉器。
handler.apply() -> 函数调用操做 的捕捉器。
handler.construct() -> new 操做符 的捕捉器。数组
has(target, propKey)浏览器
target:目标对象微信
propKey:待拦截属性名app
做用: 拦截判断target对象是否含有属性propKey的操做
拦截操做: propKey in proxy; 不包含for...in循环
对应Reflect: Reflect.has(target, propKey)
🌰例子:
const handler = { has(target, propKey){ /* * 作你的操做 */ return propKey in target } } const proxy = new Proxy(target, handler)
get(target, propKey, receiver)
target:目标对象
propKey:待拦截属性名
receiver: proxy实例
返回: 返回读取的属性
做用:拦截对象属性的读取
拦截操做:proxy[propKey]或者点运算符
对应Reflect: Reflect.get(target, propertyKey[, receiver])
🌰例子:
const handler = { get: function(obj, prop) { return prop in obj ? obj[prop] : '没有此水果'; } } const foot = new Proxy({}, handler) foot.apple = '苹果' foot.banana = '香蕉'; console.log(foot.apple, foot.banana); /* 苹果 香蕉 */ console.log('pig' in foot, foot.pig); /* false 没有此水果 */
特殊状况
const person = {}; Object.defineProperty(person, 'age', { value: 18, writable: false, configurable: false }) const proxPerson = new Proxy(person, { get(target,propKey) { return 20 //应该return 18;不能返回其余值,不然报错 } }) console.log( proxPerson.age ) /* 会报错 */
set(target,propKey, value,receiver)
target:目标对象
propKey:待拦截属性名
value:新设置的属性值
receiver: proxy实例
返回:严格模式下返回true操做成功;不然失败,报错
做用: 拦截对象的属性赋值操做
拦截操做: proxy[propkey] = value
对应Reflect: Reflect.set(obj, prop, value, receiver)
let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { /* 若是年龄不是整数 */ throw new TypeError('The age is not an integer') } if (value > 200) { /* 超出正常的年龄范围 */ throw new RangeError('The age seems invalid') } } obj[prop] = value // 表示成功 return true } } let person = new Proxy({}, validator) person.age = 100 console.log(person.age) // 100 person.age = 'young' // 抛出异常: Uncaught TypeError: The age is not an integer person.age = 300 // 抛出异常: Uncaught RangeError: The age seems invalid
当对象的属性writable为false时,该属性不能在拦截器中被修改
const person = {}; Object.defineProperty(person, 'age', { value: 18, writable: false, configurable: true, }); const handler = { set: function(obj, prop, value, receiver) { return Reflect.set(...arguments); }, }; const proxy = new Proxy(person, handler); proxy.age = 20; console.log(person) // {age: 18} 说明修改失败
deleteProperty(target, propKey)
target:目标对象
propKey:待拦截属性名
返回:严格模式下只有返回true, 不然报错
做用: 拦截删除target对象的propKey属性的操做
拦截操做: delete proxy[propKey]
对应Reflect: Reflect.delete(obj, prop)
var foot = { apple: '苹果' , banana:'香蕉' } var proxy = new Proxy(foot, { deleteProperty(target, prop) { console.log('当前删除水果 :',target[prop]) return delete target[prop] } }); delete proxy.apple console.log(foot) /* 运行结果: '当前删除水果 : 苹果' { banana:'香蕉' } */
特殊状况: 属性是不可配置属性时,不能删除
var foot = { apple: '苹果' } Object.defineProperty(foot, 'banana', { value: '香蕉', configurable: false }) var proxy = new Proxy(foot, { deleteProperty(target, prop) { return delete target[prop]; } }) delete proxy.banana /* 没有效果 */ console.log(foot)
ownKeys(target)
target:目标对象
返回: 数组(数组元素必须是字符或者Symbol,其余类型报错)
做用: 拦截获取键值的操做
拦截操做:
1 Object.getOwnPropertyNames(proxy)
2 Object.getOwnPropertySymbols(proxy)
3 Object.keys(proxy)
4 for...in...循环
对应Reflect:Reflect.ownKeys()
var obj = { a: 10, [Symbol.for('foo')]: 2 }; Object.defineProperty(obj, 'c', { value: 3, enumerable: false }) var p = new Proxy(obj, { ownKeys(target) { return [...Reflect.ownKeys(target), 'b', Symbol.for('bar')] } }) const keys = Object.keys(p) // ['a'] // 自动过滤掉Symbol/非自身/不可遍历的属性 /* 和Object.keys()过滤性质同样,只返回target自己的可遍历属性 */ for(let prop in p) { console.log('prop-',prop) /* prop-a */ } /* 只返回拦截器返回的非Symbol的属性,无论是否是target上的属性 */ const ownNames = Object.getOwnPropertyNames(p) /* ['a', 'c', 'b'] */ /* 只返回拦截器返回的Symbol的属性,无论是否是target上的属性*/ const ownSymbols = Object.getOwnPropertySymbols(p)// [Symbol(foo), Symbol(bar)] /*返回拦截器返回的全部值*/ const ownKeys = Reflect.ownKeys(p) // ['a','c',Symbol(foo),'b',Symbol(bar)]
vue3.0 创建响应式的方法有两种:
第一个就是运用composition-api中的reactive直接构建响应式,composition-api的出现咱们能够在.vue文件中,直接用setup()函数来处理以前的大部分逻辑,也就是说咱们没有必要在 export default{ } 中在声明生命周期 , data(){} 函数,watch{} , computed{} 等 ,取而代之的是咱们在setup函数中,用vue3.0 reactive watch 生命周期api来到达一样的效果,这样就像react-hooks同样提高代码的复用率,逻辑性更强。
第二个就是用传统的 data(){ return{} } 形式 ,vue3.0没有放弃对vue2.0写法的支持,而是对vue2.0的写法是彻底兼容的,提供了applyOptions 来处理options形式的vue组件。可是options里面的data , watch , computed等处理逻辑,仍是用了composition-api中的API对应处理。
Reactive 至关于当前的 Vue.observable () API,通过reactive处理后的函数能变成响应式的数据,相似于option api里面的vue处理data函数的返回值。
咱们用一个todoList的demo试着尝尝鲜。
const { reactive , onMounted } = Vue setup(){ const state = reactive({ count:0, todoList:[] }) /* 生命周期mounted */ onMounted(() => { console.log('mounted') }) /* 增长count数量 */ function add(){ state.count++ } /* 减小count数量 */ function del(){ state.count-- } /* 添加代办事项 */ function addTodo(id,title,content){ state.todoList.push({ id, title, content, done:false }) } /* 完成代办事项 */ function complete(id){ for(let i = 0; i< state.todoList.length; i++){ const currentTodo = state.todoList[i] if(id === currentTodo.id){ state.todoList[i] = { ...currentTodo, done:true } break } } } return { state, add, del, addTodo, complete } }
options形式的和vue2.0并无什么区别
export default { data(){ return{ count:0, todoList:[] } }, mounted(){ console.log('mounted') } methods:{ add(){ this.count++ }, del(){ this.count-- }, addTodo(id,title,content){ this.todoList.push({ id, title, content, done:false }) }, complete(id){ for(let i = 0; i< this.todoList.length; i++){ const currentTodo = this.todoList[i] if(id === currentTodo.id){ this.todoList[i] = { ...currentTodo, done:true } break } } } } }
vue3.0能够根据业务需求引进不一样的API方法。这里须要
创建响应式reactive,返回proxy对象,这个reactive能够深层次递归,也就是若是发现展开的属性值是引用类型的并且被引用,还会用reactive递归处理。并且属性是能够被修改的。
创建响应式shallowReactive,返回proxy对象。和reactive的区别是只创建一层的响应式,也就是说若是发现展开属性是引用类型也不会递归。
返回的proxy处理的对象,能够展开递归处理,可是属性是只读的,不能修改。能够作props传递给子组件使用。
返回通过处理的proxy对象,可是创建响应式属性是只读的,不展开引用也不递归转换,能够这用于为有状态组件建立props代理对象。
上文中咱们说起到。用Reactive处理过并返回的对象是一个proxy对象,假设存在不少组件,或者在一个组件中被屡次reactive,就会有不少对proxy对象和它代理的原对象。为了能把proxy对象和原对象创建关系,vue3.0采用了WeakMap去储存这些对象关系。WeakMaps 保持了对键名所引用的对象的弱引用,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其余引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦再也不须要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
const rawToReactive = new WeakMap<any, any>() const reactiveToRaw = new WeakMap<any, any>() const rawToReadonly = new WeakMap<any, any>() /* 只读的 */ const readonlyToRaw = new WeakMap<any, any>() /* 只读的 */
vue3.0 用readonly来设置被拦截器拦截的对象可否被修改,能够知足以前的props不能被修改的单向数据流场景。
咱们接下来重点讲一下接下来的四个weakMap的储存关系。
rawToReactive
键值对 : { [targetObject] : obseved }
target(键):目标对象值(这里能够理解为reactive的第一个参数。)
obsered(值):通过proxy代理以后的proxy对象。
reactiveToRaw
reactiveToRaw 储存的恰好与 rawToReactive的键值对是相反的。
键值对 { [obseved] : targetObject }
rawToReadonly
键值对 : { [target] : obseved }
target(键):目标对象。
obsered(值):通过proxy代理以后的只读属性的proxy对象。
readonlyToRaw
储存状态与rawToReadonly恰好相反。
接下来咱们重点从reactive开始讲。
/* TODO: */ export function reactive(target: object) { if (readonlyToRaw.has(target)) { return target } return createReactiveObject( target, /* 目标对象 */ rawToReactive, /* { [targetObject] : obseved } */ reactiveToRaw, /* { [obseved] : targetObject } */ mutableHandlers, /* 处理 基本数据类型 和 引用数据类型 */ mutableCollectionHandlers /* 用于处理 Set, Map, WeakMap, WeakSet 类型 */ ) }
reactive函数的做用就是经过createReactiveObject方法产生一个proxy,并且针对不一样的数据类型给定了不一样的处理方法。
以前说到的createReactiveObject,咱们接下来看看createReactiveObject发生了什么。
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet]) function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { /* 判断目标对象是否被effect */ /* observed 为通过 new Proxy代理的函数 */ let observed = toProxy.get(target) /* { [target] : obseved } */ if (observed !== void 0) { /* 若是目标对象已经被响应式处理,那么直接返回proxy的observed对象 */ return observed } if (toRaw.has(target)) { /* { [observed] : target } */ return target } /* 若是目标对象是 Set, Map, WeakMap, WeakSet 类型,那么 hander函数是 collectionHandlers 否侧目标函数是baseHandlers */ const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers /* TODO: 建立响应式对象 */ observed = new Proxy(target, handlers) /* target 和 observed 创建关联 */ toProxy.set(target, observed) toRaw.set(observed, target) /* 返回observed对象 */ return observed }
经过上面源码建立proxy对象的大体流程是这样的:
①首先判断目标对象有没有被proxy响应式代理过,若是是那么直接返回对象。
②而后经过判断目标对象是不是[ Set, Map, WeakMap, WeakSet ]数据类型来选择是用collectionHandlers , 仍是baseHandlers->就是reactive传进来的mutableHandlers做为proxy的hander对象。
③最后经过真正使用new proxy来建立一个observed ,而后经过rawToReactive reactiveToRaw 保存 target和observed键值对。
大体流程图:
以前咱们介绍过baseHandlers就是调用reactive方法createReactiveObject传进来的mutableHandlers对象。
咱们先来看一下mutableHandlers对象
mutableHandlers
export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys }
vue3.0 用到了以上几个拦截器,咱们在上节已经介绍了这几个拦截器的基本用法,首先咱们对几个基本用到的拦截器在作一下回顾。
①get,对数据的读取属性进行拦截,包括 target.点语法 和 target[]
②set,对数据的存入属性进行拦截 。
③deleteProperty delete操做符进行拦截。
vue2.0不能对对象的delete操做符进行属性拦截。
例子🌰:
delete object.a
是没法监测到的。
vue3.0proxy中deleteProperty 能够拦截 delete 操做符,这就表述vue3.0响应式能够监听到属性的删除操做。
④has,对 in 操做符进行属性拦截。
vue2.0不能对对象的in操做符进行属性拦截。
例子
a in object
has 是为了解决如上问题。这就表示了vue3.0能够对 in 操做符 进行拦截。
⑤ownKeys Object.keys(proxy) ,for...in...循环 Object.getOwnPropertySymbols(proxy) , Object.getOwnPropertyNames(proxy) 拦截器
例子
Object.keys(object)
说明vue3.0能够对以上这些方法进行拦截。
若是咱们想要弄明白整个响应式原理。那么组件初始化,到初始化过程当中composition-api的reactive处理data,以及编译阶段对data属性进行依赖收集是分不开的。vue3.0提供了一套从初始化,到render过程当中依赖收集,到组件更新,到组件销毁完整响应式体系,咱们很难从一个角度把东西讲明白,因此在正式讲拦截器对象如何收集依赖,派发更新以前,咱们看看effect作了些什么操做。
vue3.0用effect反作用钩子来代替vue2.0watcher。咱们都知道在vue2.0中,有渲染watcher专门负责数据变化后的重新渲染视图。vue3.0改用effect来代替watcher达到一样的效果。
咱们先简单介绍一下mountComponent流程,后面的文章会详细介绍mount阶段的
// 初始化组件 const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { /* 第一步: 建立component 实例 */ const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )) /* 第二步 : TODO:初始化 初始化组件,创建proxy , 根据字符窜模版获得 */ setupComponent(instance) /* 第三步:创建一个渲染effect,执行effect */ setupRenderEffect( instance, // 组件实例 initialVNode, //vnode container, // 容器元素 anchor, parentSuspense, isSVG, optimized ) }
上面是整个mountComponent的主要分为了三步,咱们这里分别介绍一下每一个步骤干了什么:
① 第一步: 建立component 实例 。
② 第二步:初始化组件,创建proxy ,根据字符窜模版获得render函数。生命周期钩子函数处理等等
③ 第三步:创建一个渲染effect,执行effect。
从如上方法中咱们能够看到,在setupComponent已经构建了响应式对象,可是尚未初始化收集依赖。
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { /* 建立一个渲染 effect */ instance.update = effect(function componentEffect() { //...省去的内容后面会讲到 },{ scheduler: queueJob }) }
为了让你们更清楚的明白响应式原理,我这只保留了和响应式原理有关系的部分代码。
setupRenderEffect的做用
① 建立一个effect,并把它赋值给组件实例的update方法,做为渲染更新视图用。
② componentEffect做为回调函数形式传递给effect做为第一个参数
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { const effect = createReactiveEffect(fn, options) /* 若是不是懒加载 当即执行 effect函数 */ if (!options.lazy) { effect() } return effect }
effect做用以下
① 首先调用。createReactiveEffect
② 若是不是懒加载 当即执行 由createReactiveEffect建立出来的ReactiveEffect函数
function createReactiveEffect<T = any>( fn: (...args: any[]) => T, /**回调函数 */ options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(...args: unknown[]): unknown { try { enableTracking() effectStack.push(effect) //往effect数组中里放入当前 effect activeEffect = effect //TODO: effect 赋值给当前的 activeEffect return fn(...args) //TODO: fn 为effect传进来 componentEffect } finally { effectStack.pop() //完成依赖收集后从effect数组删掉这个 effect resetTracking() /* 将activeEffect还原到以前的effect */ activeEffect = effectStack[effectStack.length - 1] } } as ReactiveEffect /* 配置一下初始化参数 */ effect.id = uid++ effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] /* TODO:用于收集相关依赖 */ effect.options = options return effect }
createReactiveEffect
createReactiveEffect的做用主要是配置了一些初始化的参数,而后包装了以前传进来的fn,重要的一点是把当前的effect赋值给了activeEffect,这一点很是重要,和收集依赖有着直接的关系
在这里留下了一个疑点,
①为何要用effectStack数组来存放这里effect
咱们这里个响应式初始化阶段进行总结
① setupComponent建立组件,调用composition-api,处理options(构建响应式)获得Observer对象。
② 建立一个渲染effect,里面包装了真正的渲染方法componentEffect,添加一些effect初始化属性。
③ 而后当即执行effect,而后将当前渲染effect赋值给activeEffect
最后咱们用一张图来解释一下整个流程。
/* 深度get */ const get = /*#__PURE__*/ createGetter() /* 浅get */ const shallowGet = /*#__PURE__*/ createGetter(false, true) /* 只读的get */ const readonlyGet = /*#__PURE__*/ createGetter(true) /* 只读的浅get */ const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
上面咱们能够知道,对于以前讲的四种不一样的创建响应式方法,对应了四种不一样的get,下面是一一对应关系。
reactive ---------> get
shallowReactive --------> shallowGet
readonly ----------> readonlyGet
shallowReadonly ---------------> shallowReadonlyGet
四种方法都是调用了createGetter方法,只不过是参数的配置不一样,咱们这里那第一个get方法作参考,接下来探索一下createGetter。
function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { const res = Reflect.get(target, key, receiver) /* 浅逻辑 */ if (shallow) { !isReadonly && track(target, TrackOpTypes.GET, key) return res } /* 数据绑定 */ !isReadonly && track(target, TrackOpTypes.GET, key) return isObject(res) ? isReadonly ? /* 只读属性 */ readonly(res) /* */ : reactive(res) : res } }
这就是createGetter主要流程,特殊的数据类型和ref咱们暂时先不考虑。
这里用了一些流程判断,咱们用流程图来讲明一下这个函数主要作了什么?
咱们能够得出结论:
在vue2.0的时候。响应式是在初始化的时候就深层次递归处理了
可是
与vue2.0不一样的是,即使是深度响应式咱们也只能在获取上一级get以后才能触发下一级的深度响应式。
好比
setup(){ const state = reactive({ a:{ b:{} } }) return { state } }
在初始化的时候,只有a的一层级创建了响应式,b并无创建响应式,而当咱们用state.a的时候,才会真正的将b也作响应式处理,也就是说咱们访问了上一级属性后,下一代属性才会真正意义上创建响应式
这样作好处是,
1 初始化的时候不用递归去处理对象,形成了没必要要的性能开销。
*2 有一些没有用上的state,这里就不须要在深层次响应式处理。
咱们先来看看track源码:
/* target 对象自己 ,key属性值 type 为 'GET' */ export function track(target: object, type: TrackOpTypes, key: unknown) { /* 当打印或者获取属性的时候 console.log(this.a) 是没有activeEffect的 当前返回值为0 */ let depsMap = targetMap.get(target) if (!depsMap) { /* target -map-> depsMap */ targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { /* key : dep dep观察者 */ depsMap.set(key, (dep = new Set())) } /* 当前activeEffect */ if (!dep.has(activeEffect)) { /* dep添加 activeEffect */ dep.add(activeEffect) /* 每一个 activeEffect的deps 存放当前的dep */ activeEffect.deps.push(dep) } }
里面主要引入了两个概念 targetMap 和 depsMap
targetMap
键值对 proxy : depsMap
proxy : 为reactive代理后的 Observer对象 。
depsMap :为存放依赖dep的 map 映射。
depsMap
键值对:key : deps
key 为当前get访问的属性名,
deps 存放effect的set数据类型。
咱们知道track做用大体是,首先根据 proxy对象,获取存放deps的depsMap,而后经过访问的属性名key获取对应的dep,而后将当前激活的effect存入当前dep收集依赖。
主要做用
①找到与当前proxy 和 key对应的dep。
②dep与当前activeEffect创建联系,收集依赖。
为了方便理解,targetMap 和 depsMap的关系,下面咱们用一个例子来讲明:
例子:
父组件A
<div id="app" > <span>{{ state.a }}</span> <span>{{ state.b }}</span> <div> <script> const { createApp, reactive } = Vue /* 子组件 */ const Children ={ template="<div> <span>{{ state.c }}</span> </div>", setup(){ const state = reactive({ c:1 }) return { state } } } /* 父组件 */ createApp({ component:{ Children } setup(){ const state = reactive({ a:1, b:2 }) return { state } } })mount('#app') </script>
咱们用一幅图表示如上关系:
咱们在前面说过,建立一个渲染renderEffect,而后把赋值给activeEffect,最后执行renderEffect ,在这个期间是怎么作依赖收集的呢,让咱们一块儿来看看,update函数中作了什么,咱们回到以前讲的componentEffect逻辑上来
function componentEffect() { if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const { el, props } = initialVNode const { bm, m, a, parent } = instance /* TODO: 触发instance.render函数,造成树结构 */ const subTree = (instance.subTree = renderComponentRoot(instance)) if (bm) { //触发 beforeMount声明周期钩子 invokeArrayFns(bm) } patch( null, subTree, container, anchor, instance, parentSuspense, isSVG ) /* 触发声明周期 mounted钩子 */ if (m) { queuePostRenderEffect(m, parentSuspense) } instance.isMounted = true } else { // 更新组件逻辑 // ...... } }
这边代码大体首先会经过renderComponentRoot方法造成树结构,这里要注意的是,咱们在最初mountComponent的setupComponent方法中,已经经过编译方法compile编译了template模版的内容,state.a state.b等抽象语法树,最终返回的render函数在这个阶段会被触发,在render函数中在模版中的表达式 state.a state.b 点语法会被替换成data中真实的属性,这时候就进行了真正的依赖收集,触发了get方法。接下来就是触发生命周期 beforeMount ,而后对整个树结构从新patch,patch完毕后,调用mounted钩子
① 首先执行renderEffect ,赋值给activeEffect ,调用renderComponentRoot方法,而后触发render函数。
② 根据render函数,解析通过compile,语法树处理事后的模版表达式,访问真实的data属性,触发get。
③ get方法首先通过以前不一样的reactive,经过track方法进行依赖收集。
④ track方法经过当前proxy对象target,和访问的属性名key来找到对应的dep。
⑤ 将dep与当前的activeEffect创建起联系。将activeEffect压入dep数组中,(此时的dep中已经含有当前组件的渲染effect,这就是响应式的根本缘由)若是咱们触发set,就能在数组中找到对应的effect,依次执行。
最后咱们用一个流程图来表达一下依赖收集的流程。
接下来咱们set部分逻辑。
const set = /*#__PURE__*/ createSetter() /* 浅逻辑 */ const shallowSet = /*#__PURE__*/ createSetter(true)
set也是分两个逻辑,set和shallowSet,两种方法都是由createSetter产生,咱们这里主要以set进行剖析。
function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] /* shallowSet逻辑 */ const hadKey = hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) /* 判断当前对象,和存在reactiveToRaw 里面是否相等 */ if (target === toRaw(receiver)) { if (!hadKey) { /* 新建属性 */ /* TriggerOpTypes.ADD -> add */ trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { /* 改变原有属性 */ /* TriggerOpTypes.SET -> set */ trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }
createSetter的流程大体是这样的
① 首先经过toRaw判断当前的proxy对象和创建响应式存入reactiveToRaw的proxy对象是否相等。
② 判断target有没有当前key,若是存在的话,改变属性,执行trigger(target, TriggerOpTypes.SET, key, value, oldValue)。
③ 若是当前key不存在,说明是赋值新属性,执行trigger(target, TriggerOpTypes.ADD, key, value)。
/* 根据value值的改变,从effect和computer拿出对应的callback ,而后依次执行 */ export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { /* 获取depssMap */ const depsMap = targetMap.get(target) /* 没有通过依赖收集的 ,直接返回 */ if (!depsMap) { return } const effects = new Set<ReactiveEffect>() /* effect钩子队列 */ const computedRunners = new Set<ReactiveEffect>() /* 计算属性队列 */ const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || !shouldTrack) { if (effect.options.computed) { /* 处理computed逻辑 */ computedRunners.add(effect) /* 储存对应的dep */ } else { effects.add(effect) /* 储存对应的dep */ } } }) } } add(depsMap.get(key)) const run = (effect: ReactiveEffect) => { if (effect.options.scheduler) { /* 放进 scheduler 调度*/ effect.options.scheduler(effect) } else { effect() /* 不存在调度状况,直接执行effect */ } } //TODO: 必须首先运行计算属性的更新,以便计算的getter //在任何依赖于它们的正常更新effect运行以前,均可能失效。 computedRunners.forEach(run) /* 依次执行computedRunners 回调*/ effects.forEach(run) /* 依次执行 effect 回调( TODO: 里面包括渲染effect )*/ }
咱们这里保留了trigger的核心逻辑
① 首先从targetMap中,根据当前proxy找到与之对应的depsMap。
② 根据key找到depsMap中对应的deps,而后经过add方法分离出对应的effect回调函数和computed回调函数。
③ 依次执行computedRunners 和 effects 队列里面的回调函数,若是发现须要调度处理,放进scheduler事件调度
值得注意的的是:
此时的effect队列中有咱们上述负责渲染的renderEffect,还有经过effectAPI创建的effect,以及经过watch造成的effect。咱们这里只考虑到渲染effect。至于后面的状况会在接下来的文章中和你们一块儿分享。
咱们用一幅流程图说明一下set过程。
咱们总结一下整个数据绑定创建响应式大体分为三个阶段
1 初始化阶段: 初始化阶段经过组件初始化方法造成对应的proxy对象,而后造成一个负责渲染的effect。
2 get依赖收集阶段:经过解析template,替换真实data属性,来触发get,而后经过stack方法,经过proxy对象和key造成对应的deps,将负责渲染的effect存入deps。(这个过程还有其余的effect,好比watchEffect存入deps中 )。
3 set派发更新阶段:当咱们 this[key] = value 改变属性的时候,首先经过trigger方法,经过proxy对象和key找到对应的deps,而后给deps分类分红computedRunners和effect,而后依次执行,若是须要调度的,直接放入调度。
微信扫码关注公众号,按期分享技术文章