Vue3 发布后,有一个重要的有关于响应式机制的改动,html
Vue2 的时候,采用的是 Object.defineProperty
方式,重构数据的 set
、get
方法,来达到监听数据变动的方法,vue
可是在 Vue3 发布后,就再也不使用 Object.defineProperty
了,而是使用了 ES6 中的 Proxy
来对数据进行一个封装,起到一个中间代理的做用来监听数据的变动,对于 Proxy
不了解的小伙伴能够看这里:Proxyreact
下面主要是对 Vue3 的响应式机制进行一个简单的实现,主要包含两个:ref
、reactive
;git
ref
:是对基础数据进行封装监听,例如:Boolean、Numberes6
reactive
:是对复杂数据进行封装监听,例如:github
{
key1: 'Benson',
key2: {
key3: 1007,
key4: [1, 2, 3]
}
}
复制代码
let activeEffect // 用于保存当先须要依赖的函数
// mini 依赖中心
class Dep {
constructor(){
this.subs = new Set(); // 使用 Set 避免重复收集依赖
}
depend(){
// 收集依赖
if(activeEffect){
this.subs.add(activeEffect)
}
}
notofy(){
// 数据变化,触发effect执行
this.subs.forEach(effect=>effect())
}
}
function effect(fn){
activeEffect = fn; // 保存当前响应式依赖函数
fn(); // 执行依赖函数
}
const dep = new Dep() // vue3 中就变成一个大的 map
// ref 大概的原理在这了,待会后面能够看代码
function ref(val){
let _value = val
// 拦截.value操做
let state = {
get value(){
// 获取值,收集依赖 track
dep.depend()
return _value
},
set value(newCount){
// 修改,通知dep,执行有这个依赖的effect函数
// 源码这里会作判断,是否真的值发生了变化
_value = newCount
// trigger
dep.notofy()
}
}
return state
}
const state = ref(0)
effect(()=>{
// 这个函数内部,依赖state的变化
console.log(state.value)
})
setInterval(()=>{
state.value++; // 这里进行响应式数据的值改变,触发 set 方法
},1000)
复制代码
上面的案例就是对 ref
的一个简单实现了,其实已经可以很好的表示 Vue3 在源码中对 ref
的实现逻辑了。typescript
接下来能够了解一下源码是怎么样的:segmentfault
ref 在源码中会对传入的数据进行类型判断,若是判断为对象数据类型会使用 reactive
去进行响应式分装的,否者会使用 RefImpl
的 get
,set
方法去监听,这点相似于 Vue2 的 Object.definePropert。数组
在源码上为 Ref 定义了一个 interface
:缓存
// 生成一个惟一key
declare const RefSymbol: unique symbol
export interface Ref<T = any> {
/**
* value值,存放真正的数据的地方
*/
value: T
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
* autocomplete, so we use a private Symbol instead.
* 用此惟一 key,来作 Ref 接口的一个描述符, 让 isRef 函数作类型判断
*/
[RefSymbol]: true
/**
* @internal
*/
_shallow?: boolean
}
复制代码
接下来看看 ref 方法:
// 对于 ref 进行屡次重载
export function ref<T extends object>(
value: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value)
}
// 看通常状况 ref(123),使用最后一个
function createRef(rawValue: unknown, shallow = false) {
// 判断是否已是响应式 ref 数据了
if (isRef(rawValue)) {
return rawValue
}
// 建立响应式数据
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
// 转化数据为响应式数据
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
// track 的代码在 effect中,能猜到此处就是监听函数收集依赖的方法
track(toRaw(this), TrackOpTypes.GET, 'value')
// 返回数据
return this._value
}
set value(newVal) {
// 若是数据发生变化
if (hasChanged(toRaw(newVal), this._rawValue)) {
// 更新数据
this._rawValue = newVal
// 转化数据为响应式数据
this._value = this._shallow ? newVal : convert(newVal)
// 能猜到此处就是触发监听函数执行的方法
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
// 数据类型不合适使用 ref,将采用 reactive
const convert = <T extends unknown>(val: T): T =>
/**
* isObject() 从 @vue/shared 中引入,判断一个数据是否为对象
* 若是传递的值是个对象(包含数组/Map/Set/WeakMap/WeakSet),则使用 reactive 执行,不然返回原数据
*/
isObject(val) ? reactive(val) : val
// 从@vue/shared中引入,判断一个数据是否为对象
// Record<any, any>表明了任意类型key,任意类型value的类型
// 为何 val is Record<any, any> 而不是 val is object 呢?能够看下这个回答:
// https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
复制代码
以上就是 Vue3 中对 ref 的简单阅读,至于 ref 里面的各个内部方法具体逻辑,能够了解一下前面的简单例子就能大概知道了,若是要仔细了解的话,就自行一步一步去查看源码了哈~
Vue3 对于比较复杂的数据,就会采用 reactive
进行响应式的封装,下面来看看如何实现一个简易版的响应式逻辑:
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<div id="btn">click</div>
<script src="./vue.js"></script>
<script>
const root = document.getElementById('app')
const btn = document.getElementById('btn')
// 响应式封装
let obj = reactive({
name: 'Benson',
age: 24,
num: { count: 1 },
})
// 计算属性
let double = computed(()=>obj.age*2)
// 反作用,依赖函数
effect(()=>{
console.log('数据变了',obj.age)
root.innerHTML = `<h1>${obj.name}今年${obj.age}岁了,双倍${double.value}, Num: ${obj.num.count}</h1>`
})
btn.addEventListener('click',()=>{
// obj.age+=1;
obj.num.count += 1; // 测试 reactive 递归封装 Proxy 的特色
},false)
</script>
</body>
</html>
复制代码
在 index.html 中,对一个对象进行 reactive
响应式封装,而且还生成一个对 obj.age
的计算属性,这里其实计算属性就是一个特殊的依赖函数(反作用函数)
effect
反作用函数传入的方法会在响应式数据发生变化后执行。反作用函数执行以后,计算属性根据取值操做,也就是 get
方法会触发,这时候,就会触发 computed
的传入的 Effect 执行获取到最新值,这一点能够留意一下,下面的简易版实现逻辑:
<!--vue.js-->
const effectStack = [] // 这里存储当前响应式数据的依赖函数
let targetMap = new WeakMap() // 存储全部reactive,全部key对应的依赖
// {
// target1: {
// key1: [effect]
// }
// }
// target1 其实就是使用响应式源对象做为 key,对象中的属性做为 key1 ,而后该属性对应着哪一些反作用函数整合到 [effect] 中
function track(target,key){
// 收集依赖
// reactive可能有多个,一个又有N个属性key
const effect = effectStack[effectStack.length-1]
if(effect){
let depMap = targetMap.get(target)
if(!depMap){
depMap = new Map() // 相似对象类型,里面放着响应数据的属性 key 和对应 dep
targetMap.set(target, depMap)
}
let dep = depMap.get(key)
if(!dep){
dep = new Set() // 这里使用了 Set 很重要,这里的 Set 可以防止重复保存依赖函数
depMap.set(key,dep)
}
// 添加依赖
dep.add(effect)
effect.deps.push(dep)
}
}
function trigger(target,key,info){
// 触发更新
let depMap = targetMap.get(target)
if(!depMap){
return
}
const effects = new Set()
const computedRunners = new Set()
if(key){
let deps = depMap.get(key)
deps.forEach(effect=>{
if(effect.computed){
computedRunners.add(effect)
}else{
effects.add(effect)
}
})
}
// 计算属性传入的 `fn` 会依赖 `reactive` 对象的属性 A
// 因此这个 `fn` 也会在属性 A 依赖集合 `deps` 进行存储,属性 A
// 发生了变化也会执行这个 `fn`
computedRunners.forEach(computed=>computed())
// 这里会执行通常的函数,这里就是主要就是执行:root.innerHTML 更新视图
effects.forEach(effect=>effect())
}
function effect(fn,options={}){
// {lazy:false,computed:false}
// 反作用
// computed是一个特殊的effect
let e = createReactiveEffect(fn,options)
if(!options.lazy){
// lazy决定是否是首次就执行effect
e()
}
return e
}
const baseHandler = {
get(target,key){
const res = Reflect.get(target, key); // reflect更合理的
// 收集依赖
track(target,key)
// 当使用到内部属性的时候,再进行 Proxy 封装,
if (typeof res === 'object') {
return reactive(res);
}
return res
},
set(target,key,val){
const info = {oldValue:target[key], newValue:val}
Reflect.set(target, key, val); // Reflect.set
// 触发更新
trigger(target,key,info)
}
}
function reactive(target){
if (typeof target === 'object') {
/*
if (target instanceof Array) {
// 若是是一个数组,那么取出来数组中的每个元素
// 判断每个元素是否又是一个对象,若是又是一个对象,那么也须要包装成 Proxy
target.forEach((item, index) => {
if (typeof item === 'object') {
target[index] = reactive(item);
}
});
} else {
// 若是是一个对象,那么取出对象属性的值
// 判断对象属性的值是否又是一个对象,若是又是一个对象,那么也须要包装成 Proxy
for (let key in target) {
const item = target[key];
if (typeof item === 'object') {
target[key] = reactive(item);
}
}
}
*/
// target变成响应式
const observerd = new Proxy(target, baseHandler);
return observerd;
} else {
console.warn('请传入 Object');
return target;
}
}
function createReactiveEffect(fn,options){
const effect = function _effect(...args){
/* 这里的 _effect 和 fn 都会由于在 run 函数中保存在 effectStack,
* 而后执行 fn 触发数据的 get 方法,保存在 targetMap 对应响应式数据属性 key 的 dep 中,
* 因此 _effect 和 fn 都会一直处于闭包状态,而不会消失,
* 这时候,设置响应式数据的 set 方法时,就会触发执行 _effect 方法,
* 而且从新执行 run 和里面的 fn,这时候 fn执行时,
* 又会触发响应数据的 get 方法,触发收集依赖函数,
* 此时就是由于收集依赖的是 new Set(),一所不会致使重复收集相同的依赖,流程就是这样了
*/
return run(_effect,fn,args)
}
// 为了后续清理 以及缓存
effect.deps = []
effect.computed = options.computed
effect.lazy = options.lazy
return effect
}
function run(effect,fn,args){
if(effectStack.indexOf(effect)===-1){
try{
/**
* 这里计算属性取值的时候,会调用计算属性的 fn 获得返回值,若是没有 if (!effect.computed) 这个条件,
* 那么计算属性中所依赖的属性好比:age 就会绑定上 fn 这个依赖,而不是绑定上 root.innerHTML 这个依赖
* 会致使更新 age 值,没法刷新视图,由于对于这种状况:
* effect(()=>{
* root.innerHTML = `<h1>双倍${double.value}</h1>`
* })
* 没有取值 obj.age,只作了 double.value 的取值的话,就没法让计算属性中的 age 绑定正确的更新函数了
* 固然 vue3 源码中也并非这样作的,这里只是简单了一下,待更新中...
*/
if (!effect.computed) effectStack.push(effect)
return fn(...args)
}finally{
effectStack.pop()
}
}
}
function computed(fn){
// 特殊的effect
const runner = effect(fn, {computed:true,lazy:true})
return{
effect:runner,
get value(){
return runner() // 这里计算属性取值的时候,会执行这个 runner 从而获得最新的值,这个值是依赖于计算属性传入的 fn 而来的
}
}
}
复制代码
上诉案例就是简单的 reactive
实现,里面还有一个特殊的计算属性的响应式实现,基本流程作了什么,都在注释上进行标识了。
有一点注意的是: reactive 会进行嵌套封装 Proxy ,但它又不是一次性的,须要用到内部属性的时候会去给内部属性也封装 Proxy,这样返回的数据进行变动的时候,也能进行代理。
// 源码
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
复制代码
除了 reactive
、ref
外,还有两个相似的 Api:shallowreactive
、shallowref
,这两个和前两个的区别就是不执行嵌套对象的深度响应式转换,只封装第一层 Proxy。
代码中使用到了 ES6 的 Proxy 和 Reflect,不懂的小伙伴还得须要去了解一下这几个知识点滴~