💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
* Vue **数据驱动**除了**数据渲染DOM**之外,还有一个很重要的体现就是**数据的变更会触发DOM的变化**。 前端最重要的两个工作:一个是把数据渲染到页面,另一个是处理用户交互。 处理的问题: 1. 需要修改哪块DOM? 2. 修改效率和性能是不是最优的? 3. 需要对每一次的修改都去操作DOM吗? 4. 需要case by case去修改DOM的逻辑吗? ## 响应式对象 Vue.js的响应式的核心是利用了**ES5的Object.defineproperty**,因此**不能兼容IE8及以下浏览器**。 ### Object.defineproperty 会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象 ~~~ Object.defineProperty(obj, prop, descriptor) ~~~ descriptor是被定义或修改的属性描述符,有很多可选键值。 get是一个给属性提供getter方法,访问该属性的时候会触发getter方法,set是一个给属性提供的setter方法,对属性做修改的时候会触发setter 一旦对象拥有了getter和setter,我们可以简单的把这个对象成为响应式对象。 ### initState Vue初始化阶段,_init方法执行的时候,会执行initState(vm)方法。 **主要对props、methods、data、computed和watcher等属性做了初始化操作**。 #### initProps props的初始化主要过程,就是遍历定义的props配置,遍历的过程中有两件事情 * **调用defineReactive,把每个prop对应的值变成响应式**,可以通过`vm._props.xxx`访问到定义props中对应的属性。 ~~~ defineReactive(props, key, value) ~~~ * **通过proxy把vm._props.xxx的访问代理到vm.xxx上**。 ~~~ proxy(vm, `_props`, key) ~~~ #### initData data的初始化的主要过程也是做两件事: * **对定义data的函数返回对象的遍历,通过proxy把每个值vm._data.xxx都代理到vm.xx上。** ~~~ let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} ~~~ ~~~ proxy(vm, `_data`, key) ~~~ * **调用observe方法观测整个data的变化,把data也变成响应式。**可以通过vm._data.xxx访问定义到data返回函数中对应的属性。 ~~~ observe(data, true /* asRootData */) ~~~ ### proxy代理 作用是把props和data上的属性代理到vm的实例上。vm实例可以直接访问props ~~~ let comP = { props: { msg: 'hello' }, methods: { say() { console.log(this.msg) } } } ~~~ ~~~ const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) } ~~~ proxy方法的实现很简单,通过Object.defineProperty把`target[sourceKey][key]`的读写变成了对`target[key]`的读写。 ### observe **observe的功能就是用来监测数据的变化**。 observe方法的作用就是**给非VNode的对象类型数据添加一个Observer**,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer对象实例。 ~~~ ob = new Observer(value) ~~~ ### Observer Observer是一个类,它的作用是给**对象的属性添加getter和setter**,用于**依赖收集和派发更新**。 ~~~ export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } ~~~ Observer的构造函数逻辑很简单,首先**实例化Dep对象**,接着**执行def函数**把自身实例添加到数据对象的value的__ob__属性上。接下来**对value做判断**,数组和对象处理方法不同,数组是遍历数组再次**调用observe方法**,而walk(对象)是遍历对象的key**调用defineReactive方法**。 ### defineReactive defineReactive的功能就是**定义一个响应式对象**,给对象动态添加getter和setter。 ~~~ export function defineReactive ( obj: Object, key: string, val: any, ) { const dep = new Dep() // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) } ~~~ dep.depend() dep.notify() defineReactive函数**最开始初始化Dep对象**的实例,接着拿到**obj的属性描述符**,**对子对象递归调用observe方法,可以保证obj的结构多复杂,它的所有子属性也能变成响应式的对象**。这样访问或修改obj中一个嵌套很深的属性,也能触发getter和setter。最后利用Object.defineproperty去给obj的属性的key添加getter和setter。 ### 总结 响应式对象:核心利用Object.defineproperty给数据添加getter和setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑。 **getter做的是依赖收集,setter做的是派发更新。** ## 依赖收集 响应式对象getter相关的逻辑就是依赖收集 ~~~ const dep = new Dep() get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, ~~~ * **`const dep = new Dep()`实例化一个Dep的实例** * **get函数中通过dep.depend()做依赖收集。** ### Dep Dep是真个getter依赖收集的核心 ~~~ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null const targetStack = [] export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() } ~~~ Dep是一个Class,定义了一些属性和方法,它有一个**静态属性target**,这个是**全局唯一的Watcher**。因为**同一个时间只能有一个全局的Watcher被计算**,另外它自身属性**subs也是Watcher的数组**。 Dep就是对Watcher的一种管理。 ### Watcher Watcher是一个class,它的构造函数中定义了一些和Dep相关的属性。 ~~~ this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() ~~~ this.deps和this.newDeps**表示Watcher实例持有的Dep实例的数组**。 Watcher还定义了一些原型的方法,和依赖收集相关的有get,addDep,cleanupDeps。 ### 过程分析 对数据对象的访问会触发他们的getter方法。Vue的mount过程是通过mountComponent函数。 实例化一个渲染watcher的时候,首先进入watcher的构造函数逻辑,然后执行this.get()方法:把**Dep.target赋值当前的渲染watcher**并压入栈(为了恢复) ~~~ value = this.getter.call(vm, vm) ~~~ this.getter对应的就是updateComponent函数,就是执行 ~~~ vm._update(vm._render(), hydrating) ~~~ **它会先执行vm._render()方法,生成渲染VNode,这个过程中会对vm上的数据访问,这个时候就触发了数据对象的getter。** **每个对象值的getter都持有一个dep,触发getter时调用dep.depend(),也会就会执行Dep.target.addDep(this)** 这时Dep.target已经被赋值为渲染watcher,那么就执行到addDep方法: ~~~ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } ~~~ 保证同一数据不会被添加多次,后执行**dep.addSub(this)**,把**当前watcher订阅到这个数据持有的subs中**,目的是**为后续数据变化能通知到哪些subs做准备**。 **所以在vm._render()过程中,会触发所有数据的getter,这样实际上已经完成了一个依赖收集的过程**。 完成依赖收集后,还有几个逻辑要执行 * 递归访问value,触发它所有的子项的getter * popTarget()实际就是把Dep.target恢复成上一个状态。因为档期vm的数据收集已经完成,那么对应的渲染Dep.target也需要改变。 ~~~ Dep.target = targetStack.pop() ~~~ * 最后执行:依赖清空 ~~~ this.cleanupDeps() ~~~ Vue是数据驱动的,每次数据变化都会重新render, 那么vm._render()又会再次执行,并再次出发数据的getters,所以Watcher在构造函数中会初始化2个Dep实例数组newDeps表示新添的Dep实例数组,而deps表示上一次添加的Dep实例数组。 cleanupDeps移除dep.subs数组中的Watcher的订阅,depIds和deps最新值,newDepIds和newDeps清空。 为什么要做移除deps订阅呢,id不是去重避免重复订阅了吗? 考虑场景: v-if去渲染不同子模板a和b,渲染a时会访问a中的数据,这时对a使用的数据添加了getter,做了依赖收集,修改a的数据是,理应通知这些订阅者。 但改变条件渲染b模板,又会对b使用的数据添加了getter,如果没有依赖移除,那么这时去修改a模板的数据,会通知a数据的订阅的回调,显然是浪费的。 因此Vue设计了在**每次添加完新的订阅,会移除旧的订阅**。 ### 总结 收集依赖的目的是**为了当这些响应式数据发生变化,触发他们的setter的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,这个过程叫做派发更新。** Watcher和Dep就是一个**非常经典的观察者设计模式**的实现。 ## 派发更新 收集依赖的目的就是**为了当我们修改数据的时候,可以对相关的依赖派发更新。** setter逻辑 ~~~ set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } ~~~ 两个关键点: * 一个是`childOb = !shallow && observe(newVal)`,如果`shallow`为 false 的情况,会对新设置的值变成一个响应式对象; * **dep.notify(),通知所有订阅者**。 ### 过程分析 当在组件中对响应的数据做了修改,就会触发setter的逻辑,最后调用dep.notify(),它是Dep的一个实例方法 ~~~ class Dep { // ... notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } ~~~ **遍历所有的subs,也就是Watcher的实例数组,调用每一个watcher的update方法。** ~~~ class Watcher { update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } } ~~~ 对于Watcher的不同状态,就会执行不同的逻辑。 一般组件更新的场景,会走到最后一个queueWatcher(this)的逻辑 引入了队列的概念,这也是Vue在做派发更新的时候的**一个优化的点**。 **它并不会每次数据改变都触发watcher的回调,而是把这些watcher先添加到一个队列,然后在nextTick后执行flushSchedulerQueue**。需要保证对nextTick的调用逻辑只有一次 nextTick是下一个tick,也就是异步去执行flushSchedulerQueue * 队列排序 `queue.sort((a, b) => a.id - b.id)`对队列做了从小到大的排序 1. **组件的更新由父到子;因为父组件的创建过程是先于子的,所以watcher的创建也是先父后子的,执行书序也应该保持先父后子**。 2. **用户的自定义watcher要优先于渲染watcher执行;因为用户自定义的watcher是在渲染watcher之前建立的** 3. 如果一个组件的父组件的watcher执行期间被销毁,那么对应的watcher执行都可以跳过。 * 队列遍历 对queue排序后,遍历执行watcher.run(),主要queue可能发生变化 * 状态恢复 把控制流程状态的一些变量恢复到初始值,把watcher队列情况。 run函数实际就是执行this.getAndInvoke方法,并传入watcher的回调函数,先通过this.get()得到当前值,然后判断,**如果满足新旧值不等,新值是对象类型,deep模式任何一个条件**,则执行watcher的回调。 对于渲染watcher而言,它在执行this.get()求值时,会执行getter方法 ~~~ updateComponent = () => { vm._update(vm._render(), hydrating) } ~~~ 这就是当我们去**修改组件相关的响应式数据的时候,会触发组件重新渲染的原因**,接着就会重新执行patch的过程。和首次选人有所不同。 ### 总结 当数据发生变化的时候,**触发setter逻辑,把在依赖过程中定义的所有观察者,也就是watcher,都触发他们的update过程**。这个过程由利用了**队列做了进一步优化**,在**nextTick后执行所以watcher的run**,最后执行他们的回调函数。 ## nextTick nextTick是Vue的一个核心实现。 ### JS运行机制 JS执行是单线程的,它是基于事件循环的,事件循环大致分为以下几个步骤 1. 所有同步任务都在主线程上执行,形成一个执行栈 2. 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,就在“任务队列”中放置一个事件 3. 一旦“执行栈”中所有的同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 4. 主线程不断重复上面的第三步。 ![](https://box.kancloud.cn/06bbd6ccaf724621f0d5edf0bf85290a_717x516.png) **主线程执行过程就是一个tick**,而所有的异步结果都通过“任务队列”来调度。 消息队列中存放的是一个个的任务(task) 规范字task分为两大类:**macro task(宏任务)和micro task(微任务)**,并且每个macro task结束后,都要清空micro task。 ~~~ for (macroTask of macroTaskQueue) { // 1. Handle current MACRO-TASK handleMacroTask(); // 2. Handle all MICRO-TASK for (microTask of microTaskQueue) { handleMicroTask(microTask); } } ~~~ 浏览器中,常见的**macro task有setTimeout、messageChannel、postMessage、setImmediate;** **常见的micro task有MutationObserver和Promise.then**。 ### Vue的实现 Vue2.5+后,nextTick的实现单独有一个js文件来维护它。 `next-tick.js`申明了`microTimerFunc`和`macroTimerFunc`2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生`setImmediate`,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的`MessageChannel`,如果也不支持的话就会降级为`setTimeout 0`;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。 nex-tick.js对外暴露了2个函数 nextTick:在上一节执行`nextTick(flushSchedulerQueue)`所用到的函数。把传入的回调函数cb压入callbacks数组,最后一次性的根据useMacroTask条件执行macroTimerFunc或者microTimerFunc,而它们**都会在下一个tick执行flushCallbacks**。flushCallbacks,对callbacks遍历,然后执行相应的回调函数。 使用callbacks而不是直接在nextTick中执行回调函数的原因是保证在同一个tick内多次执行nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个tick执行完毕。 `next-tick.js`还对外暴露了`withMacroTask`函数,它是对函数做一层包装,确保**函数执行过程中对数据任意的修改**,触发变化执行`nextTick`的时候强制走`macroTimerFunc`。比如对于一些 **DOM 交互事件**,如`v-on`绑定的事件回调函数的处理,会强制走** macro task**。 ### 总结 **数据的变化到DOM的重新渲染是一个异步过程,发生在下一个tick**。 比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法依赖了修改后的DOM变化,我们就**必须在nextTick后执行**。 Vue.js 提供了 2 种调用`nextTick`的方式,一种是全局 API`Vue.nextTick`,一种是实例上的方法`vm.$nextTick`。 ## 检测变化的注意事项 特殊情况: ### 对象添加属性 对于使用Object.defineproperty实现响应式的对象,当我们去给对象添加新属性的时候,**是不能够触发它的setter的**(初始化时没有该属性)。 Vue为了解决这个问题,定义全局API,Vue.set ~~~ export function set (target: Array<any> | Object, key: any, val: any): any { ~~~ 3个参数,target是数组或者普通对象,key代表数组下标或对象键值,value代表值。 * 如果是数组且key合法下标 ~~~ target.splice(key, 1, val) ~~~ * 若key已经存在target中,则直接赋值返回,因为是可以检测到的 * 判断target是否是Observer,如果不是说明target不是一个响应式对象,则直接赋值并返回。 * 最后通过defineReactive(obj.value, key, val)把新添加的属性变成响应式对象。 * 再通过ob.dep.notify()手动触发依赖通知。 ### 数组 Vue不能检测到以下变动的数组 1. 当你利用索引直接设置一个项时,`vm.items[indexOfItem] = newValue` 2. 当你修改数组的长度时,`vm.items.length = newLength` 第一种情况 `Vue.set(example1.items, indexOfItem, newValue)`; 第二种情况: `vm.items.splice(newLength)` 可见splice能让添加的UI小变成响应式的。 对数组中所有能改变数组自身的方法如push、pop等进行了重写,重写后会先执行本身原有的逻辑,并对能增加数组长度的**3个方法push、unshift、splice方法做了判断**,获取到插入的值,然后把**新添加的值变成一个响应式对象**,并且**再调用ob.dep.notify()手动触发依赖通知**。 ### 总结 对于对象属性的删除,也会用同样的问题,Vue同样提供了Vue.del的全局API。 ## 计算属性 vs 侦听属性 计算属性computed和侦听属性watch ### computed 计算属性的初始化时发生在Vue实例初始化阶段initState函数。 函数首先创建了**vm._computedWatchers为一个空对象**,接着对**computed对象做遍历**,拿到计算属性的每一个**userDef**,然后尝试获取这个userDef对应的getter函数。**接下来为每一个getter创建一个watcher**。**这个watcher和渲染watcher有一点很大的不同**。 **computed的属性名不能被data或prop所占用**。 接下来defineComputed: 利用Object.defineproperty给计算属性对应的key添加getter和setter,setter情况很少,我们重点关注getter部分,缓存配置也先忽略。 ~~~ function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } } } ~~~ **computed watcher并不会立即求值,同时持有一个dep实例** 当我们的render函数**访问到**时,就**触发了计算属性的getter**,它会拿到计算属性对应的watcher,然后执行**watcher.depend()**. 这个时候Dep.target是渲染watcher,所以this.dep.depned()相当于**渲染watcher订阅了这个computed watcher的变化**。 然后再执行**watcher.evaluate()**去求值:判断this.dirty,如果true则求值。 这里需要特别注意的是,由于`this.firstName`和`this.lastName`都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的`dep`添加到当前正在计算的`watcher`中,这个时候`Dep.target`就是这个`computed watcher`。 一旦我们队计算属性依赖的数据做修改,则会触发setter过程,通知所有订阅它变化的watcher更新,执行watcher.update()方法。 技术性这样的computed watcher,有2中模式:**lazy和active**。 如果this.dep.subs.length===0,怎没人订阅这个computed watcher的变化。this.dirty=true,只有下次再访问时才会重新求值。 求值后会执行回调函数,this.dep.notify(),触发渲染watcher重新渲染。 设计:**Vue想确保不仅仅是计算属性依赖的值发生变化,而是当技术性最终计算的值发生变化才会触发渲染watcher重新渲染,本质上是一种优化**。 ### watch 侦听属性的初始化也发生在Vue的实例初始化阶段的initState函数中,在computed初始化之后。 对watch对象遍历,拿到每一个handler,handle如果是一个数组,遍历数组,调用createWatcerh方法,否则直接调用createWatcher `vm.$watch(keyOrFn, handler, options)`函数,`$watch`是 Vue 原型上的方法,它是在执行`stateMixin`的时候定义的。 通过实例化watcher的方式,一旦我们watch的数据发生变化,它最终指向watcher的run方法,指向回调函数cb。 本质上侦听属性也是基于Watcher实现的,他是一个user watcher。 其实Watcher支持了不同的类型 ### Watcher options ~~~ if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync // ... } else { this.deep = this.user = this.computed = this.sync = false } ~~~ 所有watcher总共有4种类型。 #### deep watcher 深度观察对象。 ~~~ watch: { a: { deep: true, handler(newVal) { console.log(newVal) } } } ~~~ 对对象做深层递归遍历。遍历过程就是对一个子对象访问,触发getter过程,可以收集到依赖,也就是订阅它们变化的watcher。 #### user watcher vm.$watch创建的watcher就是一个user watcher。功能很简单。 #### computed watcher 为计算属性量身定制。 #### sync watcher 当响应式数据发送变化后,**触发了watcher.update(),只是把这个watcher推送到一个队列中,在nextTick后才真正执行watcher的回调函数**。一旦设置了sync,就可以在当前Tick中同步执行watcher的回调函数。 ### 总结 计算属性本质上是`computed watcher`,而侦听属性本质上是`user watcher`。 应用场景而言: 计算属性:**适合在模板渲染中,某个值是依赖了其他的响应式对象甚至属性计算而来。** 侦听属性:**侦听属性适用于观测摸个值的变化去完成一段复杂的业务逻辑**。 ## 组件更新 我们介绍了Vue的组件化实现过程,不过我们只讲了Vue组件的创建过程,并没有涉及到组件数据发生变化,更新组件的过程。我们对数据的响应式原理进行了分析,了解当数据发生变化的时候,**会触发渲染watcher的回调函数**,进而**执行组件的更新**过程。 这里执行的patch的逻辑和首次渲染是不一样的,**因为oldVnode不为空**,并且**它和vnode都是VNode类型**,接下来会通过sameVNode(oldVnode,vnode)判断它们**是否是相同的VNode来决定走不同的更新逻辑**。 sameVnode的逻辑非常简单,如果**两个vnode的key不相等**,则不相同;否则继续判断对于同步组件,判断isComment、data、input类型等是否相同,对于异步组件,则判断asyncFactory是否相同。 根据新旧vnode是否为sameVnode,会走到不同的更新逻辑 ### 新旧节点不同 如果新旧vnode不同,更新逻辑比较简单,**本质上是要替换已存在的节点**,三步: 1. 创建新节点 以当前旧节点为参考节点,创建新节点,并插入到DOM中。createElm 2. 更新父的占位符节点 3. 删除旧节点 遍历待删除的vnodes做删除。 ### 新旧节点相同 会调用patchVNode方法:就是把**新的vnode patch到旧的vnode**上。核心了逻辑拆成四步 1. 执行prepatch函数 prepatch方法就是拿到**新的vnode的组件配置以及组件实例**。由于更新了vnode,那么vnode对应的实例vm的一系列属性也会发生变化,包括占位符vm.$vnode的更新,slot的更新,listeners的更新,props的更新。 2. 执行update钩子函数 回到patchVNode函数,在执行完新的vnode的prepatch函数,会执行所有module的update钩子函数以及用户自定义的update钩子函数。 3. 完成patch过程 如果vnode是个文本节点且新旧文本不同,则直接替换文本内容。如果不是文本节点,则判断它们的子节点: * oldCh与ch都存在且不相同时,使用updateChildren来更新子节点 * 如果只有ch存在,表示旧节点不需要了。如果旧的节点是文本节点则先将节点的文本清除,然后通过addVnodes将ch批量插入到新节点elm下 * 如果只有oldCh存在,表示更新的是空节点,则需要将旧的节点通过removeVnodes全部清除。 * 当只有旧节点是文本节点的时候,则清除其节点文本内容、 4. 执行postpatch钩子函数 再执行完patch过程后,会执行postpatch函数,是组件自定义的钩子函数,有则执行。 在整个patchVnode过程中,最复杂的就是updateChildren方法 ### updateChildren 逻辑比较复杂。 ### 总结 组件更新的过程核心就是新旧vnode diff,对新旧节点相同以及不同的情况下分别做不同的处理。 新旧节点不同的更新流程是**创建新节点-》更新父占位符节点-》删除旧节点**。 而新旧节点相同的更新流程是**去获取它们的children,根据不同情况做不同的更新逻辑**。最复杂的情况就是新旧节点相同且它们都存在子节点,那么会执行updateChildren逻辑。 ## 原理图 ![](https://box.kancloud.cn/332126972a55de87d0248dcbb6c5349d_1141x752.png)