vue2必知必会

258 阅读8分钟

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) {
    // 不停的向上查找 解析属性 while()一直找$parent
    const result = resolveInject(vm,$options,inject, vm)
    // 遍历找到的属性定义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.vuejs.org/zh/guide/ 主要流程 state mutation action getters namespace
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
  })
  // 访问的时候  return this._vm._data.$$data
  mutation和action都是发布订阅模式 在初始化的时候通过传入的options保存在数组中 我们调用dispatch和commit的时候触发对应的回调
  getter使用了Object.defineProperty重新定义返回value[this.state] 借助vue中的计算属性computed
  // 遍历options.getters
  computed[key] = () => return value.call(this,this.state) // value是getter对应的fn 靠coomputed属性做优化
  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: {}}}
// 1.参数的格式化 在initState中会对options的watch做初始化操作
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 { // key-value
      createWatcher(vm, key, handler)
    }
  }
}
// 调用原型上的$watch方法 借助vue响应式原理  watcher dep
function createWatcher(vm, key, handler, options) {
  if(isObject(handler)) { // handler是对象
    options = handler // 配置项options
    handler = handler.handler
  }
  if(typeof handler === 'string') { // 在methods中取对应的方法
    handler = vm[handler]
  }
  return vm.$watch(key, handler, options) // watch的原理就是$watch
}
// 2.$watch的实现
class Watch {
  constructor(vm, exprOrFn, cb, options) {
    this.user = options.user // 用户自定义watcher
    this.sync = options.sync // 同步执行的
    exprOrFn可能是表达式需要变成函数
    if(typeof exprOrFn === 'function') {
      this.getter = exprOrFn // 保留之前的逻辑
    } else {
      this.getter = function() {
        // 变成函数根据表达式在vm上取值 调用get就会取值
      }
    }
    this.value = this.get()
  }
  // 在执行get函数的时候还是做watch的收集
  get() {
    pushTarget(this)
    const value = this.getter.call(this.vm)
    popTarget(this)
    return value
  }
  // update的时候如果是sync的同步就不放到队列中直接执行run方法
  update() {
    if(this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  // 在更新的时候如果是用户watch执行callback
  run() {
    let value = this.get()
    let oldValue = this.value
    this.value = value
    if(this.user) this.callback.call(this.vm, value, oldValue)
  }
  
}

// computed 计算属性 computed: {fullName() {return this.a + this.b}}
// watch在内部会对变量取值 computed默认是不会取值的 对computed中的值取值了才会执行get
// 缓存效果 
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 }) // lazy标识计算属性
    // 计算属性可以直接通过vm来取值 将属性定义到实例上
    defineComputed(vm, key, userDef)
  }
}
// 定义计算属性watcher
function defineComputed(vm, key, userDef) {
  if (typeof userDef === 'function') {
    // sharedPropertyDefinition 属性描述器 
    // sharedPropertyDefinition.get = userDef
    // 如果是userDef会多次执行 这要创建缓存getter
    sharedPropertyDefinition.get = createComputedGetter(key)
  } else {
    sharedPropertyDefinition.get = createComputedGetter(userDef.get);
    sharedPropertyDefinition.set = userDef.set || (() => {});
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 2. 创建计算属性get
function createComputedGetter(key) {
  return function() {
    const watcher = this._computedWatchers[key]
    if(watcher) {
      if(watcher.dirty) { // 第一次为true调用get 将dirty设置为false缓存 数据变化了改为true
      // 计算出新值 get取值的时候就会取依赖项的属性就会把watch收集起来 
      // 依赖项收集了计算属性的watcher 当依赖的值发生了变化就将dirty设置为true
        watcher.evaluate() 
      }
      if(Dep.target) {
        watcher.depend() // 还需要收集渲染watcher
      }
      return watcher.value
    }
  }
}
// 3. Watch
class Watch {
  constructor(vm, exprOrFn, cb, options) {
    this.lazy = options.lazy // 计算属性标识
    this.dirty = this.lazy // dirty为true就调用get方法取值
    // 计算属性值创建一个watch不取值
    this.value = this.lazy ? undefined : this.get()
  }
  // 当dirty为true的时候执行取值操作
  evaluate() {
    this.value = this.get()
    this.dirty = false // 起到缓存的作用
  }
  depend() {
    let i = this.deps.length
    while(i--) {
      this.deps[i].depend() // 将watcher存到当前dep中 收集渲染watcher
    }
  }
  // 依赖项发生变化之后执行update将dirty改为true 下次取值就会执行get
  update() {
    if(this.lazy) { // 计算属性
      this.dirty = true
    }
  }
}


// 区别
1. watch默认会取值 computed不会 内部都是基于Watch的
2. computed有缓存效果 依赖项不变就不会重新执行
3. computed变量一般用户模板的渲染 watch一般是执行cb

5. 为什么我们可以直接使用this.xxx访问属性 Vue中组件的data为什么是一个函数?

// 我们在初始化数据的时候将属性都挂到了this上 避免重名 优先级? props methods data computed watch
function Vue(options) {this._init(options)} vue就是一个函数(options api)
// 我们在init过程中会初始化状态数据 Vue.prototype._init = function() {} 在原型上扩展一系列的方法
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. 数据劫持过程 分为数组和对象
// 处理对象和数组 递归处理(不要嵌套太深) 这里也是vue2性能不好 vue3使用proxy
// 在initData(vm)过程中我们会观测数据 observe(data) 在Observe类中

// 对象处理
this.walk(data) 
// 会遍历data中的key执行defineReactive处理
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
// defineReactive会递归的处理使用 defineProperty
function defineReactive(data, key, value) {
  observe(value) // 递归处理
  Object.defineProperty(data, key, {
    get() {
      // 依赖收集
      return value
    }
    set(newValue) {
      if(newValue === value) return
      observe(newValue) // 设置的可能是一个对象
      // 派发更新
      newValue = value
    }
  }
}

// 数组处理
// 我们很少直接操作数组的下标 就没有对数组使用defineProperty拦截
//  在观察数据的时候value可能是数组 我们不走walk逻辑 observe(data)
value.__ob__ = this // 使用这个属性来保存this 重写方法的时候调用observeArray检测新增的元素
if(Array.isArray(data)) {
  // 数组的处理 改写数组的其他能改变原数组的方法 原型链
  value.__proto = arrayMethods
  this.observeArray(value) // 观测数组中的每一项 数组元素可能是对象
} else {
  this.walk(data)
}
// 观察数组中的每一项 但是没有处理push新增的元素
observeArray(value) {
  for(let i = 0; i < value.length; i++) {observe(value[i])}
}
// 重写数组的方法
// 使用aop切片重写数组的方法增加我们自己的逻辑 原型
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)
    // todo 我们新增的元素可能是对象类型的
    const ob = this.__ob__ // value调用的push方法 value上有一个__ob__属性就是this实例
    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
  }
})

// __ob__也是一个对象 这样会导致无限循环 不使用value.__ob__ = this
// 我们看到属性都有一个__ob__属性
// 在观察数据observer(data)的时候如果有__ob__表示已经被观察过了就不处理了 if(data.__ob__) return 
Object.defineProperty(value,'__ob__',{
  enumerable:false,
  configurable:false,
  value:this
}

data() {return {arr: [{name: 'vue'}]}}
// 页面中直接取数组的值 {{arr}} 调用JSON.stringify会对对象取值 里面的值会做依赖收集

// 2.响应式数据变化 (更新数据) watcher dep 依赖收集 派发更新 (effect)
// 3. data=> view view => data变化

// 总结
1. Object.defineProperty数据劫持
2. 数组利用原型链重写数组的方法实现
3. 内部依赖收集和派发更新过程 dep和watcher相互订阅
4. vue3的proxy
5. 性能优化 层级 Object.freeze()冻结

7. $set实现原理

function set(target, key, val) {
  if(Array.isArray(target)) { // 数组使用splice方法 splice已经改写了
    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
  }
  // 将属性定义成响应式的 Object.defineProperty
  defineReactive(ob.value, key, val)
  // 手动通知视图更新
  ob.dep.notify()
  return val
}

// 总结
当我们给一个数据增加属性时该属性不是响应式的我们可以通过$set添加
1. 对于数组内部使用了splice方法(我们重写的方法)
2. 对于对象我们使用了Object.defineProperty定义添加的属性
3. 通过ob.dep.notofy() 手动通知视图更新

8. 简述Vue中模板编译原理 vue-loader主要做了什么

// 将模板template编译成render函数
const render = compileToFunction(template)
// 编译三步曲 vue-loader使用vue-template-compiler模块
1. parse() 对模板做解析生成ast抽象语法树 
  // createASTElement()解析一部分advance(前进)一部分 直到结束<开始  一系列的正则匹配(有限状态机)
  // 生成ast{tag:'',type:'',children: [], attrs: [], parent:''}
  // 标签匹配 stack 有效括号
  parseHTML(template, options)
  while(html) {}
2. optimize() 优化ast语法树 markUp 静态节点 静态根 static
3. codegen ast树重新生成代码 genData genChildren 
  // 核心 字符串拼接
  let code = generate(root)
  // with(this) + new Function() eval不干净的执行 作用域
  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) // 根据虚拟节点生成真实dom
    parentElm.insertBefore(el,oldElm.nextSibling) // 插入元素
    // 更新走diff过程
  }
  function createElm(vnode) {
    const {tag} = vnode
    vnode.el = document.createElement(tag)
    return vnode.el // 我们在使用自定义指令的时候可以通过vnode拿到真实的dom节点
  }

3. 通过渲染watcher执行上面函数 
  let updateComponent = () => {vm._update(vm._render())}
  // 默认vue是通过watcher来进行渲染的 每一个组件都有一个渲染watcher 执行updateComponent
  new Watcher(vm, updateComponent, () => {}, true) // true表示是渲染watcher
  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 {
  // 在执行fn的时候使用get函数
  constructor(vm, exprOrFn, cb, options) {
    this.id = id++
    this.getter = exprOrFn
    this.deps = [] // 记住dep的数组
    this.depsId = new Set() // 去重
    this.get()
  }
  get() {
    pushTarget(this) // Dep.target = watcher
    this.getter() // vm._update(vm._render()) 渲染watcher
    popTarget(this) // Dep.target = null
  }
  addDep(dep) {
    let id = dep.id
    if(!this.depsId.has(id)) { // 去重
      this.deps.push(dep)
      this.depsId.add(id)
      dep.addSub(this) // 让dep记住watcher dep去重了watcher就不需要去重了 watch记住dep
    }
  }
}

let id = 0
Dep.target = null
class Dep {
  constructor() {
    this.id = id++ // 唯一的id
    this.subs = []  // dep记住watcher
  }
  depend() {
    Dep.target.addDep(this) // watcher.addDep() 让watcher记住dep
  }
  addSub(watcher) {
    this.subs.push(watcher) // dep记住对应的watcher 这样就相互有关系了
  }
}

// 在取值的时候让属性的dep记住这个watcher
function defineReactive(data, key, value) {
  let dep = new Dep()
  Object.defineProperty(data, key, {
    get() {
      if(dep.target) dep.depend() // 让属性的dep记住watcher 数据变化之后就通知对应的watcher
      return value
    },
    set(newValue) {
      observe(newValue)
      value = newValue
      dep.notify() // 通知dep中记录的watcher去更新
    }
  }
}
// 数据发生变化在set中使用notify来通知watcher更新
class Dep {
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}
class Watcher {
  update() {
    this.cleanupDeps() // 清除deps
    this.get() // 先暴力的使用get方法更新 批量更新
  }
}

// 数组 数组的拦截是重写的方式 响应式也是需要在get收集依赖 在重写的方法派发更新
class Observer {
  constructor(value) {
    // 数组不会给index添加dep所以给数组本身添加一个dep
    this.dep = new Dep() // 给对象本身添加一个dep属性 defineReactive中的dep是给对象的属性添加的
  }
}

// 依赖收集
function defineReactive(data, key, value) {
  let dep = new Dep() // 给属性添加的dep
  let childOb = observe(value) // observe返回的是Observer实例
  Object.defineProperty(data, key, {
    get() {
      if(Dep.target) {
        dep.depend() 
        // childOb 可能是对象也可能是数组 $set() 依赖收集
        if(childOb) {
          childOb.dep.depend() // 对数组取值 将当前的watcher和数组进行关联
          // 嵌套数组
          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就是当前的实例
  ob.dep.notify()
  return result
}

// 总结
1. 

11. vue如何实现批量更新 vue更新原理

// 当数据变化触发set通知对应的watcher做更新的时候 我们需要批量更新
class Watcher {
  update() {
    // this.get() // 这样每个属性都会走一遍更新的流程 会多次的渲染 我们要实现一个异步更新
    queueWatcher(this)
  }
  run() {
    this.get() // run方法真正执行的地方
  }
}
// 异步更新 批量更新
let queue = [], has = {}
function queueWatcher() {
  let id = watcher.id
  if(has[id] === null) { // 去重
    queue.push(watcher)
    has[id] = true
    // 异步更新
    setTimeout(flushSchedularWatcher, 0) // 清空队列
  }
}
function flushSchedularWatcher () {
  // 清空queue遍历执行更新
  for(let i = 0; i < queue.length; i++) {
    let watcher = queue[i]
    watcher.run() // this.get()
  }
  queue = []
  has = {}
}

12. nextTick在哪里使用?原理是?

vue多次更新数据 最终进行批处理更新 内部使用nextTick实现延迟更新 自定义的nextTick的回调会被延迟
到更新完成之后调用 可以获取到更新的dom
// 优雅降级 promise,mutationObserver,setImmediate,setTimeout 宏任务和微任务
// 使用nextTick来更新
let pending  = false
function queueWatcher() {
  if(!pending) {
    pending = true // 保证只有一个nextTick
    nextTick(flushSchedularWatcher)
  }
}
function flushSchedularWatcher () {
  queue.forEach(item => item.run())
  pending = false
}

let cbs = [], waiting = false
function nextTick(cb) {
  cbs.push(cb) // 默认的cb是渲染逻辑 用户的逻辑放在渲染之后就行了
  if(!waiting) {
     waiting = true
     Promise.resolve().then(flushCallbacks) // vue3直接使用Promise.then()
  }
}
function flushCallbacks() {
  cbs.forEach(item => item())
  waiting = false
}

13. Vue的生命周期方法有哪些 生命周期钩子是如何实现的

// beforeCreate(mixin) created beforeMount mounted beforeUpdate updated beforeDestroy destroyed
// 生命周期中的this都是实例vm 为什么不能使用箭头函数
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() } // 执行beforeUpdate
  watcher.run() // 更新操作 执行get
  callUpdatedHooks(updatedQueue)
  // 遍历queue执行更新钩子
  callHook(vm, 'updated')

4. 在组件销毁过程中 $destroy
  callHook(vm, 'beforeDestroy')
  // 执行一系列的销毁
  vm.__patch__(vm._vnode, null)
  callHook(vm, 'destroyed')

// callHook就是遍历执行
function callHook(vm, hook) {
  const handlers = vm.$options[hook];
  if(handlers) {
    for(let i = 0; i < handlers.length; i++) {
      handlers[i].call(vm) // 绑定this为当前的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 // 返回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() {}

// extend

15. vue的组件化 组件渲染流程

// 查找规则 先找自己的没有就找父亲的 __proto__ 核心Object.create()
Vue.component(componentName, definition) Vue.extend 通过对象创建一个类 类去创建组件
components:{}

// 1. Vue.component调用Vue.extend生成子类构造器Ctor
Vue.options._base = Vue // vue的构造函数
Vue.options.components = {} // 用来存放组件的定义
Vue.component = function(id, definition) {
  definition.name = definition.name || id
  definition = this.options._base.extend(definition) // 保证是Vue.extend
  // 将组件定义放到Vue.options.components上
  this.options.components[id] = definition
}
// 2.Vue.extend创建出一个子类Sub返回这个类 原型继承Vue的构造器返回Sub
let cid = 0;
Vue.extend = function(extendOptions) {
  extendOptions = extendOptions || {}
  const Super = this // Vue
  const SuperId = Super.cid
  // cachedCtors // 缓存 避免多次构造
  const Sub = function VueComponent(options) {
    this._init(options) // 初始组件化调用init方法
    // vm.$options = mergeOptions(vm.constructor.options,options);
  }
  Sub.cid = cid++
  Sub.prototype = Object.create(Super.prototype) // 原型继承 通过通过Vue继承的 父子关系
  Sub.prototype.constructor = Sub
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.component = Super.component
  // 每次声明组件 都会把父级的定义放在了自己的身上
  Sub.options = mergeOptions(Super.options, extendOptions) // 合并选项
  return Sub
}
// 3.mountComponent执行vm._update(vm._render())在render过程中会创建元素的vnode
function createElement(vm, tag, data = {}, ...children) {
  if(typeof tag === 'string') {
    // tag为字符串的时候也可能是组件
    if(isReservedTag(tag)) {
      return vnode(tag, data, children,undefined, undefined, context); // 原始标签
    } else {
      // 组件的渲染 拿到组件的定义 通过组件的定义创建虚拟节点
      const Ctor = vm.$options.components[tag] // 内部是对象 外部是Vue.extend的函数
      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)
};
// 4. 创建组件的vnode 组件的核心就是extend
function createComponent(vm, tag, data, children, Ctor) {
  if(isObject(Ctor)) Ctor = vm.$options._base.extend(Ctor) // 都变成函数 子类构造函数
  // resolveConstructorOptions(Ctor)
  // installComponentHooks(data) // 安装组件钩子函数
  data.hook = {
    init(vnode) {
      // let child = vnode.componentInstance = createComponentInstanceForVnode(vnode)
      // 实例化Ctor然后就会执行init方法
      let child = vnode.componentInstance = new vnode.componentOptions.Ctor({}) 
      child.$mount() // 手动挂载没有app不是挂载到真实的节点 mountComponent(vm)
    },
    prepatch(oldVnode, vnode){},
    insert(vnode){},
    destroy(vnode){}
  }
  const name = Ctor.options.name || tag;
  const vnode = new Vnode( // 实例化组件vnode
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, // 组件的唯一标识
    data, undefined, undefined, undefined, context, // 组件vnode是没有children的
    { Ctor, propsData, listeners, tag, children } // 组件选项 children是在这里面的 非常主要的Ctor属性
  )
  return vnode
}
// 5.生成vnode之前执行_update到patch函数 vm.patch将vnode转换成真正的dom节点
function patch(oldVnode, newVnode) {
  if(!oldVnode) return createElm(newVnode) // 直接根据vnode创建
}
function createElm(vnode) {
  if(typeof tag === 'string') {
    // tag为字符串的时候也可能是组件 组件上有hook 执行init函数
    return vnode.componentInstance.$el // 执行了init方法 返回组件内容
  }
}
function createComponent(vnode) {
  let i = vnode.data
  if ((i = i.hook) && (i = i.init)) {
    i(vnode) // 执行init的hook函数 vnode.componentInstance.$el
  }
  if (vnode.componentInstance) { // 执行了init方法就有组件实例
    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

// dom diff 两个虚拟节点的比对 同级比较的 patch
// 在数据更新的时候
function _update(vnode) {
  const vm  = this
  const prevVnode = vm._vnode; // 保留上一次的vnode
  vm._vnode = vnode;
  if(!prevVnode) {
    vm.$el = patch(vm.$el,vnode) // 初次渲染
  } else {
    vm.$el = patch(prevVnode,vnode) // dom diff比对
  }
}
// patch更新
function patch(oldVnode, newVnode) {
  if (isRealElement) {
    // 原始标签
  } else {
    // diff算法 两个虚拟节点的比对
    // 1.两个虚拟节点的标签不一样 div => p 直接用新的替换老的
    if (oldVnode.tag !== newVnode.tag) {
      return oldVnode.el.parentNode.replaceChild(createElm(newVnode), oldVnode.el)
    }

    // 2.标签一样 但是是两个文本元素 tag都为undefined
    if (!oldVnode.tag) {
      if (oldVnode.text !== vnode.text) {
        oldVnode.el.textContent = vnode.text // 标签一致 文本元素比较文本的内容
      }
    }

    // 3. 标签一样 需要比对属性和children 复用标签
    let el = newVnode.el = oldVnode.el
    // 比对属性 老的属性有新的没有直接删除 新的属性有老的没有就添加
    updateProperties(vnode, oldVnode.data)

    // 4. 更新儿子 新的有儿子老的也有儿子
    let oldChildren = oldVnode.children || []
    // 4.1 都有儿子 dom diff
    if (oldChildren.length > 0 && newChildren.length > 0) {
      updateChildren(el, oldChildren, newChildren)
    }
    // 4.2 老的有儿子新的没有儿子 直接删掉
    else if (oldChildren.length > 0) {
      el.innerHTML = ''
    }
    // 4.3 老的没有新的有 直接新增
    else if (newChildren.length > 0) {
      for (let i = 0; i < newChildren.length; i++) {
        let child = newChildren[i];
        el.appendChild(createElm(child));
      }
    }
  }
}

// 核心的diff算法 双指针
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 {
      // 为什么v-for需要增加key key为什么不能使用index
      // 1.当前索引和老的key的关系 移动的时候通过新的key找到索引 找到老的节点移动
      let moveIndex = map[newStartVnode.key] // 在老的map中找
      if (moveIndex == undefined) { // 老的中没有将新元素插入
        parent.insertBefore(createElm(newStartVnode), oldStartVnode.el)
      } else { // 有的话做移动操作
        let moveVnode = oldChildren[moveIndex] // 找到需要移动的vnode
        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;
      // insertBefore传入null相当于appendChild
      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://router.vuejs.org/zh/

1. Vue.use()原理
Vue.use(Router)
// vue主要是执行install方法 vue-router需要提供一个install方法
Vue.use = function(plugin, options) {
  plugin.install(this, options)
}
// vue-router的使用
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() // 执行install方法
    VueRouter.install = install
  2. install方法 通过mixin混入 router属性(实例对象) 初始化init
    function install(Vue) {
      Vue.mixin({
        beforeCreate() {
          if(this.$options.router) { // new Vue的时候router挂载到了根组件上
            this._routerRoot = this
            this._router = this.$options.router
            this._router.init(this) // 初始化路由 实例的init方法
            // 定义成响应式数据
            Vue.util.defineReactive(this,'_route',this._router.history.current)
          } else {
            // 保证所有子组件都有_routerRoot属性 指向根实例 this._routerRoot._router可以拿到路由实例对象
            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) {
        // 根据用户options中传递的routes创建匹配关系 生成一个map映射表
        // match:match方法用来匹配规则 addRoutes:用来动态添加路由
        this.matcher = createMatcher(options.routes || []);
        // 路由系统 mode属性
        this.history = new HashHistory(this)
      }
      init(app) { // 实例的init方法

      }
    }
    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); // 更新current后 更新_route属性
      }
      listen(cb) { // app._route = route
        this.cb = cb; // 注册函数
      }
    }
    // hash路由
    class HashHistory extends History {
      constructor(router) {
        super(router);
      }
      getCurrentLocation() { // 获取当前路径
        return getHash(); // return window.location.hash.slice(1);
      }
      setupListener() { // 根据当前的hash值切换到对应路径
        window.addEventListener('hashchange', () => {this.transitionTo(getHash())})
      }
    }
    // 路由初始化过程中
    class VueRouter{
      init(app) {
        const history = this.history
        const setupHashListener = () => { history.setupListener() }
        history.transitionTo( // 跳转路径 路径和cb
          history.getCurrentLocation(), // 子类获取对应的路径
          setupHashListener
        )
        // 路径变化的时候 更改了_route属性 响应式数据变化了就会更新视图 
        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) // $route => this._routerRoot._route
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
// 1.生成ast语法树处理
function addHandler() { // 添加事件处理
  modifiers = modifiers || emptyObject
  if(modifiers.capture) { // capture添加!
    delete modifiers.capture
    name = prependModifierMarker('!', name, dynamic)
  }
  if(modifiers.once) { // once添加~
    delete modifiers.once
    name = prependModifierMarker('~', name, dynamic)
  }
  if (modifiers.passive) { // 如果是passive 加&
    delete modifiers.passive
    name = prependModifierMarker('&', name, dynamic)
  }
}
// 2.codeGen过程的处理
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;`
// on事件处理
for(name in on) {
  // 处理& ! ~ 内部使用addEventListener
  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的使用场景和原理

// 主要是缓存 采用LRU算法 最近最久未使用法 抽象组件
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; i < hooks.length; ++i) {
  cbs[hooks[i]] = []
  for (j = 0; j < modules.length; ++j) { 
    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 // 获取ref
  const vm = vnode.context
  const ref = vnode.componentInstance || vnode.elm // 如果是组件则采用实例 否则真是dom
  const refs = vm.$refs
  // v-for中就是数组
  refs[key] = ref // 保存在$refs属性上
}

24. vue中使用的设计模式有哪些

工厂模式  传入模式即可创建实例 createElement 根据传入的参数不同返回不同的实例
单例模式 整个程序中有且仅有一个实例
发布订阅者模式 事件系统
观察者模式 watcher和dep
代理模式 vue3的proxy
中介者模式 vuex 中介者是一个行为设计模式 通过提供一个统一的接口让系统不同部分通信
策略模式 合并策略 mergeOptions
外观模式、适配器模式、迭代器模式、模板方法模式

25. 谈谈对vue性能优化的理解

todo

ssr服务端渲染 pwa WebComponent
单元测试
vue-loader主要做了什么
vue-cli常见配置(webpack部分)
如何封装好一个组件 业务组件和通用组件的封装
组件库搭建 脚手架搭建原理
项目组遇到哪些难点和解决方案
vue3优化点 响应式数据proxy effect 调度 自定义渲染器 hook ...