【记录】前端知识点 - Vue

MVC、MVP、MVVM

(针对前端框架的理解)
在这里插入图片描述

Vue没有完全遵守MVVM规范。因为严格的MVVM要求View和Model不能直接通信,而Vue通过$ref属性可以在ViewModel之外操纵到dom,也就是可以让Model操纵View。

参考JavaScript设计模式(推荐去看看,里面讲的更详细,落实到具体如何实现。后续再出个blog。。。)

为什么data要写成函数式,而不是对象式?

在拥有多个组件的情况下,写成函数式,每复用一次组件,都会返回新的data,相当于为每个组件实例创建一个私有数据空间。如果写成对象式,所有的组件事例都会共享一个data。

内置指令

v-bind

  • 简写:
  • 单向绑定(data=>页面)。

v-model

  • 双向绑定。

v-on

  • 简写@

v-if、v-else-if、v-else

v-show

v-for

v-text

v-html

v-once

v-pre

  • 跳过该节点及其子节点的编译。
  • 可以利用它跳过:没有使用指令语法、插值语法的节点。

v-cloak

  • Vue实例创建完毕并接管容器后会删除v-cloak属性。
  • 使用css配合v-cloak可以解决网速慢时页面展现出{{}}的问题。
<style>
	[v-cloak]{
		display: none;
	}
</style>
<h1 v-cloak>{{name}}</h1>

v-if和v-show的区别

v-if

  • 会被转换为三元表达式,条件不满足时不渲染。
  • 适用于运行时很少改变条件、不需要频繁切换条件的场景。

v-show

  • 会被编译成指令,条件不满足时控制样式将节点隐藏(display: none)。
  • 适用于需要频繁切换条件的场景。

display: none和visiblity: hidden和opacity: 0的区别

display: none

  • 隐藏后不占位置。
  • 不会被子元素继承,子元素不会显示。
  • 隐藏后无法再触发绑定的事件。
  • 过渡动画transition对其无效。

visibility: hidden

  • 隐藏后占位置。
  • 会被子元素继承,通过设置子元素visibility: visible可以让子元素显示。
  • 隐藏后不会再触发绑定的事件。
  • 过渡动画transition对其无效。

opacity: 0

  • 隐藏后占位置。
  • 会被子元素继承,无法通过设置子元素来让子元素显示。
  • 隐藏后可以触发绑定的事件。
  • 过渡动画transition对其有效。

v-if和v-for为什么不建议一起使用?

因为解析时会先解析v-for再解析v-if。如果遇到需要同时使用时可以考虑写成计算属性。

事件修饰符

stop

  • 阻止事件冒泡。

prevent

  • 阻止默认行为。

capture

  • 使用事件捕获模式。(事件从自身往内传播)

self

  • 只有event.target是当前元素自身时才触发事件。

once

  • 事件只触发一次。

passive

  • 事件的默认行为立即执行,无需等待回调函数执行完毕再执行。
//假设设定了监听滚动事件
//默认情况下,浏览器会等待回调函数执行完才执行默认行为。

camel

  • 识别驼峰。
//不加camel viewBox会被识别成viewbox
<svg :viewBox="viewBox"></svg>

//加了camel viewBox才会被识别成viewBox
<svg :viewBox.camel="viewBox"></svg>

sync

  • 当父组件传值进子组件,子组件想要改变这个值时使用。
//未使用
//父组件
<children :foo="bar" @update:foo="val => bar = val"></children>
//子组件
this.$emit('update:foo', newValue);

//使用
//父组件
<children :foo.sync="bar"></children>
//子组件
this.$emit('update:foo', newValue);

生命周期

beforeCreate

  • 初始化数据监测、数据代理。
  • 无法访问到data、methods、computed、watch上的数据和方法。

created

  • 可以访问到data、methods、computed、watch上的数据和方法。

解析模版,生成虚拟DOM。

beforeMount

  • 此时页面显示的是未经编译的DOM结构。此时对DOM的操作,最终都不奏效。

将虚拟DOM转为真实DOM挂载在页面上。

mounted

  • 此时页面显示的是经过编译的DOM结构。此时对DOM的操作都有效。

beforeUpdate

  • 此时数据是新的,但页面是旧的(尚未和数据保持同步)。

虚拟DOM重新渲染,打补丁(patch)。

updated

  • 此时数据是新的,页面也是新的(页面和数据保持同步)。
  • 该钩子在服务器渲染期间不被调用。

beforeDestory

  • 此时所有的data、method等都是可用的。
  • 一般在此进行:关闭定时器、取消订阅消息、解绑自定义事件等收尾操作。

移除监视、子组件、事件监听器。

destroyed

  • 自定义事件会失效,但原生DOM事件依然有效。
  • 该钩子在服务端渲染期间不被调用。

异步请求一般在哪个生命周期?

可以在created、beforeMount、mounted中进行异步请求。
如果异步请求不依赖DOM,推荐在created中调用异步请求。因为能够更快地获取到服务端数据,减少页面loading时间;ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性。

父子组件生命周期钩子函数执行顺序

在这里插入图片描述

组件通信方式

props和$emit

//props:只读,不可修改;若要修改,复制后再修改。
//父组件
<Children name="a"/>
//子组件
export default{
	name: 'Children'
	data(){
		return{}
	},
	props: ['name'],
}
//emit
//子组件
this.$emit('name', data);
//父组件
<Chilren @name="event"/>
...
event(e){
	console.log(e);
}

$attrs和$listeners

  • $attrs获取父组件给子组件绑定的未被props声明的属性。
    inheritAttrs: false// 可以关闭自动挂载到组件根元素上的没有在props声明的属性。

  • $listeners获取父组件给子组件绑定的非原生事件。

provide和inject

  • 允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
//不可响应
//父组件
export default{
	provide: {
		name: 'a'
	}
}
//子组件
export default{
	inject: ['name'],
	mounted(){
		console.log(this.name);
	}
}

globalEventBus

//main.js
new Vue({
	...
	beforeCreated(){
		Vue.prototype.$bus = this;
	}
})
//提供数据
methods:{
	sendData(){
		this.$bus.$emit('事件名', data);
	}
},
beforeDestoryed(){
	this.$off('事件名');
}
//接受数据
mounted(){
	this.$bus.$on('事件名', (data)=>{
		...
	});
}					  

vuex

$refs直接访问

参考

computed和watch的区别、应用场景

computed

  • 计算属性,依赖其他属性计算值。computed的值有缓存,只有当计算值发生变化时才会返回内容。
  • 不能进行异步操作。

watch

  • 监听属性。当监听的值发生变化时执行回调,在回调里完成一些逻辑操作。
  • 可以进行异步操作。

Vue中使用了哪些设计模式?

路由模式

hash模式
通过触发hashchange事件实现路由切换。
刷新页面时可以加载到hash值对应页面。

  • 优点
    兼容性好。
    无需服务端配置。
  • 缺点
    可能和锚点功能冲突。
    SEO不友好。

history模式
通过pushState、replaceState方法修改history对象,触发popState事件,实现路由切换。
刷新页面会向服务端发送请求,需服务端将路由重定向至根页面,否则会404。根页面通过js判断加载相应的组件。

  • 优点
    SEO友好。
  • 缺点
    服务端需额外配置。
    兼容性差。

响应式数据原理

数据劫持+观察者模式
对象内部通过defineReactive方法,使用Object.defineProperty对属性进行劫持;数组通过重写数组方法来实现。每个属性都有自己的dep,存放着订阅它的watcher,当属性发生变化时会通知自己的watcher更新。

Object.defineProperty不能完全劫持所有数据的变化

  • 新建、删除的属性,视图无法更新=>this.$set(obj, key, value)、this.$delete(obj, key)
  • 无法利用下标和长度修改数组=>this.$set(obj, key, value)、用数组的原生方法进行修改。arr.splice(0, 1, ‘a’)

原文
参考

//传入构造函数里的对象被称为options对象
new Vue({
	el: '#app',
	router,
	store,
	render: (h)=>h(app),
});
/*
	数据初始化
*/
// src/index.js
import { initMixin } from './init.js';
//Vue构造函数
function Vue(options){
	//调用初始化函数
	this._init(options);
}
//调用函数,把初始化函数挂载在Vue原型上
initMixin(Vue);
export default Vue;

// src/init.js
import { initState } from './state';
export function initMixin(Vue){
	Vue.prototype._init = function(options){
		//this指向调用_init方法的对象
		const vm = this;
		vm.$options = options;
		initState(vm);
	}
};

// src/state.js
import { observe } from './observer/index.js';
export function initState(vm){
	const opts = vm.$options;
	//注意顺序:props>methods>data>computed>watch
	if(opts.props){
		initProps(vm);
	}
	if(opts.methods){
		initMethods(vm);
	}
	if(opts.data){
		initData(vm);
	}
	if(opts.computed){
		initComputed(vm);
	}
	if(opts.watch){
		initWatch(vm);
	}
}
function initData(vm){
	let data = vm.$options.data;
	//判断data是否是函数式,如果是函数式用call改变this的指向,执行data函数
	data = vm._data = typeof data === 'function'? data.call(vm) : data || {};
	
	//数据代理
	//把data代理到vm身上,也就是说在vm上,可以通过this.a来访问this._data.a
	//相当于遍历data的属性,在vm身上添加相同属性
	for(let key in data){
		proxy(vm, `_data`, key);
	}
	
	//数据监测
	observe(data);
}
function proxy(object, sourceKey, key){
	Object.defineProperty(object, key, {
		get(){
			//读取该值时,返回原data身上的值
			return object[sourceKey][key];
		},
		set(newValue){
			//修改该值时,将原data身上的值修改为新值
			object[sourceKey][key] = newValue;
		}
	})
}

// src/observer/index.js
import { arrayMethods } from './array';
class Observer{
	constructor(value){
		this.dep = new Dep();
		
		//给value添加不可枚举的__ob__属性,表示该value已被响应式处理,防止被反复观测
		Object.defineProperty(value, `__ob__`, {
			value: this,//this指向observer实例
			enumberable: false,//不可枚举
			writable: true,
			configurable: true
		});
		
		if(Array.isArray(value)){
			//是数组,另外处理(如果数组走正常的响应式处理给每个数组元素添加getter/setter会造成性能上的问题)
			//改写数组原型方法
			value.__proto__ = arrayMethods;
			//对数组元素继续observe(如果是基本数据类型直接return,如果是对象或数组继续监测)
			this.observeArray(value);
			//也就是说数组本身没有做数据劫持,只有数组内的对象做了数据劫持。
		}else{
			//是对象,走数据劫持
			this.walk(value);
		}
	}
	walk(data){
		let keys = Object.keys(data);
		for(let i = 0; i < keys.length; i++){
			let key = key[i];
			let value = data[key];
			defineReactive(data, key, value);
		}
	}
	observeArray(items){
		for(let i = 0; i < items.length; i++){
			observe(items[i]);
		}
	}
}
//数据劫持
function defineReactive(data, key, value){
	//递归
	observe(value);
	Object.defineProperty(data, key, {
		get(){
			return value;
		},
		set(newValue){
			if(newValue === value) return;
			value = newValue;
		}
	});
}
export function observe(value){
	if(
		Object.prototype.toString.call(value) === '[object Object]' ||
		Array.isArray(value)
	){
		return new Observer(value);
	}
}

// src/observer/array.js
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = {
	"push",
	"pop",
	"shift",
	"unshift",
	"splice",
	"reverse",
	"sort"
};
methodsToPatch.forEach(method=>{
	arrayMethods[method] = function (...args){
		const result = arrayProto[method].apply(this, args);
		
		const ob = this.__ob__;
		
		//数组是否有新增操作
		let inserted;
		switch(method):{
			case "push":
			case "unshift":
				inserted = args;
				break;
			case "splice":
				inserted = args.slice(2);
			default:
				break;
		}
		
		//如果有新增元素,再进行监测
		if(inserted) ob.observeArray(inserted);
		
		//派发更新
		ob.dep.notifty();
		return result;
	}
});

渲染更新原理

先了解观察者模式和发布订阅者模式。

// src/lifecycle.js
export function mountComponent(vm, el){
	let updateComponent = () => {
		vm._update(vm._render());
	}
	new Watcher(vm, updateComponent, null, true);
}

// src/observer/index.js
function defineReactive(data, key, value){
	let childOb = observe(value);
	
	let dep = new Dep();
	
	Object.defineProperty(data, key, {
		get(){
			if(Dep.target){
				//将dep加入watcher的deps队列,同时把watcher加入dep的sub队列
				dep.depend();
				
				if(childOb){
					//依赖收集
					childOb.dep.depend();
					if(Array.isArray(value)){
						//数组递归依赖收集
						dependArray(value);
					}
				}
			}
			return value;
		},
		set(newValue){
			if(newValue === value) return;
			observe(newValue);
			value = newValue;
			//派发更新
			dep.notify();
		}
	});
}
function dependArray(value){
	for(let e, i = 0, l = value.length; i < l; i++){
		e = value[i];
		e && e.__ob__ && e.__ob__.dep.depend();
		if(Array.isArray(e)){
			dependArray(e);
		}
	}
}

// src/observer/watcher.js
//分配watcher id
let id = 0;
export default class Watcher {
	constructor(vm, exprOrFn, cb, options){
		this.vm = vm;
		this.exprOrFn = exprOrFn;
		this.cb = cb;//回调函数
		this.options = options;//true表示渲染watcher
		
		this.id = id++;
		
		this.deps = [];
		this.depsId = new Set();
		
		if(typeof exprOrFn === 'function'){
			this.getter = exprOrFn;
		}
		
		this.get();
	}
	get(){
		//调用方法前,将当前watcher实例推到Dep.target上
		pushTarget(this);
		this.getter();
		//调用方法后,将当前watcher实例从Dep.target上移除
		popTarget();
	}
	addDep(dep){
		let id = dep.id;
		if(!this.depsId.has(id)){
			this.depsId.add(id);
			this.deps.push(dep);
			dep.addSub(this);
		}
	}
	update(){
		//缓存队列
		queueWatcher(this);
	}
	run(){
		this.get();
	}
}

// src/observer/dep.js
//分配dep id
let id = 0;
export default class Dep{
	constructor(){
		this.id = id++;
		this.subs = [];
	}
	depend(){
		if(Dep.target){
			Dep.target.addDep(this);
		}
	}
	notify(){
		this.subs.forEach(watcher => watcher.update());
	}
	addSub(){
		this.subs.push(watcher);
	}
}
Dep.target = null;
const targetStack = [];
export function pushTarget(watcher){
	targetStack.push(watcher);
	Dep.target = watcher;
}
export function popTarget(){
	targetStack.pop();
	Dep.target = targetStack[targetStack.length - 1];
}

// src/observer/scheduler.js
import { nextTick } from '../util/next-tick';
let queue = [];
let has = {};
function flushSchedulerQueue(){
	for(let index = 0; index < queue.length; index++){
			queue[index].run();
	}
	//清空队列
	queue = [];
	has = {};
}
export function queueWatcher(watcher){
	const id = watcher.id;
	if(has[id] === undefined){
		queue.push(watcher);
		has[id] = true;
		nextTick(flushSchedulerQueue);
	}
}

nextTick

nextTick的回调是下次DOM更新循环结束之后执行的回调。
把要执行的回调放在队列里,采用微任务优先的方式调用异步方法执行。

// src/util/next-tick.js
let callbacks = [];
let pending = false;
function flushCallbacks(){
	pending = false;
	for(let i = 0; i < callbacks.length; i++){
		callbacks[i]();
	}
}
let timerFunc;//定义异步方法 采用优雅降级
if(typeof Promise !== 'undefined'){
	const p = Promise.resolve();
	timerFunc = () => {
		p.then(flushCallbacks);
	};
}else if(typeof MutationObserver !== 'undefined'){
	let counter = 1;
	const observer = new MutationObserver(flushCallbacks);
	const textNode = document.createTextNode(String(counter));
	observer.observe(textNode, {
		characterData: true,
	});
	timerFunc = () => {
		counter = (counter + 1)%2;
		textNode.data = String(counter);
	};
}else if(typeof setImmediate !== 'undefined'){
	timerFunc = () => {
		setImmediate(flushCallbacks);
	};
}else{
	timerFunc = () => {
		setTimeout(flushCallbacks, 0);
	};
}


export function nextTick(cb){
	callbacks.push(cb);
	if(!pending){
		pending = true;
		timerFunc();
	}
}

diff算法

function patch(oldVnode, newVnode){
	//是否为同类型节点
	if(sameVnode(oldVnode, newVnode)){
		//进行深层次比较
		patchVnode(oldVnode, newVnode);
	}else{
		//创建新虚拟节点的真实DOM,插入父节点下,移除旧虚拟节点对应的真实DOM
		...
	}
}
function sameVnode(oldVnode, newVnode){
	return (
		oldVnode.key === newVnode.key && //key值是否一样
		oldVnode.tagName === newVnode.tagName &&//标签名是否一样
		oldVnode.isComment === newVnode.isComment &&//是否都为注释节点
		isDef(oldVnode.data) === isDef(newVnode.data) &&//是否都定义了data
		sameInputType(oldVnode, newVnode)//标签为input时,type是否相同
	);
}
function patchVnode(oldVnode, newVnode){
	const el = newVnode.el = oldVnode.el;//获取真实dom对象
	const oldCh = oldVnode.children, newCh = newVnode.children;
	if(oldVnode === newVnode) return;
	//都是文本节点,且文本不一样
	if(oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text){
		//更新文本
		api.setTextContent(el, newVnode.text);
	}else{
		//都存在子节点,且子节点不一样
		if(oldCh && newCh && oldCh !== newCh){
			updateChildren(el, oldCh, newCh);
		}else if(newCh){
		//新节点有子节点,旧节点没有
			createEle(newVnode);
		}else if(oldCh){
		//旧节点有子节点,新节点没有
			api.removeChild(el);
		}
	}
}
//双指针移动比较新旧节点
//1、新旧节点的头头、尾尾、头尾、尾头比较,将真实节点移到对应的位置,命中移动双指针。
//2、以上都未命中,对旧节点建立key->index的map对象,按照新节点的头指针的key值查找,如果查找到将真实节点移到相应的位置,指针向右移动。如果找不到,新增相关节点。
//3、当新旧节点的头指针索引大于位指针索引时,跳出循环。
function updateChildren(){

}

Mixin


watch属性

为每个监听属性创建一个user watcher,当被监听的属性更新时,调用传入的回调函数。

// src/state.js
export function initState(vm){
	const opts = vm.$options;
	if(opts.watch){
		initWatch(vm);
	}
}
function initWatch(vm){
	let watch = vm.$options.watch;
	for(let k in watch){
		const handler = watch[k];
		if(Array.isArray(handler)){
			handler.forEach(handle => {
				createWatcher(vm, k, handler);
			});
		}else{
			createWatcher(vm, k, handler);
		}
	}
}
function createWatcher(vm, exprOrFn, handler, options = {}){
	if(typeof handler === 'object'){
		options = handler;
		handler = handler.handler;
	}
	if(typeof handler === 'string'){
		handler = vm[handler];
	}
	return vm.$watch(exprOrFn, handler, options);
}

import Watcher from './observer/watcher';
Vue.prototype.$watch = function(exprOrFn, cb, options){
	const vm = this;
	//user: true表示是一个用户watcher
	let watcher = new Watcher(vm, exprOrFn, cb, {...options, user:true});
	//立即执行回调
	if(options.immediate){
		cb();
	}
}

// src/observer/watcher.js
import { isObject } from '../util/index';
export default class Watcher{
	constructor(vm, exprOrFn, cb, options){
		this.vm = vm;
		this.exprOrFn = exprOrFn;
		this.cb = cb;
		this.options = options;
		this.id = id++;
		this.deps = [];
		this.depsId = new Set();
		
		this.user = options.user;
		
		if(typeof exprOrFn === 'function'){
			this.getter = exprOrFn;
		}else{
			this.getter = function(){
				//可能穿进来的是个字符串a.a.a.b
				let path = exprOrFn.split('.');
				let obj = vm;
				for(let i = 0; i < path.length; i++){
					obj = obj[path[i]];
				}
				return obj;
			};
		}
		
		this.value = this.get();
	}
	get(){
		pushTarget(this);
		const res = this.getter.call(this.vm);
		popTarget();
		return res;
	}
	addDep(){
		let id = dep.id;
		if(!this.depsId.has(id)){
			this.depsId.add(id);
			this.deps.push(dep);
			dep.addSub(this);
		}
	}
	run(){
		const newVal = this.get();
		const oldVal = this.value;
		this.value = newVal;
		//走用户watcher
		if(this.user){
			if(newVal !== oldVal || isObject(newVal)){
				this.cb.call(this.vm, oldVal);
			}
		}else{
		//走渲染watcher
			this.cb.call(this.vm);
		}
	}
}

computed属性

初始化:为computed属性构造lazy watcher。
首次模版渲染:渲染watcher检测到computed属性,调用lazy watcher的getter;computed属性依赖于其他属性,调用其他属性的getter,同时保存引用关系并缓存结果,将缓存结果返回给渲染watcher进行模板渲染。
多次模板渲染:直接取lazy watcher中的缓存值给渲染watcher。
依赖属性更新:按照链式从被依赖的属性到lazy watcher到渲染watcher,向上通知。

// src/state.js
function initComputed(vm){
	const computed = vm.$options.computed;
	const watchers = (vm._computedWatchers = {});
	for(let k in computed){
		const userDef = computed[k];
		const getter = typeof userDef === 'function' ? userDef : userDef.getter;
		watchers[k] = new Watcher(vm, getter, ()=>{}, {lazy: true});
		defineComputed(vm, k, userDef);
	}
}
const sharedPropertyDefinition = {
	enumerable: true,
	configurable: true,
	get: ()=>{},
	set: ()=>{}
};
function defineComputed(target, key, userDef){
	if(typeof userDef === 'function'){
		sharedPropertyDefinition.get = createComputedGetter(key);
	}else{
		sharedPropertyDefinition.get = createComputedGetter(key);
		sharedPropertyDefinition.set = userDef.set;
	}
	//对计算属性的get和set进行劫持
	Object.defineProperty(target, key, sharedPropertyDefinition);
}
//重写get方法
function createComputedGetter(key){
	return function(){
		const watcher = this._computedWatchers[key];
		if(watcher){
			//如果dirty了就重新算
			if(watcher.dirty){
				watcher.evaluate();
			}
			if(Dep.target){
				watcher.depend();
			}
			return watcher.value;
		}
	}
}

// src/observer/watcher.js
export default class Watcher{
	constructor(vm, exprOrFn, cb, options){
		...
		this.lazy = options.lazy;
		this.dirty = this.lazy;//默认值true
		
		...
		this.value = this.lazy ? undefined : this.get();
	}
	get(){
		pushTarget(this);
		const res = this.getter.call(this.vm);
		popTarget();
		return res;
	}
	...
	update(){
		if(this.lazy){
			this.dirty = true;
		}else{
			queueWatcher(this);
		}
	}
	evaluate(){
		this.value = this.get();
		this.dirty = false;
	}
	depend(){
		let i = this.deps.length;
		while(i--){
			this.deps[i].depend();
		}
	}
}

keep-alive

Vuex

Vue-Router

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值