阅读前须知
- 本文是笔者学习卡颂的《React设计原理》的读书笔记,对书中有价值内容以Q&A方式进行呈现,同时结合了自己的理解🤔
- 阅读时推荐先看问题,想想自己的答案,再和答案比对一下
- 本文属于前端框架科普,读完快速对前端框架有个概览👍
文章目录
- 前端框架原理概览
- 1 react是库还是前端框架?框架和库有啥区别?
- 2 如何理解前端框架的原理`UI = f(state)`
- 3 如何同时描述UI和逻辑
- 4 组件是存放UI和逻辑的松耦合单元,那么组件如何组织UI和逻辑
- 5 数据如何在组件间传递
- 6 前端框架分类
- 7 如何理解reduce和reducer
- 8 useReducer如何使用,和useState有什么关系区别,如何利用useReducer实现一个useState
- 9 react hooks中的自变量和因变量
- 前端框架使用的主流技术
- 1 useState如何实现state改变时,useEffect重新执行
- 2 useState如何实现state数值改变之后如何触发UI的更新
- 3 AOT和JIT有什么区别
- 4 利用AOT的框架(多适用于模板语法描述UI的框架)solid svelte
- 5 VDOM框架(jsx描述UI的框架+模板语法描述UI的框架)blockdom inferno
- 6 前端框架实现原理 🌟
- 6.1 元素级框架 svelte(AOT)
- 6.2 Vue3原理(组件级框架)
- 6.3 React原理(应用级框架)
- 前端框架总结
前端框架原理概览
1 react是库还是前端框架?框架和库有啥区别?
react本身是构建UI的库,将其称为框架是约定俗称的说法,其实不是框架
2 如何理解前端框架的原理UI = f(state)
框架f就是一个函数,自变量state是当前数据,因变量是宿主环境的视图
● state: 当前数据
● f:框架内部运行机制
○ 根据自变量的变化计算出UI的变化
○ 根据UI变化执行宿主环境的API
● UI:宿主环境的视图
3 如何同时描述UI和逻辑
4 组件是存放UI和逻辑的松耦合单元,那么组件如何组织UI和逻辑
state数据:自变量
f函数:是逻辑
UI:因变量
y=f(x) 自变量x的变化,可能会导致依赖x的因变量y的变化
const App = () => {
const [count, setCount] = useState(0);
// 自变量count改变导致因变量y1改变(纯函数)
const y1 = useMemo(()=>count*2,[count]);
// 有副作用
useEffect(()=>{
document.title="new title";
console.log("副作用");
},[])
}
5 数据如何在组件间传递
当前组件的自变量或者因变量借助于UI传递给子组件,作为其自变量
子组件自身的自变量称为state
其他组件传递的自变量称为props
6 前端框架分类
7 如何理解reduce和reducer
reduce:函数式编程当中的一个术语,reduce操作被称为Fold折叠
// 数组中的reduce函数,第一个参数通常被称为reducer
const sum = [0, 1, 2].reduce((prev, item) => {
return prev + item;
}, 0)
拿JavaScript来理解。reduce属于一种高阶函数,它将其中的回调函数reducer递归应用到数组的所有元素上并返回一个独立的值。这也就是“缩减”或“折叠”的意义所在了。
reducer:(state, action) => newState
redux中的reducer函数是因为它的入参和返回值都非常类似于arr的reduce中传入的回调函数
// reducer接收两个参数: state=[], action
// reducer返回值是一个新的state
const todoReducer = (state=[], action) => {
switch(action){
case "ADD":
return [...state, {id: 111}];
default:
return state;
}
}
8 useReducer如何使用,和useState有什么关系区别,如何利用useReducer实现一个useState
● useRedcuer是useState的替换方案,和useState相比,它更适合state逻辑复杂,或者state是个对象,包含多个子值,或者下一个state依赖于之前的stated的情况。相当于收敛逻辑于reducer函数中进行管理
● useReducer使用
const reducer = (state, action) => {
switch (action) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
function Reducer() {
const [num, dispatch] = useReducer(reducer, 1);
return (
<div>
<p>
useStateByReducer :
<button
onClick={() => {
// 区分dispatch函数和reducer函数
dispatch('DECREMENT');
}}
>
-
</button>
{num}
<button
onClick={() => {
dispatch('INCREMENT');
}}
>
+
</button>
</p>
</div>
);
}
export default Reducer;
● 用useReducer实现一个useState
import { useReducer, useRef } from 'react';
const isFunction = (fn) => {
return Object.prototype.toString.call(fn) === '[object Function]';
};
export default (initialState) => {
const reducer = (state, action) => {
return isFunction(action) ? action(state) : action;
};
return useReducer(reducer, initialState);
};
9 react hooks中的自变量和因变量
前端框架使用的主流技术
1 useState如何实现state改变时,useEffect重新执行
细粒度更新:依赖追踪技术如何实现的
实现useState的state和useEffect的双向绑定
function useState(value?: any) {
/**
* 问题:state绑定的不是回调函数,而是数据结构
* 解法:创建一个数据结构:effect={execute: () => {}, deps: [subs1, subs2],}
*/
const subs: Set<Effect> = new Set();
const getter = () => {
// 问题:如何实现隐式绑定,如何知道执行getter时,所对应的effect
// 解法:执行useEffect前,将effect加入堆栈,getter函数获取当前处于栈顶部的effect,即为当前执行的上下文,执行完之后,移出堆栈
if (effectStack.length > 0) {
const curEffect = effectStack[effectStack.length - 1];
subscribe(subs, curEffect);
}
return value;
};
const setter = (newValue?) => {
value = newValue;
// emit
for (const sub of [...subs]) {
console.log('setter emit subs');
// debugger
sub.execute();
}
};
return [getter, setter];
}
**
* 创建一个的useEffect
* 1 不需要显示指定依赖
* 2 useEffect执行后,回调函数自动执行
* 3 依赖变化时,useEffect自动执行
*/
function useEffect(fn) {
const execute = () => {
// 每次执行,都会先删除依赖
cleanup(effect);
effectStack.push(effect);
// 首次会执行一次回调,来触发“自动依赖收集”
try {
fn();
} finally {
effectStack.pop();
}
};
const effect = {
execute,
deps: new Set<Set<Effect>>(), // III:使用set来存发布订阅的数据结构,而不是list
};
execute();
}
/**
* 双向删除订阅关系
* @param effect
*/
function cleanup(effect: Effect) {
// console.log('cleanup', effect);
// 找到useState的subs,清理该effect(在别人那里,清除自己的痕迹)
for (const subs of [...effect.deps]) {
subs.delete(effect);
}
// 清理effect的deps
effect.deps.clear();
}
/**
* 双向订阅
* @param subs
* @param effect
*/
function subscribe(subs, effect) {
subs.add(effect);
effect.deps.add(subs);
}
useMemo是useEffect包了一层
function useMemo(fn) {
const [state, setState] = useState();
useEffect(() => setState(fn()));
return state();
}
2 useState如何实现state数值改变之后如何触发UI的更新
在编译阶段:AOT
在运行阶段VDOM:
3 AOT和JIT有什么区别
4 利用AOT的框架(多适用于模板语法描述UI的框架)solid svelte
用模板语法描述UI的框架可以从AOT中受益,因为模板语法中“静态”部分可以和“动态”部分很轻易的分离
从而减少框架在“自变量变化计算出UI变化”这部分工作量
5 VDOM框架(jsx描述UI的框架+模板语法描述UI的框架)blockdom inferno
VDOM是什么:虚拟DOM也是为了描述UI,为了实现“自变量变化计算出UI变化”
VDOM什么用:将元素描述的UI改成VDOM描述的UI,通过比较VDOM的变化,来计算UI的变化
VDOM好处
● 真实DOM元素冗余属性较多,没必要全部进行比较,减少内存开销
● 描述能力强大??
● 可以多平台渲染
6 前端框架实现原理 🌟
6.1 元素级框架 svelte(AOT)
Q: 如何建立自变量和UI元素的对应关系
编译器编译时绑定update方法和入参
Svelte编译器在编译<App/>
时,直接追踪<script>
标签中所有变量声明
一旦涉及count++/count=1等变量赋值的语句,该变量就会被提取到instance函数中,instance函数返回值作为组件的入参ctx
Q: 自变量变化时,如何更新UI元素
更新流程开始于元素,编译时已经绑定了具体元素
dirty可以指定自变量和元素的变化关系,set_data可以改dom
自变量变化时,标记dirty,调度更新fragment执行p方法,p方法内的if语句直接和dirty标记对应,执行set_data(), set_data函数会执行具体dom操作
编译时绑定UI和state的关系,触发变化时,标记dirty,更新入参,执行updateUI函数
等代码预先编译为create_fragment函数,创建dom,挂载,更新等
将state作为组件create_fragment的入参
function create_fragment(ctx) {
let h1;
let t;
let dispose;
return {
c() {
h1 = element("h1");
t = text(/*count*/ ctx[0]);
},
m(target, anchor) {
insert(target, h1, anchor);
append(h1, t);
// 在mount时候,绑定dom元素事件,回调函数
dispose = listen(h1, "click", /*update1*/ ctx[1]);
},
p(ctx, [dirty]) {
// count变,元素h1的t变,建立了自变量和元素的对应关系
if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(h1);
dispose();
}
};
}
// ctx 执行上下文 包括state和改变state的行为以及回调函数
function instance($$self, $$props, $$invalidate) {
let count = 0;
function update1() {
// 触发ctx更新,dirty标记
$$invalidate(0, count++, count);
}
setTimeout(
() => {
$$invalidate(0, count++, count);
},
1000
);
return [count, update1];
}
6.2 Vue3原理(组件级框架)
● 如何建立自变量和UI组件的对应关系
○ 细粒度更新
○ 每个组件都有一个watchEffect(类似useEffect),实现state变化时候,自动执行watchEffect中回调函数
● 自变量变化时,如何更新元素(更新流程开始于组件
○ 自变量变化,执行render函数,生成当前组件的VNode,和上一次生成的prevNode一起作为patch函数的入参,返回值为UI中变化的元素
watchEffect的回调函数中包括:render,patch一条龙
AOT如何帮助Vue3
<div>
<h1>hello</h1>
<p>{{name}}</p>
</div>
AOT后(识别模板代码,标记可能发生变化的量)
const render = (_ctx, cache) => {
return(_openBlock(), _createElementBlock("div", null, [
_createElementVNode("h1", null, "helllo"),
// patchFlag 为1 表示变化的元素,为text, 2表示class
_createElementVNode("p", null, _ctx.name, 1 /* TEXT*/)
]))
}
// 生成的VNode大致表示成
const vnode = {
tag: "div",
children: [
{tag: "h1", children: "hello"},
{tag: "p", children: ctx.name, patchFlag: 1}
],
dynamicChildren:[
{tag: "p", children: ctx.name, patchFlag: 1}
]
}
之后执行patch时,只需要比对dynamicChildren即可
6.3 React原理(应用级框架)
● 如何建立自变量和UI元素的对应关系
○ 不需要建立
● 自变量变化时,如何更新元素
○ 只要自变量改变,就从根节点开始,重新遍历应用,找到diff,执行dom
● react每次自变量变化都会从根节点遍历应用,会不会性能差?
不会
○ 内部有优化机制
○ 提供一些优化api,减少不必要的遍历: shouldComponentUpdate,memo,PureComponent等