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. 当新增、修改和删除时如何实现响应式
相关推荐
月下点灯10 分钟前
使用Set集合新特性,快速实现一个商品SKU(单品)规格选择器
前端·javascript·vue.js
大侠Luffy10 分钟前
做了这些SEO动作,独立开发的网站开始被搜索引擎逐量收录
前端·seo
四棱子17 分钟前
炫酷!18.5kb实现流体动画,这个开源项目让个人主页瞬间高大上!
前端·开源
Sparkxuan18 分钟前
封装WebSocket
前端·websocket
工呈士18 分钟前
Redux 实践与中间件应用
前端·react.js·面试
Nano19 分钟前
深入解析 JavaScript 数据类型:从基础到高级应用
前端
无羡仙19 分钟前
浮动与BFC容器
前端
xphjj19 分钟前
树形数据模糊搜索
前端·javascript·算法
刺客_Andy19 分钟前
React 第三十四节 Router 开发中 useLocation Hook 的用法以及案例详解
前端·react.js
我的div丢了肿么办20 分钟前
HarmonyOS鸿蒙tabBar的详细讲解
前端·javascript·harmonyos