[{"data":1,"prerenderedAt":731},["ShallowReactive",2],{"msfe-doc:\u002Fextensions\u002Fprojections":3},{"id":4,"title":5,"body":6,"description":718,"extension":719,"keywords":720,"locale":723,"meta":724,"navigation":725,"order":233,"path":726,"section":727,"seo":728,"stem":729,"__hash__":730},"docs\u002Fextensions\u002Fprojections.zh-CN.md","投影层与组件生命周期",{"type":7,"value":8,"toc":709},"minimark",[9,14,23,117,121,131,148,151,317,330,345,366,377,380,387,390,425,440,538,545,570,574,625,635,670,673,684,695,698,705],[10,11,13],"h2",{"id":12},"源码描述和-dom-三层","源码、描述和 DOM 三层",[15,16,17,18,22],"p",{},"源码 codec 读取语义；投影层根据已接受输入返回声明式描述；renderer 才创建 DOM 或框架组件。不能在 ",[19,20,21],"code",{},"build()"," 中创建图表、发网络请求或调用编辑器提交事务。",[24,25,26,42],"table",{},[27,28,29],"thead",{},[30,31,32,36,39],"tr",{},[33,34,35],"th",{},"输出",[33,37,38],{},"典型用途",[33,40,41],{},"限制",[43,44,45,59,72,85,102],"tbody",{},[30,46,47,53,56],{},[48,49,50],"td",{},[19,51,52],{},"mark",[48,54,55],{},"高亮、拼写标记",[48,57,58],{},"装饰范围使用规范源码坐标",[30,60,61,66,69],{},[48,62,63],{},[19,64,65],{},"line",[48,67,68],{},"原生段落\u002F容器行样式",[48,70,71],{},"锚定行起点",[30,73,74,79,82],{},[48,75,76],{},[19,77,78],{},"widget",[48,80,81],{},"控件、提示",[48,83,84],{},"组件不可接管正文输入",[30,86,87,96,99],{},[48,88,89,92,93],{},[19,90,91],{},"replace"," + ",[19,94,95],{},"text",[48,97,98],{},"源码中立的字面呈现",[48,100,101],{},"文本不会按 HTML 解释",[30,103,104,111,114],{},[48,105,106,92,108],{},[19,107,91],{},[19,109,110],{},"view",[48,112,113],{},"图表和复杂预览",[48,115,116],{},"跨行结构需要 block 层",[10,118,120],{"id":119},"inline-和-block-不能混用","inline 和 block 不能混用",[15,122,123,126,127,130],{},[19,124,125],{},"placement: 'inline'"," 在有界可见范围中工作，不应返回跨行结构性替换。",[19,128,129],{},"placement: 'block'"," 的结果在 CodeMirror 计算视口之前提供，可参与多行高度计算。",[15,132,133,134,137,138,141,142,147],{},"普通节点源码 renderer 的 ",[19,135,136],{},"placement: 'replace' | 'before'"," 是行内视图入口，不能把它等同于任意整块编辑容器。图片可用特定的 ",[19,139,140],{},"measureHeight"," 协议；大型围栏预览使用 block 层，见 ",[143,144,146],"a",{"href":145},"\u002Fextensions\u002Fdiagrams","图表完整示例","。",[10,149,150],{"id":150},"声明依赖和稳定身份",[152,153,158],"pre",{"className":154,"code":155,"language":156,"meta":157,"style":157},"language-ts shiki shiki-themes github-light github-light github-dark","addProjectionLayers() {\n  const name = this.name\n  return [{\n    key: `${name}.highlight`,\n    placement: 'inline',\n    build(input) {\n      return input.query(name).matches.map(match => ({\n        type: 'mark', from: match.from, to: match.to, class: 'my-source-mark',\n      }))\n    },\n  }]\n}\n","ts","",[19,159,160,172,192,201,220,231,247,276,293,299,305,311],{"__ignoreMap":157},[161,162,164,168],"span",{"class":65,"line":163},1,[161,165,167],{"class":166},"sgQzM","addProjectionLayers",[161,169,171],{"class":170},"sVPnS","() {\n",[161,173,175,179,183,186,189],{"class":65,"line":174},2,[161,176,178],{"class":177},"sDD9d","  const",[161,180,182],{"class":181},"sUNQB"," name",[161,184,185],{"class":177}," =",[161,187,188],{"class":181}," this",[161,190,191],{"class":170},".name\n",[161,193,195,198],{"class":65,"line":194},3,[161,196,197],{"class":177},"  return",[161,199,200],{"class":170}," [{\n",[161,202,204,207,211,214,217],{"class":65,"line":203},4,[161,205,206],{"class":170},"    key: ",[161,208,210],{"class":209},"s-HEn","`${",[161,212,213],{"class":170},"name",[161,215,216],{"class":209},"}.highlight`",[161,218,219],{"class":170},",\n",[161,221,223,226,229],{"class":65,"line":222},5,[161,224,225],{"class":170},"    placement: ",[161,227,228],{"class":209},"'inline'",[161,230,219],{"class":170},[161,232,234,237,240,244],{"class":65,"line":233},6,[161,235,236],{"class":166},"    build",[161,238,239],{"class":170},"(",[161,241,243],{"class":242},"sde8i","input",[161,245,246],{"class":170},") {\n",[161,248,250,253,256,259,262,265,267,270,273],{"class":65,"line":249},7,[161,251,252],{"class":177},"      return",[161,254,255],{"class":170}," input.",[161,257,258],{"class":166},"query",[161,260,261],{"class":170},"(name).matches.",[161,263,264],{"class":166},"map",[161,266,239],{"class":170},[161,268,269],{"class":242},"match",[161,271,272],{"class":177}," =>",[161,274,275],{"class":170}," ({\n",[161,277,279,282,285,288,291],{"class":65,"line":278},8,[161,280,281],{"class":170},"        type: ",[161,283,284],{"class":209},"'mark'",[161,286,287],{"class":170},", from: match.from, to: match.to, class: ",[161,289,290],{"class":209},"'my-source-mark'",[161,292,219],{"class":170},[161,294,296],{"class":65,"line":295},9,[161,297,298],{"class":170},"      }))\n",[161,300,302],{"class":65,"line":301},10,[161,303,304],{"class":170},"    },\n",[161,306,308],{"class":65,"line":307},11,[161,309,310],{"class":170},"  }]\n",[161,312,314],{"class":65,"line":313},12,[161,315,316],{"class":170},"}\n",[15,318,319,320,323,324,326,327,329],{},"层的 ",[19,321,322],{},"key"," 全局唯一；视图条目的 ",[19,325,322],{}," 在层内稳定。不要在 ",[19,328,21],{}," 中创建新的 renderer 函数，避免每次状态改变都卸载组件。示例保留 renderer 模块级常量，但实例资源仍由组件作用域管理。",[15,331,332,333,336,337,340,341,344],{},"读取 ",[19,334,335],{},"input.selection"," 的构建器必须设置 ",[19,338,339],{},"selection: true","；读取状态槽时把槽列入 ",[19,342,343],{},"states","。普通源码显露已由运行时处理，不需要因此让每次光标移动都重建全部图表。",[15,346,347,350,351,354,355,147,358,361,362,365],{},[19,348,349],{},"modes"," 默认 ",[19,352,353],{},"live"," \u002F ",[19,356,357],{},"locked",[19,359,360],{},"suppressDuringComposition"," 默认 true：Markdown 编辑中，仅与输入法选区相交的描述暂时回退源码；替换描述以 ",[19,363,364],{},"revealRange"," 判断完整语义范围，行描述以该行判断。其他块保留呈现与组件身份。隐藏标记的文档编辑保持投影，原生正文仍由 CodeMirror 接受组合输入。",[15,367,368,369,372,373,376],{},"组合输入开始\u002F结束只重新物化已有描述，不要求重建整篇层。预览仍在页面上时，“编辑源码”等操作也必须检查 ",[19,370,371],{},"readSourcePresentation(editor.state).composing","，避免抢走输入法焦点。不要为视觉稳定取消输入保护。冲突替换可能回退源码，可查看 ",[19,374,375],{},"editor.work.projectionConflicts"," 定位所有权争用。",[10,378,379],{"id":379},"普通输入时只更新受影响块",[15,381,382,383,386],{},"当输出只依赖当前语法块时，可以声明 ",[19,384,385],{},"incremental: true","。块级层会收到变更后的完整语法块，未受影响的描述映射到新源码位置并保留组件身份；内联层仍按可见范围查询，并复用映射后的组件身份。省略该选项时继续完整构建，以兼容依赖全文的插件。",[15,388,389],{},"启用前需满足以下约定：",[391,392,393,404,411,420],"ul",{},[394,395,396,397,400,401,403],"li",{},"使用 ",[19,398,399],{},"input.query(name)"," 或本次范围内的源码，输出及 ",[19,402,364],{}," 属于所查询的块。不要在每次构建中扫描全文、给全文编号或生成跨块目录。",[394,405,406,407,410],{},"引用链接依赖其他位置的定义时声明 ",[19,408,409],{},"references: true","，新增、删除或修改定义都会重新构建该层。其他全局依赖仍应使用保守构建。",[394,412,413,416,417,419],{},[19,414,415],{},"selection"," 和 ",[19,418,343],{}," 仍须声明；权限、模式或这些依赖变化时，运行时会重新评估完整范围。",[394,421,422,424],{},[19,423,322],{}," 在一次构建中标识唯一源码位置，renderer 保持稳定。组件身份由运行时沿源码变化映射，整块覆盖或删除会终止旧身份。",[15,426,427,428,431,432,435,436,439],{},"如果组件属性带有源码位置，用 ",[19,429,430],{},"componentView"," 的第三个参数显式映射。下面是放在 ",[19,433,434],{},"input.query(name).matches.map(...)"," 中的视图片段，",[19,437,438],{},"renderPreview"," 是模块级 renderer：",[152,441,443],{"className":154,"code":442,"language":156,"meta":157,"style":157},"view: componentView(renderPreview, {\n  source: input.document.slice(match.from, match.to),\n  range: { from: match.from, to: match.to },\n}, (props, offset) => ({\n  ...props,\n  range: {\n    from: props.range.from + offset,\n    to: props.range.to + offset,\n  },\n}))\n",[19,444,445,457,468,473,495,503,508,519,528,533],{"__ignoreMap":157},[161,446,447,449,452,454],{"class":65,"line":163},[161,448,110],{"class":166},[161,450,451],{"class":170},": ",[161,453,430],{"class":166},[161,455,456],{"class":170},"(renderPreview, {\n",[161,458,459,462,465],{"class":65,"line":174},[161,460,461],{"class":170},"  source: input.document.",[161,463,464],{"class":166},"slice",[161,466,467],{"class":170},"(match.from, match.to),\n",[161,469,470],{"class":65,"line":194},[161,471,472],{"class":170},"  range: { from: match.from, to: match.to },\n",[161,474,475,478,481,484,487,490,493],{"class":65,"line":203},[161,476,477],{"class":170},"}, (",[161,479,480],{"class":242},"props",[161,482,483],{"class":170},", ",[161,485,486],{"class":242},"offset",[161,488,489],{"class":170},") ",[161,491,492],{"class":177},"=>",[161,494,275],{"class":170},[161,496,497,500],{"class":65,"line":222},[161,498,499],{"class":177},"  ...",[161,501,502],{"class":170},"props,\n",[161,504,505],{"class":65,"line":233},[161,506,507],{"class":170},"  range: {\n",[161,509,510,513,516],{"class":65,"line":249},[161,511,512],{"class":170},"    from: props.range.from ",[161,514,515],{"class":177},"+",[161,517,518],{"class":170}," offset,\n",[161,520,521,524,526],{"class":65,"line":278},[161,522,523],{"class":170},"    to: props.range.to ",[161,525,515],{"class":177},[161,527,518],{"class":170},[161,529,530],{"class":65,"line":295},[161,531,532],{"class":170},"  },\n",[161,534,535],{"class":65,"line":301},[161,536,537],{"class":170},"}))\n",[15,539,540,541,544],{},"映射函数只返回新属性，不创建 DOM、请求资源或提交事务。renderer 的 ",[19,542,543],{},"update(next)"," 必须保存最新属性，点击操作不能闭包捕获第一次挂载的位置；预览内容没有变化时，应保留现有图表实例。这样在前方输入后，即使预览稍后才滚入视口，其“编辑源码”操作也会定位到正确块。",[15,546,547,548,551,552,555,556,555,559,555,562,565,566,569],{},"局部更新不等于只解析光标所在行。修改代码围栏、列表\u002F容器边界、表格分隔行或引用定义，可能合法地影响前后块；运行时以解析器确认的结构变化扩大更新范围。可对比 ",[19,549,550],{},"editor.work"," 中的 ",[19,553,554],{},"syntaxDiffBuilds","、",[19,557,558],{},"syntaxDiffChunks",[19,560,561],{},"syntaxDiffComparisons",[19,563,564],{},"layerVisitedNodes"," 与 ",[19,567,568],{},"blockBuilds","，并验证 DOM\u002F资源是否复用。当前仍需映射既有描述并生成装饰，不保证所有输入都是恒定成本。",[10,571,573],{"id":572},"componentview-与-nodeview","ComponentView 与 NodeView",[24,575,576,589],{},[27,577,578],{},[30,579,580,583,586],{},[33,581,582],{},"协议",[33,584,585],{},"输入",[33,587,588],{},"场景",[43,590,591,611],{},[30,592,593,596,608],{},[48,594,595],{},"NodeView",[48,597,598,555,601,555,604,607],{},[19,599,600],{},"nodeView.attrs",[19,602,603],{},"updateAttributes",[19,605,606],{},"getRange"," 等",[48,609,610],{},"与源码字段绑定的控件",[30,612,613,616,622],{},[48,614,615],{},"ComponentView",[48,617,618,619,621],{},"自定义 ",[19,620,110],{}," 属性",[48,623,624],{},"搜索、折叠按钮、图表等通用组件",[15,626,627,628,354,631,634],{},"Vue 使用 ",[19,629,630],{},"VueNodeViewRenderer",[19,632,633],{},"VueComponentViewRenderer","，React 使用对应适配器。两类视图都需要原来的 Provider 树中的 Host；不要给每个节点创建独立应用。",[15,636,637,638,641,642,555,645,555,647,555,650,416,653,656,657,555,660,663,664,147,667,669],{},"原生 ",[19,639,640],{},"ComponentViewRenderer\u003CProps>"," 取得 ",[19,643,644],{},"dom",[19,646,480],{},[19,648,649],{},"host",[19,651,652],{},"scope",[19,654,655],{},"requestMeasure","，返回 ",[19,658,659],{},"update",[19,661,662],{},"destroy","，可选 ",[19,665,666],{},"ignoreEvent",[19,668,666],{}," 默认 true；只有明确希望 CodeMirror 消费的事件才返回 false。",[10,671,672],{"id":672},"高度与卸载",[15,674,675,676,679,680,683],{},"挂载容器归运行时所有。renderer 管理自己的子 DOM、监听器、观察器和第三方实例，不改写 ",[19,677,678],{},".cm-content","。异步图片或图表尺寸变化后调用 ",[19,681,682],{},"requestMeasure()","，它合并测量请求。",[15,685,686,687,690,691,147],{},"视图更新应复用资源；源码或关键渲染参数变化时取消旧请求。",[19,688,689],{},"destroy()"," 移除框架入口，scope 结束时释放资源。二者都要能正确处理未完成的加载。完整模式见 ",[143,692,694],{"href":693},"\u002Fextensions\u002Fresources","资源生命周期",[10,696,697],{"id":697},"需要验证的交互",[15,699,700,701,147],{},"检查鼠标拖选经过预览、键盘进入源码、搜索命中、只读复制、IME、滚动卸载、快速更新及换文档。先比较工作计数变化和资源存活数量，再检查最终画面；光标移动不应触发全文解析或所有图表重新初始化。协议见 ",[143,702,704],{"href":703},"\u002Fapi\u002Frendering","渲染 API",[706,707,708],"style",{},"html pre.shiki code .sgQzM, html code.shiki .sgQzM{--shiki-default:#6F42C1;--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVPnS, html code.shiki .sVPnS{--shiki-default:#24292E;--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sDD9d, html code.shiki .sDD9d{--shiki-default:#D73A49;--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sUNQB, html code.shiki .sUNQB{--shiki-default:#005CC5;--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s-HEn, html code.shiki .s-HEn{--shiki-default:#032F62;--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sde8i, html code.shiki .sde8i{--shiki-default:#E36209;--shiki-light:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":157,"searchDepth":174,"depth":174,"links":710},[711,712,713,714,715,716,717],{"id":12,"depth":174,"text":13},{"id":119,"depth":174,"text":120},{"id":150,"depth":174,"text":150},{"id":379,"depth":174,"text":379},{"id":572,"depth":174,"text":573},{"id":672,"depth":174,"text":672},{"id":697,"depth":174,"text":697},"正确实现内联装饰、跨行预览、状态依赖和测量。","md",[721,615,595,655,722],"ProjectionLayer","block","zh-CN",{},true,"\u002Fextensions\u002Fprojections","扩展开发",{"title":5,"description":718},"extensions\u002Fprojections.zh-CN","dObOKdhlaMq7TKiTh8S15RtLk2Oh0QGyTiGbqe1fFwQ",1793046311420]