首页 > 其他分享 >Solid 之旅 —— 为什么 props 被解构后会导致响应式丢失

Solid 之旅 —— 为什么 props 被解构后会导致响应式丢失

时间:2024-11-17 18:16:27浏览次数:3  
标签:const name Solid age 解构 props return desc

在前面的文章中,我们学习了 Solid 的响应式原理,深入了了解其实现方式。

Solid 之旅 —— Signal 响应式原理

这篇文章将主要深入解析组件内部props的原理,为什么结构后会导致响应式丢失?

案例

我们以一个例子作为参考,由浅入深的讲解其中的奥秘。

Parent.tsx

import { createSignal } from 'solid-js'
import Child from './child'

export default function Props() {
  const [name, setName] = createSignal('JinSo')
  const [age, setAge] = createSignal(23)

  const update = () => {
    setName('Jason')
    setAge(99)
  }

  return (
    <section class='text-gray-700 p-8'>
      <Child name={name()} age={age()} />
      <button type='button' onClick={update}>
        Update
      </button>
    </section>
  )
}

Child.tsx

interface ChildProps {
  name: string
  age: number
}

export default function Child(props: ChildProps) {
  return (
    <div>
      <div>name: {props.name}</div>
      <div>age: {props.age}</div>
    </div>
  )
}

组件编译

先来看看组件编译后是什么样子的,官网也提供了 Playground,可以进行尝试:

Solid Playground

Parent.tsx 组件编译效果

import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
var _tmpl$ = /*#__PURE__*/_$template(`<section class="text-gray-700 p-8"><button type=button>Update`);
import { render } from "solid-js/web";
import { createSignal } from "solid-js";

export default function Props() {
  const [name, setName] = createSignal('JinSo');
  const [age, setAge] = createSignal(23);
  const update = () => {
    setName('Jason');
    setAge(99);
  };
  return (() => {
    var _el$ = _tmpl$(),
      _el$2 = _el$.firstChild;
    _$insert(_el$, _$createComponent(Child, {
      get name() {
        return name();
      },
      get age() {
        return age();
      }
    }), _el$2);
    _el$2.$$click = update;
    return _el$;
  })();
}

render(() => _$createComponent(Props, {}), document.getElementById("app"));
_$delegateEvents(["click"]);

Child.tsx 组件编译效果

import { template as _$template } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
var _tmpl$ = /*#__PURE__*/_$template(`<div><div>name: </div><div>age: `);
import { render } from "solid-js/web";

function Child(props) {
  return (() => {
    var _el$ = _tmpl$(),
      _el$2 = _el$.firstChild,
      _el$3 = _el$2.firstChild,
      _el$4 = _el$2.nextSibling,
      _el$5 = _el$4.firstChild;
    _$insert(_el$2, () => props.name, null);
    _$insert(_el$4, () => props.age, null);
    return _el$;
  })();
}

编译内容和源码本身其实大部分是一致的,只是对最终的 DOM 实现这块进行了特殊处理;暂时剥去其他的内容先不管,看看 Child 组件被编译成了什么:

_$createComponent(Child, {
  get name() {
    return name()
  },
  get age() {
    return age()
  },
})

可以看到,使用了 createComponent 生成组件,同时将 props 转换成一个对象进行传递。

createComponent 里实际就是调用了 Comp 函数式组件:

export function createComponent<T>(Comp: Component<T>, props: T): JSX.Element {
  if ("_SOLID_DEV_") return devComponent(Comp, props || ({} as T));
  return untrack(() => Comp(props || ({} as T)));
}

先来看一下这个转换后的 props 对象,重点关注一下它创建对象的这种方式,说一下这种方式的好处:

  1. 这种方式直接将数据封装在对象内部,而不是直接暴露,同时,也无法去修改属性值。
  2. 和 Solid 结合,这样,每次调用 name() 的时候,都能拿到最新的值。
  3. 延迟计算,只有在访问的时候才会执行。

原因

根据之前文章(响应式原理)那一篇,我们能得知,保持响应式的关键就是这个 name()age() 这两个 Signal,它们内部执行会进行依赖收集操作。

那如果把它进行结构,就会直接获取到 name() 的值,后续时候就会丢失 readSignal 的执行,只是单纯的一个值。

const a = props.name

props.name -> name()
a -> 'JinSo' // 导致响应式丢失

这也是,为什么不建议拆分 props 进行使用的原因。

官方对此也有说明:

Props - SolidDocs

如果你想解构下来使用,可以通过以下方式:

const a = () => props.name

当然,这种方式,实际还是调用的 Signal,这在 Solid 里面有一个术语,叫 Derived Signal(派生 Signal)。

Derived signals - SolidDocs

特殊例子

再来看几个特殊的方式,如果我把传递 Signal 的方式改为传递函数呢?

export default function Props() {
  // ...

  return (
    <section class='text-gray-700 p-8'>
      <Child name={name} age={age} />
    </section>
  )
}

这样,你是可以在子组件中解构下来使用的,因为这时你传入的是一个函数了,不再是一个简单的 Signal 值。

再把子组件改成如下方式调用。

function Child(props: ChildProps) {
  const {name, age} = props

  return (
    <div>
      <div>name: {name()}</div>
      <div>age: {age()}</div>
    </div>
  )
}

实际上,也是生效的。

对照一开始那个案例来看,我们知道 props 实现响应式的本质还是 Signal 的处理。所以这里的 Child 并没有违背这个理念,只是换了种方式,而且是可行的。

甚至说,子组件不变都是生效的。

// 原
function Child(props: ChildProps) {
  return (
    <div>
      <div>name: {props.name}</div>
      <div>age: {props.age}</div>
    </div>
  )
}

这其实和 Vue 使用 ref 是一个道理,在 Vue 中的 template 使用 ref 并不需要手动添加 value 属性,因为 Vue 在编译的时候给你处理了。

同理,Solid 也会在编译的时候给 Signal 做处理。

可以回头看一下 Child 组件被编译的结果,会调用一个 insert 函数。

_$insert(_el$2, () => props.name, null);

来看一下这个函数内部的处理:

dom-expresssions/packages/dom-expresssions/src/client.js

export function insert(parent, accessor, marker, initial) {
  if (marker !== undefined && !initial) initial = [];
  if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);

	// 这里,对于 accessor是个函数的情况下,会手动做一层响应式处理
  effect(current => insertExpression(parent, accessor(), current, marker), initial);
}

对于函数的情况,Solid 内部会自动做一层响应式处理。

额外

我们知道 Solid 提供了两个方法,来管理/操作 props 的属性。

来看看这两个方法里面做了什么,能保持响应式不会丢失的。

mergeProps

以下面这个为例:

const [s, set] = createSignal(undefined)
const props = {
  get name() {
    return s();
  },
  get age() {
    return 26;
  }
}

const mergedProps = mergeProps({name: 'default'}, props)

我们来先来看一下关于属性合并相关的源码:

export function mergeProps<T extends unknown[]>(...sources: T): MergeProps<T> {
  // ...

  const sourcesMap: Record<string, any[]> = {};
  const defined: Record<string, PropertyDescriptor | undefined> = Object.create(null);
  //let someNonTargetKey = false;

  for (let i = sources.length - 1; i >= 0; i--) {
    const source = sources[i] as Record<string, any>;
    if (!source) continue;
    const sourceKeys = Object.getOwnPropertyNames(source);
    //someNonTargetKey = someNonTargetKey || (i !== 0 && !!sourceKeys.length);
    for (let i = sourceKeys.length - 1; i >= 0; i--) {
      const key = sourceKeys[i];
      if (key === "__proto__" || key === "constructor") continue;
      // props 中的 Signal 是包含 get 的属性描述符的
      const desc = Object.getOwnPropertyDescriptor(source, key)!;
      if (!defined[key]) {
        defined[key] = desc.get
          ? {
              enumerable: true,
              configurable: true,
              // 注意,这里将 desc.get 绑定到 source 上,这样在 resolveSources 的时候,就可以通过 source 访问到对应的 key 的值
              // 这里注意, sourceMap[key] 是数组,所以 resolveSources 的时候,会遍历 sourceMap[key],执行对应的 get 方法
              // 同时,因为是数组,所以如果定义了多个同名 key 的 props,会执行多次,会执行 Signal 的响应式和默认值处理。
              get: resolveSources.bind((sourcesMap[key] = [desc.get.bind(source)]))
            }
          : desc.value !== undefined
          ? desc
          : undefined;
      } else {
        // 这里正常是用来处理多个同名 key 的 props 的,比如:
        // const mergedProps = mergeProps({name: 'default'}, props)
        // 这里就会将 name 添加到 sourcesMap[name] 中
        const sources = sourcesMap[key];
        if (sources) {
          // 合并 props 走这里,因为存在不同的 desc.get
          if (desc.get) sources.push(desc.get.bind(source));
          // 合并 默认值 走这里,将 desc.value 添加到 source 上
          else if (desc.value !== undefined) sources.push(() => desc.value);
        }
      }
    }
  }
  // 最后做统一合并
  const target: Record<string, any> = {};
  const definedKeys = Object.keys(defined);
  for (let i = definedKeys.length - 1; i >= 0; i--) {
    const key = definedKeys[i],
      desc = defined[key];
    if (desc && desc.get) Object.defineProperty(target, key, desc);
    else target[key] = desc ? desc.value : undefined;
  }
  return target as any;
}

主要就是对参数内的对象进行合并,生成一个新的 target 对象。

同时,对内部特殊的属性(如props上的属性包含get),做特殊处理,同时每个 key 有一个独立的 sources,用于做多个同名 key 处理,包括合并多个 props、默认值等处理。

再看一下 resolveSources 做了什么:

function resolveSources(this: (() => any)[]) {
  for (let i = 0, length = this.length; i < length; ++i) {
	  // 注意这里的执行,对于 Signal 的话,会做响应式处理
    const v = this[i]();
    if (v !== undefined) return v;
  }
}

按上面的案例来说,这里的 this(sources) 数据为 [props.name, () ⇒ name(’default’)]

因为此时 props.nameundefined,所以会走默认值。

这里注意,props.name 也是执行的,即执行了 Signal,所以后面 name 更新的时候,这里的数据也会更新。

所以说,实际上 mergeProps 只是做了层代理,最终调用的还是 props 上的属性的 get 来实现的响应式。

splitProps

同理,来看个案例:

console.log(props) // {a: 1, b: 2, c: 3, d: 4, e: 5, foo: "bar"}
const [vowels, consonants, leftovers] = splitProps(
  props,
  ["a", "e"],
  ["b", "c", "d"]
)
console.log(vowels) // {a: 1, e: 5}
console.log(consonants) // {b: 2, c: 3, d: 4}
console.log(leftovers.foo) // bar

实现上和 mergeProps 的基本类似:

export function splitProps<
  T extends Record<any, any>,
  K extends [readonly (keyof T)[], ...(readonly (keyof T)[])[]]
>(props: T, ...keys: K): SplitProps<T, K> {
	// ...

  const otherObject: Record<string, any> = {};
  const objects: Record<string, any>[] = keys.map(() => ({}));

  for (const propName of Object.getOwnPropertyNames(props)) {
    const desc = Object.getOwnPropertyDescriptor(props, propName)!;
    const isDefaultDesc =
      !desc.get && !desc.set && desc.enumerable && desc.writable && desc.configurable;
    // 利用 keys 进行划分,将 propName 划分到对应的 objects 中
    let blocked = false;
    let objectIndex = 0;
    for (const k of keys) {
      if (k.includes(propName)) {
        blocked = true;
        isDefaultDesc
          ? (objects[objectIndex][propName] = desc.value)
          : Object.defineProperty(objects[objectIndex], propName, desc);
      }
      ++objectIndex;
    }
    if (!blocked) {
      isDefaultDesc
        ? (otherObject[propName] = desc.value)
        : Object.defineProperty(otherObject, propName, desc);
    }
  }
  return [...objects, otherObject] as any;
}

根据 keysprops 进行拆分,划分到不同的 object 返回即可。

回到原文,我们已经知道 props 能保持响应式的原理是什么了,以及为什么会导致其响应式丢失的问题;实际上本质还是 Signal 去实现的。

标签:const,name,Solid,age,解构,props,return,desc
From: https://blog.csdn.net/qq_41496424/article/details/143785789

相关文章

  • Solidity学习笔记-1
    01.HelloWorld开发工具Remix//SPDX-License-Identifier:MIT//软件许可,不写编译会出现警告//版本,“0.8.21”-->不能小于这个版本,"^"-->不能大于0.9.0pragmasolidity^0.8.21;//创建合约contractHelloWorld{stringpublichelloworld="HelloWorld!";}......
  • 解析 React Scheduler 原理,Solid 竟也在使用!
    对于ReactScheduler,它通过将任务切片并异步执行,避免了阻塞浏览器的主线程。很多人其实都看到过类似的文章了,甚至说去手写调度器,都写的很不错,所以本文将从一个新的角度探讨ReactScheduler,揭示它是如何利用几个简单的API实现这一壮举的。ReactScheduler解析首先,让......
  • 【前端】浅谈SOLID原则在前端的使用
    原创宁奇舞精选本文作者系360奇舞团前端开发工程师简介SOLID原则是由RobertC.Martin在2000年提出的一套软件开发准则,最初用于面向对象编程(OOP),旨在解决软件开发中的复杂性和维护问题。随着时间推移,它不仅在传统OOP语言中广泛应用,也被引入到JavaScript和TypeS......
  • JavaScript中的解构赋值
    写在前面在JavaScript中,解构赋值是一种简洁而强大的语法特性,它允许我们从数组或对象中提取值并将其分配给变量。这个功能可以大大简化代码,提高可读性和可维护性。今天,我们将深入探讨解构赋值的用法和规则。数组解构赋值数组解构赋值允许我们从数组中提取值并将其分配给变......
  • vue-props配置
    原文链接:vue-props配置–每天进步一点点1.props作用props主要用于组件实例对象之间传递参数,比如我们前面创建的student组件,我们在组件中让他显示一些信息,比如下面这样:Student组件如下:1234567891011121314151617181920<template>  <div> ......
  • C# Solidworks二次开发:宏录制实战讲解(第一讲)
    大家好,今天要讲的是关于在做Solidworks二次开发的时候,想要实现一些软件中操作的时候,我们最直接的办法就是使用宏录制功能,会将我们想要的API直接录制出来,这样避免了很多我们自己去查询的过程。下面通过一些录制的例子为大家讲解一下:(1)首先讲一个使用曲线文件的方式来创建曲线,并......
  • 【vue】15.组件通信的方式(一)props、emits、v-model
    Vue组件通信指的是在Vue.js应用程序中,不同组件之间传递数据和信息的过程。在一个项目中,通常有很多个组件,其中每个组件都是独立的实例,都拥有自己的状态和方法。因此,为了构建复杂的应用,组件之间需要能够互相传递数据和信息,也就是进行组件通信,接下来的几篇内容将对vue组件通信......
  • SOLIDWORS许可证错误问题分析
    当大家在安装SOLIDWORKS可能遇到无法获得下列许可,该文章主要介绍常见几种情况与解决办法。1.当在下载的过程中遇到该问题我们首先因该检测电脑名称是为中文字符,如果为中文字符请将中文字符更改为字母字符,在下图中的重命名中更改。然后在重新破解。2.如果在安装后出现该情况......
  • vue3.5 测试props解构能不能响应式
    代码测试<template><divclass='box'>demo:{{d}}<div><child:abc='abc'/></div></div></template><scriptlang='ts'setup>import{ref,reactive,comput......
  • SolidWorks一键重命名、使之独立、创建新版
    在老铁们的大力支持之下,第5版终于面世了。本宏程序基于solidworks2021版设计,未在其余版本的solidworks下测试。本程序文件共享在网盘中,关注微信公众号“全栈开发的码农”并回复消息“一键重命名”即可获得下载链接和提取码。下载程序文件后,在solidworks打开任意文档,添加并运行宏。......