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. 当新增、修改和删除时如何实现响应式
相关推荐
Cachel wood9 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端10 分钟前
0基础学前端-----CSS DAY9
前端·css
joan_8514 分钟前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
还是大剑师兰特37 分钟前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248941 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_748235611 小时前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O3 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink6 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss