赞
踩
响应系统是Vue.js的重要组成部分,主要由两部分组成,一是响应式数据,二是副作用函数,副作用函数本质上就是函数包裹器,用于跟踪正在运行的函数,在函数调用前启动跟踪,在Vue派发更新时就能找到这些被收集起来的副作用函数,当数据发生更新时就会重新执行它
响应式系统的工作流程:当读取操作时,将副作用函数收集到"桶"中;当设置操作时,从桶中取出副作用函数并执行
下面是组件自身初始化的部分代码
由上述的组件对象,可以执行以下操作
const { data , render } = 组件对象
(1) 调用data函数,将其包装成响应式数据
const state = reactive(data())
effect(()=>{
(2) 调用render函数得到subTree
const subTree = render.call(state,state)
(3) patch
patch(null,subTree)
})
这样实现了组件自身的响应式数据发生变化,那么组件就会自动执行渲染函数,从而实现更新
副作用函数:会产生副作用的函数(废话)
响应式数据:在我们读取某值是,添加副作用函数,待值发生改变,即设置操作,可以使副作用函数重新执行
如何将数据obj变成响应式?
主要两个步骤(还是上面所述):
Vue2中采用的Object.definePrototype函数实现,而Vue3中采用的是代理的方式
首先我么需要知道Object.definePrototype函数的所出现的问题:
通过一个简单的例子了解一下:
const bucket = new Set() const data = { text:'hello' } let proxy = new Proxy(data,{ get(target,key){ bucket.add(effect) return target[key] }, set(target,key,newValue){ target[key] = newValue bucket.forEach(fn=>fn()) return true } }) //注册副作用函数 function effect(){ document.body.innerHTML = proxy.text } //执行副作用函数,触发读取操作 effect() setTimeout(()=>{ proxy.text = 'Vue' },1000)
上面代码是个小型的简单的响应系统,出现的问题已经还是很多,硬编码了副作用函数,我们需要的是哪怕副作用函数是一个匿名函数,也可以被正确的收集到“桶”数据结构中,另外,如何设计这个“桶”结构又是个问题
首先需要设计一个注册副作用函数的effect函数,那么即使是匿名函数可以先注册,再被收集
//使用全局变量存储被注册的副作用函数
let activeEffect
//注册副作用函数
function effect(fn){
activeEffect = fn
fn()
}
桶的设计需要将副作用函数和被操作的目标字段之间建立明确联系,所以需要在三个角色中建立联系,被操作的代理对象obj,被操作的字段名text,使用effect函数注册的副作用函数effectFn,Vue3中采用的WeakMap,WeakMap由target和Map组成,Map由key和Set组成,WeakMap的键是目标对象target,值是一个Map实例,而Map实例时原始对象target的key,Map的值是由一个副作用函数组成的Set
至于为什么使用WeakMap原因:
const proxy = new Proxy(data,{ get(target,key){ track(target,key) return target[key] }, set(target,key,newValue){ target[key] = newValue trigger(target,key) } }) function track(target,key){ if(!activeEffect) return let depsMap = bucket.get(target) if(!depsMap){ bucket.set(target,depsMap = new Map()) } let deps = depsMap.get(key) if(!deps){ depsMap.set(key,deps = new Set()) } deps.add(activeEffect) } function trigger(target,key){ const depsMap = bucket.get(target) if(!depsMap) return const effects = depsMap.get(key) effects && effects.forEach(fn => fn()); }
调度器scheduler用来控制副作用函数执行的时机和方式
基本流程:
首先定义一个conputed函数,接收参数为getter函数,将getter函数作为副作用函数,用它来创建一个带有Lazy的effect.computed函数会返回一个对象,该对象的value属性是一个访问器属性,只有在读取value值时,才会手动调用effectFn函数并返回结果。另外,在调度器中增加dirty属性标识确定是否需要重新计算,如果dirty为true时,表示值被污染,那么需要重新计算。computed的值可以被缓存就是这个原理
function computed(getter){ let value,dirty = true const effectFn = effect(getter,{ lazy:true, scheduler(){ if(!dirty){ dirty = true //当计算属性依赖的响应式数据发生变化,手动调用trigger函数触发响应 trigger(obj,'value') } } }) const obj = { get value(){ if(dirty){ value = effectFn() dirty = false } //当读取value时,手动调用track函数进行跟踪 track(obj,'value') return value } } return obj }
本质上就是检测响应式数据,当数据发生变化时通知并执行响应的回调函数
定义一个watch函数,接收的参数是source和cb,如果source是函数,那么将其传给getter;如果是对象,则递归读取,通过调度器执行回调函数cb
watch( () => obj.foo, (newValue,oldValue) => { console.log(newValue,oldValue)//1,2 } ) obj.foo ++ function watch(source,cb){ let getter if(typeof source === 'function'){ getter = source }else{ getter = ()=> traverse(source) } //定义新值和旧值 let oldValue , newValue const effectFn = effect( () => getter(), { lazy:true, scheduler(){ newValue = effectFn() cb(newValue,oldValue) oldValue = newValue } } ) oldValue = effectFn() } //如果传入的参数是对象,这样就能读取对象上的任意属性,从而属性发生变化都能触发回调函数的执行 function traverse(value,seen = new Set()){ //如果读取的值是原始值或者已经读取过了,那么直接跳过 if(typeof value !== 'object' || value == null || seen.has(value)) return seen.add(value) for(let k in value){ traverse(value[k],seen) } return value }
代理和反射是为开发者提供拦截并向基本操作嵌入额外行为的能力
代理是对目标对象的一种关联的代理对象,而这个代理对象可以作为抽象的目标对象来进行使用
const target = {
id:'001'
}
const handler = {}
const proxy = new Proxy(target,handler)
console.log(target.id)//001
console.log(proxy.id)//001
代理是目标对象的抽象,使用代理的目标就是为了定义捕获器,捕获器就是在处理程序对象中定义基本操作的拦截器,每个捕获器都对应一种基本操作,可以直接在代理对象上调用;处理程序对象中所有可以捕获的方法都有响应的反射(Reflect)API方法,这些方法与捕获器拦截的方法具有想用的名称和函数签名,而且也具有与被拦截方法相同的行为
1.get() const target = { } const proxy = new Proxy(target,{ get( target,property,receiver){ console.log('get()') return Reflect.get(...arguments) } }) proxy.foo//get() //捕获器处理程序参数 --target:目标对象 --property:应用目标对象上的字符串键属性 --receiver代理对象或继承代理对象的对象 2.set() const target = { } const proxy = new Proxy(target,{ set( target,property,value,receiver){ console.log('set()') return Reflect.set(...arguments) } }) proxy.foo = 'bar'//set() //拦截操作 --proxy.property = value --proxy[property] = value //捕获器处理程序参数 --target:目标对象 --property:应用目标对象上的字符串键属性 --value:赋值给属性的值 --receiver代理对象或继承代理对象的对象 3.has() const target = { } const proxy = new Proxy(target,{ has( target,property){ console.log('has()') return Reflect.has(...arguments) } }) 'foo' in proxy//has()
那么为什么Proxy要配合Reflect一起使用呢
console.log(receiver === proxy)//true
表示这里的receiver的确是代理对象,可以将Reflect.get(target,key,receiver)理解为target[key].call(receiver),this变成了代理对象,才会在副作用函数和响应式数据之间建立响应联系,从而达到收集依赖的效果。
2. 框架的健壮性
使用 Object.defineProperty() 重复声明的属性 报错了,因为 JavaScript 是单线程语言,一旦抛出异常,后边的任何逻辑都不会执行,所以为了避免这种情况,我们在底层就要写 大量的 try catch 来避免,不够优雅。使用 **Reflect.defineProperty() 是有返回值的,所以通过 返回值 来判断你当前操作是否成功
对象必要的内部使用方法
对于正常的额访问属性,直接通过get拦截函数实现
const obj = {foo:1}
const p = new Proxy(obj,{
get( target,key,receiver){
//建立联系
track(target,key)
//返回属性值
return Reflect.get(target,key,receiver)
}
})
in关键字如何拦截,通过has
const obj = {foo:1}
const p = new Proxy(obj,{
has(target,key){
track(target,key)
return Reflect.has(target,key)
}
})
for…in循环
const obj = {foo:1}
const ITERATE_KEY = Symbol()
let p = new Proxy(obj,{
ownKeys(target){
track(target,ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。