做前端这些年,我有个特别直观的感受:很多同事Vue业务写得飞快,组件、路由、状态管理玩得贼溜,可一旦被追问"数据改变时视图是怎么刷新的""key到底解决了什么问题""v-for里到底能不能用index",很多人就沉默了。Vue核心(一)这个系列的目的很单纯——把Vue家门口那块地基挖开,让你看看底下到底是什么结构。
这套内容我建议三类人重点看:第一类是写Vue写了一两年、想从"会用"跳到"懂原理"的前端开发;第二类是准备面试的工程师,Vue核心机制几乎是必考区;第三类是已经在做技术方案评审或者带新人的同学,你需要把"为什么选Vue""Vue的边界在哪"讲清楚,光会写代码不够。这篇是系列第一篇,我会把Vue最核心的三条主线拎出来:响应式系统怎么工作、虚拟DOM和diff在干嘛、模板编译的过程。全程用可复现的代码和真实踩坑记录来讲,不堆术语,更不背书。
先放一个总纲。Vue这个框架,表面上看是"写模板 + 写数据 + 写方法",实际上它内部只干三件事:监听数据变化(响应式)、把模板变成可执行的渲染函数(编译)、用虚拟DOM对比新旧状态来高效更新页面(diff)。这三件事互相咬合,任何一环出了问题,你看到的症状都是"页面不对",但根源可能差得很远。后面每一章,我都会先说清楚它解决什么问题,再拆实现逻辑,最后给实操建议。
1. Vue核心设计理念:框架到底帮你做了什么
1.1 核心模块的三角关系
如果把Vue的源码摊开看,可以粗暴地分成三大块:reactivity(响应式模块)、compiler(编译器)、runtime(运行时渲染)。它们之间的关系可以用一句话概括:compiler把template编译成render函数,render函数执行时会读取响应式数据,从而触发依赖收集,数据变化后再由runtime通过虚拟DOM diff找到需要更新的节点,最后操作真实DOM。
这里有个关键理解:Vue的"声明式渲染"本质上是把"操作DOM"这个脏活累活从你手里接走了。你自己写原生JS时,要手动getElementById、手动改innerHTML、手动维护状态和视图的一致性;写Vue时你只需要声明"数据是什么、页面长什么样",剩下的同步工作交给框架。作为开发者,理解这个三角关系是第一步,因为后续所有对Vue的疑问,追根溯源都会落回这三个模块之一。
额外提一句源码层面的经验:如果你去看Vue 3的源码目录,会发现packages下面有reactivity、compiler-core、runtime-core等独立包,每个包可以单独发布和测试。这个拆分不光是工程上的整洁,更是概念边界的体现——你要理解Vue,最好也按这个边界去理解,不要混着学。
1.2 从日常写法反推设计取舍
很多人觉得Vue"好用"但说不出为什么好用。我的理解是,Vue做了三个非常关键的设计决策,每个都是取舍后的结果。
第一个决策是模板优先。Vue选择让用户写HTML风格的模板,而不是像早期React那样用JSX(当然现在Vue也支持JSX,但主推还是模板)。模板的好处是对后端转前端的同学友好、静态分析容易做优化、并且编译器可以在编译期帮我们做不少事情(比如静态节点提升)。代价是模板的能力上限低于完整JS,遇到特别灵活的场景时得退回render函数或JSX。
第二个决策是响应式自动追踪。Vue不需要你手动声明"哪些数据会影响哪个视图",它在数据读取时自动记录依赖、在数据修改时自动通知更新。这个设计让业务代码非常干净,但也带来了"依赖追踪有时会失灵"的隐患,后面我会花一整章讲。
第三个决策是渐进式框架的定位。Vue不像某些框架那样上来就要求全家桶,你可以在一个页面里引入一个script标签就开始用,也可以搭完整的工程化体系。这意味着Vue的核心必须足够自洽、足够独立,这也反过来塑造了它的源码结构。
这三个决策合在一起,决定了Vue的"性格":上手容易、中间层能力强大、边界清晰。理解了设计取舍,你在遇到"这个功能Vue能不能做""Vue适合不适合我现在的项目"这类问题时,就有自己的判断力了,而不是去网上问别人。
2. 响应式系统:数据变化如何驱动视图更新
2.1 两代实现:defineProperty与Proxy
Vue 2的响应式建立在Object.defineProperty之上,Vue 3换成了Proxy。这个替换不是Vue团队闲得慌,而是defineProperty有几个天生缺陷。
第一个缺陷是:defineProperty只能劫持对象已有的属性,新增属性和删除属性它管不着。所以Vue 2才需要Vue.set和Vue.delete这两个补丁API。实际开发中我见过太多人忘记用Vue.set,结果数据变了页面不动,排查半天发现是新增属性没有响应式。
第二个缺陷是:defineProperty需要递归遍历对象的每个属性来设置getter/setter,对象层级越深初始化越慢,而且这个慢发生在首次渲染之前,体感明显。Proxy则不同,它劫持的是整个对象,不需要递归设置属性,在访问时才做懒代理,性能分布更均匀。
第三个缺陷是:defineProperty无法监听数组的各种变更方法,Vue 2只能通过重写push、pop、shift、unshift、splice、sort、reverse这七个方法来模拟,而且像arr[0] = xxx这种索引赋值和arr.length = 0这种操作在Vue 2里都是不响应的。Vue 3的Proxy天然支持这些。
有一个很形象的类比:defineProperty像你在每间教室门口安排一个保安,每个保安只盯自己那间教室,得先给整栋楼的每间教室都排好保安(递归初始化);Proxy则是在教学楼门口安排一个总闸,任何人进出教学楼都得经过它,而且不需要提前知道楼里有多少间教室。
2.2 依赖收集与派发更新的完整链路
响应式的核心链路只有三步:读取时收集依赖,修改时触发更新,更新时重新读取并收集新依赖。
具体来说,当一个响应式对象的属性被读取时,会触发getter,这时候框架会记录"当前正在读取的副作用函数",把该属性对应的依赖集合中加上它;当属性被修改时,会触发setter,框架就从依赖集合里取出所有副作用函数逐个执行。所谓的副作用函数,放在组件层面就是组件的render函数(以及计算属性、watch等)。
这里有个细节值得展开:Vue 3用activeEffect这个全局变量来标记当前正在执行的副作用函数。当render函数调用时,框架先把activeEffect设为当前的副作用函数,然后执行render函数体,render执行过程中读取的所有响应式属性都会把activeEffect收进自己的依赖列表。render执行完毕,activeEffect再恢复原样。这就是"依赖收集"的完整实现。
派发更新的关键点在于"在正确的时机做正确的事"。Vue不会在数据变化的瞬间立刻重渲染,而是把更新任务放进一个异步队列,用微任务(Promise.then)来调度。同一个事件循环里多次修改数据,只会有一次渲染。这个机制在Vue 2叫nextTick的调度机制,Vue 3用了更精细的调度器(scheduler),但核心思想一致:合并更新、批量执行、渲染异步化。
为什么必须异步?因为如果数据每次变化都触发一次同步渲染,一次交互里改了十个状态就渲染十次,性能直接爆炸。批量异步更新相当于把十次渲染压成一次,代价是你在数据修改后立刻读DOM会拿到旧值,所以需要nextTick来"等一等"。
2.3 响应式的边界:哪些场景会失效
实操中响应式"失灵"的常见场景我总结成下面这张表,每个都踩过:
| 场景 | 表现 | 解决办法 |
|---|---|---|
| 新增未声明属性 | 视图不更新 | 预先声明所有属性;对象用reactive包裹后整体替换 |
| 解构响应式对象 | 解构后丢失响应式 | 用toRefs保住响应式 |
| Map/Set等集合 | Vue 2完全不行 | Vue 3的reactive对集合有支持 |
| v-for用index作key | 列表复用错乱 | 用唯一id作key |
| 深度监听超大数据 | 初始化卡顿 | 用shallowRef/shallowReactive按需浅响应 |
给新手一个忠告:组件里声明的data,最好把所有字段提前定义好,哪怕是空值。不是为了"规范",而是为了不给响应式系统添麻烦。Vue 3虽然用Proxy解决了新增属性的问题,但"后加的属性会不会在视图里第一时间出现"这个问题,依然受限于渲染时机的设计,别指望它像魔法一样无延迟。
还有一个小技巧值得记下来:如果你发现自己频繁给对象加新字段,说明数据结构设计得不够好,把那些可能出现的字段都声明成null或者空数组,能省掉大量后续排查时间。我自己以前写过一版购物车逻辑,购物车里商品的折扣率是后端动态下发的,前端最开始没声明discount字段,加进去之后页面上就是死活不显示,最后定位到就是响应式初始化时没有这个属性。后来我学乖了,凡是接口里可能出现的字段,一律提前声明。
3. 虚拟DOM与Diff:性能优化的核心枢纽
3.1 为什么要有虚拟DOM
很多人一听到虚拟DOM就以为是"比操作真实DOM更快",这是个流传甚广的误解。虚拟DOM真正解决的问题不是"比原生DOM快",而是"在保持可维护性的前提下,把DOM操作的次数降到最低"。
直接操作真实DOM有几个痛点:一是DOM节点的属性非常多,读写开销大;二是浏览器渲染管线复杂,频繁改动会引发重排重绘;三是命令式代码难以维护,你得自己记住每一处改动的状态。虚拟DOM的做法是:在JS内存里维护一棵轻量级的对象树(vnode),每次数据变化后重新生成一棵新树,与旧树做diff,找出最小变更集合,再批量执行真实DOM操作。
所以虚拟DOM的核心价值是:用一种可维护的声明式编程方式,换取对DOM操作的"可计算优化"。它不保证每次都最快,但保证在绝大多数业务场景下足够快,而且代码复杂度远低于手动优化。
额外的好处是跨平台。因为vnode是纯JS对象,不依赖浏览器DOM API,所以Vue能渲染到小程序、原生应用甚至终端界面上。Vue 3官方推荐的渲染器架构,本质上就是"平台无关的vnode + 可替换的渲染器"。你在项目中如果用到uni-app这类跨端方案,底层原理就是这个。
3.2 同层级Diff的核心逻辑
虚拟DOM的diff算法,大多数人知道"同层比较"和"双端指针",但未必理清里面的取舍。
先记住一条铁律:Vue的diff只做同层级比较,绝不跨层级移动节点。为什么?因为跨层级移动在真实业务里极其罕见,而支持跨层级移动会让算法复杂度从O(n)直接膨胀到O(n^3)——那是完全不可接受的。这个"用精度换复杂度"的取舍,是现代前端框架diff算法的共识。
在这个大前提下,diff的流程是:先比较新旧vnode的key和标签是否相同,如果不同就直接替换整个节点;如果相同,就比较属性和子节点。子节点的比较是diff里最复杂的部分,Vue 3用了"头头、尾尾、头尾、尾头"四个指针优先尝试快速匹配,实在匹配不上再用key建立索引查找。用key时甚至支持节点移动复用。
真正的复杂度控制在于:当新旧子节点数组长度不一致时,删除多出来的、创建缺的。Vue 3在编译阶段还会做静态标记(patchFlag),把"只有文字变化""只有class变化"等场景精确区分,diff时跳过无关属性的比较,这比Vue 2的"无差别全量比对"又省了一大截。
拿一个实际例子说:你有一个列表,每条数据只有名字会变,其他都不动。Vue 2拿到新旧两个vnode,要挨个对比props、class、style等所有属性;Vue 3编译时给这个节点打上TEXT标记,diff时发现只有TEXT标记,就直接更新textContent,其他属性一概不看。列表越长,这个差距越明显。
3.3 key到底解决了什么
关于key,网上教程会说"用于复用节点",但更准确的说法是:key是diff算法识别节点身份的凭据。
没有key时,Vue对列表项的比较采用"就地复用"策略——按位置比对,如果你在列表头部插入一项,后面所有项都被当成了"更新"而不是"新增",导致状态错乱。最经典的翻车案例是v-for渲染的输入框列表,没加key时在头部插一项,输入框里的内容会跟着"跑"到错误的位置。
加了key之后,Vue能精确知道哪个节点是新增、哪个节点是移动、哪个节点是删除,从而复用真正需要复用的DOM。注意:key必须稳定且唯一,用index作key实际上等于没有key(因为index在插入删除时会变),这就是很多人明明加了key还是出问题的原因。
实操建议:列表数据有id就用id,没有id就用内容组合出来的稳定标识,别偷懒用index。这条规矩在绝大多数场景下都成立,唯一的例外是纯静态展示且无内部状态的列表——但那种情况你也不差这点性能。
我见过一个很有意思的案例:某同事用index做key,初始渲染一切正常,后来需求变成"列表第一行固定展示一个推荐位",push进去一个新对象到数组头部,结果所有输入框的值全乱了。排查了半小时,最后把key从index换成业务id,一行代码解决。diff算法本身没有错,是你给的"身份凭证"变了。
4. 模板编译:从template到render函数
4.1 编译过程的三段式结构
Vue的模板编译可以分成三个阶段:解析(parse)、转换(transform)、生成(generate)。
解析阶段把模板字符串变成AST(抽象语法树)。AST你可以理解成模板的结构化描述:标签、属性、文本、指令、插值表达式都被拆成节点,节点之间用父子关系连成一棵树。Vue 2的parser和Vue 3的parser大体思路一致,但Vue 3的AST设计更规整,为后续优化留了空间。
转换阶段是Vue 3新增的亮点。编译器会在AST上做各种标记和优化:识别静态节点、静态属性、动态绑定类型,给每个动态节点打patchFlag,标注"这个节点只有文本会变"或者"只有属性会变"。这些标记在运行时被diff算法利用,实现了精确更新。转换阶段还处理指令的展开,比如v-if会被拆成条件表达式,v-for会被转换成渲染列表的辅助函数调用。
生成阶段就是字符串拼接:把AST转成render函数的代码字符串,再用new Function或eval把它变成真正的函数。所以你在Vue DevTools里看到render函数那串代码,就是编译器生成的。
理解编译过程对日常开发有一个非常实际的好处:遇到模板写法和预期不符时,你可以在脑子里"编译"一遍模板,猜出它生成的代码大概长什么样,问题往往立刻浮出水面。这个能力比背十个API都管用。
4.2 模板语法背后的真实样貌
很多人觉得v-if和v-show差不多,其实编译结果完全不同。v-if在编译后是一个三元表达式:条件成立渲染A节点,不成立渲染B节点(或者注释空节点),真正的"创建或销毁";v-show编译后则只是把display样式在none和原值之间切换,节点始终存在。所以频繁切换的场景用v-show,条件很少变化的场景用v-if,这个建议背后就是编译结果决定的。
v-for编译后会生成一个renderList辅助函数调用,渲染时遍历数组、为每一项生成一个vnode。注意v-for里用到的变量作用域与v-if有优先级问题:同一节点上v-for和v-if同时存在时,Vue 3明确规定v-if优先级更高,也就是说v-if不能访问v-for的变量。这个问题在Vue 2里是v-for优先,行为是"先循环再判断",两者不同,升级时特别容易踩雷。
插值表达式{{ message }}编译后变成字符串拼接_ctx.message,_ctx是组件实例的代理对象。这就是为什么你在模板里写的变量都会被框架"翻译"成上下文属性的读取——你写的message,本质上是this.message的简写。理解这一点,你就明白为什么模板里能直接访问data属性、props、计算属性、method方法,因为它们都被挂到了同一份上下文的代理上。
顺带说一个常见的编译期优化失效场景:如果你在模板里写了一长串复杂表达式,比如{{ user.addresses.filter(a => a.city === '北京').length }},编译器没法对它做静态标记,每次更新都会重新计算。建议把这类逻辑挪到computed里,不仅代码更清晰,还能利用计算属性的缓存机制。这不是风格偏好,是实打实的性能差异。
4.3 编译优化的红利与代价
Vue 3编译优化带来的最大红利是:对于纯静态的节点,编译器会在首次渲染后缓存起来,后续更新直接复用,连diff都不用参与。这种"静态提升"在长列表、复杂模板里效果非常明显,因为渲染时不再需要为不变的节点重新创建vnode对象。
代价是:模板的写法会影响优化效果。举个例子,如果你在模板里写了一个复杂的内联对象,比如:style="{ color: active ? 'red' : 'blue' }",编译器只能把它标为动态属性;如果这个对象是静态的,能被静态提升,那渲染成本就低很多。所以一个实际的优化建议是:能写静态的别写动态的,能用计算属性提前算出结果就别把计算逻辑堆进模板。这些不是玄学,是编译标记差异导致的真实性能差距。
还有个细节:模板里的大段静态文本,编译器会直接合并成一个静态vnode缓存,这也是为什么你经常看到别人的模板里"注释掉一大块内容"对性能几乎零影响的原因。反过来,如果你用render函数手写节点,这些优化全都没有,因为编译器来不及帮你分析。能用模板就尽量用模板,这是Vue团队持续优化的主战场。
5. 组件机制与生命周期:Vue的复用单元
5.1 组件通信的几种姿势
组件是Vue复用的基本单元,但组件之间怎么说话,很多人一直没理清。按使用场景,我习惯把通信方式分成四类。
第一类:父子通信,用props向下传、用emit向上传。这是最基本也最推荐的方式。props是单向数据流,子组件不能直接改props,要改就emit一个事件让父组件改。为什么要单向?因为如果子组件随意修改props,数据流的来源就乱套了,排查问题时会疯掉。这条约束不是Vue故意添堵,是工程实践的教训。
第二类:跨层级通信,用provide/inject。适合祖先组件给深层后代传数据,避免了层层透传props的繁琐。注意inject的数据如果不是响应式的,后代拿到的就是静态值;配合ref/reactive使用时才能拿到响应式版本。
第三类:兄弟或任意层级通信,传统方案是事件总线(Vue 2里是EventBus),Vue 3官方不推荐全局事件总线,因为组件实例销毁后事件监听容易残留,导致内存泄漏。更稳妥的是用状态管理工具(如Pinia)。
第四类:获取组件实例的直接交互,用ref引用。比如父组件想调用子组件暴露的方法,可以用defineExpose显式暴露。这里有个陷阱:模板ref必须在组件挂载完成后才能拿到,在onMounted或nextTick里访问才安全。
对比一下这四种方式的适用边界:props/emit适合关系明确、层级浅的组件;provide/inject适合祖先与深层后代之间的"广播式"传递;状态管理适合跨页面、跨模块的共享状态;ref直接交互适合"命令式"调用。选哪一种,核心标准是"数据流向是否清晰、组件解耦是否彻底"。
5.2 生命周期执行时机速查
Vue 3的组合式API生命周期和Vue 2的选项式对应关系如下:
| Vue 2 | Vue 3组合式 | 核心场景 |
|---|---|---|
| beforeCreate | setup()内部 | 初始化数据前的准备工作 |
| created | setup()内部 | 初始化、请求数据 |
| beforeMount | onBeforeMount | DOM挂载前 |
| mounted | onMounted | 操作DOM、绑定事件 |
| beforeUpdate | onBeforeUpdate | 数据已变、视图未更新 |
| updated | onUpdated | 视图已更新、操作新DOM |
| beforeDestroy | onBeforeUnmount | 清理定时器和监听 |
| destroyed | onUnmounted | 组件销毁后收尾 |
实际开发里,最高频的是onMounted和onBeforeUnmount这一对:前者做初始化、绑定事件、发请求;后者做清理,比如清除setInterval、移除全局事件监听、取消订阅。遗漏清理的后果是组件销毁后定时器还在跑,轻则报错,重则内存泄漏。我见过不止一次线上问题最后查出来是"组件卸载了,轮询还在跑"。
有个细节很多人不知道:父子组件的生命周期执行顺序是父beforeCreate -> 父created -> 父beforeMount -> 子created -> 子mounted -> 父mounted。也就是说父组件的mounted要等所有子组件挂载完才触发。理解了这点,你就不会在父组件的mounted里直接操作子组件的DOM而报"元素不存在"的错了。
再补充一个实战经验:如果某个子组件的数据请求特别耗时,父组件的mounted也会被拖住,因为父mounted要等子mounted。遇到这种情况,可以考虑让子组件不要阻塞挂载,用v-if控制子组件先不渲染,等数据就绪再挂载。这也是性能优化的一个隐蔽方向,很多人只盯着接口响应时间,没想到组件挂载顺序也在影响首屏速度。
6. 常见问题排查笔记
6.1 数据变了但视图没变,怎么一步步定位
这是所有Vue开发者都会遇到的头号问题。我的排查路径固定如下:先确认数据真的变了(在方法里console.log输出一遍);再确认变量是响应式的(打印它的类型,看看是不是reactive/ref包裹的对象);然后确认自己改的是不是响应式对象的属性而不是解构后的普通变量;最后检查是不是在同一轮事件循环里改了数据又立刻读DOM,如果是,用await nextTick()再读。
这套流程能覆盖九成以上的"视图不更新"场景。剩下的一成,多半是组件本身的更新被跳过,或者父组件传进来的props是普通对象、没有通过响应式API管理。遇到这种,把数据源换成reactive再试一次,多半就通了。
一个容易被忽略的点:对象深层次的属性变更。Vue 3的reactive是深度响应的,但如果你直接把对象的某个子对象整体替换掉,也能正常触发;真正麻烦的是那种"改了嵌套数组里某一项的一个字段"的操作,代码写对了才能触发更新。建议在这种场景下,优先用不可变更新的写法,也就是生成一个新数组或新对象再赋值,响应式系统对这种写法最友好。
6.2 v-for与key的连环坑
列表渲染的坑,几乎都和key有关。最典型的场景是:接口返回的数据没有id字段,开发图省事直接用了index。最开始一切正常,直到产品说"列表加一个删除功能",于是你发现删除第一行后,第二行输入框里的内容跑到第一行了。原因在3.3节说过,index在删除时不稳定,diff复用错位。
更隐蔽的坑是key用了一个可能重复的值,比如用户的name做key,结果两个用户重名,页面渲染错乱。正确做法是让后端返回业务唯一id,或者前端用内容哈希生成。如果没有现成的唯一标识,可以在前端数据进入表格之前加工一层:map一遍,给每条数据加上一个用随机数和时间戳组合的标识字段。
另一类坑是key放错了位置。很多人以为给v-for所在的元素加key就行,实际上如果列表项内部还有多个兄弟节点参与diff,你需要给真正代表"列表项身份"的那个根节点加key。我见过有的项目在组件根元素上加了key,然后在组件内部又用index循环渲染子列表,照样出问题——key只管它所在的那一层diff,管不了更深的层级。
6.3 异步渲染与nextTick的时机问题
另一个高频场景:修改数据后想立刻拿到更新后的DOM高度或宽度,结果拿到的是旧值。原因就是响应式更新是异步的,数据改了、DOM还没重新渲染。解决办法是等渲染完成:
const list = ref([]) list.value = newArray await nextTick() const height = listDom.offsetHeight注意nextTick要await,不然时序还是不对。如果是在Vue 2里,还可以用this.$nextTick回调。这个机制属于"知道原理就不会踩"的类型,2.2节讲过了——异步更新机制是性能优化的一部分,你不能既要性能优化又要同步拿到最新DOM,二选一。
还有一个相似的坑藏在v-model里:v-model本质上是一个语法糖,编译成value属性和input事件的双向绑定。如果你在input事件里同步修改另一个字段,而那个字段又被同一个输入框依赖,可能造成诡异的更新循环。遇到这种问题,先把v-model拆开成value + @input手写,看看是不是事件触发的时序导致的问题。
6.4 组合式API的常用模式分享
最后分享两个我在实际项目里沉淀下来的组合式API使用模式。
第一个是"可复用的业务逻辑封装"。凡是涉及请求数据、定时刷新、错误处理的逻辑,我都建议抽成useXxx的函数,例如useUserList,内部封装loading、data、error三个状态和load、refresh两个方法。组件里调用一次就完事,逻辑既清晰又方便多个组件复用。这套模式在Vue 3里非常顺手,核心就是组合式API天然支持"按功能组织代码"。
第二个是"组件对外暴露方法的联动"。如果你需要在父组件里控制子组件做一些操作(比如打开弹窗、滚动到底部),用ref拿到子组件实例后,子组件用defineExpose暴露对应方法。比起用一个visible的props来控制,这种方式在"子组件内部状态复杂"时更干净。不过要注意,一旦你用了defineExpose,子组件默认的公开实例内容就被覆盖了,只暴露你想暴露的。
我用useUserList举个具体例子,你感受一下这种封装带来的变化。以前Vue 2的mixin写法,一个mixin里既有数据字段又有方法名,多个mixin叠加时命名冲突是家常便饭。组合式函数不一样,每个useXxx内部的状态和作用域是隔离的,你甚至可以调用两次,互不干扰。把接口请求、错误提示、刷新重试全部收进一个函数里,组件的setup函数就剩下两三行调用代码,可读性直接上一个台阶。
我个人在实际项目里的体会是:Vue的核心原理其实一点都不神秘,它就是一套精心设计的"数据驱动视图"的工程方案。真正拉开开发水平差距的,从来不是背了多少API,而是遇到问题时能不能快速定位到"这是响应式的问题、还是diff的问题、还是编译优化的问题"。这个系列后面几篇,我打算把渲染器、调度器和源码级调试方法继续展开,如果大家有特别想先看的模块,也可以在评论区提。先把第一篇里的这几个核心概念反复咀嚼,特别是响应式的依赖收集和异步更新,这两个点吃透了,后面看什么都顺。