Vue3响应系统

响应式系统的作用和实现

响应系统是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)
})
这样实现了组件自身的响应式数据发生变化,那么组件就会自动执行渲染函数,从而实现更新
1.响应式数据和副作用函数

副作用函数:会产生副作用的函数(废话)
响应式数据:在我们读取某值是,添加副作用函数,待值发生改变,即设置操作,可以使副作用函数重新执行

2.响应式数据的基本实现

如何将数据obj变成响应式?
主要两个步骤(还是上面所述):

  1. 读取obj时,将副作用函数effect存储在某个数据结构“桶”中(weakMap)
  2. 当进行设置操作时,在将副作用函数effect从“桶”中取出并执行

Vue2中采用的Object.definePrototype函数实现,而Vue3中采用的是代理的方式
首先我么需要知道Object.definePrototype函数的所出现的问题:

  • Object.defineProperty()监听对象的属性而非对象本身
  • 这就导致了必须遍历对象中的每个属性
  • 如果对象中某属性依旧是对象,需要递归调用
  • 对于动态插入对象的属性,需要手动添加监听
  • 无法监听数组变化,这也就是Vue2为何重写了数组中七个方法的原因(push,pop,unshift,shift,reverse,sort,splice)这些方法调用原数组会发生改变
    为了解决上述一些问题,Vue3引入了Proxy,那么Proxy有哪些优点?
  • Proxy可以直接监听数组变化
  • Proxy可以直接监听对象而非对象属性
  • Proxy有13种拦截方法,更加丰富

通过一个简单的例子了解一下:

    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)
3.完善的响应式系统

上面代码是个小型的简单的响应系统,出现的问题已经还是很多,硬编码了副作用函数,我们需要的是哪怕副作用函数是一个匿名函数,也可以被正确的收集到“桶”数据结构中,另外,如何设计这个“桶”结构又是个问题
首先需要设计一个注册副作用函数的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原因:

  • 这是ES6新增的弱映射,什么叫弱,描述的是JS垃圾回收程序对待弱映射中键的方式
  • WeakMap的键必须是对象,所引用的对象是弱引用,如果对象的其他引用被删除,那么回收机制就会释放该对象所占的内存
  • 对象作为WeakMap的键,如果没有指向该对象的应用,那么当代吗执行完,这个对象键就会消失,然后这个键/对就会从弱映射中消失,成为一个空映射,那么就会变成垃圾回收的目标
  • 可以设想,如果target对象没有任何应用,说明用户并不需要它了,那么垃圾回收机制就会完成回收任务,但是如果通过Map来代替WeakMap,那么用户的代码没有任何应用,这个target也不会被回收,最终就会导致内存的溢出
    将上述代码进行补充和封装,得到代码如下:
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());
}
4.computed和lazy

调度器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
}
5.watch实现

本质上就是检测响应式数据,当数据发生变化时通知并执行响应的回调函数
定义一个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
}

非原始值的响应式方案

Proxy和Reflect

代理和反射是为开发者提供拦截并向基本操作嵌入额外行为的能力
代理是对目标对象的一种关联的代理对象,而这个代理对象可以作为抽象的目标对象来进行使用

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一起使用呢

  1. 触发代理对象劫持时保证正确的this上下文指向
    在内部打印
console.log(receiver === proxy)//true

表示这里的receiver的确是代理对象,可以将Reflect.get(target,key,receiver)理解为target[key].call(receiver),this变成了代理对象,才会在副作用函数和响应式数据之间建立响应联系,从而达到收集依赖的效果。
2. 框架的健壮性
使用 Object.defineProperty() 重复声明的属性 报错了,因为 JavaScript 是单线程语言,一旦抛出异常,后边的任何逻辑都不会执行,所以为了避免这种情况,我们在底层就要写 大量的 try catch 来避免,不够优雅。使用 **Reflect.defineProperty() 是有返回值的,所以通过 返回值 来判断你当前操作是否成功

代理对象

对象必要的内部使用方法

  • [[GetPrototypeOf]] 查明为该对象提供继承属性的对象
  • [[SetPrototypeOf]] 将该对象与提供继承属性的另一个对象向关联
  • [[IsExtensible]] 查明是否允许向该对象添加其他属性
  • [[PreventExtensions]] 控制是否允许向该对象添加其他属性
  • [[GetOwnProperty]] 返回该对象自身属性的描述符
  • [[DefineOwnProperty]] 创建或更改自己的属性
  • [[HasProperty]] 返回该对象是否已经拥有键为propertyKey的自己的或继承的属性
  • [[Get]]
  • [[Set]]
  • [[Delete]]
  • [[OwnPropertyKeys]] 返回list,元素为对象自身的属性键
  • [[Call]] 将运行代码和this对象关联
  • [[Constructor]] 创建一个对象,
    代理对象本质上就是进行“读取”操作,响应系统拦截一切读取操作,数据发生变化时触发响应
    正常对象的可能读取操作如下:
  • 访问属性:obj.foo
  • 判断给定key是否在对象或原型上:key in obj
  • 使用for…in循环遍历对象:for(let key in obj){}

对于正常的额访问属性,直接通过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)
  } 
})
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值