Vue2最后一个正式版本2.7.16
https://github.com/vuejs/vue/tags
v-show和v-if的异同
共同点
- 能控制元素在页面是否显示
区别
控制手段
- v-show,当表达式都为false时,隐藏则是为该元素添加css–display:none,dom元素依旧还在
- v-if显示隐藏是将dom元素整个添加或删除
编译层面
-
v-if在编译的时候会被转化为三元表达式,条件不满足则不渲染此节点
-
源码位置:src\compiler\codegen\index.ts
-
function genIfConditions( conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string ): string { if (!conditions.length) { // 没条件返回空元素 return altEmpty || '_e()' } const condition = conditions.shift()! // 如果有表达式 if (condition.exp) { // 三元表达式 return `(${condition.exp})?${genTernaryExp( condition.block )}:${genIfConditions(conditions, state, altGen, altEmpty)}` } else { return `${genTernaryExp(condition.block)}` } // v-if with v-once should generate code like (a)?_m(0):_m(1) function genTernaryExp(el) { return altGen ? altGen(el, state) : el.once ? genOnce(el, state) : genElement(el, state) } } export function genFor( el: any, state: CodegenState, altGen?: Function, altHelper?: string ): string { const exp = el.for const alias = el.alias const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' if ( __DEV__ && state.maybeComponent(el) && el.tag !== 'slot' && el.tag !== 'template' && !el.key ) { state.warn( `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` + `v-for should have explicit keys. ` + `See https://v2.vuejs.org/v2/guide/list.html#key for more info.`, el.rawAttrsMap['v-for'], true /* tip */ ) } el.forProcessed = true // avoid recursion return ( `${altHelper || '_l'}((${exp}),` + `function(${alias}${iterator1}${iterator2}){` + `return ${(altGen || genElement)(el, state)}` + '})' ) }
-
-
v-show原理是会被编译成一个指令,有transition就执行transition,没有就直接设置display属性
-
源码位置:src\platforms\web\runtime\directives\show.ts
-
import VNode from 'core/vdom/vnode' import type { VNodeDirective, VNodeWithData } from 'types/vnode' import { enter, leave } from 'web/runtime/modules/transition' // recursively search for possible transition defined inside the component root function locateNode(vnode: VNode | VNodeWithData): VNodeWithData { // @ts-expect-error return vnode.componentInstance && (!vnode.data || !vnode.data.transition) ? locateNode(vnode.componentInstance._vnode!) : vnode } export default { bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) { vnode = locateNode(vnode) const transition = vnode.data && vnode.data.transition // 取原来的display值,保存起来 const originalDisplay = (el.__vOriginalDisplay = el.style.display === 'none' ? '' : el.style.display) if (value && transition) { vnode.data.show = true enter(vnode, () => { el.style.display = originalDisplay }) } else { // 如果传来的值,则把原来的值放上去,否则就是none el.style.display = value ? originalDisplay : 'none' } }, update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) { /* istanbul ignore if */ if (!value === !oldValue) return vnode = locateNode(vnode) const transition = vnode.data && vnode.data.transition if (transition) { vnode.data.show = true if (value) { enter(vnode, () => { el.style.display = el.__vOriginalDisplay }) } else { leave(vnode, () => { el.style.display = 'none' }) } } else { el.style.display = value ? el.__vOriginalDisplay : 'none' } }, unbind( el: any, binding: VNodeDirective, vnode: VNodeWithData, oldVnode: VNodeWithData, isDestroy: boolean ) { if (!isDestroy) { el.style.display = el.__vOriginalDisplay } } }
-
生命周期触发
- v-show 由false变为true的时候不会触发组件的生命周期
- v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子
- 由true变为false的时候触发组件的beforeDestory、destoryed方法
性能消耗
- v-show不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换,v-show 有更高的初始渲染开销
- v-if 有更高的切换开销
使用场景
- 如果需要非常频繁地切换,则使用 v-show 较好
- 如果在运行时条件很少改变,则使用 v-if 较好
Vue2响应式数据的理解
https://blog.csdn.net/qq_40588441/article/details/140337629?spm=1001.2014.3001.5501
- 对象内部通过
defineReactive
方法,使用Object.defineProperty
将属性进行劫持(只会劫持已经存在的属性)- 消耗大性能差,不存在的属性,如果新增的话不能重新渲染视图
- 多层对象是通过递归来实现劫持。(所以会有性能问题,建议减少嵌套层级)
- 数组则是通过重写数组方法来实现。
- 数组通过索引进行修改 或者 修改数组的长度,响应式不生效(可以通过$set解决[本质是splice方法])
源码位置:src\core\observer\index.ts
// 定义响应式数据
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
const dep = new Dep()
// 如果不可以配置直接return
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
// 对数据进行观测
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 取数据时进行依赖收集
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
// 让对象本身进行依赖收集
if (childOb) {
// {a:1} => {} 外层对象
childOb.dep.depend()
// 如果是数组 {arr:[[],[]]} vm.arr取值只会让arr属性和外层数组进行收集
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
return dep
}
Vue2中如何检测数组变化?
- 数组考虑性能原因没有用
defineProperty
对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse
)方法。 - 数组中如果是对象数据类型也会进行递归劫持
- 数组的索引和长度变化是无法监控到的
源码位置:src\core\observer\array.ts
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 新增的数据需要进行观测
if (inserted) ob.observeArray(inserted)
// notify change
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {
ob.dep.notify()
}
return result
})
})
Vue2中如何进行依赖收集?
- 每个属性都拥有自己的
dep
属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新 - 默认在初始化时会调用render函数,此时会触发属性依赖收集
dep.depend
- 当属性发生修改时会触发
watcher
更新dep.notify()
如何理解Vue2中模板编译原理
问题核心:如何将template转换成render函数 ?
-
1.将template模板转换成
ast
语法树 -parserHTML
-
2.对静态语法做静态标记 -
markUp
diff
来做优化的 静态节点跳过diff
操作 -
3.重新生成代码 - `codeGen`
src/compiler/index.js:11
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 1.解析ast语法树
if (options.optimize !== false) {
optimize(ast, options) // 2.对ast树进行标记,标记静态节点
}
const code = generate(ast, options) // 3.生成代码
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
Vue2生命周期钩子是如何实现的
- Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。
- 内部会对钩子函数进行处理,将钩子函数维护成数组的形式
src/core/instance/init.js:38
初始化合并
src/core/util/options.js:388
合并选项
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
}
Vue2的生命周期方法有哪些?一般在哪一步发送请求及原因
beforeCreate
在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。- created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMount
在挂载开始之前被调用:相关的 render 函数首次被调用。mounted
el 被新创建的vm.$el
替换,并挂载到实例上去之后调用该钩子。beforeUpdate
数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。updated
由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用。destroyed
Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。keep-alive
(activated 和 deactivated)
在哪发送请求都可以,主要看具体你要做什么事
Vue.mixin的使用场景和原理
-
Vue.mixin
的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions
方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。 -
mixin中有很多缺陷 “命名冲突问题”、“依赖问题”、“数据来源问题”
-
源码位置:src\core\global-api\mixin.ts
import type { GlobalAPI } from 'types/global-api' import { mergeOptions } from '../util/index' export function initMixin(Vue: GlobalAPI) { Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this } }
Vue组件data为什么必须是个函数?
- 每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响
function Vue() {}
function Sub() { // 会将data存起来
this.data = this.constructor.options.data;
}
Vue.extend = function(options) {
Sub.options = options;
return Sub;
}
let Child = Vue.extend({
data: { name: 'hs' }
});
// 两个组件就是两个实例, 希望数据互不干扰
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = 'jw';
console.log(child2.data.name);
data的合并策略,源码位置:src\core\util\options.ts
export function mergeDataOrFn(
parentVal: any,
childVal: any,
vm?: Component
): Function | null {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn() {
return mergeData(
isFunction(childVal) ? childVal.call(this, this) : childVal,
isFunction(parentVal) ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn() {
// instance merge
const instanceData = isFunction(childVal)
? childVal.call(vm, vm)
: childVal
const defaultData = isFunction(parentVal)
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
// 合并2个对象
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): Function | null {
// 组件在合并时并没有产生实例,所以会校验类型
if (!vm) {
if (childVal && typeof childVal !== 'function') {
__DEV__ &&
warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
nextTick在哪里使用?原理是?
nextTick
中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。- 可用于获取更新后的 DOM。
- Vue中数据更新是异步的,使用
nextTick
方法可以保证用户定义的逻辑在更新之后执行。
源码位置:src\core\util\next-tick.ts
computed和watch区别
- computed和watch都是基于Watcher来实现的
- computed属性是具备缓存的,依赖的值不发生变化(dirty标识是否已经读取过改值),对其取值时计算属性方法不会重新执行
- watch则是监控值的变化,当值发生变化时调用对应的回调函数
源码位置:src\core\instance\state.ts
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
/// 如果值是脏的 进行求值操作
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
if (__DEV__ && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
target: this,
type: TrackOpTypes.GET,
key
})
}
// 让计算属性所依赖的属性 收集渲染watcher
watcher.depend()
}
return watcher.value
}
}
}
Vue.prototype.$watch = function (
expOrFn: string | (() => any),
cb: any,
options?: Record<string, any>
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 标记为用户watcher
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn() {
watcher.teardown()
}
}
Vue.set方法是如何实现的
- 我们给对象和数组本身都增加了
dep
属性 - 当给对象新增不存在的属性则触发对象依赖的watcher去更新
- 当修改数组索引时我们调用数组本身的splice方法去更新数组
源码位置:src\core\observer\index.ts
export function set(
target: any[] | Record<string, any>,
key: any,
val: any
): any {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${target}`
)
}
if (isReadonly(target)) {
__DEV__ && warn(`Set operation on key "${key}" failed: target is readonly.`)
return
}
const ob = (target as any).__ob__
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
// when mocking for SSR, array methods are not hijacked
if (ob && !ob.shallow && ob.mock) {
observe(val, false, true)
}
return val
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
if ((target as any)._isVue || (ob && ob.vmCount)) {
__DEV__ &&
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock)
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ADD,
target: target,
key,
newValue: val,
oldValue: undefined
})
} else {
// 通知视图更新
ob.dep.notify()
}
return val
}
Vue为什么需要虚拟DOM
- Virtual DOM就是用
js
对象来描述真实DOM,是对真实DOM的抽象 - 由于直接操作DOM性能低但是
js
层的操作效率高,可以将DOM操作转化成对象操作,最终通过dif
f算法比对差异进行更新DOM(减少了对真实DOM的操作)。 - 虚拟DOM不依赖真实平台环境从而也可以实现跨平台。
源码路径:src\core\vdom\create-element.ts
import config from '../config'
import VNode, { createEmptyVNode } from './vnode'
import { createComponent } from './create-component'
import { traverse } from '../observer/traverse'
import {
warn,
isDef,
isUndef,
isArray,
isTrue,
isObject,
isPrimitive,
resolveAsset,
isFunction
} from '../util/index'
import { normalizeChildren, simpleNormalizeChildren } from './helpers/index'
import type { Component } from 'types/component'
import type { VNodeData } from 'types/vnode'
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data as any).__ob__)) {
__DEV__ &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\n` + 'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
// support single function children as default scoped slot
if (isArray(children) && isFunction(children[0])) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (
__DEV__ &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== 'component'
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if (
(!data || !data.pre) &&
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
// direct component options / constructor
vnode = createComponent(tag as any, data, context, children)
}
if (isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
function applyNS(vnode, ns, force?: boolean) {
vnode.ns = ns
if (vnode.tag === 'foreignObject') {
// use default namespace inside foreignObject
ns = undefined
force = true
}
if (isDef(vnode.children)) {
for (let i = 0, l = vnode.children.length; i < l; i++) {
const child = vnode.children[i]
if (
isDef(child.tag) &&
(isUndef(child.ns) || (isTrue(force) && child.tag !== 'svg'))
) {
applyNS(child, ns, force)
}
}
}
}
// ref #5318
// necessary to ensure parent re-render when deep bindings like :style and
// :class are used on slot nodes
function registerDeepBindings(data) {
if (isObject(data.style)) {
traverse(data.style)
}
if (isObject(data.class)) {
traverse(data.class)
}
}
src\core\vdom\vnode.ts
import type { Component } from 'types/component'
import type { ComponentOptions } from 'types/options'
import type { VNodeComponentOptions, VNodeData } from 'types/vnode'
/**
* @internal
*/
export default class VNode {
tag?: string
data: VNodeData | undefined
children?: Array<VNode> | null
text?: string
elm: Node | undefined
ns?: string
context?: Component // rendered in this component's scope
key: string | number | undefined
componentOptions?: VNodeComponentOptions
componentInstance?: Component // component instance
parent: VNode | undefined | null // component placeholder node
// strictly internal
raw: boolean // contains raw HTML? (server only)
isStatic: boolean // hoisted static node
isRootInsert: boolean // necessary for enter transition check
isComment: boolean // empty comment placeholder?
isCloned: boolean // is a cloned node?
isOnce: boolean // is a v-once node?
asyncFactory?: Function // async component factory function
asyncMeta: Object | void
isAsyncPlaceholder: boolean
ssrContext?: Object | void
fnContext: Component | void // real context vm for functional nodes
fnOptions?: ComponentOptions | null // for SSR caching
devtoolsMeta?: Object | null // used to store functional render context for devtools
fnScopeId?: string | null // functional scope id support
isComponentRootElement?: boolean | null // for SSR directives
constructor(
tag?: string,
data?: VNodeData,
children?: Array<VNode> | null,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child(): Component | void {
return this.componentInstance
}
}
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
export function createTextVNode(val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode(vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
Vue2中diff
算法原理
- Vue的
diff
算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。 - 1.先比较是否是相同节点 key tag
- 2.相同节点比较属性,并复用老节点
- 3.比较儿子节点,考虑老节点和新节点儿子的情况
- 4.优化比较:头头、尾尾、头尾、尾头
- 5.比对查找进行复用
- Vue3中采用最长递增子序列来实现
diff
优化
源码位置
src/core/vdom/patch.js
:700src/core/vdom/patch.js`:501 比较两个虚拟节点 `patchVnode() src/core/vdom/patch.js`:404 比较两个虚拟节点 `patchChildren()
既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff
检测差异
- 响应式数据变化,Vue确实可以在数据发生变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能。
- 而且粒度过细也会导致更新不精准的问题,所以vue采用了组件级的watcher配合diff来检测差异
请说明Vue中key的作用和原理,谈谈你对它的理解
- Vue在patch过程中通过key可以判断两个虚拟节点是否是相同节点。 (可以复用老节点)
- 无key会导致更新的时候出问题
- 尽量不要采用索引作为key
谈一谈对Vue组件化的理解
- 组件化开发能大幅提高应用开发效率、测试性、复用性等;
- 常用的组件化技术:属性、自定义事件、插槽等
- 降低更新范围,只重新渲染变化的组件
- 组件的特点:高内聚、低耦合、单向数据流
Vue的组件渲染流程
源码位置:src\core\vdom\patch.ts
- 渲染的时候,先父后子
- 卸载的时候,先子后父
- 产生组件虚拟节点 -> 创建组件的真实节点 -> 插入到页面中
1.在渲染父组件时会创建父组件的虚拟节点,其中可能包含子组件的标签
2.在创建虚拟节点时,获取组件的定义使用 Vue. extend
生成组件的构造函数。
3.将虚拟节点转化成真实节点时,会创建组件的实例并且调用组件的 Smount 方法。
4.所以组件的创建过程是先父后子
Vue组件更新流程
属性更新时会触发
patchVnode
方法 -> 组件虚拟节点会调用prepatch
钩子 -> 更新属性 -> 组件更新
Vue中异步组件原理
- 默认渲染异步占位符节点 -> 组件加载完毕后调用
forceUpdate
强制更新
函数组件的优势及原理
-
函数式组件的特性:无状态、无生命周期、无this。但是性能高 正常组件是一个类继承了Vue, 函数式组件就是普通的函数,没有new的过程,也没有
init
、prepatch
src/vdom/create-component.js:163
if (isTrue(Ctor.options.functional)) { // 函数式组件 return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // 处理事件 // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn // 处理原生事件 // install component management hooks onto the placeholder node installComponentHooks(data) // 初始化组件钩子方法
Vue组件间传值的方式及之间区别
props
和$emit
父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的$parent
,$children
获取当前组件的父组件和当前组件的子组件$attrs
和$listeners
A->B->C。Vue 2.4 开始提供了$attrs
和$listeners
来解决这个问题- 父组件中通过
provide
来提供变量,然后在子组件中通过inject
来注入变量。 $refs
获取实例envetBus
平级组件数据传递 这种情况下可以使用中央事件总线的方式vuex
状态管理- …
props实现原理
<my-component a="1" b="2" c="3" @xxx @qqq @click.native></my-component>
src\core\vdom\create-component.js:192
const vnode = new VNode( // 创建组件虚拟节点
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }, // 包含组件的属性及事件
asyncFactory
)
src\core\instance\init.js:36
export 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 // 将属性添加到$options中
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
src\core\instance\state.js
属性的初始化
function initProps (vm: Component, propsOptions: Object) { // propsOptions 校验属性
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.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) { // 如果时根元素,属性需要定义成响应式的
toggleObserving(false)
}
for (const key in propsOptions) {// 用户用户的 props:{}
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value) // 定义到_props中
}
// 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)) {
proxy(vm, `_props`, key) // 将_props代理到实例上
}
}
toggleObserving(true)
}
$on , $emit
<my-component @change="fn" @change="fn" @change="fn"></my-component> // this.$on('change')
<script>
this.$emit('change')
</script>
opts._parentListeners = vnodeComponentOptions.listeners // 用户在组件上定义的事件
src\core\instance\events.js:12
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners) // 更新组件的事件
}
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm // 更新事件,采用add 、 remove方法
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)
}
内部采用的就是发布订阅模式来进行实现
p a r e n t , parent, parent,children
src\core\instance\lifecycle.js:32
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) { // 排除抽象组件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm) // 让父实例记住当前组件实例
}
vm.$parent = parent // 增加$parent属性 指向父实例
vm.$root = parent ? parent.$root : vm
// ...
}
$attrs, $listeners
<my-component a="1" b="2"></my-component> => $vnode.data.attrs = {a:1,b:2}
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 parentData = parentVnode && parentVnode.data // 占位符节点上的数据
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
provide & inject
src\core\instance\inject.js:7
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) { // 将用户定义的provide 挂载到_provided
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) { // inject:[a,b,c]
const result = resolveInject(vm.$options.inject, vm) // 不停的向上查找 inject的属性
if (result) {
toggleObserving(false)
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)
}
}
$ref
src\core\vdom\modules\ref.js:20
export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
const key = vnode.data.ref // 获取ref
if (!isDef(key)) return
const vm = vnode.context
const ref = vnode.componentInstance || vnode.elm // 当前组件的实例 或者 组件的真实节点
const refs = vm.$refs
if (isRemoval) { // 删除ref
if (Array.isArray(refs[key])) {
remove(refs[key], ref)
} else if (refs[key] === ref) {
refs[key] = undefined
}
} else {
if (vnode.data.refInFor) {
if (!Array.isArray(refs[key])) { // 在v-for中是数组
refs[key] = [ref]
} else if (refs[key].indexOf(ref) < 0) {
// $flow-disable-line
refs[key].push(ref)
}
} else {
refs[key] = ref
}
}
}
$attrs
是为了解决什么问题出现的,provide和inject不能解决它能解决的问题吗? v-bind=“
a
t
t
r
s
"
v
−
o
n
=
"
attrs" v-on="
attrs"v−on="listeners”
$attrs
主要的作用就是实现批量传递数据。provide/inject更适合应用在插件中,主要是实现跨级数据传递
v-if
和v-for
哪个优先级更高?
-
在V2当中,v-for的优先级更⾼,⽽在V3当中,则是v-if的优先级更⾼。在V3当中,做了v-if的提 升优化,去除了没有必要的计算,但同时也会带来⼀个⽆法取到v-for当中遍历的item问题,这就需要 开发者们采取其他灵活的⽅式去解决这种问题
-
v-for和v-if不要在同一个标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。
src/compiler/index.js:19
src/compiler/codegen/index.js::56
解析v-if 和 v-for
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // 处理v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // 处理v-if
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
}
v-if
,v-model
,v-for
的实现原理
v-for实现原理
v-for`实现原理 `src/compiler/codegen/index.js:187
export function genFor(
el: any,
state: CodegenState,
altGen ? : Function,
altHelper ? : string
): string {
const exp = el.for // 拿到表达式arr
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (process.env.NODE_ENV !== 'production' &&
state.maybeComponent(el) && // slot 和 template不能进行v-for操作
el.tag !== 'slot' &&
el.tag !== 'template' &&
!el.key
) {
state.warn(
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
el.forProcessed = true // avoid recursion 生成循环函数
const r = `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
return r;
}
v-if实现原理
src/compiler/codegen/index.js:147
function genIfConditions(
conditions: ASTIfConditions,
state: CodegenState,
altGen ? : Function,
altEmpty ? : string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
const condition = conditions.shift()
if (condition.exp) { // 如果有表达式
return `(${condition.exp})?${ // 将表达式拼接起来
genTernaryExp(condition.block)
}:${ // v-else-if
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp(el) {
return altGen ?
altGen(el, state) :
el.once ?
genOnce(el, state) :
genElement(el, state)
}
}
v-model实现原理
普通元素上的v-model指令
src/compiler/codegen/index.js:310
function genDirectives(el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives // 获取所有指令
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn) // 添加input事件 和 value属性
}
if (needRuntime) {
hasRuntime = true // 是否需要运行时
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) { // directives:[{name:"model",rawName:"v-model",value:(msg),expression:"msg"}] 生成对应指令
let result = res.slice(0, -1) + ']'
return result;
}
}
组件上的v-model指令
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value' // 默认采用value属性
const event = (options.model && options.model.event) || 'input' // 默认采用input事件
;(data.attrs || (data.attrs = {}))[prop] = data.model.value // 绑定属性
const on = data.on || (data.on = {}) // 绑定事件
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}
Vue中slot是如何实现的?什么时候使用它?
- 普通插槽(模板传入到组件中,数据采用父组件数据),普通插槽是在父组件中渲染的
- 作用域插槽(在父组件中访问子组件数据),作用域插槽是在子组件中渲染的
const templateCompiler = require('vue-template-compiler');
let r = templateCompiler.compile(`
<div>
<slot name="title"></slot>
<slot name="content"></slot>
</div>`);
// with(this){return _c('div',[_t("title"),_v(" "),_t("content")],2)}
console.log(r.render)
let r1 = templateCompiler.compile(`
<my>
<h1 slot="title">标题</h1>
<div slot="content">内容</div>
</my>`)
/**
with(this){
return _c('my',[
_c('h1',{attrs:{"slot":"title"},slot:"title"},[_v("标题")]),_v(" "),
_c('div',{attrs:{"slot":"content"},slot:"content"},[_v("内容")])
])
}
**/
console.log(r1.render)
et r3 = templateCompiler.compile(`
<div>
<slot :article="{title:'标题',content:'内容'}"></slot>
</div>`);
// with(this){return _c('div',[_t("default",null,{"article":{title:'标题',content:'内容'}})],2)}
console.log(r3.render)
let r4 = templateCompiler.compile(`
<my>
<template slot-scope="{article}">
<h1 slot="article.title">标题</h1>
<div slot="article.content">内容</div>
</template>
</my>`)
/**
with(this){return _c('my',
{scopedSlots:_u([
{key:"default",fn:function({article}){
return [
_c('h1',{attrs:{"slot":"article.title"},slot:"article.title"},[_v("标题")]),
_v(" "),
_c('div',{attrs:{"slot":"article.content"},slot:"article.content"},[_v("内容")])
]
}
}
])
})}
*/
console.log(r4.render)
通插槽,渲染在父级, 作用域插槽在组件内部渲染!
Vue.use是干什么的?原理是什么?
Vue.use
是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等。- 会调用插件的
install
方法,将Vue的构造函数默认传入,这样在插件中可以使用Vue
无需依赖Vue
库
src/core/global-api/use.js
Vue.use = function (plugin: Function | Object) {
// 插件缓存
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) { // 如果已经有插件 直接返回
return this
}
// additional parameters
const args = toArray(arguments, 1) // 除了第一项其他的参数整合成数组
args.unshift(this) // 将Vue 放入到数组中
if (typeof plugin.install === 'function') { // 调用install方法
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') { // 直接调用方法
plugin.apply(null, args)
}
installedPlugins.push(plugin) // 缓存插件
return this
}
组件中写name选项有哪些好处及作用?
- 增加name选项会在
components
属性中增加组件本身,实现组件的递归调用。 - 可以标识组件的具体名称方便调试和查找对应组件。
src/core/global-api/extend.js:67
Sub.options.components[name] = Sub
Vue事件修饰符有哪些?其实现原理是什么?
- .stop、.prevent、.capture!、.self、.once~、.passive&
src\compiler\helpers.js:69
export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
)
}
if (modifiers.right) {
if (dynamic) {
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
name = 'contextmenu'
delete modifiers.right
}
} else if (modifiers.middle) {
if (dynamic) {
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
name = 'mouseup'
}
}
// check capture modifier
if (modifiers.capture) { // 如果capture 用!标记
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) { // 如果是once 用~ 标记
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) { // 如果是passive 用 &标记
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
src\compiler\codegen\events.js:42
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
let code = ''
let genModifierCode = ''
const keys = []
for (const key in handler.modifiers) {
if (modifierCode[key]) {
genModifierCode += modifierCode[key]
// left/right
if (keyCodes[key]) {
keys.push(key)
}
} else if (key === 'exact') {
const modifiers: ASTModifiers = (handler.modifiers: any)
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(keyModifier => !modifiers[keyModifier])
.map(keyModifier => `$event.${keyModifier}Key`)
.join('||')
)
} else {
keys.push(key) // modifiers中表达式存起来
}
}
if (keys.length) {
code += genKeyFilter(keys)
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode
}
const handlerCode = isMethodPath
? `return ${handler.value}.apply(null, arguments)`
: isFunctionExpression
? `return (${handler.value}).apply(null, arguments)`
: isFunctionInvocation
? `return ${handler.value}`
: handler.value
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`
}
src\platforms\web\runtime\modules\events.js:105
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) { // 循环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)) {
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)
}
}
}
Vue中.sync修饰符的作用,用法及实现原理
src\compiler\parser\index.js:798
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`) // 转.async 改成 ${value} = xxx
if (!isDynamic) {
addHandler( // 添加update事件
el,
`update:${camelize(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
if (hyphenate(name) !== camelize(name)) {
addHandler(
el,
`update:${hyphenate(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
}
} else {
// handler w/ dynamic event name
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true // dynamic
)
}
}
}
let r5 = templateCompiler.compile(`
<my :value.sync="xxxx"></my>
`);
// with(this){return _c('my',{attrs:{"value":xxxx},on:{"update:value":function($event){xxxx=$event}}})}
console.log(r5.render)
如何理解自定义指令
-
1.在生成
ast
语法树时,遇到指令会给当前元素添加directives属性 -
2.通过
genDeirectives
生成指令代码 -
3.在
patch
前将指令的钩子提取到cbs
中,在patch
过程中调用对应的钩子 -
4.当执行
cbs
对应的钩子时,调用对应指令定义的方法src/vdom/patch.js:77
提取钩子函数const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]); // 收集hook,patch过程中调用 // {create:[fn,fn],activate:[fn,fn]...} } } }
src/vdom/modules/directives.js:7
指令钩子export default { // 指令的钩子, 在创建和更新过程中会调用 create、update、destroy钩子 create: updateDirectives, update: updateDirectives, destroy: function unbindDirectives (vnode: VNodeWithData) { updateDirectives(vnode, emptyNode) } } function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (oldVnode.data.directives || vnode.data.directives) { // 如果有指令 _update(oldVnode, vnode) } }
keep-alive平时在哪里使用?原理是?
- 使用keep-alive包裹动态组件时, 会对组件进行缓存。避免组件的重新创建
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<component :is="component"></component>
</keep-alive>
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<router-view></router-view>
</keep-alive>
-
实现原理(lRU 算法)
export default { name: 'keep-alive', abstract: true, // 不会放到对应的lifecycle props: { include: patternTypes, // 白名单 exclude: patternTypes, // 黑名单 max: [String, Number] // 缓存的最大个数 }, created () { this.cache = Object.create(null) // 缓存列表 this.keys = [] // 缓存的key列表 }, destroyed () { for (const key in this.cache) { // keep-alive销毁时 删除所有缓存 pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { // 监控缓存列表 this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) 、// 获得第一个组件 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回 // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key // 生成缓存的key if (cache[key]) { // 如果有key 将组件实例直接复用 vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) // lru算法 } else { cache[key] = vnode // 缓存组件 keys.push(key) // prune oldest entry if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个 } } vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性 } return vnode || (slot && slot[0]) } }
keep-alive
第一次渲染的时候,会将其第一个子组件,缓存起来。- 当组件后续在次被激活时,会复用上一次缓存的实例进行渲染。
src\core\vdom\patch.js:210
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) // 将原来的elm,插入到页面中 if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
src\core\vdom\create-component.js:36
const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive // 有keepAlive, 不在执行组件的初始化流程 ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) // 组件挂载 当前组件实例中 包含$el属性 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } } }
Vue-Router有几种钩子函数,具体是什么及执行流程是怎样的?
钩子函数的种类有:全局守卫、路由守卫、组件守卫
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated), // 离开钩子
// global before hooks
this.router.beforeHooks, // 全局before钩子
// in-component update hooks
extractUpdateHooks(updated), // 更新钩子 beforeRouteUpdate
// in-config enter guards
activated.map(m => m.beforeEnter), // beforeEnter钩子
// async components
resolveAsyncComponents(activated) // 异步组件
)
runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated) // beforeRouteEnter
const queue = enterGuards.concat(this.router.resolveHooks) // beforeResolve
runQueue(queue, iterator, () => {
afterEachs.forEach(fn=>fn())
})
})
}
Vue-Router的两种模式的区别
Vue-Router
有三种模式hash
、history
、abstract
abstract
模式是在不支持浏览器API
环境使用,不依赖于浏览器历史hash
模式:hash
+popState/hashChange
兼容性好但是不够美观,hash
服务端无法获取。不利于seo
优化history
模式:historyApi
+popState
美观,刷新会出现404 -> CLI webpack history-fallback
谈一下你对vuex
的个人理解
vuex
是专门为vue提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例new Vue
)
方法:
replaceState
、subscribe
、registerModule
、namespace(modules)
、辅助函数…
mutation
和action
的区别
mutation
: 主要在于修改状态,必须同步执行action
: 执行业务代码,方便复用,逻辑可以为异步,不能直接修改状态
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, function () {
if ((process.env.NODE_ENV !== 'production')) {
assert(store._committing, "do not mutate vuex store state outside mutation handlers.");
}
}, { deep: true, sync: true }); // 同步watcher监控状态变化
}
Vue中的性能优化有哪些?
- 数据层级不易过深,合理设置响应式数据
- 使用数据时缓存值的结果,不频繁取值。
- 合理设置Key属性
- v-show和v-if的选取
- 控制组件粒度 -> Vue采用组件级更新
- 采用函数式组件 -> 函数式组件开销低
- 采用异步组件 -> 借助
webpack
分包的能力 - 使用
keep-alive
缓存组件 - 虚拟滚动、时间分片等策略…
- 打包优化
Vue
中使用了哪些设计模式?
- 单例模式 - 单例模式就是整个程序有且仅有一个实例
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
- 工厂模式 - 传入参数即可创建实例 (
createElement
)
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
// ....
}
- 发布订阅模式 - 订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
- 观察者模式 -
watcher
&dep
的关系 - 代理模式 - 代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。
_data属性、proxy、防抖、节流 let p = new Proxy
- 装饰模式 -
Vue2
装饰器的用法 (对功能进行增强 @) - 中介者模式 - 中介者是一个行为设计模式,通过提供一个统一的接口让系统的不同部分进行通信。
Vuex
- 策略模式 - 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案。 mergeOptions
- 外观模式 - 提供了统一的接口,用来访问子系统中的一群接口