小笔记(vue篇)

201 阅读12分钟

笔记打算记录,自己整理好的面试题,
争取保持准确性和专业性,然后用直白的语言描述出来,
希望能做成一个API文档,让我日后翻到能直接对着复习就好,
如果也能帮助到你就最好了(我会不定时的更新的)

vue响应式的理解

我认为vue核心的地方是做这几件事:

  1. 通过改造data变量,实现知道变量被什么函数所依赖
  2. 当变量发生改变时,调用依赖该变量的函数

围绕这2个核心点,针对变量改造:

  • vue2是对data变量通过循环进行判断
  1. 当遇到数组类型,是通过复制数组原型方法,然后进行改写来收集和派发(共7个方法)

  2. 当遇到对象类型,是通过Object.defineProperty的方法,在get和set方法中加入逻辑

  • vue3的核心部分不变,改造方法有发生变化(主动提供2个API方法 ref , reactive
  1. 实例一个 ref 的类,类中有get和set方法的逻辑
  2. reactive只支持对象类型,通过 proxy 的API,在handler中使用 reflect 对对象的属性进行对应逻辑操作

被收集的函数是指由vue实例生成的 watcher类:

  1. template模板是渲染函数,称为 render watcher
  2. 用户定义的computed,本质是一个 watcher类 称为 computed watcher
  3. 用户定义的watch,也是一个 watcher类 称为 user watcher

vue 响应式变量改造的经历了什么

响应式改造(initState)的顺序是 props - method - data - computed - watch

  • vue2中是经历了以下几步
  1. 先将 data中的所有变量代理到 this上,此处会执行一次Object.defineProperty,实现在后续的代码中,通过 this.a 则能访问到 data属性中的 a变量
//初始化data
function initData(vm) {
  let data = vm.$options.data;
  //实例中的 _data 等于传入的data
  //data推荐使用function   (call方法理解为 在vm中 执行 data()的函数)
  data = vm._data = typeof data === "function" ? data.call(vm) : (data || {});

  //把data数据代理到this上, 实现this.a = this._data.a  (如何实现 this.a 访问到数据)
  for (let key in data) {
    proxy(vm, "_data", key);
  }

  //对数据进行观测
  observe(data);
}

//数据代理
function proxy(object, sourceKey, key) {

  //实现 this.key 返回 this.sourceKey.key;
  Object.defineProperty(object, key, {
    get() {
      return object[sourceKey][key];
    },
    set(newValue) {
      object[sourceKey][key] = newValue;
    }
  })
}
  1. 对变量进行改造,将会生成一个 Observer类(收集依赖和派发更新的作用),在实例 class的过程中会通过递归循环的模式查询出 data中所有对象和数组,然后对其再进行改造
export function observe(value) {
  if (
      Object.prototype.toString.call(value) === '[object Object]' || 
      Array.isArray(value)
      ) {
    return new Observer(value);
  }
}
  1. Observer类中会做几件事:创建Dep类、定义一个__ob__(指向自己)的属性、分开处理数组和对象
export class Observer {
  constructor(value) {
    this.value = value;
    //创建Dep实例
    this.dep = new Dep();

    //创建一个__ob__ 存储自己
    def(value, '__ob__', this);

    //处理数组
    if (Array.isArray(value)) {
      this.observeArray(value);
    }
    //处理对象
    else {
      this.walk(value);
    }
  }

  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      //属性改造
      defineReactive(obj, keys[i]);
    }
  }

  //数组
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}
  1. 关于数组的改造:是通过复制一份数组的原型方法(对其中的7个方法写入逻辑,push ,pop,shift,unshift,splice,sort,reverse),在函数中,获取事先定义好的 __ob__属性,数组新增的项时会通过 ob属性 调用实例中的方法进行响应式改造,然后通过 ob属性 调用notify进行派发更新(ob属性是作为判断变量是否改造的标识)

  2. 关于对象的改造:对象内部的每一个属性都会新建一个 Dep类,然后通过Object.defineProperty,在 get方法(收集依赖)和 set方法(派发更新指令)中写入逻辑,然后完成基础的改造

  3. 其中如果直接给变量赋值新的对象,会触发 set方法直接对 新的整个对象 进行响应式改造 (可避开单独使用下标赋值导致响应式中断的问题)

export function defineReactive(obj, key, val, customSetter, shallow) {
  const dep = new Dep();
  
  //...

  //递归处理
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val

      //如果有watcher
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }

      return value
    },
    set: function reactiveSetter(newVal) {
      
      //...

      //新值是对象 添加响应
      childOb = !shallow && observe(newVal);
      //更新
      dep.notify();
    }
  })

}
  • vue3中的改造是由用户使用API来定义的,reactive只支持定义对象变量,ref支持定义任何类型(如果reactive的参数不是对象时,会报错拦截)
  1. ref() 是通过 new 一个变量名为Ref(类)来完成改造,实例的类中有get方法set方法的逻辑(使用 变量.value 进行访问)。实例化时对变量参数有一个判断,如果调用 ref 的参数是对象类型的变量,内部会使用 reactive 的方法
const toReactive = (value) => isObject(value) ? reactive(value) : value;

class RefImpl {
    constructor(value, __v_isShallow) {
      //...
      this._value = __v_isShallow ? value : toReactive(value);
    }
    get value() {
      trackRefValue(this);
      return this._value;
    }
    set value(newVal) {
      //...
    }
  }
  1. reactive() 是通过 new Proxy 的方法将对象变量进行拦截改造,并在handler中配合 Reflect 对对象内部的属性进行访问

后续文档更推荐全程使用 ref 的API进行变量定义,其中的理由有几点

  1. 因为ref定义的变量,全部都需要配合 .value 使用,更加的统一
  2. reactive() 只能定义对象类型,稍显局限性
  3. reactive 定义的变量,对属性进行简单解构时,会断开响应式关联,需配合 toRef 使用
  4. reactive 定义的变量进行赋值时,只能通过一个个属性改变的形式,或者使用 Object.assign 进行赋值,否则会断开关联

Dep类、Watcher类的理解(如何收集依赖、computed、watch的理解)

  • Dep类(变量用于收集函数)和Watcher类(函数)是相互的,收集后当变量有改变则触发函数调用,各自都有一个id标识,用于收集是去重使用

Dep类是在2个地方会生成,(1个是对整个对象类型的变量、1个是对象的每一个属性)都会 new 一个Dep实例。Dep类中有一个全局 target属性(该属性是用于指向函数 watcher类),Dep类中 使用数组结构(subs)进行依赖收集(提供方法 depend、addSub),并且提供一个 notify 触发更新的方法

notify() {
    //执行依赖 watcher的update
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
}

watcher类分为三种:

  1. render watcher
  2. computed watcher(我们写的computed属性)
  3. user watcher(我们写的watch属性)

当我们实例化watcher时,会触发 get方法保存其 value信息,并将Dep.target指向实例后的 watcher类(this)

  • user watcher(watch) 提供了 immediate属性deep属性,并且return 一个 teardown的函数,用于销毁watcher类。
    immediate:实例化时立刻调用 watch的 cb方法
    deep:实现原理是通过循环递归对象,访问每一个对象变量的形式(等于是触发一次get方法),实现将当前 的 user watcher类 都存到每一个对象属性的 Dep类 中。收集依赖的过程中,其中使用 Set 作为存储堆栈,保证 DepId 唯一(防止对象中的属性重复引用)。使用 deep 循环时,代码拦截了基础类型的变量
function $watch(data, expOrFn, handler, options) {
  const watcher = new Watcher(data, expOrFn, handler, options);
  
  //如果为 immediate (立刻执行)
  if (options.immediate) {
    handler.call(data, watcher.value);
  }
  
}

function _traverse(val, seen) {
  let i, keys;
  const isA = Array.isArray(val);
  //如果不是数组 不是对象 直接触发对象属性的get(进行watcher收集) 然后return掉
  if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
    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);
  }
}
  • computed watcher(computed)增加了 lazydirty 的标识,实例后 value值 不会立即计算。computed watcher 创建新的get方法和set方法,通过 Object.defineProperty 方法在 vm实例中新增变量(this.computed属性)。

  • 访问 computed属性时(如果内部有 响应式变量),初始化的时候 lazy属性为true,执行 computed属性的 getter方法,完成属性 Dep 和 computed watcher 的收集

  • 更新的逻辑是:当计算属性内部的 响应式变量 发生变化时,会触发computed 的 update方法,将会修改其 dirty标识(改为true)。此时仍然不会触发 computed属性的更新。只有当下一次访问到 computed属性时,在 get方法中会判断 dirty标识,如果有更新了则执行 evaluate函数 进行更新。(如果内部依赖的属性未发生变化,就不会触发更新,所以说 computed属性拥有缓存)

//初始化 computed属性
function initComputed(vm) {
  for (const key in computed) {
    watchers[key] = new Watcher(vm, { lazy: true })
  }
}

//实例化 (计算属性不需要调用get方法)
class Watcher {
  //初始化的时候 lazy的值为true
  this.dirty = this.lazy;
  this.value = this.lazy ? undefined : this.get();
  
  update() {
    //计算属性发生变化 重置标识
    if (this.lazy) {
      this.dirty = true;
    }
  }
  
  //重新计算
  evaluate() {
    this.value = this.get();
    this.dirty = false;
  }
}

//computed
//重写计算属性
function createComputedGetter(key) {
  return function () {
    const watcher = this._computedWatchers[key];
    if (watcher) {
      //值的计算
      if (watcher.dirty) {
        watcher.evaluate();
      }
      
      //完成依赖收集
      if (Dep.target) {
        //如果dep有target
        watcher.depend();
      }

      return watcher.value;
    }
  }
}
  • Dep 和 Watcher 如何进行依赖收集?

在 template 模版中的字符串,通过字符串逐个判断,在解析过程中生产对应的 AST树,然后从 AST树 parser的过程中,进行收集。

data属性为什么是函数

如果data使用普通对象,当组件复用时,修改data变量(因为对象是引用类型的),会出现修一处动全身的问题,全被影响了。使用函数 return对象的形式,可以保证每一个组件都有一个私有的空间,互不影响

动态给 vue 的data新增属性会怎么样

在vue2中,变量都是改造过的,直接新增属性的话,当后续修改变量时,无法与页面内容关联起来(即页面上看不到更新),需要使用 $set 进行新增赋值

vue nexttick的理解

先讲一下nextick的实现原理:nexttick是加入了异步任务、队列、惰性函数的概念

实现的逻辑,首选promise(微任务的概念)、次选mutation Observer(微任务的概念)、接着setImmediate、最后兜底方案setTimeout。

  1. 将nexttick的回调函数push到数组中,在promise的回调中执行数组中保存的cb方法
  2. 加入一个flag的标识,防止多次频繁调用

因为JS中DOM的操作是同步的,vue在节点渲染时为了优化性能,render的更新是用队列的形式,结合使用nexttick去完成的(用异步去完成)
所以会出现一个问题,如果使用传统方法,当变量更新后,直接获取DOM的信息不是最新的

//异步机制
export function queueWatcher(watcher) {
  const id = watcher.id;

  //watcher去重
  if (has[id] === undefined) {
    queue.push(watcher);
    has[id] = true;
    //异步调用 (执行watcher类的run方法)
    nextTick(flushSchedulerQueue);
  }
}

JS的事件循环:分为同步任务和异步任务,异步任务中有宏任务和微任务的概念
一个常规的事件循环是:同步任务完成后,优先执行微任务(在同一个事件循环中完成),然后在下一个事件循环里完成宏任务。
所以nexttick是利用微任务的原理,在执行 render 更新的后面增加了一个平级的微任务(回调函数),成功解决了变量更新后,能获取到最新的DOM信息

感谢这篇文章的讲解

vue2和vue3的区别

1. 在模板template中:

vue3支持多个根节点
vue2只支持1个根节点

2. 代码写法上:

vue3支持componentAPI和optionsAPI 2种写法
vue2是支持optionsAPI

3. v-model属性:

vue3在标签上支持多个v-model的写法
vue2只支持1个v-model

4. v-if和v-for的优先级:

vue3是v-if的优先级高一些
vue2是v-for的高(如渲染同时出现时,会重复判断,浪费性能)

5. 响应式原理的变化:

vue3是提供2个API方法(ref, reactive),ref方法 是通过新建Ref类(代码内部会对参数变量进行判断,如果参数是对象类型的会调用 toReactive 的方法),reactive方法是把对象类型变量通过proxy+reflect的模式进行监控
vue2是数组重写数组原型方法,对象通过Object.defineProperty的模式

6. 全局实例的写法:

vue3是const app = createAPP()
vue2是Vue.prototype.xx

7. 虚拟DOM的优化:

vue3在创建 vnode时,会对静态节点(指没有变量的DOM节点)进行标记,后续在 diff时,会跳过静态节点,只对动态节点进行对比。当重复渲染时,vue2 对静态节点也会进行对比,和重建vnode。动态节点内的静态属性,也遵循该核心对比逻辑。 vue3 虚拟DOM优化详解

8. typescript的支持:

常见的 vue3 API理解

  • isRef(option): 只用于判断当前参数是否被 ref改造
function isRef(r) {
    return !!(r && r.__v_isRef === true);
}

let a = ref({ x: 1 }); // isRef(a) true
const ll = reactive([ {name: 'ff'} ]) // isRef(ll) false
  • isReactive(option): 只用于判断当前参数是否被 reactive改造
  • unref(option): 用户获取参数的值,如果是 ref改造的 返回 option.value 否则返回 option
  • toValue(option): unref的超集,如果参数是函数类型的,会先执行函数后返回
function unref(ref2) {
    return isRef(ref2) ? ref2.value : ref2;
  }
function toValue(source) {
    return isFunction(source) ? source() : unref(source);
}
  • InjectionKey: 是用于在 provide 和 inject,定义key值类型的一个API,本质上是一个symbol()类型的变量

  • expose:因为vue3对组件实例做了私密控制,无法像以前 ref 实例能直接访问到子组件的全部内容,所以用户需要通过 expose属性配置主动配置想要暴露出来的内容

  • shallowRef:字面意思是浅层响应式引用,与ref(嵌套的多层对象变量,会自动递归解析)相比,shallowRef只把对象的第一层进行响应式改造。如果是嵌套的对象,深层的属性发生改变,不会触发视图更新(适用于只关心第一层属性,不需要监控深层次变量的场景)

  • toRef:指对变量进行Ref的改造,常结合 reactive 的对象变量,解构单个属性的时候使用。(如果单次改造多个,使用toRefs

vue的钩子函数

  • vue2
  1. beforeCreate:vm只完成了 option(传入参数、全局)的合并,data此时还未进行改造,无法访问
  2. created:完成了参数变量的响应式改造、provide/inject注入,未进行 mount 渲染( $el为空 )
  3. beforeMount:在 created的基础上,完成了 vue实例 vm.$el 的赋值
  4. mounted:已完成 DOM节点渲染
  5. beforeDestory:在准备执行销毁节点操作之前触发
  6. destoryed:解除 data变量的关联,执行了 watcher 类的 teardown 方法
  7. update/beforeUpdate:当响应式数据有发生变化时会触发。beforeUpdate指数据更新,模板DOM未更新、update指节点已经更新
  • vue3

在使用componentAPI的写法中,setup函数将 created 和 beforeCreate 合并了

vue组件的通信

  • provide + inject

在下方有解析 provide / inject的理解

  • props

当进行变量响应式改造 initState 时,其中最先进行的改造是 initProps,会在组件的 vm实例上的 $options属性 新增:( propsData / _propKeys )和 在vm实例新增 _props 属性,后续通过 validateProp 函数完成 props值的获取,获取完最后对 prop变量 进行响应式的改造 props解析详细文章

//init.js
if (opts.props) initProps(vm, opts.props)

//initProp的过程
function initProps (vm: Component, propsOptions: Object) {
  //记录解析标签 props属性的值
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  //记录传入props 的 key值
  const keys = vm.$options._propKeys = []
  
  for (const key in propsOptions) {
    keys.push(key)
    //完成一系列的赋值 (判断标签传入的 props值,否则取设定的default值)
    const value = validateProp(key, propsOptions, propsData, vm)
    //...
  }
  
  //进行响应式改造 (将key 定义到 vm._props属性上)
  defineReactive(props, key, value);
  
  //将props的变量 proxy至vm上 (this.xx = this._props.xx)
  proxy()
}

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
) {
    //获取 props对应的值
    // ...
}

关于 props属性 触发更新的原理,假设 父组件传递一个响应式的变量给到子组件:

  1. 父组件的 render 已经收集到 响应式变量的 subs中
  2. 子组件的依赖收集(变量是响应式改造后的 props属性),如果传入的变量是 对象类型 的,props做响应式改造时,会识别到对象内部的 ob属性 而停止重复改造。子组件的 render 是收集到 父组件 对象 的具体属性的 subs中

当变量更新时,触发 notify() 进行更新,

  1. 父组件重新渲染触发了update(执行 diff),更新了传入子组件的 props属性
  2. props属性发生了变化,触发了 props属性里面的 update(完成闭环)
  3. 如果 props属性传入的是对象,因为子组件是被收集到 对象内部的属性,所以更新只触发子组件
  • on + emit

on事件 @event事件订阅 的模式,当执行 v-on时,在vue的实例 vm._events 堆栈中会把当前的函数 push进去

(vm._events[event] || (vm._events[event] = [])).push(fn)

v-on 收集的回调函数,派发相对应的是 emit 方法,当执行 $emit 时,会从 event的堆栈中取出对应名称的内容,然后逐个进行调用,完成这个闭环。

//简易版
Vue.prototype.$emit = function(event) {
  let cbs = vm._events[event];
  for (let i = 0; i < cbs.length; i++) {
    cbs[i].apply(vm, arguments)
  }
}
  • vuex / pinia

(等待补充)

  • ref

在渲染组件时,为 vm 增加 ref 属性,该属性指向vm实例,所以可以通过 this.$ref 的形式去访问组件内的属性和方法(在vue3 直接访问已被禁止)

const ref = vnode.componentInstance || vnode.elm

vue 插槽的理解

在initRender时,在解析父节点时,当发现ast树中的 attr属性里有 slot相关的关键字, 会 预先 生成子节点的 vnode对象,然后通过对象 key<指slot的关键字> : value <指vnode> 的形式存入父节点的 $slots属性中。

//父节点 ast树
var ast = {
  _c("div", child: [
    //...
    attr: {
      slot: //名称
    }
  ])
}
//render.js 会执行生成的slots对象的方法
vm.$slots = {
    header: [vnode],
    body: [vnode],
    default: [vnode]
}

//instance / render-helper 文件夹中有解析slot的方法

在子组件 render时,根据slot 属性的key,获取父组件保存的 vnode信息,然后return 出来 讲解传送门-slot插槽原理

vue provide ➕ inject 的理解

该场景主要用于 父子组件嵌套较深并且需要通信,一层层使用props会变得复杂繁琐

通过由父组件通过 provide 注册需要传递的变量(API传送门),在子组件中使用 inject 去接收。

  • provide:可通过 普通对象的形式定义、支持使用 函数(return this变量)的模式定义
  • inject:可通过 传统数组 接收、使用 对象 的形式接受
//子组件数组
inject: ['bar'];
//子组件对象的形式
inject: {
  foo: {
    //指的是来源的 provideKey
    from: 'bar',
    //支持设置默认值
    default: 'aaa'
  }
}

代码实现原理:

  1. init 父组件时获取 provide的信息,并存入实例的 vm._provided
  2. inject 获取值的方式有点像(instanceof的原理),通过 while 不断地判断 vm实例(当前实例没有的话通过 vm = vm.$parent)判断是否有 vm._provided 属性
  3. 如果能在 vm._provided 属性中获取到( return 对应的数据),获取不到时会判断 inject是否有设置 default值,否则提示 warning
  4. 为 inject属性 执行 响应式改造 defineReactive
// 在实例化init,merge的过程中,通过 normalizeInject函数
// 会将所有 inject数据,都格式化为 拥有 from 的对象,便于后续获取 provide值使用

// 函数代码位置: /vue/src/core/util/options.js

v-show 和 v-if的理解

2个指令的共同点,控制元素是否显示(v-show适用于要频繁切换的场景)

  • v-show 是给元素增加 css属性,display: none/block(来回切换),切换时不会触发钩子
  • v-if 在源码上是返回 node 的节点,true/false 切换时,节点是销毁或者渲染(频繁切换会消耗性能),并且会触发钩子函数

vue中key的原理

key可以理解为 vnode 的一个id,可以在 diff 的时候,根据 key 更准确、更快的找到 vnode节点。因为在 patch 时,会生成一张 key 的 map,保存 vnode 的节点信息。

  • 如果不用 key,vue 在 diff 时采取最近原则。( 列表渲染时慎用 index索引 作为key )
  • 如果使用 keydiff 时能快速匹配到相同的节点
//vue 源码中有一个 sameVnode的判断 最关键的前提是 key是否相同
function sameVnode (a, b) {
  return (
    a.key === b.key && ( ... )
  )
}

vue diff的理解

当进行 _update时,新旧 vnode 节点比较时,资源最大化利用,使用最少消耗、最高性价比进行DOM节点更新的一系列对比操作就称作 diff。

diff 的比较只在同层级进行,不会跨层级进行比较。(在 patch 渲染函数中,使用 while 方法对 vnode节点 进行循环),当新老vnode 都存在时,才会进行具体的判断逻辑。有5个情况:

  1. 新旧tag不一致,直接replace
  2. 如果是文本节点,直接进行更新
  3. 如果只有老节点(无新节点),直接清空节点
  4. 如果只有新节点,全部新增
  5. 如果都有节点(严格说,此处对比的过程才是 diff)

diff 采用 双链条四指针(start、end),两头往中间收拢的形式进行比较(通过循环的形式进行)

//循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // 指针节点匹配情况分为4种: 头头 头尾 尾尾 尾头匹配
    // 如果 4种情况都不符合的情况,则直接新增一个节点
}

// 匹配相同节点 有一个 sameNode方法,节点的 key属性是一个非常关键的参数
function sameVnode (a, b) {
  return (
    a.key === b.key && ( ... )
  )
}

key属性在 diff 中起到非常关键的作用(详细原理请看上一条解析),匹配到的节点,更新和移动的操作都是在 oldVnode 上进行操作。如果 index 循环完毕后,仍有剩余的 vnode节点还没检测:

  • 剩余老节点(一个个删除)
  • 如果有新的节点(一个个新增)

vue 标签定义事件的理解

查看(vue组件的通信)中的 on事件描述

vue mixin的理解

mixin常见于功能模块,有利于代码复用。

  1. 局部mixin(在组件中单独引入)
  2. 全局mixin(在入口处通过 Vue.mixin 引入)

mixin特点:

  1. mixin中的钩子函数会和组件的合并成一个数组,然后一起执行。(钩子函数mixin的优先级比组件高)
  2. mixin中设定的data、method如果和组件同名将会被组件覆盖
  3. 在组件中引入mixin后,可以直接使用mixin中的属性。

优点:减少重复代码,提高代码的复用性
缺点:过多的mixin,会加大代码复杂度,降低代码阅读性,代码变得不好维护

vue keep-alive组件的理解

缓存组件:

  1. 根据组件的name属性来匹配(匿名组件不匹配),
  2. 新增2个钩子函数(activated/deactivated)
  • activated:首次进入是 mounted后触发,后续是 route的钩子函数触发
// vdom/create-component.js
// 在 insert方法中(createElm时会触发),加入了 keepalive的逻辑,并加入 新钩子函数的判断
insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
},
destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
}

// core/instance/lifecycle.js
// 钩子函数逻辑
export function activateChildComponent (vm: Component, direct?: boolean) {
  // ...
  callHook(vm, 'activated')
}

export function deactivateChildComponent (vm: Component, direct?: boolean) {
  // ...
  callHook(vm, 'deactivated')
}
  1. 新增3个属性:include(白名单)、exclude(不缓存)、max(最大缓存个数)

实现原理:(include、exclude: 匹配组件是使用字符串或者正则进行匹配)

// 新增cache属性,保存vnode
this.cache = Object.create(null)
// 保存组件的 key
this.keys = []

/*
    在render函数中 有加入判断逻辑原理
    
    cache中会保存组件的 vnode
    cache[key] = vnode
    
    首先 进行判断 是否已经缓存过该组件,
    1、如果匹配上则取出直接进行展示,keys的数组,将组件的key取出重新push
    (所以表示 keys数组中越旧的数组越不活跃)
    2、如果没有缓存则进行存储
    3、如果存储的数额达到 max,将销毁destory掉 最旧的vnode
    
*/

if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    // make current key freshest
    remove(keys, key)
    keys.push(key)
  } 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)
    }
  }


如果include/exclude有同名组件时,实现原理中 exclude的优先级会更高

if (
    // 如果有 include (如果能匹配到组件,前面的判断为 false,将会走 || 后面的判断)
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
  ) {
    return vnode
}

vue-router的理解

  • vue-router 是一个 vue插件,需要配合 vue.use 进行注册 install。注册成功后会在 vm实例中挂载 route(操作url路由相关的方法)、 router(router实例信息)
  • install vue-router 时,具体做了以下几件事情:
  1. vue-router 内部实例一个 class类
  2. 将用户定义的路由表 router文件 进行循环解析,解析嵌套的路由数据,使用 路径(path: a/b) = component 的格式,通过 this.routerMap 进行保存
  3. 注册一个 current变量,使用 vue 进行响应式改造(让其拥有收集依赖和派发更新的能力),用于记录 url 路径信息
  4. 提供 2种模式2个核心组件
  5. addEventListener 注册了监听 域名变化的 api函数
  • 提供 2种模式 mode: 'hash' | 'history'

当URL变化,页面发生变化而不整体刷新页面(reload)的原理:

  1. hash模式:url上有关键字(#/),背后是通过 location.hash 实现
  2. history模式:通过 history.pushState 的方法进行 path赋值实现
//监听了对应的 api 方法才实现 url 和 路由页面的关联

//history模式
window.addEventListener(
  'popstate', 
  () => { this.data.current = location.pathname }
);
//hash模式
window.addEventListener(
  'hashchange', 
  () => { this.data.current = location.hash }
)

vue-router 提供了 2个核心组件 router-view , router-link

router-link:最终会 render 成一个 a标签阻止了 a标签的默认行为),

  • 提供 一个 props参数 接收跳转的path信息
  • 内置了一个 click 事件
initLink() {
  _Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        return h('a', {
          attrs: { href: this.to },
          on: {
            click: // ...
          }
        }, [this.$slots.default])
  }
  
  // 点击标签进行 url转换时,手动 对current 进行赋值
  // 并且手动进行 pushState 和 hash赋值 完成闭环
  // history: pushstate、hash: location.hash
}

router-view:返回组件的渲染函数,(通过 域名和路由表 进行匹配),因为render Watcher 与 current变量 进行的捆绑,所以后续当 current变量 发生变化,会触发 render 的 update

  initView () {
    _Vue.component('router-view', {
      render (h) {
        // ...
        // 渲染对应的组件
        return h(component)
      }
    })
  }

vue常见的API

  • Vue.use

一般是指注册 vue的插件(配合带有 install的对象使用),执行完方法后,会在 installPlugin 数组中 push vue.use传入的plugin参数,以防重复注册。

  • $set

会判断参数的类型,如果是数组:

  1. 先判断传入的 key 跟数组变量的长度(取更大的值)
  2. 然后进行调用 splice 进行响应式的改造,

如果是对象类型的

  1. 先判断 key 是否在对象中(如果在对象中,直接进行赋值)
  2. 取出响应式改造的 ob 属性,如果没有响应式改造,也是直接赋值,
  3. 前2种都不符合,就是按照新的属性,进行响应式改造
  • Vue.extend

vue.extend是一个方法,会return一个 构造器Sub,返回一个函数出来,其原理是通过原型链的形式,将子类与父类关联起来(this指向Vue的实例),然后将参数进行合并。最后当挂载到Vue原型上后,会通过 调用 init 方法进行初始化。(等同于构造了一个Vue 的子类)

//创建子类继承Vue父类 便于属性扩展
  Vue.extend = function (extendOptions) {
    //子类的构造函数
    const Sub = function VueComponent(options) {
      //调用Vue初始化方法
      this._init(options);
    }
    Sub.cid = cid++;
    //子类原型指向父类
    Sub.prototype = Object.create(this.prototype);
    //constructor指向自己
    Sub.prototype.constructor = Sub;
    //合并vue方法
    Sub.options = mergeOptions(this.options, extendOptions);
    if (Sub.options.props) { 
        initProps(Sub) 
    }
    // 将初始化computed挂载在原型对象下 
    if (Sub.options.computed) { 
        initComputed(Sub) 
    }
    return Sub;
}
  • _c(),指createElement函数
  • _t(),指renderSlot(插槽的方法)
  • _f(),指 filter 过滤器的方法
  • _s(),执行 toString()
  • _v(),创建 文本节点
  • _l(),v-for 的列表渲染函数

vue 修饰符的理解

  • .trim:去掉字符串首尾的空格
  • .number:属性转换为 number属性
  • .stop:停止冒泡
  • .prevent:停止默认行为
  • .once:事件方法只触发执行 1次
  • .sync:vue2中(因标签只可以有一个 v-model),属性双向绑定使用

vue filter的理解 (等待补充)

vue3已不适用

局部filter 优先级高于 全局filter

//使用方法
<p>{{ value | myFilter }}</p>

vue directive的理解

vue 样式scoped 的原理

解析模板内带有scope属性的style标签,利用postcss插件对样式进行改造

  1. 为css的样式,加入data-v-hash值的后缀属性
  2. 将模板中匹配的DOM节点标签,加上data-v-hash值,完成关联的关系

vue 组件的理解

子组件可以改变父组件的属性吗

可以改变(但不能直接改),父组件将变量通过 props 传入子组件,子组件修改属性后,可以通过 emit 结合 on 绑定方法完成传输。或者子组件通过 @input 的模式实现 v-model的语法

vue 模板解析的原理

(等待补充)

  1. 使用正则表达式进行字符串匹配(①捕获标签名 ②获取文本 ③获取属性、方法指令)

vuex的理解