vue2源码解析之整体流程(生命周期)
前言
本章主要把之前讲解的数据的响应式和模板编译等模块进行串联起来;
整体流程
vue中从实例的创建到实例被销毁的过程中需要处理很多事情,整个过程就是vue的生命周期;在vue的生命周期中的不同阶段提供了不同的钩子函数,这些钩子函数的作用是为了用户在不同阶段处理一些额外的事情,例如实例创建,模板编译,模板挂载等;
官网的vue生命周期流程图
- 初始阶段:创建Vue实例、初始化事件和生命周期、beforeCreate、初始化注入和校验、created;
- 模板编译阶段:进行模板编译转成render函数;
- 挂载阶段:把模板渲染到真实dom中;
- 已挂载(更新)阶段: 挂载完毕,数据的变动会触发dom的更新;
- 销毁阶段:销毁当前组件的实例,取消相应的依赖和事件监听;
初始化阶段
初始阶段:创建vue实例,初始化事件和生命周期,执行breforeCreate,初始化注入和校验,执行created;
创建vue实例
创建vue实例其实就是执行Vue构造函数,Vue构造函数中执行了原型上的_init函数;
// 源码位置: src/core/instance/index.js
function Vue (options) {
// 非生产环境判断是否是new构造出来的实例
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 执行init函数
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
_init函数是在initMixin函数中定义的;_init函数中把传递的options和当前实例的options和父级的options进行合并;后续进行了初始化生命周期、初始化事件、初始化渲染函数、执行beforeCreate生命周期钩子、初始化注入、初始化state、初始化provide、执行created生命周期钩子、判断是否传递了el,如果传递了就执行$mount
进行模板的编译和挂载,如果没有传递就需要用户手动执行$mount
进入下个阶段,否则一直停留在这个阶段;
// 源码位置 src/core/instance/init.js
function initMixin (Vue: Class<Component>) {
// 原型上添加_init函数
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
// 合并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)
} 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)
// 初始化事件
initEvents(vm)
// 初始化render
initRender(vm)
// 执行beforeCreate钩子
callHook(vm, 'beforeCreate')
// 初始化注入
initInjections(vm) // resolve injections before data/props
// 初始化props,methods,data,computed,watch
initState(vm)
// 初始化provide
initProvide(vm) // resolve provide after data/props
// 执行created钩子
callHook(vm, 'created')
// 如果用户传递了el,那么执行$mount进行模板的编译和挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
vm.$options.el存在的情况
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
vm.$options.el不存在的情况,用户手动调用
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
面试题1:beforeCreate和created的区别? 在beforeCreate钩子执行之前进行初始化了生命周期,初始化了事件,初始化了渲染函数,在beforeCreate和created之间进行了初始化注入,初始化state(props,methods,data,computed,watch)此时就是调用observe给数据设置响应式,初始化provide;
面试题2:为什么inject初始化放在provide的前面? 因为inject中提供的是注入到当前组件的方法或属性,而且当前组件中的data或methods中存在同名的会进行覆盖,因此需要放在data,props,methods等前面;而provide是给当前组件里面的子孙组件提供数据,它可能使用到data或methods中的属性,因此需要当在它们的后面;
options的合并
options的合并区分当前是组件还是非组件;
如果当前是组件,就通过initInternalComponent函数,把组件中的options属性放在实例的$options上,并且把父组件中的一些属性挂载到当前组件上;
// 源码位置 src/core/instance/init.js
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
如果不是组件,通过mergeOptions合并传递的、当前实例上的和父级的options;mergeOptions函数中处理了传递进来的options上的props、inject、directives、extends、mixins,通过策略模式处理了它和它父级的属性;
// 源码位置 src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (typeof child === 'function') {
child = child.options
}
// 设置props,把带有-的属性名改成驼峰,存到一个新的对象中进行返回
normalizeProps(child, vm)
// 设置注入的属性 返回一个{from:val}的对象
normalizeInject(child, vm)
// 设置指令 返回指令的值为{ bind: def, update: def }的形式
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
// 递归处理extends和mixins
if (!child._base) {
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
for (key in parent) { // 遍历父级进行设置属性
mergeField(key)
}
for (key in child) { // 遍历子级进行设置属性
if (!hasOwn(parent, key)) { // 父级不存在此属性就进行设置
mergeField(key)
}
}
// 通过策略模式给不同的属性设置对应的执行函数,
function mergeField (key) {
// 获取到不同属性的函数
const strat = strats[key] || defaultStrat
// 执行对应的函数重新设置属性
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
props、methods、inject、computed属性的处理函数就是把父子属性进行合并
// 源码位置 src/core/util/options.js
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (!parentVal) return childVal
const ret = Object.create(null)
// 把parentVal的复制一份到ret中
extend(ret, parentVal)
// 如果有childVal 进行复制到ret中
if (childVal) extend(ret, childVal)
return ret
}
watch属性的处理,把父子中的watch进行合并并且通过数组的形式进行连接,防止子级中同名的进行覆盖;
// 源码位置 src/core/util/options.js
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
// 遍历子级中的watch进行合并并且如果不是数组就转成数组
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
生命周期的处理,生命周期也是合并父子的生命周期并且存放在数组中防止子级覆盖同名的父级;
// 源码位置 src/core/util/options.js
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
data属性的处理,data函数把父级中的data属性子级中不存在的就添加到子级的data中,并且设置为响应式的;
// 源码位置 src/core/util/options.js
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
// 遍历父级中的data属性
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) { // 子级中没有这个属性就进行设置
set(to, key, fromVal)
} else if ( // 如果父子都是对象通过递归处理
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
// 源码位置 src/core/observe/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// 如果是数组就进行替换
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 是对象就直接赋值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 获取到当前实例
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
return val
}
if (!ob) {
target[key] = val
return val
}
// 添加响应
defineReactive(ob.value, key, val)
// 进行监听
ob.dep.notify()
return val
}
初始化生命周期
初始化生命周期很简单,首先获取到当前的父级,如果父级存在并且当前不是抽象组件,就进行while循环向上查找直到找到第一个非抽象组件,把当前非抽象组件的parent作为组件的父级,把当前组件作为父级的children;定义并且初始化一些变量;以$开头的是提供给用户使用的外部属性,以_开头的是内部使用的属性;
// 源码位置 src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到没有abstract属性的父级
let parent = options.parent
// 父级存在并且当前不是抽象组件
if (parent && !options.abstract) {
// 循环向上找,第一个是非抽象组件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
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
}
初始化实例上的事件(initEvents)
例子:
<select @click="a" v-once />
获取到父组件的事件,把这些事件添加到当前组件上
// 源码位置 src/core/instance/events.js
export function initEvents (vm: Component) {
// 初始化_events为空对象
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
// 获取到父级的事件
const listeners = vm.$options._parentListeners
// 存在就把事件更新到当前组件
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
let target: any
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
// 给当前组件注册事件
function add (event, fn) {
target.$on(event, fn)
}
// 移动当前组件上的事件
function remove (event, fn) {
target.$off(event, fn)
}
// 创建once函数
function createOnceHandler (event, fn) {
const _target = target
return function onceHandler () {
// 执行一次之后就移除
const res = fn.apply(null, arguments)
if (res !== null) {
_target.$off(event, onceHandler)
}
}
}
updateListeners函数中,遍历当前的事件列表,如果这个事件在之前的事件列表中不存在,表示需要添加这个事件,进行添加;遍历之前的事件列表,如果这个事件在当前事件列表中不存在,表示需要删除,进行删除;
// 源码位置 src/core/vdom/helpers/update-listeners.js
export function updateListeners (
on: Object,
oldOn: Object,
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]
// 获取到符号后面的事件名 比如~select返回select
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(old)) { // 如果之前的事件不存在那么就添加这个事件
// 如果当前fns不存在,那么就进行创建,防止重复创建
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
}
}
// 遍历之前的事件列表
for (name in oldOn) {
// 如果当前事件列表不存在,表示已经删除了就把对应的事件进行删除
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
normalizeEvent函数判断对应的事件,并且获取到对应的事件名
// 源码位置 src/core/vdom/helpers/update-listeners.js
const normalizeEvent = cached((name: string): {
name: string,
once: boolean,
capture: boolean,
passive: boolean,
handler?: Function,
params?: Array<any>
} => {
const passive = name.charAt(0) === '&'
name = passive ? name.slice(1) : name
const once = name.charAt(0) === '~' // Prefixed last, checked first
name = once ? name.slice(1) : name
const capture = name.charAt(0) === '!'
name = capture ? name.slice(1) : name
return {
name,
once,
capture,
passive
}
})
createFnInvoker函数进行事件函数的代理返回一个新的函数
// 源码位置 src/core/vdom/helpers/update-listeners.js
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
function invoker () {
const fns = invoker.fns
if (Array.isArray(fns)) { // 如果是数组就遍历执行这个fns中的函数
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)
}
} else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)
}
}
// 把事件放在invoker的fns属性上 并且返回这个invoker函数
invoker.fns = fns
return invoker
}
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
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
}
初始化render
初始化两个创建虚拟节点的函数,分别提供内部和外部使用;通过observer下的defineReactive实现attrs和listeners数据的监听;
// 源码位置 src/core/instance/render.js
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
// _c创建虚拟节点的函数 内部使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// _c创建虚拟节点的函数 外部使用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
// 把attrs和listeners转为响应数据
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)
}
}
执行钩子beforeCreate函数
通过oberver中的dep进行搜集依赖,获取到钩子函数数组,遍历这个钩子函数的数组进行挨个执行,移除依赖;
// 源码位置 src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
// 搜集依赖
pushTarget()
// 找到这个钩子函数
const handlers = vm.$options[hook]
const info = `${hook} hook`
// 如果钩子函数存在就遍历执行
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
// 移除依赖
popTarget()
}
// 源码位置 src/core/observer/dep.js
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
初始化注入initInjections
vue中的inject注入的值可以是字符串数组,可以是一个对象,inject中的属性可以通过from指定来源,也可以设置default默认值;
// 父级
const Provider = {
provide () { // 向下级提供数据
return {
bar: '123'
}
}
}
// 子级
const Child = {
inject: { // 向当前组件注入父级提供的数据
foo: {
from: 'bar', // 指定来源于父级的bar
default: () => [1, 2, 3] // 默认值是一个函数
}
}
}
initInjections中把inject的数据转成键值对的形式result = {'foo':'bar'},遍历result把这些数据添加到当前实例上,并且不设置为响应式数据;toggleObserving(false)的目的就是表示在执行defineReactive的时候不要给数据添加响应式;defineReactive函数就是obverser中给当前实例添加数据的方法;但是如果父级提供的数据就是响应式的那么它还是具有响应的;
// 源码位置 src/core/instance/inject.js
export function initInjections (vm: Component) {
// 把inject中的数据转成键值对的形式返回 result = {'foo':'bar'}
const result = resolveInject(vm.$options.inject, vm)
if (result) {
// 设置数据不进行响应
toggleObserving(false)
// 遍历result 把它上面的数据全部通过definedReactive添加到实例上
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)
}
}
// 源码位置 src/core/observer/index.js
export function toggleObserving (value: boolean) {
shouldObserve = value
}
resolveInject函数中把indect数据转成键值对;inject中的key都是由父级或祖先级中的provide提供的,因此从当前组件开始一级一级的向上级查找key,直到找到为止;如果没有找到就判断是否设置了默认值,设置了默认值再判断默认值是否是一个函数,是函数就执行之后取其返回值,如果不是函数就直接取其作为值;如果没有设置默认值就报错;
// 源码位置 src/core/instance/inject.js
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)
// 获取到inject的所有key
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
// 遍历key
for (let i = 0; i < keys.length; i++) {
// 获取到key
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
// 获取到inject中的from属性
const provideKey = inject[key].from
let source = vm
// 从祖先级中找到from对应的属性
// 遍历组件,从当前组件一直向上搜集到祖先级组件中的provide中的provideKey对应的字段值
while (source) {
// 当前组件有provide并且provideKey对应的字段是它的自有属性就存入到result中
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
// 父级不存在 就取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中的key的值的from属性?那字符串数据怎么没有处理?是因为在 mergeOptions处理options的时候执行了normalizeInject方法,此方法就是把inject中的数据统一转成{from:val}的形式返回;因此在上面代码中无需处理数组还是对象;
// 源码位置 src/core/util/options.js
function normalizeInject (options: Object, vm: ?Component) {
const inject = options.inject
if (!inject) return
const normalized = options.inject = {}
// 如果是一个数组
if (Array.isArray(inject)) {
for (let i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] }
}
} else if (isPlainObject(inject)) { // 是一个对象
for (const key in inject) {
const val = inject[key]
// 如果值是一个对象,直接把值复制到from所在这个对象中
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "inject": expected an Array or an Object, ` +
`but got ${toRawType(inject)}.`,
vm
)
}
}
初始化实例属性initState
初始化实例属性就是,初始化props,data,methods,computed和watch属性;把这些属性中的数据挂载带实例上;initState函数中通过判断不同类型的数据是否存在,存在就调用相应的方法进行处理;给数据添加响应式就是再次函数中完成的
// 源码位置 src/core/instance/state.js
export function initState (vm: Component) {
// 初始化_watchers属性进行watch的存储
vm._watchers = []
const opts = vm.$options
// 如果有props就调用initProps进行处理props属性
if (opts.props) initProps(vm, opts.props)
// 如果有methods就调用initMethods进行处理methods
if (opts.methods) initMethods(vm, opts.methods)
// 如果有Data就通过initData进行处理
if (opts.data) {
initData(vm)
} else { // 如果没有data就通过observe进行初始化data
observe(vm._data = {}, true /* asRootData */)
}
// 如果有computed 就通过initComputed进行处理
if (opts.computed) initComputed(vm, opts.computed)
// 有watch 通过initWatch进行处理
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
处理props属性initProps
props的使用方式有以下三种,可以是字符串数组,可以是对象,对象的属性值可以直接是类型,对象的属性值也可以是一个对象用于指定类型和默认值;
// 方式一
props: ['name']
// 方式二
props: {
name: 'String',
}
// 方式三
props: {
name: {
type: 'String',
default: '123'
}
}
vue中针对这三种不同的形式没有做出分出的处理,而是在处理options的时候,就把这三种情况进行了处理,处理成统一的形式:
props: {
name: {
type: XXXX,
default: XXX,
}
}
在mergeOptions中已经解析过对props的处理,normalizeProps函数分别对数组,对象的形式进行了处理统一转成{type:XXX},并且对于key进行了驼峰处理;
// 源码位置 /src/core/util/options.js
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
// 如果是数组 props: ['first-name','age'] 处理方式一
if (Array.isArray(props)) {
i = props.length
// 进行遍历
while (i--) {
// 获取到value
val = props[i]
// 如果值是字符串
if (typeof val === 'string') {
// 处理成驼峰形式
name = camelize(val)
// 进行存储
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) { // 如果是对象
// 遍历对象
for (const key in props) {
// 获取到值
val = props[key]
// key转成驼峰
name = camelize(key)
// 如果值还是对象直接存储,否则放入type下
res[name] = isPlainObject(val)
? val // 处理方式3
: { type: val } // 处理方式2
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
// 处理的结果放在props下
options.props = res
}
// -转成驼峰 first-name 转成firstName
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
initProps函数,遍历子组件的props属性,校验每个属性的类型和父组件传递进来的属性类型是否一直并且返回属性的值;校验完成之后把属性添加到当前实例的_props上;如果当前实例上没有这个属性,那么添加此属性并且访问和修改都是操作_props中的此属性(设置属性代理);
// 源码位置 src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
// 父组件传递的props数据
const propsData = vm.$options.propsData || {}
// 初始化_props
const props = vm._props = {}
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
// 父级组件不存在 则不用给props的属性设置为响应式
if (!isRoot) {
toggleObserving(false)
}
// 遍历子组件的props
for (const key in propsOptions) {
// 收集key属性
keys.push(key)
// 校验父级传递的props属性和子组件设置的props属性类型是否一致并且返回值
const value = validateProp(key, propsOptions, propsData, vm)
// 把props中的属性添加到_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.
// 如果当前实例上没有这个属性,那么在vm上访问此属性的时候直接访问代理的_props中的此属性
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
// 开启响应设置
toggleObserving(true)
}
validateProp函数,用于判断父组件传递的props的值是否是子组件中设置的类型,并且返回props中的key对应的值;函数中获取到子组件中props的key对应的值标记为prop,获取到父组件传递的props的key对应的值value;在prop的type中查找是否有布尔类型,如果有就返回索引,如果没有返回-1;如果存在布尔,判断父组件是否传递了这个props,如果没有传递并且子组件中没有设置对应的默认值,那么就把value设置为false;如果父组件传递了这个props,并且传递的值为空,或者props的key和value相同,那么就在prop的type中查找是否有字符串类型,如果没有或布尔类型在字符串的前面,那么布尔的类型优先级高于字符串;此时设置value为true;其他类型就判断value是否是undefined;如果是就查找默认值,并且设置为响应的,最后返回value;
// 源码位置 src/core/util/prop.js
export function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
// 获取到子组件的props中的key的值 { type:XXX,value:XXX}
const prop = propOptions[key]
// 父组件上的propsData上没有key这个属性
const absent = !hasOwn(propsData, key)
// 获取到父组件传递的props的值
let value = propsData[key]
// boolean casting
// 查看是否是布尔类型
const booleanIndex = getTypeIndex(Boolean, prop.type)
// 如果存在布尔类型 布尔类型设置它的值为true或false 其他类型如果不为空直接返回
if (booleanIndex > -1) {
// 如果没有这个属性并且没有设置默认值 把value设置为False
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) { // 如果值不存在或者值等于key,
// only cast empty string / same name to boolean if
// boolean has higher priority
// 获取到String类型
const stringIndex = getTypeIndex(String, prop.type)
// 字符串类型不存在或布尔类型在字符串类型的前面,那么设置值为true
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// check default value
// 值为undefined 就从子组件中的props获取默认值
if (value === undefined) {
// 获取到这个key的默认值
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
// 通过observe设置值为响应的
observe(value)
toggleObserving(prevShouldObserve)
}
if (
process.env.NODE_ENV !== 'production' &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && ('@binding' in value))
) {
assertProp(prop, key, value, vm, absent)
}
return value
}
// 情况一 父组件没有传递name属性,子组件设置的类型为布尔和字符串,那么最后返回的value=fase
// 父组件
<Child />
// 子组件中的props
props: {
name: {
type: [Boolean,String],
}
}
// 情况2 父组件传递了name但是没有指定值或age的值也是age,子组件设置了布尔和字符串类型,
// 并且布尔在字符串的前面,最后得到的value=true
// 父组件
<Child name age="age" />
// 子组件中的props
props: {
name: {
type: [Boolean,String],
},
age: {
type: [Boolean,String],
}
}
// 情况3 父组件传递的name的值为undefined,子组件设置了默认值,那么通过observe设置这个值为响应的
// 父组件
<Child :name="undefined" />
// 子组件中的props
props: {
name: {
type: [Boolean,String],
default: '123'
}
}
// 情况4 其他类型并且父组件传递了值 直接返回这个值
// 父组件
<Child :name="data" />
// 子组件中的props
props: {
name: {
type: [Object],
default: {}
}
}
getPropDefaultValue获取子组件设置的默认值,如果父组件传递的值为undefined并且子组件中的_props已经有这个值了,那么直接返回这个值;如果默认值是函数类型,并且子组件设置的类型不是函数,那么执行这个函数并且返回结果,否则直接返回这个默认值;
// 源码位置 src/core/util/prop.js
function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): any {
// no default, return undefined
if (!hasOwn(prop, 'default')) {
return undefined
}
// 获取到默认值
const def = prop.default
// 如果父组件传递的值为undefined并且子组件中的值存在,那么直接返回子组件的值
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
// 如果值为函数并且子组件中设置的类型不是函数,那么就执行这个函数返回执行的结果,否则直接返回这个值
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
}
proxy设置代理属性,给对象上添加属性,在这个属性get或set的时候操作代理对象,这就是代理属性;
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
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)
}
处理methods
遍历methods属性,把每项添加到当前实例上;判断每项是否是函数,不是函数就设置默认的一个空函数,否则就通过当前实例去调用这个函数;
// 源码位置 src/core/instance/state.js
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
// 遍历 判断每项是否是函数,不是函数就设置一个默认的空函数,否则就通过当前实例执行这个函数
for (const key in methods) {
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
处理data
定义一个变量data,判断传递进来的data的类型是否是一个函数,如果是函数通过getData执行这个函数返回执行的结果,如果不是函数就直接取data;判断data是否是一个对象,如果不是一个对象,默认设置为一个空对象;遍历这个data对象,判断每项的key是否是在props或methods中存在,存在就警告;否则判断key是否是以$或_开头的,不是则通过代理添加到当前实例上;最后通过oberve给data的每一项添加响应;响应式数据的章节
// 源码位置 src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
// 判断Data是否是一个函数,是函数通过getData执行获取到返回值,否则直接获取到此属性
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://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
// 获取到Data对象的keys
const keys = Object.keys(data)
// 获取到父组件传递的props属性
const props = vm.$options.props
// 获取到父组件传递的methods属性
const methods = vm.$options.methods
let i = keys.length
// 遍历Data
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
// props中存在这个属性,表示data中重复了进行提示
} else 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
proxy(vm, `_data`, key)
}
}
// observe data
// 给data添加响应
observe(data, true /* asRootData */)
}
getData函数,用来执行传递进来的函数,并且处理执行中的错误,并且进行依赖收集;
// 源码位置 src/core/instance/state.js
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()
}
}
处理computed
// 源码位置 src/core/instance/state.js
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
// 创建一个空对象进行初始化_computedWatchers用于缓存
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
// 是不是SSR
const isSSR = isServerRendering()
// 遍历computed
for (const key in computed) {
// 获取到对应的值
const userDef = computed[key]
// 如果值是一个函数,那么直接使用,否则就取它的get属性
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.
// 创建一个watcher实例给computed属性
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
// 如果当前实例没有这个属性
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
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)
}
}
}
}
定义一个空对象作为watchers的初始值,用来进行缓存;判断是否是服务端渲染(SSR)
// 创建一个空对象进行初始化_computedWatchers
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
// 是不是SSR
const isSSR = isServerRendering()
遍历compute对象,获取到对应的值;判断值是否是一个函数,不是函数就获取它的get属性;如果不是服务端渲染就创建一个Watcher的实例对象,赋值给watchers中的属性;其实不是服务端渲染的话每个计算属性都是一个Watcher实例,并且添加到watchers中进行缓存;
// 获取到对应的值
const userDef = computed[key]
// 如果值是一个函数,那么直接使用,否则就取它的get属性
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.
// 创建一个watcher实例给computed属性
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
如果当前实例上不存在计算属性,那么就进行添加;这样的话就可以通过当前实例直接获取到计算属性;
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
defineComputed函数用来给实例上添加对应的computed属性值;
// 源码位置 src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 不是在服务端就进行缓存
const shouldCache = !isServerRendering()
// 如果值是一个函数
if (typeof userDef === 'function') {
// 如果需要缓存就调用createComputedGetter
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
// set设置为一个空函数
sharedPropertyDefinition.set = noop
} else {
// 获取到get存在并且需要缓存就执行createComputedGetter,不存在就设置为空函数
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
)
}
}
// 给当前实例上添加这个属性
Object.defineProperty(target, key, sharedPropertyDefinition)
}
如果不是在服务端渲染,表示需要进行缓存(计算属性会进行缓存,当数据没有变化的时候从缓存中获取);如果计算属性的值是一个函数,那么执行createComputedGetter;是函数表示没有设置set; set的值设置为一个空函数;
// 不是在服务端就进行缓存
const shouldCache = !isServerRendering()
// 如果值是一个函数
if (typeof userDef === 'function') {
// 如果需要缓存就调用createComputedGetter
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
// set设置为一个空函数
sharedPropertyDefinition.set = noop
}
如果不是一个函数就是一个对象,判断此对象的get属性是否存在,存在判断是否需要缓存,需要就执行createComputedGetter;否则没有get就设置一个空函数;如果有set就获取set没有就设置一个空函数;
else {
// 获取到get存在并且需要缓存就执行createComputedGetter,不存在就设置为空函数
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
给当前的实例上添加此属性
Object.defineProperty(target, key, sharedPropertyDefinition)
createComputedGetter函数是一个高阶函数,执行计算属性的时候会执行computedGetter函数,这个函数中获取到计算属性的watcher实例,通过实例上的dirty来判断属性是否发生了变化,为true表示发生了变化,执行evaluate进行获取最新值;否则就没有发生变化直接获取之前的watcher.value值;
function createComputedGetter (key) {
return function computedGetter () {
// 获取到key对应的计算属性的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果dirty存在表示计算属性发生了变化 执行evaluate获取最新值
if (watcher.dirty) {
watcher.evaluate()
}
// 如果有依赖 把渲染的watcher进行添加到计算属性的相关数据的依赖中
if (Dep.target) {
watcher.depend()
}
// 返回值
return watcher.value
}
}
}
watcher中的evaluate方法;执行get获取到值之后就会把dirty设置为false;如果数据没有变化,再次获取的时候就不会执行evaluate;如果数据变化会执行update,dirty设置为true;再次获取的时候就会执行evaluate;
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
一个组件中的计算属性上有a和b两个方法:
computed: {
a()[},
b(){}
}
在定义好计算属性之后,vue内部在初始化计算属性的时候就把a和b的值通过Watcher进行监听,并且把watcher的实例添加到缓存_computedwatchers中;在使用a和b的时候,会从缓存中获取到对应的watcher实例,通过实例上的dirty判断当前计算属性中依赖的数据是否发生变化,如果变化了就是true,执行watcher实例的evaluate方法获取到最新的值,最后返回value,如果没有变化直接返回value;
视图根据计算属性中数据的变化而更新
在之前章节的数据响应式篇中的watcher是不完整的,在这里进行补充完整;并且需要把下面模板编译阶段看完再回过头看此分析,因为会联系到模板编译阶段的渲染Watcher
当视图中使用了计算属性,首次渲染的时候视图能够显示计算属性的值;当计算属性中的数据变化之后,视图中的计算属性是不会变化的,因此下面分析下计算属性中的数据变化的时候视图也更新是怎么做到的?
上面在处理计算属性的时候给每个计算属性定义了Watcher,它只是用来监听计算属性,当计算属性中的值变化的时候,在下一次使用计算属性能够获取到最新的值;也就是计算属性中使用了响应式数据能够收集到这个watcher,当数据变化的时候通知这个watcher进行更新获取到最新的值;而无法让视图进行更新;因为它所使用到的数据只收集了计算属性的watcher而没有收集渲染视图的watcher;
收集渲染视图的watcher的整体流程:
- 在模板挂载阶段,给每个组件定义一个渲染watcher;
- Watcher监听的是更新视图的函数,在new Watcher的时候就会执行这个函数,如果视图中使用了计算属性,那么就会获取计算属性的值,此时就会执行到计算属性中使用到的响应式数据,再此响应式数据就可以收集到渲染视图的watcher;
- 等到计算属性中的响应式数据变化的时候就会通过渲染视图的watcher进行更新;
那么如何知道计算属性中使用到了哪些响应式数据呢?
当定义计算属性的watcher的时候,计算属性中的响应式数据就会收集当前的watcher(计算属性中的每个响应式数据都会收集当前计算属性的watcher作为依赖),那么此时就是多对一的关系,多个数据对应一个watcher;如果这个watcher中也保存下这些数据,在读取计算属性的时候就可以通过当前的计算属性的watcher获取到这些数据,从而再给这些数据收集渲染视图的watcher; 完善依赖收集器Dep
// 源码位置 ./src/core/observer/dep.js
// 之前的depend方法
depend () {
// 依赖挂载到Dep构造器的Target属性上
if (Dep.target) {
this.addSub(Dep.target)
}
}
// 修改之后
let id = 0
export class Dep {
constructor () {
...
this.id = id++
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
...
}
可以看到之前收集依赖,是直接调用了Dep中的addSub方法,而修改之后是调用了Dep.target中的addDep,也就是watcher中的addDep方法,并且把当前的dep传递进去;并且给每个dep都添加了id作为唯一标识;
完善watcher
let id = 1
export class Watcher {
constructor(vm, exporOrFn, cb, options){
...
// 防止收集重复的dep
this.depsId = new Set()
this.id = id++
// 收集dep
this.deps = []
...
}
// 给当前的watcher中添加dep
addDep (dep) {
// 防止重复添加一个数据的dep,因为首次读取的时候就会添加,当修改完数据之后会更新视图会再次执行数据的getter方法进行添加依赖;
if (!this.depsId.has(dep.id)) {
this.depsId.add(dep.id)
this.deps.push(dep)
// 再把当前的Watcher存入到dep中
dep.addSub(this)
}
}
修改之前是没有addDep方法的,现在给watcher添加了addDep方法,用来给当前的watcher收集dep;在构造函数中通过Set定义了一个收集dep的id的属性,同时定义了deps用来收集dep;在addDep中通过dep的id先判断是否已经收集了这个dep,如果没有收集就把id添加到depsId中,把dep添加到deps中,接着执行了dep的addSub方法,并且把当前的watcher传递进去;这样dep也收集了当前的watcher;现在计算属性中的响应式数据中的dep收集了计算属性的watcher,计算属性的watcher也收集了这些响应式数据的dep,那么这些dep又如何收集渲染视图的watcher呢?
计算属性中的数据收集渲染视图的Watcher
上面的第2步的时候,在挂载的时候就会定义渲染watcher,此时就会执行渲染watcher中的get方法;
get(){
const vm = this.vm
console.log('this', this)
pushTarget(this)
let result = this.getter.call(vm, vm)
console.log('result', result)
popTarget()
return result
}
pushTarget(this)中的this就是当前的渲染watcher;接着执行this.getter也就是更新视图的_update方法,此时会执行render函数生成虚拟dom,此时就会读取模板中的计算属性,从而执行计算属性的get方法也就是上面分析的计算属性中的createComputedGetter函数返回的方法
function createComputedGetter (key) {
return function () {
// 从缓存中获取
debugger
const watcher = this._computedWatchers[key]
if (watcher) {
// 如果相关数据改变,那么执行watcher重新获取最新数据
if (watcher.dirty) {
watcher.evaluate()
}
console.log('Dep.target', Dep.target)
// 如果有依赖 把渲染的watcher进行添加到计算属性的相关数据的依赖中
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
此时的watcher.dirty为true,就会执行watcher.evaluate,evaluate中又会执行this.get;
get(){
const vm = this.vm
console.log('this', this)
pushTarget(this)
let result = this.getter.call(vm, vm)
console.log('result', result)
popTarget()
return result
}
此时pushTarget(this)中的this就是计算属性的watcher;然后执行计算属性的this.getter,执行完毕之后就执行popTarget方法把Dep.target清空;执行完get之后继续执行evaluate,执行完evaluate之后,继续执行createComputedGetter下的代码,判断Dep.target是否有值,此时Dep.target是没有值的,因此获取不到渲染视图的watcher;怎么获取到渲染视图的watcher就需要修改watcher中使用到的pushTarget方法;
function createComputedGetter (key) {
return function () {
// 从缓存中获取
debugger
const watcher = this._computedWatchers[key]
if (watcher) {
// 如果相关数据改变,那么执行watcher重新获取最新数据
if (watcher.dirty) {
watcher.evaluate()
}
console.log('Dep.target', Dep.target)
// 如果有依赖 把渲染的watcher进行添加到计算属性的相关数据的依赖中
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
修改pushTarget和popTarget方法用来存储渲染视图的Watcher;
// 源码位置 src/core/observer/dep.js
Dep.target = null
const stack = []
export function pushTarget (watcher) {
Dep.target = watcher
stack.push(watcher)
}
export function popTarget () {
stack.pop() // 删除掉当前的依赖
// // 获取上个依赖
Dep.target = stack[stack.length - 1]
}
可以看到,现在不仅把watcher添加到Dep.target上,并且还存储到一个数组中,在清空的时候直接从数组中获取最后一个值;这样在执行更新视图函数的watcher的时候就会把渲染视图的Watcher存储到stack中,视图中读取计算属性的时候,就会执行计算属性的watcher,此时就会把计算属性的Watcher也添加到stack中,当计算属性的watcher执行完就会从Stack中把计算属性的Watcher移除,把渲染视图的watcher赋值给Dep.target,再执行如下函数的Dep.target的时候就是渲染视图的watcher,接着执行计算属性Watcher的depend方法,给计算属性的watcher中的dep添加当前渲染视图的Watcher;
function createComputedGetter (key) {
return function () {
...
// 如果有依赖 把渲染的watcher进行添加到计算属性的相关数据的依赖中
if (Dep.target) {
watcher.depend()
}
...
}
}
}
给watcher类中添加depend方法
depend () {
// 找到当前watcher中的所有的dep.把当前的watcher添加到当前watcher中的所有的dep中
let i = this.deps.length
while(i--){
this.deps[i].depend()
}
}
可见,就是遍历当前Watcher中的deps,执行每个dep中的depend进行收集渲染视图的watcher;
以上就是计算属性中如何收集计算属性的watcher和渲染视图的watcher;
小结:
计算属性: 其实每个计算属性都是一个Watcher,在初始化计算属性的时候,给当前实例上添加了一个计算属性的缓存对象,遍历计算属性对象,每个计算属性都通过Watcher进行监听,把每个Watcher的实例添加到缓存对象中;然后再把每个计算属性添加到当前的实例上,通过defineProperty给当前实例添加对应的计算属性,每次获取的时候从缓存中获取到Watcher的实例,通过实例上的dirty判断是否被修改了,如果修改了就执行实例上的evaluate重新获取新值,否则就返回实例上的value;(当计算属性中的数据被修改了,那么就会被watcher监听到从而执行update的时候会把dirty设置为true)
处理watcher
遍历wathcer,获取到属性的值,判断属性的值是否是一个数组,如果是数组就进行遍历一个一个的调用createWatcher进行处理,否则直接调用createWatcher;
// 源码位置 src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
// 遍历watch
for (const key in watch) {
// 获取到对应的值
const handler = watch[key]
// 判断值是否是一个数组
if (Array.isArray(handler)) {
// 遍历数组值
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
watcher的用法,监听对象的值可以是一个对象也可以是一个数组或字符串;
{
watch: {
a: [
function aa(){},
'cc',
],
b: {
handler: function(){},
deep: true
},
c: "ccc"
}
}
/*
处理
a: [
function aa(){},
'cc',
],
*/
if (Array.isArray(handler)) {
// 遍历数组值
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
}
createWatcher给watch中的每个属性通过watcher进行监听;
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 是对象
if (isPlainObject(handler)) {
// 把值作为options配置项
options = handler
// 获取到值中的handler属性
handler = handler.handler
}
// 如果是字符串
if (typeof handler === 'string') {
// 直接获取到实例上的这个属性
handler = vm[handler]
}
// 进行监听
return vm.$watch(expOrFn, handler, options)
}
如果是对象就把它作为选项,这样就可以处理deep等配置项,并且获取对象中的handler;如果是字符串就直接从实例上找到这个属性的值;
/**
处理以下的情况
b: {
handler: function(){},
deep: true
},
*/
// 是对象
if (isPlainObject(handler)) {
// 把值作为options配置项
options = handler
// 获取到值中的handler属性
handler = handler.handler
}
/*
处理 c: "ccc"
*/
// 如果是字符串
if (typeof handler === 'string') {
// 直接获取到实例上的这个属性
handler = vm[handler]
}
初始化initProvide
initProvide比较简单,获取到provide之后,判断是否是一个函数,如果是函数就执行这个函数并且把返回结果放到实例的_provided属性上,如果不是函数直接把值放在实例的_provided上;因为provide的用法,可以是一个函数也可以是一个对象;
// // 源码位置 src/core/instance/inject.js
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
执行钩子created函数
上面已经介绍过了callHook,created也是通过callHook执行的;
callHook(vm, 'created')
created钩子的执行,标志着初始化阶段完成,下面代码的执行就进入了模板编译阶段;
模板编译编译阶段
initMixin方法中最后执行了vm.$mount(vm.$options.el)
;这个的执行表示进入了模板编译阶段;
在执行$mount之前,先了解下vue有两个版本,分别是运行时版本(vue.runtime.js)和完整版(vue.js);它们的区别就是完整版包含模板编译,而运行时版本不包含模板编译;
运行时版为什么不包含模板编译?
因为运行时版本,模板的编译是在代码构建的时候完成的,通过vue-loader或vueify插件在构建的时候把.vue文件进行了编译;
两个版本使用的场景
完整版:当在选项中使用template选项的时候,并且传递了字符串dom;此时就会用到vue中的模板编译进行编译;
new Vue({
template: "<div>哈哈</div>"
})
运行时版:使用render函数去定义渲染过程
new Vue({
render(h){
return h(App)
}
})
模板编译需要消耗一定的性能,因此在构建阶段就完成模板编译,可以提高性能;并且运行时的源码体积要比完整版的小约30%;实际工作中我们使用webpack或其他构建工具进行项目的构建,此时会使用vue-loader插件进行模板编译,因此构建出的代码不仅体积小而且也提高了性能;
解析$mount
$mount其实也有两个版本,分别也是运行时版和完整版,运行时版本中没有模板编译的代码,而完整版中有;
- 运行时版本的$mount
// 源码位置 src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
首先判断有没有el并且是不是在浏览器环境下,如果是则通过query获取到对应的dom元素,否则就是undefined;再通过mountComponent进行挂载(在下面的挂载阶段进行解析);此函数中没有模板编译的代码;
// 源码位置 src/platforms/web/util/index.js
export function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}
判断当前元素是不是字符串,是字符串就通过查询选择器进行获取返回,否则直接返回这个元素(默认为dom元素)
- 完整版本中的$mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取到元素
el = el && query(el)
// 如果传递的el是body或html就会进行警告,并且不再往下执行,因为vue会把el中的内容进行替换
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
const options = this.$options
// 没有render函数,就获取template进行编译
if (!options.render) {
// 拿到template
let template = options.template
// 目标存在
if (template) {
// 如果是字符串
if (typeof template === 'string') {
// 有id
if (template.charAt(0) === '#') {
// 获取到id对应的元素作为模板
template = idToTemplate(template)
}
// 如果是节点
} else if (template.nodeType) {
// 直接获取到其中的内容
template = template.innerHTML
} else {
// 没有模板直接返回this
return this
}
} else if (el) { // 如果没有模板,直接获取元素上的内容作为模板
template = getOuterHTML(el)
}
if (template) {
// 编译模板转成render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
首先保存之前的$mount
方法,保存的这个$mount
其实是运行时的$mount
,因为不管是否进行模板编译,下个阶段都会进入挂载阶段,而上面运行时的$mount
中只有执行了挂载的方法,因此在这里保存运行时的这个方法,以便下面调用,而无需再写重复的代码;代码中首先获取了el对应的dom元素和options选择;
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取到元素
el = el && query(el)
const options = this.$options
}
接着判断options中是否配置了render函数,如果有配置表示不需要进行模板编译,否则就获取到template,如果template存在,就判断它是否是字符串,如果是字符串就判断是否是#开头,是就表示是id选择器,直接通过id选择器获取到对应的dom元素;如果不是字符串是节点元素,就直接获取到innerHTML作为模板,既不是字符串也不是节点那么直接返回this;如果没有传递Template,就判断是否有el,有el就获取el的outerHTML作为模板,获取到模板之后通过compileToFunctions函数把模板转成render函数;最后执行mount进入挂载阶段;compileToFunctions在模板编译阶段进行了解析;
// 没有render函数,就获取template进行编译
if (!options.render) {
// 拿到template
let template = options.template
// 目标存在
if (template) {
// 如果是字符串
if (typeof template === 'string') {
// 是否是id选择器
if (template.charAt(0) === '#') {
// 获取到id对应的元素作为模板
template = idToTemplate(template)
}
// 如果是节点
} else if (template.nodeType) {
// 直接获取到其中的内容
template = template.innerHTML
} else {
// 没有模板直接返回this
return this
}
} else if (el) { // 如果没有模板,直接获取元素上的内容作为模板
template = getOuterHTML(el)
}
if (template) {
// 编译模板转成render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
idToTemplate获取到元素的innerHTML
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
getOuterHTML获取到元素的outerHTML,如果元素不存在outerHTML,就通div进行包裹一层;
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
挂载和更新阶段
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 没有render就默认赋值一个函数,此函数是创建空的注释Vnode函数
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
// 执行beforeMount钩子
callHook(vm, 'beforeMount')
let updateComponent
// 定义一个updateComponent函数
updateComponent = () => {
// 执行_update进行新旧节点的对比更新,_render的执行会获取到新的vnode
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实例,对数据进行监控,当数据变化的时候执行更新视图的依赖updateComponent
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
}
代码的前半部分是挂载阶段,后半部分是已挂载(更新)阶段;
- 挂载阶段 判断有没有render,如果没有就初始一个创建空的注释vnode函数; 执行beforeMount钩子
// 没有render就默认赋值一个创建空的注释Vnode函数
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
// 执行beforeMount钩子
callHook(vm, 'beforeMount')
接着定义了一个函数,这个函数内部执行了_update方法,_update函数传递了_render方法的执行,_render方法的执行就会执行模板编译生成的render函数(查看模板编译篇),从而可以得到模板的虚拟节点,_update这个方法会执行patch进行新旧节点的对比更新(diff算法篇);(此时就把模板编译成的render函数和diff算法联系在一起了)
let updateComponent
// 定义一个updateComponent函数
updateComponent = () => {
// 执行_update进行新旧节点的对比更新,_render的执行会获取到新的vnode
vm._update(vm._render(), hydrating)
}
以上就是挂载阶段,挂载阶段就是把新的模板渲染到页面上;下面就是更新阶段;每个组件都会创建一个watcher实例作为渲染watcher,用于模板中相关数据变化的时候进行依赖收集和触发(视图的更新);
// 创建watcher实例,对数据进行监控,当数据变化的时候执行相应的依赖
new Watcher(vm, updateComponent, noop, {
before () { // 执行完watcher依赖之后会执行这个回调,触发beforeUpdate钩子
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
大概流程: new Watcher的时候就会执行一次updateComponent方法,此方法内部就会执行_update方法和_render方法;_render内部又执行了render函数生成对应的Vnode;render函数的执行就会使得模板中使用到的数据进行获取,从而这些数据会收集此时的Watcher作为依赖;当模板中的数据变化的时候就会执行这个watcher中的update方法,从而再次执行updateComponent函数,又会执行_render函数从而模板中就会读取最新的数据进行显示;_render函数执行完成之后会把返回的Vnode作为参数传递给_update函数,_update函数内部会获取到新旧的虚拟节点作为参数传递给patch方法。执行patch方法进行新旧节点对比更新;
最后调用mounted钩子,并且返回当前实例
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
面试题:
- created到beforeMount到mounted之间做了什么? created到beforeMount阶段:如果需要模板编译,那么就会进行模板编译(render->template->el),把字符串模板转成ast抽象语法树,并且根据抽象语法树转成render函数;如果不需要模板编译,那么只通过el获取到对应的dom元素; beforeMount到mounted阶段:给每个组件创建一个watcher,并且执行render函数生成对应的虚拟节点,通过patch中的diff算法进行对比更新显示到页面上;因此beforeMount的时候执行模板编译完成生成了render函数,此时还没创建真实的dom,因此不能进行操作真实的dom;而mounted的时候模板已经生成了真实的dom并且挂载到了页面上,因此可以操作真实的dom;
销毁阶段
销毁阶段主要是把当前实例从父级中进行删除,并且把实例上所有的依赖和事件监听器进行移除;
// 源码位置 src/core/instance.lifecycle.js
Vue.prototype.$destroy = function () {
const vm: Component = this
// 判断是否正在被销毁,如果是就直接返回
if (vm._isBeingDestroyed) {
return
}
// 执行beforeDestroy钩子
callHook(vm, 'beforeDestroy')
// 设置正在被销毁
vm._isBeingDestroyed = true
// remove self from parent
// 获取到当前实例的父级
const parent = vm.$parent
// 如果父级存在并且父级没有被销毁并且不是抽象组件,把当前实例从父级上进行删除
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
// 其他数据所依赖的当前实例的watcher进行删除
if (vm._watcher) {
vm._watcher.teardown()
}
// 把当前实例所依赖的其他watcher进行删除
let 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--
}
// 给当前实例上添加_isDestroyed属性来表示当前实例已经被销毁
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
// 将实例的VNode树设置为null
vm.__patch__(vm._vnode, null)
// fire destroyed hook
// 执行destoryed钩子
callHook(vm, 'destroyed')
// turn off all instance listeners.
// 关闭事件监听
vm.$off()
// remove __vue__ reference
// __vue__指向null
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
判断当前实例是否正在被销毁,防止重复操作;再执行beforeDestory钩子;
const vm: Component = this
// 判断是否正在被销毁,如果是就直接返回
if (vm._isBeingDestroyed) {
return
}
// 执行beforeDestroy钩子
callHook(vm, 'beforeDestroy')
设置正常被销毁的flag,获取到当前实例的父级,并且从父级上删除当前实例
// 设置正在被销毁
vm._isBeingDestroyed = true
// remove self from parent
// 获取到当前实例的父级
const parent = vm.$parent
// 如果父级存在并且父级没有被销毁并且不是抽象组件,把当前实例从父级上进行删除
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
当前实例可能被其他数据所依赖,也可能依赖其他实例的watcher;因此需要删除这两项依赖;当前实例所依赖的其他watcher是在initState
函数中进行收集的;
// 其他数据所依赖的当前实例的watcher进行删除
if (vm._watcher) {
vm._watcher.teardown()
}
// 把当前实例所依赖的其他watcher进行删除
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
添加当前实例被销毁的标志,并且把当前实例的_vnode设置为null;
// 移除实例内响应式数据的引用
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// 给当前实例上添加_isDestroyed属性来表示当前实例已经被销毁
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
// 将实例的VNode树设置为null
vm.__patch__(vm._vnode, null)
最后执行destoryed钩子,并且移除实例上的所有事件
// 执行destoryed钩子
callHook(vm, 'destroyed')
// turn off all instance listeners.
// 关闭事件监听
vm.$off()
// remove __vue__ reference
// __vue__指向null
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
面试题
-
nextTick的原理?在哪里被使用? 原理:nextTick内部通过异步任务的方式(通过以下顺序判断是否存在,从而异步执行回调,promise, MutationObserver,setImmediate,setTimeout)去遍历执行回调函数;多个nextTick的调用,最终都会被合并成一个,放在一个数组中执行; 使用:vue中视图的重新渲染就是异步的,在数据修改执行watcher的run方法都是通过nextTick进行异步执行的,多次修改数据只会渲染一次视图;因此可以在nextTick中获取到修改之后的dom;
-
以下输出什么?为什么?
<div id="app">
{{ name }}
</div>
const vm = new Vue({
el: '#app',
data: {
name: 1
}
})
vm.nextTick(() => {
console.log(vm.$el.innerHTML)
})
vm.name = 2
输出:1;
nextTick中是按照写的先后顺序,把回调函数存储到一个数组中,再异步遍历执行这个数组中的回调;所以以上先执行nextTick把它里面的回调push到数组中,再执行vm.name=2,再把渲染视图的watcher在push到数组中,最后异步遍历这个数组执行回调,因此输出的还是1;
那么怎样才能输出2呢? 把vm.name=2放在nextTick的上面,这样存入数组的顺序就是显示渲染watcher再是nextTick的回调;
- vue中组件之间的传值方式以及区别?
- props: 父子之间的数据传递,vue内部把验证之后的props存储到当前实例的_props上,并且通过definedReactive方法进行定义的因此都是响应的,并且会把_props上的属性代理到当前实例上,组件在渲染的时候直接从实例上获取数据的值;
- emit: 子组件触发父组件中的方法,在组件创建虚拟节点的时候会把所有的事件绑定到listeners上,通过发布订阅者模式在
$on
进行绑定事件,通过$emit
进行触发事件 - events Bus: 通过中间实例的方式进行代理,也是发布订阅者模式
$parent $children
: 创建子组件的时候把父组件的实例传递进去,在初始化组件的时候就会构建组件之后的父子关系,通过$parent
获取到父组件的实例,通过$children
获取到子组件的实例;- provide inject:在父组件中通过provide提供给子组件数据,在子组件中通过inject获取到提供的数据;在子组件中通过递归向上查找数据;
$attrs
和$listeners
:$attrs
:获取到组件上所有的属性,不包含props中声明的、class、style;可以通过v-bind="$attrs"
传入内部组件;$listeners
: 包含了父作用域中的 (不含.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件;- vuex
-
v-if和v-for哪个优先级高? v-for的优先级高,在编译的时候会把v-for渲染成_l函数,v-if渲染成三元表达式判断;因此它两不能在一个标签中使用;通过计算属性进行处理再遍历;
-
v-if和v-show的区别? v-if:在模板编译时生成render函数的时候,通过三元表达式进行判断是否生成虚拟dom;因此它直接控制元素是否被渲染; v-show: 在模板编译的时候生成指令,指令中通过Css的display=none来控制隐藏,并且保留之前的display的值,如果显示直接赋值之前的值;否则就是none; v-show中为什么不用visibility:hidden或opacity?因为它们虽然可以让元素不显示,但是元素在文档中还是占据位置的;
-
v-model的原理?
放在input上可以实现双向绑定,不同类型的input,vue内部做了不同的处理,对于文本类型的,在编译的时候会被编译成value+input+指令,input中修改数据就会把值赋值给value实现数据的双向绑定,但是对于中文input内部不会立即赋值,而是监听输入完之后通过触发自定义事件修改value的值;指令就是用来监听是否输入完和触发自定义事件的;对于checked是通过change事件来实现双向绑定; 放在组件上,在编译的时候会创建一个model对象,对象中的prop和event默认是value和input,如果有自定义的model就会采用自定义的model, 组件内部通过emit这个event事件从而可以修改value的值;
- Vue.use的原理?它是用来做什么的?
原理:内部先从缓存中获取,如果有直接返回当前实例表示已经安装过,如果缓存中没有,就判断传递进来的参数的install是不是一个函数,如果是就执行这个函数并且把当前实例作为第一个参数传递进去;如果不是那么判断传递进来的参数是不是一个函数,如果是直接执行这个函数并且把当前实例作为第一个参数传递进去;最后返回当前实例并且存入缓存中; 作用:用来给当前vue添加插件,把当前vue传递给插件中;使得插件中使用的vue和当前使用的vue是同一个;
- 组件中写name属性的作用?
可以在组件内部通过name来获取到组件自身,从而循环调用组件自身;也可以通过递归的形式在内层或外层组件中通过name找到对应的组件;从而可以操作组件上的方法或属性实现组件之间的通信;
- vue中slot是如何实现的?什么时候使用它?
普通插槽:
- 在父组件模板编译转成渲染函数的时候,会把父组件中的内容作为此渲染函数的children;
- 执行渲染函数生成虚拟节点的时候,chilren会被放在虚拟节点的componentOptions属性上;
- 把虚拟节点的componentOptions属性的children值放到组件的
$options._renderChildren
中 - 把插槽名称和插槽做一个key和value的对应,如果没有名称就是defalut作为名称,并且放在
$slots
上,把$slots
上的内容通过函数进行包裹放在$scopeSlots
上 - 子组件生成渲染函数的时候,它内部的solt元素会被_t函数进行处理,并且会把插槽名传递进去,执行渲染函数的时候此时就会去
$scopeSlots
上找到对应的虚拟节点
整体流程:
// 创建组Com件
<div><solt></solt></div>
// 父组件中使用
<Com><div>我是插槽中的内容</div></Com>
对应的渲染函数
with(this){
return _c('div',null,[_t('default')],2)
}
with(this){
return _c('Com',[_c('div',[_v(_s(msg))])])
}
由上可以看出,com中的内容作为children,在com渲染函数执行
的时候就执行了,因此它的作用域是在父组件中渲染的
具名插槽:
在普通插槽的基础上,具名插槽就是多了名称;通过名称从$scopeSlots
中找到对应渲染函数进行执行;
作用域插槽:
- 模板编译生成渲染函数的时候把组件中的内容作为此元素渲染函数的属性,属性为scopeSlots;
- 通过插槽名和插槽进行对应,插槽放在一个函数中,并且把插槽的作用域属性作为参数,最后把对应起来的对象赋值给$scopeSlots;
- 子组件模板编译成渲染函数的时候,slot元素会被_t函数进行处理,并且把插槽名和作用域值传递进去;在_t函数中通过插槽名在$scopeSlots对象上找到对应的渲染函数,执行这个函数并且把作用域值传递进去
- 因此作用域插槽的作用域是在子组件中渲染
// 创建组Com件
<div><solt :msg="msg"></solt></div>
// 父组件中使用
<Com><div slot-scope="{msg}">{{ msg }}</div></Com>
最后编译成的渲染函数
with(this){
return _c('div',null,[_t('default',null,{'msg':msg})],2)
}
with(this){
return _c('com',{scopeSlots:_u([{key:'default',fn:function({msg}){
return _c('div',{},[_v(_s(msg))])
}}]) })
}
由上可以看出组件中的内容作为属性,而不是children,因此在com渲染函数执
行的时候是不会进行执行的;而是在子组件渲染的时候通过调用这个函数进行执
行,因此它的作用域是在子组件中渲染的;
普通插槽和作用域插槽的区别:
- 普通插槽中的内容在模板编译之后,生成渲染函数的时候作为父组件的children,并且在此渲染函数执行的时候也会进行执行;
- 作用域插槽中的内容是在模板编译之后,生成渲染函数的时候作为父组件的属性,并且内容的渲染函数放在一个函数中,并且把作用域数据作为参数传递进去;通过插槽名和函数映射出一个对象,此对象放在实例的
$scopeSlots
上,在子组件的渲染函数执行的时候会通过_t中的插槽名称到$scopeSlots
上找到对应的渲染函数进行执行;
10.keep-alive的使用和原理
- 它在路由和动态组件中使用,被它包裹的会有缓存;
- keep-alive会默认缓存加载过的组件对应的实例,内部采用了LRU最近最少使用的缓存策略
- 下次组件切换加载的时候,会找到对应的缓存的节点上的$el来插入,无需通过虚拟节点创建真实节点的流程;
- 更新和销毁会触发actived和deactived钩子
-
自定义指令的原理,使用场景? 自定义指令其实就是在规定的五个(bind,inserted,update,componentUpdate,unbind)生命钩子中做一些dom的操作,自定义指令是用来操作dom的; 应用场景:可以应用在图片懒加载和防抖等功能; 原理:在这个dom创建到销毁的各个阶段执行对应的钩子函数,并且传递一个数据供开发者使用;
-
vue的事件有哪些修饰符?原理? 修饰符: stop,prevent,capture,once等和键盘事件; 原理:在模板编译的时候直接放在事件内部,然后返回并且执行具体的事件;
-
vue中组件的data为什么是一个函数? 根组件可以是一个对象,因为根组件只会被new一次;而子组件必须是一个函数,因为子组件创建之后,可以被多次使用也就是被new好几次,每次创建子组件的实例,如果是对象的话,那么这些实例都共用一个data对象,会造成互相响应;而如果是一个函数,那么每次创建子组件的时候都会通过这个函数返回一个新的对象,从而不会造成多个实例共用一个对象的情况,因此就不会有相互影响的结果;
-
computed和watcher的区别? 相同点:底层都是一个watcher; 不同点:computed默认不会立即执行,在使用的时候才会执行,并且会进行缓存,它是同步执行的;watcher可以设置立即执行,并且watcher的回调是异步的; 原理上区别:computed内部是一个watcher,并且会把watcher进行缓存,再次使用的时候,获取到缓存的watcher,通过dirty属性来判断computed内部使用的数据有没有变化,如果没有变化直接返回watcher上的value,如果变化了就执行watcher的一个方法获取到最新的值,标记dirty为false;
-
$set
方法的作用?原理? 作用:当我们直接通过索引修改数组中的数据的时候,vue中是没法监听到的,因此模板也不会进行更新;因此需要使用$set
方法; 原理:内部判断如果是对象就通过splice来新增或修改此项的属性;splice是vue内部重写了此方法因此可以监听到数据的改变从而更新视图;如果是对象,如果是已有属性那么直接修改对象上的此属性,如果不是已有属性,判断当前目标对象是否是响应式的如果不是直接修改对象上的此属性,如果是响应式的,通过defineReactive方法给目标对象添加此属性,把属性设置为响应式的,最后调用dep上的notify方法进行更新; -
Vue中为什么需要虚拟dom? 虚拟dom就是通过对象的形式来描述一个dom节点,因此它最大的好处就是通过此对象可以应用在不同的平台中进行转换成对应的真实节点;并且虚拟dom更方便实现diff算法,来对比差异无需直接操作真实dom,将dom之间的不同进行更新,因此可以提高性能。
-
vue中的diff算法 diff算法中主要通过递归,双指针和平级的方式进行比较; 先比较是否是同一个节点(标签名和key相同),如果是同一个接着比较属性是否相同,不同就更新属性;如果不是同一个节点,直接使用新节点进行替换; 如果是同一个节点就比较它的子元素,如果一方有子元素一方没有,则进行删除或新增;如果两方都有子元素;通过双指针进行头头,尾尾比较,如果满足以上的一种情况直接进行递归处理,移动双指针;不满足进行交叉比较,旧后和新前相同,那么把旧后移动到旧前的前面,如果旧前和新后相同,那么就把旧前移动到旧后的后面,如果以上都不满足,那么就会在旧节点的头尾之间通过key和索引做一个映射表,把新节点的key在这个映射表中进行查找,如果存在就移动这个节点到头部,否则就插入到头部,最后再删除多余的节点; 缺陷:会进行多移动的操作,在vue3中进行了优化;
-
vue中可以通过数据劫持精准的探测到数据的变化,为什么还需要diff算法比对差异? 如果不这样做,就得给每个数据都添加一个watcher,相比diff算法更耗性能;因此就使用了diff算法和响应式数据给每个组件添加一个watcher;组件中数据变化的时候通过dep通知到数据收集的watcher来更新视图,然后通过diff算法对比出差异进行渲染;
-
请说下vue中的key的作用和原理? 在diff算法中,通过key和标签名等属性来比较是否是同一个节点,如果每次key的改变,那么diff算法中就认为它不是相同的节点,直接进行替换,因此key必须是唯一的并且不能为数组或对象的索引;
-
你对vue组件化的理解? 组件的优点:组件可以进行复用,合理的规划组件在更新的时候只更新变动的组件;但不为拆分太细,因为一个组件就是一个watcher; 组件化可以使得页面结构更加清晰;
-
vue中组件渲染流程?
- 模板解析阶段:在通过render函数生成虚拟dom的时候,会进行判断是组件还是原生的标签,如果是组件就会创建组件对应的虚拟节点,在创建组件的虚拟节点的时候,会去
$options
上的components中找出对应的组件,再通过父组件的extend方法创建出组件的构造函数;并且给当前组件的虚拟dom的属性对象添加init钩子,钩子内部创建了组件的实例并且把实例赋值到虚拟dom的一个属性上,然后调用实例的$mount
方法进行创建真实组件; - 挂载阶段:根据虚拟dom创建真实dom的时候,如果虚拟dom上存在组件的实例,表示是创建组件,获取到属性中的init钩子进行执行,此步就是上面分析的创建组件的实例,调用mount进行创建组件的真实dom,然后挂载到父级元素中;
-
组件的更新流程? 组件更新,会在patch函数中调用prepatch方法,此方法会复用组件(旧节点上的组件实例),再比较属性(把传递进来的属性和在props中定义的进行验证和取值),事件(新旧节点上的事件进行新增或删除或修改的比较)和插槽(通过插槽的名称把插槽放在一个对象中,再强制更新模板)
-
异步组件的作用和原理? 异步组件的定义
// 第一种方式 回调函数
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
// 第二种方式 promise
Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
// 第三种方式 高级玩法
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
作用:大组件渲染比较慢,可以使用异步加载组件的方式,先加载一个loading组件; 原理:
- 通过cid判断是否是异步组件,异步组件就创建resolve和reject方法,执行并传递resolve和reject到异步组件函数中,(还会根据返回的结果判断是promise,有无component等进行处理),最后判断有没有加载属性,如果有就返回加载组件的构造器,否则就返回当前异步组件的构造器,此时可能还未执行resolve或reject,因此返回的组件的构造器是undefined;
- 函数外部判断返回的结果如果是undefined,那么就创建一个空的虚拟节点表示占位组件;
- 生成加载组件的虚拟节点
- patch创建真实节点,此时加载组件就会渲染到页面上
- 如果此时异步组件执行了resolve方法,那么在Resolve函数内部会创建异步组件的构造器,并且通过$forceUpdate进行强制更新,此时就会执行watcher的update方法更新视图;把异步组件渲染到页面上
-
函数式组件的优势和原理? 函数式组件中没有this,没有data,没有生命周期,不需要创建对应的Watcher,直接调用Render函数拿到返回结果进行渲染,因此它的性能好;
-
props的原理? 模板编译的时候会解析出标签上的属性,再和props中的属性进行验证和分类,通过defineReactive方法定义到_props上,最后代理到当前组件的实例上;
-
emit的原理? 在创建虚拟节点的时候会把自定义事件存储到listeners上,原生的事件放在nativeon中,初始化事件的时候会把这些事件存储到当前实例的_events上,emits的时候就会通过事件名找出对应的事件执行;
-
ref的原理? 在通过虚拟节点创建真实dom的时候,在插入到页面之前会处理data属性,通过生命周期钩子的合并执行来处理ref;如果当前是组件就把组件的实例赋值给ref,否则就是dom元素,把dom元素赋值给Ref;
-
inject和provide的原理? provide:把provide放在当前实例的_provided上; inject: 遍历inject对象把所有的属性通过递归的形式,一层一层的向上查找此属性,如果找到就把值存储起来,如果没有找到就inject中当前属性中的default默认值,进行存储,最后遍历存储的对象全部存储到当前实例上并且设置为不是响应式的;
转载自:https://juejin.cn/post/7239191802506756157