[{"data":1,"prerenderedAt":578},["ShallowReactive",2],{"article-frontend\u002Fvue3":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"tags":11,"body":13,"_type":572,"_id":573,"_source":574,"_file":575,"_stem":576,"_extension":577},"\u002Farticles\u002Ffrontend\u002Fvue3","frontend",false,"","Vue3 框架设计与源码解析","从全局视角理解 Vue3 框架设计思想，涵盖声明式与命令式、运行时与编译时、编译器与渲染器的核心原理","2023-03-23",[12],"软件工程",{"type":14,"children":15,"toc":559},"root",[16,21,29,34,40,46,51,56,61,70,75,80,110,115,123,133,138,146,151,159,164,172,177,185,193,201,209,214,219,226,231,236,244,251,256,264,272,280,288,296,306,316,322,327,332,340,348,358,363,369,374,383,388,394,403,408,413,418,426,433,441,462,470,483,488,493,501,509,514,522,527],{"type":17,"tag":18,"props":19,"children":20},"element","hr",{},[],{"type":17,"tag":22,"props":23,"children":25},"h2",{"id":24},"theme-fancy",[26],{"type":27,"value":28},"text","theme: fancy",{"type":17,"tag":22,"props":30,"children":32},{"id":31},"框架设计概述",[33],{"type":27,"value":31},{"type":17,"tag":35,"props":36,"children":38},"h3",{"id":37},"前言",[39],{"type":27,"value":37},{"type":17,"tag":41,"props":42,"children":43},"p",{},[44],{"type":27,"value":45},"作为学习者，我们在学习框架的时候，很容易被细节困住，看不清全貌。所以我们要先从全局的角度对框架的设计拥有清晰的认知。",{"type":17,"tag":41,"props":47,"children":48},{},[49],{"type":27,"value":50},"这篇文章作为学习vue3源码细节的前篇，让大家对vue3有个全面大体的认识",{"type":17,"tag":35,"props":52,"children":54},{"id":53},"声明式与命令式",[55],{"type":27,"value":53},{"type":17,"tag":41,"props":57,"children":58},{},[59],{"type":27,"value":60},"从范式的角度来看，框架应该设计成命令式还是声明式的、要设计成纯运行时还是纯编译型的、甚至是运行时+编译时的呢",{"type":17,"tag":41,"props":62,"children":63},{},[64],{"type":17,"tag":65,"props":66,"children":67},"strong",{},[68],{"type":27,"value":69},"我们先举一个现实中的例子：",{"type":17,"tag":41,"props":71,"children":72},{},[73],{"type":27,"value":74},"张三的妈妈让张三去买酱油",{"type":17,"tag":41,"props":76,"children":77},{},[78],{"type":27,"value":79},"张三需要这么做：",{"type":17,"tag":81,"props":82,"children":83},"ol",{},[84,90,95,100,105],{"type":17,"tag":85,"props":86,"children":87},"li",{},[88],{"type":27,"value":89},"拿起钱",{"type":17,"tag":85,"props":91,"children":92},{},[93],{"type":27,"value":94},"打开门",{"type":17,"tag":85,"props":96,"children":97},{},[98],{"type":27,"value":99},"到商店",{"type":17,"tag":85,"props":101,"children":102},{},[103],{"type":27,"value":104},"拿钱买酱油",{"type":17,"tag":85,"props":106,"children":107},{},[108],{"type":27,"value":109},"回到家",{"type":17,"tag":41,"props":111,"children":112},{},[113],{"type":27,"value":114},"这里张三妈妈的行为就是声明式，张三就是命令式",{"type":17,"tag":41,"props":116,"children":117},{},[118],{"type":17,"tag":65,"props":119,"children":120},{},[121],{"type":27,"value":122},"我们再用一个编程的例子来理解声明式与命令式，需求：",{"type":17,"tag":124,"props":125,"children":127},"pre",{"code":126},"1. 获取 id 为 app 的 div 标签\n2. 它的文本内容为 hello world\n3. 为其绑定点击事件\n4. 当点击时弹出提示：ok\n",[128],{"type":17,"tag":129,"props":130,"children":131},"code",{"__ignoreMap":7},[132],{"type":27,"value":126},{"type":17,"tag":41,"props":134,"children":135},{},[136],{"type":27,"value":137},"命令式：看重过程，指示编译器每一步该怎么做，最终实现结果",{"type":17,"tag":124,"props":139,"children":141},{"code":140},"const div = document.querySelector('#app') \u002F\u002F 获取 div\ndiv.innerText = 'hello world' \u002F\u002F 设置文本内容\ndiv.addEventListener('click', () => { alert('ok') }) \u002F\u002F 绑定点击事件\n",[142],{"type":17,"tag":129,"props":143,"children":144},{"__ignoreMap":7},[145],{"type":27,"value":140},{"type":17,"tag":41,"props":147,"children":148},{},[149],{"type":27,"value":150},"声明式：看重结果，将结果告诉编译器，编译器帮你完成过程",{"type":17,"tag":124,"props":152,"children":154},{"code":153},"\u003Cdiv @click=\"() => alert('ok')\">hello world\u003C\u002Fdiv>\n",[155],{"type":17,"tag":129,"props":156,"children":157},{"__ignoreMap":7},[158],{"type":27,"value":153},{"type":17,"tag":41,"props":160,"children":161},{},[162],{"type":27,"value":163},"两者差异：对于用户来说，声明式更快捷方便，可维护性更强，命令式不方便快捷，但性能更好",{"type":17,"tag":41,"props":165,"children":166},{},[167],{"type":17,"tag":65,"props":168,"children":169},{},[170],{"type":27,"value":171},"举例：",{"type":17,"tag":41,"props":173,"children":174},{},[175],{"type":27,"value":176},"假设现在我们要将 div 标签的文本内容修改为 hello vue3",{"type":17,"tag":41,"props":178,"children":179},{},[180],{"type":17,"tag":65,"props":181,"children":182},{},[183],{"type":27,"value":184},"命令式：",{"type":17,"tag":124,"props":186,"children":188},{"code":187},"div.textContent = 'hello vue3' \u002F\u002F 直接修改需要改动的地方\n",[189],{"type":17,"tag":129,"props":190,"children":191},{"__ignoreMap":7},[192],{"type":27,"value":187},{"type":17,"tag":41,"props":194,"children":195},{},[196],{"type":17,"tag":65,"props":197,"children":198},{},[199],{"type":27,"value":200},"声明式：",{"type":17,"tag":124,"props":202,"children":204},{"code":203},"01 \u003C!-- 之前： -->\n02 \u003Cdiv @click=\"() => alert('ok')\">hello world\u003C\u002Fdiv>\n03 \u003C!-- 之后： -->\n04 \u003Cdiv @click=\"() => alert('ok')\">hello vue3\u003C\u002Fdiv>  \u002F\u002F 需要全部重新渲染\n",[205],{"type":17,"tag":129,"props":206,"children":207},{"__ignoreMap":7},[208],{"type":27,"value":203},{"type":17,"tag":41,"props":210,"children":211},{},[212],{"type":27,"value":213},"这里我们可以发现两者主要的区别是声明式无论修改了什么，整条语句都会重新执行",{"type":17,"tag":41,"props":215,"children":216},{},[217],{"type":27,"value":218},"所以为了实现最优的更新性能，声明式需要找到前后的差异并只更新变化的地方，但是最终完成这次更新的代码仍然是：",{"type":17,"tag":124,"props":220,"children":221},{"code":187},[222],{"type":17,"tag":129,"props":223,"children":224},{"__ignoreMap":7},[225],{"type":27,"value":187},{"type":17,"tag":41,"props":227,"children":228},{},[229],{"type":27,"value":230},"这也是vue3相较于vue2做出的调整「diff算法的优化」",{"type":17,"tag":35,"props":232,"children":234},{"id":233},"运行时与编译时",[235],{"type":27,"value":233},{"type":17,"tag":41,"props":237,"children":238},{},[239],{"type":17,"tag":65,"props":240,"children":241},{},[242],{"type":27,"value":243},"我们用一个例子来理解声明式与命令式",{"type":17,"tag":245,"props":246,"children":248},"h4",{"id":247},"纯运行时",[249],{"type":27,"value":250},"纯运行时：",{"type":17,"tag":41,"props":252,"children":253},{},[254],{"type":27,"value":255},"我们设计一个框架，它提供一个 Render 函数，用户可以为该函数提供一个树型结构的数据对象，然后 Render 函数会根据该对象递归地将数据渲染成 DOM 元素。我们规定树型结构的数据对象如下：",{"type":17,"tag":124,"props":257,"children":259},{"code":258},"const obj = {\n  tag: 'div',\n  children: [\n    { tag: 'span', children: 'hello world' }\n  ]\n}\n",[260],{"type":17,"tag":129,"props":261,"children":262},{"__ignoreMap":7},[263],{"type":27,"value":258},{"type":17,"tag":41,"props":265,"children":266},{},[267],{"type":17,"tag":65,"props":268,"children":269},{},[270],{"type":27,"value":271},"Render函数：",{"type":17,"tag":124,"props":273,"children":275},{"code":274},"function Render(obj, root) {\n  const el = document.createElement(obj.tag)\n  if (typeof obj.children === 'string') {\n    const text = document.createTextNode(obj.children)\n    el.appendChild(text)\n  } else if (obj.children) {\n    \u002F\u002F 数组，递归调用 Render，使用 el 作为 root 参数\n    obj.children.forEach((child) => Render(child, el))\n  }\n​\n  \u002F\u002F 将元素添加到 root\n  root.appendChild(el)\n}\n",[276],{"type":17,"tag":129,"props":277,"children":278},{"__ignoreMap":7},[279],{"type":27,"value":274},{"type":17,"tag":41,"props":281,"children":282},{},[283],{"type":17,"tag":65,"props":284,"children":285},{},[286],{"type":27,"value":287},"用户可以这样使用：",{"type":17,"tag":124,"props":289,"children":291},{"code":290},"const obj = {\n  tag: 'div',\n  children: [\n    { tag: 'span', children: 'hello world' }\n  ]\n}\n\u002F\u002F 渲染到 body 下\nRender(obj, document.body)\n",[292],{"type":17,"tag":129,"props":293,"children":294},{"__ignoreMap":7},[295],{"type":27,"value":290},{"type":17,"tag":41,"props":297,"children":298},{},[299,304],{"type":17,"tag":65,"props":300,"children":301},{},[302],{"type":27,"value":303},"这就是纯运行时框架",{"type":27,"value":305},"：让用户生成树型结构对象，框架提供reader函数将对象渲染成页面，最重要的是等代码执行的时候，也就是render函数执行的时候才会生成html标签。",{"type":17,"tag":41,"props":307,"children":308},{},[309,314],{"type":17,"tag":65,"props":310,"children":311},{},[312],{"type":27,"value":313},"缺点",{"type":27,"value":315},"：是html对象需要用户提供，并且不能进行差别更新",{"type":17,"tag":245,"props":317,"children":319},{"id":318},"运行时-编译时",[320],{"type":27,"value":321},"运行时 + 编译时：",{"type":17,"tag":41,"props":323,"children":324},{},[325],{"type":27,"value":326},"于是有一天，你的用户抱怨说：“手写树型结构的数据对象太麻烦了，而且不直观，能不能支持用类似于HTML 标签的方式描述树型结构的数据对象",{"type":17,"tag":41,"props":328,"children":329},{},[330],{"type":27,"value":331},"为此，你编写了一个叫作 Compiler 的程序，它的作用就是把 HTML 字符串编译成树型结构的数据对象",{"type":17,"tag":41,"props":333,"children":334},{},[335],{"type":17,"tag":65,"props":336,"children":337},{},[338],{"type":27,"value":339},"用户可以这么用：",{"type":17,"tag":124,"props":341,"children":343},{"code":342},"const html = `\n\u003Cdiv>\n  \u003Cspan>hello world\u003C\u002Fspan>\n\u003C\u002Fdiv>\n`\n\u002F\u002F 调用 Compiler 编译得到树型结构的数据对象\nconst obj = Compiler(html)\n\u002F\u002F 再调用 Render 进行渲染\nRender(obj, document.body)\n",[344],{"type":17,"tag":129,"props":345,"children":346},{"__ignoreMap":7},[347],{"type":27,"value":342},{"type":17,"tag":41,"props":349,"children":350},{},[351,356],{"type":17,"tag":65,"props":352,"children":353},{},[354],{"type":27,"value":355},"这就是运行时+编译时：",{"type":27,"value":357}," 框架提供Compiler函数「将html标签生成html对象」和Render对象，这样我们可以根据html标签的不同，进行差别更新html对象，不需要全部更新。用户可以直接提供数据对象从而无须编译；又支持编译时，用户可以提供 HTML 字符串，我们将其编译为数据对象后再交给运行时处理。",{"type":17,"tag":41,"props":359,"children":360},{},[361],{"type":27,"value":362},"Vue采用的就是这种方式",{"type":17,"tag":245,"props":364,"children":366},{"id":365},"纯编译时",[367],{"type":27,"value":368},"纯编译时：",{"type":17,"tag":41,"props":370,"children":371},{},[372],{"type":27,"value":373},"简单来说就是将HTML字符串直接编译成命令时代码",{"type":17,"tag":41,"props":375,"children":376},{},[377],{"type":17,"tag":378,"props":379,"children":382},"img",{"alt":380,"src":381},"vue3纯编译时.png","https:\u002F\u002Fp9-juejin.byteimg.com\u002Ftos-cn-i-k3u1fbpfcp\u002F92cbf3e4888549a6a733c4729e8e910d~tplv-k3u1fbpfcp-watermark.image?",[],{"type":17,"tag":41,"props":384,"children":385},{},[386],{"type":27,"value":387},"优缺点：直接编译成可执行的 JavaScript 代码，因此性能会更好，但有损灵活性，即用户提供的内容必须编译后才能用",{"type":17,"tag":35,"props":389,"children":391},{"id":390},"tree-shaking",[392],{"type":27,"value":393},"Tree Shaking",{"type":17,"tag":395,"props":396,"children":397},"blockquote",{},[398],{"type":17,"tag":41,"props":399,"children":400},{},[401],{"type":27,"value":402},"消除那些永远不会执行的代码",{"type":17,"tag":41,"props":404,"children":405},{},[406],{"type":27,"value":407},"vue.js对于那些永远不会用到的代码，在打包的时候会将它清除，减少包的体积",{"type":17,"tag":35,"props":409,"children":411},{"id":410},"编译器与渲染器",[412],{"type":27,"value":410},{"type":17,"tag":245,"props":414,"children":416},{"id":415},"编译器",[417],{"type":27,"value":415},{"type":17,"tag":395,"props":419,"children":420},{},[421],{"type":17,"tag":41,"props":422,"children":423},{},[424],{"type":27,"value":425},"作用：将模版(template标签)编译成虚拟DOM，交给渲染器，在script标签中，所有在vue中使用模版或者虚拟DOM对象编写页面最后的结果是一样",{"type":17,"tag":41,"props":427,"children":428},{},[429],{"type":17,"tag":65,"props":430,"children":431},{},[432],{"type":27,"value":171},{"type":17,"tag":124,"props":434,"children":436},{"code":435},"\u003Ctemplate>\n  \u003Cdiv @click=\"handler\">\n    click me\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n​\n\u003Cscript>\nexport default {\n  data() {\u002F* ... *\u002F},\n  methods: {\n    handler: () => {\u002F* ... *\u002F}\n  }\n}\n\u003C\u002Fscript>\n",[437],{"type":17,"tag":129,"props":438,"children":439},{"__ignoreMap":7},[440],{"type":27,"value":435},{"type":17,"tag":41,"props":442,"children":443},{},[444,446,452,454,460],{"type":27,"value":445},"其中",{"type":17,"tag":129,"props":447,"children":449},{"className":448},[],[450],{"type":27,"value":451},"\u003Ctemplate>",{"type":27,"value":453}," 标签里的内容就是模板内容，编译器会把模板内容编译成渲染函数并添加到 ",{"type":17,"tag":129,"props":455,"children":457},{"className":456},[],[458],{"type":27,"value":459},"\u003Cscript>",{"type":27,"value":461},"标签块的组件对象上，所以最终在浏览器里运行的代码就是：",{"type":17,"tag":124,"props":463,"children":465},{"code":464},"01 export default {\n02   data() {\u002F* ... *\u002F},\n03   methods: {\n04     handler: () => {\u002F* ... *\u002F}\n05   },\n06   render() {\n07     return h('div', { onClick: handler }, 'click me')\n08   }\n09 }\n",[466],{"type":17,"tag":129,"props":467,"children":468},{"__ignoreMap":7},[469],{"type":27,"value":464},{"type":17,"tag":41,"props":471,"children":472},{},[473,475,481],{"type":27,"value":474},"如果你学过",{"type":17,"tag":129,"props":476,"children":478},{"className":477},[],[479],{"type":27,"value":480},"react",{"type":27,"value":482},"，你会觉得编译后的代码很react很相似。",{"type":17,"tag":41,"props":484,"children":485},{},[486],{"type":27,"value":487},"所以，无论是使用模板还是直接手写渲染函数，对于一个组件来说，它要渲染的内容最终都是通过渲染函数产生的，然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM",{"type":17,"tag":245,"props":489,"children":491},{"id":490},"渲染器",[492],{"type":27,"value":490},{"type":17,"tag":395,"props":494,"children":495},{},[496],{"type":17,"tag":41,"props":497,"children":498},{},[499],{"type":27,"value":500},"渲染器(reader函数)将虚拟DOM渲染成真实DOM",{"type":17,"tag":124,"props":502,"children":504},{"code":503},"01 function renderer(vnode, container) {\n02   \u002F\u002F 使用 vnode.tag 作为标签名称创建 DOM 元素\n03   const el = document.createElement(vnode.tag)\n04   \u002F\u002F 遍历 vnode.props，将属性、事件添加到 DOM 元素\n05   for (const key in vnode.props) {\n06     if (\u002F^on\u002F.test(key)) {\n07       \u002F\u002F 如果 key 以 on 开头，说明它是事件\n08       el.addEventListener(\n09         key.substr(2).toLowerCase(), \u002F\u002F 事件名称 onClick ---> click\n10         vnode.props[key] \u002F\u002F 事件处理函数\n11       )\n12     }\n13   }\n14\n15   \u002F\u002F 处理 children\n16   if (typeof vnode.children === 'string') {\n17     \u002F\u002F 如果 children 是字符串，说明它是元素的文本子节点\n18     el.appendChild(document.createTextNode(vnode.children))\n19   } else if (Array.isArray(vnode.children)) {\n20     \u002F\u002F 递归地调用 renderer 函数渲染子节点，使用当前元素 el 作为挂载点\n21     vnode.children.forEach(child => renderer(child, el))\n22   }\n23\n24   \u002F\u002F 将元素添加到挂载点下\n25   container.appendChild(el)\n26 }\n",[505],{"type":17,"tag":129,"props":506,"children":507},{"__ignoreMap":7},[508],{"type":27,"value":503},{"type":17,"tag":41,"props":510,"children":511},{},[512],{"type":27,"value":513},"编译器和渲染器本质都是函数",{"type":17,"tag":41,"props":515,"children":516},{},[517],{"type":17,"tag":65,"props":518,"children":519},{},[520],{"type":27,"value":521},"如前所述，组件的实现依赖于渲染器，模板的编译依赖于编译器",{"type":17,"tag":35,"props":523,"children":525},{"id":524},"总结",[526],{"type":27,"value":524},{"type":17,"tag":81,"props":528,"children":529},{},[530,544,549,554],{"type":17,"tag":85,"props":531,"children":532},{},[533,535],{"type":27,"value":534},"声明式：Vue.js 是一个声明式的框架，好处在于，它直接描述结果，用户不需要关注过程。vue.js同样支持使用虚拟DOM来描述UI。",{"type":17,"tag":81,"props":536,"children":538},{"start":537},0,[539],{"type":17,"tag":85,"props":540,"children":541},{},[542],{"type":27,"value":543},"运行时+编译时：vue.js采 用 运行时+编译时的方式 来降低用户的心智负担，并且保证框架的性能",{"type":17,"tag":85,"props":545,"children":546},{},[547],{"type":27,"value":548},"TreeShaking： vue.js对于那些永远不会用到的代码，在打包的时候会将它清除，减少包的体积",{"type":17,"tag":85,"props":550,"children":551},{},[552],{"type":27,"value":553},"编译器：vue.js的模版会被编译器编译成渲染函数",{"type":17,"tag":85,"props":555,"children":556},{},[557],{"type":27,"value":558},"渲染器：将虚拟 DOM 对象渲染为真实 DOM 元素",{"title":7,"searchDepth":560,"depth":560,"links":561},2,[562,563],{"id":24,"depth":560,"text":28},{"id":31,"depth":560,"text":31,"children":564},[565,567,568,569,570,571],{"id":37,"depth":566,"text":37},3,{"id":53,"depth":566,"text":53},{"id":233,"depth":566,"text":233},{"id":390,"depth":566,"text":393},{"id":410,"depth":566,"text":410},{"id":524,"depth":566,"text":524},"markdown","content:articles:frontend:vue3源码解析.md","content","articles\u002Ffrontend\u002Fvue3源码解析.md","articles\u002Ffrontend\u002Fvue3源码解析","md",1779811690463]