关于vue.js:上帝视角看Vue源码整体架构相关源码问答

0次阅读

共计 38774 个字符,预计需要花费 97 分钟才能阅读完成。

前言

这段时间利用课余时间夹杂了很多很多事把 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 源码「初始化」致命五问。

  1. beforeCreate 钩子函数前实现了什么?
  2. 父子组件中,子组件调用执行自身注册的自定义事件 A(),那么父子组件中,谁监听事件 A() 的执行调用?
  3. created 钩子函数前实现了什么?
  4. initInjections(vm)initState(vm)initProvide(vm) 三者的执行程序可否变动?
  5. 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、watch
export 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

// 为对象创立观察者 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
}

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 源码「响应式原理」致命五问。

  1. 什么是 MVVM 模式?
  2. Vue 的双向绑定原理?
  3. Vue 如何解决响应式数据?
  4. computed 和 watch 的个性区别?
  5. 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 的实例去实现的响应式,次要有以下个性区别。

  1. computed 默认为懒执行,dirty 为 true。watch 有 immediate 配置,能够实现立刻执行一次 cb。
  2. computed 反对缓存,依赖数据产生扭转,才会从新进行计算。watch 不反对缓存,立刻响应式变动。
  3. computed 不反对异步。watch 反对异步。
  4. computed 的 cb 函数默认走 get 办法。watch 的 cb 函数第一个参数是新值,第二个参数是旧值。

五答

问:computed 和 watch 的应用场景区别?

答:computed 和 watch 应用场景的区别根本原因是因它们的个性不同,大抵有以下的场景区别。

  • 抉择 computed

    1. 当数据须要缓存时
    2. 当数据依赖其余数据计算失去时
    3. 逻辑较为简单并无需异步操作时(watch 耗费较大)
  • 抉择 watch

    1. 当执行异步操作时
    2. 即时监听数据实现较为简单的回调函数时

异步更新

Vue 源码的异步更新也就是响应式原理的进一步深刻,上面援用以下官网对于异步更新的介绍来进一步理解这个概念。

可能你还没有留神到,Vue 在更新 DOM 时是 异步 执行的。只有侦听到数据变动,Vue 将开启一个队列,并缓冲在同一事件循环中产生的所有数据变更。如果同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除反复数据对于防止不必要的计算和 DOM 操作是十分重要的。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行理论 (已去重的) 工作。Vue 在外部对异步队列尝试应用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不反对,则会采纳 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 timerFunc
if (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 源码「异步更新」致命五问。

  1. Vue 响应式原理中的异步更新是如何实现?
  2. Vue 默认更新是同步的还是异步的?
  3. Vue 是如何防止反复执行同一次异步更新?
  4. Vue 的 nextTick 全局 API 是如何实现的?
  5. 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() 办法详情:

  1. 首先判断两个非凡标记

    • 是否为 lazy 懒更新,则设置 dirty 为 true,以标记以后 watcher 为懒更新
    • 再判断是否有 sync 同步更新标记,间接执行 watcher.run(),Vue 官网不举荐应用,文档没有该属性。
  2. 而后将 watcher 放入 queue 队列中,放入队列有两种形式,以 flushing 标记判断

    • 若无在刷新队列中,间接 push 进 queue 队列
    • 若正在刷新队列中,按 watcher.id 进行升序排序,确保更新的程序
  3. 而后调用 nextTick(),将 flushSchedulerQueue(刷新以后 watcher 队列的办法)放入 callbacks 数组中。若浏览器的工作队列中无 flushCallbacks 函数,则执行 timerFunc()。(用 pending 来判断管制)
  4. timerFunc() 将 flushCallbacks 函数(执行第 3 点中 callbacks 数组中的所有 flushSchedulerQueue 办法)放入浏览器的异步工作队列中
  5. 期待浏览器异步工作队列执行 callbacks 数组中的 flushSchedulerQueue 办法。
  6. 每个 flushSchedulerQueue 办法中先将 queue 队列排序,再遍历 queue 执行 watcher.before() 和 watcher.run() 办法,而后再初始化异步更新队列,自此异步更新实现。

二答

问:Vue 默认更新是同步的还是异步的?

答:Vue 默认异步更新,通过 watcher.async。Vue 源码还设置了开启同步更新的操作,能够通过设置 watcher.sync 的属性,在 watcher.update() 办法时并间接执行 watcher.run() 办法进行更新操作。但 Vue 官网不举荐应用该属性,因同步更新机制将阻塞后续工作的执行,整个组件更新将大打折扣。

三答

问:Vue 是如何防止反复执行同一次异步更新?

答:通过三个标识符的操作来进行防止反复执行同一次的异步更新。

  1. 在将 watcher 放入 watcher 队列时,进行了 id 的缓存,防止反复 watcher 增加到 queue 数组。
  2. 通过 waiting 判断是否正在刷新 queue 队列,防止反复执行刷新 queue 队列。
  3. 通过 pending 判断浏览器的异步工作队列中是否有刷新 callbacks(放的是刷新 queue 队列的工作)数组的工作,防止浏览器异步工作队列反复执行刷新 callbacks 数组的工作。

四答

问:Vue 的 nextTick 全局 API 是如何实现的?

答:Vue.nextTick 将传递的刷新 watcher 队列的回调函数 用 try catch 包裹而后放入 callbacks 数组。
在浏览器异步工作队列无其余刷新 callbacks 数组的办法时,执行 timerFunc 函数,放入以后刷新 callbacks 数组的办法。
进而达到 在下次 DOM 更新循环完结之后执行提早回调。在批改数据之后立刻应用这个办法,获取更新后的 DOM。 的性能

五答

问:Vue 是如何将刷新 callbacks 数组的函数放入浏览器工作队列进行异步更新的?

答:依据浏览器工作队列异步执行的效率来抉择放入办法的优先级,别离为:

  1. Promise.resolve().then(flushCallbacks)
  2. new MutationObserver(flushCallbacks)

    • 提供了监督对 DOM 树所做更改的能力(HTML5 中的新个性)
  3. setImmediate(flushCallbacks)
  4. setTimeout(flushCallbacks, 0)

Vue 全局 API

地位:/src/core/global-api/index.js

调用: /src/core/index.js

入口

// 初始化全局配置和 API
export 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」致命六问。

  1. Vue 初始化全局 API 时,做了什么?
  2. Vue 全局 API 有什么作用?
  3. Vue 中当父子组件配置选项发生冲突时,是如何解决?
  4. 初始化后,自定义往 Vue 实例上的响应式对象增加属性,增加的属性是否具备响应式?
  5. 如何自定义数据实现响应式?
  6. 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(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 插入队列并生成返回 vnode
function 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 有,则表明删除节点 removeVnodes
function 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」致命七问。

  1. Vue 初始化阶段和更新阶段,是如何进入 patch 阶段。(或 Vue 初始化和更新阶段别离产生什么等相干问题)
  2. Vue patch 阶段做了什么?
  3. 你晓得 patch 办法有几个参数?最初两个参数别离有什么作用?
  4. diff 算法是什么?起到什么作用?
  5. 若节点 key 值相等且节点不同,新节点会笼罩旧节点吗?
  6. vnode 是什么?有什么用?
  7. Vue 如何解决 Vnode 上的属性?

思考问题后,答案在下方,依据本人浏览整顿源码,对本人提出有意义的问题并自我答复。不确保是面试热点题噢(切勿入题太深)

致命七答

一答

问:Vue 初始化阶段和更新阶段,是如何进入 patch 阶段。(或 Vue 初始化和更新阶段别离产生什么等相干问题)

答:

  • Vue 初始化分为以下几个阶段

    1. 初始化时执行 Vue._init(),初始化组件的各种属性和事件并触发 beforeCreate 钩子函数,之后初始化响应式数据并最初触发 created 钩子函数
    2. 执行 vm.$mount(),调用 mountComponent(),初始化 render 函数和组件的框架调用 beforeMount 钩子函数,初始化 dep.target。
    3. 创立以后组件的 Watcher 实例,执行 watcher.get() 办法获取以后 watcher 上的数据。
    4. 执行 updateComponent() 回调来执行 vm.update() 办法,因初始化渲染,故间接调用 vm.__patch__ 创立空元素。生成 vnode 虚构节点。
    5. 执行 proxy 对数据进行响应式解决,执行 dep.depend() 收集对应响应式数据上所有 watcher 的依赖,watcher 也收集 dep 的依赖实现双向绑定。
    6. 开始调用 render 渲染函数(要害是 _createElement())依据 vnode 递归遍历实现整个实在页面。
  • Vue 更新分为以下几个阶段

    1. 当数据更新时,进入数据对应的监听者 observe.set() 办法中调用 dep.notify() 公布告诉所有 watcher 执行 update() 办法。
    2. 接下来就是异步更新内容,封装各种 watcher 队列和刷新函数队列,进入 nextTick() 中执行 timerFunc() 利用浏览器异步工作队列来实现异步更新。
    3. 等到浏览器异步工作队列开始执行 flushCallbacks(),便调用 callbacks 中每个 flushSchedulerQueue() 执行回调 watcher.run()
    4. watcher 通过 get() 调用 updateComponent() 中的 vm.__patch__(prevVnode, vnode) 开始进入递归遍历节点的 patch 阶段。
    5. patch 阶段通过判断新老子节点的状况,调用 updateChildren() 开始 diff 算法假如和优化,最终造成 vnode 虚构节点。
    6. 开始调用 render 渲染函数,依据 vnode 递归遍历实现整个实在页面。

二答

问:Vue patch 阶段做了什么?

答:patch 阶段次要进行了四点内容。

  1. vnode 不存在,则捣毁 oldVnode
  2. vnode 存在且 oldVnode 不存在,示意组件首次渲染,增加标示且创立根节点
  3. vnode 和 oldVnode 都存在时

    1. oldVnode 不是实在节点示意更新阶段(都是虚构节点),执行 patchVnode,生成 vnode
    2. oldVnode 是实在元素,示意初始化渲染,执行 createElm 基于 vnode 创立整棵 DOM 树并插入到 body 元素下,递归更新父占位符节点元素,实现更新后移除 oldnode。
  4. 最初 vnode 插入队列并生成返回 vnode。

三答

问:你晓得 patch 办法有几个参数?最初两个参数别离有什么作用?

答:patch(oldVnode, vnode, hydrating, removeOnly),patch 办法共有四个参数,最初两个参数为 hydratingremoveOnly。它们的作用别离为:

  1. hydrating 判断是否服务器渲染执行。在 patch 阶段时,oldVnode 是实在元素,初始化渲染时,若 oldVnode 是元素节点且有服务器渲染的属性,则设置 hydrating 为 true,示意服务端渲染。
  2. removeOnly 判断节点是否被 <transition-group> 包裹着。在 updateChildren 中判断插入执行 nodeOps.insertBefore(),如轮播图等案例。

四答

问:diff 算法是什么?起到什么作用?

答:diff 算法是在 patch 阶段,遍历比拟更新子节点时,利用 web 惯例操作的思维做的四种假如,一旦命中假如,就 防止了循环,以进步执行效率,起到绝大部分更新状况的优化成果

  • 四种假如别离为:

    1. 老开始和新开始节点雷同
    2. 老完结和新完结节点雷同
    3. 老开始和新完结节点雷同
    4. 老完结和新开始节点雷同
      当 diff 算法阶段都未命中假如时,则利用 key 值映射 oldVnode 的下标值生成 map 对象,以此来利用 key 值疾速找到新节点在旧节点中的下标地位,进行判断比对,若 没有 key 值 ,则只能利用新节点的值暴力遍历比拟旧节点的值进行判断更新。
      最初新老数组中某一数组遍历实现,则进行增加或删除节点操作。

五答

问:若节点 key 值相等且节点不同,新节点会笼罩旧节点吗?

答:在 diff 算法阶段,当新节点找到在老节点雷同 key 且节点不同时,会看作是创立新节点执行 createElm()

六答

问:vnode 是什么?有什么用?

答:vnode 是利用 JS 对象模仿实在 DOM 树,形象了渲染的过程,造成一个 JS 对象。作用如下:

  1. 缩小对实在 DOM 的操作,大大加重了浏览器的累赘。
  2. 因 JavaScript 实质是弱语言跨平台的性质,故虚构 DOM 能够跨平台应用。
  3. 虚构 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}
}
正文完
 0