san-book
Faq
兼容 IE 以及体积小都是旧时代的需求, 也许在 2016 年 san 的这些特点还足够突出, 但是在 2023 的今天, 是否还有必要使用 san 呢? 性能也不是最优的
san-native 还是有虚拟 DOM 嘛? 那之前去掉的一层是什么东西? 因为性能原因去掉了, 改成直接调用 nativeRenderAPI
Event Native 修饰符
看不懂在说什么, P39
aNode 就是 Vdom 么?
Chapter1
MVVM 对比
san 组件依赖模版编译阶段生成的节点关系熟,确定需要变更的最早是土
从功能角度看, 组件就是 ViewModel
从页面的角度看, 组件是 html 元素的扩展
运行时和预编译
react 是重运行时的代表, 数据变化需要生成新的虚拟 DOM, 再通过 diff 算法
svelte 是重编译、轻运行时的代表, 在编译和构建阶段将应用转换为 JS 应用
san 和 vue 介于两者之间, 保留了虚拟 DOM, 通过响应式控制虚拟 DOM 的粒度
同构和跨端
svelte 直接编译成 js, 性能接近原生, 为什么 san 和 vue 还要保留虚拟 dom 呢?
虚拟 dom 除了可以参与 diff 计算也描述了视图本身, 在跨端领域意义重大, 视图层返回了找个对象, 渲染层就可以调用不同的平台的渲染 API 去绘制
san ssr 和 san-native 的基础都是虚拟 DOM
Note
react 钩子出现的目的之一就是解决类中 this 混乱的问题,
类和 react 钩子本质上是对面向对象编程和函数式编程两种编程思想的讨论
Chapter2
从纯 HTML 到 San 组件的逻辑
纯 html 无法复用 -> js component -> js component 依然需要操作 DOM , DOM 很复杂 -> 数据驱动视图
- 需要手动绑定, 解绑事件, 而且没有解除事件绑定的时机
- 状态暴露在全局,难以管理
为了方便的操作 html 树 所以有了 AST, san 的 AST 具体表现为 ANode -> 通过模板解析器把 html 字符串转化成 JSON 格式的 AST
数据如何驱动视图? 在数据变化之后, 需要有一个操作去更新视图:
- 使用 definedProperty 或者 proxy 来被动监听数据的修改操作
- 通过一系列与第一的数据修改 API 来进行数据修改, 通过这些 API 来感知数据的修改
- 进行脏数据检查
依赖搜集和触发视图更新的逻辑
- 通过差值语法描述视图
- 差值语法默认进行 html 转义, 避免符号冲突和 XSS 攻击
- 支持表达式
- 支持方法调用, 这里有一个让人迷惑的地方, 就是向方法传参数, 总感觉函数只会调用一次, 但是其实每次数据更新时都会重新传参数, 是一个类似于在毁掉函数中再嵌套一层的做法, 在 react 中就是这样写的
- 支持事件绑定
- 支持指令
视图模板解析源码分析
san 组件被解析为 ANode, 视图模板上的属性会被解析为属性数组, 每个属性会调用 src/parser/integrate-attrs.js 中的 integrateAttr 方法来单独解析每个属性
组件生命周期
生命周期本质上是状态管理, 每个生命周期代表着组件的状态
san 认为 updated 并不是组件的一个状态. 仅仅是一个行为, 所以 san 没有提供 updated 生命周期. 但是提供了行为钩子, updated 钩子函数被认为是一个行为钩子
组件反解
组件实例的属性 el 表示组件对应的 html 元素, 在组件初始化时可以通过 option 传入.
基本上在编写组件时不需要关心它,但是如果在初始化组件是传入 el, 意味着让组件以此元素作为组件跟元素.
组件将:
- 不会使用预设的 template 渲染视图
- 不会创建跟元素
- 直接到达生命周期的 compiled, created, attached 节点
有时, 我们为了节省首屏时间, 期望初始的视图时直接的 html, 不希望初始视图是由组件渲染的. 但是我们希望组件为我们管理数据、逻辑与视图, 后续的用户交互行为与视图交换通过组件管理, 这时通过 el 传入一个现有的元素, 组件将以此元素为组件的跟元素, 并反解析出视图结构. 这个过程被称为 组件反解
看起来是 attached 之后再生成一个完整的 san 组件来做后续的逻辑 接管
数据处理
完成 data 类的实现, 框架通过 data 类堆原始数据进行封装, 开发者通过 Data 泪提供的 API 来修改数据, 触发 API 中的 this.fire 方法, 遍历 listeners 中所有的依赖函数, 并诸葛调用, 触发视图更新.
响应式原理
数据的变动是如何被感知的, 感知之后就可以进行视图的更新
被动式追踪
defineProperty 开发者可以使用 JS 的语法直接修改数据, 但是有一定的缺点:
- 实现成本高, 需要循环对象, 对每个属性进行监听
- 覆盖的操作有限, 无法应对数组的修改, 需要使用 slice 等, 让开发者迷惑
proxy: 新 api, 兼容性没那么好
主动式追踪
san 的做法, 通过 this.data.set 更新数据并进行视图刷新
- san 支持
this.data.set('a[0].b.c')
, 这样的 path, 通过 parseExpr 方法把字符串解析成结构化数据, 类似于 ast 一样可以遍历执行去更新数据 - 结合 immutableset 来更新数据, 这样可以更方便的感知数据的变化
- 如果使用普通方式修改数据, 如果我们想知道那些数据发生了变化, 只能一层层的进行遍历
- 相对的, 使用 不可变的方式修改, 我们只需要沿着变化路径寻找就行了.
- 关键就是不可方式是如何维护这个变化路径的?
- 沿着路径修改, 每次都会做复制, 复制之后新的对象继续沿着路径递归
- 那么不在修改路径上的就会引用着原来的数据, 在修改路径上的就会引用着复制出来的数据
- 这样自然就知道了引用着就数据的是没有变化的, 引用着复制数据的就是发生了变化的
普通方式做修改, 只有 G 发生了变化, 需要遍历到 G 才能知道已经变化了
- A
- B
- D
- E
- C
- F
- G'
- B
不可变方式做修改, A C G 都发生了变化
- A'
- B
- D
- E
- C'
- F
- G'
- B
通过 data.set 只能更新对象形式的数据, 为了让所有的数据操作都可以通过 data 更新, san 还对数组的几个方法进行了重写, splice
搜集数据依赖
当数据进行了更新之后, 一个刷新视图的朴素方法就是所有组件基于新的数据重新执行一遍渲染逻辑, 但是这样性能太差了, 点名批评 react. 更好的做法是通过依赖搜集, 将数据和组件进行绑定, 通过数据找到使用的组件.
- 不进行依赖搜集, react
- 数据组与组件进行绑定, san : 对象 dataA 发生了更新, 相应的组件以及它的所有子组件都进行更新
- 数据字段与组件进行绑定 vue:
- 数据字段与 DOM 节点进行绑定, 太复杂, 太耗性能了. 但是貌似 solid 就是在编译时做了这样的绑定, 数据更新了直接调用 DOM 更新
绑定的越精细, 更新视图的成本就越低, 但是相应的需要更复杂的代码来实现, 这些实现又会影响性能
如何维护 listeners 数组中的值
- 对于组件中的 data 实例, 可以在组件创建时就添加依赖
按照目前描述来看, 一个 data 只需要与一个组件做绑定就 ok 了, 所有子组件都会更新, 那么这个 data 不就是组件自身的 data 么, 干嘛还要绑定, 搞不懂了
因为除了自己, 只有子组件能依赖自己的data, 所以没有必要再记录更多的组件了. 每次更新都会自然的遍历子组件
找到组件内的节点进行更新
aNode 与 template 是等价的, 就是 ast 呗
通过 aNode hotspot 让每个节点知道自己依赖了哪些数据, 如果自己以及子节点没有依赖 changeData 就不用继续递归了
在预热的过程中, 维护一个 ANode stack, 这样除了自己知道依赖哪些数据, 所有的父节点都可以方便的添加依赖的数据
这个过程其实就是 shouldcomponentupdate 做的事情, 但是因为 jsx 太灵活了, 做不到模板的静态编译, 所以在这部分也损失了性能
根据 aNode hotspot 执行 shouldElementUpdate
Props 的处理
子组件是否需要声明 props ? san 选择里不需要了, react 选择了需要
Computed 实现
通过改变 this 来做拦截, 在 computed 内访问了 this.data. 就先去计算依赖的属性