1. v-for和v-if为什么不推荐一起使用 优先级 v-for v-if v-model实现原理 v-if和v-show的区别
1. v-for的优先级是比v-if要高的 我们一般在外层加template使用v-if或者使用计算属性
const compiler = require('vue-template-compiler')
const ast = compiler.compile('<div v-if="false" v-for="i in 3">i</div')
// vue-loader在编译过程中主要使用vue-template-compiler来编译 在执行_l(list)的时候每次都会在内部判断 先执行循环再在内部判断
// with(this){return _l((3),function(i){return (false)?_c('div',[_v("i"),_v("</div")]):_e()})}
console.log(ast.render)
// vue在编译阶段 processFor(element) processIf(element)
2. v-if 转换为一个三元表达式
const ast1 = compiler.compile('<div v-if="false"></div')
console.log(ast1.render) // with(this){return (false)?_c('div'):_e()}
3. v-for 编译成_l执行3次
const ast2 = compiler.compile('<div v-for="i in 3">i</div')
console.log(ast2.render) // with(this){return _l((3),function(i){return _c('div',[_v("i")])})}
4. v-model是一个指令directives value值和input事件
const ast3 = compiler.compile('<input v-model="name" />')
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
console.log(ast3.render)
// v-model还可以用在组件上 给组件添加一个value属性和一个cb回调
const ast4 = compiler.compile('<comp v-model="name"></comp>')
console.log(ast4.render) // with(this){return _c('comp',{model:{value:(name),callback:function ($$v) {name=$$v},expression:"name"}})}
5. v-show也是解析为指令 根据条件来改变样式实现
const ast5 = compiler.compile('<div v-show="false"></div>')
console.log(ast5.render) // with(this){return _c('div',{directives:[{name:"show",rawName:"v-show",value:(false),expression:"false"}]})}
// 总结
1. v-for的优先级高于v-if 不推荐连用 会印象性能 每次_l的时候都会去判断v-if 我们一般使用计算属性来避免
2. v-for经过编译会生成一个_l的循环
3. v-if会生成一个三元表达式当条件不满足的时候不会渲染具体的div v-show编辑是指令 根据value值改变css属性
4. v-model可以使用在input等还可以使用在自定义组件上 在input上会解析为指令 绑定value值和input事件 在自定义组件上会给组件添加一个value值和一个cb的回调
2. vue组件间传值的方式和区别
1. props
我们给组件传递props属性 ast语法树会生成vnode
new Vnode() 组件组件会有一个componentOptions {Ctor, propsData, listeners, tag, children}
在组件初始化的时候 opts.propsData = vnodeComponentOptions.propsData做一个混合
initProps的过程中 const propsData = vm.$options.propsData || {}
然后循环propsData 将属性定义到自己的实例上
2. $on $emit的实现原理 自定义实现事件系统 (发布订阅)
vue中响应式数据变化就是典型的观察者模式 watcher dep
vue中的事件绑定就是发布订阅模式
观察者模式中被观察者和观察者存在关联 包含了发布订阅
组件vnode上有一个listeners属性 _parentListeners(vm.$options)
在初始化事件的时候 initEvents(vm) 执行updateComponentListeners(vm, listeners)
add就是@on绑定事件 remove就是$off
$on vm._events[event].push(fn) 典型的发布订阅
$emit 触发 执行回调
3. $parent和$children的区别 快速拿到父子组件的实例 $broadcast $dispatch 给组件添加name属性 (递归组件)
parent vue中父子关系如何维护的? $parent $root $children
4. provide和inject的原理 一般在组件库和插件中使用
function initProvide(vm) {
const provide = vm.$options.provide
if(provide) {
vm._provided = typeof provide === 'function' ? provide.call(vm) : provide
}
}
function initInjections(vm) {
const result = resolveInject(vm,$options,inject, vm)
Object.keys(result).forEach(key => defineReactive(vm, key, result[key]))
}
5. $eventBus new Vue() 使用vue提供的事件系统 发布订阅
6. $attrs $listeners 快速传递属性
7. $refs获取组件实例 组件暴露的一些方法属性
8. vuex状态管理
3. 谈谈你对vuex的理解
https:
vuex是vue中用来做数据状态管理的 数据可以几种管理 实现多组件状态共享 vue数据是单向数据流的
1. vuex的核心是一个store仓库在vuex中数据时单向数据流的 主要流程是
vue component通过dispatch触发action(可以发送异步请求) 在通过commit提交动作触发mutation来改变state
state的变化会引起vue component的重渲染 在严格模式下 mutation是唯一改变状态的方式 我们要避免直接修改state的值
2. 在使用vuex的时候我们使用Vue.use(Vuex)当成Vue的一个插件使用我们需要使用install方法
install通过mixin在beforeCreate钩子中混入全局的$store属性(和vue-router实现不一致)
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
new Vuex.Store() 我们会初始化一个store实例
state中的数据借助了vue的特定实现了数据响应式变化
this._vm = new Vue({
data: { $$state: state },
computed
})
mutation和action都是发布订阅模式 在初始化的时候通过传入的options保存在数组中 我们调用dispatch和commit的时候触发对应的回调
getter使用了Object.defineProperty重新定义返回value[this.state] 借助vue中的计算属性computed
computed[key] = () => return value.call(this,this.state)
Object.defineProperty(this.getters, key, {
get: () => this._vm[key]
}
3. 在项目中我们一般会将不同模块的数据放到不同的js文件中然后通过设置namespace:true属性增加命名空间
增加命名空间之后 我们在触发对应的mutation和action需要带上对应的namespace的名字
在注册的时候使用ModuleCollection对数据进行格式化 注册模块 主要是使用reduce找模块定义到parent上 然后递归注册将子模块全部定义一遍 变成一个树形结构
在安装模块的时候 installModule 取到key值遍历 然后递归处理children 也是使用reduce
4. 当我们刷新页面的时候 vuex中的数据会丢失 我们可以使用插件来实现数据的持久化
vuex的插件就是一个函数 接收store参数 可以监听mutation 也可以提交mutation
5. 为了我们调用方便 vuex还提供了一系列的辅助函数
mapState 主要是使用forEach遍历找到对应的值
6. vuex还有一些常用的实例方法
replaceState 替换原来的store状态
subscribe 订阅store的mutation 就是往数组中push对应的回调
subscribeAction订阅store中的action
registerModule动态注册 register和install
unregisterModule 卸载
hotUpdate热重载
对其他数据管理的了解 redux mobx
4. Vue中computed和watch的区别 实现原理
1. watch的实现是基于Watch的(渲染watcher参数为true, 自定义watcher设置user为true 计算属性也是watch lazy)
watch: {a: {handler: {}}}
function initWatch(vm, watcher) {
for(let key in watcher) {
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)
}
}
}
function createWatcher(vm, key, handler, options) {
if(isObject(handler)) {
options = handler
handler = handler.handler
}
if(typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(key, handler, options)
}
class Watch {
constructor(vm, exprOrFn, cb, options) {
this.user = options.user
this.sync = options.sync
exprOrFn可能是表达式需要变成函数
if(typeof exprOrFn === 'function') {
this.getter = exprOrFn
} else {
this.getter = function() {
}
}
this.value = this.get()
}
get() {
pushTarget(this)
const value = this.getter.call(this.vm)
popTarget(this)
return value
}
update() {
if(this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run() {
let value = this.get()
let oldValue = this.value
this.value = value
if(this.user) this.callback.call(this.vm, value, oldValue)
}
}
1. 参数的格式化
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = {}
for (let key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
watchers[key] = new Watcher(vm, getter, () => { }, { lazy: true })
defineComputed(vm, key, userDef)
}
}
function defineComputed(vm, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
} else {
sharedPropertyDefinition.get = createComputedGetter(userDef.get);
sharedPropertyDefinition.set = userDef.set || (() => {});
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter(key) {
return function() {
const watcher = this._computedWatchers[key]
if(watcher) {
if(watcher.dirty) {
watcher.evaluate()
}
if(Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
class Watch {
constructor(vm, exprOrFn, cb, options) {
this.lazy = options.lazy
this.dirty = this.lazy
this.value = this.lazy ? undefined : this.get()
}
evaluate() {
this.value = this.get()
this.dirty = false
}
depend() {
let i = this.deps.length
while(i--) {
this.deps[i].depend()
}
}
update() {
if(this.lazy) {
this.dirty = true
}
}
}
1. watch默认会取值 computed不会 内部都是基于Watch的
2. computed有缓存效果 依赖项不变就不会重新执行
3. computed变量一般用户模板的渲染 watch一般是执行cb
5. 为什么我们可以直接使用this.xxx访问属性 Vue中组件的data为什么是一个函数?
function Vue(options) {this._init(options)} vue就是一个函数(options api)
initData() data = vm._data = typeof data === 'function' ? data.call(vm) : data
// 然后我们会遍历属性 this.name在this._data上取name属性
for(let key in data) {
Object.defineProperty(vm, '_data', {
get() return vm['_data'][key]
set(newValue) vm['_data'][key] = newValue
})
}
6. vue2响应式原理 请说一下响应式数据的理解 vue双向数据绑定
1. 数据劫持过程 分为数组和对象
this.walk(data)
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
function defineReactive(data, key, value) {
observe(value)
Object.defineProperty(data, key, {
get() {
return value
}
set(newValue) {
if(newValue === value) return
observe(newValue)
newValue = value
}
}
}
value.__ob__ = this
if(Array.isArray(data)) {
value.__proto = arrayMethods
this.observeArray(value)
} else {
this.walk(data)
}
observeArray(value) {
for(let i = 0; i < value.length; i++) {observe(value[i])}
}
let oldArrayProtoMethods = Array.prototype
export let arrayMethods = Object.create(Array.prototype)
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
methods.forEach(method => {
arrayMethods[method] = function (...args) {
const result = oldArrayProtoMethods[method].call(this, ...args)
const ob = this.__ob__
let inserted
switch(method){
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2)
default:
break
}
if (inserted) ob.observeArray(inserted)
return result
}
})
Object.defineProperty(value,'__ob__',{
enumerable:false,
configurable:false,
value:this
}
data() {return {arr: [{name: 'vue'}]}}
1. Object.defineProperty数据劫持
2. 数组利用原型链重写数组的方法实现
3. 内部依赖收集和派发更新过程 dep和watcher相互订阅
4. vue3的proxy
5. 性能优化 层级 Object.freeze()冻结
7. $set实现原理
function set(target, key, val) {
if(Array.isArray(target)) {
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
当我们给一个数据增加属性时该属性不是响应式的我们可以通过$set添加
1. 对于数组内部使用了splice方法(我们重写的方法)
2. 对于对象我们使用了Object.defineProperty定义添加的属性
3. 通过ob.dep.notofy() 手动通知视图更新
8. 简述Vue中模板编译原理 vue-loader主要做了什么
const render = compileToFunction(template)
1. parse() 对模板做解析生成ast抽象语法树
parseHTML(template, options)
while(html) {}
2. optimize() 优化ast语法树 markUp 静态节点 静态根 static
3. codegen ast树重新生成代码 genData genChildren
let code = generate(root)
let render = `with(this){return ${code}}`
new Function(render)
9. 简述vue初始化流程
new Vue() => mountComponent()
1. vm._render()生成vnode
let vnode = render.call(vm) (template编译生成render函数)
2. vm._update()调用patch生成真实的dom元素
vm.$el = patch(vm.el, vnode) 初始化渲染 diff更新
function patch(oldVnode,vnode) {
let el = createElm(vnode)
parentElm.insertBefore(el,oldElm.nextSibling)
}
function createElm(vnode) {
const {tag} = vnode
vnode.el = document.createElement(tag)
return vnode.el
}
3. 通过渲染watcher执行上面函数
let updateComponent = () => {vm._update(vm._render())}
new Watcher(vm, updateComponent, () => {}, true)
class Watch {
constructor(vm, fn, cb, options) {
this.fn()
}
}
1. initState初始化状态 数据拦截 响应式数据
2. mount挂载执行mountComponent
3. new Watcher(updateComponent) 渲染watcher
4. vm._render 生成vnode render.call() compileToFunction()编译生成render函数
5. vm._update调用createElm生成el
6. appendChild插入元素
10. vue响应式数据
vue核心:组件化和响应式数据
在页面初始化过程中我们会调用render方法就会对模板中的数据取值 get方法做数据依赖收集 记住watcher
当数据发生变化时会执行set方法派发更新 我们需要让dep和watcher相互记住
// 对象
let id = 0
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.id = id++
this.getter = exprOrFn
this.deps = []
this.depsId = new Set()
this.get()
}
get() {
pushTarget(this)
this.getter()
popTarget(this)
}
addDep(dep) {
let id = dep.id
if(!this.depsId.has(id)) {
this.deps.push(dep)
this.depsId.add(id)
dep.addSub(this)
}
}
}
let id = 0
Dep.target = null
class Dep {
constructor() {
this.id = id++
this.subs = []
}
depend() {
Dep.target.addDep(this)
}
addSub(watcher) {
this.subs.push(watcher)
}
}
function defineReactive(data, key, value) {
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
if(dep.target) dep.depend()
return value
},
set(newValue) {
observe(newValue)
value = newValue
dep.notify()
}
}
}
class Dep {
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
class Watcher {
update() {
this.cleanupDeps()
this.get()
}
}
class Observer {
constructor(value) {
this.dep = new Dep()
}
}
function defineReactive(data, key, value) {
let dep = new Dep()
let childOb = observe(value)
Object.defineProperty(data, key, {
get() {
if(Dep.target) {
dep.depend()
if(childOb) {
childOb.dep.depend()
if(Array.isArray()) {
dependArray(value)
}
}
}
}
}
}
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let current = value[i];
current.__ob__ && current.__ob__.dep.depend()
if (Array.isArray(current)) {
dependArray(current)
}
}
}
arrayMethods[method] = function (...args) {
const result = oldArrayProtoMethods[method].call(this, ...args)
const ob = this.__ob__
ob.dep.notify()
return result
}
1.
11. vue如何实现批量更新 vue更新原理
class Watcher {
update() {
queueWatcher(this)
}
run() {
this.get()
}
}
let queue = [], has = {}
function queueWatcher() {
let id = watcher.id
if(has[id] === null) {
queue.push(watcher)
has[id] = true
setTimeout(flushSchedularWatcher, 0)
}
}
function flushSchedularWatcher () {
for(let i = 0; i < queue.length; i++) {
let watcher = queue[i]
watcher.run()
}
queue = []
has = {}
}
12. nextTick在哪里使用?原理是?
vue多次更新数据 最终进行批处理更新 内部使用nextTick实现延迟更新 自定义的nextTick的回调会被延迟
到更新完成之后调用 可以获取到更新的dom
let pending = false
function queueWatcher() {
if(!pending) {
pending = true
nextTick(flushSchedularWatcher)
}
}
function flushSchedularWatcher () {
queue.forEach(item => item.run())
pending = false
}
let cbs = [], waiting = false
function nextTick(cb) {
cbs.push(cb)
if(!waiting) {
waiting = true
Promise.resolve().then(flushCallbacks)
}
}
function flushCallbacks() {
cbs.forEach(item => item())
waiting = false
}
13. Vue的生命周期方法有哪些 生命周期钩子是如何实现的
beforeCreate: 实例初始化之前 vuex vue-router混入一些属性 mixin
created 实例创建完成 没挂载
beforeMount 挂载之前
mounted el被vm.$el替换 可以进行一些dom操作
beforeUpdate 数据更新前 虚拟dom重新渲染和patch
updated 由于数据变化导致的虚拟dom重新渲染和打补丁 避免在这个阶段更改状态
beforeDestroy 实例销毁之前 做一些优化操作 清楚定时器 解绑事件等
destroyed 实例销毁之后调用 移除事件 子实例销毁
// 实现
内部使用callHook方法来调用对应的方法 核心是一个发布订阅者模式 将钩子订阅好(使用数组存储)在对应的阶段发布
1.在初始化过程中
vm.$options = mergeOptions(vm.constructor.options, options)
callHook(vm,'beforeCreate');
initState()
callHook(vm,'created');
2. 组件挂载过程 vm.$mount(vm.$options.el) mountComponent
callHook(vm, 'beforeMount')
let updateComponent = () => vm._update(vm._render())
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
})
callHook(vm, 'mounted')
3. 数据发生变化 批量更新 scheduler flushSchedulerQueue中
if (watcher.before) { watcher.before() }
watcher.run()
callUpdatedHooks(updatedQueue)
callHook(vm, 'updated')
4. 在组件销毁过程中 $destroy
callHook(vm, 'beforeDestroy')
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed')
function callHook(vm, hook) {
const handlers = vm.$options[hook];
if(handlers) {
for(let i = 0; i < handlers.length; i++) {
handlers[i].call(vm)
}
}
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
function mergeHook(parentVal, childVal) {
if(childVal) {
if (parentVal) {
return parentVal.concat(childVal)
} else {
return [childVal]
}
} else {
return parentVal
}
}
14. Vue.mixin的使用场景和原理? mixin和extend的区别 mergeOptions实现原理
mixin extend 都是Vue的静态方法
initGlobalAPI(Vue)给Vue拓展一些静态属性和方法
抽离公共的业务逻辑(原理类型对象的继承) 组件初始化的时候使用mergeOptions方法进行合并 采用策略模式针对不同的属性合并
数据来源不明确 依赖问题 数据是不共享的 变量名冲突 数据冲突采用就近原则
Vue.mixin = function(Vue) {
this.options = mergeOptions(this.options, mixin)
return this
}
export function mergeOptions(parent, child, 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
}
const defaultStrat = function(parentVal, childVal) {
return childVal === undefined ? parentVal : childVal
}
strats.data = function(parentVal, childVal, vm) {
return mergeDataOrFn(parentVal, childVal, vm)
}
strats.watch = function() {}
strats.props = strats.methods = strats.computed = function() {}
15. vue的组件化 组件渲染流程
Vue.component(componentName, definition) Vue.extend 通过对象创建一个类 类去创建组件
components:{}
Vue.options._base = Vue
Vue.options.components = {}
Vue.component = function(id, definition) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
this.options.components[id] = definition
}
let cid = 0;
Vue.extend = function(extendOptions) {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const Sub = function VueComponent(options) {
this._init(options)
}
Sub.cid = cid++
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.component = Super.component
Sub.options = mergeOptions(Super.options, extendOptions)
return Sub
}
function createElement(vm, tag, data = {}, ...children) {
if(typeof tag === 'string') {
if(isReservedTag(tag)) {
return vnode(tag, data, children,undefined, undefined, context);
} else {
const Ctor = vm.$options.components[tag]
return createComponent(vm, tag, data, context , children, Ctor)
}
}
}
function makeMap(str) {
const map = Object.create(null)
const list = str.split(',');
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return (key) => map[key];
}
const isHTMLTag = makeMap('html,body,div,title,h1,aside,img,text,span,input,p,button,ul,li')
const isSVG = makeMap('svg')
export const isReservedTag = function (tag) {
return isHTMLTag(tag) || isSVG(tag)
};
function createComponent(vm, tag, data, children, Ctor) {
if(isObject(Ctor)) Ctor = vm.$options._base.extend(Ctor)
data.hook = {
init(vnode) {
let child = vnode.componentInstance = new vnode.componentOptions.Ctor({})
child.$mount()
},
prepatch(oldVnode, vnode){},
insert(vnode){},
destroy(vnode){}
}
const name = Ctor.options.name || tag;
const vnode = new Vnode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }
)
return vnode
}
function patch(oldVnode, newVnode) {
if(!oldVnode) return createElm(newVnode)
}
function createElm(vnode) {
if(typeof tag === 'string') {
return vnode.componentInstance.$el
}
}
function createComponent(vnode) {
let i = vnode.data
if ((i = i.hook) && (i = i.init)) {
i(vnode)
}
if (vnode.componentInstance) {
return true;
}
return false
}
1. Vue.component调用Vue.extend生成一个子类构造器Ctor扩展一些属性返回 典型的原型继承
2. 执行mountComponent过程中 _render生成组件vnode有一个componentOptions里面有Ctor 安装组件钩子函数hook
3. 在执行patch过程中 createElm会创建组件 执行hook中的init方法
4. init方法会new Ctor()实例化 然后走_init过程返回child
5. 手动挂载$mount() 返回componentInstance.$el做挂载
6. 父亲先执行 然后遇到children就会children.forEach(child => {return vnode.el.appendChild(createElm(child));})
7. vue是组件级别更新的 组件渲染过程中会创建新的watcher
函数组件 异步组件 抽象组件实现 promise
函数式组件的优势 异步组件 如何实现
16. Vue中的diff原理 为什么要设置key
function _update(vnode) {
const vm = this
const prevVnode = vm._vnode;
vm._vnode = vnode;
if(!prevVnode) {
vm.$el = patch(vm.$el,vnode)
} else {
vm.$el = patch(prevVnode,vnode)
}
}
function patch(oldVnode, newVnode) {
if (isRealElement) {
} else {
if (oldVnode.tag !== newVnode.tag) {
return oldVnode.el.parentNode.replaceChild(createElm(newVnode), oldVnode.el)
}
if (!oldVnode.tag) {
if (oldVnode.text !== vnode.text) {
oldVnode.el.textContent = vnode.text
}
}
let el = newVnode.el = oldVnode.el
updateProperties(vnode, oldVnode.data)
let oldChildren = oldVnode.children || []
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(el, oldChildren, newChildren)
}
else if (oldChildren.length > 0) {
el.innerHTML = ''
}
else if (newChildren.length > 0) {
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
}
}
function updateChildren(parent, oldChildren, newChildren) {
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartVnode = newChildren[0];
let newEndIndex = newChildren.length - 1;
let newEndVnode = newChildren[newEndIndex];
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map;
}
let map = makeIndexByKey(oldChildren);
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex];
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
}
else if (isSameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode)
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode)
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
patch(oldStartVnode, newEndVnode);
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
patch(oldEndVnode, newStartVnode);
parent.insertBefore(oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex]
} else {
let moveIndex = map[newStartVnode.key]
if (moveIndex == undefined) {
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el)
} else {
let moveVnode = oldChildren[moveIndex]
oldChildren[moveIndex] = undefined
parent.insertBefore(moveVnode.el, oldStartVnode.el)
patch(moveVnode, newStartVnode)
}
newStartVnode = newChildren[++newStartIndex]
}
}
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
let ele = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el;
parent.insertBefore(createElm(newChildren[i]), ele);
}
}
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldChildren[i];
if (child != undefined) {
parent.removeChild(child.el)
}
}
}
}
1. vue的dom diff是同层比较的 两个vnode
2. 先比较两个vnode的标签
2.1 两个标签不一样就直接使用新的替换老的
2.2 两个标签相同 但是都为undefined(文本) 就比较文本内容
2.3 标签相同 更新属性
2.4 比较children
2.4.1 老的有儿子新的没有 直接删除
2.4.2 老的没有新的有 新增插入
2.4.3 老的有新的也有 核心的diff算法updateChildren(el, oldChildren, newChildren)
3. 核心diff算法 使用头尾双指针的方式
1. 遍历新旧while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {} 当越界的时候结束
3.1 头头比较 判断是否我sameVnode
3.2 头尾比较
3.3 头尾比较
3.4 尾头比较
3.5 乱序的使用map保存老的映射关系 新的找不到就新建 找到了就使用旧的vnode做移动操作
3.6 跳过为空的
2. while之后判断新旧的index
老的多就直接删掉
新的多就插入新的节点
17. vue-router有几种钩子函数?具体是什么及执行流程是怎样的? 权限的设计 两种模式 vue.use的作用
https:
1. Vue.use()原理
Vue.use(Router)
Vue.use = function(plugin, options) {
plugin.install(this, options)
}
new Router({
routes: [{}]
})
new Vue({
router
})
2. vueRouter是一个构造函数 前端路由实现 两种模式 hash模式、history模式 abstract
1.spa应用 路径切换可以重新渲染组件(不刷新页面)
2.hash 兼容性好 # location.hash = xxx window.addEventListener('hashchange')
3.history需要服务端支持 history.pushState 监听popstate history-fallback(webpack插件)
3. 钩子函数 导航解析流程
1. 导航被触发
2. 在失活的组件里调用离开守卫
3. 调用全局的beforeEach守卫
4. 在重用的组件里调用beforeRouteUpdate守卫
5. 在路由配置里调用beforeEnter
6. 解析异步路由组件
7. 在被激活的组件里调用beforeRouteEnter
8. 调用全局的beforeResolve
9. 导航被确认
10. 调用全局的afterEach钩子
11. 触发dom更新
12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调
4. vue-router的使用 权限设计 路由级别 按钮级别
1. 将不同路由配置放在不同的js文件中 index.router.js user.router.js
2. 路由信息一般配合webpack的import语法懒加载
3. 区分需要权限控制的路由和都需要加载的路由信息
4. require.context('./', true, /\.router.js$/) files.keys().forEach() 聚合文件
5. 在beforeEach钩子中请求个人信息获取权限列表通过匹配得到需要动态加载的路由
6. 调用router.addRoutes(accessRoutes)动态的加载路由信息
7. 按钮级别的通过v-permission指令实现 或者定义$hasPermission方法
5. 说说你对vue-router理解
1. 需要实现一个install方法
2. 提供了<router-link>和<router-view>两个组件 $router $route
3. matcher路由匹配器(addRoutes match) 路由就是匹配到对应的路径显示对应的组件 一个匹配一个路径切换
4. 路径切换 transitionTo 匹配一个路径然后进行路径切换
6. vue-router简单实现
1. 实现一个install方法 (VueRouter是一个类)
class VueRouter {}
Vue.use()
VueRouter.install = install
2. install方法 通过mixin混入 router属性(实例对象) 初始化init
function install(Vue) {
Vue.mixin({
beforeCreate() {
if(this.$options.router) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this,'_route',this._router.history.current)
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
})
Object.defineProperty(Vue.prototype,'$route',{
get(){
return this._routerRoot._route
}
})
Vue.component('router-view')
Vue.component('router-link')
}
3. 路由就是匹配一个路径显示对应的组件 matcher用来匹配
export default class VueRouter {
constructor(options) {
this.matcher = createMatcher(options.routes || []);
this.history = new HashHistory(this)
}
init(app) {
}
}
function createMatcher(routes) {
let {pathList,pathMap} = createRouteMap(routes);
function match(){}
function addRoutes(routes) {
createRouteMap(routes,pathList,pathMap)
}
return {match, addRoutes}
}
4. 路由匹配器 创建映射关系(pathMap)
function createRouteMap(routes,oldPathList,oldPathMap){
let pathList = oldPathList || [];
let pathMap = oldPathMap || Object.create(null);
routers.forEach(route => addRouteRecord(route, pathList, pathMap))
return {pathList, pathMap}
}
5. 浏览器路由系统 mode模式 history hash
class History {
constructor(router) {
this.router = router;
this.current = createRoute(null, {path: '/'})
this.cb = null;
}
transitionTo(location, onComplete) {
let route = this.router.match(location)
this.updateRoute(route);
onComplete && onComplete()
}
updateRoute(route) {
this.current = route
this.cb && this.cb(route);
}
listen(cb) {
this.cb = cb;
}
}
class HashHistory extends History {
constructor(router) {
super(router);
}
getCurrentLocation() {
return getHash();
}
setupListener() {
window.addEventListener('hashchange', () => {this.transitionTo(getHash())})
}
}
class VueRouter{
init(app) {
const history = this.history
const setupHashListener = () => { history.setupListener() }
history.transitionTo(
history.getCurrentLocation(),
setupHashListener
)
history.listen((route) => { app._route = route })
}
}
1. new Vue的过程中给根实例添加router属性 在install方法中通过mixin混入 每个实例都可以获取到router属性 router-link router-view 两个全局组件
2. 根据用户传入的配置 创建一个匹配器matcher options.routes 映射表(pathMap) 用来匹配match(根据path找到对应的记录record)和动态添加路由
3. 根据mode创建不同的路由系统 hash history hash使用hashchange popstate 默认跳转到对应的路径 hash性能不如popstate 可以使用popstate替代hashchange
4. 路由初始化 路由匹配到对应的路径 切换路径 setupListener
5. 路由history基类实现transitionTo(核心逻辑) 根据路径match(根据路径找到记录)匹配到组件渲染 数据变化更新视图 current响应性对象 Vue.util.defineReactive(this,'_route',this._router.history.current)
6. 数据变化了视图更新(响应式数据) app._route = route 更新_route改变了响应式数据
7. router-view 函数组件(没有this) 根据matched 渲染对应的router-view record.component
8. router-link 默认a标签 to路径 点击事件 push 插槽this.$slots.default
9. 钩子函数(导航守卫) (to, from ,next) => {} hooks = [fn1, fn2] runQueue()
18. Vue事件修饰符有哪些?其实现原理是什么
.capture .once .passive .stop .self .prevent
function addHandler() {
modifiers = modifiers || emptyObject
if(modifiers.capture) {
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if(modifiers.once) {
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
if (modifiers.passive) {
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}
}
const modifierCode: { [key: string]: string } = {
stop: '$event.stopPropagation();',
prevent: '$event.preventDefault();',
self: genGuard(`$event.target !== $event.currentTarget`),
}
return `function($event){${code}${handlerCode}}`
const genGuard = condition => `if(${condition})return null;`
for(name in on) {
event = normalizeEvent(name)
}
19. slot的用法和实现原理
// 插件分为普通插槽(模板传入到组件中 数据采用父组件数据)和作用域插槽(在父组件中访问子组件数据)
vue3是统一为slot
1. 普通插槽 在父组件中渲染 调用_t的时候直接使用生成好的内容替换
const compiler = require('vue-template-compiler')
// 父组件直接渲染了 构成一个映射关系 {a: a, b: b, c: c}
const {render} = compiler.compile(`
<div>
<div slot="a">a</div>
<div slot="b">a</div>
default
</div>`
)
// with(this){return _c('div',[_c('div',{attrs:{"slot":"a"},slot:"a"},[_v("a")]),_v(" "),_c('div',{attrs:{"slot":"b"},slot:"b"},[_v("a")]),_v("\n default\n")])}
console.log(render) // 立即渲染 插入到子组件中
// 在子组件中使用slot
const {render} = compiler.compile(`
<div>
<slot name='a'></slot>
<slot name='b'></slot>
<slot></slot>
</div>
`)'
// with(this){return _c('div',[_t("a"),_v(" "),_t("b"),_v(" "),_t("default")],2)}
console.log(render) // _t('a') 渲染a插槽 使用父组件中渲染好的内容替换
2. 作用域插槽(父组件中可以访问子组件数据) 父组件中没有渲染 在子组件中执行函数渲染
const {render} = compiler.compile(`
<div slot-scope="msg">{{msg.a}}</div>
`)
// fn: function(msg){return _c('div',{},[_v(_s(msg.a))])} 函数在父组件中没有渲染
console.log(render)
// 子组件使用
const {render} = compiler.compile(`
<div><slot a='1'></slot></div>
`)
// 调用方法 传入参数 然后使用内容替换 在子组件中渲染的
// with(this){return _c('div',[_t("default",null,{"a":"1"})],2)}
console.log(render)
20. keep-alive的使用场景和原理
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null)
this.keys = []
}
mounted() {
this.$watch('include', val => {})
this.$watch('exclude', val => {})
}
render() {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
if(componentOptions) {
if( (include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) return vnode
const { cache, keys } = this
if(cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
} else {
cache[key] = vnode
keys.push(key)
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
21. v-model的实现原理 .sync修饰符的作用实现原理
v-model可以使用在组件上也可以使用在原生input select上 一般可以理解为value+input的语法糖
.sync 也是一种语法糖 v-bind.sync="title"
绑定一个属性和一个更新函数 v-bind:title v-on:update:title
22. 自定义指令的实现原理
// ASSET_TYPES: components directive filter
ASSET_TYPES.forEach(type => {
Vue[type] = function(id, definition) {
if(type === 'component') { // 组件的处理
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') { // 指令的处理
definition = { bind: definition, update: definition } // 将函数放到bind和update钩子上
}
this.options[type + 's'][id] = definition // 将指令的定义绑定在Vue.options上
return definition
}
})
// 自定义指令 编译 => 代码生成 => 指令钩子
1. 在生成ast语法树的时候 遇到指令就会给当前元素添加directives属性
function addDirective() { // 添加指令 变成一个对象
(el.directives || (el.directives = [])).push(rangeSetItem({})
}
2. 通过genDirectives生成指令代码
function genDirectives () {
let res = 'directives:['
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
return res.slice(0, -1) + ']'
}
3. 在patch前将指令的钩子提取到cbs中再patch过程中调用对应的构造
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
for(i = 0
cbs[hooks[i]] = []
for (j = 0
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
4. 在执行指令对应的钩子函数时 调用对应指令定义的方法
function updateDirectives() {
_update(oldVnode, vnode)
}
function _update() {
callHook() // 执行对应的钩子
}
23. $refs是如何实现的
将真实的dom或者组件实例挂载到当前实例的$refs属性上
function registerRef(vnode) {
const key = vnode.data.ref
const vm = vnode.context
const ref = vnode.componentInstance || vnode.elm
const refs = vm.$refs
refs[key] = ref
}
24. vue中使用的设计模式有哪些
工厂模式 传入模式即可创建实例 createElement 根据传入的参数不同返回不同的实例
单例模式 整个程序中有且仅有一个实例
发布订阅者模式 事件系统
观察者模式 watcher和dep
代理模式 vue3的proxy
中介者模式 vuex 中介者是一个行为设计模式 通过提供一个统一的接口让系统不同部分通信
策略模式 合并策略 mergeOptions
外观模式、适配器模式、迭代器模式、模板方法模式
25. 谈谈对vue性能优化的理解
todo
ssr服务端渲染 pwa WebComponent
单元测试
vue-loader主要做了什么
vue-cli常见配置(webpack部分)
如何封装好一个组件 业务组件和通用组件的封装
组件库搭建 脚手架搭建原理
项目组遇到哪些难点和解决方案
vue3优化点 响应式数据proxy effect 调度 自定义渲染器 hook ...