Vue2
响应式数据的理解(先知道基本的问题在哪里,源码的角度来回答,用的时候会有哪些问题)可以监控一个数据的修改和获取操作。针对对象格式会给每个对象的属性进行劫持 Object.defineProperty
源码层面 initData -> observe -> defineReactive方法(内部对所有属性进行了重写 性能问题) 递归增加对象中的对象增加getter和setter
我们在使用Vue的时候,如果层级过深(考虑优化) 如果数据不是响应式的就不要放在data中了。我们属性取值的时候尽量避免多次取值。
如果有些对象是放到data中的但是不是响应式的可以考虑采用Object.freeze()来冻结对象
let total
for(let i = 0; i < 100; i++) {
// this.timer += i
total += i
}
this.timer = total;
Vue
中如何检测数组变化?vue2中检测数组的变化并没有采用defineProperty 因为修改索引的情况不多(如果直接使用defineProperty会浪费大量性能) 采用重写数组的变异方法来实现(函数劫持)
initData -> observe -> 对我们传入的数组进行原型链修改,后续调用的方法都是重写后的方法 ->对数组中每个对象也再次进行代理
修改数组索引,修改长度是无法进行监控的 arr[1] = 100; arr.length = 300; 不会触发视图更新
arr[0].xxx = 100; 因为数组中的对象会被observe
Vue
中如何进行依赖收集?多对多的关系 一个dep对应多个watcher,一个watcher有多个dep。默认渲染的时候会进行依赖收集(会触发get方法),数据更新了就找到属性对应的watcher去触发更新
取值的时候收集依赖,设值的时候更新试图
Vue
中模板编译原理我们用户传递的是template属性,我们需要将这个template编译成render函数
最终每次渲染可以调用render函数返回对应的虚拟节点(递归是先子后父) I
就是内部利用了一个发布订阅模式,将用户写的钩子维护成了一个数组,后续一次调用 callHook
内部就是一个发布订阅模式
为什么有些钩子的执行是先子后父亲,有些是先父后子,组件渲染是如何渲染的
// 遇到父组件就先渲染父组件
<div id='app'>
// 遇到子组件就渲染子组件
<my-button >
// 先渲染子组件后,完成才能渲染完毕父组件
</div>
Vue
的生命周期方法有哪些?一般在哪一步发送请求及原因beforeCreate 这里没有实现响应式数据 vue3中不需要了 没用
created √ 拿到的是响应式数据(不涉及到dom渲染),这个api可以在服务端渲染中使用
beforeMount
mounted √ 可以获取$el
beforeUpdate
updated
actived
deactivated
beforeDestroy √ 手动调用移除会触发
destroyed √ 销毁后触发
errorCaptured 捕获错误
-> 一般最多是在mounted(created不是比mounted早嘛?代码是同步进行的,请求是异步的)
Vue.mixin
的使用场景和原理我们可以通过Vue.mixin来实现逻辑的复用,问题在于数据来源不明确。声明的时候可能会导致命名冲突。高阶组件,vue3采用的就是compositionAPI解决了复用问题
Vue.mixin({
data() {
return {xxx:11}
},
beforeCreate() {
this.$store = new Store()
},
beforeDestory() {
}
})
Vue.component('my', {
data() {
return { xxx: 222 }
},
template: '{{xxx}}'
})
Vue
组件data为什么必须是个函数?原因就在于针对根实例而言,new Vue, 组件时通过同一个构造函数多次创建实例,如果同一个对象的话,那么数据会被相互影响;每个组件的数据源
都是独立的,那就每次都调用data返回一个新对象。
const Vue = {}
function Sub() {}
Vue.extend = function(options) {
function Sub() {
this.data = this.constructor.options.data()
}
Sub.options = options;
return Sub;
}
let Child = Vue.extend({
data() {
return { a: 1}
}
})
let c1 = new Child()
c1.data.a = 100;
let c2 = new Child()
console.log(c2.data.a);
nextTick
在哪里使用?原理是?nextTick内部采用了异步任务进行了包装(多个nextTick调用会被合并成一次 内部会合并回调) 最后在异步任务中批处理
主要应用场景就是异步更新(默认调度的时候 就会添加一个nextTick任务) 用户为了获取最终的渲染结果需要在内部任务执行之后再执行用户逻辑
这时候用户需要将对应的逻辑放到nextTick中
computed
和watch
区别computed 和 watch的相同点:底层都会创建一个watcher(用法区别:computed定义的属性可以在模板中使用,watch不能在试图中使用)
vue.set
方法是如何实现的Vue.set 方法时vue中的补丁方法。(正常添加的属性时不会触发更新的,我们数组无法监控索引和长度)
如何实现的 我们给每一个对象都增添了一个dep属性
vue3中也不需要此方法了(当属性添加或删除时 手动触发对象本身dep来进行更新)
Vue
为什么需要虚拟DOMdiff算法:针对更新的时候,有了虚拟dom之后我们可以通过diff算法来找到最后的差异进行修改真实dom(类似于在真实dom之间做了一层缓存)
跨平台、diff算法
Vue
中diff
算法原理diff算法的特点就是平级比较,内部采用了双指针方式进行了优化 优化了常见的操作。采用了递归比较的方式。
O(n)复杂度的递归比较
diff
检测差异isSameVnode中会根据key来判断两个元素是否是同一个元素,key不相同说明不是同一个元素(key在动态列表中不要使用索引 -> bug)
我们使用key尽量保证key的唯一性(这样可以优化diff算法)
组件的优点: 组件的复用可以根据数据渲染对应的组件,把组件相关的内容放在一起(方便复用)合理规划组件,可以做到更新的时候是组件级更新
(组件化中的特性:属性,事件,插槽)
Vue中怎样处理组件
1. Vue.extend 根据用户的传入的对象生成一个组件的构造函数
2. 根据组件产生对应的虚拟节点 data:{hook:init}
3. 做组件初始化:将我们的虚拟节点转换成真实节点(组件的init方法) new Sub().$mount
Vue
的组件渲染过程 (init)Vue
组件更新流程 (prepatch)Vue
中异步组件原理Vue中异步组件的写法有很多,主要用作,大的组件可以异步加载的,markdown组件,editor组件,就是先渲染一个注释标签,等组件加载完毕,最后在重新渲染forceUpdate(图片懒加载) 使用异步组件会配合webpack
React中也区分两种组件 一种叫函数式组件(Sub就是类组件,有this) (函数组件 没有类,就是this,也没有所谓的状态,没有生命周期 beforeCreate, created… 好处就是性能好,不需要创建watcher了)
props 父传递数据给子组件 属性的原理就是把解析后的props,验证后就会将属性定义再当前的实例上 vm._props(这个对象上的属性都是通过defineReactive来定义的(都是响应式的)组件再渲染的过程中会去vm上取值 _props属性会被代理到vm上)
emit 儿子触发组件更新 在创建虚拟节点的时候将所有的事件 绑定到了listeners,通过 o n 方法绑定事件通过 on方法绑定事件 通过 on方法绑定事件通过emit 方法来触发事件(发布订阅模式)
events Bus原来就是 发布订阅模式 $bus = new Vue() 简单的通信可以采用这种方式
$parent $children 就是在创建子组件的时候,会将父组件的实例传入。在组件本身初始化的时候会构建组件间的父子关系 p a r e n t 获取父组件的实例,通过 parent获取父组件的实例,通过 parent获取父组件的实例,通过children 可以获取子组件的实例
ref 可以获取dom元素和组件的实例 (虚拟没有处理ref,这里无法拿到实例 也无法获取组件)
创建dom的时候如何处理ref的。会将用户所有的dom操作及属性 都维护到一个cbs属性中 cbs(created update insert destory…) 依次调用cbs中create方法。这里就包含ref相关的操作,会操作ref并且赋值
provide (在父组件中将属性暴露出来) inject 在后代组件中通过inject属性 在父组件中提供数据,在子组件中递归向上查找
$attrs(所有的组件上的属性 不涵盖props) $lisnteners(组件上所有的事件)
Vue.observal 可以创造一个全局的对象用于通信 用的也少
vuex
function render() {
with(this) {
return _c('div', _l((3), function(i) {
return (flag) ? _c('span') : _e()
}), 0)
}
}
v-for的优先级更高,在编译的时候,会将 v-for渲染成_l函数 v-if会变成三元表达式。 v-if和v-for不要在一起中使用
v-if(是否渲染) / v-show(控制的样式 display:none) v-show=“true” 放在span上面会变成块元素嘛
为什么不用 visbility: hidden? 不能响应事件(占位的) 为什么不用 opacity呢? (透明度为0 占位) 可以相应事件的
v-if 在编译的时候,会变成一个三元表达式 v-show在编译的时候变成一个指令
v-if会被编译成三元表达式
v-for会被编译成_l 循环 (render-list.js)
v-model 放在表单元素上可以实现双向绑定,放在组件上就不一样了
v-model 放在不同的元素上回编译出不同的结果,针对文本来说会处理文本(会被编译成 value + input + 指令)
<input type="input" v-model="msg" />
function render() {
with(this) {
return _c('input', {
// 这里在指令里面会监听中文的输入完毕的事件(compositionstart compositionend)
// compositionend 方法里面会将composing变成false, 并且手动触发input事件,手动触发
directives: [{
name: 'model',
rawName: 'v-model',
value: (msg),
expression: 'msg'
}],
attrs: {
type: 'text'
},
domProps: {
value: (msg)
},
on: {
input: function($event) {
if($event.target.componsing) return;
msg = $event.target.value
}
}
})
}
}
<div id="app">
<my>
<div>{{msg}}</div>
</my>
</div>
//组件my
new Vue({
data: {
msg: 'outer'
},
components: {
my: {
data() {
return { msg: 'inner'}
},
template: `<div class='my'><slot></slot></div>`
}
}
})
编译父组件
const templateComplier = require('vue-template-compiler')
let result = templateComplier.compile(`<my><div>{{msg}}</div></my>`)
// result被编译成 [_c('div',[_v(_s(msg))])]) 会被立即执行
// _c('my', [_c('div',[_v(_s(msg))])])
1. 编译虚拟节点,生成vnode
//_c 最终编译成
// new Vnode = {'tag': 'my', componentOptions: {children: {tag: 'div', 'hello'}}}
组件的孩子叫插槽,元素的孩子就是孩子
2. 进行组件初始化,创建组件的真实节点
// 进行组件初始化的时候,调用 initInternalComponent方法
function initInternalComponent() {
...
opts._renderChildren = vnodeComponentOptions.children
}
// 创建组件的真实节点, 在slots上面增加属性
function initRender() {
...
this.$slots = resolveSlots(options._renderChildren) // options._renderChildren 这里拿到的是 initInternalComponent 处理过的 _renderChildren
}
function resolveSlots(chilren) {
for(let i = 0, l = children.length; i < l; i++) {
const child = children[i]
slot.defalut.push(child)
}
}
// this.$slots = [default: [儿子虚拟节点]]
// 将结果做一个映射:this.$scopeSlots = { a:fn, b: fn, defalut: fn }
编译子组件
templateComplier.compile(`<div class="my"><slot><</div>`)
最终会被编译成:
_c('div', {staticClass: 'my'}, [_t('defalut')])
// 组件渲染的时候,会采用组件的模板:_t('defalut'), 在子组件渲染的时候,会通过 _t找到刚才的映射关系进行替换
普通插槽 (普通插槽渲染作用域在父组件中的)
具名插槽 多增加了名字
作用域插槽 (作用域插槽渲染作用域在子组件中的)
<div @click.prevent></div>
<div @click.stop></div>
<div @click.passive></div>
<div @click.capture></div>
<div @click.one></div>
<my :xx.sync="info"></my>
function render() {
with(this) {
return _c('my', {
attrs: {
'xx': info
},
on: {
'update:xx': function($event) {
info = $event
}
}
})
}
}
1.keep-alive在路由中使用
2.在component:is 中使用(缓存)
keep-alive的原理是默认缓存加载过的组件对应的实例 内部采用LRU算法
下次组件切换加载的时候 此时会找到对应缓存的节点来进行初始化,但是会采用上次缓存$el来触发
更新和销毁会触发 actived 和 deactived