前言
本来今天是想继续昨天的内容,深入源码探究插槽作用域的实现,但是由于篇幅过长还未能及时整理完善,所以先来补充一下前置知识:关于组件挂载时经历的生命周期。
大家都知道生命周期是 Vue 中的重要概念,每个 Vue 组件都有自己的生命周期,通过深入了解 Vue 的生命周期,能够更好地掌握 Vue 框架的工作原理和运行机制,进行高效地开发。
这里预警一下? 这又是一篇长文哦
希望你们能有足够的耐心看完,当然也可以选择感兴趣的部分进行阅读?
vue 的生命周期
组件生命周期经历的几个阶段:
- 数据初始化(beforeCreate、created) 为Vue实例上初始化一些属性,事件以及响应式数据
- 模板编译 created ==>> beforeMount 之间进行, 将模板编译成渲染函数
- 挂载阶段(beforeMount、mounted) 将模板渲染到真实的DOM中,挂载指定的DOM上
- 卸载阶段(beforeDestory、destoryed)将实例自身从父组件中删除,并取消依赖追踪及事件监听器
数据更新时 beforeUpdate updated
activated deactivated
errorCaptured 在捕获一个来自后代组件的错误时被调用。
vue实例化
let vm = new Vue({})
执行了new Vue()之后,实际上是通过调用 _init
方法触发一系列的初始化流程。而_init
方法是通过initMixin方法挂载到Vue原型中的。
src\core\instance\index.js
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')
}
// 调用 Vue.prototype._init 方法,该方法是在 initMixin 中定义的
this._init(options)
}
// 定义 Vue.prototype._init 方法
initMixin(Vue)
// 定义了 $data、 $props、 $set、 $delete、$watch
stateMixin(Vue)
// 处理事件相关 $emit、$on、$off、$once
eventsMixin(Vue)
// _update $forceUpdate $destory
lifecycleMixin(Vue)
// $nextTick _render installRenderHelpers
renderMixin(Vue)
附:生命周期图解
数据初始化 _init()
beforeCreated: 此时 data和
el 均未初始值 undefined。 beforeCreated 是在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。此时的 options 中的 data 和 this 还未进行绑定。
beforeCreated ===>> created期间: 实现 注入和响应式的初始化 initInject initState initProvide
created: 实例创建并完成 数据观测(data observer)、data、 methods、 watch、event事件回调 ,options中的属性都绑定到了this实例对象上,并对属性都设置了响应式处理, created 是最早能获取数据的 hook。
initMixin:
Vue.prototype._init:
src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid 每个实例都有一个 uid 每实例化一个 uid + 1
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// 子组件的初始化
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
// 根组件走这里 选项合并 将全局配置选项合并到根组件的局部配置上
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
/**
* 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,
* 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
*/
initEvents(vm)
// 渲染初始化 初始化插槽 获取 this.$slots 定义 this._c 即 createElement 方法 平时使用 h 函数
initRender(vm)
// 执行 beforeCreate 生命周期钩子函数
callHook(vm, 'beforeCreate')
// 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,对结果数据进行响应式处理,并代理每个 key 到 vm 实例
initInjections(vm) // resolve injections before data/props
// 数据响应式的重点,处理 props、methods、data、computed、watch
initState(vm)
// 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
initProvide(vm) // resolve provide after data/props
// 执行 created 生命周期钩子函数
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果发现配置项上有 el 选项,则自动调用 $mount 方法
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
由此可以初步得出数据初始化的顺序: beforeCreate ==> inject ==>> provide ==>> created
provide / inject 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
mergeOption: 整合、初始化部分属性
在接收到options之后,会先交给mergeOptions整合一遍部分属性,它负责按照预设定逻辑合并上级options(如果有)和规范prop,inject, Directives的命名。
并且它还会接受上级mixins(上级组件和全局)。合并两个选项,出现相同配置项时,子选项会覆盖父选项的配置。
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 选项的标准化处理 props、Inject、Directives 命名驼峰化
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 递归合并选项 合并过的选项会使用 _base 作为标记
if (!child._base) {
// extends 基于 组件扩展另一个组件
if (child.extends) {
// 通过地递归处理选项 如果是子组件则合并父组件项
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
// 把混入的复用项合并到实例中
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
// 遍历父级所有的 option,如果和子组件的option有冲突则使用子组件的option
for (key in parent) {
mergeField(key)
}
// 遍历 子选项,如果父选项不存在该配置,则合并;否则跳过,因为父子拥有同一个属性的情况在上面处理父选项时已经处理过了,用的子选项的值
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 合并选项,childVal 优先级高于 parentVal
function mergeField (key) {
// strats = Object.create(null)
const strat = strats[key] || defaultStrat
// 值为如果 childVal 存在则优先使用 childVal,否则使用 parentVal
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
// 从组件构造函数中解析配置对象 options,并合并基类选项
export function resolveConstructorOptions (Ctor: Class<Component>) {
// 从实例构造函数上获取选项
let options = Ctor.options
if (Ctor.super) {
// 存在基类,递归解析基类构造函数的选项
const superOptions = resolveConstructorOptions(Ctor.super)
// 缓存
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// 基类的配置项 与缓存中的不一致 说明 基类发生了更改
Ctor.superOptions = superOptions
// 找到更改的选项 去更新
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
// 如果存在被修改或增加的选项,则合并两个选项
extend(Ctor.extendOptions, modifiedOptions)
}
// 将新增的选项赋值给 当前选项的 options
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
// 解析构造函数选项中后续被修改或者增加的选项
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
let modified
const latest = Ctor.options
const sealed = Ctor.sealedOptions
// 对比两个选项,记录不一致的选项
for (const key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = latest[key]
}
}
return modified
}
initLifecycle: 挂载默认属性
// 创建生命周期
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到父节点然后将当前元素和父节点进行关联
let parent = options.parent
//找到第一个非抽象父节点
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
// 为子组件时自行添加到本组件的 $chidren 数组中
parent.$children.push(vm)
}
// $开头属性都是提供用户使用的,内部属性都是以下划线_;
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
在 initLifecycle 中 初始化实例属性,对当前的 vue 的 parent 元素进行绑定, 并设置 $root 根属性, 初始化 _isMounted
、__isDestroyed
生命周期状态属性
initEvents: 初始化 v-on 注册事件
将父组件上注册的事件添加到子组件实例的 _events 对象中
export function initEvents (vm: Component) {
vm._events = Object.create(null) // 用于存储事件
vm._hasHookEvent = false // 是否存在 hook 事件
// 初始化父组件的监听事件
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners) // 将父组件向子组件注册的事件注册到子组件实例中的_events对象里
}
}
let target: any
function add (event, fn) {
target.$on(event, fn) // 监听绑定事件
}
function remove (event, fn) {
target.$off(event, fn) // 解绑事件
}
function createOnceHandler (event, fn) {
const _target = target
// 创建只执行一次的 once 事件
return function onceHandler () {
const res = fn.apply(null, arguments)
if (res !== null) {
_target.$off(event, onceHandler)
}
}
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm // 更新监听器
target = undefined
}
updateListeners
src\core\vdom\helpers\update-listeners.js
export function updateListeners (
on: Object, // 父组件的事件监听对象
oldOn: Object, // 如果不是初次渲染, 原先的 vm 实例上可能存在一些原有的事件
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) { // 遍历父组件的事件对象
def = cur = on[name] // 父组件中的事件
old = oldOn[name] // 当前组件中已存在的同名事件
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(cur)) { // 父组件事件不存在, 直接报警告
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) { // 父组件事件存在, 并且当前组件不存在该事件的同名事件
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) { // 如果是 once 修饰符修饰过的事件
// 为当前组件绑定 once 类型事件
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
// 将父组件的事件存入当前组件事件对象
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
// 父组件的事件 与 当前组件的事件同名,但处理方式不同
// 父组件存在该事件,子组件存在同名事件, 并且父子组件对于同一个事件的处理函数不相同
// 则采用从父组件传递过来的处理函数
old.fns = cur
on[name] = old
}
}
// 删除 vm 上之前存在、现在不存在的事件
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
初始化事件 实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。
initRender: 初始化渲染
- 将组件的插槽编译成虚拟节点 DOM 树, 以列表的形式挂载到 vm 实例,初始化作用域插槽为空对象;
- 将模板的编译函数(把模板编译成虚拟 DOM 树)挂载到 vm 的 _c 和 $createElement 属性;
- 最后把父组件传递过来的 listeners 定义成响应式的。
export function initRender (vm: Component) {
vm._vnode = null
vm._staticTrees = null
// 合并后的 option 赋值
// 找到父组件节点 parentVnode 和渲染内容 renderContext
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode
const renderContext = parentVnode && parentVnode.context
// 初始化插槽 resolveSlots 找出当前组件下父组件传入的slot,并且返回;与子组件的插槽对应
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 将createElement fn绑定到这个实例,这样就能在里面获得合适的渲染环境。
// createElement 用来生成虚拟DOM
// 挂载到 vm 的 _c (从模板 <template> 编译得到的组件) 和 $createElement (用户使用 render 渲染写法) 两个属性上
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
const parentData = parentVnode && parentVnode.data
// 把父组件的 $attrs 和 $listeners 用 defineReactive()设置为响应式
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
attrs,可直接获取根组件所传递参数(不包含
class
, style
和事件属性),而不用每一级组件逐层传递。
listeners”`传入内部组件。
$listeners
是组件的内置属性,它的值是父组件(不含.native
修饰器的)v-on
事件监听器
- 组件可以通 过在自己的子组件上使用
v-on=”$listeners”
,进一步把值传给自己的子组件
callHook触发钩子函数
遍历数组,执行钩子函数
export function callHook (vm: Component, hook: string) {
// 在执行生命周期钩子函数期间禁止依赖收集
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
// handlers 数组
// 从组件的配置项中获取 mounted 生命周期钩子函数
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
// 执行生命周期函数
for (let i = 0, j = handlers.length; i < j; i++) {
// 通过 invokeWithErrorHandling 方法执行生命周期函数
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
// Hook Event,如果设置了 Hook Event,比如 <comp @hook:mounted="method" />,则通过 $emit 触发该事件
// vm._hasHookEvent 标识组件是否有 hook event,这是在 vm.$on 中处理组件自定义事件时设置的
// 如果存在 HookEvent 执行插入的对应的函数
if (vm._hasHookEvent) {
// 通过 $emit 触发 hook: mounted 事件
// 执行 vm._events['hook:mounted'] 数组中的所有处理函数
vm.$emit('hook:' + hook)
}
// 关闭依赖收集
popTarget()
}
/**
* 通用函数,执行指定函数 handler
* 传递进来的函数会被用 try catch 包裹,进行异常捕获处理
*/
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
// 执行传递进来的函数 handler,并将执行结果返回
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
vm.$on
/**
* 监听实例上的自定义事件,vm._event = { eventName: [fn1, ...], ... }
* @param {*} event 单个的事件名称或者有多个事件名组成的数组
* @param {*} fn 当 event 被触发时执行的回调函数
* @returns
*/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
// event 是有多个事件名组成的数组,则遍历这些事件,依次递归调用 $on
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 将注册的事件和回调以键值对的形式存储到 vm._event 对象中 vm._event = { eventName: [fn1, ...] }
(vm._events[event] || (vm._events[event] = [])).push(fn)
// hookEvent,提供从外部为组件实例注入声明周期方法的机会
// 比如从组件外部为组件的 mounted 方法注入额外的逻辑
// 该能力是结合 callhook 方法实现的
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。
在beforeCreate 之前完成了 属性、 事件、渲染 的初始化准备。
此后
beforeCreate ===>> created 之间 依次执了initInjections、initState、initProvide,完成了依赖注入和响应式的初始化。
initInject:依赖注入
初始化 inject 配置项 : 向上级查找对应的provided
1、从祖代组件的配置中找到 对应的 provide ,配置项中每一个 key 对应的 val, 得到 result[key] = val
2、对结果数据进行响应式处理,代理每个 key 到 vm 实例
export function initInjections (vm: Component) {
// 解析 inject 配置项,然后从祖代组件的配置中找到每个 provide 对应的值,配置项中每一个 key 对应的 val 最后得到 result[key] = val
const result = resolveInject(vm.$options.inject, vm)
if (result) {
// 设置 shouldObserve 为 false, 不设置 Observe 观察
toggleObserving(false)
// 对结果数据进行响应式处理, 当 shouldObserve 为 false 时,defineReactive 只是代理每个 key 到 vm 实例
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
// 解析 inject 配置项,从祖代组件的 provide 配置中找到 key 对应的值,否则用 默认值,最后得到 result[key] = val
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key === '__ob__') continue
// 获取 key 的 from 属性作为 provideKey
const provideKey = inject[key].from
let source = vm
// 遍历所有的祖代组件,直到 根组件,找到组件中配置的 _provide 选项,从而找到对应 key 的值,
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
// 如果上一个循环未找到,则采用 inject[key].default,
// 如果没有设置 default 值,则抛出错误
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
inject 祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
inject 使用方式
祖代像所有子代组件透传数据
provide可以是一个对象,也可以是一个返回对象的函数。
inejct:可以是字符串数组或者一个对象。
// 1.父组件提供向子组件要传递的参数
provide() {
return {
listType: this.listType,
}
}
// 2.子组件使用:
inject: ['listType'],
// 或者在 inject 中指定你的默认值和你参数的来源:
inject:{
listType:{
from:"par"//provide定义的名字
default:1
}
}
inject 的响应式
provide
和 inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
provide 里的值如果引用了 data 的数据,传的是基本类型, data 被改变了,inject 里的值是不会被改变的,因为在初始化的时候,没有监听数据。
如果需要父组件的数据改变了子组件的 inject 数据一起更新,需要传入引用类型,这样在 Object.defineProperty 的 get 时,当前 Dep.target 为 render 或其他的wacther,能够收集到引用的对象的值。
initProvide:初始化 provide
解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
inject
选项接收到注入的值有可能被以上这些数据所使用到,所以在初始化完inject
后需要先初始化这些数据,然后才能再初始化provide
,所以在调用initInjections
函数对inject
初始化完之后需要先调用initState
函数对数据进行初始化,最后再调用initProvide
函数对provide
进行初始化。
initState: 响应式
数据响应式的入口:分别处理 props、methods、data、computed、watch
优先级:props、methods、data、computed 对象中的属性不能出现重复,优先级和列出顺序一致
其中 computed 中的 key 不能和 props、data 中的 key 重复,methods 不影响
//响应式原理入口
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 处理 props 对象,为 props 对象的每个属性设置响应式,并将其代理到 vm 实例上
if (opts.props) initProps(vm, opts.props)
// 处理 methos 对象,校验每个属性的值是否为函数、和 props 属性比对进行判重处理,最后得到 vm[key] = methods[key]
if (opts.methods) initMethods(vm, opts.methods)
/**
* 1、判重处理,data 对象上的属性不能和 props、methods 对象上的属性相同
* 2、代理 data 对象上的属性到 vm 实例
* 3、为 data 对象的上数据设置响应式
*/
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
/**
* computed 缓存实现原理 本质是通过 watcher 实现
* 1、对每个 computed[Key] 创建 watcher 实例 默认懒执行
* 2、将 computed[Key] 代理到 vue 实例上 支持通过 this.computedKey 的方式访问 computed.key
* 3、判重 computed 中的 key 不能和 data、props 中的属性重复
*/
if (opts.computed) initComputed(vm, opts.computed)
/**
* 1、处理 watch 对象
* 2、为 每个 watch.key 创建 watcher 实例,key 和 watcher 实例可能是 一对多 的关系
* 3、如果设置了 immediate,则立即执行 回调函数
*/
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
proxy
将属性代理到实力上的方式
// 设置代理,将 key 代理到 target 上
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)
}
initProps
处理 props 对象,为 props 对象的每个属性设置响应式,并将其代理到 vm 实例上
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array instead of dynamic object key enumeration.
// 缓存 props 的每个 key 用数组进行迭代,性能优化
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
// 遍历 props 对象
for (const key in propsOptions) {
// 缓存 key
keys.push(key)
// 获取 props[key] 的默认值
const value = validateProp(key, propsOptions, propsData, vm)
// 为 props 的每个 key 设置数据响应式 将 key 代理到 props 上
defineReactive(props, key, value)
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
// 代理 key 到 vm 对象上
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
initMethods
- 校验 methoss[key],必须是一个函数
-
判重
-
methods 中的 key 不能和 props 中的 key 相同
-
methos 中的 key 与 Vue 实例上已有的方法重叠,一般是一些内置方法,比如以 $ 和 _ 开头的方法
-
-
将 methods[key] 放到 vm 实例上,得到 vm[key] = methods[key]
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
// 遍历多有的 methods 校验 methoss[key],必须是一个函数
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
// methods 中的 key 不能和 props 中的 key 相同
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
// methos 中的 key 与 Vue 实例上已有的方法重叠
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
// 将 methods[key] 放到 vm 实例上,得到 vm[key] = methods[key]
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
initData
- 判重处理,data 对象上的属性不能和 props、methods 对象上的属性相同
- 代理 data 对象上的属性到 vm 实例
- 为 data 对象的上数据设置响应式
function initData (vm: Component) {
// 得到 data 对象
let data = vm.$options.data
// 保证后续处理的 data 是一个对象
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
// 判重处理,data 中的属性不能和 props、methods 中的属性重复
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// 不能与 methods 同名
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 不能与 props 同名
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 将data 对象上的属性到 vm 实例
proxy(vm, `_data`, key)
}
}
// observe中 通过defineReactive()设置响应式, 实现了数据的响应式处理
observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
initComputed
- 为 computed[key] 创建 watcher 实例
- 代理 computed[key] 到 vm 实例
- 判重,computed 中的 key 不能和 data、props 中的属性重复
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 遍历 computed 对象
for (const key in computed) {
// 获取 key 对应的值,即 getter 函数
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
// 为 computed 属性创建 watcher 实例
watchers[key] = new Watcher( //watcher的取值函数就是我们在computed中定义的函数
vm,
getter || noop,
noop,
// computed 配置项 懒执行
computedWatcherOptions // { lazy: true }
)
}
if (!(key in vm)) {
// 代理 computed 对象中的属性到 vm 实例
// 这样就可以使用 vm.computedKey 访问计算属性了
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// 非生产环境有一个判重处理,computed 对象中的属性不能和 data、props 中的属性相同
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
/**
* 代理 computed 对象中的 key 到 target(vm)上
*/
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 将 computed 配置项中的 key 代理到 vue 实例上,
// 支持通过 this.computedKey 的方式访问 computed 中的属性
// 拦截对 target.key 的访问和设置
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
// computed 属性值会缓存的原理也是在这里结合 watcher.dirty、watcher.evalaute、watcher.update 实现的
return function computedGetter () { //当视图对newMsg进行取值的时候会执行这里
// 得到当前 key 对应的 watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 执行 watcher.evaluate()
// 执行 computed.key 的值(函数) 得到函数的执行结果 赋值给 watcher.value
// 将 watcher.dirty 值为 false
// 待页面更新后 执行wathcer.update() 将 watcher.dirty 重新置为 true,
// computed 和 watcher 的区别
// computed 实现原理: 一次渲染中只执行一次 computed 函数 后续访问是不在执行,
// 直到下一次更新之后再次执行 computed 函数
// watcher.dirty 值为 false 时 不执行 evaluate 直接从 watcher.value 中拿值
if (watcher.dirty) { //这里要对照Watcher的构造函数来看,默认watcher.dirty = watcher.lazy,首次执行为true
watcher.evaluate() //会执行watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
initWatch
function initWatch (vm: Component, watch: Object) {
// 遍历 watch 配置项
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
// handler 为数组,遍历数组,获取其中的每一项,然后调用 createWatcher
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
/**
* 两件事:
* 1、兼容性处理,保证 handler 肯定是一个函数
* 2、调用 $watch
* @returns
*/
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
stateMixin
export function stateMixin (Vue: Class<Component>) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
// 处理 data 数据 定义 get 方法 访问 this._data
const dataDef = {}
dataDef.get = function () { return this._data }
// 处理 prop 数据
const propsDef = {}
propsDef.get = function () { return this._props }
// 异常提示
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
// 设置时 会提示 props 只读
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
// 将 $data 和 $props 挂载到 Vue原型链上 支持通过 this.$data 和 this.$props 的方法访问
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
// $set 和 $delete
// 是 全局API set 和 del 的别名
Vue.prototype.$set = set
Vue.prototype.$delete = del
// this.$watch
/**
* 创建 watcher,返回 unwatch,共完成如下 5 件事:
* 1、兼容性处理,保证最后 new Watcher 时的 cb 为函数
* 2、标记用户 watcher
* 3、创建 watcher 实例
* 4、如果设置了 immediate,则立即执行一次 cb
* 5、返回 unwatch 事件 用于解除 watch 监听
* @param {*} expOrFn key 或者是 function
* @param {*} cb 回调函数
* @param {*} options 配置项,用户直接调用 this.$watch 时可能会传递一个 配置项
* @returns 返回 unwatch 函数,用于取消 watch 监听
*/
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 兼容性处理,因为用户调用 vm.$watch 时设置的 cb 可能是对象
// 处理 cb 是对象的情况 保证后续处理的 cb 是一个函数
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 标记 这是一个 用户 watcher
options.user = true
// 实例化 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// 存在 immediate 为 true 则立即执行回调函数
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
// 返回 unwatcnFn 用于解除 watcher 监听
return function unwatchFn () {
watcher.teardown()
}
}
}
由 initState 可得知 数据初始化的顺序为
beforeCreate ==> inject ==>> props ==>> methods ==>> data ==>> computed ==>> watch ==>> provide ==>> created
在 initData 中实现了数据的响应式
小结
在created的之前initInjections,initProvide,initState(这里面初始化了一些常用的属性,
其中initData里面体现了init reactivity ,所以beforeCreate之前相当于在为后面的生命周期进行做准备,
所以在created之后就会完成一些常用属性的初始化,以及data数据的响应式绑定。也就是说在created之后就可以在vue中用this.data调用data里面的数据了。
- 完成数据观测,属性与方法的运算,watch、event事件回调的配置
- 可调用methods中的方法,访问和修改data数据触发响应式渲染dom,通过computed和watch完成数据计算
- 但此时vm.$el 并没有被创建
模板编译
created ==>> beforeMount期间: 实现了模板编译,生成模板字符串且并未挂载
- 判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译
- vm.el获取到的是挂载DOM的HTML
- 优先级:render > template > outerHTML
initMixin created之后则是进入了模板编译阶段
// 如果发现配置项上有 el 选项,则自动调用 $mount 方法,
// 反之,没有 el 则必须手动调用 $mount
// el 选项为 vue实例挂载的目标
if (vm.$options.el) {
// 调用 $mount 方法,进入编译阶段
vm.$mount(vm.$options.el)
}
runtime-compile $mount函数
src\platforms\web\entry-runtime-with-compiler.js
// 初始化最后一步的$mount 做备份
const mount = Vue.prototype.$mount
// 覆写 $mount
Vue.prototype.$mount = function (
// el挂载点
el?: string | Element,
hydrating?: boolean
): Component {
// 得到挂载点
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
// 不能挂载到html或者body元素上
`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
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
// id选择器
if (template.charAt(0) === '#') {
// 通过idToTemplate 获得el的innerHTML
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
//获取outerHTML
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// compileToFunctions 的实现 返回 render()
// 将模板编译成 ast 语法树
// 将 AST 进行一个优化,其实主要优化是打上了静态节点的标记
// 将编译好的 AST 对象,转化为字符串。,这个字符串将在渲染函数中用来生成 Vnode
const { render, staticRenderFns } = compileToFunctions(template, {
// 标记元素在HTML 模板字符串中的开始和结束索引位置
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
// 定界符 {{}}
delimiters: options.delimiters,
// 编译时是否保留注释内容
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 存在render直接执行备份的$mount方法执行挂载
return mount.call(this, el, hydrating)
}
runtime compile 它缓存了原型的$mount方法,再重写了这个方法,在里面加入了编译生成render的部分。runtime-compile版本,会判断是否有tempalte,如果有就用compileToFunctions函数将其编译成render函数,如果没有template就将el挂载的外部html编译成render函数。
compile版本的$mount函数中对 tempalt 进行判断,再通过 compileToFunctions对tempalte 进行处理,将其编译成render函数。
runtime-compiler:template ==>>ast(abstract syntax tree:抽象语法数) ==>> render函数 ==>> vdom(virtual 虚拟dom) ==>> 真实DOM
原型的 $mount (runtime only)
src\platforms\web\runtime\index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent: 组件的挂载
src\core\instance\lifecycle.js
// $mount 调用
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* 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')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && 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 = () => {
// 执行_update 进入更新阶段 首先执行 _render 将组件变成 VNode
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
runtime-compile main.js 配置
import Vue from "vue/dist/vue.esm.js"; //修改引入的源文件
console.log(`Vue`, Vue);
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.config.productionTip = false;
Vue.use(ElementUI);
new Vue({
router,
store,
components: { App }, //组件的注册
template: "<App/>", //组件的使用
}).$mount("#app");
runtime-only main.js 配置
import Vue from "vue/dist/vue.esm.js"; //修改引入的源文件
console.log(`Vue`, Vue);
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.config.productionTip = false;
Vue.use(ElementUI);
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
runtime compiler和runtime only的区别
- main.js 的不同 :在complier的main.js文件中,Vue实例内部需要先注册组件,然后设置模板;而在only的main.js文件中,直接用render函数代替了这两个过程。
- runtime-compiler:template ==>>ast(abstract syntax tree:抽象语法数) ==>> render函数 ==>> vdom(virtual 虚拟dom) ==>> 真实DOM
runtime-only:render函数 ==> vdom(virtual 虚拟dom) ==> 真实DOM, 不能解析template 模板,只能通过 vue-template-compiler 将 template 转化为 render函数。(npm安装vue-template-compiler,)
因此
runtime-only 性能更高,因为不需要 ,template ==>> ast(abstract syntax tree:抽象语法数)这两步
挂载阶段:
beforeMount:在模板编译阶段 template通过compileToFunctions() 生成了 render() , 但是还未生成虚拟DOM
beforeMount ==>> mounted 期间:做好渲染准备了之后,执行 vm._render
生成虚拟DOM,然后再通过vm._update
渲染成真实的DOM
mounted: 虚拟Dom已经被挂载到真实Dom上,此时我们可以获取Dom节点,$ref
在此时也是可以访问的。
beforeMount:
src\core\instance\lifecycle.js mountComponent() 中
// $mount 调用
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 挂载点
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* 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
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && 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 = () => {
// 执行_update 进入更新阶段
// _update 中主要功能是调用patch,
// 1. 首先执行 _render 将组件变成 VNode ,
// 2. 再将vnode转换为真实DOM,并且更新到页面中
vm._update(vm._render(), hydrating)
}
}
// 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 时会执行 this.get()
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
// vm.$vnode 为空,则为根实例,即渲染完毕,开始进入 mounted 阶段
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mounted:
src\core\instance\lifecycle.js mountComponent 后半段
// beforeMount
callHook(vm, 'beforeMount')
...
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && 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 = () => {
// 执行_update 进入更新阶段
// 首先执行 _render 生成 VNode,最终调用 vm._update 更新 成真实DOM
vm._update(vm._render(), hydrating)
}
}
// 实例化 watcher 时主动触发 watcher.get()
// 通过 updateComponent 回调(_render)实现页面的更新
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
// vm.$vnode 为空,则为根实例,即渲染完毕,开始进入 mounted 阶段
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
实例化 一个 watcher 在初始化时调用 this.get() 方法 触发 updateComponent() 回调 ,然后观察到vm中数据发生变化的时候再执行updateComponent()更新DOM。
updateComponent 中通过 vm._render
方法先生成虚拟DOM,最终调用 vm._update
更新 成真实DOM
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._render()
render的作用主要是生成vnode
// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
render() 是在created ===>> beforeMoun 之间
由template通过 compileToFunctions() 生成的,而 render 调用的参数 vm.createElement 是在 beforeCreate 初始化准备工作 initRender() 把 createElement 挂载在vm上的 _c
和 createElement 属性上的,而 vm._c 是在模板编辑成的 render 中使用,vm.createElement是在用户手写的 render 中使用,所以真正创建虚拟DOM的其实是 createElement 函数创建了一个VNode Tree
vm._update 触发的时机
- DOM的初次渲染
- 数据发生变化的时候更新DOM
vm._update 把 虚拟DOM转换成真实的DOM
小结
mounted之后就代表着渲染的完成,已经真实的DOM已经挂载到html上面,此时可以进行DOM的相关操作,在beforeMount和mounted函数之间,vue调用了render,将模板字符串转化成虚拟DOM
更新阶段:(响应式)
数据的更新随即进入了响应式原理的实现
beforeUpdate: 在数据发生改变后,DOM 被更新之前被调用。此时数据已更新,页面未更新 date 与 view暂未同步
beforeUpdate ==>> updated期间: 虚拟DOM 重新渲染并更新应用(date ==>> view) 的更新
updated: 数据与页面保持同步
Watcher:通过 watcher 实现数据的更新从而触发页面的更新
渲染watcher 在 vm.$mount() 执行时创建 ,传入 updateComponent 回调,调用 this.getter() 时触发 updateComponent 进行页面的更新, this.before() 触发beforeUpdate 生命周期
// 初次渲染 以及 更新组件
updateComponent = () => {
// 执行_update 进入更新阶段 首先执行 _render 将组件变成 VNode
vm._update(vm._render(), hydrating)
}
// init render watcher
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
Vue的_update
方法是实例上的一个私有方法,主要作用是把VNode渲染成真实的dom,它在首次渲染和数据更新的时候被调用。在数据更新的时候会发生VNode和旧VNode对比,获取差异更新视图,我们常说的diff
就是发生在此过程中。
_update
src\core\instance\lifecycle.js
// 组件初次渲染和更新的入口
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 页面挂载点 真实的元素
const prevEl = vm.$el
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.
// preNode 表示旧的节点
if (!prevVnode) {
// initial render (首次渲染)
// patch 阶段 patch diff算法
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
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.
}
vm.__patch__
__patch__
进入 patch 算法,主要是将vnode转换成dom,渲染在视图中, vm.__patch__
原型上挂载的位置:
- diff 算法 — 比较两棵虚拟 DOM 树的差异;
- pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树
/src/platforms/web/runtime/index.js
// 在 Vue 原型链上安装 web 平台的 patch 函数
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 所以对于客户端来说真实执行的是patch,其中noop表示空函数,如果不在客户端的话,就都是在服务端,所以不需要渲染真实得dom。
// patch import from src\platforms\web\runtime\patch.js
// patch 工厂函数,为其传入平台特有的一些操作,然后返回一个 patch 函数
export const patch: Function = createPatchFunction({ nodeOps, modules })
craetePatchFunction
其中包括 patch 执行过程中的工具方法,最终将 patch() 方法返回
src\core\vdom\patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
/**
* 工厂函数,注入平台特有的一些功能操作,并定义一些方法,然后返回 patch 函数
*/
export function createPatchFunction (backend) {
let i, j
const cbs = {}
/**
* modules: { ref, directives, 平台特有的一些操纵,比如 attr、class、style 等 }
* nodeOps: { 对真实 DOM 元素的增删改查 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
*/
return patch
}
patch
实现 diff 算法的入口函数,通过比较新旧节点来判断节点的操作
- 新节点不存在,老节点存在,调用 destroy,销毁老节点
- 新节点存在,老节点不存在,首次渲染组件
- 如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
-
如果 oldVnode 是真实元素,则替换已存在的节点
-
创建新节点 createElm()
-
递归更新父的占位符节点
-
删除旧节点 removeVnodes()
-
比较新旧节点的不同: 采用替换节点的方式
新旧节点节点相同的情况:采用 Diff算法 进行比较
src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 新节点不存在,老节点存在 则销毁老节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 新节点存在,老节点不存在,首次渲染组件时
// 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)) {
// oldVNode 不是真实元素 而且 oldVNode 和 VNode 是同一个节点 则进行patch 更新节点
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// oldVNode 是真实节点 则需要替换已存在节点
// replacing existing element
/**
* 替换已存在的节点
* 1. 创建新节点 -- 以当前就节点为参考节点,通过 creatElm() 创建新节点并插入到Dom 中
* 2. 更新父的占位符节点
* 3. 删除旧节点
*/
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.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 基于 oldVNode 是一个真实节点创建的 vnode
oldVnode = emptyNodeAt(oldVnode)
}
// 获取节点的真实元素
const oldElm = oldVnode.elm
// 获取节点的父节点 即body
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 基于新 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)
)
// update parent placeholder node element, recursively
// 递归更新父占位符节点元素
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
}
}
// destroy old node
// 删除旧节点
if (isDef(parentElm)) {
// 把 oldVnode 从当前 DOM 树中删除,如果父节点存在,则执行 removeVnodes 方法:
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
createElm():创建新节点
在 patch 节点比较的过程中,发现有新的节点则调用 createElm() 创建真实的 dom 节点 并插入到父节点上
// 创建真实的 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 是一个组件,则创建 则执行 data.hook.init() 实例化组件并挂载
// 为组件执行各个模块的 create 钩子函数
// 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
)
}
}
// 创建 DOM 节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
// 递归创建所有子节点 生成整颗 DOM 树
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)
}
}
createComponent(): 创建组件
如果 vnode 是一个组件,则执行 init 钩子,创建组件实例,并挂载,为组件执行各个模块的 create 方法
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 判断 组件实例是否已经存在 && 组件被 keep-alive 包裹
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 执行组件的 data.hook.init() 实例化组件 挂载
// 执行 vnode.data.init 钩子函数,该函数在讲 render helper 时讲过
// 如果是被 keep-alive 包裹的组件:则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
// 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 1. 调用组件vnode.data.hook上的init方法,执行了子组件构造函数的_init函数,并且执行了子组件的$mount方法。
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 为组件执行各个模块的 create 钩子函数
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
sameVnode
判断两个节点是否相同,
- 如果两个 vnode 的 key 是否相同
- 继续判断对于同步组件,则判断
isComment
注释节点、data
属性、input
输入框类型 是否相同
- 判断异步组件 (asyncFactory) 是否相同
/**
* 判读两个节点是否相同
*/
function sameVnode (a, b) {
return (
// key 必须相同,需要注意的是 undefined === undefined => true
a.key === b.key && (
(
// 标签相同
a.tag === b.tag &&
// 都是注释节点
a.isComment === b.isComment &&
// 都有 data 属性
isDef(a.data) === isDef(b.data) &&
// input 标签的情况
sameInputType(a, b)
) || (
// 异步占位符节点
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
removeVnodes: 删除旧节点
把 oldVnode
从当前 DOM 树中删除,如果父节点存在,则执行 removeVnodes
方法:
遍历待删除的 vnode 做删除,
- 调用removeAndInvokeRemoveHook() 从 dom 中移除节点并执行 remove 钩子函数,并对它的子节点递归调用 removeAndInvokeRemoveHook();
- invokeDestroyHook()执行 module 的
destory
钩子函数以及vnode
的destory
钩子函数,并对它的子vnode
递归调用invokeDestroyHook
函数;
- 最后执行removeNode() 调用平台的 dom API 将真正的节点移除
// 移除指定索引范围(startIdx —— endIdx)内的节点
function removeVnodes (parentElm, 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
// 调用平台的 DOM API 去把真正的 DOM 节点移除
removeNode(ch.elm)
}
}
}
}
// removeAndInvokeRemoveHook
// 从 dom 中移除节点并执行 remove 钩子函数,并对它的子节点递归调用 removeAndInvokeRemoveHook
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
let i
const listeners = cbs.remove.length + 1
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners)
}
// recursively invoke hooks on child component root node
if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm)
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm)
}
if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
i(vnode, rm)
} else {
rm()
}
} else {
removeNode(vnode.elm)
}
}
// 销毁老节点
// 执行组件的 destroy 钩子,即执行 $destroy 方法
// 执行组件各个模块(style、class、directive 等)的 destroy 方法
// 如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
function invokeDestroyHook (vnode) {
let i, j
// 获取 data 系列
const data = vnode.data
if (isDef(data)) {
// 执行 data.hook.destroy 钩子
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])
}
}
}
patchVnode: 新旧节点相同
在新旧节点相同的情况下,比较新旧节点的孩子节点,在对比两个虚拟节点过程中去更新真实dom
更新节点
- 全量的属性更新再去比较所有的孩子节点
- 如果新旧节点都有孩子,则递归执行 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
}
// 跳过静态节点的更新
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// 执行 prepatch 钩子函数
// 当更新的 vnode 是一个组件 vnode 的时候,会执行 prepatch 的方法
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 获取新老节点的所有孩子节点 Vue 3.0 在这里做了很多的优化
const oldCh = oldVnode.children
const ch = vnode.children
// 比较新旧节点的所有孩子节点
// 全量更新节点的所有属性 执行 update 钩子函数
// 在执行完新的 vnode 的 prepatch 钩子函数,会执行所有 module 的 update 钩子函数以及用户自定义 update 钩子函数
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)
}
// 判断 vnode 是否为文本节点
/*
* 1. 新旧节点为文本节点 且新旧节点的文本内容不同时 直接替换文本内容 更新文本节点
* 2. 不是文本节点则判断子节点:
* 1. oldCh 和 ch 都存在且不相同时,通过 updateChildren (diff 算法)来更新子节点
* 2. 只有 ch 时,即新增新节点,删除旧节点,如果旧的节点是文本节点则先将节点的文本清除,
* 再通过 addVnodes 创建这些新孩子节点批量插入到新节点的 elm 下
* 3. 只有 oldCh 存在时,即移除旧节点,则通过 removeVnodes 清空旧节点
* 4. 只有旧节点是文本节点时:旧节点的文本存在 新节点的文本不存在 则清空文本
*/
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)
}
}
小结
数据更新是通过观察者对象的实例方法 run 完成的,从代码中可以看到:在数据更新前会调用实例对象上的 before 方法,从而执行 beforeUpdate 生命周期钩子函数;在数据更新完成后,通过执行 callUpdatedHooks 函数完成 updated 生命周期函数的调用。
数据更新过程中的流程图
销毁阶段:
vue会将自身从父组件中删除,取消实例上所有依赖的追踪并且移除所有的事件监听器
beforeDestory: 进入销毁阶段,实例身上的数据仍可使用
beforeDestory ==>> destoryed: 卸载 watchers、 data、 将组件从已经渲染的DOM中卸载
destoryed: 组件完全销毁,取消实例上所有依赖的追踪并且移除所有的事件监听器
// $destory 完全销毁一个实例, 触发 beforeDestory、destoryed 钩子
// 清理它与其他实例的连接,接触她的全部指令及事件监听器
Vue.prototype.$destroy = function () {
var vm = this;
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy');
vm._isBeingDestroyed = true;
// 将自身从父组件中删除
var parent = vm.$parent;
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm);
}
// 卸载 wather
if (vm._watcher) {
vm._watcher.teardown();
}
var i = vm._watchers.length;
while (i--) {
vm._watchers[i].teardown();
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--;
}
// call the last hook...
vm._isDestroyed = true;
// 执行一次null的pathc将虚拟dom已经渲染的dom卸载
vm.__patch__(vm._vnode, null);
// fire destroyed hook
callHook(vm, 'destroyed');
// 执行destory钩子函数,执行off卸载,绑定事件,解除与el的绑定
vm.$off();
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null;
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null;
}
};
小结
在 beforeDestory 进入实例的销毁阶段,但此时还能获取实例生的数据、事件,之后进入卸载阶段,首先 将自身从父组件上移除开始卸载 watcher、组件上的 data 数据 、卸载子组件的DOM ,在 destoryed 时子实例已被销毁,数据不可使用,移除事件监听器,父组件赋值为null。
完结
今天的生命周期就先到这里啦,能看到这里的同学们都很棒哦?
希望你们也能从中有所收获,由于篇幅有限,生命周期相关的问题我也会在后续更新
敬请期待哦。。。
参考
- 生命周期源码解读 :juejin.cn/post/699989…
- Vue2.0源码阅读笔记:juejin.cn/post/684490…
- initInject的实现 :juejin.cn/post/687778…
- 响应式原理、watcher的实现:zhuanlan.zhihu.com/p/168768245
- vue的响应式原理: juejin.cn/post/691827…