Vue总结

180 阅读12分钟
new Vue做的事
  • 调用_init方法,传入用户的options。在_init方法中首先将用户的options和Vue构造函数上的options合并;
  • 调用initLifecycle方法初始化$parent、$root、$refs、$children;
  • 调用initEvents方法初始化事件vm._events={}
  • 调用initRender方法初始化createElement渲染方法;
  • 调用beforeCreate生命周期函数;
  • 调用initInjections方法初始化inject;
  • 调用initState方法初始化props、methods、data、computed、watch;
  • 调用initProvide方法初始化provide;
  • 调用created生命周期函数;
  • 如果用户在options中定义了el,则调用$mount;
  • 通过mountComponent挂载
  • 将updateComponent当作回调创建渲染watcher,用来更新界面。
  • updateComponent主要做了:通过_render生成虚拟dom,调用_update进行patch。
Vue通讯方式
  1. props和emit进行父子传值。
  2. provide和inject传值:父组件(或祖父组件)的provide对象会存放在组件实例的_provided上。子孙组件中的inject会被规范化处理成每一个key对应有from属性,根据from对应的值一层层往上找,直到找到父组件实例的_provided上存放from对应的属性的值,如果找到根组件都没找到,则获取default默认值。
  3. 通过$children,$refs获取子组件实例,然后获取值;$parent,$root获取父组件或根组件实例,然后获取值。
  4. $attrs获取父组件中绑定在子组件上的属性值。$listeners获取父组件中绑定在子组件上的事件。
  5. EventBus:原理主要是在vue初始化时,会在vue实例上创建一个_events空对象,并且会在Vue原型上挂载$on,$off,$emit,$once方法,用来实现订阅和发布;eventBus就是创建一个公共的Vue实例用来订阅和发布事件。
  6. Vuex。
  7. 利用浏览器的sessionstorage和localstorage。
v-if和v-for优先级
  • vue2中v-for的优先级是高于v-if,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件。因为在编译部分的generate生成代码字符串部分,是先执行genFor处理v-for然后再执行genIf处理v-if。
  • vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常。
生命周期
  • beforeCreate:组件实例创建之初,到此为止主要做了,options与vue构造函数上options合并、初始化了事件系统_events={}、初始化了生命周期的一些变量、定义好了createElement函数。

  • created:组件实例已经完全创建,并且初始化好了provide、inject、props、methods、data、computed、watch。数据已经被响应式化处理好。

  • beforeMount:组件挂载之前,在mountComponent函数中首先调用beforeMount钩子函数。然后才会去创建渲染watcher。

  • mounted:组件挂载到实例上去之后。创建渲染watcher时,会将vm._render生成的vnode进行patch完成挂载。然后调用mounted钩子函数。

  • beforeUpdate:在异步更新watcher队列时,对于渲染wather会执行每一个wathcer的before方法,before方法中会执行beforeUpdate钩子函数。

  • updated:当异步更新完watcher队列后,对于队列中的每一个渲染watcher执行updated钩子函数。

  • beforeDestroy:组件实例销毁之前。调用完beforeDestroy钩子函数后开始销毁组件实例。

  • destroyed:组件实例销毁之后。

组件v-model原理

v-model是语法糖,默认情况下相当于:value@input

响应式原理

data的响应式处理是在initData中,通过observe方法对data进行侦测,如果data已经被响应式处理过了就能获取到__ob__这个Observer实例并将其返回,如果没有处理过就会new一个Observer实例并返回。

在创建Observer实例过程中,会将这个实例添加到data的__ob__上作为响应式处理的标记。并且每一个被侦测的对象都会对应创建一个Dep,用于后续依赖收集。然后就开始分别对数组和对象进行响应式处理。

对象的响应式处理:对象调用walk方法,循环对象中所有的key,调用defineReactive方法。在defineReactive中,每一个key都会对应创建一个dep,用于访问key时依赖收集。然后通过Object.defineProperty来为对象每一个key的访问和设置进行拦截。key对应的值会通过observe继续递归侦测。在读取对象时会触发get,get中会通过dep.depend进行依赖收集。在设置对象的的值时会触发set,对于新设置的值通过observe侦测,通过 dep.notify派发更新。

数组的响应时处理:原型式继承Array的原型,重写数组的push、pop、shift、unshift、splice、sort、reverse方法。对于push、unshift、splice这三个用于数组新增值的方法,对于新增的值会通过observe侦测,获取数组的__ob__也就说Observe实例上的dep来进行notify派发更新,返回调用原来方法得到的值。数组的原型链指向新原型。然后循环数组的每一项通过observe侦测。在Object.defineProperty的get中如果key对应的value是数组,就会循环这个值,使值的每一项都进行dep.depend依赖收集。

依赖收集的过程:通过dep.depend进行收集,depend方法中会调用当前操作watcher的addDep方法。addDep方法会将这个dep添加到自身的deps数组中,并且将自身这个watcher添加到dep的subs数组中。这样dep与watcher中就形成相互关联关系。

派发更新过程:通过dep.notify派发更新,循环dep上的subs数组,调用每一个watcher的update方法。watcher的更新上异步的,通过queueWatcher实现。在queueWatcher中如果当前队列不在刷新就直接入队,如果已经刷新了就根据id从小到大排序插入到指定位置。然后通过nextTick进行异步刷新队列。刷新队列之前首先会根据watcher的id从小到大排序,因此父组件对应的watcher在前,子组件的watcher在后,循环队列中的每一个watcher,如果时渲染watcher,那就会调用beforeUpdate钩子函数(因此是父组件的beforeUpdate先执行),然后在调用watcher的run方法。然后清空队列,再倒着循环提前拷贝好的队列,对于渲染watcher调用updated钩子函数(因为是倒着的,所以子组件先执行updated)。在watcher的run方法中会调用get方法,进行patch过程,组件时图更新。

watch和computed区别
  • watch:监听值的变化,值被读取时收集依赖,当值改变时会异步更新watcher队列,执行每一个watcher的run方法,run方法会调用get获取新的值,并且执行回调。

  • computed:它是一个lazyWatcher,在创建watcher的时候options会传递一个lazy:true。Watcher中的dirty属性也为true。因此首次获取计算属性值时dirty为true,所以会执行watcher的evaluate方法。evaluate方法中会执行get方法获取计算属性值,并且将dirty置为false。当下一次获取计算属性值时,就不会调用evaluate方法,也就不会调用get去重新计算了。当计算属性依赖的值发生变化时,派法更新会重新出发lazyWatcher的update方法,然后把dirty置为true,就会重新调用get计算得到新值。

$delete、$set 原理
  • $delete:删除数组或对象的值时,能响应式触发视图更新。 如果是数组则调用splice方法,因为splice是重写过的所以会派发更新。获取对象上的__ob__的值:Observer实例ob,通过ob上存储的dep进行notify派发更新。如果要删除的key都存在对象上就什么也不会做。如果对象不是响应式对象则直接删除值。

  • $set:数组或对象添加值时,能响应式触发视图更新,如果新增数组的索引大于原数组长度就扩大数组长度,然后通过splice添加。如果key原本就在对象上,就跳过不做处理。如果对象不是响应式对象则直接设置值。如果对象上存在Observer实例ob,则通过defineReactive响应式设置值,并且通过ob.dep.notify派发更新。

Vue.install原理

installedPlugins存储已经安装的plugin,保证不重复安装,将 Vue 构造函数放到第一个参数位置,然后将这些参数传递给安装的方法,plugin是对象就调用上面的install方法,如果plugin是方法就调用这个方法安装。

nextTick原理

nextTick中传递的回调函数,会进行包裹,捕获异常,然后存放到callbacks数组中。然后当不在pending状态时会异步调用flushCallbacks方法,首先会考虑微任务new Promise.resolve().then中调用,然后再考虑setImmediate,最后考虑setTimeout(flushCallbacks, 0)。在flushCallbacks中,会循环callbacks数组,调用每一个函数,最后清空callbacks。

对于VNode虚拟Dom理解

VNode是一个JavaScript对象,它上面通过一系列属性描述了真实Dom。通过操作VNode可以减少对真实Dom的操作,减少重绘和回流。VNode的生成发生在_render()时,分为编译生成的_c和用户提供的$createElement,两者都通过createElement生成VNode,两者的区别是规范子节点时的处理。

_c的生成:对template进行编译:解析标签及上面的属性生成ast,标记静态节点和静态根节点,将优化好的ast生成代码字符串形式,然后转换为_c函数,用来生成VNode。

Patch过程及Diff算法

path过程中封装了操作真实Dom的方法在nodeOps中。当新的vnode不存在,旧的vnode存在时,就会调用各模块的destory钩子函数,并将vnode从真实Dom中移除。如果旧vnode不存在时,会根据新的vnode创建一个真实的Dom插入到父元素中。如果新旧vnode都存在,并且两个vnode的key和标签都相同,则会进行patchVnode更新节点。

更新节点过程:如果新旧节点完全一样或者都是静态节点则跳过。如果新vnode是文本节点,则无论旧节点是什么都会通过setTextContent设置新文本内容。如果新vnode不是文本节则会判断新旧vnode是否都有子节点,如果都有则会进入updateChildren过程。如果新vnode有子节点,旧vnode没有(旧vnode是文本节点则先清空文本),则循环子节点将每个子节点创建成真实dom插入。如果新vnode没有子节点,那么旧vnode有子节点就删除子节点,有文本就清空文本。

updateChildren过程:在新旧子节点列表中,首先比较旧前和新前是否相同,如果相同则通过patchVnode更新节点。不同则再比较旧后和新后是否相同,相同则通过patchVnode更新节点。不同则再比较旧前和新后是否相同,相同则通过patchVnode更新节点,再将节点移动到oldchildren中所有未处理节点的最后面。不同则再比较旧后和新前,相同则通过patchVnode更新节点。然后将节点移动到oldchildren中所有未处理节点的最前面。如果这四种方式比较完毕都不符合,则通过key的映射或者循环在oldchildren找到与新前节点相同的节点,然后patchVnode更新节点,然后插入到oldchildren中所有未处理节点的最前面,如果没找到则直接创建vnode插入。如果因为旧列表退出循环,则剩余的新子节点直接插入。如果新列表退出循环,则剩余的旧子节点删除。

key的作用
  1. key的作用主要是为了更高效的更新虚拟DOM。vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件。在patch过程中会通过sameVnode来判断两个VNode是否相同:首先会比较key是否相同,如果key不相同那么就表示两者不是相同VNode;然后再比较标签是否相同,是否都具有属性。对于input标签会这样处理:如果两个input对应的type相同,或者两个type的值都是在这范围内(text,number,password,search,email,tel,url),则表示这两个vnode是相同的。
  2. 当通过diff比较子节点时,如果新前旧前、新后旧后、新前旧后、新后旧前这四种情况都不成立时,会根据老子节点中每个节点的key和索引之间建立映射关系,如果新子节点设置了key,则会根据映射关系找到这个节点在老子节点中的索引,如果没设置key则需要通过一次循环才能找到在老子节点中的索引。然后根据索引移动节点。
Vue编译过程

首先会将template转换为模板字符串形式,如果没有tempalte则通过el去转换。然后进行编译:通过parseHTML去解析模板字符串,并传入start、end、chars、comment钩子函数。它会查找 < 的位置。如果就在开头位置,通过正则匹配是注释节点,就通过comment生成注释的ast。然后处理条件注释和Doctype标签。如果匹配到开始标签就会解析出所有属性,并调用start钩子函数,start中会处理v-model指令的input标签,处理指令然后生成对应的ast,如果标签是非自闭和标签,就会将当前ast放入栈中,然后将currentParent指向这个ast。当解析到结束标签时,会调用end钩子函数,end中移除栈中最后一个元素,表示当前ast已经处理完,然后再将currentParent执向栈最后一个元素,然后调用closeElement,closeElement中将自己放到父元素的 children 数组中,然后设置自己的 parent 属性为 currentParent,使ast形成父子关系。如果 < 不在开头那就当作文本处理,通过chars处理成文本ast。

静态标记:首先递归遍历所有节点,给静态节点打上标记,静态节点要求:存在v-pre或者是pre标签。不能存在指令和事件们,需要是内置标签。然后标记静态根节点:节点本身是静态节点,而且有子节点,而且子节点不只是一个文本节点,则标记为静态根,静态根节点不能只有静态文本的子节点,因为这样收益太低,这种情况下始终更新它就好了。

将ast转换为代码字符串_c,然后通过new Function生成真正的函数。

keep-alive组件原理

keep-alive渲染的时候,会从子节点列表中找到第一个是组件的vnode,如果组件名不被include匹配或者被exclude匹配,那么就直接返回这个组件的vnode,不会做缓存。如果缓存中存在这个vnode,就使用缓存:缓存对象中获取组件实例给 vnode,并将对应的key放在key数组最后面(表示最新使用的,当缓存满的时候会删除数组第一个,也就是最不常用的)。不存在则添加进缓存,并给组件vnode添加keepAlive标记。

首次渲染会将所有命中匹配的进行缓存,当组件切换时,会进行prepatch,如果命中规则就从缓存中获取了,子组件不会重新mount挂载,而是执行activated钩子函数。

当keep-alive组件销毁前会将缓存的所有组件执行$destroy进行销毁。

vue3响应式原理

vue3通过reavtive和ref定义响应式变量。

reactive:首先会通过isReadonly判断当前待处理的对象是不是通过readonly创建的(通过对象上的__v_isReadonly标记判断),如果是通过readonly创建的则不做后续处理直接返回,否则通过createReactiveObject使其响应式。

createReactiveObject接收四个参数:target(待处理对象)、isReadonly(是否只读)、baseHandlers(普通对象、数组的Proxy对应的handlers)、collectionHandlers(set和map的Proxy对应的handlers)、proxyMap(用来缓存Proxy对象的,避免重复创建Proxy对象)。

函数内部针对以下情况不会创建proxy对象:

  1. target不是对象,抛出警告直接返回。
  2. 如果targe已经是Proxy对象了(带有__v_raw标记、除了readonly(reactive(obj))这种情况外),直接返回。
  3. 如果缓存(proxyMap)中存在这个target,则返回他对应的值,也就是target对应的Proxy对象。
  4. 被markRaw处理过的对象(带__v_skip标记)意味着永远不会转换为Proxy对象,直接返回。
  5. 对象不可扩展的(不能添加新的属性,也就是Object.isExtensible=false),直接返回。

如果target是set或map类型,则使用collectionHandlers创建Proxy对象,否则使用baseHandlers创建Proxy对象。将创建的Proxy对象存入proxyMap缓存中。

baseHandlers:

  • get操作:拦截读取target的属性和方法,通过track来收集effect。通过createGetter函数实现,可以分别传入isReadonly(是否只读),shallow(是否浅层)。非只读情况下:重写数组的includes、indexOf、lastIndexOf方法(数组的length属性和每一项都会进行收集、因为传入的参数可能是proxy对象,当查找不到时会将传入的proxy对象转为原始对象后再查找)。重写数组的push、pop、shift、unshift、splice方法(因为这些操作不仅会读取数组的length,还会设置length,为了避免无限递归所以需要维护一个全局变量 shouldTrack 来避免length属性被收集)。重写hasOwnProperty方法(因为delete可能会影响hasOwnProperty结果,所以需要收集)。其他属性读取时都会被收集,除了当key是内置的Symbol类型(比如Symbol.iterator)以及特殊的key(__proto__ , __v_isRef , __isVue)时不会进行收集直接返回对应的值。如果属性值是通过ref创建的(带有__v_isRef标记),访问的是数组的索引就返回这个ref对象,否则就返回ref对象的value属性。深层情况下,属性值如果是一个对象则会进行递归(根据isReadonly情况调用readonly或者reactive递归)。

  • has操作:拦截in操作符 if(key in obj){}。会对key进行收集,除了当key是内置的Symbol类型(比如Symbol.iterator)以及特殊的key(__proto__ , __v_isRef , __isVue)时。

  • ownKeys操作:拦截for...in... 。对象和数组都能通过for...in...循环,如果是数组,就会对length收集,对象则通过symbol创建一个key(ITERATE_KEY)来进行收集。

  • set操作:拦截对属性设置值,通过trigger触发之前收集的effect。通过createSetter函数实现,传入参数shallow(是否浅层)。会判断设置的值是新增的还是修改原来已存在的。然后分别使用ADD或者SET来调用trigger。

  • deleteProperty操作:拦截delete操作符。如果要删除的key在对象上,则会调用DELETE的trigger来触发触发之前收集的effect。

collectionHandlers:

  • get操作:调用get方法读取key对应的值时需要对key收集,如果key是一个proxy对象,也需要对原始key也进行收集。深层情况下需要对值递归调用reactive。

  • get size():获取size时,会对ITERATE_KEY进行收集。

  • has操作:调用has方法时,需要对key收集,如果key是一个proxy对象,也需要对原始key也进行收集。

  • forEach操作:收集关于ITERATE_KEY的依赖,将forEach函数的两个参数都通过reactive处理成proxy对象。

  • 重新实现迭代器方法(keys, values, entries, Symbol.iterator):如果是keys方法会对MAP_KEY_ITERATE_KEY进行收集(因为设置时map的key不一定发生变化、所以不需要触发keys方法),其他是ITERATE_KEY。对于返回的迭代器对象中的value通过reactive处理成响应式。

  • add操作:set调用add方法时,通过trigger触发之前收集的effect(ADD类型)

  • set操作:map调用set方法时,可能是新增可能是设置,别使用ADD或者SET来调用过trigger触发收集的effect。

  • delete操作:当要删除的元素存在时,则会调用DELETE的trigger来触发触发之前收集的effect。

  • clear操作:调用clear方式时, 则会调用CLEAR的trigger来触发触发之前收集的effect。

track收集effect:

通过targetMap来收集effect,targetMap中的key就是每一个通过reactive创建的原始对象,value是depsMap。depsMap中的每一个key就是原始对象中的每一个key,value是一个dep,存放的都是通过track收集的effect集合。 通过trackEffects来使dep和effect之间建立联系。

trackEffects:dep函数中添加activeEffect(当前正在使用的effect函数),activeEffect函数的deps属性添加这个dep,两者之间建立联系。

trigger触发effect:key的值如果被修改,则将其对应的effect统一存放至deps数组中。

如果设置数组的length属性比原来的length小,那么数组后面的内容则会被删掉,因此需要将被删掉索引对应的effect以及length对应的effect都添加至deps数组中。

如果是清空操作(CLEAR):将触发set和map所有的effect都添加到deps数组中。

如果是添加操作(ADD):对象则将ITERATE_KEY对应的effect添加至deps数组(for...in...、set或map时,迭代器方法,size,forEach都需要触发);数组则将length对应的effect添加至deps数组;map类型则将MAP_KEY_ITERATE_KEY对应的effect添加至deps数组(map的迭代器方法keys()涉及到key);

如果是删除操作(DELETE):对象则将ITERATE_KEY对应的effect添加至deps数组;delete 后数组长度是不变的,所以不需要处理length对应的effect;map类型则将MAP_KEY_ITERATE_KEY对应的effect添加至deps数组;

如果是设置操作(SET):将ITERATE_KEY对应的effect添加至deps数组(map类型时,迭代器方法,size,forEach都需要触发)

通过triggerEffects循环调用deps数组中每一个effect的run方法。run方法中会首先调用cleanupEffect清除effect和dep之间的联系。然后调用创建Effect实例时的回调,刷新视图。

ref:通过createRef创建RefImpl的实例。通过ref创建时,如果初始值是一个对象,则会被reactive对这个对象进行响应式处理。读取value值时,会将对应的effect添加到ref实例上的deps属性中。设置value时调用实例上deps中每一个effect的run方法。