前言
Vue 这个框架,我们使用的很是熟练了,那么我们有人想过new Vue()帮我们做了什么?
const app = new Vue({
el:"#app",
// 对象格式
data:{
foo:"foo"
},
// 函数格式
data(){
return {
foo:"foo"
}
}
})
本文涉及源码版本:2.7.14
一、new Vue() 做了什么?
首先,我们可以先了解一下,new Vue 涉及了以下五个方面,所以我们可以从这五个方面进行分析:
- init初始化
- mount挂载
- compiler编译
- render渲染
- patcch补丁
1.1 init初始化
执行 init 操作,包括且不限制 initLifecycle、initState等。
通过入口文件,将配置项传入,调用 _init 方法进行初始化
Vue方法的源码如下:vue-2.7.14\src\core\instance\index.ts
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
import type { GlobalAPI } from 'types/global-api'
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//@ts-expect-error Vue has function type
initMixin(Vue)
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)
export default Vue as unknown as GlobalAPI
通过上面的源码我们可以了解到,new Vue()在不出意外的情况下,执行了方法this._init()
this._init()方法主要步骤:
- 合并配置项
- 初始化生命周期、初始化事件中心、初始化渲染、调用
beforCreate钩子,初始化injection、state/data/props/computed...、provide、调用create钩子; - 最后通过判断有无
vm.$options.el进行vm.$mount
this._init()源码:vue-2.7.14\src\core\instance\init.ts
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 一个标志,将其标记为 vue 实例,而无需执行实例
vm._isVue = true
// 避免实例添加observer
vm.__v_skip = true
// 影响范围(作用域)
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = true
// 合并配置:业务逻辑以及组件的一些特性全都放到了 vm.$options 中
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else { // 顶层的vm
vm.$options = mergeOptions(
// 一些内置组件(keep-alive...)和指令(show、model...)
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
// 初始化vm._renderProxy 为后期渲染做准备
// 开发环境并支持proxy vm._renderProxy = new Proxy(vm)
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化生命周期
initLifecycle(vm)
// 初始化事件
initEvents(vm)
// 初始化渲染
initRender(vm)
// 调用 beforeCreate 回调
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
// 初始化 injections
initInjections(vm) // resolve injections before data/props
// 初始化 state
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// 调用 created 回调
callHook(vm, 'created')
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// el存在就进入下一步操作 - 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
1.2 mount挂载
通过 init 源码我们可以了解到在进行一系列初始化操作后,下一步操作就是挂载
挂载过程中完成了最重要的两件事:
- 初始化
- 建立更新机制
1.2.1 生成 render 函数 -- vm.prototype.$mount()
vm.$mount()方法主要作用是若配置项中没有 render 方法则将 template 作为编译函数的参数生成该方法,最后调用 runtime 中的 mount 方法
mount源码:vue-2.7.14\src\platforms\web\runtime-with-compiler.ts
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 1. 不允许挂载在 html 和 body 上
if (el === document.body || el === document.documentElement) {
__DEV__ &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
// 获得配置项
const options = this.$options
// resolve template/el and convert to render function
// render 不存在
if (!options.render) {
let template = options.template
// 2. 初始化 template
if (template) {
if (typeof template === 'string') {
// 如果模板开头是#,说明是一个ID选择器,通过idToTemplate获取相应的innerHtml
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) { // 如果是node对象,直接获取innerHtml
template = template.innerHTML
} else { // 不合法配置项
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// @ts-expect-error
template = getOuterHTML(el)
}
// 3. 将模板转化为 render 函数
if (template) {
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
mark('compile')
}
// 3.1 将模板转换为 render 函数,得到 ast 抽象树获得的render函数
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
// 3.2保存渲染函数 with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_v(\"\\n……
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 4. 调用$mount方法
return mount.call(this, el, hydrating)
}
1.2.2 完成挂载——mountComponent
这是runtime时的mount方法。最后直接调用了mountComponent方法。所以直接看mountComponent方法吧。
源码:vue-2.7.14\src\platforms\web\runtime\index.ts
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent方法就是 vue 从 beforeMount 到 mounted 的过程。主要步骤如下:
render函数是生成 DOM 的关键,所以在一开始会先判断是否存在,如果不存在创建一个空的虚拟DOM- 调用生命周期钩子
beforeMounts - 生成
updateComponent方法,用于触发页面的更新 - 生成
watcherOptions配置,里面before方法会触发beforeUpdate钩子,将会在触发updateComponent前调用 - 生成组件更新的
watcher, 前面两部分是为了这部分做准备。new Watcher()后触发updateComponent方法的调用,生成页面虚拟DOM,将watcher加入到影响页面变化data的依赖收集器中,这样当data发送变化时,就会触发页面更新,最终进行dom diff,生成真实dom - 调用生命周期钩子
mounted
mountComponent方法源码如下:vue-2.7.14\src\core\instance\lifecycle.ts
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el
// render 不存在 - 警告
if (!vm.$options.render) {
// @ts-expect-error invalid type
vm.$options.render = createEmptyVNode
if (__DEV__) {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 调用钩子函数 beforeMount
callHook(vm, 'beforeMount')
// 初始化 updateComponent
let updateComponent
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
// vm._render() 得到Vnode
// vm._update() 更新页面
vm._update(vm._render(), hydrating)
}
}
// 页面更新前会调用 before 函数,触发钩子函数 beforeUpdate
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 创建组件更新的 watcher,后面会加入到影响页面变化 data 的 deps
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
// flush buffer for flush: "pre" watchers queued in setup()
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run()
}
}
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
// 挂载完成,触发狗仔函数 mounted
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
1.2.3 更新机制形成 - new Watcher()
这里主要分析上面 new Watcher() 后,如何触发 updateComponent 方法的调用。因此只涉及部分 Watcher 类的代码。
在 new Watcher() 构造方法里会初始化一些属性,最重要的是将 this.getter = expOrFn,将 updateComponent 方法赋给了 this.getter。最后初始化 this.value 时,调用了 this.get()。
new watcher() 构造方法源码如下:vue-2.7.14\src\core\observer\watcher.ts
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
recordEffectScope(
this,
// if the active effect scope is manually created (not a component scope),
// prioritize it
activeEffectScope && !activeEffectScope._vm
? activeEffectScope
: vm
? vm._scope
: undefined
)
// 将 watcher 挂载到 vm._watcher 上
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this
}
// options
// 根据 options 初始化一些属性
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
if (__DEV__) {
this.onTrack = options.onTrack
this.onTrigger = options.onTrigger
}
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.post = false
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
// parse expression for getter
// render 类型 expOrFn 肯定是一个方法,所以 this.getter 就是页面更新方法了
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
__DEV__ &&
warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 因为 render 类型的 watcher lazy 值不会是 true(只有computed才会是),所以接下来会调用 get 方法
this.value = this.lazy ? undefined : this.get()
}
在 get 方法中,可以看到这行代码 value = this.getter.call(vm, vm)。就是这里调用了 updateComponent 方法。调用了 updateComponent 方法会触发 vm._update(vm.render(), hydrating)。所以接下来分析 vm._render 方法。
get() 方法如下:vue-2.7.14\src\core\observer\watcher.ts
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
// 相当于 Dep.target = this
pushTarget(this)
let value
const vm = this.vm
try {
// 调用 this.getter 方法,即调用了 updateComponent 方法
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
1.3 生成虚拟DOM - vm._render()
1.3.1 用户自定义 render 函数
这种情况是用户自定义 render 函数。上面调用 render 方法处,传了一个 vm.$createElement 函数作为参数,所以 h 即 vm.$createElement
new Vue({
router,
render: h => h(App),
}).$mount('#app')
找到 vm.$createElement 源码发现最终调用的是 createElement 方法。
vm.$createElement 源码:vue-2.7.14\src\core\instance\render.ts
// 这段代码在initRender中
// 用于用户自己手写的 render 函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
1.3.2 编译生成的 render 函数
这段html代码通过编译后生成的 render 函数如下:
<div id="app">
{{num1}}
{{hello}}
<button @click="changeNum">num1</button>
<button @click="changeNum2">num2</button>
<div v-for="(item,idx) of arr" :key="item">
{{item}}
</div>
<comp :msg="msg" @log-msg="logMsg"></comp>
</div>
代码中,使用了 with(this){} 说明 {} 所有引用都指向 this 对象(即vm)。所以代码中的 _c = vm._c,因此最终其实也是调用 createElement 函数生成虚拟DOM 。
在执行这段代码时,会读取到 data 中的属性,比如 num1、arr等等,读取时会调用这些属性的 getter 方法,在 getter 方法中会判断到 Dep.target 中存在值,会将该值存入依赖收集器,从而 完成依赖收齐。
属性发生改变时,触发 setter 方法,通过 dep 和 watcher 最终会调用 updateComponent 方法进行页面的更新。
vm._c 源码:vue-2.7.14\src\core\instance\render.ts
// src\core\instance\render.ts
// 这段代码在initRender中
// 用于编译后产生的render函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
1.3.3 createElement
createElement源码如下:vue-2.7.14\src\core\vdom\create-element.ts
/**
* SIMPLE_NORMALIZE: 浅扁平
* ALWAYS_NORMALIZE:深扁平
*/
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any, // 使用哪种扁平化方式
alwaysNormalize: boolean // 当render 函数是手写的时候为true
): VNode | Array<VNode> {
// 实际上是平判断 data 是否存在,如果不存在参数向前进一个
if (isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
_createElement方法是创建Vnode的核心:
- 规范
children:children按照normalizationType类型进行扁平化处理,这个目的是规范children,对于同一深度层次的元素,不管是单个元素还是该元素在数组中,只要是属于同一深度层次,都扁平化一个一维数组中 - 生成
Vnode:如果是普通标签,直接new Vnode()。Vue组件则需要通过createComponent生成Vnode
_createElement 源码如下:vue-2.7.14\src\core\vdom\create-element.ts
/**
* 生成虚拟DOM
* @param context: vm
* @param tag: 标签(div、p、ul)
* @param data: data标签上的属性
* @param children: 子节点
* @param normalizationType
* @returns Vnode: 虚拟dom
*/
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// Vnode data 不能为响应式对象
// 有 __ob__ 代表这个对象为响应式对象
if (isDef(data) && isDef((data as any).__ob__)) {
__DEV__ &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\n` + 'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
// <Component :is=""/>
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
// support single function children as default scoped slot
if (isArray(children) && isFunction(children[0])) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根据 normalizationType 对应不同扁平化处理方式
if (normalizationType === ALWAYS_NORMALIZE) {
// 深层扁平化
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 浅层扁平化
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 标签为字符串类型
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (
__DEV__ &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== 'component'
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
// 创建普通标签的Vnode
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if (
(!data || !data.pre) &&
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
) {
// component
// 为组件,调用 createComponent 创建 Vnode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
// 直接创建 Vnode
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
// direct component options / constructor
// tag为组件构造函数或组件选项,创建Vue组件
vnode = createComponent(tag as any, data, context, children)
}
if (isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
1.3.4 创建Vue组件 - createComponent
createComponent作用是返回 Vue 组件的虚拟DOM。同时在这过程中,会构造子类构造函数(这里会调用 _init 方法完成组件初始化)、安装组件钩子函数
所谓的组件化就是把页面拆分成多个组件,组件吧、内部包含自己的 HTML、CSS、JavaScript,这样可以拼成一个模块,并且组件可以复用、拼接,等同于积木一样,一个大的页面可以由很多小的组件拼接而成,接下来我们就用一个例子来看 vue 的组件内部是如何工作的
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
const app = new Vue({
render: (h) => h(App),
}).$mount("#app");
console.log(app);
这段代码是通过 render 函数去渲染的,render 函数调用 createElement,createElement 根据 tag 的不同调用不同的方法生成 Vnode
在例子中,我们可以看到传入的是一个名为 App 的对象,所以继续执行 createComponent。
createComponent在这里主要做了三件事:
- 把传入的组件对象构造成 vue 的子类
- 安装组件钩子函数
- 实例化 Vnode 并返回
createComponent源码如下:vue-2.7.14\src\core\vdom\create-component.ts
export function createComponent(
Ctor: typeof Component | Function | ComponentOptions | void,
data: VNodeData | undefined,
context: Component,
children?: Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// 根实例
// 在 initGlobalAPI 中定义的 vue.options._base = Vue
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
// 1.
// 通过 Vue 的 extend 方法,生成子类构造函数,使得子类也有 Vue 根实例的一些方法
// 其实就是构造Vue的子类
// src/core/global-api/extend.js
Ctor = baseCtor.extend(Ctor as typeof Component)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (__DEV__) {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
// 异步组件
let asyncFactory
// @ts-expect-error
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
// 解析构造函数选项,如果全局混合后应用
// 创建组件构造函数
resolveConstructorOptions(Ctor as typeof Component)
// transform component v-model data into props & events
if (isDef(data.model)) {
// @ts-expect-error
transformModel(Ctor.options, data)
}
// extract props
// @ts-expect-error
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
// @ts-expect-error
// 函数式组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(
Ctor as typeof Component,
propsData,
data,
context,
children
)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
// @ts-expect-error
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
// 2. 安装组件钩子函数
installComponentHooks(data)
// return a placeholder vnode
// @ts-expect-error
// 3.实例化 Vnode 并返回。需要注意组件Vnode没有children,这点在之后的patch在分析
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
// @ts-expect-error
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
// @ts-expect-error
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
1.4 生成真实DOM - vm._update(vnode)
_update 源码:vue-2.7.14\src\core\instance\lifecycle.ts
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 存储在前面的el
const prevEl = vm.$el
// 存储在前面的Vnode
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) {
// initial render 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 更新
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
let wrapper: Component | undefined = vm
while (
wrapper &&
wrapper.$vnode &&
wrapper.$parent &&
wrapper.$vnode === wrapper.$parent._vnode
) {
wrapper.$parent.$el = wrapper.$el
wrapper = wrapper.$parent
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
通过上面源码,我们可以发现,不管是首次渲染,还是更新都执行了方法__patch__。__patch__方法的主要作用是创建、渲染和返回节点。
1.4.1 __patch__
__patch__的源码:vue-2.7.14\src\platforms\web\runtime\index.ts
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
__patch__是来自于 patch 方法的赋值,该方法源码:vue-2.7.14\src\platforms\web\runtime\patch.ts
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
createPatchFunction方法才是__patch__的核心所在
1.4.2 createPatchFunction
源码:vue-2.7.14\src\core\vdom\patch.ts
提示:由于该函数的代码量较大, 所以调整了一下代码结构,方便阅读和理解
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
/**
* 工厂函数,注入平台特有的一些功能操作,并定义一些方法,然后返回 patch 函数
*/
export function createPatchFunction (backend) {
let i, j
const cbs = {}
/**
* modules: { ref, directives, 平台特有的一些操纵,比如 attr、class、style 等 }
* nodeOps: { 对元素的增删改查 API }
*/
const { modules, nodeOps } = backend
/**
* hooks = ['create', 'activate', 'update', 'remove', 'destroy']
* 遍历这些钩子,然后从 modules 的各个模块中找到相应的方法,比如:directives 中的 create、update、destroy 方法
* 让这些方法放到 cb[hook] = [hook 方法] 中,比如: cb.create = [fn1, fn2, ...]
* 然后在合适的时间调用相应的钩子方法完成对应的操作
*/
for (i = 0; i < hooks.length; ++i) {
// 比如 cbs.create = []
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
// 遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组中
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
/**
* vm.__patch__
* 1、新节点不存在,老节点存在,调用 destroy,销毁老节点
* 2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
* 3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
*/
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 如果新节点不存在,老节点存在,则调用 destroy,销毁老节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 新的 VNode 存在,老的 VNode 不存在,这种情况会在一个组件初次渲染的时候出现,比如:
// <div id="app"><comp></comp></div>
// 这里的 comp 组件初次渲染时就会走这儿
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 判断 oldVnode 是否为真实元素
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 不是真实元素,但是老节点和新节点是同一个节点,则是更新阶段,执行 patch 更新节点
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.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// 走到这儿说明不是服务端渲染,或者 hydration 失败,则根据 oldVnode 创建一个 vnode 节点
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// 拿到老节点的真实元素
const oldElm = oldVnode.elm
// 获取老节点的父元素,即 body
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
}
}
// 移除老节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
1 销毁节点 - invokeDestroyHook
/**
* 销毁节点:
* 执行组件的 destroy 钩子,即执行 $destroy 方法
* 执行组件各个模块(style、class、directive 等)的 destroy 方法
* 如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
*/
function invokeDestroyHook(vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef((i = vnode.children))) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
2 判断两个节点是否相同 - sameVnode
function sameVnode(a, b) {
return (
// key 必须相同,需要注意的是 undefined === undefined => true
a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((
// 标签相同
a.tag === b.tag &&
// 都是注释节点
a.isComment === b.isComment &&
// 都有 data 属性
isDef(a.data) === isDef(b.data) &&
// input 标签的情况
sameInputType(a, b)) ||
// 异步占位符节点
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
)
}
3 创建空 VNode - emptyNodeAt
/**
* 为元素(elm)创建一个空的 vnode
*/
function emptyNodeAt(elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
通过上面的源码我们可以了解到,createPatchFunction中,在新旧VNode经过diff算法,得到最新的VNode,将其渲染到真实DOM上
4 基于 vnode 创建整棵 DOM 树,并插入到父节点上 - createElm
/**
* 基于 vnode 创建整棵 DOM 树,并插入到父节点上
*/
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
/**
* 重点
* 1、如果 vnode 是一个组件,则执行 init 钩子,创建组件实例并挂载,
* 然后为组件执行各个模块的 create 钩子
* 如果组件被 keep-alive 包裹,则激活组件
* 2、如果是一个普通元素,则什么也不错
*/
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 获取 data 对象
const data = vnode.data
// 所有的孩子节点
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 未知标签
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// 创建新节点
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)
}
}
5 创建组件实例 - createComponent
/**
* 如果 vnode 是一个组件,则执行 init 钩子,创建组件实例,并挂载
* 然后为组件执行各个模块的 create 方法
* @param {*} vnode 组件新的 vnode
* @param {*} insertedVnodeQueue 数组
* @param {*} parentElm oldVnode 的父节点
* @param {*} refElm oldVnode 的下一个兄弟节点
* @returns 如果 vnode 是一个组件并且组件创建成功,则返回 true,否则返回 undefined
*/
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
// 获取 vnode.data 对象
let i = vnode.data
if (isDef(i)) {
// 验证组件实例是否已经存在 && 被 keep-alive 包裹
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 执行 vnode.data.init 钩子函数,该函数在讲 render helper 时讲过
// 如果是被 keep-alive 包裹的组件:则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
// 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
// 如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例,并挂载
// 这时候就可以给组件执行各个模块的的 create 钩子了
initComponent(vnode, insertedVnodeQueue)
// 将组件的 DOM 节点插入到父节点内
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
// 组件被 keep-alive 包裹的情况,激活组件
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
6 向父节点插入节点 - insert
/**
* 向父节点插入节点
*/
function insert(parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
7 移除节点 - removeVnodes
/**
* 移除指定索引范围(startIdx —— endIdx)内的节点
*/
function removeVnodes(vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}
8 更新节点 - patchVnode
/**
* 更新节点
* 全量的属性更新
* 如果新老节点都有孩子,则递归执行 diff
* 如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
* 如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
* 更新文本节点
*/
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 老节点和新节点相同,直接返回
if (oldVnode === vnode) {
return
}
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 一样,并且新节点被 clone 了 或者 新节点有 v-once指令,则重用这部分节点
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
// 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
if (isDef(data) && isPatchable(vnode)) {
// 执行新节点所有的属性更新
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)) {
// 如果新老节点都有孩子,则递归执行 diff 过程
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 老孩子不存在,新孩子存在,则创建这些新孩子节点
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 老孩子存在,新孩子不存在,则移除这些老孩子节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 老节点是文本节点,则将文本内容置空
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新节点是文本节点,则更新文本节点
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
9 更新子节点 - updateChildren
/**
* diff 过程:
* diff 优化:做了四种假设,假设新老节点开头结尾有相同节点的情况,一旦命中假设,就避免了一次循环,以提高执行效率
* 如果不幸没有命中假设,则执行遍历,从老节点中找到新开始节点
* 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
* 如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
* 如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
*/
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 老节点的开始索引
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
// removeOnly是一个特殊的标志,仅由 <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]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 老开始节点和新开始节点是同一个节点,执行 patch
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// patch 结束后老开始和新开始的索引分别加 1
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 老结束和新结束是同一个节点,执行 patch
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// patch 结束后老结束和新结束的索引分别减 1
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 老开始和新结束是同一个节点,执行 patch
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 处理被 transtion-group 包裹的组件时使用
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// patch 结束后老开始索引加 1,新结束索引减 1
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 老结束和新开始是同一个节点,执行 patch
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// patch 结束后,老结束的索引减 1,新开始的索引加 1
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果上面的四种假设都不成立,则通过遍历找到新开始节点在老节点中的位置索引
// 找到老节点中每个节点 key 和 索引之间的关系映射 => oldKeyToIdx = { key1: idx1, ... }
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 在映射中找到新开始节点在老节点中的位置索引
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]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 如果这两个节点是同一个,则执行 patch
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// patch 结束后将该老节点置为 undefined
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 最后这种情况是,找到节点了,但是发现两个节点不是同一个节点,则视为新元素,执行创建
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)
}
}
10 检查key是否重复 - checkDuplicateKeys
/**
* 检查一组元素的 key 是否重复
*/
function checkDuplicateKeys(children) {
const seenKeys = {}
for (let i = 0; i < children.length; i++) {
const vnode = children[i]
const key = vnode.key
if (isDef(key)) {
if (seenKeys[key]) {
warn(
`Duplicate keys detected: '${key}'. This may cause an update error.`,
vnode.context
)
} else {
seenKeys[key] = true
}
}
}
}
11 新增节点 - addVnodes
/**
* 在指定索引范围(startIdx —— endIdx)内添加节点
*/
function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
}
}
12 创建指定节点的关系映射 - createKeyToOldIdx
/**
* 得到指定范围(beginIdx —— endIdx)内节点的 key 和 索引之间的关系映射 => { key1: idx1, ... }
*/
function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
13 找节点位置索引 - findIdxInOld
/**
* 找到新节点(vnode)在老节点(oldCh)中的位置索引
*/
function findIdxInOld(node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
14 调用 各个模块的 create 方法 - invokeCreateHooks
/**
* 调用 各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等 ,然后执行组件的 mounted 生命周期方法
*/
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
// 组件钩子
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
// 组件好像没有 create 钩子
if (isDef(i.create)) i.create(emptyNode, vnode)
// 调用组件的 insert 钩子,执行组件的 mounted 生命周期方法
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
15 创建子节点们 - createChildren
/**
* 创建所有子节点,并将子节点插入父节点,形成一棵 DOM 树
*/
function createChildren(vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
// children 是数组,表示是一组节点
if (process.env.NODE_ENV !== 'production') {
// 检测这组节点的 key 是否重复
checkDuplicateKeys(children)
}
// 遍历这组节点,依次创建这些节点然后插入父节点,形成一棵 DOM 树
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
// 说明是文本节点,创建文本节点,并插入父节点
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
二、总结
综上所述,new Vue() 的整体过程大致可以划分为四部分,分别是初始化、挂载、生成虚拟节点,生成真实节点,最后调用 mounted 钩子
- 初始化:合并配置项,初始化生命周期、事件、
data、computed、watch等等,完成了beforeCreate到created的整个过程 - 挂载:如果
options.render不存在,将template编译成render函数。接下来就是建立更新机制,创建watcher,通过watcher中的get方法,调用组件更新updateComponent方法 - 生成虚拟DOM:调用更新函数,会调用
vm._render方法,这个方法会调用vm.$options.render方法用于生成虚拟DOM。简单讲,就是执行render函数,生成VNode - 生成真实DOM:通过
vm._render方法得到虚拟DOM后,会作为vm._update()方法的参数去生成Vnode相应的真实DOM。简单讲,就是新旧VNode经过diff算法后,渲染到真实DOM上- Vue 的 patch 算法的作用:负责首次渲染和后续更新或者销毁组件
-
如果老的 VNode 是真实元素,则表示首次渲染,创建整棵 DOM 树,并插入 body,然后移除老的模板节点
-
如果老的 VNode 不是真实元素,并且新的 VNode 也存在,则表示更新阶段,执行 patchVnode
1)首先是全量更新所有的属性
2)如果新老 VNode 都有孩子,则递归执行 updateChildren,进行 diff 过程
针对前端操作 DOM 节点的特点进行如下优化:
- 同层比较(降低时间复杂度)深度优先(递归)
- 而且前端很少有完全打乱节点顺序的情况,所以做了四种假设,假设新老 VNode 的开头结尾存在相同节点,一旦命中假设,就避免了一次循环,降低了 diff 的时间复杂度,提高执行效率。如果不幸没有命中假设,则执行遍历,从老的 VNode 中找到新的 VNode 的开始节点
- 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
- 如果老的 VNode 先于新的 VNode 遍历结束,则剩余的新的 VNode 执行新增节点操作
- 如果新的 VNode 先于老的 VNode 遍历结束,则剩余的老的 VNode 执行删除操纵,移除这些老节点
3)如果新的 VNode 有孩子,老的 VNode 没孩子,则新增这些新孩子节点
4)如果老的 VNode 有孩子,新的 VNode 没孩子,则删除这些老孩子节点
5)剩下一种就是更新文本节点
-
如果新的 VNode 不存在,老的 VNode 存在,则调用 destroy,销毁老节点
-
- Vue 的 patch 算法的作用:负责首次渲染和后续更新或者销毁组件
- 大致流程图:
- 详细流程图: