前言
这段时间利用课余时间夹杂了很多很多事把 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 类型申明
Vue 初始化
地位:/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)}
外围代码
源码外围代码程序以深度遍历模式
initMixin
地位:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) { // 负责 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、watch initState(vm) // 解析实例 vm.$options.provide 对象,挂载到 vm._provided 上,和 inject 对应。 initProvide(vm) // 调用 created 钩子函数 callHook(vm, 'created') }}
相干vue源码视频解说:进入学习
致命五问
Vue 源码「初始化」致命五问。
beforeCreate
钩子函数前实现了什么?- 父子组件中,子组件调用执行自身注册的自定义事件 A(),那么父子组件中,谁监听事件 A() 的执行调用?
created
钩子函数前实现了什么?initInjections(vm)
、initState(vm)
、initProvide(vm)
三者的执行程序可否变动?- Vue 的初始化过程?
思考问题后,答案在下方,依据本人浏览整顿源码,对本人提出有意义的问题并自我答复。不确保是面试热点题噢(切勿入题太深)
致命五答
一答
问:beforeCreate 钩子函数前实现了什么?
答:beforeCreate 之前,次要是在解决 vm 实例上的各种属性配置和自定义事件属性,也就是将 Vue 的壳初始化实现。
首先合并了组件的配置项挂载到全局 vm.$options 上。初始化组件实例关系属性,如:$parent、$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/props initState(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、watchexport function initState (vm: Component) { // 初始化以后实例的 watchers 数组 vm._watchers = [] // 拿到上边初始化合并后的 options 配置项 const opts = vm.$options // props 响应式,挂载到 vm if (opts.props) initProps(vm, opts.props) // 1. 判断 methods 是否为函数 // 2. 办法名与 props 判重 // 3. 挂载到 vm if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { // 初始化 data 并挂载到 vm initData(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) }}
外围代码
源码外围代码程序以深度遍历模式
observe
地位:/src/core/observer/index.js
// 为对象创立观察者 Observeexport 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}
Observer
地位:/src/core/observer/index.js
// 监听器类export class Observer { // ... 配置 constructor (value: any) { this.value = value // 实例化一个发布者 Dep this.dep = new Dep() this.vmCount = 0 def(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<any>) { for (let i = 0, l = items.length; i < l; i++) { // 判断,优化,创立观察者实例 observe(items[i]) } }}
Dep
地位:/src/core/observer/dep.js
// 订阅器类export default class Dep { constructor () { // 该 dep 发布者的 id this.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 () { // ...省略代码 }}
Watcher
地位:/src/core/observer/watcher.js
// 订阅者类,一个组件一个 watcher,订阅的数据扭转时执行相应的回调函数export default class Watcher { ...代码省略:constructor() 结构配置一个 watcher get () { // 关上 Dep.target,Dep.target = this pushTarget(this) // value 为回调函数执行的后果 let value const vm = this.vm try { // 这里执行 updateComponent,进入 patch 阶段更新视图。 value = this.getter.call(vm, vm) } catch (e) { // ...捕捉异样 } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (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 */ // // 懒执行如 computed if (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.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subscriber list. */ teardown () { // ...销毁该 watcher 实例 }}
defineReactive
地位:/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) : val if (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 // 无新值,不必更新则间接 return if (newVal === value || (newVal !== newVal && value !== value)) { return } // 没有 setter,只读属性,则间接 return if (getter && !setter) return // 设置新值 if (setter) { setter.call(obj, newVal) } else { val = newVal } // 将新值进行响应式 childOb = !shallow && observe(newVal) // dep 发布者告诉更新 dep.notify() } })}
proxy
地位:/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) }}
外围代码
源码外围代码程序以深度遍历模式
queueWatcher
地位:/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 - 1 while (i > index && queue[i].id > watcher.id) { i-- } // 用数组切割办法 queue.splice(i + 1, 0, watcher) } // queue the flush // 正在刷新队列 if (!waiting) { // 设置标记,确保只有一条异步更新队列 waiting = true if (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) } }}
run
地位:
/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.value this.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) } } }}
nextTick
地位:/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-line if (!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 timerFuncif (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { // 第一选 Promise.resolve().then() 放入 flushCallbacks p.then(flushCallbacks) // 若挂掉了,采纳增加空计时器来“强制”刷新微工作队列。 if (isIOS) setTimeout(noop) } isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.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发生变化时被调用。 // MDN const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.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 = false const 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)
Vue 全局 API
地位:
/src/core/global-api/index.js
调用:
/src/core/index.js
入口
// 初始化全局配置和 APIexport function initGlobalAPI (Vue: GlobalAPI) { // 全局配置 config const configDef = {} configDef.get = () => config if (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.xx Vue.util = { // 正告 warn, // 选项扩大 extend, // 选项合并 mergeOptions, // 设置响应式 defineReactive } // Vue.set() Vue.set = set // Vue.delete() // 解决操作与下列 set() 基本一致。 // target 为对象时,采纳运算符 delete Vue.delete = del // Vue.nextTick() // 不多 BB 就是上节 异步更新原理中的 nextTick // 1.将回调函数(flushSchedulerQueue) 放入 callbacks 数组。 // 2.向浏览器工作队列中增加 flushCallbacks 函数,达到下次 DOM 渲染更新后立刻调用 Vue.nextTick = nextTick // Vue.observable() 响应式办法 // 也不多 BB 就是上上节 响应式原理中的 observe // 为对象创立一个 Oberver 监听器实例,并监听 Vue.observable = <T>(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)}
外围代码
源码外围代码程序以深度遍历模式
set()
地位:/src/core/observer/index.js
// 通过 vm.$set() 办法给对象或数组设置响应式export function set (target: Array<any> | 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] = val return val } // 设置未定义的对象值 // 获取以后 target 对象的 __ob__,判断是否已被 observer 设置为响应式对象。 const ob = (target: any).__ob__ // ...省略代码:不能向 _isVue 和 ob.vmCount = 1 的根组件增加新值 // 若 target 不是响应式对象,间接往 target 设置动态属性 if (!ob) { target[key] = val return val } // 若 target 是响应式对象 // defineReactive() 增加上响应式属性 // 立刻调用对象上的订阅器 dep 派发更新 defineReactive(ob.value, key, val) ob.dep.notify() return val}
initExtend
地位:/src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) { // 每个实例构造函数(包含Vue)都有一个惟一的 cid。这使咱们可能创立包装的“子对象”,用于原型继承和缓存它们的构造函数。 Vue.cid = 0 let cid = 1 // Vue 去扩大子类 Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid // 缓存屡次 Vue.extend 应用同一个配置项时 const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } // 是否为无效的配置项名,防止反复 const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) } // 定义 Sub 构造函数,筹备合并 const Sub = function VueComponent(options) { // 就是 Vue 实例初始化的 init() 办法 this._init(options) } // 通过原型继承的形式继承 Vue Sub.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.extend Sub.mixin = Super.mixin Sub.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.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // 缓存 cachedCtors[SuperId] = Sub return Sub }}function initProps (Comp) { const props = Comp.options.props for (const key in props) { proxy(Comp.prototype, `_props`, key) }}function initComputed (Comp) { const computed = Comp.options.computed for (const key in computed) { defineComputed(Comp.prototype, key, computed[key]) }}
initAssetRegisters
地位:/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,默认为 id definition.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] = definition this.options[type + 's'][id] = definition return definition } } })}
致命六问
Vue 源码「全局 API」致命六问。
- Vue 初始化全局 API 时,做了什么?
- Vue 全局 API 有什么作用?
- Vue 中当父子组件配置选项发生冲突时,是如何解决?
- 初始化后,自定义往 Vue 实例上的响应式对象增加属性,增加的属性是否具备响应式?
- 如何自定义数据实现响应式?
- vm.$set() 和 vm.$delete() 办法,别离如何操作对象和数组? 思考问题后,答案在下方,依据本人浏览整顿源码,对本人提出有意义的问题并自我答复。不确保是面试热点题噢(切勿入题太深)
致命六答
一答
问:Vue 初始化全局 API 时,做了什么?
答:1.Vue 初始化了全局的 config 配置并设为响应式。2.裸露一些工具办法,如日志、选项扩大、选项合并、设置对象响应式3.裸露全局初始化办法,如 Vue.set、Vue.delete、Vue.nextTick、Vue.observable4.裸露组件配置注册办法,如 Vue.options.components、Vue.options.directives、Vue.options.filters、Vue.options._base5.裸露全局办法,如 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(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) 截取删除。
Vue patch 渲染更新
地位:
/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 = this const prevEl = vm.$el // vm._vnode 由 vm._render() 生成 // 老虚构节点 const prevVnode = vm._vnode const 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__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well // 当父子节点的虚构节点统一,也更新父节点的 $el if (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.}
外围代码
源码外围代码程序以深度遍历模式
patch()
地位:/src/core/observer/index.js
// patch 办法,hydrating 是否服务端渲染,removeOnly 是否应用了 <transition group> 过渡组// 1.vnode 不存在,则捣毁 oldVnode// 2.vnode 存在且 oldVnode 不存在,示意组件首次渲染,增加标示且创立根节点// 3.vnode 和 oldVnode 都存在时// 3.1.oldVnode 不是实在节点示意更新阶段(都是虚构节点),执行 patchVnode,生成 vnode// 3.2.oldVnode 是实在元素,示意初始化渲染,执行 createElm 基于 vnode 创立整棵 DOM 树并插入到 body 元素下,递归更新父占位符节点元素,实现更新后移除 oldnode。// 4.最初 vnode 插入队列并生成返回 vnodefunction patch(oldVnode, vnode, hydrating, removeOnly) { // vnode 不存在,示意删除节点,则捣毁 oldVnode if (isUndef(vnode)) { // 执行 oldVnode 也就是未更新组件生命周期 destroy 钩子 // 执行 oldVnode 各个模块(style、class、directive 等)的 destroy 办法 // 如果有 children 递归调用 invokeDestroyHook if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // vnode 存在且 oldVnode 不存在 if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element // 组件首次渲染,创立根节点 isInitialPatch = true createElm(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.elm const 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.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (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.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (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}
createElm
地位:src/core/vdom/patch.js
// 基于 vnode 创立实在 DOM 树function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { // 间接复制缓存的 vnode if (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.children const tag = vnode.tag if (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) }}
patchVnode
地位:/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 有,则表明删除节点 removeVnodesfunction patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { // 老节点和新节点雷同,间接返回 if (oldVnode === vnode) { return } // 缓存过的 vnode,间接克隆 vnode if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = 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.componentInstance return } // 执行组件的 prepatch 钩子 let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } // 孩子 const oldCh = oldVnode.children const 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) }}
removeVnodes
地位:/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) } } }}
updateChildren
src/core/vdom/patch.js
// 更新子节点采纳了 diff 算法// 做了四种假如,假如新老节点结尾结尾有雷同节点的状况,一旦命中假如,就防止了一次循环,以进步执行效率// 如果可怜没有命中假如,则执行遍历,从老节点中找到新开始节点// 找到雷同节点,则执行 patchVnode,而后将老节点挪动到正确的地位// 如果老节点先于新节点遍历完结,则残余的新节点执行新增节点操作// 如果新节点先于老节点遍历完结,则残余的老节点执行删除操作,移除这些老节点function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 为 diff 算法假如做初始化:新老子节点的头尾下标和对应值 let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // <transition-group> 的标识符 const canMove = !removeOnly if (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 right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // <transtion-group> 包裹的组件时应用,如轮播图状况。 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(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 element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) // 新开始节点存在老节点中,开始判断状况更新 } else { vnodeToMove = oldCh[idxInOld] // 如果两个节点岂但 key 雷同,节点也是雷同,则间接返回 patchVnode if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 将该老节点置为 空,防止新节点重复找到同一个节点 oldCh[idxInOld] = undefined // 还是判断 <transition-group> 标签的状况 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].elm addVnodes(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 判断节点是否被
<transition-group>
包裹着。在 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<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance }}