当前位置: 首页 > news >正文

前端响应式原理与DOM优化实战:从defineProperty到虚拟DOM

1. 项目概述:一个被长期误读的前端工程实践符号

“Ruby's Louvre”——这五个单词组合在一起,初看像某位艺术家的个人展览名,又似巴黎卢浮宫的某种变体拼写,甚至让人联想到 Ruby 编程语言的社区分支。但事实上,它既不是框架、也不是开源库,更不是某个 SaaS 产品的商标。它是一个真实存在、持续活跃超过十五年的中文前端技术博客品牌,由国内资深前端架构师司徒正美(网名“司徒正美”,曾用 ID “RubyLouvre”)于 2008 年前后创建并长期主理。这个名称中的 “Ruby” 并非指代编程语言 Ruby,而是取自其英文名 “Rui Bo” 的音译谐音;“Louvre” 则是刻意借用卢浮宫(Le Louvre)的意象,隐喻“收藏经典、沉淀思想、开放共享”的技术精神——就像卢浮宫收藏人类文明杰作一样,这个博客致力于系统性地收藏、解构、重实现那些被时间验证过的前端底层原理与工程范式。

我从 2011 年开始关注这个博客,当时 jQuery 正处鼎盛,Backbone.js 刚崭露头角,而“Ruby's Louvre”已连续发布《JavaScript 设计模式》《DOM 操作性能陷阱全解析》《IE6/7 兼容性黑盒逆向笔记》等系列长文。它不追热点,不炒概念,所有内容都围绕一个核心命题展开:在浏览器这个最不可控的运行环境中,如何用最朴素的 JavaScript 原生能力,构建出稳定、可测、可维护的 UI 构建基座?这个定位,让它成为早期国内少有的、真正深入 DOM 渲染管线、事件循环机制、CSSOM 构建流程的深度技术输出源。它影响了包括 avalon(司徒本人主导开发的 MVVM 框架)、Vue.js 早期响应式设计、以及大量企业级中后台低代码平台的底层数据绑定与虚拟 DOM 差分逻辑。今天你看到的 Vue 的Object.defineProperty响应式劫持、React 的 Fiber 调度中断点设计、甚至现代微前端沙箱的属性拦截策略,都能在其 2012–2015 年的存档文章中找到清晰的雏形推演与手写实现。

对刚入行的前端新人来说,“Ruby's Louvre” 是一座绕不开的“原理碑林”——它不教你怎么用 Vue CLI 创建项目,但会手把手带你用 200 行代码写出一个支持依赖收集、异步批量更新、嵌套对象监听的响应式系统;对资深架构师而言,它是工程决策的“历史对照组”——当你在为是否引入 Proxy、是否放弃 IE 支持、是否采用编译时优化而犹豫时,翻一翻它 2013 年那篇《兼容性与先进性的十字路口:我们为什么坚持 defineProperty》,答案往往就藏在当年的权衡细节里。它不是教程,不是文档,而是一份持续十五年的、带着体温的技术手记。

2. 核心内容体系拆解:从 DOM 操控到现代框架内核的完整演进链

2.1 DOM 操作与性能优化:一切前端工程的物理基石

“Ruby's Louvre” 的内容起点,牢牢钉死在浏览器最原始的 API 层——DOM。在 jQuery 仍被奉为圭臬的年代,它就已开始系统性地解剖document.createElementinnerHTMLinsertBeforeDocumentFragment等原生方法的底层行为差异。其核心观点非常直白:DOM 操作不是“快或慢”的问题,而是“触发多少次重排重绘”的问题。它用大量实测数据证明:在 IE9 下,连续调用 10 次element.appendChild(child),比先创建DocumentFragment再一次性appendChild(fragment)慢 47 倍;而在 Chrome 中,这一差距缩小至 3.2 倍,但重排次数却从 10 次降为 1 次。

这种量化思维贯穿始终。它没有停留在“应该用 DocumentFragment”的结论上,而是进一步拆解:为什么DocumentFragment能避免重排?因为它的nodeType是 11,不属于活动文档树(live document tree),浏览器不会为其计算布局;当它被 append 到真实 DOM 时,整个 fragment 子树才作为一个整体参与一次 layout 计算。这个解释直接关联到浏览器渲染管线的 Layout 阶段原理。它还给出了可落地的封装建议:一个轻量级的batchAppend工具函数,内部自动判断是否启用 fragment,对老版本 IE 回退到innerHTML拼接,对现代浏览器则使用createDocumentFragment+cloneNode(true)组合。这个函数后来被多个内部框架复用,成为 DOM 批量操作的事实标准模板。

提示:它特别强调一个易被忽略的细节——innerHTML的“安全边界”。很多人认为innerHTML = '<div>xxx</div>'是纯字符串替换,其实不然。浏览器在解析innerHTML时,会同步执行其中<script>标签的脚本、触发<img>的加载、甚至解析<link rel="stylesheet">。这意味着,如果你在innerHTML中插入用户可控内容,不仅有 XSS 风险,还可能意外触发网络请求或脚本执行。它给出的解决方案不是简单过滤<script>,而是建立一套“HTML 片段白名单解析器”,只允许divspanp等无副作用标签,并对srchref属性做协议校验(仅允许http:https:data:)。这套思路,正是如今 React 的dangerouslySetInnerHTML和 Vue 的v-html指令背后的安全设计原型。

2.2 事件系统与委托机制:从冒泡捕获到合成事件的底层映射

事件处理是前端交互的生命线,而“Ruby's Louvre”对事件系统的剖析,堪称教科书级别。它没有止步于addEventListener的基本用法,而是深入到浏览器事件模型的三个阶段:捕获(capturing)、目标(target)、冒泡(bubbling)。它用一个经典案例说明差异:给<body>添加捕获阶段监听器,再给<button>添加目标阶段监听器,点击按钮时,事件流是body(capture) → button(target) → body(bubble),而非直觉上的“先目标后冒泡”。

更关键的是,它首次在国内系统性地提出“事件委托的性能临界点”概念。通过构造包含 5000 个<li><ul>列表,对比“为每个 li 绑定 click”与“为 ul 绑定 delegate click”两种方案,它发现:在 Chrome 中,委托方案内存占用低 68%,首次绑定耗时少 92%;但在 iOS Safari 8 上,由于事件委托需遍历event.target的祖先链,当嵌套层级超过 12 层时,委托反而比直绑慢 15%。这个发现直接催生了其自研框架 avalon 的ms-on指令优化:对浅层结构(层级 ≤ 8)默认启用委托,对深层结构则自动回退为直绑,并提供delegate="false"手动开关。

它对“合成事件”(Synthetic Event)的解读尤为深刻。React 的SyntheticEvent不是简单包装原生事件,而是构建了一套独立的事件池(Event Pool)。它指出:React 在事件回调执行完毕后,会立即调用event.persist()之外的所有事件对象的e.nativeEvent属性置空,并将事件对象放回池中复用。这意味着,如果你在setTimeout中访问e.target,拿到的将是null。它给出的解决方案不是“记得调用e.persist()”,而是从根本上理解事件池的设计意图——减少 GC 压力。它手写了一个极简版事件池模拟器,用Array.push()/Array.pop()管理 20 个预分配的事件对象,实测在高频点击场景下,GC 暂停时间从平均 12ms 降至 1.8ms。这个例子让无数开发者第一次意识到:框架的“便利性”背后,是精密的内存管理权衡。

2.3 数据绑定与响应式原理:从 defineProperty 到 Proxy 的演进全景图

如果说 DOM 和事件是前端的“肌肉”,那么数据绑定就是它的“神经”。而“Ruby's Louvre”对响应式原理的探索,构成了其最具影响力的内容板块。它早在 2012 年就发布了《Object.defineProperty 深度剖析》,这篇长文至今仍是理解 Vue 2.x 响应式的最佳入门材料。它没有堆砌 API,而是用三步走清逻辑:

  1. defineProperty 的本质:它不是一个“魔法”,而是浏览器为 JavaScript 对象属性提供的“访问器描述符”(accessor descriptor)控制接口。当你写Object.defineProperty(obj, 'a', { get() { return val }, set(newVal) { val = newVal; notify(); } }),你实际上是在 obj.a 这个属性上安装了两个钩子函数。
  2. 依赖收集的时机:关键在于get钩子何时被触发。它指出,只有当某个属性在“求值上下文”中被读取时,get才会执行。比如render()函数中写了return
    ${this.name}
    ,那么在render()执行过程中,this.nameget就会被调用,此时name就能将当前的render` 函数(即“Watcher”)记录为自己的依赖。
  3. 通知更新的粒度set钩子触发后,它通知的不是“整个组件”,而是所有依赖该属性的 Watcher。如果nameage都被同一个render依赖,那么修改name只会触发render一次,而非两次。这就是“精确更新”的来源。

它用一个 150 行的极简实现,完整复现了 Vue 2.x 的核心响应式逻辑:Observer类负责递归遍历对象,为每个属性安装definePropertyDep类作为依赖容器,存储所有 Watcher;Watcher类代表一个观察者,在get时把自己加入Dep,在update时触发回调。这个实现没有 Vue 的复杂调度系统,但已足够揭示响应式的核心契约。

当 Proxy 成为新宠时,它没有盲目拥抱,而是冷静分析:Proxy 的优势在于能监听数组索引赋值(arr[0] = 1)、新增属性(obj.newKey = val)、delete操作,这是defineProperty的硬伤;但 Proxy 的劣势同样明显——兼容性差(IE 全系不支持)、内存开销大(每个被代理对象都需额外创建 Proxy 实例)、且无法 polyfill。它给出的工程建议非常务实:“新项目可用 Proxy,但存量 IE11 项目,请继续深耕defineProperty的优化空间”,并附上一份《IE11 下 defineProperty 响应式性能压测报告》,详细列出不同数据结构(扁平对象、嵌套对象、大型数组)在 1000 次变更下的平均耗时与内存增长曲线。

2.4 模板编译与虚拟 DOM:从字符串解析到 diff 算法的手写实践

模板引擎是前端框架的“翻译官”,而“Ruby's Louvre”对它的解构,展现了惊人的工程耐心。它没有直接使用new Function()来动态编译模板,而是从最基础的词法分析(Lexical Analysis)讲起。它把一个简单的模板<div>{{name}}<span v-if="show">Hello</span></div>拆解为 Token 流:[TAG_START, "div"], [TEXT, "{{name}}"], [TAG_START, "span"], [DIRECTIVE, "v-if", "show"], [TEXT, "Hello"], [TAG_END, "span"], [TAG_END, "div"]。它指出,{{name}}不是简单的字符串替换,而是一个“表达式节点”,需要被new Function('scope', 'return ' + expression)安全包裹执行;而v-if="show"则是一个“指令节点”,其值show必须在作用域中可求值,且结果必须为布尔类型。

基于此,它手写了一个微型模板编译器,核心只有三个函数:

  • parse(template):将字符串转为 AST(抽象语法树),每个节点包含type(如'Element','Text','Expression')、childrenprops等字段;
  • generate(ast):将 AST 转为可执行的render函数字符串,例如对<div id="app">{{msg}}</div>,生成return h('div', {id: 'app'}, [scope.msg])
  • compile(template):组合前两者,返回一个render函数。

这个编译器虽小,却完整覆盖了模板解析、AST 转换、代码生成的全流程。它甚至考虑了错误处理:当parse遇到未闭合标签时,抛出TemplateSyntaxError: Unclosed tag 'div' at line 3, column 12,并附带精准的行列号定位——这正是现代框架如 Vue 的vue-template-compiler错误提示的雏形。

对于虚拟 DOM,它没有陷入“diff 算法有多快”的争论,而是聚焦一个根本问题:“为什么需要 diff?” 答案是:为了最小化真实 DOM 操作。它用一个直观类比解释:真实 DOM 就像一台昂贵的工业机床,每次启动、校准、加工都要耗费巨大成本;而虚拟 DOM 就是一张低成本的加工图纸,你可以随意修改、对比、优化图纸,直到确定最优加工路径,再一次性驱动机床执行。它手写的patch函数,只处理四种基本操作:CREATE(创建新节点)、REMOVE(删除旧节点)、TEXT(更新文本内容)、PROPS(更新属性)。它特别强调key的作用:没有key时,patch会按顺序一一比对子节点,导致<ul><li>A</li><li>B</li></ul>变成<ul><li>B</li><li>A</li></ul>时,会错误地认为两个li都需要更新内容;而加上key="A"key="B"后,patch能通过key快速定位到节点对应关系,只交换 DOM 位置,不触发内容更新。这个例子,让“key 的必要性”从一句口号变成了可触摸的性能事实。

3. 实操复现:用 300 行代码搭建一个微型响应式视图系统

3.1 系统设计目标与模块划分

要真正吃透“Ruby's Louvre”的思想,最好的方式是亲手复现一个极简但完整的系统。这里我们以“Ruby's Louvre”2014 年发布的《一个 200 行的 MVVM》为蓝本,扩展为一个功能完备的 300 行微型响应式视图系统,命名为MiniVue。它的设计目标非常明确:不依赖任何外部库,纯原生 JavaScript,支持数据响应式、模板插值、条件渲染、列表渲染、事件绑定,且所有代码可调试、可打断点。这与当下动辄数万行的框架形成鲜明对比,恰恰体现了“Ruby's Louvre”一贯主张的“可控性优于便利性”。

整个系统划分为四个核心模块:

  • Observer:负责将 data 对象转换为响应式对象,核心是defineProperty的递归应用;
  • Dep:依赖管理器,每个响应式属性拥有一个Dep实例,用于收集和通知依赖;
  • Watcher:观察者,代表一个需要被响应式数据驱动的函数(如 render 函数),它在求值时收集自身为依赖,在数据变更时被通知更新;
  • Compiler:模板编译器,负责解析 HTML 模板字符串,提取插值、指令,生成可执行的render函数。

这四个模块之间通过清晰的契约交互:Observerset时调用dep.notify()Depnotify时遍历subs数组,调用每个watcher.update()Watcherget时将自己的id注册到Dep.target,从而完成依赖收集。这种松耦合、高内聚的设计,正是“Ruby's Louvre”推崇的“小而精”工程哲学的体现。

3.2 Observer 模块:递归劫持与数组变异方法重写

Observer模块是响应式的基石。它的核心任务是:当用户传入一个普通对象data时,将其所有属性(包括嵌套对象)都转换为带有get/set钩子的响应式属性。代码实现如下:

function observe(data) { if (!data || typeof data !== 'object') return; // 避免重复观测 if (data.__ob__) return; // 为 data 添加 __ob__ 属性,标记已观测 Object.defineProperty(data, '__ob__', { value: new Observer(data), enumerable: false, writable: true, configurable: true }); // 递归观测所有属性 Object.keys(data).forEach(key => { defineReactive(data, key, data[key]); }); } function defineReactive(obj, key, val) { // 为每个属性创建一个专属的 Dep const dep = new Dep(); // 递归观测嵌套对象 observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 依赖收集:当 Dep.target 存在时(即在 watcher 求值中),将 watcher 加入 dep if (Dep.target) { dep.addSub(Dep.target); } return val; }, set(newVal) { if (val === newVal) return; val = newVal; // 新值也需观测 observe(newVal); // 通知所有依赖更新 dep.notify(); } }); }

这段代码的关键在于observe(val)的递归调用。它确保了data.user.profile.name这样的深层属性,也能被get/set劫持。但defineProperty对数组的索引赋值(arr[0] = 1)和长度修改(arr.length = 0)无能为力。为此,“Ruby's Louvre”的解决方案是:重写数组的变异方法。它创建了一个arrayMethods对象,继承自Array.prototype,并重写了pushpopshiftunshiftsplicesortreverse这七个会改变原数组的方法。重写逻辑很简单:在调用原生方法后,手动触发dep.notify()。然后,将data中所有数组的__proto__指向这个arrayMethods。这样,当用户调用data.items.push(item)时,不仅数组内容改变,还会触发依赖更新。这个技巧,是 Vue 2.x 数组响应式的核心秘密,也是“Ruby's Louvre”对原生 API 深度掌控的明证。

3.3 Compiler 模块:从 HTML 字符串到可执行 render 函数

Compiler模块是连接模板与数据的桥梁。它接收一个 HTML 字符串(如'<div>{{msg}}</div>'),输出一个render函数,该函数执行后返回一个虚拟 DOM 节点。其核心流程是“解析 -> 生成”:

  1. 解析(Parse)parseHTML函数遍历 HTML 字符串,识别开始标签<div>、结束标签</div>、文本节点{{msg}}、注释<!-- -->等,并构建 AST。AST 是一个树状对象,例如<div>{{msg}}</div>的 AST 为:

    { "type": "Element", "tag": "div", "children": [ { "type": "Expression", "exp": "msg" } ] }
  2. 生成(Generate)generate函数遍历 AST,根据节点type生成对应的 JavaScript 代码字符串。对Expression节点,它生成scope.msg;对Element节点,它生成h('div', {}, [scope.msg])。最终,整个 AST 被编译为一个render函数体:

    with(this) { return h('div', {}, [msg]) }

    这里的with(this)是关键,它让msg能直接访问this.msg,无需写this.msg。虽然with语句在严格模式下被禁用,但“Ruby's Louvre”指出,在框架内部可控环境下,它带来的简洁性远超其微小的性能损耗。

  3. 编译(Compile)compileToFunctions函数将parsegenerate组合,用new Function('scope', 'h', code)将生成的代码字符串编译为真正的函数。h是一个虚拟 DOM 创建函数,定义为function h(tag, props, children) { return { tag, props, children }; }。至此,一个模板就变成了一段可执行、可调试的 JavaScript 代码。

3.4 Watcher 与 Dep:响应式系统的“神经突触”

WatcherDep共同构成了响应式系统的“神经网络”。Dep是一个简单的依赖容器:

class Dep { constructor() { this.subs = []; // 存储所有 watcher } addSub(sub) { this.subs.push(sub); } notify() { // 遍历所有 watcher,触发 update this.subs.forEach(sub => sub.update()); } } // 全局唯一 target,用于依赖收集 Dep.target = null;

Watcher则是这个网络中的“神经元”:

class Watcher { constructor(vm, expOrFn, cb) { this.vm = vm; this.cb = cb; this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn); this.value = this.get(); } get() { // 将自己设为全局 target,触发依赖收集 Dep.target = this; const value = this.getter.call(this.vm, this.vm); Dep.target = null; return value; } update() { const oldValue = this.value; this.value = this.get(); this.cb && this.cb(this.value, oldValue); } }

parsePath是一个辅助函数,它将字符串路径"user.name"解析为一个函数function(scope) { return scope.user.name; },这样Watcher就能通过this.getter.call(this.vm)安全地获取值。整个过程形成了一个完美的闭环:Watcher.get()Dep.target = thisobj.prop.get()dep.addSub(this)Dep.target = null。当obj.prop被修改时,dep.notify()watcher.update()watcher.get()→ 触发新的依赖收集。这个闭环,就是响应式系统得以运转的全部奥秘。

4. 常见问题与实战避坑指南:来自十五年一线踩坑的独家经验

4.1 “响应式失效”问题的根因排查与修复

在实际项目中,“数据变了,视图没更新”是最令人抓狂的问题。根据“Ruby's Louvre”的经验,这类问题 90% 以上源于对响应式原理的误解,而非框架 Bug。以下是几个最典型的场景及解决方案:

问题现象根本原因修复方案“Ruby's Louvre” 原文引用
this.obj.newProp = 'value'后视图不更新defineProperty无法监听对象新增属性使用this.$set(this.obj, 'newProp', 'value')Vue.set(this.obj, 'newProp', 'value')defineProperty的盲区:它只能劫持已存在的属性。新增属性,如同在墙上凿新窗,必须用set这把特制的凿子。”
this.arr[index] = newValue后视图不更新数组索引赋值无法被defineProperty捕获使用this.$set(this.arr, index, newValue)this.arr.splice(index, 1, newValue)“数组不是普通对象,它的length和索引是特殊的。splice是唯一能同时触发setlength更新的‘合法’操作。”
this.obj = { ...this.obj, newProp: 'value' }后视图不更新整个对象被替换,旧的响应式引用丢失避免直接赋值新对象,改用this.$setObject.assign(this.obj, { newProp: 'value' })“响应式不是魔法,它依赖于对象的‘身份’。this.obj = {}是斩断了旧的身份,创建了一个全新的、非响应式的躯壳。”

这些经验,都是在无数次线上事故后总结出的“血泪教训”。它提醒我们:框架的 API 设计,永远是其底层原理的忠实映射。理解set$setsplice的存在意义,远比记住它们的用法更重要。

4.2 模板编译性能瓶颈与优化策略

模板编译是一个“一次性成本”,但它对首屏加载时间影响巨大。在大型项目中,一个包含数百个组件的 SPA,其模板编译耗时可能高达 200ms。“Ruby's Louvre”通过大量压测,总结出三大性能杀手:

  1. 正则表达式过度回溯:早期模板解析常使用/\{\{([^}]+)\}\}/g匹配插值,但当模板中出现{{ a }} {{ b }} {{ c }}时,正则引擎会进行大量无效回溯。它推荐改用“状态机”解析:逐字符扫描,遇到{{进入插值状态,遇到}}退出。实测在 10KB 模板下,状态机比正则快 3.7 倍。
  2. AST 构建的深拷贝开销:每次parse都会创建大量临时对象。它建议对 AST 节点进行“对象池”复用,预先创建 100 个ElementNodeTextNode实例,parse时从池中pop()patchpush()回池。这使内存分配次数减少 65%。
  3. new Function的 JIT 编译延迟new Function创建的函数,首次执行时需经过 V8 的 Full-codegen 编译,耗时较长。它给出的终极方案是“预编译”:在构建时(build time)就将模板编译为 JS 代码,打包进 bundle,运行时直接evalimport。这正是 Vue 的vue-loader和 React 的babel-plugin-transform-react-jsx的核心思想。

4.3 跨框架集成与沙箱隔离的实践智慧

在微前端或 legacy 系统改造场景中,经常需要将一个基于“Ruby's Louvre”思想的微型框架,与 React/Vue 应用共存。“Ruby's Louvre”的建议非常务实:不要试图“融合”,而要“隔离”。它提出了“三层沙箱”模型:

  • CSS 沙箱:为每个微型应用的根节点添加唯一>
http://www.jsqmd.com/news/1110891/

相关文章:

  • 从Samba漏洞到Jenkins沦陷:CVE-2017-7494攻击链深度剖析与防御实践
  • 2026毕业季救星!6款AI论文工具实测,从框架到初稿一路畅写
  • 微信小程序抓包实战:从原理到工具配置与安全分析
  • 深度兴趣网络与时间感知在实时推荐系统中的工程实践
  • 企业AI提效五大实操场景:本地化、零API、合规落地
  • 换新手机怕私密笔记、证件照全丢失?这款不上云保险箱一键导出加密备份
  • 3步掌握安卓应用管理神器:APKMirror安卓客户端终极指南
  • Java 微服务向 AI 原生演进:从 Spring Cloud 到 Agentic Platform 的技术路线
  • EmbodiedClaw:对话式工作流如何革新具身智能开发范式
  • 大语言模型如何理解表格数据:表示学习与检索增强生成实践
  • 2026 年求职招聘新变量:AI 重塑行业逻辑,个人开发者机会几何?
  • 112、hypothesis 属性测试:让机器自动生成测试用例,发现你从未想过的边界
  • AI武器化风险与硬件出口控制的动态评估框架
  • BiliDownloader终极指南:简单快速免费下载B站视频的完整教程
  • 暑期旅游邮件营销深度拆解:你的促销邮件为什么没人看?
  • SpringBoot 整合 WebSocket 实现校园二手平台私信聊天,环境配置 + 踩坑记录
  • 帐号注册与帐号登陆互联
  • 毕设查重率高的 8 个高危写法(附降重改写示例)
  • 离线-在线混合强化学习:环境偏移下的遗憾分析与算法设计
  • FeaXDrive:基于轨迹扩散模型与可行性感知GRPO的自动驾驶规划新范式
  • Gemini 2.5 Computer Use生产落地的三大硬门槛
  • Multi-Head Latent Attention:大模型长文本压缩的新范式
  • 静态博客搭建与SEO优化实战指南
  • Git篇(一): 读懂 Git:从 Linux 安装到底层目录、版本回退完整拆解
  • 2026年黑苦荞全株茶大比拼:哪家公司更值得信赖?
  • visual studio 2026 快捷键如何找一个文件?比如SPexc.cpp
  • Windows音频路由终极指南:用Synchronous Audio Router实现专业级音频管理
  • 2026真实测评!主流跨境B2B系统优缺点对比哪款最值得入手?
  • 掌握AI教材编写技巧,低查重AI工具让教材生成不再难!
  • 2026年AI论文软件深度剖析:哪几款能真正贴合学术规范和格式要求