资讯动态

从new Vue到页面刷新:Vue源码核心渲染链路全解析

发布时间:2026/10/14 2:50:23 来源:尧图企业网站定制
Vue 源码里有一条特别值得顺下来的主线从new Vue开始到数据状态发生变化、页面最终刷新为止。这条线几乎串联了框架最核心的几块拼图——数据驱动、实例挂载、render、Virtual DOM、createElement、update。我最初啃 Vue 源码就是从这条线入手的读完之后回头看整个框架的骨架一下就立起来了看其他模块也不再觉得零散。这篇文章就顺着这条线走一遍把每个环节在源码里到底做了什么、为什么要这么做掰开讲清楚。适合什么样的人读两种。一是已经在用 Vue 写业务、想往底层走一步的开发者你能在文章里找到模板到底是怎么变成页面的这个问题的完整答案二是准备面试、被问过new Vue 之后发生了什么却答不透的朋友这篇基本可以当作一个源码级的口述底稿。我会尽量用大白话解释关键机制但涉及源码的地方也会直接贴代码建议对着源码文件再核一遍效果会更好。1. 数据驱动Vue 为什么绕了这么一大圈1.1 数据驱动到底是什么意思很多人把数据驱动挂在嘴边但真问一句数据驱动和命令式操作 DOM 的区别是什么又说不太清楚。数据驱动的本质是你只负责声明数据是什么样界面就该是什么样至于数据变化之后怎么去更新页面那是框架替你完成的事。举个例子。页面上有一段文字你想把它从 hello 改成 world。命令式的写法是document.getElementById(app).textContent world你得亲自找到那个 DOM 节点再亲手改掉它的内容。数据驱动则是data() { return { msg: hello } } // 之后只需要 this.msg world你改的只是一个普通的 JavaScript 对象属性剩下的交给 Vue。听起来很神奇但源码层面看它其实就拆成两件事一是数据变化后我如何知道——这是响应式系统的事二是知道之后我如何把新状态反映到页面上——这就是本篇文章主线要讲的挂载、render、Virtual DOM、patch 这一整套流程。1.2 声明式写法和命令式写法的分水岭命令式的核心特征是过程可控但你得事必躬亲声明式的核心特征是结果导向、过程交给框架。Vue 选择数据驱动最直接的原因是当页面复杂度上升时手写 DOM 操作的维护成本会急剧膨胀。你想象一个待办列表新增一条、删除一条、勾选一条、按条件过滤。用命令式写你得为每一种操作都写一遍找出对应 DOM、执行对应改动的代码而且这些代码非常容易在状态交叉变化时出错——比如你删了列表中间的一项却没有同步更新序号页面就花了。Vue 的思路是开发者只维护一份数据todos数组里有什么页面就渲染什么。数据的增删改是纯 JavaScript 操作简单、可预测框架随后自动把这份数据翻译成 DOM。这个翻译的过程在 Vue 2 里就是一条固定流水线数据变化触发渲染 Watcher渲染 Watcher 调用render函数生成一棵新的 Virtual DOM 树接着update把这棵新树和旧树做 diff最后把差异落到真实 DOM 上。这就是标题里那几个关键词串起来的主线。1.3 数据驱动给源码阅读指了一条路我读源码的习惯是先找主线、再抠细节。数据驱动这条设计理念恰好就圈定了 Vue 源码里最值得先读的那几个文件core/instance/init.js初始化、core/instance/lifecycle.js挂载与更新、core/instance/render.jsrender、core/vdom/Virtual DOM 相关、core/vdom/patch.jscreateElement 与 update 的落地点。只要顺着数据变了 → 界面跟着变这个问题往下追你一定会经过这些文件而且每经过一个文件你对框架的理解就会深一层。2. new Vue 发生时构造函数入口和初始化顺序2.1 入口其实只有一个 _init先看最表面的东西。你在业务里写new Vue({...})Vue 的构造函数长这样function Vue(options) { if (process.env.NODE_ENV ! production !(this instanceof Vue)) { warn(Vue is a constructor and should be called with the new keyword) } this._init(options) }构造函数本身没什么逻辑就是把所有初始化的活儿交给了this._init(options)。这里有个细节值得注意_init并不是写在Vue类内部的而是通过initMixin混入到原型上的。// core/instance/init.js export function initMixin(Vue) { Vue.prototype._init function (options) { // ... } }这是 Vue 源码组织方式的一大特点按功能把实现拆成一个个xxxMixin函数再往Vue.prototype上挂。你读源码的时候会发现renderMixin、stateMixin、eventsMixin、lifecycleMixin等等各自的职责非常清晰。这样做的好处是每个模块可以独立维护也方便在入口统一装配。2.2 初始化模块的顺序与设计原因进入_init之后Vue 按固定顺序做了这么几件事。我直接贴简化后的源码流程Vue.prototype._init function (options) { const vm this vm._uid uid vm._isVue true // 选项合并 if (options options._isComponent) { initInternalComponent(vm, options) } else { vm.$options mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } vm._renderProxy vm vm._self vm // 按顺序初始化各个模块 initLifecycle(vm) // 父子关系、生命周期状态 initEvents(vm) // 事件监听 initRender(vm) // 渲染相关的属性和方法 callHook(vm, beforeCreate) initInjections(vm) // inject 初始化 initState(vm) // props / methods / data / computed / watch initProvide(vm) // provide 初始化 callHook(vm, created) // 如果配置了 el就自动挂载 if (vm.$options.el) { vm.$mount(vm.$options.el) } }这个顺序不是随便排的每一条都有讲究。initLifecycle在最前面因为后面很多模块都要用到实例的父子关系标记比如$parent、$children、_isMounted、_isDestroyed这些字段先初始化好后续在钩子和渲染过程中才能正确维护。initEvents紧随其后把父组件传给子组件的事件监听先登记好。initRender在beforeCreate之前执行这一条经常被人忽略。initRender里做了两件关键事给实例挂上vm.$createElement和vm._c这两个创建 VNode 的方法同时定义了vm.$attrs和vm.$listeners。之所以这么早是因为后续initState里初始化 computed、watch 时它们的求值过程可能依赖这些属性晚了就取不到。initInjections和initProvide的位置也非常有讲究initInjections必须在initState之前因为你的data、computed里可能直接用到了注入的值必须先注入才能使用而initProvide放在initState之后因为provide的取值本身可能来自data里的响应式数据。2.3 选项合并和 el 的触发时机_init里另一个重点就是选项合并。mergeOptions做的事是把构造器层面的Vue.options里面包括components、directives、filters、mixins等和实例传入的options合并成一个完整的vm.$options。平时你写的methods、data、computed、生命周期钩子就是在这里被标准化并挂到$options上后面initState再从$options里取。还有一个很多人没注意到的点_init的末尾判断了vm.$options.el如果有el就自动调用vm.$mount。换句话说你写不写vm.$mount(#app)其实都行只要el存在Vue 会自己把挂载这件事接上。这也解释了为什么new Vue({ el: #app, ... })和new Vue({...}).$mount(#app)效果完全等价——后者只是把挂载动作显式表达了而已。3. 实例挂载的实现$mount 与 mountComponent 全链路3.1 为什么存在两个版本的 $mount$mount在 Vue 2 里其实有两个版本。入口文件不同挂载到原型上的$mount也不同完整版vue.js/vue.esm.js带模板编译器运行时版vue.runtime.js/vue.runtime.common.js不带。运行时版的原生$mount是这样的Vue.prototype.$mount function (el, hydrating) { el el query(el) return mountComponent(this, el, hydrating) }而完整版会在调用前先劫持一遍把模板编译成 render 函数再调用回运行时版的$mountconst mount Vue.prototype.$mount Vue.prototype.$mount function (el, hydrating) { el el query(el) const options this.$options // 如果用户没提供 render尝试把 template 或 el 的 outerHTML 编译成 render if (!options.render) { let template options.template if (template) { // 处理 template 字符串或 DOM 节点 } else if (el) { template getOuterHTML(el) } if (template) { const { render, staticRenderFns } compileToFunctions(template, {...}, this) options.render render options.staticRenderFns staticRenderFns } } return mount.call(this, el, hydrating) }这里有两个关键点。第一完整版会优先使用options.render只有在你既没写 render 也没写 template 时才拿el对应 DOM 的outerHTML当作模板去编译。这也就是为什么生产环境推荐用运行时版加模板预编译运行时省去编译器的体积和编译开销模板在构建阶段就已经变成了 render 函数。第二编译结果会写回options.render。所以你在后面看mountComponent的时候它拿到的vm.$options.render可能来自你手写的 render也可能来自模板编译来源不同但用法完全一致。Vue 在这里做了一个非常聪明的统一抽象无论是模板还是手写 render最终都收敛为同一个 render 函数。顺带一提mountComponent里对el还做了一个安全校验如果把 body 或 html 作为挂载点会直接报警告。因为 Vue 会用$mount里拿到的 DOM 替换掉整个挂载区域你要是挂在 body 上等于把整个页面都换掉了结果必然不是你想要的。3.2 mountComponent 内部的核心逻辑不管哪个版本的$mount最终都会走到mountComponent。它的核心代码可以精简成这样export function mountComponent(vm, el, hydrating) { vm.$el el // 如果没有 render 函数就渲染一个空节点兜底 if (!vm.$options.render) { vm.$options.render createEmptyVNode } callHook(vm, beforeMount) // 关键的 updateComponent渲染 更新 let updateComponent () { vm._update(vm._render(), hydrating) } // 为当前组件实例创建一个渲染 Watcher new Watcher(vm, updateComponent, noop, { before() { if (vm._isMounted !vm._isDestroyed) { callHook(vm, beforeUpdate) } } }, true) hydrating false // 挂载完成后标记状态并触发 mounted 钩子 if (vm.$vnode null) { vm._isMounted true callHook(vm, mounted) } return vm }mountComponent做的事本质上就是三件第一确认有 render 函数。没有就塞一个生成空节点的函数避免后续出错。第二定义updateComponent它内部就是vm._update(vm._render())两步走。_render调用 render 函数得到一棵新的 VNode 树_update把新旧 VNode 交给 patch 去更新真实 DOM。第三创建一个渲染 Watcher并把updateComponent作为 getter 传进去。这一步是整个响应式系统的枢纽之后数据变化时渲染 Watcher 会重新执行updateComponent从而触发重新渲染。3.3 渲染 Watcher首次渲染和后续更新的发动机渲染 Watcher 是理解 Vue 更新机制的关键。它本质上就是core/observer/watcher.js里Watcher类的一个实例只不过这一次专门负责渲染这个任务。Watcher 构造的时候做了这样一件事class Watcher { constructor(vm, expOrFn, cb, options, isRenderWatcher) { // ... if (typeof expOrFn function) { this.getter expOrFn } // ... this.value this.get() } get() { pushTarget(this) let value this.getter.call(vm, vm) popTarget() return value } }new Watcher(vm, updateComponent, ...)时updateComponent被当作expOrFn传入那么在构造函数里就会立即执行一次get()get()里执行updateComponent也就是执行vm._update(vm._render())。所以第一次渲染就是这么被拉起来的。更关键的是render函数执行过程中会读取各种响应式数据比如this.msg。读数据会触发响应式数据getter里的dep.depend()把当前正在渲染的这个 Watcher收集到msg这个 key 的依赖列表里。之后任何地方改了msg都会触发dep.notify()这个渲染 Watcher 就会被通知进入异步更新队列最终重新执行updateComponent。这里要记住一个概念每个组件实例有且只有一个渲染 Watcher它不是每个数据一个 Watcher而是每个组件一个。这个设计让数据变化到组件重渲染的粒度保持在组件级别配合 Virtual DOM 的 diff在组件内部做精确更新。面试里常问的Vue 更新粒度答案就在这一层。4. render从模板到 VNode 的翻译官4.1 render 的本质是一份 VNode 工厂到了这一层我们要把目光从初始化和挂载移到真正生成界面结构的地方。render 函数的作用用一句话概括输入当前实例的各类数据输出一棵描述界面结构的 VNode 树。在源码里Vue.prototype._render长这样简化Vue.prototype._render function () { const vm this const { render, _parentVnode } vm.$options vm.$vnode _parentVnode let vnode try { vnode render.call(vm._renderProxy, vm.$createElement) } catch (e) { // 渲染出错时的处理 } if (!(vnode instanceof VNode)) { vnode createEmptyVNode() } vnode.parent _parentVnode return vnode }render.call(vm._renderProxy, vm.$createElement)这句话信息量很大。它做了三件事第一把 render 函数内部的this指向vm._renderProxy。如果你不写箭头函数在 render 里写this.msg拿到的是当前实例的数据。_renderProxy在开发环境下还做了一件事当你访问实例上不存在的属性时它给出一个更友好的报错提示方便排查问题。第二把vm.$createElement作为第一个参数传给 render 函数。所以你在 render 里写的h或者createElement其实就是一个创建 VNode 的函数大家平时在 JSX 里用的h也是同一个东西。第三最后强制校验返回结果必须是VNode否则用空节点兜底保证后续 patch 流程拿到的结构是统一的。4.2 模板编译与 render 的对应关系有人说 Vue 是模板驱动其实从源码角度看真正驱动一切的是 render 函数模板只是一个更友好的入参。你用template写的东西在运行时或构建时会被编译成 render。举个例子div idapp p{{ msg }}/p /div会被编译成类似这样的 render 函数(function anonymous() { return _c(div, { attrs: { id: app } }, [ _c(p, [_v(_s(msg))]) ]) })_c是createElement的缩写_v创建文本节点_s做字符串化。你会发现模板里的插值{{ msg }}变成了_s(msg)而这个msg在 render 函数执行时会从当前实例上读取——读取的过程同时完成了响应式依赖的收集。这就是为什么模板能自动响应数据变化。_c和$createElement之间还有一层细微差别。vm._c是模板编译内部使用的vm.$createElement是手写 render / JSX 使用的。两者底层都是createElement函数区别在于$createElement会强制走更严格的 children 规范化流程alwaysNormalize true因为手写 render 时开发者传 children 的方式更自由需要框架兜底处理更多边界情况。4.3 手写 render 的几个细节如果你平时用模板可能永远不需要手写 render但读源码时看懂 render 能帮你反向理解模板的能力边界。手写 render 时最常踩的坑是 children 的类型。官方文档会告诉你h的用法是h(ul, { class: list }, [ h(li, item 1), h(li, item 2) ])children 可以是一个数组也可以是一个 VNode甚至可以是一个字符串会被当成文本节点。但当你真的传入各种奇怪的格式时createElement内部会调用 children 规范化逻辑把不统一的输入最终转换成 VNode 数组。另一个细节是data 参数可以不传但位置不能错。如果你只传两个参数比如h(p, hello world)源码里的createElement会帮你做参数左移——它检测到第二个参数不是对象而是基本类型时会把它当作 children 处理把 data 置为 undefined。这种容错设计谈起来是小细节但正是这些细节让手写 render 的体验没那么别扭。5. Virtual DOM为什么中间要隔一层 VNode5.1 VNode 的数据结构先看 VNode 类本身。core/vdom/vnode.js里定义了 VNode 构造函数我摘几个核心字段export default class VNode { constructor(tag, data, children, text, elm, context, componentOptions, asyncFactory) { this.tag tag // 节点标签名如 div this.data data // 节点上的属性、事件、指令等 this.children children // 子节点 VNode 数组 this.text text // 文本节点的内容 this.elm elm // 对应的真实 DOM 节点 this.ns undefined // 命名空间SVG 等 this.context context // 当前组件的上下文 this.componentOptions componentOptions this.componentInstance undefined // 如果是组件节点这里是组件实例 this.parent undefined // 父 VNode this.raw false this.isStatic false // 静态节点标记 this.isRootInsert true this.isComment false this.isCloned false this.isOnce false // ... } }VNode 就是一个普通对象用来描述一个界面节点。它不会直接操作浏览器 API所以它不挑环境浏览器、服务端、甚至小程序平台都能用同一套 VNode 结构。字段名elm背后有个重要的设计思路VNode 上会保存它对应的真实 DOM 节点。这样在 diff 过程中如果某个节点被判定为可以复用就直接拿旧的elm做原地更新而不需要重新创建 DOM。也就是说VNode 不是一个永远脱离 DOM 的纯数据结构它和真实 DOM 之间有绑定关系只是这种绑定关系由框架管理不让开发者操心。5.2 VNode 的常见类型实际运行时VNode 大概有这些形态类型特点典型产生场景元素节点有tag无componentOptionsh(div, [...])组件节点有tag和componentOptions模板里的自定义组件文本节点无tag有text插值表达式、纯文本注释节点isComment true模板中的注释空节点无 tag、无 textisComment为 truerender 缺失时的兜底克隆节点isCloned true静态节点复用组件节点是理解 Vue 内部机制很关键的一类。当模板里出现child-component这种标签时createElement在创建 VNode 的过程中会通过createComponent走一套完全不同的逻辑它会解析出这个组件的构造器 options生成componentOptions挂在 VNode 上并且创建一个占位 VNode。等到 patch 阶段发现这个 VNode 是组件节点就会实例化子组件、走子组件的_init和$mount。这也是为什么子组件数据变化时能独立触发自己的渲染 Watcher——每个组件自己管自己。5.3 这层抽象带来了什么很多初学者不理解Vue 都拿到数据了直接生成 DOM 字符串塞进页面不就行了非要在中间搞一层 VNode不是多此一举吗这里有个很实际的理由性能。直接操作真实 DOM 最大的问题是你很难知道这次到底改了多少东西。如果每次数据变化都把整个模板重新渲染一遍再整体替换 DOM代价非常大。有了 VNode 这层中间结构数据变化后先重新生成一棵 VNode 树然后和上一棵 VNode 树做 diff算出最小差异最后只把差异落到真实 DOM。这样重新生成 VNode是纯 JavaScript 计算开销远小于真实 DOM 操作。另一个理由是可移植性。因为 VNode 不依赖浏览器 DOM APIVue 的渲染逻辑可以复用到多个平台。比如服务端渲染本质上就是把 VNode 树转成 HTML 字符串在移动端场景下同一套 VNode 结构可以对接不同的平台渲染器。Virtual DOM 不是 Vue 独有的设计但在 Vue 里它和响应式系统、组件系统耦合得非常紧密是整个渲染管线的枢纽。6. createElement 与 updateVNode 落地成真实 DOM 的最后一公里6.1 createElement 的双重身份接下来进入整条链路的执行端。createElement这个函数在源码里承担了一个非常重要的功能把对界面结构的描述变成VNode 实例。它有两个入口前面已经提到过一个是vm._c供模板编译后的 render 使用另一个是vm.$createElement供手写 render 和 JSX 使用。它们的差异在于 children 的规范化力度。源码里createElement的大致逻辑是export function createElement(context, tag, data, children, normalizationType, alwaysNormalize) { // 参数容错data 被省略时把参数左移 if (Array.isArray(data) || isPrimitive(data)) { normalizationType children children data data undefined } if (isTrue(alwaysNormalize)) { normalizationType ALWAYS_NORMALIZE } return _createElement(context, tag, data, children, normalizationType) }_createElement内部会依次做几件事检查 data 上是否有响应式标记带__ob__的 data 会警告因为响应式对象不能直接挂到 VNode 的 data 上、规范化 children、然后根据 tag 的类型决定创建普通元素节点还是组件节点。children 规范化是这里最容易出问题的地方。你写的h(div, [ text, h(span, hello), null, [h(b, nested)] ])里面混了字符串、VNode、null、嵌套数组不规范。normalizeChildren会把它们全部拍平、过滤掉null和空值最终得到一个合法的 VNode 数组。这一步从源码上看有点繁琐但它的目标很明确不管用户怎么传框架内部永远能拿到结构一致的 children。6.2 createElm从 VNode 到真实 DOMVNode 创建出来之后真正把它变成 DOM 的函数叫createElm。它在 patch 阶段被调用核心逻辑是这样的简化function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { // ... const tag vnode.tag const data vnode.data const children vnode.children if (isDef(tag)) { // 处理组件节点走子组件初始化流程 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // 普通元素节点创建真实 DOM vnode.elm nodeOps.createElement(tag) // 递归创建子节点 createChildren(vnode, children, insertedVnodeQueue) // 触发创建相关的钩子属性、事件、指令等 if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } // 插入父节点 insert(parentElm, vnode.elm, refElm) } else if (isTrue(vnode.isComment)) { vnode.elm nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }createElm本质上就是一个递归生成 DOM 并挂进文档的过程。先创建当前节点再递归创建所有子节点最后把这棵子树插入到父节点里。组件节点会被createComponent提前拦截直接创建子组件实例这样每个组件就独立负责自己的子树渲染了。invokeCreateHooks这步平时注意不到但它很关键VNode 的data里如果带有class、style、attrs、on事件等都是在这里被应用到真实 DOM 上的。简单说这个函数负责给创建出来的 DOM 节点赋值、绑定事件、应用指令。6.3 patch 的 diff 过程与 key 的作用有了createElm解决的是第一次渲染时怎么从无到有建立 DOM但数据更新时我们面对的是旧 DOM 已经存在如何用最小的代价把它变成新状态。这个任务交给了patch。patch的整体策略是如果旧节点是真实 DOM首次挂载时的vm.$el就把它包装成一个空的 VNode然后整体走createElm流程。如果新旧都是 VNode并且sameVnode判断为是同一个节点就走patchVnode做精细化更新。如果sameVnode不成立就直接用新 VNode 创建 DOM 替换旧的。sameVnode的判断条件是这样的function sameVnode(a, b) { return ( a.key b.key a.tag b.tag a.isComment b.isComment isDef(a.data) isDef(b.data) sameInputType(a, b) ) }patchVnode内部除了更新属性、更新文本外最核心的是处理 children 的 diff。如果新旧节点都有 children就走updateChildren——这就是面试里常说的 diff 算法核心。updateChildren用了一种非常经典的双端比较策略。它维护四个指针新 children 的头尾、旧 children 的头尾然后从两端向中间靠拢不断尝试旧头 vs 新头相等直接patchVnode两个指针都往后移。旧尾 vs 新尾相等直接patchVnode两个指针都往前移。旧头 vs 新尾相等复用节点并把它移动到尾部。旧尾 vs 新头相等复用节点并把它移动到头部。如果四种情况都不满足就在旧 children 里按 key 查找能被复用的节点找到就移动位置并更新找不到就新建。这个过程要处理大量边界条件源码里有大量removeVnodes、insertBefore等操作是 Vue 里最复杂的一段。读源码时不需要一次看懂每一行但一定要抓住尽量复用、移动替代删除重建的核心策略。key 的作用在这个 diff 里体现得淋漓尽致。如果你在列表渲染时给每一项绑定了稳定的 keyVue 就能精确定位旧列表里哪个节点对应新列表里的哪个节点如果不写 keyVue 只能退而求其次地按位置去比对一旦列表中间插入一项后续所有节点都可能被误判为不同节点而重建。所以业务里常见的警告key 不能是 index根源就在这个 diff 算法——毕竟 index 会随列表变化而漂移它并不能代表一个节点的身份。6.4 从数据变化到页面刷新的完整闭环到了这里整条主线基本上通了。我再用一个最简化的流程把它串一遍方便你建立全局观。一次数据更新完整经过是这样的你修改 this.msg → 触发 msg 的 setter → 通知收集到的依赖 dep.notify() → 渲染 Watcher 进入异步更新队列 → 执行 updateComponent → vm._render() 调用 render 函数生成新的 VNode 树 → vm._update() 调用 __patch__(旧VNode, 新VNode) → sameVnode 判断 → patchVnode → updateChildren diff → 把差异落到真实 DOM整个链路里render负责生成新状态的样子createElement负责把描述变成 VNodeupdate负责把 VNode 变化落到 DOM。这三件事各司其职中间通过渲染 Watcher 和响应式依赖收集衔接起来。你看源码时如果觉得某段特别绕就想想它在这条链路里属于哪一环方向感立刻就有了。实际操作层面我最想提醒一件事读这段源码不要试图记住每个函数的每一行而要做一个小抄。我自己的习惯是把调用链画在一张便签上标注每个文件、每个函数、每个关键参数读完一遍就对着这张便签给别人讲一遍讲不出来的地方就是还没真懂的地方。Vue 源码里各种函数名非常直白比如createElm就是create elementpatchVnode就是patch vnode顺着名字捋比死记硬背快得多。另外调试时多利用 Vue 的 warn 提示和__patch__断点。在updateComponent里打一个条件断点等数据变化触发它时你可以亲眼看到_render返回的新 VNode 和_update接收到的旧 VNode然后单步进patchVnode观察 diff 过程中哪些节点被复用了、哪些被重建了。这种看见的感觉比读十遍源码都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑