这段时间利用课余时间夹杂了很多很多事把 Vue2 源码学习了一遍,但很多都是跟着视频大概过了一遍,也都画了自己的思维导图。但还是对详情的感念模糊不清,故这段时间对源码进行了总结梳理。
本篇文章更合适于已看过 Vue2 源码,进一步总结加深概念的人群。若还未读过源码或零碎一知半解的小伙伴,也可以挑选阶段进行总结梳理,个人还是强烈认为需要过一遍源码。
├── benchmarks 性能、基准测试
├── dist 构建打包的输出目录
├── examples 案例目录
├── flow flow 语法的类型声明
├── packages 一些额外的包,比如:负责服务端渲染的包 vue-server-renderer、配合 vue-loader 使用的的 vue-template-compiler,还有 weex 相关的
│ ├── vue-server-renderer
│ ├── vue-template-compiler
│ ├── weex-template-compiler
│ └── weex-vue-framework
├── scripts 所有的配置文件的存放位置,比如 rollup 的配置文件
├── src vue 源码目录
│ ├── compiler 编译器
│ ├── core 运行时的核心包
│ │ ├── components 全局组件,比如 keep-alive
│ │ ├── config.js 一些默认配置项
│ │ ├── global-api 全局 API,比如熟悉的:Vue.use()、Vue.component() 等
│ │ ├── instance Vue 实例相关的,比如 Vue 构造函数就在这个目录下
│ │ ├── observer 响应式原理
│ │ ├── util 工具方法
│ │ └── vdom 虚拟 DOM 相关,比如熟悉的 patch 算法就在这儿
│ ├── platforms 平台相关的编译器代码
│ │ ├── web
│ │ └── weex
│ ├── server 服务端渲染相关
├── test 测试目录
├── types TS 类型声明
位置:
/src/core/instance/index.js
// 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')}// 在 /src/core/instance/init.js,// 1.初始化组件实例关系属性// 2.自定义事件的监听// 3.插槽和渲染函数// 4.触发 beforeCreate 钩子函数// 5.初始化 inject 配置项// 6.初始化响应式数据,如 props, methods, data, computed, watch// 7.初始化解析 provide// 8.触发 created 钩子函数this._init(options)
}
源码核心代码顺序以深度遍历形式
位置:
/src/core/instance/init.js
export function initMixin (Vue: Class) {// 负责 Vue 的初始化过程Vue.prototype._init = function (options?: Object) {vm._self = vm // 将 vm 挂载到实例 _self 上// 初始化组件实例关系属性,比如 $parent、$children、$root、$refs...initLifecycle(vm)// 自定义事件的监听:谁注册,谁监听initEvents(vm)// 插槽信息:vm.$slot// 渲染函数:vm.$createElement(创建元素)initRender(vm)// beforeCreate 钩子函数callHook(vm, 'beforeCreate')// 初始化组件的 inject 配置项initInjections(vm)// 数据响应式:props、methods、data、computed、watchinitState(vm)// 解析实例 vm.$options.provide 对象,挂载到 vm._provided 上,和 inject 对应。initProvide(vm)// 调用 created 钩子函数callHook(vm, 'created')}
}
Vue 源码「初始化」致命五问。
beforeCreate钩子函数前完成了什么?- 父子组件中,子组件调用执行本身注册的自定义事件 A(),那么父子组件中,谁监听事件 A() 的执行调用?
created钩子函数前完成了什么?initInjections(vm)、initState(vm)、initProvide(vm)三者的执行顺序可否变化?- Vue 的初始化过程?
思考问题后,答案在下方,根据自己阅读整理源码,对自己提出有意义的问题并自我回答。不确保是面试热点题噢(切勿入题太深)
参考Vue3源码视频讲解:进入学习
问:beforeCreate 钩子函数前完成了什么?
答:beforeCreate 之前,主要是在处理 vm 实例上的各种属性配置和自定义事件属性,也就是将 Vue 的壳初始化完成。
首先合并了组件的配置项挂载到全局 vm.options上。初始化组件实例关系属性,如:options 上。初始化组件实例关系属性,如:options上。初始化组件实例关系属性,如:parent、children、children、children、root、$refs 等等,然后初始化自定义的事件监听,最后初始化组件的插槽 slot 和作用域插槽scopedSlots,createElement(即 render 函数,同时定义了组件 attrs 和 $listeners属性。)
问:父子组件中,子组件调用执行本身注册的自定义事件 A(),那么父子组件中,谁监听事件 A() 的执行调用?
答:谁注册了自定义事件,则谁监听自定义事件。故是子组件监听事件。
问:created 钩子函数前完成了什么?
答:created 钩子函数是在 Vue 壳构建完成后,开始初始化实例的响应式数据和方法。
首先初始化好 inject 配置项,再初始化各种响应式数据和方法如:props、methods、data、computed、watch,最后初始化 vm._provided 属性。
问:initInjections(vm)、initState(vm)、initProvide(vm) 三者的执行顺序可否变化?
答:不可以,源码中有官方注释。
inject 配置项是注入数据,在后续的 computed 和 data 中均可以或需要使用注入数据,故解析 injections 需要在 data/props 前。
解析 provide 实际上只是将 vm.$options.provide 挂载到 vm._providedinject 上,需要等响应式数据和方法初始化完毕后再执行。inject 和 provide 是成对出现的,一个注入,一个接收。
initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/props
问:Vue 的初始化过程?
答:Vue 初始化过程其实就是 beforeCreate 钩子函数和 created 钩子函数前执行的内容。
- 在 beforeCreate 前,主要先初始化搭建了 Vue 实例的壳,如组件的 options 配置项,组件实例的关系属性,处理了自定义事件。
- 在 created 前,主要是初始化实例的响应式数据和方法,首先初始化 inject 配置项,再初始化数据响应式和方法,最后解析组件配置项上的 provide 对象。总结来说构建初始化 Vue 实例对象 vm。
位置:
/src/core/instance/index.js
// 初始化数据响应式:props、methods、data、computed、watch
export function initState (vm: Component) {// 初始化当前实例的 watchers 数组vm._watchers = []// 拿到上边初始化合并后的 options 配置项const opts = vm.$options// props 响应式,挂载到 vmif (opts.props) initProps(vm, opts.props)// 1. 判断 methods 是否为函数// 2. 方法名与 props 判重// 3. 挂载到 vmif (opts.methods) initMethods(vm, opts.methods)if (opts.data) {// 初始化 data 并挂载到 vminitData(vm)} else {// 响应式 data 上的数据observe(vm._data = {}, true /* asRootData */)}// 1. 创建 watcher 实例,默认是懒执行,并挂载到 vm 上// 2. computed 与上列 props、methods、data 判重if (opts.computed) initComputed(vm, opts.computed)// 1. 处理 watch 对象与 watcher 实例的关系(一对一、一对多)// 2. watch 的格式化和配置项if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
}
源码核心代码顺序以深度遍历形式
位置:
/src/core/observer/index.js
// 为对象创建观察者 Observe
export function observe (value: any, asRootData: ?boolean): Observer | void {// 非对象和 VNode 实例不做响应式处理if (!isObject(value) || value instanceof VNode) {return}let ob: Observer | void// 若 value 对象上存在 __ob__ 属性并且实例是 Observer 则表示已经做过观察了,直接返回 __ob__ 属性。if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__} else if (// 一堆判断对象的条件shouldObserve &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {// 创建观察者实例ob = new Observer(value)}// if (asRootData && ob) {ob.vmCount++}return ob
}
位置:
/src/core/observer/index.js
// 监听器类
export class Observer {// ... 配置constructor (value: any) {this.value = value// 实例化一个发布者 Depthis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)if (Array.isArray(value)) {// ...处理数组} else {// value 为对象,为对象的每个属性设置响应式// 也就是为啥响应式对象属性的对象也是响应式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) {for (let i = 0, l = items.length; i < l; i++) {// 判断,优化,创建观察者实例observe(items[i])}}
}
位置:
/src/core/observer/dep.js
// 订阅器类
export default class Dep {constructor () {// 该 dep 发布者的 idthis.id = uid++// 存放订阅者this.subs = []}// 添加订阅者addSub (sub: Watcher) {this.subs.push(sub)}// 添加订阅者removeSub (sub: Watcher) {remove(this.subs, sub)}// 向订阅者中添加当前 dep// 在 Watcher 中也有这个操作,实现双向绑定depend () {if (Dep.target) {Dep.target.addDep(this)}}// 通知 dep 中的所有 watcher,执行 watcher.update() 方法notify () {// ...省略代码}
}
位置:
/src/core/observer/watcher.js
// 订阅者类,一个组件一个 watcher,订阅的数据改变时执行相应的回调函数
export default class Watcher {...代码省略:constructor() 构造配置一个 watcherget () {// 打开 Dep.target,Dep.target = thispushTarget(this)// value 为回调函数执行的结果let valueconst vm = this.vmtry {// 这里执行 updateComponent,进入 patch 阶段更新视图。value = this.getter.call(vm, vm)} catch (e) {// ...捕获异常} finally {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value)}// 最后清除 watcher 实例的各种依赖收集popTarget()this.cleanupDeps()}return value}addDep (dep: Dep) {const id = dep.id// watcher 订阅着 dep 发布者并进行缓存判重if (!this.newDepIds.has(id)) {// 缓存 dep 发布者this.newDepIds.add(id)this.newDeps.push(dep)// 发布者收集订阅者 watcher// 在 dep 中也有这个操作,实现双向绑定if (!this.depIds.has(id)) {dep.addSub(this)}}}/** * Clean up for dependency collection. */cleanupDeps () {// ...代码省略// 清除 dep 发布者的依赖收集}// 订阅者 update() 更新update () {/* istanbul ignore else */// // 懒执行如 computedif (this.lazy) {this.dirty = true// 同步执行,watcher 实例的一个配置项} else if (this.sync) {// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,this.run()} else {// 大部分 watcher 更新进入 watcher 的队列queueWatcher(this)}}// 1. 同步执行时会调用// 2. 浏览器异步队列刷新 flushSchedulerQueue() 会调用run () {// ...代码省略,active = false 直接返回// 使用 this.get() 获取新值来更新旧值// 并且执行 cb 回调函数,将新值和旧值返回。}// 订阅者 watcher 懒执行evaluate () {this.value = this.get()this.dirty = false}/** * Depend on all deps collected by this watcher. */depend () {// 调用当前 watcher 依赖的所有 dep 发布者的 depend()let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}/** * Remove self from all dependencies' subscriber list. */teardown () {// ...销毁该 watcher 实例}
}
位置:
/src/core/observer/index.js
// 设置响应式对象
export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean
) {...省略// 响应式核心Object.defineProperty(obj, key, {enumerable: true,configurable: true,// get 拦截对象的读取操作get: function reactiveGetter () {const value = getter ? getter.call(obj) : valif (Dep.target) {// 依赖收集并通知实现发布者 dep 和订阅者 watcher 的双向绑定dep.depend()// 依赖收集对象属性中的对象if (childOb) {childOb.dep.depend()// 数组情况if (Array.isArray(value)) {// 为数组项为对象的项添加依赖dependArray(value)}}}return value},// set 拦截对对象的设置操作set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val// 无新值,不用更新则直接 returnif (newVal === value || (newVal !== newVal && value !== value)) {return}// 没有 setter,只读属性,则直接 returnif (getter && !setter) return// 设置新值if (setter) {setter.call(obj, newVal)} else {val = newVal}// 将新值进行响应式childOb = !shallow && observe(newVal)// dep 发布者通知更新dep.notify()}})
}
位置:
/src/core/instance/state.js
const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
}// 为每个属性设置拦截代理,并且挂载到 vm 上(target)
// 如 proxy(vm, `_props`, key)、proxy(vm, `_data`, key)
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)
}
Vue 源码「响应式原理」致命五问。
- 什么是 MVVM 模式?
- Vue 的双向绑定原理?
- Vue 如何处理响应式数据?
- computed 和 watch 的特性区别?
- computed 和 watch 的使用场景区别?
思考问题后,答案在下方,根据自己阅读整理源码,对自己提出有意义的问题并自我回答。不确保是面试热点题噢(切勿入题太深)
问:什么是 MVVM 模式?
答:MVVM(Model–View–ViewModel ) 是一个软件架构设计模式。其进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 分为以下三层
- 1.View 视图层,也就是构建出来的用户页面。
- 2.Model 数据层,就是存放数据状态。
- 3.ViewModel 视图数据层,是 MVVM 模式的核心层,作为其余两层的中间枢纽,更新视图层且操作改变数据层的状态。
问:Vue 的双向绑定原理?
答:Vue 双向绑定采用的是 MVVM 模式。监听器
Observer、订阅器Dep、订阅者Watcher、解析器Compile。
- Compile 解析器:扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
- Observer 监听器:调用 defineReactive 劫持并监听所有属性,getter 向 Dep 依赖。
- Dep 订阅器:收集观察者 Watcher 和通知观察者目标更新。每个属性拥有自己的消息订阅器dep,用于存放所有订阅了该属性的观察者对象,当数据发生改变时,通知所有的 watch 执行自己的update逻辑。
- Watcher 订阅者:观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖数据的dep添加到自身的deps中),当被观察的值发生变化时,会接收到来自dep的通知,从而触发回调函数。
- Watcher类的实现比较复杂,因为他的实例分为渲染 watcher(render-watcher)、计算属性 watcher(computed-watcher)、侦听器 watcher(normal-watcher)三种。
- computed-watcher:我们在组件钩子函数computed中定义,这类 watcher 有个特点:当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。
- normal-watcher:我们在组件钩子函数watch 中定义,即只要监听的属性改变了,都会触发定义好的回调函数。
- render-watcher:每一个组件都会有一个 render-watcher,当 data/computed 中的属性改变的时候,会调用该 render-watcher 来更新组件的视图。
- 这三种 watcher 也有固定的执行顺序,分别是:
computed-render -> normal-watcher -> render-watcher。尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值了,如果 render-watcher 排在 computed-render 前面,就会导致页面更新的时候 computed 值为旧数据。- 而 Dep 订阅器和 Watcher 订阅者又是一种观察者模式。Watcher 用来订阅属性的变化通,从而更新视图。Dep 用来收集 Watcher 的依赖,当 Observer 更新时,通过 dep.notify() 统一派发给 Watcher,实现了双向绑定。
- 综上:简单来说通过数据劫持+发布订阅模式,通过以下初始化和更新的过程来实现双向绑定,也就是响应式原理。
- 初始化:
- 1.Observer 对数据进行响应式绑定
- 2.Compiler 编译解析模块指令,初始化渲染页面,并将每个指令的节点绑上更新函数,实例化监听监听数据的订阅者 Watcher。
- 3.数据 getter 时,执行对应数据的 dep 收集所有 watcher 依赖
- 更新:
- 1.更新时触发 dep.notify(),派发通知所有订阅者 watcher
- 2.订阅者 watcher 执行 update() 回调函数
- 3.调用对应 Compiler 编译解析模块,重新更新视图
问:Vue 如何处理响应式数据?
答:响应式的数据主要分为两类:Object 和 Array
- Object 对象则利用 defineReactive(),来循环遍历整个对象,通过 Object.defineProperty 设置 getter 和 setter 的拦截,再通过观察者模式双向绑定来实现对象响应式原理
- Array 数组则利用
def()方法对Array.prototype.push()/pop()/shift()/unshift()/splice()/sort()/reverse()进行 Object.defineProperty 拦截,实现响应式。(感谢「故心」大佬提醒纰漏)
Vue.set()/delete()方法处理数组异步更新利用的是Array.splice()。
问:computed 和 watch 的特性区别?
答:通过源码阅读 computed 和 watch 在本质是没有区别的,都是通过 Watcher 的实例去实现的响应式,主要有以下特性区别。
- computed 默认为懒执行,dirty 为 true。watch 有 immediate 配置,可以实现立即执行一次 cb。
- computed 支持缓存,依赖数据发生改变,才会重新进行计算。watch 不支持缓存,立即响应式变化。
- computed 不支持异步。watch 支持异步。
- computed 的 cb 函数默认走 get 方法。watch 的 cb 函数第一个参数是新值,第二个参数是旧值。
问:computed 和 watch 的使用场景区别?
答:computed 和 watch 使用场景的区别根本原因是因它们的特性不同,大致有以下的场景区别。
- 选择 computed
- 当数据需要缓存时
- 当数据依赖其他数据计算得到时
- 逻辑较为简单并无需异步操作时(watch 消耗较大)
- 选择 watch
- 当执行异步操作时
- 即时监听数据完成较为复杂的回调函数时
Vue 源码的异步更新也就是响应式原理的进一步深入,下面引用以下官方对于异步更新的介绍来进一步了解这个概念。
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。例如,当你设置
vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。
异步更新发生在响应式原理更新 dep.notify() 派发通知给 watcher 调用 update() 更新回调方法。
位置:
/src/core/observer/watcher.js
// watcher 异步更新入口
update () {// computed 懒加载走这if (this.lazy) {this.dirty = true} else if (this.sync) {// 当给 watcher 实例设置同步选项,也就是不走异步更新队列,直接执行 this.run() 调用更新// 这个属性在官方文档中没有出现this.run()} else {// 大部分都走 queueWatcher() 异步更新队列queueWatcher(this)}
}
源码核心代码顺序以深度遍历形式
位置:
/src/core/observer/scheduler.js
// 将当前 watcher 放入 watcher 的异步更新队列
export function queueWatcher (watcher: Watcher) {const id = watcher.id// 避免重复添加相同 watcher 进异步更新队列if (has[id] == null) {// 缓存标记has[id] = true// flushing 正在刷新队列if (!flushing) {// 直接入队queue.push(watcher)} else {// 正在刷新队列// 将 watcher 按 id 递增顺序放入更新队列中。let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}// 用数组切割方法queue.splice(i + 1, 0, watcher)}// queue the flush// 正在刷新队列if (!waiting) {// 设置标记,确保只有一条异步更新队列waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {// 直接刷新队列:// 1.异步更新队列 queue 升序排序,确保按 id 顺序执行// 2.遍历队列调用每个 watcher 的 before()、run() 方法并清除当前 watcher 缓存(也就是 id 置为空)// 3.调用 resetSchedulerState(),重置异步更新队列,等待下一次更新。(也就是清除缓存,初始化下标,俩标志设为 false)flushSchedulerQueue()return}// 也就是 vm.$nextTick、Vue.nextTick// 做了两件事:// 1.将回调函数(flushSchedulerQueue) 放入 callbacks 数组。// 2.向浏览器任务队列中添加 flushCallbacks 函数,达到下次 DOM 渲染更新后立即调用nextTick(flushSchedulerQueue)}}
}
位置:
/src/core/observer/watcher.js调用:flushSchedulerQueue() 遍历调用每个 watcher 的 run()
/** * 由 刷新队列函数 flushSchedulerQueue 调用,如果是同步 watch,则由 this.update 直接调用,完成如下几件事: * 1、执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数) * 2、更新旧值为新值 * 3、执行实例化 watcher 时传递的第三个参数,比如用户 watcher 的回调函数 */
run () {if (this.active) {// 调用 watcher.get() 获取当前 watcher 的值。const value = this.get()if (value !== this.value ||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value) ||this.deep) {// 更新值const oldValue = this.valuethis.value = value// 若果是用户定义的 watcher,执行用户 cb 函数,传递新值和旧值。if (this.user) {try {this.cb.call(this.vm, value, oldValue)} catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)}} else {// 其余走渲染 watcher,this.cb 默认为 noop(空函数)this.cb.call(this.vm, value, oldValue)}}}
}
位置:
/src/core/util/next-tick.js
const callbacks = []
let pending = false// cb 函数是 flushSchedulerQueue 异步函数队列
export function nextTick (cb?: Function, ctx?: Object) {let _resolve// callbacks 数组推进 try/catch 封装的 cb(避免异步队列中某个 watcher 回调函数发生错误无法排查)callbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})// 执行了 flushCallbacks() 函数,表示当前浏览器异步任务队列无 flushCallbacks 函数if (!pending) {pending = true// nextTick() 的重点!// 执行 timerFunc,重新在浏览器的异步任务队列中放入 flushCallbacks 函数timerFunc()}// 做 Promise 异常处理// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}// timerFunc 将 flushCallbacks 函数放入浏览器的异步任务队列中。
// 关键在于放入浏览器异步任务队列的优先级!
// 1.Promise.resolve().then(flushCallbacks)
// 2.new MutationObserver(flushCallbacks)
// 3.setImmediate(flushCallbacks)
// 4.setTimeout(flushCallbacks, 0)
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()timerFunc = () => {// 第一选 Promise.resolve().then() 放入 flushCallbacksp.then(flushCallbacks)// 若挂掉了,采用添加空计时器来“强制”刷新微任务队列。if (isIOS) setTimeout(noop)}isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||// PhantomJS and iOS 7.xMutationObserver.toString() === '[object MutationObserverConstructor]'
)) {// Use MutationObserver where native Promise is not available,// e.g. PhantomJS, iOS7, Android 4.4// (#6466 MutationObserver is unreliable in IE11)let counter = 1// 第二选 new MutationObserver(flushCallbacks)// 创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用。// MDNconst observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {// 第三选 setImmediate()timerFunc = () => {setImmediate(flushCallbacks)}
} else {// 第四选 setTimeout() 定时器timerFunc = () => {setTimeout(flushCallbacks, 0)}
}// 最终一条浏览器异步队列执行 callbacks 数组中的方法来达到 nextTick() 异步更新调用方法。
function flushCallbacks () {// 设置标记,开启下一次浏览器异步队列更新pending = falseconst copies = callbacks.slice(0)// 清空 callbacks 数组callbacks.length = 0// 执行异步更新队列其中存储的每个 flushSchedulerQueue 函数for (let i = 0; i < copies.length; i++) {copies[i]()}
}
Vue 源码「异步更新」致命五问。
- Vue 响应式原理中的异步更新是如何实现?
- Vue 默认更新是同步的还是异步的?
- Vue 是如何避免重复执行同一次异步更新?
- Vue 的 nextTick 全局 API 是如何实现的?
- Vue 是如何将刷新 callbacks 数组的函数放入浏览器任务队列进行异步更新的?
思考问题后,答案在下方,根据自己阅读整理源码,对自己提出有意义的问题并自我回答。不确保是面试热点题噢(切勿入题太深)
问:Vue 响应式原理中的异步更新是如何实现?
答:Dep 订阅器派发通知给每个 watcher 订阅器,执行
update()方法开始异步更新。
异步更新原理总体来说是:将 每个 watcher 放入 queue 全局队列中=>调用 nextTick() 方法将刷新 watcher 队列的方法 flushSchedulerQueue 放入 callbacks 数组中=>将刷新 callbacks 数组的函数 flushCallbacks 通过 timerFunc() 方法放进浏览器的异步任务队列中=>最后浏览器遍历执行 callbacks 数组中的刷新 watcher 队列方法 flushSchedulerQueue=>刷新 watcher 队列方法遍历执行 queue 队列的每个 watcher.before() 和 watcher.run() 方法=>继续下一次异步更新。
以下是 update() 方法详情:
- 首先判断两个特殊标记
- 是否为 lazy 懒更新,则设置 dirty 为 true,以标记当前 watcher 为懒更新
- 再判断是否有 sync 同步更新标记,直接执行
watcher.run(),Vue 官方不推荐使用,文档没有该属性。- 然后将 watcher 放入 queue 队列中,放入队列有两种方式,以 flushing 标志判断
- 若无在刷新队列中,直接 push 进 queue 队列
- 若正在刷新队列中,按 watcher.id 进行升序排序,确保更新的顺序
- 然后调用 nextTick(),将 flushSchedulerQueue(刷新当前 watcher 队列的方法)放入 callbacks 数组中。若浏览器的任务队列中无 flushCallbacks 函数,则执行 timerFunc()。(用 pending 来判断控制)
- timerFunc() 将 flushCallbacks 函数(执行第 3 点中 callbacks 数组中的所有 flushSchedulerQueue 方法)放入浏览器的异步任务队列中
- 等待浏览器异步任务队列执行 callbacks 数组中的 flushSchedulerQueue 方法。
- 每个 flushSchedulerQueue 方法中先将 queue 队列排序,再遍历 queue 执行 watcher.before() 和 watcher.run() 方法,而后再初始化异步更新队列,自此异步更新完成。
问:Vue 默认更新是同步的还是异步的?
答:Vue 默认异步更新,通过
watcher.async。Vue 源码还设置了开启同步更新的操作,可以通过设置watcher.sync的属性,在 watcher.update() 方法时并直接执行 watcher.run() 方法进行更新操作。但 Vue 官方不推荐使用该属性,因同步更新机制将阻塞后续任务的执行,整个组件更新将大打折扣。
问:Vue 是如何避免重复执行同一次异步更新?
答:通过三个标识符的操作来进行避免重复执行同一次的异步更新。
- 在将 watcher 放入 watcher 队列时,进行了 id 的缓存,避免重复 watcher 添加到 queue 数组。
- 通过 waiting 判断是否正在刷新 queue 队列,避免重复执行刷新 queue 队列。
- 通过 pending 判断浏览器的异步任务队列中是否有刷新 callbacks(放的是刷新 queue 队列的任务) 数组的任务,避免浏览器异步任务队列重复执行刷新 callbacks 数组的任务。
问:Vue 的 nextTick 全局 API 是如何实现的?
答:Vue.nextTick 将传递的刷新 watcher 队列的回调函数 用
try catch包裹然后放入 callbacks 数组。
在浏览器异步任务队列无其他刷新 callbacks 数组的方法时,执行 timerFunc 函数,放入当前刷新 callbacks 数组的方法。
进而达到在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 的功能
问:Vue 是如何将刷新 callbacks 数组的函数放入浏览器任务队列进行异步更新的?
答:根据浏览器任务队列异步执行的效率来选择放入方法的优先级,分别为:
- Promise.resolve().then(flushCallbacks)
- new MutationObserver(flushCallbacks)
- 提供了监视对DOM树所做更改的能力(HTML5 中的新特性)
- setImmediate(flushCallbacks)
- setTimeout(flushCallbacks, 0)
位置:
/src/core/global-api/index.js调用:
/src/core/index.js
// 初始化全局配置和 API
export function initGlobalAPI (Vue: GlobalAPI) {// 全局配置 configconst configDef = {}configDef.get = () => configif (process.env.NODE_ENV !== 'production') {configDef.set = () => {warn('Do not replace the Vue.config object, set individual fields instead.')}}// 给 Vue 挂载全局配置,并拦截。Object.defineProperty(Vue, 'config', configDef)// Vue 的全局工具方法: Vue.util.xxVue.util = {// 警告warn,// 选项扩展extend,// 选项合并mergeOptions,// 设置响应式defineReactive}// Vue.set()Vue.set = set// Vue.delete()// 处理操作与下列 set() 基本一致。// target 为对象时,采用运算符 deleteVue.delete = del// Vue.nextTick()// 不多 BB 就是上节 异步更新原理中的 nextTick// 1.将回调函数(flushSchedulerQueue) 放入 callbacks 数组。// 2.向浏览器任务队列中添加 flushCallbacks 函数,达到下次 DOM 渲染更新后立即调用Vue.nextTick = nextTick// Vue.observable() 响应式方法// 也不多 BB 就是上上节 响应式原理中的 observe// 为对象创建一个 Oberver 监听器实例,并监听Vue.observable = (obj: T): T => {observe(obj)return obj}Vue.options = Object.create(null)// ASSET_TYPES = ['component', 'directive', 'filter']ASSET_TYPES.forEach(type => {// 初始化挂载 Vue.options.xx 实例对象Vue.options[type + 's'] = Object.create(null)})// Vue.options._base 挂载 Vue 的构造函数Vue.options._base = Vue// 在 Vue.options.components 中扩展内置组件,比如 keep-alive// 在 /src/shared/utils.js:(for in 挂载)extend(Vue.options.components, builtInComponents)// Vue.use 全局 API:安装 plugin 插件// 1.installedPlugins 缓存判断当前 plugin 是否已安装// 2.调用 plugin 的安装并缓存initUse(Vue)// Vue.mixin 全局 API:混合配置// this.options = mergeOptions(this.options, mixin)// 出现相同配置项时,子选项会覆盖父选项的配置:options[key] = strat(parent[key], child[key], vm, key)initMixin(Vue)// Vue.extend 全局 API:扩展一些公共配置或方法initExtend(Vue)// Vue.component/directive/filter 全局 API:创造组件实例注册方法initAssetRegisters(Vue)
}
源码核心代码顺序以深度遍历形式
位置:
/src/core/observer/index.js
// 通过 vm.$set() 方法给对象或数组设置响应式
export function set (target: Array | Object, key: any, val: any): any {// ...省略代码:警告// 更新数组通过 splice 方法实现响应式更新:vm.$set(array, idx, val)if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)target.splice(key, 1, val)return val}// 更新已有属性,直接更新最新值:vm.$set(obj, key, val)if (key in target && !(key in Object.prototype)) {target[key] = valreturn val}// 设置未定义的对象值// 获取当前 target 对象的 __ob__,判断是否已被 observer 设置为响应式对象。const ob = (target: any).__ob__// ...省略代码:不能向 _isVue 和 ob.vmCount = 1 的根组件添加新值// 若 target 不是响应式对象,直接往 target 设置静态属性if (!ob) {target[key] = valreturn val}// 若 target 是响应式对象// defineReactive() 添加上响应式属性// 立即调用对象上的订阅器 dep 派发更新defineReactive(ob.value, key, val)ob.dep.notify()return val
}
位置:/src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {// 每个实例构造函数(包括Vue)都有一个唯一的 cid。这使我们能够创建包装的“子对象”,用于原型继承和缓存它们的构造函数。Vue.cid = 0let cid = 1// Vue 去扩展子类Vue.extend = function (extendOptions: Object): Function {extendOptions = extendOptions || {}const Super = thisconst SuperId = Super.cid// 缓存多次 Vue.extend 使用同一个配置项时const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})if (cachedCtors[SuperId]) {return cachedCtors[SuperId]}// 是否为有效的配置项名,避免重复const name = extendOptions.name || Super.options.nameif (process.env.NODE_ENV !== 'production' && name) {validateComponentName(name)}// 定义 Sub 构造函数,准备合并const Sub = function VueComponent(options) {// 就是 Vue 实例初始化的 init() 方法this._init(options)}// 通过原型继承的方式继承 VueSub.prototype = Object.create(Super.prototype)Sub.prototype.constructor = Sub// 唯一标识Sub.cid = cid++// 选项合并Sub.options = mergeOptions(Super.options,extendOptions)// 挂载自己的父类Sub['super'] = Super// 将上边合并的配置项初始化配置代理到 Sub.prototype._props/_computed 对象上// 方法在下边if (Sub.options.props) {initProps(Sub)}if (Sub.options.computed) {initComputed(Sub)}// 实现多态方法Sub.extend = Super.extendSub.mixin = Super.mixinSub.use = Super.use// 实现 component、filter、directive 三个静态方法ASSET_TYPES.forEach(function (type) {Sub[type] = Super[type]})// 递归组件的原理并注册if (name) {Sub.options.components[name] = Sub}// 在扩展时保留对基类选项的引用,可以检查 Super 的选项是否是最新。Sub.superOptions = Super.optionsSub.extendOptions = extendOptionsSub.sealedOptions = extend({}, Sub.options)// 缓存cachedCtors[SuperId] = Subreturn Sub}
}function initProps (Comp) {const props = Comp.options.propsfor (const key in props) {proxy(Comp.prototype, `_props`, key)}
}function initComputed (Comp) {const computed = Comp.options.computedfor (const key in computed) {defineComputed(Comp.prototype, key, computed[key])}
}
位置:/src/core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {// ASSET_TYPES = ['component', 'directive', 'filter']ASSET_TYPES.forEach(type => {// 每个 Vue 上挂载实例注册方法Vue[type] = function (id: string, definition: Function | Object): Function | Object | void {// 无方法if (!definition) {// 返回空return this.options[type + 's'][id]} else {if (type === 'component' && isPlainObject(definition)) {// 组件若为 name,默认为 iddefinition.name = definition.name || id// 调用 Vue.extend,将该组件进行扩展,也就是可以实例化该组件definition = this.options._base.extend(definition)}// bind 绑定和 update 更新指令均调用该 defintion 方法if (type === 'directive' && typeof definition === 'function') {definition = { bind: definition, update: definition }}// this.options.components[id] = definition || this.options.directives[id] = definition || this.options.filter[id] = definitionthis.options[type + 's'][id] = definitionreturn definition}}})
}
Vue 源码「全局 API」致命六问。
- Vue 初始化全局 API 时,做了什么?
- Vue 全局 API 有什么作用?
- Vue 中当父子组件配置选项发生冲突时,是如何处理?
- 初始化后,自定义往 Vue 实例上的响应式对象添加属性,添加的属性是否具有响应式?
- 如何自定义数据实现响应式?
- vm.set()和vm.set() 和 vm.set()和vm.delete() 方法,分别如何操作对象和数组? 思考问题后,答案在下方,根据自己阅读整理源码,对自己提出有意义的问题并自我回答。不确保是面试热点题噢(切勿入题太深)
问:Vue 初始化全局 API 时,做了什么?
答:
1.Vue 初始化了全局的 config 配置并设为响应式。
2.暴露一些工具方法,如日志、选项扩展、选项合并、设置对象响应式
3.暴露全局初始化方法,如 Vue.set、Vue.delete、Vue.nextTick、Vue.observable
4.暴露组件配置注册方法,如 Vue.options.components、Vue.options.directives、Vue.options.filters、Vue.options._base
5.暴露全局方法,如 Vue.use、Vue.mixin、Vue.extend、Vue.initAssetRegisters()
问:Vue 全局 API 有什么作用?
答:
- Vue.use(): 用来安装 plugin 插件,对插件进行缓存优化,并执行 install() 安装。
- Vue.mixin():用来在 Vue 的全局配置上合并 options 配置。并且每个组件生成 vnode 时会合并全局配置和组件配置,因此可以作为抽离公共的业务逻辑,实现公共的业务逻辑,也就是类的继承。
- Vue.extend():用来在 Vue 实例扩展子类,可以用于一些公共组件化配置上。与 Vue.mixin() 区别,我认为 extend 更多的是公众的组件化,也就是类的多态,外观模式。
- Vue.initAssetRegisters():用来将实例上的 component、directive、filter 对象配置到全局的 Vue.options 上。
问:Vue 中当父子组件配置选项发生冲突时,是如何处理?
答:Vue 混合父子组件配置选项时,采用配置项的 key 值作为标识,若 key 值相等冲突,则子组件的配置选项将覆盖父组件的配置选项。
问:初始化后,自定义往 Vue 实例上的响应式对象添加属性,添加的属性是否具有响应式?
答:Vue 响应式是在初始化过程进行双向绑定和发布订阅模式实现的,若在后续自定义手动添加属性,无论是原始数据类型还是复杂数据类型都是不具备响应式的。
问:如何自定义数据实现响应式?
答:首先要保证挂载的对象是响应式的,也就是有
target.\_\_ob__的标识符才能实现响应式,否则只能一种普通对象的静态挂载。
我们可以使用vm.$set()来实现自定义数据的响应式,如对象:vm.set(obj,key,val),数组:vm.set(obj, key, val),数组:vm.set(obj,key,val),数组:vm.set(array, idx, val)。
问:
vm.$set()和vm.$delete()方法,分别如何操作对象和数组?
答:
vm.$set()
- 操作对象使用的是 defineReactive(ob.value, key, val) 方法,原理是 Object.definePrototype() 来拦截,并调用 ob.dep.notify() 通知该对象已完成操作。
- 操作数组使用的是遍历数组,对指定下标使用 target.splice(key, 1, val),实现响应式。
vm.$delete()
- 操作对象使用操作符 delete,并调用 ob.dep.notify() 通知该对象已完成操作。
- 操作数组的方法与
vm.$set()一致,指定下标使用 target.splice(key, 1, val) 截取删除。
位置:
/src/core/instance/lifecycle.js我根据打断点,来明确一下初始化/更新时 patch 调用的顺序逻辑
初始化调用:
this._init(options)=>vm.$mount(vm.$options.el)=>mountComponent(this, el, hydrating)=>new Watcher()=>watcher.get()=>updateComponent()=>vm._update(vm._render(), hydrating)=>vm.__patch__(vm.$el, vnode, hydrating, false)
更新时调用:
observe.set()=>dep.notify()=>watcher.update()=>nextTick()=>watcher.run()=>watcher.get()=>updateComponent()=>vm._update(vm._render(), hydrating)=>vm.__patch__(prevVnode, vnode)
// patch 渲染更新的入口
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$el// vm._vnode 由 vm._render() 生成// 老虚拟节点const prevVnode = vm._vnodeconst restoreActiveInstance = setActiveInstance(vm)// 新虚拟节点vm._vnode = vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if (!prevVnode) {// 只有新虚拟节点,即为首次渲染,初始化页面时走这里vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)} else {// 有新老节点,即为更新数据渲染,更新页面时走这里vm.$el = vm.__patch__(prevVnode, vnode)}// 缓存虚拟节点restoreActiveInstance()// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as well// 当父子节点的虚拟节点一致,也更新父节点的 $elif (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.
}
源码核心代码顺序以深度遍历形式
位置:
/src/core/observer/index.js
// patch 方法,hydrating 是否服务端渲染,removeOnly 是否使用了 过渡组
// 1.vnode 不存在,则摧毁 oldVnode
// 2.vnode 存在且 oldVnode 不存在,表示组件初次渲染,添加标示且创建根节点
// 3.vnode 和 oldVnode 都存在时
// 3.1.oldVnode 不是真实节点表示更新阶段(都是虚拟节点),执行 patchVnode,生成 vnode
// 3.2.oldVnode 是真实元素,表示初始化渲染,执行 createElm 基于 vnode 创建整棵 DOM 树并插入到 body 元素下,递归更新父占位符节点元素,完成更新后移除 oldnode。
// 4.最后 vnode 插入队列并生成返回 vnode
function patch(oldVnode, vnode, hydrating, removeOnly) {// vnode 不存在,表示删除节点,则摧毁 oldVnodeif (isUndef(vnode)) {// 执行 oldVnode 也就是未更新组件生命周期 destroy 钩子// 执行 oldVnode 各个模块(style、class、directive 等)的 destroy 方法// 如果有 children 递归调用 invokeDestroyHookif (isDef(oldVnode)) invokeDestroyHook(oldVnode)return}let isInitialPatch = falseconst insertedVnodeQueue = []// vnode 存在且 oldVnode 不存在if (isUndef(oldVnode)) {// empty mount (likely as component), create new root element// 组件初次渲染,创建根节点isInitialPatch = truecreateElm(vnode, insertedVnodeQueue)} else {// 判断 oldVnode 是否为真实元素const isRealElement = isDef(oldVnode.nodeType)// 不是真实元素且 oldVnode 和 vnode 是同一个节点,执行 patchVnode 直接更新节点if (!isRealElement && sameVnode(oldVnode, vnode)) {patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)// 真实元素或者新老节点不相同} else {if (isRealElement) {// mounting to a real element// check if this is server-rendered content and if we can perform// a successful hydration.// oldVnode 是元素节点且有服务器渲染的属性if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {oldVnode.removeAttribute(SSR_ATTR)hydrating = true}// ...省略代码,服务端渲染执行 invokeInsertHook(vnode, insertedVnodeQueue, true)// either not server-rendered, or hydration failed.// create an empty node and replace it// 不是服务端渲染,或 hydration 失败,创建一个空的 vnode 节点oldVnode = emptyNodeAt(oldVnode)}// 拿到 oldVnode /父 oldVnode 的真实元素const oldElm = oldVnode.elmconst parentElm = nodeOps.parentNode(oldElm)// 基于 vnode 创建整棵 DOM 树并插入到 body 元素下createElm(vnode,insertedVnodeQueue,// extremely rare edge case: do not insert if old element is in a// leaving transition. Only happens when combining transition +// keep-alive + HOCs. (#4590)oldElm._leaveCb ? null : parentElm,nodeOps.nextSibling(oldElm))// 递归更新父占位符节点元素if (isDef(vnode.parent)) {let ancestor = vnode.parentconst patchable = isPatchable(vnode)while (ancestor) {for (let i = 0; i < cbs.destroy.length; ++i) {cbs.destroy[i](ancestor)}ancestor.elm = vnode.elmif (patchable) {for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, ancestor)}// #6513// invoke insert hooks that may have been merged by create hooks.// e.g. for directives that uses the "inserted" hook.const insert = ancestor.data.hook.insertif (insert.merged) {// start at index 1 to avoid re-invoking component mounted hookfor (let i = 1; i < insert.fns.length; i++) {insert.fns[i]()}}} else {registerRef(ancestor)}ancestor = ancestor.parent}}// 完成更新,移除 oldVnode// 当有父节点时,指定范围删除自己if (isDef(parentElm)) {removeVnodes([oldVnode], 0, 0)// 没有父节点时} else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode)}}}// 将虚拟节点插入队列中invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)return vnode.elm
}
位置:
src/core/vdom/patch.js
// 基于 vnode 创建真实 DOM 树
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index
) {// 直接复制缓存的 vnodeif (isDef(vnode.elm) && isDef(ownerArray)) {vnode = ownerArray[index] = cloneVNode(vnode)}vnode.isRootInsert = !nested // for transition enter check// 创建 vnode 组件if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return}// 获取 data 对象const data = vnode.data// 所有的孩子节点const children = vnode.childrenconst tag = vnode.tagif (isDef(tag)) {// ...省略代码:当标签未知时发出警告// 创建新节点vnode.elm = vnode.ns? nodeOps.createElementNS(vnode.ns, tag): nodeOps.createElement(tag, vnode)setScope(vnode)// 递归创建所有子节点(普通元素、组件)createChildren(vnode, children, insertedVnodeQueue)if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}// 将节点插入父节点insert(parentElm, vnode.elm, refElm)if (process.env.NODE_ENV !== 'production' && data && data.pre) {creatingElmInVPre--}// 处理注释节点并插入父节点} 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)}
}
位置:
/src/core/vdom/patch.js
// 更新节点
// 1.新老节点相同,直接返回
// 2.静态节点,克隆复用
// 3.全部遍历更新 vnode.data 上的属性
// 4.若是文本节点,直接更新文本
// 5.若不是文本节点
// 5.1 都有孩子,则递归执行 updateChildren 方法(diff 算法更新)
// 5.2 ch 有 oldCh 没有,则表明新增节点 addVnodes
// 5.3 ch 没有 oldCh 有,则表明删除节点 removeVnodes
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly
) {// 老节点和新节点相同,直接返回if (oldVnode === vnode) {return}// 缓存过的 vnode,直接克隆 vnodeif (isDef(vnode.elm) && isDef(ownerArray)) {// clone reused vnodevnode = ownerArray[index] = cloneVNode(vnode)}const elm = vnode.elm = oldVnode.elm// 异步占位符节点if (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)} else {vnode.isAsyncPlaceholder = true}return}// reuse element for static trees.// note we only do this if the vnode is cloned -// if the new node is not cloned it means the render functions have been// reset by the hot-reload-api and we need to do a proper re-render.if (isTrue(vnode.isStatic) &&isTrue(oldVnode.isStatic) &&vnode.key === oldVnode.key &&(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {// 新旧节点都是静态的而且两个节点的 key 一样,并且新节点被克隆了或者新节点有 v-once 指令,则用 oldVnode 的组件节点,且跳出,不进行 diff 更新vnode.componentInstance = oldVnode.componentInstancereturn}// 执行组件的 prepatch 钩子let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}// 孩子const oldCh = oldVnode.childrenconst ch = vnode.children// 更新 vnode 上的属性if (isDef(data) && isPatchable(vnode)) {// 全部遍历更新(Vue3 做了大量优化)for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)}// 新节点不是文本节点if (isUndef(vnode.text)) {if (isDef(oldCh) && isDef(ch)) {// 如果 oldCh 和 ch 不同,开始更新子节点(也就是 diff 算法)if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)// 只有 ch} else if (isDef(ch)) {if (process.env.NODE_ENV !== 'production') {// 检查是否有重复 key 值,给予警告checkDuplicateKeys(ch)}// oldVnode 中有文本信息,创建文本节点并添加if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)// 只有 oldCh} else if (isDef(oldCh)) {// 删除节点的操作removeVnodes(oldCh, 0, oldCh.length - 1)// oldVnode 上有文本} else if (isDef(oldVnode.text)) {// 置空文本nodeOps.setTextContent(elm, '')}// vnode 是文本,若 oldVnode 和 vnode 文本不相同} else if (oldVnode.text !== vnode.text) {// 更新文本节点nodeOps.setTextContent(elm, vnode.text)}// 还有 data 数据,执行组件的 prepatch 钩子if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}
}
位置:
/src/core/vdom/patch.js
// 删除 vnode 节点
function removeVnodes(vnodes, startIdx, endIdx) {for (; startIdx <= endIdx; ++startIdx) {const ch = vnodes[startIdx]// 有子节点if (isDef(ch)) {// 不是文本节点if (isDef(ch.tag)) {// patch() 方法中有说明removeAndInvokeRemoveHook(ch)invokeDestroyHook(ch)} else { // Text node// 直接移除该元素removeNode(ch.elm)}}}
}
src/core/vdom/patch.js
// 更新子节点采用了 diff 算法
// 做了四种假设,假设新老节点开头结尾有相同节点的情况,一旦命中假设,就避免了一次循环,以提高执行效率
// 如果不幸没有命中假设,则执行遍历,从老节点中找到新开始节点
// 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
// 如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
// 如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {// 为 diff 算法假设做初始化:新老子节点的头尾下标和对应值let oldStartIdx = 0let newStartIdx = 0let oldEndIdx = oldCh.length - 1let oldStartVnode = oldCh[0]let oldEndVnode = oldCh[oldEndIdx]let newEndIdx = newCh.length - 1let newStartVnode = newCh[0]let newEndVnode = newCh[newEndIdx]let oldKeyToIdx, idxInOld, vnodeToMove, refElm// 的标识符const canMove = !removeOnlyif (process.env.NODE_ENV !== 'production') {// 若重复 key 则发出警告checkDuplicateKeys(newCh)}// 遍历新老节点数组,直到一方取完值while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 老开始节点无值,表示更新过,向右移动下标(往后看)if (isUndef(oldStartVnode)) {oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left// 老结束节点无值,表示更新过,向左移动下标(往后看)} else if (isUndef(oldEndVnode)) {oldEndVnode = oldCh[--oldEndIdx]// 新老的开始/结束节点是相同节点,返回 patchVnode 阶段,不更新比较// 因为两个都不比较,同时移动下标} else if (sameVnode(oldStartVnode, newStartVnode)) {patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]} else if (sameVnode(oldEndVnode, newEndVnode)) {patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]// 新尾和老头/新头和老尾相等// 一样需要移动下标,进行 ch 数组下个节点的判断} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)// 包裹的组件时使用,如轮播图情况。canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode = oldCh[++oldStartIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved leftpatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[++newStartIdx]// 四种常规 web 操作假设都不成立,则不能优化,开始遍历更新} else {// 当老节点的 key 对应不上 idx 时// 在指定 idx 的范围内,找到 key 在老节点中的下标位置// 形成 map = { key1: id1, key2: id2, ...}if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)// 若新开始节点有 key 值,在老节点的 key 和 id 映射表 map 中找到返回对应的 id 下标值// 若新开始节点没有 key 值,则找到老节点数组中新开始节点的值,返回 id 下标idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)// 若新开始节点不存在老节点中,那就是新建元素if (isUndef(idxInOld)) { // New elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)// 新开始节点存在老节点中,开始判断情况更新} else {vnodeToMove = oldCh[idxInOld]// 如果两个节点不但 key 相同,节点也是相同,则直接返回 patchVnodeif (sameVnode(vnodeToMove, newStartVnode)) {patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)// 将该老节点置为 空,避免新节点反复找到同一个节点oldCh[idxInOld] = undefined// 还是判断 标签的情况canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// 两个节点虽然 key 相等,但节点不相等,看作新元素,创建节点createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}}// 老节点向后移动一个newStartVnode = newCh[++newStartIdx]}}// 新老节点某个数组被遍历完了// 新的有多余,那就是新增if (oldStartIdx > oldEndIdx) {refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)// 老的有多余,那就是删除} else if (newStartIdx > newEndIdx) {removeVnodes(oldCh, oldStartIdx, oldEndIdx)}
}
Vue 源码「patch」致命七问。
- Vue 初始化阶段和更新阶段,是如何进入 patch 阶段。(或 Vue 初始化和更新阶段分别发生什么等相关问题)
- Vue patch 阶段做了什么?
- 你知道 patch 方法有几个参数?最后两个参数分别有什么作用?
- diff 算法是什么?起到什么作用?
- 若节点 key 值相等且节点不同,新节点会覆盖旧节点吗?
- vnode 是什么?有什么用?
- Vue 如何处理 Vnode 上的属性?
思考问题后,答案在下方,根据自己阅读整理源码,对自己提出有意义的问题并自我回答。不确保是面试热点题噢(切勿入题太深)
问:Vue 初始化阶段和更新阶段,是如何进入 patch 阶段。(或 Vue 初始化和更新阶段分别发生什么等相关问题)
答:
- Vue 初始化分为以下几个阶段
- 初始化时执行 Vue._init(),初始化组件的各种属性和事件并触发 beforeCreate 钩子函数,之后初始化响应式数据并最后触发 created 钩子函数
- 执行 vm.$mount(),调用 mountComponent(),初始化 render 函数和组件的框架调用 beforeMount 钩子函数,初始化 dep.target。
- 创建当前组件的 Watcher 实例,执行 watcher.get() 方法获取当前 watcher 上的数据。
- 执行 updateComponent() 回调来执行 vm.update() 方法,因初始化渲染,故直接调用 vm.patch 创建空元素。生成 vnode 虚拟节点。
- 执行 proxy 对数据进行响应式处理,执行 dep.depend() 收集对应响应式数据上所有 watcher 的依赖,watcher 也收集 dep 的依赖实现双向绑定。
- 开始调用 render 渲染函数(关键是 _createElement())根据 vnode 递归遍历实现整个真实页面。
- Vue 更新分为以下几个阶段
- 当数据更新时,进入数据对应的监听者 observe.set() 方法中调用 dep.notify() 发布通知所有 watcher 执行 update() 方法。
- 接下来就是异步更新内容,封装各种 watcher 队列和刷新函数队列,进入 nextTick() 中执行 timerFunc() 利用浏览器异步任务队列来实现异步更新。
- 等到浏览器异步任务队列开始执行 flushCallbacks(),便调用 callbacks 中每个 flushSchedulerQueue() 执行回调 watcher.run()
- watcher 通过 get() 调用 updateComponent() 中的 vm.patch(prevVnode, vnode) 开始进入递归遍历节点的 patch 阶段。
- patch 阶段通过判断新老子节点的情况,调用 updateChildren() 开始 diff 算法假设和优化,最终形成 vnode 虚拟节点。
- 开始调用 render 渲染函数,根据 vnode 递归遍历实现整个真实页面。
问:Vue patch 阶段做了什么?
答:patch 阶段主要进行了四点内容。
- vnode 不存在,则摧毁 oldVnode
- vnode 存在且 oldVnode 不存在,表示组件初次渲染,添加标示且创建根节点
- vnode 和 oldVnode 都存在时
- oldVnode 不是真实节点表示更新阶段(都是虚拟节点),执行 patchVnode,生成 vnode
- oldVnode 是真实元素,表示初始化渲染,执行 createElm 基于 vnode 创建整棵 DOM 树并插入到 body 元素下,递归更新父占位符节点元素,完成更新后移除 oldnode。
- 最后 vnode 插入队列并生成返回 vnode。
问:你知道 patch 方法有几个参数?最后两个参数分别有什么作用?
答:
patch(oldVnode, vnode, hydrating, removeOnly),patch 方法共有四个参数,最后两个参数为hydrating和removeOnly。它们的作用分别为:
- hydrating 判断是否服务器渲染执行。在 patch 阶段时,oldVnode 是真实元素,初始化渲染时,若 oldVnode 是元素节点且有服务器渲染的属性,则设置 hydrating 为 true,表示服务端渲染。
- removeOnly 判断节点是否被
包裹着。在 updateChildren 中判断插入执行 nodeOps.insertBefore(),如轮播图等案例。
问:diff 算法是什么?起到什么作用?
答:diff 算法是在 patch 阶段,遍历比较更新子节点时,利用 web 常规操作的思维做的四种假设,一旦命中假设,就避免了循环,以提高执行效率,起到绝大部分更新情况的优化效果。
- 四种假设分别为:
- 老开始和新开始节点相同
- 老结束和新结束节点相同
- 老开始和新结束节点相同
- 老结束和新开始节点相同
当 diff 算法阶段都未命中假设时,则利用key值映射 oldVnode 的下标值生成 map 对象,以此来利用 key 值快速找到新节点在旧节点中的下标位置,进行判断比对,若没有 key 值,则只能利用新节点的值暴力遍历比较旧节点的值进行判断更新。
最后新老数组中某一数组遍历完成,则进行添加或删除节点操作。
问:若节点 key 值相等且节点不同,新节点会覆盖旧节点吗?
答:在 diff 算法阶段,当新节点找到在老节点相同 key 且节点不同时,会看作是创建新节点执行
createElm()
问:vnode 是什么?有什么用?
答:vnode 是利用 JS 对象模拟真实 DOM 树,抽象了渲染的过程,形成一个 JS 对象。作用如下:
- 减少对真实DOM的操作,大大减轻了浏览器的负担。
- 因 JavaScript 本质是弱语言跨平台的性质,故虚拟 DOM 可以跨平台使用。
- 虚拟 DOM 可以快速对比两次状态的差异以便更新真实 DOM。
问:Vue 如何处理 vnode 上的属性?
答:在 patchVnode 方法中,直接遍历更新 vnode 上的全部属性。Vue3 将进行大量优化更新。
最后放一个 Vnode 的类,位置:
/src/core/vdom/vnode.js
class VNode {tag: string | void;data: VNodeData | void;children: ?Array;text: string | void;elm: Node | void;ns: string | void;context: Component | void; // rendered in this component's scopekey: string | number | void;componentOptions: VNodeComponentOptions | void;componentInstance: Component | void; // component instanceparent: VNode | void; // component placeholder node// strictly internalraw: boolean; // contains raw HTML? (server only)isStatic: boolean; // hoisted static nodeisRootInsert: boolean; // necessary for enter transition checkisComment: boolean; // empty comment placeholder?isCloned: boolean; // is a cloned node?isOnce: boolean; // is a v-once node?asyncFactory: Function | void; // async component factory functionasyncMeta: Object | void;isAsyncPlaceholder: boolean;ssrContext: Object | void;fnContext: Component | void; // real context vm for functional nodesfnOptions: ?ComponentOptions; // for SSR cachingdevtoolsMeta: ?Object; // used to store functional render context for devtoolsfnScopeId: ?string; // functional scope id supportconstructor (tag?: string,data?: VNodeData,children?: ?Array,text?: string,elm?: Node,context?: Component,componentOptions?: VNodeComponentOptions,asyncFactory?: Function) {this.tag = tagthis.data = datathis.children = childrenthis.text = textthis.elm = elmthis.ns = undefinedthis.context = contextthis.fnContext = undefinedthis.fnOptions = undefinedthis.fnScopeId = undefinedthis.key = data && data.keythis.componentOptions = componentOptionsthis.componentInstance = undefinedthis.parent = undefinedthis.raw = falsethis.isStatic = falsethis.isRootInsert = truethis.isComment = falsethis.isCloned = falsethis.isOnce = falsethis.asyncFactory = asyncFactorythis.asyncMeta = undefinedthis.isAsyncPlaceholder = false}// DEPRECATED: alias for componentInstance for backwards compat./* istanbul ignore next */get child (): Component | void {return this.componentInstance}
}
上一篇:Flutter基础知识