Vue3.js“非原始值”响应式实现基本原理笔记(二)

如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~

作者:前端小王hs

阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主

此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来

书籍:《Vue.js设计与实现》 作者:霍春阳

本篇博文将在书第5.1节5.4节 的基础上进一步总结所提到的基础概念 ,附加了测试 的代码运行示例 ,方便正在学习Vue3想分析Vue3源码的朋友快速阅读

如有帮助,不胜荣幸

前文: Vue3.js"非原始值"响应式实现基本原理笔记(一)

如何代理Object

"读取"是一个很宽泛的概念(原文)

在之前的笔记中,只简单的讨论了如obj.foo这般获取对象属性值的读取,但读取有很多种,例如下面几种:

  1. 访问属性:obj.foo
  2. 判断:key in obj
  3. 遍历:for (const key in obj)
  4. ...

这一章的内容,就是针对这几种不同的读取 ,以及其他的常见行为如删除 等进行拦截

实现的逻辑主要是看操作符 对应的拦截函数

在书中是通过查阅ECMA 规范,明确操作符 运行逻辑,进而找到操作符 的运算结果是调用什么抽象方法 ,然后通过这个抽象方法 找到对应的内部方法 ,进而对比Vue3.js"非原始值"响应式实现基本原理笔记(一)提到的Proxy内部方法表,选取对应的方法拦截

拦截 in

下面in操作符为例,我们来看一下书中是如何逐步找到拦截方法

ECMA-262 规范的13.10.1 (原文),找到in操作符的运行时逻辑: 01. 让 lref 的值为 RelationalExpression 的执行结果。 02. 让 lval 的值为 ? GetValue(lref)。 03. 让 rref 的值为 ShiftExpression 的执行结果。 04. 让 rval 的值为 ? GetValue(rref)。 05. 如果 Type(rval) 不是对象,则抛出 TypeError 异常。 06. 返回 ? HasProperty(rval, ? ToPropertyKey(lval))。

关键是第6步 ,出现了HasProperty(),然后在在ECMA-262 规范的7.3.11(原文)找到关于这个方法的逻辑:

  1. 断言:Type(O) 是 Object。
  2. 断言:IsPropertyKey(P) 是 true。
  3. 返回 ? O.[[HasProperty]] (P)。

可以发现这个内部方法[[HasProperty]],然后在中找到对应的拦截函数 ------has,如下图所示:

然后就可以在Proxyhandler中使用has进行拦截了,代码如下:

js 复制代码
const obj = { foo: 1 }
const p = new Proxy(obj, {
  has(target, key) {
    track(target, key)
    return Reflect.has(target, key)
  }
})

effect(() => {
  'foo' in p // 将会建立依赖关系
})

第5章 有非常多的关于ECMA 的运行时逻辑,在书中没有解释,所以笔者在这里还是简单介绍一下一些关键词(以上述in的运行时逻辑为例):

  1. RelationalExpression:关系表达式,例如<>=ininstanceof<=>=等操作符,在这里指的操作符in左侧的表达式
  2. **ShiftExpression**:位移操作的表达式,操作符in右侧的表达式
  3. lreflvalRelationalExpression会生成一个引用lref,然后通过GetValue(lref)得到lval
  4. rreflref:逻辑同上

拦截 for...in(遍历所有可枚举属性)

逻辑和in是相同的,找规范找到最后发现可以使用ownKey()去拦截

但是在ownKey中会做一些处理,代码如下:

js 复制代码
const obj = { foo: 1 }
const ITERATE_KEY = Symbol()

const p = new Proxy(obj, {
  ownKeys(target) {
    // 将副作用函数与 ITERATE_KEY 关联
    track(target, ITERATE_KEY)
    return Reflect.ownKeys(target)
  }
})

注:这本书的特点就是类似于电视连续剧 ,整一章节的内容是不断累积的,所以的时候不要间断,同时要多复习

在之前的笔记中,track是传入targetkey,代码如下:

js 复制代码
function track(target, key) {
  // 没有 activeEffect,直接 return
  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()));
  }
  const array = Array.from(deps);
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

现在变成了传入ITERATE_KEYiterate的意思是重复

为什么传入ITERATE_KEY?

因为ownKeys是用来获取一个对象的所有键,不是与任何具体的 进行绑定,所以就定义一个ITERATE_KEY作为标识,去与副作用函数进行绑定

那么当触发的时候,也同样需要传入ITERATE_KEY,代码如下:

js 复制代码
trigger(target, ITERATE_KEY)

这里需要注意的是,遍历是遍历,触发拦截是触发拦截 ,整个过程只会触发一次 ownKeys,可以理解为执行到for..in时就触发拦截,然后for...in循环遍历自身可枚举的属性

添加属性对for...in的影响

在上述代码中,当执行p.bar=2时,for...in就会由循环一次变为两次(因为obj变为了两个 ),但此时不会触发与ITERATE_KEY关联的副作用函数

原因非常简单,执行p.bar=2,关联的是与bar相关联的副作用函数

解决的方法也非常简单,在trigger中把两者都添加到effectsToRun

js 复制代码
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  // 取得与 key 相关联的副作用函数
  const effects = depsMap.get(key)
  // 取得与 ITERATE_KEY 相关联的副作用函数
  const iterateEffects = depsMap.get(ITERATE_KEY)

  const effectsToRun = new Set()
  // 将与 key 相关联的副作用函数添加到 effectsToRun
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  // 将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRun
  iterateEffects && iterateEffects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })

  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
}

这里的ITERATE_KEY是外部定义的Symbol

target即遍历的obj

修改属性对foo...in的影响

修改属性不会对foo...in产生影响,但需要注意的是修改属性新增属性 使用的都是[[Set]],所以需要做个区分,代码如下:

js 复制代码
const p = new Proxy(obj, {  
  // 拦截设置操作
  set(target, key, newVal, receiver) {
    // 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD';
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver);
    // 将 type 作为第三个参数传递给 trigger 函数
    trigger(target, key, type);
    return res;
  },
  // 省略其他拦截函数
});

然后再在trigger中进行判断,如果是ADD时才执行depsMap.get(ITERATE_KEY),代码如下:

js 复制代码
function trigger(target, key, type) {  
  // 省略其他逻辑
  // 只有当操作类型为 'ADD' 时,才触发与 ITERATE_KEY 相关联的副作用函数  
  if (type === 'ADD') {  
    const iterateEffects = depsMap.get(ITERATE_KEY);  
    iterateEffects && iterateEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn);
      }
    });
  }
}

在书中还提到了定义枚举类型的重要性:

js 复制代码
const TriggerType = {
  SET: 'SET',
  ADD: 'ADD'
};

便于后期维护

删除属性对for...in的影响

在书中通过查阅规范,得知是通过deleteProperty去拦截的,所以代码如下:

js 复制代码
const p = new Proxy(obj, {  
  deleteProperty(target, key) {  
    // 检查被操作的属性是否是对象自己的属性  
    const hadKey = Object.prototype.hasOwnProperty.call(target, key);  
    // 使用 Reflect.deleteProperty 完成属性的删除  
    const res = Reflect.deleteProperty(target, key);  
    if (res && hadKey) {  
      // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新  
      trigger(target, key, 'DELETE');  
    }  
    return res;  
  }  
});

这里手写是进行了一个判断,删除的属性是否属于自身,这是因为执行的逻辑可能是如delete p.a,执行了但是这个属性不存在,所以需要进行一个判断

那么最后就是在trigger中继续加多一个type判断,代码如下:

js 复制代码
function trigger(target, key, type) {  
  // 省略其他逻辑
  // 只有当操作类型为 'ADD' 或 'DELETE' 时才触发 
  if (type === 'ADD' || type === 'DELETE') {  
    const iterateEffects = depsMap.get(ITERATE_KEY);  
    iterateEffects && iterateEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn);
      }
    });
  }
}

其实看到这里,能够发现在设计时是从CURD去考虑的不同情况

总结

这篇笔记主要复习了不同读取情况下的响应式实现:

  1. 如何拦截in操作符
  2. 如何拦截for...in操作符
  3. 当新增、修改和删除时如何实现响应式
相关推荐
musk12123 分钟前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘32 分钟前
js代码09
开发语言·javascript·ecmascript
万少1 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL1 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl021 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang1 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景1 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼1 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿1 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再2 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref