每日一题-平衡二叉树🟢
给定一个二叉树,判断它是否是 平衡二叉树
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4] 输出:false
示例 3:
输入:root = [] 输出:true
提示:
- 树中的节点数在范围
[0, 5000]内 -104 <= Node.val <= 104
给定一个二叉树,判断它是否是 平衡二叉树
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4] 输出:false
示例 3:
输入:root = [] 输出:true
提示:
[0, 5000] 内-104 <= Node.val <= 104在Unity的Shader Graph系统中,Custom Depth Node(自定义深度节点)是一个功能强大的工具,专门用于访问和处理高清渲染管线(HDRP)中的自定义深度缓冲区。这个节点为着色器开发者提供了精细控制深度信息的能力,是实现高级渲染效果的基石。
Custom Depth Node在不同渲染管线中的支持情况是开发者必须首先了解的关键信息。这个节点的设计初衷是为了满足HDRP的高级渲染需求,因此在兼容性上有着明确的界限划分。
高清渲染管线(HDRP)支持
HDRP作为Unity的高端渲染解决方案,专门为需要高质量图形表现的项目设计。在这个管线中,Custom Depth Node能够完全发挥其功能:
通用渲染管线(URP)不支持
URP作为轻量级的通用渲染解决方案,在深度缓冲区的管理上采用了不同的策略:
这种兼容性差异源于两个渲染管线的设计哲学和目标平台的不同。HDRP面向高端平台,追求极致的视觉效果,而URP则注重性能和跨平台兼容性。
Custom Depth Node的端口配置决定了它如何接收输入数据和输出处理结果。深入理解每个端口的功能对于正确使用该节点至关重要。
UV输入端口
UV输入端口是Custom Depth Node的核心配置项,它决定了深度采样的位置和方式:
UV端口的正确配置需要考虑多个因素:
在实际使用中,UV输入端口的配置示例:
HLSL
// 直接使用屏幕位置
float4 screenPos = GetScreenPosition();
// 手动计算UV坐标
float2 uv = float2(input.position.x / _ScreenParams.x,
input.position.y / _ScreenParams.y);
输出端口
输出端口提供了处理后的深度数据:
输出数据的解读依赖于选择的深度采样模式,不同模式下的输出含义各不相同。开发者需要根据具体的渲染需求选择合适的采样模式。
深度采样模式决定了Custom Depth Node如何处理和输出深度信息。每种模式都有其特定的应用场景和数学特性。
Linear01采样模式
Linear01模式将深度值线性化并归一化到[0,1]范围内:
Linear01模式的数学原理:
HLSL
float Linear01Depth(float z)
{
return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
在实际应用中的优势:
Raw采样模式
Raw模式直接输出深度缓冲区中的原始数值:
Raw模式的特性分析:
Eye采样模式
Eye模式将深度值转换为视空间中的实际距离:
Eye模式的转换原理:
HLSL
float LinearEyeDepth(float z)
{
return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}
这种模式在实际项目中的应用价值:
Custom Depth Node在HDRP项目中有广泛的应用场景,以下是几个典型的应用案例。
高级景深效果实现
使用Custom Depth Node可以实现电影级别的景深效果:
HLSL
// 景深效果的核心实现
void ApplyDepthOfField(float2 uv, float focusDistance, float focalLength)
{
float depth = SampleCustomDepth(uv, LINEAR_EYE);
float blurAmount = saturate(abs(depth - focusDistance) / focalLength);
// 基于深度差异应用模糊
return ApplyBlur(uv, blurAmount);
}
实现要点:
交互式水体和液体效果
Custom Depth Node在液体渲染中发挥关键作用:
HLSL
// 水体表面与场景交互
void CalculateWaterEffects(float2 uv, float waterLevel)
{
float sceneDepth = SampleCustomDepth(uv, LINEAR_EYE);
float waterDepth = max(0, sceneDepth - waterLevel);
// 基于水深调整颜色和透明度
float3 waterColor = Lerp(_ShallowColor, _DeepColor, waterDepth / _MaxDepth);
float transparency = exp(-waterDepth * _Absorption);
}
技术细节:
体积雾和大气效果
利用深度信息创建真实的体积效果:
HLSL
// 体积雾密度计算
float CalculateFogDensity(float2 uv, float3 worldPos)
{
float depth = SampleCustomDepth(uv, LINEAR_EYE);
float fogDensity = 0.0;
// 基于距离的指数雾
fogDensity = _FogDensity * exp(-depth * _FogFalloff);
// 添加高度雾
fogDensity += _HeightFogDensity * exp(-worldPos.y * _HeightFalloff);
return saturate(fogDensity);
}
优化考虑:
在使用Custom Depth Node时,性能优化是必须考虑的重要因素。
深度采样优化策略
内存带宽优化
HLSL
// 优化的深度采样模式选择
#ifndef REQUIRE_HIGH_PRECISION_DEPTH
// 使用较低精度的采样
float depth = SampleCustomDepth(uv, LINEAR01);
#else
// 需要高精度时使用完整精度
float depth = SampleCustomDepth(uv, LINEAR_EYE);
#endif
平台特定优化
不同硬件平台对深度采样的支持存在差异:
自定义深度与运动矢量结合
HLSL
// 结合深度和运动矢量实现运动模糊
void AdvancedMotionBlur(float2 uv, float2 motionVector)
{
float currentDepth = SampleCustomDepth(uv, LINEAR_EYE);
float2 prevUV = uv - motionVector;
float previousDepth = SampleCustomDepth(prevUV, LINEAR_EYE);
// 基于深度一致性验证运动矢量
if(abs(currentDepth - previousDepth) < _DepthTolerance)
{
// 应用高质量运动模糊
return ApplyMotionBlur(uv, motionVector);
}
else
{
// 回退到普通运动模糊
return FallbackMotionBlur(uv, motionVector);
}
}
深度精度问题解决
深度精度问题是深度渲染中的常见挑战:
多相机渲染中的深度管理
在复杂渲染管线中处理多相机场景:
HLSL
// 多相机深度合成
float CompositeMultiCameraDepth(float2 uv)
{
float mainCameraDepth = SampleCustomDepth(uv, LINEAR_EYE);
float secondaryCameraDepth = SampleSecondaryDepth(uv, LINEAR_EYE);
// 基于渲染优先级合成深度
return min(mainCameraDepth, secondaryCameraDepth);
}
Custom Depth Node很少单独使用,通常需要与其他Shader Graph节点配合。
与Scene Depth节点的对比使用
HLSL
// 场景深度与自定义深度的混合使用
void HybridDepthEffects(float2 uv)
{
float sceneDepth = SceneDepth(uv);
float customDepth = CustomDepth(uv, LINEAR_EYE);
// 基于特定条件选择深度源
float finalDepth = customDepth > 0 ? customDepth : sceneDepth;
// 应用深度相关效果
ApplyDepthBasedEffects(uv, finalDepth);
}
在渲染管线中的集成
Custom Depth Node需要正确集成到HDRP渲染管线中:
深度效果的调试是开发过程中的重要环节。
深度可视化工具
HLSL
// 深度值可视化
float3 VisualizeDepth(float depth, int mode)
{
switch(mode)
{
case 0: // 灰度可视化
return depth.xxx;
case 1: // 热力图
return HeatMap(depth, 0, _FarClipPlane);
case 2: // 等高线
return ContourLines(depth, _ContourSpacing);
default:
return float3(1,0,1); // 错误颜色
}
}
常见问题诊断
【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)
看完这两期视频,让你对递归的理解更上一层楼!
问:代码中的 $-1$ 是怎么产生的?怎么返回的?
答:在某次递归中,发现左右子树高度绝对差大于 $1$,我们会返回 $-1$。这个 $-1$ 会一路向上不断返回,直到根节点。
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def get_height(node: Optional[TreeNode]) -> int:
if node is None:
return 0
left_h = get_height(node.left)
right_h = get_height(node.right)
if left_h == -1 or right_h == -1 or abs(left_h - right_h) > 1:
return -1
return max(left_h, right_h) + 1
return get_height(root) != -1
class Solution {
public boolean isBalanced(TreeNode root) {
return getHeight(root) != -1;
}
private int getHeight(TreeNode node) {
if (node == null) {
return 0;
}
int leftH = getHeight(node.left);
int rightH = getHeight(node.right);
if (leftH == -1 || rightH == -1 || Math.abs(leftH - rightH) > 1) {
return -1;
}
return Math.max(leftH, rightH) + 1;
}
}
class Solution {
int get_height(TreeNode* node) {
if (node == nullptr) {
return 0;
}
int left_h = get_height(node->left);
int right_h = get_height(node->right);
if (left_h == -1 || right_h == -1 || abs(left_h - right_h) > 1) {
return -1;
}
return max(left_h, right_h) + 1;
}
public:
bool isBalanced(TreeNode* root) {
return get_height(root) != -1;
}
};
#define MAX(a, b) ((b) > (a) ? (b) : (a))
int getHeight(struct TreeNode* node) {
if (node == NULL) {
return 0;
}
int left_h = getHeight(node->left);
int right_h = getHeight(node->right);
if (left_h == -1 || right_h == -1 || abs(left_h - right_h) > 1) {
return -1;
}
return MAX(left_h, right_h) + 1;
}
bool isBalanced(struct TreeNode* root) {
return getHeight(root) != -1;
}
func getHeight(node *TreeNode) int {
if node == nil {
return 0
}
leftH := getHeight(node.Left)
rightH := getHeight(node.Right)
if leftH == -1 || rightH == -1 || abs(leftH-rightH) > 1 {
return -1
}
return max(leftH, rightH) + 1
}
func isBalanced(root *TreeNode) bool {
return getHeight(root) != -1
}
func abs(x int) int { if x < 0 { return -x }; return x }
function getHeight(node) {
if (node === null) {
return 0;
}
const leftH = getHeight(node.left);
const rightH = getHeight(node.right);
if (leftH === -1 || rightH === -1 || Math.abs(leftH - rightH) > 1) {
return -1;
}
return Math.max(leftH, rightH) + 1;
}
var isBalanced = function(root) {
return getHeight(root) !== -1;
};
use std::rc::Rc;
use std::cell::RefCell;
impl Solution {
pub fn is_balanced(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
fn get_height(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
if let Some(node) = node {
let node = node.borrow();
let left_h = get_height(&node.left);
let right_h = get_height(&node.right);
if left_h == -1 || right_h == -1 || (left_h - right_h).abs() > 1 {
return -1;
}
return left_h.max(right_h) + 1;
}
0
}
get_height(&root) != -1
}
}
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def get_height(node: Optional[TreeNode]) -> int:
if node is None:
return 0
left_h = get_height(node.left)
if left_h == -1:
return -1 # 提前退出,不再递归
right_h = get_height(node.right)
if right_h == -1 or abs(left_h - right_h) > 1:
return -1
return max(left_h, right_h) + 1
return get_height(root) != -1
class Solution {
public boolean isBalanced(TreeNode root) {
return getHeight(root) != -1;
}
private int getHeight(TreeNode node) {
if (node == null) {
return 0;
}
int leftH = getHeight(node.left);
if (leftH == -1) {
return -1; // 提前退出,不再递归
}
int rightH = getHeight(node.right);
if (rightH == -1 || Math.abs(leftH - rightH) > 1) {
return -1;
}
return Math.max(leftH, rightH) + 1;
}
}
class Solution {
int get_height(TreeNode* node) {
if (node == nullptr) {
return 0;
}
int left_h = get_height(node->left);
if (left_h == -1) {
return -1; // 提前退出,不再递归
}
int right_h = get_height(node->right);
if (right_h == -1 || abs(left_h - right_h) > 1) {
return -1;
}
return max(left_h, right_h) + 1;
}
public:
bool isBalanced(TreeNode* root) {
return get_height(root) != -1;
}
};
#define MAX(a, b) ((b) > (a) ? (b) : (a))
int getHeight(struct TreeNode* node) {
if (node == NULL) {
return 0;
}
int left_h = getHeight(node->left);
if (left_h == -1) {
return -1; // 提前退出,不再递归
}
int right_h = getHeight(node->right);
if (right_h == -1 || abs(left_h - right_h) > 1) {
return -1;
}
return MAX(left_h, right_h) + 1;
}
bool isBalanced(struct TreeNode* root) {
return getHeight(root) != -1;
}
func getHeight(node *TreeNode) int {
if node == nil {
return 0
}
leftH := getHeight(node.Left)
if leftH == -1 {
return -1 // 提前退出,不再递归
}
rightH := getHeight(node.Right)
if rightH == -1 || abs(leftH-rightH) > 1 {
return -1
}
return max(leftH, rightH) + 1
}
func isBalanced(root *TreeNode) bool {
return getHeight(root) != -1
}
func abs(x int) int { if x < 0 { return -x }; return x }
function getHeight(node) {
if (node === null) {
return 0;
}
const leftH = getHeight(node.left);
if (leftH === -1) {
return -1; // 提前退出,不再递归
}
const rightH = getHeight(node.right);
if (rightH === -1 || Math.abs(leftH - rightH) > 1) {
return -1;
}
return Math.max(leftH, rightH) + 1;
}
var isBalanced = function(root) {
return getHeight(root) !== -1;
};
use std::rc::Rc;
use std::cell::RefCell;
impl Solution {
pub fn is_balanced(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
fn get_height(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
if let Some(node) = node {
let node = node.borrow();
let left_h = get_height(&node.left);
if left_h == -1 {
return -1; // 提前退出,不再递归
}
let right_h = get_height(&node.right);
if right_h == -1 || (left_h - right_h).abs() > 1 {
return -1;
}
return left_h.max(right_h) + 1;
}
0
}
get_height(&root) != -1
}
}
欢迎关注 B站@灵茶山艾府
这道题中的平衡二叉树的定义是:二叉树的每个节点的左右子树的高度差的绝对值不超过 $1$,则二叉树是平衡二叉树。根据定义,一棵二叉树是平衡二叉树,当且仅当其所有子树也都是平衡二叉树,因此可以使用递归的方式判断二叉树是不是平衡二叉树,递归的顺序可以是自顶向下或者自底向上。
定义函数 $\texttt{height}$,用于计算二叉树中的任意一个节点 $p$ 的高度:
$$
\texttt{height}(p) =
\begin{cases}
0 & p \text{ 是空节点}\
\max(\texttt{height}(p.\textit{left}), \texttt{height}(p.\textit{right}))+1 & p \text{ 是非空节点}
\end{cases}
$$
有了计算节点高度的函数,即可判断二叉树是否平衡。具体做法类似于二叉树的前序遍历,即对于当前遍历到的节点,首先计算左右子树的高度,如果左右子树的高度差是否不超过 $1$,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归的过程。
<
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
>
###Java
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
} else {
return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
}
public int height(TreeNode root) {
if (root == null) {
return 0;
} else {
return Math.max(height(root.left), height(root.right)) + 1;
}
}
}
###C++
class Solution {
public:
int height(TreeNode* root) {
if (root == NULL) {
return 0;
} else {
return max(height(root->left), height(root->right)) + 1;
}
}
bool isBalanced(TreeNode* root) {
if (root == NULL) {
return true;
} else {
return abs(height(root->left) - height(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
}
}
};
###Python
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
def height(root: TreeNode) -> int:
if not root:
return 0
return max(height(root.left), height(root.right)) + 1
if not root:
return True
return abs(height(root.left) - height(root.right)) <= 1 and self.isBalanced(root.left) and self.isBalanced(root.right)
###C
int height(struct TreeNode* root) {
if (root == NULL) {
return 0;
} else {
return fmax(height(root->left), height(root->right)) + 1;
}
}
bool isBalanced(struct TreeNode* root) {
if (root == NULL) {
return true;
} else {
return fabs(height(root->left) - height(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
}
}
###golang
func isBalanced(root *TreeNode) bool {
if root == nil {
return true
}
return abs(height(root.Left) - height(root.Right)) <= 1 && isBalanced(root.Left) && isBalanced(root.Right)
}
func height(root *TreeNode) int {
if root == nil {
return 0
}
return max(height(root.Left), height(root.Right)) + 1
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
func abs(x int) int {
if x < 0 {
return -1 * x
}
return x
}
复杂度分析
时间复杂度:$O(n^2)$,其中 $n$ 是二叉树中的节点个数。
最坏情况下,二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是 $O(n)$。
对于节点 $p$,如果它的高度是 $d$,则 $\texttt{height}(p)$ 最多会被调用 $d$ 次(即遍历到它的每一个祖先节点时)。对于平均的情况,一棵树的高度 $h$ 满足 $O(h)=O(\log n)$,因为 $d \leq h$,所以总时间复杂度为 $O(n \log n)$。对于最坏的情况,二叉树形成链式结构,高度为 $O(n)$,此时总时间复杂度为 $O(n^2)$。
空间复杂度:$O(n)$,其中 $n$ 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 $n$。
方法一由于是自顶向下递归,因此对于同一个节点,函数 $\texttt{height}$ 会被重复调用,导致时间复杂度较高。如果使用自底向上的做法,则对于每个节点,函数 $\texttt{height}$ 只会被调用一次。
自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回 $-1$。如果存在一棵子树不平衡,则整个二叉树一定不平衡。
<
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
>
###Java
class Solution {
public boolean isBalanced(TreeNode root) {
return height(root) >= 0;
}
public int height(TreeNode root) {
if (root == null) {
return 0;
}
int leftHeight = height(root.left);
int rightHeight = height(root.right);
if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
return -1;
} else {
return Math.max(leftHeight, rightHeight) + 1;
}
}
}
###C++
class Solution {
public:
int height(TreeNode* root) {
if (root == NULL) {
return 0;
}
int leftHeight = height(root->left);
int rightHeight = height(root->right);
if (leftHeight == -1 || rightHeight == -1 || abs(leftHeight - rightHeight) > 1) {
return -1;
} else {
return max(leftHeight, rightHeight) + 1;
}
}
bool isBalanced(TreeNode* root) {
return height(root) >= 0;
}
};
###Python
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
def height(root: TreeNode) -> int:
if not root:
return 0
leftHeight = height(root.left)
rightHeight = height(root.right)
if leftHeight == -1 or rightHeight == -1 or abs(leftHeight - rightHeight) > 1:
return -1
else:
return max(leftHeight, rightHeight) + 1
return height(root) >= 0
###C
int height(struct TreeNode* root) {
if (root == NULL) {
return 0;
}
int leftHeight = height(root->left);
int rightHeight = height(root->right);
if (leftHeight == -1 || rightHeight == -1 || fabs(leftHeight - rightHeight) > 1) {
return -1;
} else {
return fmax(leftHeight, rightHeight) + 1;
}
}
bool isBalanced(struct TreeNode* root) {
return height(root) >= 0;
}
###golang
func isBalanced(root *TreeNode) bool {
return height(root) >= 0
}
func height(root *TreeNode) int {
if root == nil {
return 0
}
leftHeight := height(root.Left)
rightHeight := height(root.Right)
if leftHeight == -1 || rightHeight == -1 || abs(leftHeight - rightHeight) > 1 {
return -1
}
return max(leftHeight, rightHeight) + 1
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
func abs(x int) int {
if x < 0 {
return -1 * x
}
return x
}
复杂度分析
时间复杂度:$O(n)$,其中 $n$ 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 $O(n)$。
空间复杂度:$O(n)$,其中 $n$ 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 $n$。
以下两种方法均基于以下性质推出:当前树的深度 等于 左子树的深度 与 右子树的深度 中的 最大值 $+1$ 。
{:width=450}
此方法为本题的最优解法,但剪枝的方法不易第一时间想到。
思路是对二叉树做后序遍历,从底至顶返回子树深度,若判定某子树不是平衡树则 “剪枝” ,直接向上返回。
函数 recur(root) :
root 左 / 右子树的深度差 $\leq 1$ :则返回当前子树的深度,即节点 root 的左 / 右子树的深度最大值 $+1$ ( max(left, right) + 1 )。root 左 / 右子树的深度差 $> 1$ :则返回 $-1$ ,代表 此子树不是平衡树 。root 为空:说明越过叶节点,因此返回高度 $0$ 。函数 isBalanced(root) :
recur(root) != -1 ,则说明此树平衡,返回 $true$ ; 否则返回 $false$ 。<
,
,
,
,
,
,
,
,
,
>
###Python
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def recur(root):
if not root: return 0
left = recur(root.left)
if left == -1: return -1
right = recur(root.right)
if right == -1: return -1
return max(left, right) + 1 if abs(left - right) <= 1 else -1
return recur(root) != -1
###Java
class Solution {
public boolean isBalanced(TreeNode root) {
return recur(root) != -1;
}
private int recur(TreeNode root) {
if (root == null) return 0;
int left = recur(root.left);
if (left == -1) return -1;
int right = recur(root.right);
if (right == -1) return -1;
return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;
}
}
###C++
class Solution {
public:
bool isBalanced(TreeNode* root) {
return recur(root) != -1;
}
private:
int recur(TreeNode* root) {
if (root == nullptr) return 0;
int left = recur(root->left);
if (left == -1) return -1;
int right = recur(root->right);
if (right == -1) return -1;
return abs(left - right) < 2 ? max(left, right) + 1 : -1;
}
};
此方法容易想到,但会产生大量重复计算,时间复杂度较高。
思路是构造一个获取当前子树的深度的函数 depth(root) ,通过比较某子树的左右子树的深度差 abs(depth(root.left) - depth(root.right)) <= 1 是否成立,来判断某子树是否是二叉平衡树。若所有子树都平衡,则此树平衡。
函数 isBalanced(root) : 判断树 root 是否平衡
root 为空,则直接返回 $true$ 。abs(self.depth(root.left) - self.depth(root.right)) <= 1 :判断 当前子树 是否是平衡树。self.isBalanced(root.left) : 先序遍历递归,判断 当前子树的左子树 是否是平衡树。self.isBalanced(root.right) : 先序遍历递归,判断 当前子树的右子树 是否是平衡树。函数 depth(root) : 计算树 root 的深度
root 为空,即越过叶子节点,则返回高度 $0$ 。<
,
,
,
,
,
>
###Python
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
if not root: return True
return abs(self.depth(root.left) - self.depth(root.right)) <= 1 and \
self.isBalanced(root.left) and self.isBalanced(root.right)
def depth(self, root):
if not root: return 0
return max(self.depth(root.left), self.depth(root.right)) + 1
###Java
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) return true;
return Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
private int depth(TreeNode root) {
if (root == null) return 0;
return Math.max(depth(root.left), depth(root.right)) + 1;
}
}
###C++
class Solution {
public:
bool isBalanced(TreeNode* root) {
if (root == nullptr) return true;
return abs(depth(root->left) - depth(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
}
private:
int depth(TreeNode* root) {
if (root == nullptr) return 0;
return max(depth(root->left), depth(root->right)) + 1;
}
};
isBalanced(root) 遍历树所有节点,判断每个节点的深度 depth(root) 需要遍历 各子树的所有节点 。
depth(root) ,判断二叉树各层的节点的对应子树的深度,需遍历节点数量为 $N \times 1, \frac{N-1}{2} \times 2, \frac{N-3}{4} \times 4, \frac{N-7}{8} \times 8, ..., 1 \times \frac{N+1}{2}$ 。因此各层执行 depth(root) 的时间复杂度为 $O(N)$ (每层开始,最多遍历 $N$ 个节点,最少遍历 $\frac{N+1}{2}$ 个节点)。其中,$\frac{N-3}{4} \times 4$ 代表从此层开始总共需遍历 $N-3$ 个节点,此层共有 $4$ 个节点,因此每个子树需遍历 $\frac{N-3}{4}$ 个节点。
{:width=550}
本学习计划配有代码仓,内含测试样例与数据结构封装,便于本地调试。可前往我的个人主页获取。
github推出了 copilot chat,里面比较方便的是它可以很快速地搜索和获取各开源项目的上下文。除了单纯的Ask,其实还有执行任务的Agent(不过要开copilot pro),似乎目前是限制了每个月50次request。
![]()
因此我利用这个来学习源码,让AI回答关于源码的东西。下面的内容是出自copilot的回答(我认为比较好的),只是个人进行了整理。 这个博客的营养价值并不高,最大的价值可能就是告诉你有这个方便的途径去了解和学习源码。可以直接跳到完整示例,前面大抵只是一些铺垫。
根据官方文档,Blink 是 Chromium 的渲染引擎,负责页面内容的最终展示:
## Blink (Rendering Engine)
Blink 实现了所有在浏览器标签页内渲染内容的功能:
* 实现网络平台规范(如HTML标准),包括DOM、CSS和Web IDL
* 嵌入V8引擎并运行JavaScript
* 从底层网络栈请求资源
* **构建DOM树**
* **计算样式和布局**
* **嵌入Chrome Compositor并绘制图形**
Chromium 采用多进程架构:
// PrintRenderFrameHelper 处理渲染相关的工作
class PrintRenderFrameHelper
: public blink::WebPrintClient,
public content::RenderFrameObserver {
// 为打印准备页面框架
void OnFramePreparedForPrintPages();
void PrintPages();
bool RenderPagesForPrint(blink::WebLocalFrame* frame,
const blink::WebNode& node);
};
Blink 引擎在 DOM 树构建后,计算每个元素的样式(CSS)并确定其在页面中的位置和大小。
// 页面内容渲染到画布
void RenderPageContent(blink::WebLocalFrame* frame,
uint32_t page_index,
cc::PaintCanvas* canvas) {
TRACE_EVENT1("print", "RenderPageContent", "page_index", page_index);
frame->PrintPage(page_index, canvas);
}
Chrome Compositor 负责合成不同的图层,最终生成帧(Frame)。
Compositor 是 Chromium 的图形合成器,用于:
一个完整的渲染周期包括:
输入事件 → DOM 更新 → 样式计算 → 布局计算 → 绘制操作 → 栅格化 → 合成 → 显示
Chromium 内置性能监控:
// PDF 渲染的性能测量
if (!first_paint_metric_reported_ && !client_->IsPrintPreview()) {
first_paint_metric_reported_ = true;
base::UmaHistogramMediumTimes("PDF.FirstPaintTime",
begin_time - engine_creation_time_);
}
Chromium 的页面渲染是一个复杂的分阶段过程:
根据官方文档 life_of_a_frame.md 和代码分析,Compositor 分为以下主要部分:
输入:来自 Display Compositor 的 BeginFrame 信号
↓
输出:BeginFrameArgs(包含时间戳、帧号等)
职责:
BeginFrame
↓
[1] BeginMainFrame - 合成线程请求主线程更新
↓
输入:LayerTreeHost 配置、用户输入、动画状态
输出:BeginMainFrameAndCommitState
↓
[2] Animate - 主线程更新动画
↓
输入:当前时间、动画状态
输出:更新后的属性值
↓
[3] UpdateLayers - Blink 执行布局和绘制
↓
输入:DOM 树、样式树
输出:DisplayItemList(绘制操作)
↓
[4] Commit - 推送改变到合成线程
↓
输入:Layer 树、DisplayItemList、属性
输出:CommitState(原子性提交状态)
详细步骤:
1. Animate Phase
输入:LayerAnimationController (cc/animation/*)
输出:变换、不透明度等属性的新值
2. UpdateLayers Phase
输入:Layer 树,由 Blink 通过 LayerTreeHostClient 接口触发
- client_->UpdateLayerTreeHost()
- Blink 执行布局计算
- Blink 执行绘制,产生 DisplayItemList
输出:更新后的 Layer 树和 DisplayItemList
3. Commit Phase
- PushPropertiesTo():每个 Layer 推送属性到 LayerImpl
- 交换 Pending Tree(待定树)和 Active Tree(活动树)
- 同步动画状态、滚动状态等
这是 Compositor 的核心,分为五个关键阶段:
输入:CommitState
- Layer 树结构和属性
- DisplayItemList
- Scroll 状态
- 动画数据
处理:
- FinishCommit():合成线程接收提交的状态
- 更新 LayerImpl 树
- 更新属性树(Transform Tree、Clip Tree 等)
输出:LayerTreeImpl(合成线程的活动树)
输入:LayerTreeImpl、显示视口、缩放因子
处理:
- CalculateRasterScales():计算每个层的栅格化缩放
- PrepareTiles():
├─ CalculateLiveRects():计算可见瓦片范围
├─ AssignGpuMemoryToTiles():分配 GPU 内存预算
└─ ScheduleRasterTasks():安排栅格化任务队列
- TileManager 的职责:
├─ 优先级排序(视口内 > 视口外)
├─ 管理软件和 GPU 栅格化
├─ 管理图像解码
└─ 管理蛋糕层(cake layers)
输出:Raster Tasks(栅格化任务队列)
- 每个任务是:(Tile, RasterSource, Priority)
输入:Raster Tasks
- RasterSource(DisplayItemList 的包装)
- 目标��片大小
- 缩放因子
- 栅格化位置
处理过程(在工作线程执行):
1. PaintOpBuffer Playback
输入:DisplayItemList 中的 PaintOps
处理:
- GetOffsetsOfOpsToRaster():使用 R-Tree 查询需要的操作
- 创建 SkCanvas(CPU 或 GPU)
- 遍历相关 PaintOps,调用 Raster()
输出:像素数据或 GPU 命令
2. Software Raster(软件栅格化)
输入:PaintOps,输出大小
处理:
- 在内存中创建位图
- 使用 Skia 的 CPU 后端绘制
- 使用 SIMD 优化
输出:SkBitmap(CPU 内存中的像素)
3. GPU Raster(GPU 栅格化)
输入:PaintOps,GPU 上下文
处理:
- 序列化 PaintOps
- 通过 RasterInterface 发送到 GPU
- GPU 命令缓冲区执行绘制
- Skia 的 GPU 后端(Ganesh)处理
输出:GPU 纹理资源
输出:Rasterized Tiles
- 包含像素数据(软件)或 GPU 纹理(GPU)
关键类和函数:
// RasterSource:DisplayItemList 的包装,提供栅格化接口
class RasterSource {
void PlaybackToCanvas(
SkCanvas* raster_canvas,
const gfx::Rect& canvas_bitmap_rect, // 目标位置
const gfx::AxisTransform2d& raster_transform, // 缩放
const PlaybackSettings& settings);
};
// TileManager:协调栅格化
class TileManager {
void PrepareTiles(const PrepareTilesParams& params);
void ScheduleRasterTasks();
};
// DisplayItemList:包含 PaintOps
class DisplayItemList {
void Raster(SkCanvas* canvas, const PlaybackParams& params);
std::vector<size_t> OffsetsOfOpsToRaster(SkCanvas* canvas);
};
输入:Pending Tree(待定树,已栅格化)
处理:
- WaitForAllTilesToRasterize():等待所有关键瓦片栅格化完成
- ActivatePendingTree():
├─ Pending Tree → Active Tree
├─ 更新动画状态
├─ 更新页面缩放
└─ 清空已完成的栅格化队列
输出:Active Tree(已激活的树,可用于绘制)
输入:Active Tree、BeginFrameArgs
处理:
1. CalculateDrawProperties()
输入:Active Tree、视口、设备缩放因子
计算:
- 每个层的最终变换矩阵
- 剪裁区域
- 可见范围
输���:DrawProperties(每个 LayerImpl 都有)
2. GenerateCompositorFrame()
输入:Active Tree with DrawProperties
处理:
- 遍历 Layer 树,生成 Quads(四边形)
- 每个 Quad 包含:
├─ 纹理/资源 ID
├─ 变换矩阵
├─ 剪裁区域
├─ 不透明度
└─ 混合模式
- 为每个 Quad 创建 RenderPass
- 设置组合帧元数据
输出:CompositorFrame
3. CompositorFrame 结构
viz::CompositorFrame {
vector<RenderPass>:
- RenderPass[0]:第一个离屏渲染目标
- RenderPass[1]:第二个离屏渲染目标
- ...
- RenderPass[N]:最后一个输出到显示器的渲染通道
vector<TransferableResource>:引用的 GPU 纹理
CompositorFrameMetadata:元数据
- frame_token:帧标识符
- device_scale_factor:设备像素比
- latency_info:性能指标
- presentation_token:展示令牌
}
输出:CompositorFrame(包含所有绘制指令)
输入:CompositorFrame
处理:
1. SubmitCompositorFrame()
- 验证资源有效性
- 同步令牌处理
- 帧令牌生成
2. Viz 处理(viz/service/display/)
- AggregateSurfaces():合成多个 Surface
- ApplyFilters():应用滤镜效果
- Rasterize():最后栅格化(如果需要)
- GenerateDamageRect():计算脏区域
3. Display 合成
- 将多个来源的帧合成到最终输出表面
- 应用变换和效果
输出:最终的屏幕显示内容
┌─────────────────────────────────────────────────────────────────┐
│ MAIN THREAD (主线程) │
│ │
│ Layer Tree BeginMainFrame │
│ │ Animate │
│ │ UpdateLayers (Blink Layout & Paint) │
│ │ → DisplayItemList (PaintOps) │
│ │ │
│ └──→ Commit (PushPropertiesTo) │
│ CommitState ─────┐ │
│ │ │
└────────────────────────────┼────────────────────────────────────┘
│
┌────────▼──────────┐
│ ProxyImpl Bridge │
│ (Main ↔ Impl) │
└────────┬──────────┘
│
┌────────────────────────────▼────────────────────────────────────┐
│ COMPOSITOR THREAD (合成线程) │
│ │
│ 1. FinishCommit() │
│ CommitState → LayerTreeImpl │
│ 更新 LayerImpl 树、属性树 │
│ │
│ 2. PrepareTiles() ◄─ Scheduler 调度 │
│ ├─ 计算栅格化缩放 │
│ ├─ 分配 GPU 内存 │
│ └─ 生成 RasterTasks 队列 │
│ │ │
│ ▼ (Post to Worker Threads) │
│
│ 3. Rasterization (Worker Threads) │
│ ├─ Software: DisplayItemList → SkBitmap │
│ └─ GPU: PaintOps → GPU Textures │
│ │ │
│ ▼ │
│
│ 4. Activation() ◄─ Scheduler 调度 │
│ Pending Tree (Rasterized) → Active Tree │
│ │
│ 5. DrawFrame() / GenerateCompositorFrame() │
│ ├─ CalculateDrawProperties() │
│ ├─ BuildQuads() │
│ ├─ 生成 RenderPass 列表 │
│ └─ 输出 CompositorFrame │
│ │ │
│ ▼ │
│
│ SubmitCompositorFrame(frame) │
│ │ │
└────────────┼──────────────────────────────────────────────────┘
│
┌────────▼───────────┐
│ LayerTreeFrameSink │
│ (GPU Channel) │
└────────┬────────────┘
│
┌────────────▼──────────────────────────────────────────────────┐
│ VIZ DISPLAY COMPOSITOR (显示合成器) │
│ │
│ 1. MaybeSubmitCompositorFrame() │
│ - 验证帧 │
│ - 应用同步令牌 │
│ │
│ 2. AggregateSurfaces() │
│ - 多个 Surface 合成 │
│ - Z-order 排序 │
│ │
│ 3. Display::Draw() │
│ - GPU 驱动程序指令 │
│ - 应用滤镜和效果 │
│ │
│ 4. Swap & Present │
│ - 缓冲交换 (Backbuffer → Frontbuffer) │
│ - VSync 同步 │
│ - 显示器显示 │
│ │
└──────────────────────────────────────────────────────────────┘
| 阶段 | 输入 | 处理器 | 输出 | 执行线程 |
|---|---|---|---|---|
| Animate | AnimationState | LayerAnimationController | 属性值 | Main |
| UpdateLayers | DOM 树、样式 | Blink | DisplayItemList | Main |
| Commit | Layer 树 | LayerTreeHost | CommitState | Main→Impl |
| FinishCommit | CommitState | LayerTreeHostImpl | LayerTreeImpl | Impl |
| PrepareTiles | LayerTreeImpl | TileManager | RasterTasks | Impl |
| Rasterization | RasterTasks | RasterWorkerPool | GPU 纹理/位图 | Worker |
| Activation | Pending Tree | LayerTreeHostImpl | Active Tree | Impl |
| DrawFrame | Active Tree | LayerTreeHostImpl | CompositorFrame | Impl |
| Display | CompositorFrame | DisplayCompositor | 屏幕输出 | Display |
ProxyMain (主线程)
├─ 负责与主线程通信
├─ 接收 BeginMainFrame 信号
├─ 管理 CommitPipelineStage
└─ 回调主线程结果
ProxyImpl (合成线程)
├─ 负责与合成线程通信
├─ 控制 Scheduler
├─ 管理 LayerTreeHostImpl
└─ 提交 CompositorFrame 到 LayerTreeFrameSink
让我们假设要在网页上绘制一个蓝色的 200×200 像素正方形,位置在 (100, 100)。
用户在 HTML/CSS 中写了:
<div style="
width: 200px;
height: 200px;
background-color: blue;
position: absolute;
top: 100px;
left: 100px;
"></div>
Blink 布局完成后,知道要绘制:
然后它创建一个 DisplayItemList 来记录这个绘制操作:
// Blink 在主线程上执行
auto display_list = base::MakeRefCounted<cc::DisplayItemList>();
display_list->StartPaint();
// 1. 保存图形状态
display_list->push<cc::SaveOp>();
// 2. 绘制蓝色正方形
cc::PaintFlags blue_flags;
blue_flags.setColor(SK_ColorBLUE); // RGB(0, 0, 255)
blue_flags.setStyle(SkPaint::kFill_Style);
display_list->push<cc::DrawRectOp>(
SkRect::MakeXYWH(100, 100, 200, 200), // x, y, width, height
blue_flags
);
// 3. 恢复图形状态
display_list->push<cc::RestoreOp>();
display_list->EndPaintOfUnpaired(gfx::Rect(100, 100, 200, 200));
// 完成记录
display_list->Finalize();
// 此时 DisplayItemList 内部包含:
//
// paint_op_buffer_ = [
// SaveOp,
// DrawRectOp(x=100, y=100, w=200, h=200, color=blue),
// RestoreOp
// ]
//
// visual_rects_ = [
// {100, 100, 200, 200}, // SaveOp 的可视范围
// {100, 100, 200, 200}, // DrawRectOp 的可视范围
// {100, 100, 200, 200} // RestoreOp 的可视范围
// ]
这个阶段的输出: 一个包含"绘制指令"的对象,说明要在 (100,100) 位置绘制一个 200×200 的蓝色矩形。但这只是指令,还没有真正的像素数据!
主线程把 DisplayItemList 包装在 CommitState 中,发送给合成线程:
// 主线程创建 CommitState
auto commit_state = std::make_unique<CommitState>();
// 把 DisplayItemList 分配给对应的 Layer
// (在实际代码中,DisplayItemList 被存储在 PictureLayerImpl 中)
commit_state->source_frame_number = 120;
commit_state->device_viewport_rect = gfx::Size(1920, 1080);
commit_state->device_scale_factor = 1.0f; // 假设普通屏幕
commit_state->background_color = SK_ColorWHITE;
// 推送到合成线程
layer_tree_host_->WillCommit(...)
// ...
layer_tree_host_->ActivateCommitState() // 原子性推送
这个阶段的输出: CommitState 对象,包含所有需要的信息,包括 DisplayItemList。
合成线程收到 CommitState,开始准备栅格化:
// 合成线程上
LayerTreeHostImpl* host_impl = ...;
// 1. 接收 CommitState,更新 LayerTreeImpl
host_impl->FinishCommit(commit_state);
// 2. 准备瓦片(Tiles)
TileManager* tile_manager = host_impl->tile_manager();
tile_manager->PrepareTiles();
// 这会为 PictureLayerImpl 创建 Tiles
// 因为我们的正方形只有 200×200,可能只需要 1 个 Tile
// 假设 Tile 大小是 256×256
struct Tile {
gfx::Rect rect; // 瓦片在页面上的位置
RasterSource* source; // 指向 DisplayItemList (包装后)
float scale; // 栅格化缩放因子
int x, y; // 瓦片的网格坐标
};
// 创建一个瓦片
Tile tile0{
.rect = gfx::Rect(0, 0, 256, 256), // 页面坐标
.source = raster_source, // 包含我们的 DisplayItemList
.scale = 1.0f,
.x = 0,
.y = 0
};
// TileManager 会创建栅格化任务
struct RasterTask {
Tile* tile;
RasterSource* raster_source;
gfx::Rect target_rect; // 在瓦片中的位置
Priority priority;
};
RasterTask task{
.tile = &tile0,
.raster_source = raster_source,
.target_rect = gfx::Rect(0, 0, 256, 256),
.priority = PRIORITY_NOW // 视口内,需要立即栅格化
};
// 提交栅格化任务给工作线程
tile_manager->ScheduleRasterTasks(&task);
这个阶段的输出: 栅格化任务队列,告诉工作线程要栅格化哪些区域。
这是把绘制指令转换成实际像素的地方:
// 工作线程上执行栅格化任务
void RasterWorkerPool::RasterizeTask(const RasterTask& task) {
// 创建一个绘制目标(画布)
// 这是一个 256×256 的缓冲区,用来存放栅格化后的像素
// 方案 A:CPU 栅格化(软件)
if (use_software_raster) {
// 创建 CPU 内存中的位图
SkBitmap bitmap;
bitmap.allocN32Pixels(256, 256); // 256×256 的 32 位 RGBA 像素
// 创建 Skia ��布,指向这个位图
SkCanvas canvas(bitmap);
// 现在我们要回放 DisplayItemList 中的绘制指令
RasterSource* source = task.raster_source;
// 关键步骤:回放绘制操作!
source->PlaybackToCanvas(
&canvas, // 目标画布
gfx::Size(1920, 1080), // 内容大小
gfx::Rect(0, 0, 256, 256), // 瓦片在内容中的位置
gfx::Rect(0, 0, 256, 256), // 需要栅格化的区域
gfx::AxisTransform2d(1.0f), // 无缩放
settings
);
// 现在 bitmap 中包含了栅格化后的像素!
// 具体来说:
// - 位置 (100, 100) 到 (300, 300) 的像素被设为蓝色
// - 其他像素是白色(背景色)
// bitmap 的内存布局示意:
//
// 位置 (0,0) ──────────────────────→ (256,0)
// │ FFFFFF FFFFFF FFFFFF ...
// │ FFFFFF FFFFFF FFFFFF ...
// │ ...
// (100,100) 开始
// │ FFFFFF FFFFFF ...
// │ ...
// │ FFFFFF 0000FF 0000FF 0000FF ... ← 蓝色像素!
// │ FFFFFF 0000FF 0000FF 0000FF ...
// │ FFFFFF 0000FF 0000FF 0000FF ...
// │ ...
// (300,300) 结束
// │ FFFFFF FFFFFF FFFFFF ...
// ↓
// (256,256)
// 上传到 GPU
UploadBitmapToGPU(&bitmap, tile);
}
// 方案 B:GPU 栅格化
else {
// 创建 GPU 纹理作为渲染目标
// 尺寸:256×256,格式:RGBA_8888
GLuint texture = gl::CreateTexture(256, 256, GL_RGBA8);
// 绑定为渲染目标
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, texture, 0);
// 清除背景(白色)
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 创建 Skia 画布(绘制到 GPU)
SkSurface* surface = SkSurface::MakeFromBackendTexture(...);
SkCanvas* canvas = surface->getCanvas();
// 回放 DisplayItemList 的绘制操作
// 这次是发送 GPU 命令
DisplayItemList::Raster(canvas, ...);
// GPU 执行指令,在纹理中画出蓝色正方形
// 纹理内容现在是:位置 (100, 100) 到 (300, 300) 是蓝色像素
}
}
这个阶段发生了什么:
具体的像素数据示意:
CPU 内存(SkBitmap)或 GPU 纹理的内容
(每个方块代表一个像素,用 16 进制表示 RGBA 颜色值)
y=0 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ... (全是白色)
y=1 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
...
y=99 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
y=100 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
^ ^ (100,100) 蓝色开始 ^ (300,100) 蓝色结束
y=101 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
y=102 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
...
y=299 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
y=300 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
^ ^ (100,300) 蓝色最后一行 ^ (300,300) 蓝色结束
y=301 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
...
y=255 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
其中:
- FFFFFFFF = 白色 (RGBA: 255,255,255,255)
- 0000FFFF = 蓝色 (RGBA: 0,0,255,255)
这个阶段的输出: GPU 纹理或位图,包含 256×256 个像素的实际颜色数据。
现在我们有了栅格化后的纹理,合成线程创建最终的合成帧:
// 合成线程
void LayerTreeHostImpl::GenerateCompositorFrame() {
// 创建 CompositorFrame
viz::CompositorFrame frame;
// 设置元数据
frame.metadata.frame_token = 0x12345;
frame.metadata.device_scale_factor = 1.0f;
frame.metadata.size_in_pixels = gfx::Size(1920, 1080);
frame.metadata.begin_frame_ack = viz::BeginFrameAck(...);
// 创建渲染通道
auto render_pass = viz::CompositorRenderPass::Create();
render_pass->SetNew(
viz::CompositorRenderPassId(1),
gfx::Rect(0, 0, 1920, 1080), // 输出矩形(整个屏幕)
gfx::Rect(100, 100, 200, 200), // 脏区域(只有正方形区域需要重绘)
gfx::Transform() // 无变换
);
// 添加 Quad(纹理四边形)
auto quad = std::make_unique<viz::TextureDrawQuad>();
// 从栅格化任务中获取纹理 ID
ResourceId texture_id = 0x9999; // GPU 纹理的句柄
quad->SetNew(
nullptr, // shared_quad_state (共享状态)
gfx::Rect(0, 0, 256, 256), // Quad 在屏幕上的位置(注意:这是以瓦片的 (0,0) 开始)
gfx::Rect(0, 0, 256, 256), // 可见区域
false, // 不需要混合
texture_id, // 纹理 ID(指向我们栅格化的蓝色正方形纹理)
true, // 预乘 alpha
gfx::PointF(0, 0), // UV 左上角
gfx::PointF(1, 1), // UV 右下角
SK_ColorWHITE, // 背景色
{1.0f, 1.0f, 1.0f, 1.0f} // 混合颜色
);
// 重要!实际位置需要从瓦片坐标转换
// 瓦片 (0,0) 对应屏幕位置 (0,0)
// 但我们的正方形在 DisplayItemList 中是 (100, 100)
// 所以最终 Quad 的 rect 应该是 (100, 100, 200, 200)
// 更正:
quad->rect = gfx::Rect(100, 100, 200, 200); // 正确的屏幕位置
quad->visible_rect = gfx::Rect(100, 100, 200, 200);
quad->opacity = 1.0f;
render_pass->quad_list.push_back(std::move(quad));
// 添加资源
viz::TransferableResource resource;
resource.id = texture_id;
resource.mailbox_holder = gpu::MailboxHolder(mailbox, sync_token, target);
frame.resource_list.push_back(resource);
// 添加渲染通道
frame.render_pass_list.push_back(std::move(render_pass));
return frame;
}
// CompositorFrame 现在包含:
// {
// metadata: {
// frame_token: 0x12345,
// device_scale_factor: 1.0,
// size_in_pixels: (1920, 1080),
// ...
// },
// render_pass_list: [
// {
// output_rect: (0, 0, 1920, 1080),
// damage_rect: (100, 100, 200, 200),
// quad_list: [
// TextureDrawQuad {
// rect: (100, 100, 200, 200),
// texture_id: 0x9999,
// opacity: 1.0,
// ...
// }
// ]
// }
// ],
// resource_list: [
// {
// id: 0x9999,
// mailbox: ... (指向包含栅格化像素的 GPU 纹理)
// }
// ]
// }
这个阶段的输出: CompositorFrame,包含:
// Viz Display Compositor(显示合成器)
void Display::DrawFrame(...) {
// 1. 接收 CompositorFrame
viz::CompositorFrame frame = ...;
// 2. 遍历所有 Quads
for (auto& render_pass : frame.render_pass_list) {
for (auto& quad : render_pass->quad_list) {
if (auto texture_quad = quad.As<viz::TextureDrawQuad>()) {
// 3. 获取纹理(包含栅格化的蓝色正方形像素)
GLuint texture = GetTextureFromResourceId(texture_quad->resource_id);
// 4. 在屏幕上绘制这个纹理
// 位置:(100, 100)
// 大小:200×200
// 内容:我们栅格化的蓝色正方形纹理
glBindTexture(GL_TEXTURE_2D, texture);
glUniform2f(position_uniform, 100.0f, 100.0f);
glUniform2f(size_uniform, 200.0f, 200.0f);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 绘制四边形
}
}
}
// 5. 交换缓冲区
SwapBuffers();
// GPU 把缓冲区中的像素显示到屏幕上
}
这个阶段发生了什么:
┌─────────────────────────────────────────────────────────────┐
│ 1. HTML/CSS 描述 │
│ <div style="width:200px; height:200px; │
│ background-color:blue; top:100px; left:100px;">
└─────────────────────────────────────────────────────────────┘
↓ (Blink 布局)
┌─────────────────────────────────────────────────────────────┐
│ 2. DisplayItemList (绘制指令,不是像素) │
│ [SaveOp, ││ DrawRectOp(x=100, y=100, w=200, h=200, color=blue), ││ RestoreOp] │
└─────────────────────────────────────────────────────────────┘
↓ (提交)
┌─────────────────────────────────────────────────────────────┐
│ 3. CommitState │
│ (包含 DisplayItemList 和其他属性) │
└─────────────────────────────────────────────────────────────┘
↓ (栅格化)
┌─────────────────────────────────────────────────────────────┐
│ 4. GPU 纹理(实际像素)256×256 │
│ FFFFFF... FFFFFF... 蓝色(0000FF)... 蓝色... FFFFFF... │
│ (100,100) 到 (300,300) 的像素是蓝色,其他是白色 │
└─────────────────────────────────────────────────────────────┘
↓ (生成帧)
┌─────────────────────────────────────────────────────────────┐
│ 5. CompositorFrame │
│ quad: { │
│ position: (100, 100), │
│ size: (200, 200), │
│ texture_id: 0x9999 → 指向包含蓝色像素的纹理 │
│ } │
└─────────────────────────────────────────────────────────────┘
↓ (GPU 绘制)
┌─────────────────────────────────────────────────────────────┐
│ 6. 屏幕帧缓冲(1920×1080) │
│ (100, 100) 到 (300, 300) 的像素现在是蓝色 │
│ 其他像素是白色 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 白白白白白... 白白 │ │
│ │ 白白白白白... 白白 │ │
│ │ ... │ │
│ │ 白白蓝蓝蓝... 蓝白 │ ← (100,100) │
│ │ 白白蓝蓝蓝... 蓝白 │ │
│ │ 白白蓝蓝蓝... 蓝白 │ │
│ │ ... │ │
│ │ 白白蓝蓝蓝... 蓝白 │ ← (300,300) │
│ │ 白白白白白... 白白 │ │
│ └──────────────────────────────────┘ │
│ │
│ ✓ 正方形显示在屏幕上! │
└─────────────────────────────────────────────────────────────┘
整个过程就像:
好问题!栅格化是整个过程中最有趣的部分。让我用最直观的方式解释它。太好了!现在我可以用实际的代码来解释栅格化的原理。让我用一个最简单的蓝色正方形例子详细讲解:
栅格化的秘密就是:Chromium 使用一个叫 Skia 的图形库,它能把绘制指令转换成像素。
想象 Skia 就像一个虚拟画家:
// 工作线程上执行
// 假设我们要栅格化一个 256×256 的瓦片
// 方案 A:CPU 栅格化(软件方式)
// ====================================
// 创建一个 256×256 的位图(像素缓冲区)
SkBitmap bitmap;
bitmap.allocN32Pixels(256, 256);
// 现在 bitmap 中有 256*256 = 65536 个像素
// 每个像素 4 字节(RGBA),共 262,144 字节的内存
// 内存布局示意:
// bitmap.getPixels() 返回一个指向这块内存的指针
// 内存中的数据:
// [像素(0,0)的RGBA] [像素(1,0)的RGBA] [像素(2,0)的RGBA] ...
// [像素(0,1)的RGBA] [像素(1,1)的RGBA] [像素(2,1)的RGBA] ...
// ...
// [像素(255,255)的RGBA]
// 创建一个 Skia 画布,指向这个位图
SkCanvas canvas(bitmap);
// 方案 B:GPU 栅格化
// ====================================
// 创建一个 GPU 纹理作为渲染目标
GLuint framebuffer = CreateFramebuffer();
GLuint texture = CreateTexture(256, 256, GL_RGBA8);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, texture, 0);
// 创建指向这个 GPU 纹理的 Skia 画布
SkSurface* surface = SkSurface::MakeFromBackendTexture(...);
SkCanvas* canvas = surface->getCanvas();
现在我们有了一个空白画布,所有像素都是未初始化的。
// 把所有像素设置为白色(背景色)
canvas->clear(SK_ColorWHITE);
// 底层发生了什么:
// Skia 填充整个 256×256 的像素区域
// 对于每一个像素 (x, y),设置它的值为:
// [R=255, G=255, B=255, A=255] // 白色 RGBA
// 清除后的内存示意:
// [FFFFFFFF] [FFFFFFFF] [FFFFFFFF] ... (全是 0xFFFFFFFF = 白色)
// 总共 65536 个这样的 4 字节值
现在所有像素都是白色。
这是最关键的部分!
// RasterSource::PlaybackDisplayListToCanvas()
void PlaybackDisplayListToCanvas(SkCanvas* raster_canvas,
const PlaybackSettings& settings) {
// 获取 DisplayItemList
DisplayItemList* display_list = ...; // 包含我们的绘制指令
// 创建回放参数
PlaybackParams params(settings.image_provider, SkM44());
// 关键步骤:回放 DisplayItemList
display_list->Raster(raster_canvas, params);
}
// DisplayItemList::Raster() 的实现
void DisplayItemList::Raster(SkCanvas* canvas,
const PlaybackParams& params) const {
// 1. 获取需要绘制的操作的偏移量
// (使用 R-Tree 优化:只获取与画布相交的操作)
std::vector<size_t> offsets = OffsetsOfOpsToRaster(canvas);
// offsets = [0, 8, 24] // SaveOp, DrawRectOp, RestoreOp 的偏移量
// 2. 遍历 paint_op_buffer_ 中的操作,执行它们
paint_op_buffer_.Playback(canvas, params, true, &offsets);
}
// PaintOpBuffer::Playback() 的实现
void PaintOpBuffer::Playback(SkCanvas* canvas,
const PlaybackParams& params,
bool local_ctm,
const std::vector<size_t>* offsets) const {
// 遍历所有操作
for (const PaintOp& op : PaintOpBuffer::OffsetIterator(*this, offsets)) {
// 对于每一个操作,调用它的 Raster() 函数
op.Raster(canvas, params);
// 这就是魔法发生的地方!
// Raster() 是虚函数,每个 PaintOp 子类有自己的实现
}
}
现在让我们看看具体的操作执行:
// DisplayItemList 中有这些操作(序列化的形式):
// [SaveOp] [DrawRectOp(...)] [RestoreOp]
// ============================================
// 操作 1:SaveOp::Raster()
// ============================================
void SaveOp::Raster(const SaveOp* op,
SkCanvas* canvas,
const PlaybackParams& params) {
canvas->save();
// 这保存了当前的图形状态(颜色、变换、剪裁等)
}
// ============================================
// 操作 2:DrawRectOp::Raster()(最关键!)
// ============================================
void DrawRectOp::RasterWithFlags(const DrawRectOp* op,
const PaintFlags* flags,
SkCanvas* canvas,
const PlaybackParams& params) {
// op->rect = SkRect{x=100, y=100, w=200, h=200}
// flags->color = SK_ColorBLUE = 0x0000FFFF (RGBA)
// 这是最关键的调用!
canvas->drawRect(op->rect, *flags);
// Skia 会在这一刻做什么?
// 它会遍历要填充的矩形中的每一个像素,设置它们的颜色为蓝色
}
// Skia 内部如何执行 drawRect?
// ====================================
// 伪代码,大致思路:
void SkCanvas::drawRect(const SkRect& rect, const SkPaint& paint) {
// 1. 将 SkRect 转换为像素坐标
int x1 = (int)rect.left(); // 100
int y1 = (int)rect.top(); // 100
int x2 = (int)rect.right(); // 300
int y2 = (int)rect.bottom(); // 300
SkColor color = paint.getColor(); // 0x0000FFFF (蓝色)
// 2. 遍历矩形内的所有像素
for (int y = y1; y < y2; y++) {
for (int x = x1; x < x2; x++) {
// 3. 对于每一个像素,写入蓝色颜色值
uint32_t* pixel_ptr = bitmap.getAddr32(x, y);
*pixel_ptr = color; // 设置为 0x0000FFFF
}
}
// 实际上 Skia 做的更复杂(处理反锯齿、混合等),
// 但核心思想就是这样:写入像素值
}
// 栅格化后的内存状态:
// y=0 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ... (全是白色)
// y=99 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
// y=100 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF... (开始有蓝色!)
// ^ ^ ^
// 0 100 100-300 是蓝色
// y=101 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF...
// ...
// y=299 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF...
// y=300 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF...
// y=301 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ... (又都是白色了)
// ============================================
// 操作 3:RestoreOp::Raster()
// ============================================
void RestoreOp::Raster(const RestoreOp* op,
SkCanvas* canvas,
const PlaybackParams& params) {
canvas->restore();
// 恢复之前保存的图形状态
}
经过这三个操作,我们已经有了一个完整的栅格化结果!
// 开始栅格化
// 输入 1:DisplayItemList(绘制指令)
DisplayItemList {
paint_op_buffer: [
SaveOp,
DrawRectOp(
rect={x:100, y:100, w:200, h:200},
color=0x0000FFFF // 蓝色
),
RestoreOp
],
visual_rects: [{100,100,200,200}, ...]
}
// 输入 2:空白画布(256×256 的位图)
SkBitmap bitmap {
width: 256,
height: 256,
pixels: [
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 0 行(256 个白色像素)
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 1 行
...
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF // 第 255 行
]
}
// ============================================================
// 执行回放
// ============================================================
canvas->save();
canvas->drawRect(SkRect{100,100,200,200}, blue_paint);
canvas->restore();
// 栅格化后的输出:位图内容被修改了
SkBitmap bitmap {
width: 256,
height: 256,
pixels: [
// y=0 到 y=99:全是白色
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 0 行
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 1 行
...
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 99 行
// y=100 到 y=299:中间是蓝色,边缘是白色
0xFFFFFFFF, ..., 0xFFFFFFFF, 0x0000FFFF, 0x0000FFFF, ..., 0x0000FFFF, 0xFFFFFFFF, ...,
// |
// x=0 到 99 100 到 299(蓝色区域) 300 到 255
0xFFFFFFFF, ..., 0xFFFFFFFF, 0x0000FFFF, 0x0000FFFF, ..., 0x0000FFFF, 0xFFFFFFFF, ...,
// ... 重复 200 次(y=100 到 y=299)
// y=300 到 y=255:全是白色
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 300 行
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 301 行
...
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF // 第 255 行
]
}
关键是 Skia 库的设计:
// Skia 的核心就是提供一个虚拟的 SkCanvas
// 无论你是在 CPU 上还是 GPU 上绘制,接口都是一样的
class SkCanvas {
public:
// 这些方法有多种实现:
// - CPU 实现:直接修改像素缓冲区
// - GPU 实现:生成 GPU 指令
virtual void drawRect(const SkRect& rect, const SkPaint& paint);
virtual void drawPath(const SkPath& path, const SkPaint& paint);
virtual void drawImage(const SkImage* image, ...);
// ... 等等
};
// CPU 版本的 drawRect(简化)
class SkRasterCanvas : public SkCanvas {
void drawRect(const SkRect& rect, const SkPaint& paint) override {
// 直接操作 fBitmap 的像素
uint32_t* pixels = fBitmap.getPixels();
for (int y = rect.top(); y < rect.bottom(); y++) {
for (int x = rect.left(); x < rect.right(); x++) {
int index = y * width + x;
pixels[index] = paint.getColor(); // 设置像素颜色
}
}
}
};
// GPU 版本的 drawRect(简化)
class SkGPUCanvas : public SkCanvas {
void drawRect(const SkRect& rect, const SkPaint& paint) override {
// 生成 GPU 指令
glBindFramebuffer(GL_FRAMEBUFFER, fFramebuffer);
glClearColor(paint.getColor());
glClear(GL_COLOR_BUFFER_BIT);
// ... 等等
}
};
总结:
canvas->drawRect()这就是为什么能把绘制指令转换成像素!
void RasterSource::PlaybackDisplayListToCanvas(
SkCanvas* raster_canvas,
const PlaybackSettings& settings) const {
CHECK(display_list_);
// 创建回放参数
PlaybackParams params(settings.image_provider, SkM44());
params.raster_inducing_scroll_offsets =
settings.raster_inducing_scroll_offsets;
params.destination_hdr_headroom = settings.hdr_headroom;
// 核心:回放 DisplayItemList
// 这会遍历所有 PaintOps 并执行它们
display_list_->Raster(raster_canvas, params);
}
// 这就是一切!
// display_list_->Raster() 会:
// 1. 遍历 DisplayItemList 中的每个 PaintOp
// 2. 对于每个 PaintOp,调用它的 Raster() 方法
// 3. 每个 Raster() 方法使用 Skia canvas 修改像素
// 4. 最后得到一个填满颜色的位图
大家好,我是1024小神,想进 技术群 / 私活群 / 股票群 或 交朋友都可以私信我,如果你觉得本文有用,一键三连 (点赞、评论、关注),就是对我最大的支持~
![]()
发布GitHub pages会有不同的几种状态,可以参考
409:表示已经发布过了
{
"message": "GitHub Pages is already enabled.",
"documentation_url": "https://docs.github.com/rest/pages/pages#create-a-apiname-pages-site",
"status": "409"
}
errored:表示发布失败
通常是发布过后的pages静态文件更新后,会自动重新发布,比如总的更新文件是3个,但是更新了1个之后就会出触发自动更新,但是第二个紧跟着就更新了,就会再次触发更新,这个时候第一个更新状态就会变成errored
{
"url": "https://api.github.com/repos/xxxxxx/xxxxxxx/pages",
"status": "errored",
"cname": null,
"custom_404": false,
"html_url": "https://xxxxxxx.github.io/xxxxxxx7/",
"build_type": "legacy",
"source": {
"branch": "main",
"path": "/docs"
},
"public": true,
"protected_domain_state": null,
"pending_domain_unverified_at": null,
"https_enforced": true
}
status:null 说明正在发布pages
{
"url": "https://api.github.com/repos/1024xiaoshen/PakePlus-Android-v2.1.6/pages",
"status": null,
"cname": null,
"custom_404": false,
"html_url": "https://1024xiaoshen.github.io/PakePlus-Android-v2.1.6/",
"build_type": "legacy",
"source": {
"branch": "main",
"path": "/docs"
},
"public": true,
"protected_domain_state": null,
"pending_domain_unverified_at": null,
"https_enforced": true
}
built:表示已经发布好了
{
"url": "https://api.github.com/repos/1024xiaoshen/PakePlus-Android-v2.1.6/pages",
"status": "built",
"cname": null,
"custom_404": false,
"html_url": "https://1024xiaoshen.github.io/PakePlus-Android-v2.1.6/",
"build_type": "legacy",
"source": {
"branch": "main",
"path": "/docs"
},
"public": true,
"protected_domain_state": null,
"pending_domain_unverified_at": null,
"https_enforced": true
}
如果你有好的想法或需求,都可以私信我,我这里有很多程序员朋友喜欢用代码来创造丰富多彩的计算机世界有人说投资现在的越南,就好比投资十几年前的中国,越南是全世界最年轻的国家之一。本期视频我们来讲一讲越南,越南近现代的历史是怎样的?为什么会起步晚了几十年?是什么样的契机让它获得了全球资本的青睐?越南未来的发展前景如何?
下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动
本文是 blade-code 技术深度系列的第 1 篇,深入剖析如何从零实现一个生产级的 MCP(Model Context Protocol)客户端,包括连接管理、OAuth 认证、健康监控、工具注册等核心功能。
MCP(Model Context Protocol)是 Anthropic 推出的开放协议,用于 AI 应用与外部工具/数据源的标准化通信。它解决了以下问题:
MCP 提供了:
blade-code 的 MCP 集成采用三层架构:
graph TB
A[Agent Runtime 调用层] --> B[McpRegistry 管理层]
B --> C[McpClient 通信层]
A1[工具调用<br/>参数验证] -.-> A
B1[服务器注册/注销<br/>工具冲突处理<br/>状态监控] -.-> B
C1[连接管理<br/>协议通信<br/>错误重试<br/>OAuth 认证] -.-> C
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#ffe1f5
设计原则:
McpClient 是 MCP 集成的核心,负责与单个 MCP 服务器的通信。
MCP 支持三种传输方式,blade-code 通过工厂模式统一创建:
private async createTransport(): Promise<Transport> {
const { type, command, args, env, url, headers } = this.config;
if (type === 'stdio') {
// 子进程通信(本地工具)
return new StdioClientTransport({
command,
args: args || [],
env: { ...process.env, ...env },
stderr: 'ignore', // 忽略子进程的 stderr 输出
});
} else if (type === 'sse') {
// Server-Sent Events(远程服务)
return new SSEClientTransport(new URL(url), {
requestInit: { headers },
});
} else if (type === 'http') {
// HTTP 长轮询
const { StreamableHTTPClientTransport } = await import(
'@modelcontextprotocol/sdk/client/streamableHttp.js'
);
return new StreamableHTTPClientTransport(new URL(url), {
requestInit: { headers },
});
}
throw new Error(`不支持的传输类型: ${type}`);
}
关键点:
stdio 适合本地工具(如文件系统、数据库)sse 适合远程服务(实时推送)http 适合 RESTful API生产环境中,网络不稳定是常态。blade-code 实现了智能重试机制:
async connectWithRetry(maxRetries = 3, initialDelay = 1000): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.doConnect();
this.reconnectAttempts = 0; // 重置重连计数
return; // 成功连接
} catch (error) {
lastError = error as Error;
const classified = classifyError(error);
// 如果是永久性错误,不重试
if (!classified.isRetryable) {
console.error('[McpClient] 检测到永久性错误,放弃重试:', classified.type);
throw error;
}
// 指数退避
if (attempt < maxRetries) {
const delay = initialDelay * Math.pow(2, attempt - 1);
console.warn(`[McpClient] 连接失败(${attempt}/${maxRetries}),${delay}ms 后重试...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError || new Error('连接失败');
}
错误分类:
enum ErrorType {
NETWORK_TEMPORARY = 'network_temporary', // 临时网络错误(可重试)
NETWORK_PERMANENT = 'network_permanent', // 永久网络错误
CONFIG_ERROR = 'config_error', // 配置错误
AUTH_ERROR = 'auth_error', // 认证错误
PROTOCOL_ERROR = 'protocol_error', // 协议错误
UNKNOWN = 'unknown', // 未知错误
}
function classifyError(error: unknown): ClassifiedError {
const msg = error.message.toLowerCase();
// 永久性配置错误(不应重试)
const permanentErrors = [
'command not found',
'no such file',
'permission denied',
'invalid configuration',
];
if (permanentErrors.some((permanent) => msg.includes(permanent))) {
return { type: ErrorType.CONFIG_ERROR, isRetryable: false, originalError: error };
}
// 临时网络错误(可重试)
const temporaryErrors = [
'timeout',
'connection refused',
'econnreset',
'etimedout',
'503',
'429',
];
if (temporaryErrors.some((temporary) => msg.includes(temporary))) {
return { type: ErrorType.NETWORK_TEMPORARY, isRetryable: true, originalError: error };
}
// 默认视为临时错误(保守策略:允许重试)
return { type: ErrorType.UNKNOWN, isRetryable: true, originalError: error };
}
为什么这样设计?
MCP 服务器可能随时断开(进程崩溃、网络中断),blade-code 通过监听 onclose 事件自动重连:
this.sdkClient.onclose = () => {
this.handleUnexpectedClose();
};
private handleUnexpectedClose(): void {
if (this.isManualDisconnect) {
return; // 手动断开,不重连
}
if (this.status === McpConnectionStatus.CONNECTED) {
console.warn('[McpClient] 检测到意外断连,准备重连...');
this.setStatus(McpConnectionStatus.ERROR);
this.emit('error', new Error('MCP服务器连接意外关闭'));
this.scheduleReconnect();
}
}
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
console.error('[McpClient] 达到最大重连次数,放弃重连');
this.emit('reconnectFailed');
return;
}
// 指数退避:1s, 2s, 4s, 8s, 16s(最大30s)
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
this.reconnectTimer = setTimeout(async () => {
try {
await this.doConnect();
console.log('[McpClient] 重连成功');
this.reconnectAttempts = 0;
this.emit('reconnected');
} catch (error) {
const classified = classifyError(error);
if (classified.isRetryable) {
this.scheduleReconnect(); // 继续重连
} else {
this.emit('reconnectFailed'); // 永久失败
}
}
}, delay);
}
关键点:
McpRegistry 是单例模式的注册中心,管理多个 MCP 服务器。
async registerServer(name: string, config: McpServerConfig): Promise<void> {
if (this.servers.has(name)) {
throw new Error(`MCP服务器 "${name}" 已经注册`);
}
const client = new McpClient(config, name, config.healthCheck);
const serverInfo: McpServerInfo = {
config,
client,
status: McpConnectionStatus.DISCONNECTED,
tools: [],
};
// 设置客户端事件处理器
this.setupClientEventHandlers(client, serverInfo, name);
this.servers.set(name, serverInfo);
this.emit('serverRegistered', name, serverInfo);
try {
await this.connectServer(name);
} catch (error) {
console.warn(`MCP服务器 "${name}" 连接失败:`, error);
}
}
多个 MCP 服务器可能提供同名工具,blade-code 通过前缀解决冲突:
async getAvailableTools(): Promise<Tool[]> {
const tools: Tool[] = [];
const nameConflicts = new Map<string, number>();
// 第一遍:检测冲突
for (const [_serverName, serverInfo] of this.servers) {
if (serverInfo.status === McpConnectionStatus.CONNECTED) {
for (const mcpTool of serverInfo.tools) {
const count = nameConflicts.get(mcpTool.name) || 0;
nameConflicts.set(mcpTool.name, count + 1);
}
}
}
// 第二遍:创建工具(冲突时添加前缀)
for (const [serverName, serverInfo] of this.servers) {
if (serverInfo.status === McpConnectionStatus.CONNECTED) {
for (const mcpTool of serverInfo.tools) {
const hasConflict = (nameConflicts.get(mcpTool.name) || 0) > 1;
const toolName = hasConflict
? `${serverName}__${mcpTool.name}`
: mcpTool.name;
const tool = createMcpTool(serverInfo.client, serverName, mcpTool, toolName);
tools.push(tool);
}
}
}
return tools;
}
命名策略:
toolName
serverName__toolName
示例:
服务器 A: read_file
服务器 B: read_file
→ 最终工具: A__read_file, B__read_file
MCP 支持 OAuth 2.0 认证,blade-code 实现了完整的 OAuth 流程。
export class OAuthTokenStorage {
private readonly tokenFilePath: string;
constructor() {
const homeDir = os.homedir();
const configDir = path.join(homeDir, '.blade');
this.tokenFilePath = path.join(configDir, 'mcp-oauth-tokens.json');
}
async saveToken(
serverName: string,
token: OAuthToken,
clientId?: string,
tokenUrl?: string
): Promise<void> {
const credentials = await this.loadAllCredentials();
const credential: OAuthCredentials = {
serverName,
token,
clientId,
tokenUrl,
updatedAt: Date.now(),
};
credentials.set(serverName, credential);
await this.saveAllCredentials(credentials);
}
isTokenExpired(token: OAuthToken): boolean {
if (!token.expiresAt) {
return false; // 没有过期时间,认为不过期
}
// 提前 5 分钟视为过期,留出刷新时间
const buffer = 5 * 60 * 1000;
return Date.now() >= token.expiresAt - buffer;
}
}
安全措施:
0o600(仅所有者可读写)export class OAuthProvider {
private tokenStorage = new OAuthTokenStorage();
async getValidToken(
serverName: string,
oauthConfig: OAuthConfig
): Promise<string | null> {
const credentials = await this.tokenStorage.getCredentials(serverName);
if (!credentials) {
return null; // 没有令牌,需要认证
}
// 检查是否过期
if (this.tokenStorage.isTokenExpired(credentials.token)) {
// 尝试刷新
if (credentials.token.refreshToken) {
try {
const newToken = await this.refreshToken(credentials, oauthConfig);
await this.tokenStorage.saveToken(
serverName,
newToken,
credentials.clientId,
credentials.tokenUrl
);
return newToken.accessToken;
} catch (error) {
console.error('[OAuthProvider] 刷新令牌失败:', error);
return null; // 刷新失败,需要重新认证
}
}
return null; // 没有 refresh_token,需要重新认证
}
return credentials.token.accessToken;
}
async authenticate(
serverName: string,
oauthConfig: OAuthConfig
): Promise<OAuthToken> {
// 1. 生成授权 URL
const authUrl = this.buildAuthUrl(oauthConfig);
console.log(`请访问以下 URL 进行授权:\n${authUrl}`);
// 2. 启动本地回调服务器
const code = await this.startCallbackServer(oauthConfig.redirectUri);
// 3. 用授权码换取令牌
const token = await this.exchangeCodeForToken(code, oauthConfig);
// 4. 保存令牌
await this.tokenStorage.saveToken(
serverName,
token,
oauthConfig.clientId,
oauthConfig.tokenUrl
);
return token;
}
}
流程图:
graph TD
A[用户请求] --> B{检查令牌}
B -->|有效| C[返回令牌]
B -->|无效/过期| D{有 refresh_token?}
D -->|是| E[刷新令牌]
E --> F[返回新令牌]
D -->|否| G[启动 OAuth 流程]
G --> H[返回新令牌]
style C fill:#90EE90
style F fill:#90EE90
style H fill:#90EE90
生产环境中,MCP 服务器可能"僵死"(连接正常但不响应)。blade-code 实现了主动健康检查:
export class HealthMonitor extends EventEmitter {
private intervalTimer: NodeJS.Timeout | null = null;
private consecutiveFailures = 0;
constructor(
private client: McpClient,
private config: HealthCheckConfig
) {
super();
}
start(): void {
if (this.intervalTimer) {
return; // 已经启动
}
this.intervalTimer = setInterval(async () => {
try {
await this.performHealthCheck();
this.consecutiveFailures = 0; // 重置失败计数
} catch (error) {
this.consecutiveFailures++;
console.warn(
`[HealthMonitor] 健康检查失败 (${this.consecutiveFailures}/${this.config.maxFailures}):`,
error
);
if (this.consecutiveFailures >= this.config.maxFailures) {
this.emit('unhealthy', this.consecutiveFailures, error);
await this.attemptReconnect();
}
}
}, this.config.intervalMs);
}
private async performHealthCheck(): Promise<void> {
const timeout = this.config.timeoutMs || 5000;
await Promise.race([
this.client.listTools(), // 调用一个轻量级方法
new Promise((_, reject) =>
setTimeout(() => reject(new Error('健康检查超时')), timeout)
),
]);
}
private async attemptReconnect(): Promise<void> {
console.log('[HealthMonitor] 尝试重连...');
try {
await this.client.disconnect();
await this.client.connect();
this.consecutiveFailures = 0;
this.emit('reconnected');
} catch (error) {
console.error('[HealthMonitor] 重连失败:', error);
}
}
}
配置示例:
{
enabled: true,
intervalMs: 30000, // 每 30 秒检查一次
timeoutMs: 5000, // 超时时间 5 秒
maxFailures: 3 // 连续失败 3 次触发重连
}
MCP 工具需要转换为 blade-code 的 Tool 接口:
export function createMcpTool(
client: McpClient,
serverName: string,
mcpTool: McpToolDefinition,
toolName?: string
): Tool {
return {
name: toolName || mcpTool.name,
description: mcpTool.description || `MCP工具: ${mcpTool.name}`,
parameters: mcpTool.inputSchema || { type: 'object', properties: {} },
metadata: {
source: 'mcp',
serverName,
originalName: mcpTool.name,
},
async execute(args: Record<string, unknown>): Promise<ToolResult> {
try {
const response = await client.callTool(mcpTool.name, args);
return {
success: true,
output: formatMcpResponse(response),
metadata: {
serverName,
toolName: mcpTool.name,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
serverName,
toolName: mcpTool.name,
},
};
}
},
};
}
关键点:
originalName)用于调试blade-code 的错误处理遵循以下原则:
临时错误(可重试):
- 网络超时
- 连接被拒绝
- 速率限制(429)
- 服务不可用(503)
永久错误(不重试):
- 配置错误(命令不存在)
- 认证失败(401)
- 权限不足(403)
- 协议错误(格式错误)
指数退避:
- 第 1 次:1 秒后重试
- 第 2 次:2 秒后重试
- 第 3 次:4 秒后重试
- 第 4 次:8 秒后重试
- 第 5 次:16 秒后重试
- 最大延迟:30 秒
stateDiagram-v2
[*] --> DISCONNECTED
DISCONNECTED --> CONNECTING: connect()
CONNECTING --> CONNECTED: 成功
CONNECTING --> ERROR: 失败
ERROR --> CONNECTING: 重试
CONNECTED --> ERROR: 意外断连
CONNECTED --> DISCONNECTED: disconnect()
// 配置文件
{
"mcpServers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/workspace"],
"env": {
"NODE_ENV": "production"
}
}
}
}
// 使用
const registry = McpRegistry.getInstance();
await registry.registerServer('filesystem', config.mcpServers.filesystem);
const tools = await registry.getAvailableTools();
// 输出: [{ name: 'read_file', ... }, { name: 'write_file', ... }]
{
"mcpServers": {
"github": {
"type": "sse",
"url": "https://api.github.com/mcp",
"oauth": {
"enabled": true,
"authUrl": "https://github.com/login/oauth/authorize",
"tokenUrl": "https://github.com/login/oauth/access_token",
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"scopes": ["repo", "user"],
"redirectUri": "http://localhost:3000/callback"
}
}
}
}
// 首次使用会自动触发 OAuth 流程
await registry.registerServer('github', config.mcpServers.github);
// 输出: 请访问以下 URL 进行授权: https://github.com/login/oauth/authorize?...
{
"mcpServers": {
"database": {
"type": "stdio",
"command": "mcp-database-server",
"healthCheck": {
"enabled": true,
"intervalMs": 30000,
"timeoutMs": 5000,
"maxFailures": 3
}
}
}
}
// 监听健康事件
registry.on('serverError', (name, error) => {
console.error(`服务器 ${name} 出错:`, error);
});
registry.on('healthMonitorReconnected', () => {
console.log('健康监控触发重连成功');
});
blade-code 的 MCP 集成实现了以下核心功能:
相关资源:
讨论:欢迎在 GitHub Issues 或我的博客评论区交流!
在JavaScript的世界里,继承不是简单的复制粘贴,而是一场关于"原型链"的奇妙冒险。想象一下:别的语言继承就像领养孩子,直接给一套新房子和新衣服;而JavaScript的继承更像是家族传承——孩子不仅有自己的家,还能随时去祖辈家里串门拿东西!
今天,就让我们一起揭开JavaScript继承的神秘面纱,亲手打造一个属于自己的"家族传承"系统。
让我们先来看看JavaScript中最"朴实"的继承方式。
假设我们有一个Animal(动物)家族:
function Animal(name, age) {
this.name = name; // 名字
this.age = age; // 年龄
}
Animal.prototype.species = '动物'; // 所有动物都有的物种属性
现在,Cat(猫)家族想要继承Animal家族的优良传统。最简单的做法是什么?
function Cat(name, age, color) {
// 先把Animal家族的基本功学过来
Animal.call(this, name, age);
this.color = color; // 猫特有的毛色
}
// 关键一步:成为Animal家族的"亲传弟子"
Cat.prototype = new Animal();
// 但别忘了改个名,不然别人还以为你是Animal
Cat.prototype.constructor = Cat;
const garfield = new Cat('加菲猫', 2, '黄色');
console.log(garfield.species); // ✅ 输出:动物(成功继承了物种!)
这里发生了什么?
Cat.prototype = new Animal():相当于Cat家族把Animal请来当顾问场景想象:你想请Animal当顾问,结果人家拖家带口、把全部家当都搬来了!new Animal()创建了一个完整的Animal实例,但我们需要的仅仅是Animal的"知识库"(原型),而不是它的全部身家。
三大痛点:
new Animal()时需要参数,但作为原型时不知道传什么有人可能想:"既然只是要原型,那直接共享不就行了?"
// 看似聪明的偷懒方法
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
危险!这是个陷阱!
// 猫家族想给自己加个技能
Cat.prototype.eatFish = function() {
console.log('我爱吃鱼!');
};
// 但意外发生了...
const dog = new Animal('旺财', 3);
dog.eatFish(); // 😱 输出:我爱吃鱼!(狗怎么爱吃鱼了?!)
问题所在:
Cat.prototype和Animal.prototype指向同一个对象
我们需要一个既能继承知识,又不造成混乱的方法。这就是我们的"空函数中介"模式——一个聪明的"传话筒"。
function extend(Parent, Child) {
// 1. 请一个"中间人"(空函数F)
// 它就像家族间的专业翻译,只传话,不添乱
var F = function() {};
// 2. 让中间人学习Parent的知识库
F.prototype = Parent.prototype;
// 3. 让Child拜中间人为师
Child.prototype = new F();
// 4. 给Child正名:你姓Child,不是Parent
Child.prototype.constructor = Child;
}
// 使用我们的extend函数
function Cat(name, age, color) {
// 继承Animal的"个人能力"
Animal.apply(this, [name, age]);
this.color = color; // 猫的独有特征
}
// 启动传承仪式!
extend(Animal, Cat);
// 猫家族发展自己的特色
Cat.prototype.purr = function() {
console.log('喵呜~发出呼噜声');
};
// 见证奇迹的时刻
const kitty = new Cat('小橘', 1, '橘色');
console.log(kitty.species); // ✅ "动物"(继承了Animal的物种)
kitty.purr(); // ✅ "喵呜~发出呼噜声"(猫的独有技能)
const bird = new Animal('小鸟', 0.5);
console.log(bird.purr); // ✅ undefined(完全没影响到Animal!)
三层隔离保护:
内存关系图:
kitty(猫实例)
↓ "我可以找我的家族要东西"
Cat.prototype(猫家族知识库)
↓ "我学自中间人F"
F.prototype(= Animal.prototype)
↓ "我来自Animal家族"
Animal.prototype(动物家族知识库)
↓ "我是所有对象的起点"
Object.prototype
让我们把理论变成实战代码:
// 增强版extend:更智能的传承系统
function extend(Child, Parent) {
// 1. 请专业中间人(开销极小)
var F = function() {};
// 2. 中间人学习Parent的全部知识
F.prototype = Parent.prototype;
// 3. Child拜师学艺
Child.prototype = new F();
Child.prototype.constructor = Child;
// 4. 给Child一个"家谱"(可选但很贴心)
Child.uber = Parent.prototype;
// 5. 现代JavaScript的额外支持
if (Object.setPrototypeOf) {
Object.setPrototypeOf(Child.prototype, Parent.prototype);
}
}
// 动物家族基类
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.breathe = function() {
return '我在呼吸新鲜空气';
};
// 猫家族
function Cat(name, age, color) {
// 先学Animal的"生存技能"
Animal.call(this, name, age);
this.color = color;
}
// 启动传承
extend(Cat, Animal);
// 猫家族的独门绝技
Cat.prototype.climbTree = function() {
return '我能爬上最高的树!';
};
// 看看成果
const tom = new Cat('汤姆', 3, '蓝灰色');
console.log(tom.breathe()); // ✅ "我在呼吸新鲜空气"
console.log(tom.climbTree()); // ✅ "我能爬上最高的树!"
console.log(tom.color); // ✅ "蓝灰色"
ES6给了我们更优雅的写法:
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
breathe() {
return '我在呼吸新鲜空气';
}
}
class Cat extends Animal {
constructor(name, age, color) {
super(name, age); // 这行相当于 Animal.call(this, name, age)
this.color = color;
}
climbTree() {
return '我能爬上最高的树!';
}
}
重要提醒:class只是"语法糖",底层依然是我们的原型继承。理解原型,才能真正掌握JavaScript的继承精髓。
通过这次探索,我们学到了:
编程就像家族传承:
extend函数就像是找到了完美的家族信托方案如果你要继续优化这个extend函数,你会添加哪些功能?
动手挑战:尝试实现一个支持多重继承的extend函数,让一个类可以同时继承多个父类的特性。把你的代码分享到评论区,看看谁的实现最优雅!
记住:在JavaScript的世界里,理解原型链就像掌握家族的秘密通道。通过这些通道,你可以在不破坏原有结构的前提下,构建出强大而灵活的代码"家族"。现在,你也是掌握这个秘密的开发者了!
在 JavaScript 中,new 操作符是我们创建对象实例最常用的方式之一。但你真的了解 new 背后发生了什么吗?今天我们就来深入探讨一下 new 的奥秘,并亲手实现一个自己的 new 函数。
构造函数其实就是一个普通的函数,但当我们使用 new 关键字调用它时,它就变成了一个"构造函数"。
function Person(name, age) {
this.name = name;
this.age = age;
}
// 作为普通函数调用
Person('张三', 18); // this 指向全局对象(浏览器中是 window)
// 作为构造函数调用
const person = new Person('张三', 18); // this 指向新创建的对象
比喻:想象一下工厂生产产品的过程:
具体来说,new 操作符执行以下4个步骤:
const obj = {};
__proto__ 指向构造函数的 prototype
obj.__proto__ = Constructor.prototype;
this 绑定到这个新对象,并执行构造函数Constructor.apply(obj, args);
function Person(name) {
this.name = name;
// 如果没有显式返回,默认返回 this
}
function Person2(name) {
this.name = name;
return { custom: 'object' }; // 如果返回对象,则替代新创建的对象
}
const p1 = new Person('张三'); // Person {name: "张三"}
const p2 = new Person2('李四'); // {custom: "object"}
这三个方法都用于改变函数执行时的 this 指向,但使用方式略有不同。
想象你是一家公司的CEO(函数),你需要给员工(对象)下达指令:
function introduce(greeting, punctuation) {
console.log(`${greeting}, 我是${this.name}${punctuation}`);
}
const person = { name: '张三' };
// call 接受参数列表
introduce.call(person, '你好', '!'); // "你好, 我是张三!"
// apply 接受参数数组
introduce.apply(person, ['你好', '!']); // "你好, 我是张三!"
// bind 返回一个新函数,而不是立即执行
const boundIntroduce = introduce.bind(person, '你好');
boundIntroduce('!'); // "你好, 我是张三!"
| 方法 | 立即执行 | 参数形式 | 返回值 |
|---|---|---|---|
| call | 是 | 参数列表 | 函数执行结果 |
| apply | 是 | 数组 | 函数执行结果 |
| bind | 否 | 参数列表 | 新函数 |
arguments 是函数内部的一个特殊对象,它包含了函数调用时传入的所有参数。
function showArgs() {
console.log(arguments);
console.log(arguments.length);
console.log(arguments[0]);
}
showArgs(1, 2, 3);
// 输出:
// Arguments(3) [1, 2, 3]
// 3
// 1
arguments 看起来像数组,但不是真正的数组:
function checkArguments() {
console.log('长度:', arguments.length);
console.log('可索引:', arguments[0], arguments[1]);
console.log('是数组吗?', Array.isArray(arguments)); // false
console.log('类型:', Object.prototype.toString.call(arguments)); // [object Arguments]
}
checkArguments('a', 'b', 'c');
function tryArrayMethods() {
// 这些会报错
// arguments.map(item => item * 2); // ❌ 错误
// arguments.reduce((sum, num) => sum + num); // ❌ 错误
// 但可以这样遍历
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
// 或者用 for...of(ES6+)
for (const arg of arguments) {
console.log(arg);
}
}
function convertArguments1() {
const argsArray = Array.from(arguments);
console.log(Array.isArray(argsArray)); // true
console.log(argsArray.map(x => x * 2)); // 可以正常使用数组方法
}
function convertArguments2(...args) { // 直接在参数中使用
console.log(Array.isArray(args)); // true
}
function convertArguments3() {
const argsArray = [...arguments];
console.log(Array.isArray(argsArray)); // true
}
function convertArguments4() {
const argsArray = Array.prototype.slice.call(arguments);
console.log(Array.isArray(argsArray)); // true
}
const arrowFunc = () => {
console.log(arguments); // ❌ 报错:arguments is not defined
};
// 箭头函数应该这样获取参数
const arrowFunc2 = (...args) => {
console.log(args); // ✅ 正确
};
function linkedArguments(a, b) {
console.log('a:', a, 'arguments[0]:', arguments[0]);
a = 'changed';
console.log('修改后 a:', a, 'arguments[0]:', arguments[0]);
arguments[0] = 'changed again';
console.log('再次修改后 a:', a, 'arguments[0]:', arguments[0]);
}
linkedArguments('original', 2);
// 输出:
// a: original arguments[0]: original
// 修改后 a: changed arguments[0]: changed
// 再次修改后 a: changed again arguments[0]: changed again
现在,让我们结合以上知识点,一步步实现自己的 new 函数。
function objectFactory(Constructor, ...args) {
// 1. 创建一个空对象
const obj = {};
// 2. 将新对象的原型指向构造函数的原型
obj.__proto__ = Constructor.prototype;
// 3. 将构造函数的 this 绑定到新对象,并执行构造函数
Constructor.apply(obj, args);
// 4. 返回新对象
return obj;
}
function objectFactory(Constructor, ...args) {
// 1. 创建新对象,并设置原型链
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,绑定 this
const result = Constructor.apply(obj, args);
// 3. 判断构造函数返回的是否是对象
// 如果是对象则返回该对象,否则返回新创建的对象
return typeof result === 'object' && result !== null ? result : obj;
}
function objectFactory() {
// 1. 获取构造函数(第一个参数)
const Constructor = [].shift.call(arguments);
// 2. 创建空对象,并继承构造函数的原型
const obj = Object.create(Constructor.prototype);
// 3. 执行构造函数,将 this 指向新对象
const result = Constructor.apply(obj, arguments);
// 4. 返回结果
return typeof result === 'object' ? result : obj;
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name},今年${this.age}岁`);
};
// 使用原生的 new
const person1 = new Person('张三', 18);
person1.sayHello(); // "你好,我是张三,今年18岁"
// 使用我们手写的 objectFactory
const person2 = objectFactory(Person, '李四', 20);
person2.sayHello(); // "你好,我是李四,今年20岁"
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Person); // true
console.log(person1.sayHello === person2.sayHello); // true(共享原型方法)
// 1. 构造函数返回对象的情况
function Car(model) {
this.model = model;
return { custom: 'special object' }; // 返回对象
}
const car = objectFactory(Car, 'Tesla');
console.log(car); // {custom: "special object"},而不是 Car 实例
// 2. 构造函数返回基本类型的情况
function Bike(brand) {
this.brand = brand;
return 'not an object'; // 返回基本类型,会被忽略
}
const bike = objectFactory(Bike, 'Giant');
console.log(bike); // Bike {brand: "Giant"},返回新创建的对象
许多库(如早期的 jQuery)会使用类似的技术来创建对象,避免使用 new 关键字:
// jQuery 风格的初始化
function $(selector) {
return new jQuery(selector);
}
// 或者
function $(selector) {
return objectFactory(jQuery, selector);
}
function createObjectPool(Constructor, count) {
const pool = [];
for (let i = 0; i < count; i++) {
pool.push(objectFactory(Constructor));
}
return pool;
}
// 创建 10 个默认的 Person 对象
const personPool = createObjectPool(Person, 10);
function singleton(Constructor, ...args) {
let instance = null;
return function() {
if (!instance) {
instance = objectFactory(Constructor, ...args);
}
return instance;
};
}
const getSingletonPerson = singleton(Person, '单例', 100);
const p1 = getSingletonPerson();
const p2 = getSingletonPerson();
console.log(p1 === p2); // true
通过手写 new 操作符,我们深入理解了 JavaScript 对象实例化的过程:
理解这些底层机制,不仅可以帮助我们更好地使用 JavaScript,还能在面试中脱颖而出。更重要的是,这种"知其然知其所以然"的学习方式,能够让我们在面对复杂问题时,有能力从底层原理出发,找到最优雅的解决方案。
记住,每个看似简单的 new 背后,都隐藏着 JavaScript 原型链、this 绑定、函数执行等多个核心概念的完美协作。掌握了这些,你就真正理解了 JavaScript 面向对象编程的精髓。
最近我在做一个AI聊天应用时,遇到了一个关键问题:如何让AI的回复像真人打字一样,一个字一个字地出现? 经过一番探索,我发现了流式输出 + Buffer的组合方案。今天,我就用我的实际代码,带你彻底搞懂这个技术!
想象你有一个 智能聊天机器人🤖:
生活例子📺: 想象你家电视的遥控器:
这里的 响应式 就是:按遥控器(改变数据),电视立即响应(页面更新)。
// 创建响应式数据就像给数据装上"遥控器"
const question = ref('你好'); // 创建一个能"遥控"的数据
// 在模板中显示
<div>{{ question }}</div> <!-- 显示:你好 -->
// 如果改变数据
question.value = 'Hello'; // 按下"遥控器"
// 页面自动变成
<div>Hello</div> <!-- 页面自动更新! -->
ref 是什么?ref 就是把普通数据包装成一个特殊的盒子📦:
// 普通数据
let name = "小明";
// 改变时,Vue不知道,页面不会更新
// 响应式数据
const nameRef = ref("小明");
// 实际上变成了:{ value: "小明" }
// 访问时要加 .value
console.log(nameRef.value); // "小明"
// 改变数据
nameRef.value = "小红"; // Vue 知道数据变了,会更新页面
v-model - 双向绑定双向绑定 就像 同步的记事本📝:
<!-- 创建一个输入框 -->
<input v-model="question" />
<!-- 这相当于做了两件事:
1. 输入框显示 question 的值
2. 你在输入框打字时,自动更新 question 的值
-->
实际效果:
// 你输入"你好"
question.value = "你好";
// 页面显示
<input value="你好" />
// 你再输入"大家好"
// question.value 自动变成 "大家好"
@click - 事件监听就像给按钮装上 门铃🔔:
<button @click="askLLM">提交</button>
<!-- 意思是:点击这个按钮时,执行 askLLM 函数 -->
const askLLM = async () => {
// 1. 准备问题(像写菜单)
if (!question.value) {
console.log('问题不能为空');
return;
}
// 2. 显示"思考中..."(像显示"商家接单中")
content.value = "思考中...";
// 3. 准备外卖信息
const endpoint = 'https://api.deepseek.com/chat/completions'; // 外卖平台地址
const headers = {
'Authorization': `Bearer ${你的API密钥}`, // 支付凭证
'Content-Type': 'application/json', // 说要送JSON格式
};
// 4. 下订单
const response = await fetch(endpoint, {
method: 'POST', // 点外卖用POST
headers, // 告诉商家信息
body: JSON.stringify({ // 具体订单内容
model: 'deepseek-chat',
stream: stream.value, // 要不要流式(分批送)
messages: [{
role: 'user',
content: question.value
}]
})
});
// 5. 等外卖送到并处理
// ... 后面详细讲
}
比喻🎬:
在这个应用中:
if (stream.value) { // 如果用户选了流式模式
// 第一步:清空上次的回答
content.value = ""; // 清空显示区域
// 第二步:创建"水管"和"水龙头"
const reader = response.body?.getReader();
// reader 就像水龙头,可以控制水流
const decoder = new TextDecoder();
// decoder 就像净水器,把脏水(二进制)变成干净水(文字)
let done = false; // 记录水是否流完了
let buffer = ''; // 临时水桶,装不完整的水
// 第三步:开始接水(循环读取)
while (!done) { // 只要水没流完就一直接
// 接一瓢水(读一块数据)
const { value, done: doneReading } = await reader?.read();
// value: 接到的水(二进制数据)
// doneReading: 这一瓢接完了吗?
done = doneReading; // 更新是否流完的状态
// 第四步:处理接到的水
// 把这次的水和上次没处理完的水合在一起
const chunkValue = buffer + decoder.decode(value);
buffer = ''; // 清空临时水桶
console.log("收到数据:", chunkValue);
// 数据格式类似:
// data: {"delta": {"content": "你"}}
// data: {"delta": {"content": "好"}}
// data: [DONE]
// 第五步:把一大块水分成一行一行
const lines = chunkValue.split('\n') // 按换行分割
.filter(line => line.startsWith('data: ')); // 只保留以"data: "开头的行
// 第六步:处理每一行水
for (const line of lines) {
const incoming = line.slice(6); // 去掉开头的"data: "
// 现在 incoming = '{"delta": {"content": "你"}}'
// 如果是结束标志
if (incoming === '[DONE]') {
done = true; // 停止接水
break; // 跳出循环
}
try {
// 第七步:解析JSON(把水变成能喝的东西)
const data = JSON.parse(incoming);
// data = { delta: { content: "你" } }
const delta = data.choices[0].delta.content;
// delta = "你"
if (delta) {
// 第八步:显示出来
content.value += delta; // 把"你"加到显示内容里
// 第一次:content = "你"
// 第二次:content = "你好"
// 第三次:content = "你好世"
// ... 直到完成
}
} catch (error) {
// 如果JSON解析失败(比如收到了不完整的JSON)
buffer += `data: ${incoming}`; // 存起来等下一瓢水
}
}
}
}
buffer?情景模拟: 假设AI要回复"你好世界",但网络传输时可能这样:
第一次收到:data: {"delta": {"content": "你
(JSON不完整,少了右括号)
第二次收到:好世界"}}
如果直接解析第一次的数据:
JSON.parse('{"delta": {"content": "你'); // 报错!JSON不完整
所以我们需要:
buffer = 'data: {"delta": {"content": "你'
buffer + 新数据 = 'data: {"delta": {"content": "你好世界"}}'
让我用具体的执行过程展示这个系统的精妙:
javascript
// 用户输入:"你好"
// 服务器响应流开始...
// 第1次循环:
收到数据: data: {"delta": {"content": "你"}}\n
分割成行: ['data: {"delta": {"content": "你"}}']
解析成功!→ 显示:"你"
// 第2次循环:
收到数据: data: {"delta": {"content": "好
分割成行: ['data: {"delta": {"content": "好']
JSON解析失败!→ 存入buffer: 'data: {"delta": {"content": "好'
// 第3次循环:
收到数据: "}}\n
当前数据: buffer + 新数据 = 'data: {"delta": {"content": "好"}}'
分割成行: ['data: {"delta": {"content": "好"}}']
解析成功!→ 显示:"你好"
// 第4次循环:
收到数据: data: [DONE]\n
检测到[DONE] → 结束循环
你打开页面
↓
看到输入框:[讲一个笑话]
↓
点击"提交"
↓
Vue调用 askLLM() 函数
↓
显示"思考中..."
↓
发送请求到DeepSeek
↓
AI开始思考
↓
【流式模式】
↓
收到第一个字:"有"
↓
页面显示:有
↓
收到第二个字:"个"
↓
页面显示:有个
↓
收到第三个字:"人"
↓
页面显示:有个人
↓
...(持续)
↓
收到"[DONE]"
↓
显示完整:有个人去面试...
| 概念 | 比喻 | 作用 |
|---|---|---|
ref() |
遥控器📱 | 让数据变化时页面自动更新 |
v-model |
双向镜子🪞 | 输入框和数据的双向同步 |
@click |
门铃🔔 | 点击时执行函数 |
fetch() |
外卖小哥🚴 | 发送网络请求 |
getReader() |
水龙头🚰 | 读取流式数据 |
TextDecoder() |
翻译官👨💼 | 把二进制变成文字 |
JSON.parse() |
拆包裹📦 | 把JSON字符串变成对象 |
stream.value 改成 false 看看区别console.log 里看数据变化console.log() 打印每一步的结果这个代码虽然看起来复杂,但每个部分都有明确的作用。就像搭积木一样,每块积木(函数)都有特定的功能,组合起来就实现了强大的AI聊天功能!😊
<script setup>
import { ref } from 'vue';
const question = ref('讲一个光头强和一个白富美之间的故事,20字');
const stream = ref(true);
const content = ref("");
const askLLM = async () => {
if (!question.value) {
console.log('question is empty');
return;
}
content.value = "思考中...";
const endpoint = 'https://api.deepseek.com/chat/completions';
const headers = {
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
'Content-Type': 'application/json',
};
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model: 'deepseek-chat',
stream: stream.value,
messages: [{
role: 'user',
content: question.value
}]
})
});
if (stream.value) {
content.value = "";
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let done = false;
let buffer = '';
while (!done) {
const { value, done: doneReading } = await reader?.read();
console.log(value, doneReading);
done = doneReading;
const chunkValue = buffer + decoder.decode(value);
console.log(chunkValue);
buffer = '';
const lines = chunkValue.split('\n')
.filter(line => line.startsWith('data: '));
for (const line of lines) {
const incoming = line.slice(6);
if (incoming === '[DONE]') {
done = true;
break;
}
try {
const data = JSON.parse(incoming);
const delta = data.choices[0].delta.content;
if (delta) {
content.value += delta;
}
} catch (error) {
buffer += `data: ${incoming}`;
}
}
}
} else {
const data = await response.json();
console.log(data);
content.value = data.choices[0].message.content;
}
}
</script>
<template>
<div class="container">
<div>
<label>输入:</label>
<input class="input" v-model="question"/>
<button @click="askLLM">提交</button>
</div>
<div class="output">
<div>
<label>Streaming</label>
<input type="checkbox" v-model="stream"/>
<div>{{content}}</div>
</div>
</div>
</div>
</template>
<style scoped>
* {
margin: 0;
padding: 0;
}
.container {
display: flex;
flex-direction: column;
align-items: start;
justify-content: start;
height: 100vh;
font-size: 0.85rem;
}
.input {
width: 200px;
}
button {
padding: 0 10px;
margin-left: 6px;
}
.output {
margin-top: 10px;
min-height: 300px;
width: 100%;
text-align: left;
}
</style>