笔记打算记录,自己整理好的面试题,
争取保持准确性和专业性,然后用直白的语言描述出来,
希望能做成一个API文档,让我日后翻到能直接对着复习就好,
如果也能帮助到你就最好了(我会不定时的更新的)
vue响应式的理解
我认为vue核心的地方是做这几件事:
- 通过改造data变量,实现知道变量被什么函数所依赖
- 当变量发生改变时,调用依赖该变量的函数
围绕这2个核心点,针对变量改造:
- vue2是对data变量通过循环进行判断
-
当遇到数组类型,是通过复制数组原型方法,然后进行改写来收集和派发(共7个方法)
-
当遇到对象类型,是通过Object.defineProperty的方法,在get和set方法中加入逻辑
- vue3的核心部分不变,改造方法有发生变化(主动提供2个API方法
ref,reactive)
- 实例一个
ref的类,类中有get和set方法的逻辑 - reactive只支持对象类型,通过
proxy的API,在handler中使用reflect对对象的属性进行对应逻辑操作
被收集的函数是指由vue实例生成的 watcher类:
- template模板是渲染函数,称为 render watcher
- 用户定义的computed,本质是一个 watcher类 称为 computed watcher
- 用户定义的watch,也是一个 watcher类 称为 user watcher
vue 响应式变量改造的经历了什么
响应式改造(initState)的顺序是 props - method - data - computed - watch
- vue2中是经历了以下几步
- 先将 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;
}
})
}
- 对变量进行改造,将会生成一个 Observer类(收集依赖和派发更新的作用),在实例 class的过程中会通过递归循环的模式查询出 data中所有对象和数组,然后对其再进行改造
export function observe(value) {
if (
Object.prototype.toString.call(value) === '[object Object]' ||
Array.isArray(value)
) {
return new Observer(value);
}
}
- 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]);
}
}
}
-
关于数组的改造:是通过复制一份数组的原型方法(对其中的7个方法写入逻辑,
push,pop,shift,unshift,splice,sort,reverse),在函数中,获取事先定义好的__ob__属性,数组新增的项时会通过ob属性调用实例中的方法进行响应式改造,然后通过ob属性调用notify进行派发更新(ob属性是作为判断变量是否改造的标识) -
关于对象的改造:对象内部的每一个属性都会新建一个
Dep类,然后通过Object.defineProperty,在 get方法(收集依赖)和 set方法(派发更新指令)中写入逻辑,然后完成基础的改造 -
其中如果直接给变量赋值新的对象,会触发 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的参数不是对象时,会报错拦截)
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) {
//...
}
}
reactive()是通过 newProxy的方法将对象变量进行拦截改造,并在handler中配合Reflect对对象内部的属性进行访问
后续文档更推荐全程使用
ref的API进行变量定义,其中的理由有几点
- 因为
ref定义的变量,全部都需要配合.value使用,更加的统一 reactive()只能定义对象类型,稍显局限性reactive定义的变量,对属性进行简单解构时,会断开响应式关联,需配合toRef使用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类分为三种:
- render watcher
- computed watcher(我们写的computed属性)
- 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)增加了
lazy和dirty的标识,实例后 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。
- 将nexttick的回调函数push到数组中,在promise的回调中执行数组中保存的cb方法
- 加入一个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 否则返回 optiontoValue(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
- beforeCreate:vm只完成了
option(传入参数、全局)的合并,data此时还未进行改造,无法访问 - created:完成了参数变量的响应式改造、provide/inject注入,未进行 mount 渲染( $el为空 )
- beforeMount:在 created的基础上,完成了 vue实例
vm.$el的赋值 - mounted:已完成 DOM节点渲染
- beforeDestory:在准备执行销毁节点操作之前触发
- destoryed:解除 data变量的关联,执行了 watcher 类的
teardown方法 - 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属性 触发更新的原理,假设 父组件传递一个响应式的变量给到子组件:
- 父组件的
render已经收集到 响应式变量的 subs中 - 子组件的依赖收集(变量是响应式改造后的 props属性),如果传入的变量是 对象类型 的,props做响应式改造时,会识别到对象内部的
ob属性而停止重复改造。子组件的render是收集到 父组件对象的具体属性的 subs中
当变量更新时,触发 notify() 进行更新,
- 父组件重新渲染触发了update(执行 diff),更新了传入子组件的
props属性 - props属性发生了变化,触发了 props属性里面的 update(完成闭环)
- 如果 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'
}
}
代码实现原理:
- init 父组件时获取 provide的信息,并存入实例的 vm._provided
- inject 获取值的方式有点像(instanceof的原理),通过
while不断地判断 vm实例(当前实例没有的话通过 vm = vm.$parent)判断是否有 vm._provided 属性 - 如果能在 vm._provided 属性中获取到(
return对应的数据),获取不到时会判断 inject是否有设置 default值,否则提示 warning - 为 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 ) - 如果使用
key,diff时能快速匹配到相同的节点
//vue 源码中有一个 sameVnode的判断 最关键的前提是 key是否相同
function sameVnode (a, b) {
return (
a.key === b.key && ( ... )
)
}
vue diff的理解
当进行 _update时,新旧 vnode 节点比较时,资源最大化利用,使用最少消耗、最高性价比进行DOM节点更新的一系列对比操作就称作 diff。
diff 的比较只在同层级进行,不会跨层级进行比较。(在 patch 渲染函数中,使用 while 方法对 vnode节点 进行循环),当新老vnode 都存在时,才会进行具体的判断逻辑。有5个情况:
- 新旧tag不一致,直接replace
- 如果是文本节点,直接进行更新
- 如果只有老节点(无新节点),直接清空节点
- 如果只有新节点,全部新增
- 如果都有节点(严格说,此处对比的过程才是 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常见于功能模块,有利于代码复用。
- 局部mixin(在组件中单独引入)
- 全局mixin(在入口处通过
Vue.mixin引入)
mixin特点:
- mixin中的钩子函数会和组件的合并成一个数组,然后一起执行。(钩子函数mixin的优先级比组件高)
- mixin中设定的data、method如果和组件同名将会被组件覆盖。
- 在组件中引入mixin后,可以直接使用mixin中的属性。
优点:减少重复代码,提高代码的复用性
缺点:过多的mixin,会加大代码复杂度,降低代码阅读性,代码变得不好维护
vue keep-alive组件的理解
缓存组件:
- 根据组件的name属性来匹配(匿名组件不匹配),
- 新增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')
}
- 新增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 时,具体做了以下几件事情:
- vue-router 内部实例一个 class类
- 将用户定义的路由表
router文件进行循环解析,解析嵌套的路由数据,使用路径(path: a/b) = component的格式,通过 this.routerMap 进行保存 - 注册一个
current变量,使用 vue 进行响应式改造(让其拥有收集依赖和派发更新的能力),用于记录 url 路径信息 - 提供
2种模式和2个核心组件 - addEventListener 注册了监听 域名变化的 api函数
- 提供 2种模式 mode: 'hash' | 'history'
当URL变化,页面发生变化而不整体刷新页面(reload)的原理:
hash模式:url上有关键字(#/),背后是通过location.hash实现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
会判断参数的类型,如果是数组:
- 先判断传入的 key 跟数组变量的长度(取更大的值)
- 然后进行调用 splice 进行响应式的改造,
如果是对象类型的
- 先判断 key 是否在对象中(如果在对象中,直接进行赋值)
- 取出响应式改造的 ob 属性,如果没有响应式改造,也是直接赋值,
- 前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插件对样式进行改造
- 为css的样式,加入
data-v-hash值的后缀属性 - 将模板中匹配的
DOM节点标签,加上data-v-hash值,完成关联的关系
vue 组件的理解
子组件可以改变父组件的属性吗
可以改变(但不能直接改),父组件将变量通过 props 传入子组件,子组件修改属性后,可以通过 emit 结合 on 绑定方法完成传输。或者子组件通过 @input 的模式实现 v-model的语法
vue 模板解析的原理
(等待补充)
- 使用正则表达式进行字符串匹配(①捕获标签名 ②获取文本 ③获取属性、方法指令)