传统的MVC指的是,用户操作会请求服务端路由,路由拦截分发请求,调用对应的控制器来处理。控制器会获取数据,然后数据与模板结合,将结果返回给前端,页面重新渲染。
数据流是单向的,view——>model——>view
MVVM:传统的前端会将数据手动渲染到页面上,MVVM模式不需要用户手动操作DOM元素,将数据绑定到viewModel层上,会自动将数据渲染到页面中。视图变化会通知viewModel层更新数据,viewModel就是MVVM模式的桥梁。数据驱动
数据流动时双向的,model——>viewModel<——>view
vue2——核心点:Object.defineProperty —— 修改每一个属性
默认Vue在初始化数据时,会给data中的属性使用Object.defineProperty,在获取和设置的进行拦截,重新定义所有属性。当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher)。如果属性发生变化会通知相关依赖进行更新操作。
依赖收集、派发更新的作用:如果没有这项操作,每个数据更新就会去渲染页面,极大的消耗性能。加了这项操作,去监听相关数据的改变,添加到队列里,当所有改变完事儿之后,一起进行渲染。
vue3——核心点:proxy(代理)—— 直接处理对象
解决了vue2中的处理对象递归、处理数组麻烦的问题
原理:
响应式原理图
源码:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
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) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)//递归处理子
//每一个对象属性添加get、set方法,变为响应式对象
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()//依赖收集
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()//派发更新
}
})
}
使用了函数劫持的方式,重写了数组方法
Vue将data中的数组,进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次进行监控。
Object.create(),保存原有原型
原理:
源码:
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import {
def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)//es6语法,相当于继承一个对象,添加的属性是在原型下
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
ob.dep.notify()//派发更新,渲染页面
return result
})
})
vue是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件重新渲染。为了性能考虑,vue会在本轮数据更新后,再去异步更新视图。
原理:
源码:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id//判断watcher的id是否存在
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
//wating默认为false
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)//调用nextTick方法,批量更新
}
}
}
nextTick主要是使用了宏任务和微任务,定义了一个异步方法。多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列,所以nextTick就是异步方法。
原理:
源码:
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
//callbacks是一个数组
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
//pengding默认为false
pending = true
timerFunc()//调用异步方法
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
默认computed也是一个watcher,具备缓存,只有当依赖的属性发生变化才会更新视图。
原理:
源码:
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]//获取用户定义
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${
key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,//将用户定义传到watcher中
noop,
computedWatcherOptions//lazy:true懒watcher
)
}
// 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)
}
}
}
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)//创建计算属性的getter,不是用用户传的
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${
key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
return function computedGetter () {
//用户取值的时候会调用此方法
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
//dirty为true会去进行求值,这儿的dirty起到了缓存的作用
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
当用户指定了watch中的deep属性为true时,如果当时监控的属性是数组类型,会对对象中的每一项进行求值,此时会将当前watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新。
内部原理就是递归,耗费性能
function initWatch (vm: Component, watch: Object) {
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])//每一项创建一个watcher
}
} else {
createWatcher(vm, key, handler)
}
}
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)//将watcher放到全局上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)//取值,会进行依赖收集
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${
this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
//深度监听
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
//递归处理
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
每个生命周期什么时候被调用
每个生命周期内部可以做什么事
在created的时候,视图中的DOM并没有渲染出来,此时直接去操作DOM节点,无法找到相关元素。
在mounted中,此时DOM已经渲染出来,可以直接操作DOM节点。
一般情况下都放到mounted中,保证逻辑的统一性,因为生命周期是同步执行的,ajax是异步执行的。
服务器端渲染因为没有DOM,不支持mounted方法,所以在服务器端渲染的情况下统一放到created中。
可能在当前组件使用了$on方法,需要在组件销毁前解绑
清除自己定义的定时器
解除事件的原生绑定scroll、mousemove…
模板(template)》 ast语法树(抽象语法树)》 codegen方法 ==》render函数 ==》createElement方法 ==》 Virtual Dom(虚拟dom)
模板转语法树
模板结合数据,生成抽象语法树,描述html、js语法
语法树生成render函数
render函数
生成Virtual Dom(虚拟dom),描述真实的dom节点
渲染成真实dom
v-if如果条件不成立,不会渲染当前指令所在节点的dom元素
v-show切换当前dom的显示和隐藏,本质上display:none
v-for会比v-if的优先级高一些,如果连用的话,会把v-if给每个元素都添加一下,会造成性能问题。
如果确实需要判断每一个,可以用计算属性来解决,先用计算属性将满足条件的过滤出来,然后再去循环。
两个树完全diff算法的时间复杂度为O(n3),Vue进行了优化,只考虑同级不考虑跨级,将时间复杂度降为O(n)
前端当中,很少会跨层级的移动Dom元素,所以Virtual Dom只会对同一个层级的元素进行对比
1、先同级比较,再比较儿子节点
2、先判断一方有儿子一方没儿子的情况
3、比较都有儿子的情况
4、递归比较子节点
vue3中做了优化,只比较动态节点,略过静态节点,极大的提高了效率
双指针去确定位置
diff算法原理图
v-for中为什么要用key?
解决vue中diff算法结构相同key相同,内容复用的问题,通过key(最好自定义id,不要用索引),明确dom元素,防止复用
渲染组件时,会通过Vue.extend方法构建子组件的构造函数,并进行实例化,最终手动调用$mount进行挂载。更新组件时会进行patchVnode流程,核心就是diff算法。
组件中的data为什么是个函数?
同一个组件被复用多次,会创建多个实例。这些实例用的是同一个构造函数,如果data是一个对象的话,所有组件共享了同一个对象。为了保证组件的数据独立性,要求每个组件都必须通过data函数返回一个对象作为组件的状态。
Vue中事件绑定的原理
Vue的事件绑定分为两种:一种是原生的事件绑定,一种是组件的事件绑定
原生dom事件绑定采用的是addEventListener
组件的事件绑定采用的是$on方法
v-model的实现原理及如何自定义v-model?
v-model可以看成是value+input方法的语法糖
不同的标签去触发不同的方法
可能会导致XXS攻击
v-html会替换掉标签内的子元素
原理:
加载渲染过程
父beforeCreate ==> 父created ==> 父beforeMount ==> 子beforeCreat ==>子created ==> 子beforeMount ==> 子mounted ==> 父mounted
子组件更新过程
父beforeUpdate ==> 子beforeUpdate ==> 子updated ==> 父updated
父组件更新过程
父beforeUpdate ==> 父updated
销毁过程
父beforeDestroy ==> 子beforeDestroy ==> 子destroyed ==> 父destroyed
理解
组件的调用顺序都是先父后子,渲染完成的顺序是先子后父
组件的销毁操作是先父后子,销毁完成的顺序是先子后父
Vue.mixin用法给组件每个生命周期、函数都混入一些公共逻辑
Vue.mixin({
beforeCreate(){
}//这儿定义的生命周期和方法会在每个组件里面拿到
})
源码:
import {
mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)//将当前定义的属性合并到每个组件中
return this
}
}
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
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.
if (!child._base) {
if (child.extends) {
//递归合并extends
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
//递归合并mixin
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
}
如果组件功能多,打包出的结果会变大,可以采用异步组件的方式来加载组件。主要依赖import()这个语法,可以实现文件的分割加载。
渲染的作用域不同,普通插槽是父组件,作用域插槽是子组件
插槽
keep-alive可以实现组件的缓存,当组件切换时,不会对当前组件卸载
常用的2个属性include、exclude
常用的2个生命周期activated、deactivated
源码:
export default {
name: 'keep-alive',
abstract: true,//抽象组件
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)//创建缓存列表
this.keys = []//创建缓存组件的key列表
},
destroyed () {
//keep-alive销毁时,会清空所有的缓存和key
for (const key in this.cache) {
//循环销毁
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
//会监控include和exclude属性,进行组件的缓存处理
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,就自己通过组件的标签和key和cid拼接一个key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance//直接拿到组件实例
// make current key freshest
remove(keys, key)//删除当前的[b,c,d,e,a] //LRU最近最久未使用法
keys.push(key)//将key放到后面[b,a]
} else {
cache[key] = vnode//缓存vnode
keys.push(key)//将key存入
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
//缓存的太多,超过了max就需要删除掉
pruneCacheEntry(cache, keys[0], keys, this._vnode)//要删除第0个,但是渲染的就是第0个
}
}
vnode.data.keepAlive = true//标准keep-alive下的组件是一个缓存组件
}
return vnode || (slot && slot[0])//返回当前的虚拟节点
}
}
一个优秀的Vue团队代码规范是什么样子的?
1、编码优化
2、Vue加载性能优化
3、用户体验
4、SEO优化
5、打包优化
6、缓存压缩
Vue3.0的改进
vue2采用的是defineProperty去定义get,set,而vue3改用了proxy。也代表着vue放弃了兼容ie。
通过vue的插件系统,用vue.mixin混入到全局,在每个组件的生命周期的某个阶段注入组件实例。
vue2采用的是典型的混入式架构,类似于express和jquery,各部分分模块开发,再通过一个mixin去混入到最终暴露到全局的类上。
简述一个框架的同时,说出他的设计来源、类似的框架。
这个项目主体是一个vue项目,但是因为是pc端,为了seo,我特意做了ssr。然后这个项目有一套我和同事一起做的专门的组件库。在移动端,我们为了搭配app,也做了移动混合方案。像在首页,因为数据巨大,我们采用了一些优化方案。利用本地缓存数据,对小图标进行了base64转码。