vue如何实现双向绑定
v-model通过监听用户的input事件来更新数据。
使用Object.defineProperty(),来监听数据get和set,来实现数据劫持
订阅者模式:每一个{{ name }} v-model = 'name' 都会添加一个订阅者,从而监听不同部分的变化,每一部分变化时都会循环触发相应的订阅者,更新到页面中。
Vue双向绑定和vue3.0Proxy的区别
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或修改对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
参数:要定义属性的对象、要定义或修改的属性名,要定义或修改的属性描述符
let obj
Object.defineProperty(obj, 'name', {
set: function(val) {
if (val === 'liu') {
console.log('名字叫刘xx')
} else {
copyObj.name = val
}
},
get: function() {
return copyObj.name.replace()
}
})
obj.name = 'liu' // 名字叫
堆和栈的区别
栈:自动分配大小,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。——执行栈。 堆:动态分配的内存,大小不会自动释放,存放引用类型,引用类型:指那些可能由多个值构成的对象(以及闭包的变量),保存在堆内存中,包含应用类型的变量,实际保存的不是变量本身,而是该对象的指针——内存堆。
堆和栈的优缺点
栈:存取速度快,仅次于直接位于CPU的寄存器中,数据可共享;但存在栈中的数据大小和生命周期必须是确定的,缺乏灵活性。
堆:堆内存中的对象不会随方法的结束而销毁,因为即使方法结束,这个对象还可能被另一哥引用变量所引用(参数传递)。创建对象是为了反复利用,这个对象将被保存到运行时数据区
堆和栈的溢出
栈:可以递归调用方法,随着栈深度的增加,JVM(内存模型)维持着一条长长的方法调用轨迹,直到内存不够分配,产生栈溢出。 堆:循环创建对象,即不断new一个对象
数组的方法
contact():链接两个及多个数组,并返回结果
forEach(): array.forEach(function(currentValue, index, arr), thisValue)
includes():数组中是否包含某元素,返回布尔
indexOf():返回数组中指定元素第一次出现的索引
isArray():判断是否是数组,返回布尔
join():将数组以指定间隔转换为字符串
lastIndexOf():从后向前查找,返回指定元素第一次出现的下标(方法:endWith)
map():遍历数组,返回一个新数组
pop():删除数组的最后一个元素并返回删除的元素
push():向数组末尾添加一个或多个元素,返回新的数组长度
reduce():给定初始值,遍历数组
reduceRight():给定初始值,遍历数组(从右往左)
reverse():翻转数组的元素顺序
shift():删除并返回数组的第一个元素
splice(): 向数组中添加或删除指定位置的元素
slice(): 选取数组的一部分,并返回一个新数组(同字符串中用法相同(参数指索引,不包含下标end的元素))
toString():数组转换为字符串
unshift():向数组开头添加一个或更多元素,返回新数组长度
valueOf():
copyWithin(): 从数组的指定位置拷贝元素到数组的另一个指定位置
entries(): 返回数组的可迭代对象
every(): 检测数组的每一个元素是否否符合条件
fill(): 使用一个固定的值填充数组(可在创建空数组时使用)
findIndex(): 返回符合传入函数条件的数组元素索引
find(): 返回符合条件的元素
form(): 通过给定对象创建一个数组
keys(): 返回包含数组键值的可迭代对象
some(): 检测数组中是否有符合指定条件的元素
sort(): 数组排序
filter(): 过滤元素
空div默认高度
默认20px左右,这个默认高度有页面文字大小决定。
<div style="color: red;background-color: red;">
<div style="display:inline-block"></div>
</div>
可以采用font-size: 0px;来解决
<div style="color: red;background-color: red;font-size: 0px">
<div style="display:inline-block"></div>
</div>
js 的函数作用域跟块级作用域(var和let)
全局作用域其实就是一个函数作用域
函数作用域:变量定义在函数内及嵌套的子函数内处处可见;
块级作用域:变量在离开定义的块级代码后马上被回收;
if (true) {
let a = 10;
}
console.log(a) // undefined a is not defined
//注意:使用let,const关键字声明的变量才具有块级作用域
if (true) {
var a = 10;
}
console.log(a)//10
function fn(){
var i=6;
return
}
console.log(i);//i is not defined
js中没有块级作用域,“块级作用域”中声明的变量将被添加到当前的执行环境中。
var声明的变量会自动添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境。
因为{}不会产生新的局部环境,而function会产生新的局部环境,因此,function中用var定义的变量在外部无法拿到;但{}中用var定义的变量可以拿到;
let和const会生成块级作用域,因此{}中用let和const定义的变量,{}外无法拿到;
在全局第一行var定义变量和let定义变量有什么区别?
var定义的变量会被绑定到window对象,而let定义的变量不会
算法:如何判断一个链表中是否有环的存在
单链表中的结点都是一个结点指向下一个结点这样链接起来的,知道尾结点的指针域没有指向,单链表就结束了。
存在环是指:尾节点的指针域不为空,而是指向此单链表的其他节点,就会形成环
js判断一个对象是否存在环
将一个js字面量对象转化为一个JSON格式的字符串
const obj = {a:1, b:2}
JSON.stringify(obj) // => '{"a":1,"b":2}'
当要转化的对象有“环”存在时(子节点属性赋值了父节点的引用),为了避免死循环,JSON.stringify 会抛出异常,
const obj = {
foo: {
name: 'foo',
bar: {
name: 'bar',
baz: {
name: 'baz',
aChild: null //待会让它指向obj.foo
}
}
}
}
obj.foo.bar.baz.aChild = obj.foo // foo->bar->baz->aChild->foo 形成环
JSON.stringify(obj) // => TypeError: Converting circular structure to JSON
“环”的形成是因为给对象的子节点属性赋值了父节点的引用,所以需要记录下父节点的地址,然后再拿其子节点的属性与之前记录的父节点地址做比较,当结果一致时,就形成了“环”
先遍历对象属性,因为简单数据类型不存在引用关系,因此只需对Object;类型的属性进行处理。
function cycleDetector(obj) {
var hasCircle = false, // 定义一个变量,标志是否有环
cache = []; // 定义一个数组,来保存对象类型的属性值
(function(obj) {
var keys = Object.keys(obj); //获取当前对象的属性数组,只能拿到一层属性
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = obj[key];
if (typeof value == 'object' && value !== null) {
var index = cache.indexOf(value)
if (index !== -1) {
hasCircle = true
break
} else {
cache.push(value)
arguments.callee(value)
cache.pop() // 注意:这里要推出数据,因为递归返回,后面遍历的属性不是这个数据的子属性
}
}
}
})(obj)
return hasCircle
}
arguments.callee
在函数内部,有两个特殊的对象:arguments和this,arguments主要用于保存函数参数,arguments有一个属性callee,该属性是一个指针,指向拥有这个arguments对象的函数function factorial(num){ if (num <=1) { return 1; } else { return num * factorial(num-1) } }以上递归函数中,函数的执行与函数名紧紧耦合在一起,可以使用
callee解耦function factorial(num){ if (num <=1) { return 1; } else { return num * arguments.callee(num-1); } }
nextTick的原理
用法:在下次DOM更新循环结束后执行延迟回调。在修改数据后立即使用这个方法,获取更新后的DOM。
1、DOM更新循环是指什么?
2、下次更新循环是什么时候?
3、修改数据之后使用,是加快了数据更新进度吗?
4、在什么情况下用到?
原理
VUE实现响应式并不是数据发生变化之后DOM立即变化,而是按一定的策略进行DOM的更新。 VUE异步执行DOM更新。
异步执行的运行机制:
(1) 所有同步任务都在主线程上执行,形成一个执行栈
(2) 主线程之外,存在一个“任务队列”,只要异步任务有了运行结果,就在“任务队列”中放置一个事件。
(3) “一旦”执行栈中所有的同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4) 主线程不断重复第三步操作。单线程导致主线程在遇到IO请求时,需要等待接口返回才能继续往下执行,但此时主线程是空闲的。 Event Loop实现了主线程可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等IO设备返回了结果,再回过头,把挂起的任务继续执行下去。(即,在等待异步请求接口返回时,可以先执行主线程的同步任务,再执行“任务队列”中的异步请求)
事件循环说明
Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中所有的数据变化完成后,在统一进行视图更新。
1、第一步:
(1) 修改数据(同步任务)。同一事件循环的所有同步任务都在主线程上执行,形成一个执行栈,此时还未涉及DOM
(2) Vue开启一个异步队列,并缓冲在此事件循环中发生的所有数据变化。如果同一个watcher被多次触发,只会被推入到队列中一次。(nextTick为微任务)
2、第二步:(即‘下次更新循环’)
同步任务执行完毕,开始执行异步watcher队列的任务,更新DOM。Vue在内部尝试对异步队列使用原生的Promise.the和MessageChannel方法,如果执行环境不支持,会采用setTimeout(fn, 0)
3、第三步:
即之前说的“下次DOM更新循环结束之后”
用途
需要在视图更新之后,基于新的视图进行操作
created、mounted
在created和mounted阶段,如果需要操作渲染后的视图,也需要使用nextTick方法。
mounted不会承诺所有子组件也都一起被挂载。如果希望等到整个视图都渲染完毕,可以使用vm.$nextTick替换mounted
mounted: function () { this.$nextTick(function () { }) }
如何防止冒泡
e.stopPropagation()会阻止元素冒泡事件,但不会阻止默认行为
e.preventDefault()取消目标元素的默认行为,如:a链接的跳转行为,提交按钮<input type="submit">
vue的事件修饰符
- stop:阻止事件继续传播,
e.stopPropagation() - prevent:阻止默认行文,
e.preventDefault() - capture:添加事件监听器使用事件捕获模式,即内部元素触发的条件先在此处理,然后才交由内部元素处理
- self:当事件发生的对象是在当前元素自身时触发处理函数,即事件不是从内部元素触发的
- once:点击事件只能执行一次
- passive:立即触发默认事件(不能和.prevent一起使用)
修饰符的顺序很重要:
v-on:click.prevent.self 会阻止所有的点击
v-on:click.self.prevent 只会阻止对元素自身的点击。 按键修饰符
- enter
- tab
- delete (捕获“删除”和“退格”键)
- esc
- space
- up
- down
- left
- right
<input v-on:keyup.13="submit">
适配不同分辨率的方法
1、根据不同分辨率加载不同css样式
2、采用媒体查询
<!-- 分辨率低于1280,采用test-01.css样式表 -->
<link rel="stylesheet" media="screen and (max-device-width:1280px)" href="test-01.css">
/*分辨率低于1280,采用下面的样式*/
@media screen and (max-device-width:1280px){
div{
width: 200px;
height: 200px;
background-color: green;
}
}
3、vh/vw/百分比
keep-alive的原理
是什么
keep-alive是一个抽象组件:自身不会渲染DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive用于保存组件的渲染状态
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
:include缓存白名单,会缓存其中的组件,:exclude缓存黑名单;:max定义缓存组件上限,超出上限使用LRU的策略置换缓存数据
内存管理的一种页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。
源码剖析
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判断当前组件虚拟dom是否渲染成真实dom的关键
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 缓存的组件
},
created() {
this.cache = Object.create(null) // 缓存虚拟dom
this.keys = [] // 缓存的虚拟dom的键集合
},
destroyed() {
for (const key in this.cache) {
// 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matched(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
// 先省略...
}
}
同定义组件的过程一样,先设置组件名为keep-alive,然后定义abstract属性为true。
- created:初始化两个对象分别缓存VNode(虚拟DOM)和VNode对应的键集合
- destroyed:删除this.cache中缓存的VNode实例。注:并不是单纯的将this.cache置为null,而是遍历调用
pruneCacheEntry函数删除
// src/core/components/keep-alive.js
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroyed() // 执行组件的destroy钩子函数
}
cache[key] = null
remove(keys, key)
}
- mounted:在mounted钩子中对include和exclude参数进行监听,然后实时更新(删除)this.cache对象数据。
pruneCache函数的核心也是去调用pruneCacheEntry
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
- render
render () {
const slot = this.$slots.defalut
const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
const componentOptions : ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在组件参数
// check pattern
const name: ?string = getComponentName(componentOptions) // 组件名
const { include, exclude } = this
if (// 条件匹配
// not included
(include && (!name || !matches(include, name)))||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
// 定义组件的缓存key
const key: ?string = vnode.key === null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key
if (cache[key]) { // 已经缓存过该组件
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // 调整key排序
} else {
cache[key] = vnode //缓存组件对象
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
//超过缓存数限制,将第一个删除
pruneCacheEntry(cahce, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true //渲染和执行被包裹组件的钩子函数需要用到
}
return vnode || (slot && slot[0])
}
- 第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
- 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
- 第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
- 第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);
- 第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。
渲染
Vue的渲染是从render阶段开始的,但keep-alive的渲染实在patch阶段,这是构建组件树(虚拟DOM树),并将VNode转换为真正DOM节点的过程。
package.json 打包时额外参数配置
--mode 指定环境模式(默认:production)
--dest 指定输出目录(默认:dist)
--modern 面向现代浏览器带自动回退地构建应用
--target app | lib | wc | wc-async(默认值:app)
--name 库或webComponents模式下的名字
--no-clean 在构建项目之前不清除目标目录
--report 生成report.html
--report-json 生成report.json
--watch 监听文件变化
如何避免重复刷接口?
promise
class
js实现继承的几种方法
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
1、原型链继承
将父类的实例作为子类的原型
function Cat() {
}
Cat.prototype = new Animal();
Cat.prototype.name = 'Cat'
var cat = new Cat();
function Cat(){
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name); // cat
console.log(cat.eat('fish')); // cat正在吃:fish
console.log(cat.sleep()); // cat正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现 缺点:
- 想要为子类新增属性和方法时,定义在
new Animal之前的属性和方法会被重置为undefined,想要新增属性和方法需要在new之后执行- 无法实现多继承(即无法同时继承多个父元素)
- ☆ 来自原型对象的所有属性被所有实例共享
- ☆ 创建子类实例时,无法向父类构造函数传参(
new a()时无法传参)
2、构造继承
使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name){
Animal.call(this); // 将Cat中的this指给Animal
this.name = name || 'Tom' // 覆盖父类实例的name属性
}
var cat = new Cat('cat2');
console.log(cat.name); // Tom/cat2
console.log(cat.sleep()); // Tom/cat2正在睡觉!
console.log(cat.eat(food)); // cat.eat is not a function(报错)
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
特点:
- 解决了方法1中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象) 缺点:
- 实例并不是父类的实例(实例不是Animal),只是子类的实例(实例是Cat)
- 只能继承父类的实例属性和方法,不能继承原型属性/方法(继承了sleep但无eat方法)
- ☆ 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3、实例继承
为父类实例添加新特性,作为子类实例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom2'; // 覆盖父类实例的name属性
return instance;
}
var cat = new Cat();
console.log(cat.name); // Tom2
console.log(cat.sleep()); // Tom2正在睡觉!
console.log(cat.eat('food')); // Tom2正在吃:food
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
特点:
- 不限制调用方式,不管是
new 子类()还是子类(),返回的对象具有相同的效果(即var cat = Cat()同样可以达到调用Cat子类的效果) 缺点:- 实例是父类的实例,不是子类的实例(new的cat实例是Animal的实例,不是Cat的实例)
- 不支持多继承(同方法1一样不支持多继承)
4、拷贝继承(不推荐)
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
// Cat.prototype.name = name || 'Tom'; 错误的语句,修改了原型对象,会导致单个实例修改name,会影响所有实例的name值
this.name = name || 'Tom';
}
var cat = new Cat();
特点:
- 支持多继承 缺点:
- 效率低,内存占用高(因为要拷贝父类的属性)
- 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in访问到)
5、组合继承(推荐)
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
xx函数.prototype.constructor === xx函数
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
// 组合继承也是需要修复构造函数指向的。
Cat.prototype.constructor = Cat;
var cat = new Cat();
特点:
- 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
- 即使子类的实例,也是父类的实例
- 不存在引用类型共享的问题
- 可传参
- 函数可复用 缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
6、寄生组合继承(完美)
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造时,就不会初始化两次实例方法/属性,避免了组合继承的缺点
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}
(function() {
// 创建一个没有实例的方法的类
var Super = function() {};
Super.prototype = Animal.prototype;
// 将实例作为子类的原型
Cat.prototype = new Super();
})();
Cat.prototype.constructor = Cat; // 需要修复构造函数
var cat = new Cat()
num++和++num的区别
num++先将值进行运算(如赋值等),后自加;++num先自加后进行运算;
vue中mixin混入和vuex状态管理相比
混入(mixin),用来分发组件中的可复用功能。一个混入对象可以包含任意组件选项。组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
可以在混入中定义任何的生命周期
全局中引入混入:
import Vue from 'vue'
import mixin from "./mixin/mixin.js"
Vue.mixin(mixin)
mixin特性:
- 当
组件和混入对象有同名选项时,这些选项将以恰当的方式进行“合并”。比如:数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。同名钩子函数将合并为一个数组,因此都将被调用。混入对象的钩子函数在组件自身的钩子函数之前调用。 - 值为对象的选项,如
methods、components、directives(自定义指令),将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。 mixin和Vuex的区别: - vuex中的方法和变量是可以互相读取并互相更改的,mixin不会。mixin可以定义公用的变量或方法,但是mixin中的数据是不共享的,即每个组件中的mixin实例都不一样,都是单独存活的个体,不存在相互影响。