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();
}
}
}