总结这块:断断续续花了1个多月的时间才成型最终稿
先申明:图都是盗的,为了节省时间没自己画
MVVM和MVM
- MVC —— Model-View-Controller的缩写
工作原理:用户发送事件的时候,发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上
View——毫无疑问是操作视图,比如点击、输入,用户发送事件的时候,发送指令到controller层
Model——包含api(网络请求相关、对数据库、缓存操作)和变量
Controller——对于数据逻辑的操作,包含对View的操作,又包含调用Model的变量或者函数
结合自己的经验:当时写ios逻辑的时候,就是用MVC的思想, 比如(举例代码语法不一定对,只是象征性的举例): 在controller中
label.text = 'hello';
btn.click = function(){}
这就是操作view,还可以调用model中的函数来更新view中的值
想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,这样都没法写在view层,而是写在controller层,如果逻辑很复杂,这样controller层的代码非常多甚至混乱。由此这是它很明显的缺点,总结如下:
- 在controller和view逻辑不能分离
前端上有这方面的实例吗?
2 . MVVM
结合MVC的缺点,所以变种另一种思想,得出MVVM,它最早由微软提出的。
工作原理:view层和model层完全解耦,view层发出的事件传递到VM层中,VM层去操作model层,并且将数据返回给view层
View —— 视图中
viewModel —— 调用model中的函数,可以调用view中的函数,把数据传给view
Model —— 包含api(网络请求相关、对数据库、缓存操作)
- 双向绑定:数据更新视图,视图更新数据
MVVM怎么实现数据双向绑定?
通过Object.defineProperty( )对属性设置一个set函数,把更新方法放到此处。
双向绑定
View更新变化Data,可以通过事件监听的方式来实现,比如input,checkbox,select;
Data更新变化View,重点是如何知道数据变了,通过几个步骤:
1)实现一监听器Observer:劫持和监听data属性值,当属性发生变化时(set中通过新值和旧值比较判断是否变化),调用dep.notify();
我的剖析总结:
- 遍历data对象,每个属性会实例化订阅器var dep = new Dep(),如果是对象遍历对象
- 初始化订阅器dep,每个属性实例化一个订阅器dep
- get————收集订阅者Watcher,dep.addSub(Dep.target)
想到Watcher实例怎么赋值到Dep.target上? 订阅器为什么在get中收集订阅者?
由此联想到data为什么要返回对象?
原因:对象为引用类型,当重用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题
// 1.对象方式(所有重用的实例中的data均为同一个对象)
var data = {
x: 1
}
var vm1 = {
data: data
}
var vm2 = {
data: data
}
vm1.data === vm2.data // true,指向同一个对象
// 2.函数方式(所有重用的实例中的data均为同一个函数)
var func = function () {
return {
x: 1
}
}
var vm3 = {
data: func
}
var vm4 = {
data: func
}
vm3.data() === vm4.data() // false,指向不同对象
console.log({x:1} === {x:1} ) // false
console.log(new Object({x:1}) === new Object({x:1})) //false
函数方式中data都指向同一个函数,但这个函数每次的返回值都是一个新的对象,所以都是false
console.log(String({x:1})===String({x:1})) //true
console.log(({x:1}).toString()===({x:1}).toString()) //true
var data = {
x: 1
}
console.log(data === data ) //true
=== 涉及到类型转换,参考数据类型转换营
/**
* 循环遍历数据对象的每个属性
*/
function observable(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj;
}
/**
* 将对象的属性用 Object.defineProperty() 进行设置
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了...`);
return val;
},
set(newVal) {
console.log(`${key}属性被修改了...`);
val = newVal;
}
})
}
defineReactive: function(data, key, val) {
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
2)实现一订阅者watcher:收到属性变化的通知,并且执行相应更新的方法,从而更新视图(怎么跟视图关联起来?)
我的剖析总结:
- 订阅者Watcher结构函数中参数包含:vue实例vm, data中的某一属性exp, 更新函数cb;
- 一个模板指令相对于一个订阅者Watcher;
- 触发监听器Observer里的get函数,将自己添加到订阅器Dep中;
- 包含update更新函数;
- 更新View界面;
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 全局变量 订阅者 赋值
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 全局变量 订阅者 释放
return value;
}
};
3)实现一订阅器dep:收集订阅者watcher,统一管理监听器observer和订阅者watcher(怎么统一管理?也就是提供函数给监听器observer调用,并调用订阅者watcher中的函数)
我的剖析总结:
- 充当通知和收集的角色
- 通知函数notify和收集订阅者addSub都在订阅器Dep中定义,只不过在监听器observer中调用;
- 通知函数notify要遍历订阅者watcher,并调用订阅者watcher各自的更新函数sub.update()
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
4)实现一解析器compile:解析每个节点的相关指令,对模板数据和订阅器进行初始化(怎么初始化?)
我的剖析总结:
- 做解析dom节点和绑定工作;
- 解析模板指令,并替换模板数据,初始化视图;
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅者Watcher;
为了解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,因此这个环节需要对dom操作比较频繁,所有可以先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理:
function nodeToFragment (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
}
接下来需要遍历各个节点,对含有相关指定的节点进行特殊处理,这里咱们先处理最简单的情况:
对 '{{变量}}' 这种形式的指令处理的关键代码进行分析
el为节点,比如跟节点#app
new Vue({
el: '#app'
}
function compileElement (el) {
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent;
if (self.isTextNode(node) && reg.test(text)) {
// 判断是否符合这种形式{{}}的指令
self.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
self.compileElement(node); // 继续递归遍历子节点
}
});
},
function compileText (node, exp) {
var self = this;
var initText = this.vm[exp];
updateText(node, initText); // 将初始化的数据初始化到视图中
// 将这个指令exp初始化为一个订阅者Watcher并绑定更新函数,
//后续 exp 改变时,就会触发这个更新回调,从而更新视图
new Watcher(this.vm, exp, function (value) {
// value在Watcher中传入
self.updateText(node, value);
});
},
function updateText (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
参考:
www.cnblogs.com/canfoo/p/68… (vue的双向绑定原理及实现)
面试回答:通过MVVM的思想实现双向绑定。
Proxy
用于修改某些操作的默认行为,在对象之前架设一层"拦截",可以对外界的访问进行过滤和改写
let proxy = new Proxy({},{
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
})
- proxy支持13种拦截操作;
- Proxy可以直接监听对象而非属性,直接
可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty; - Proxy可以直接监听数组的变化;
definePrototype
Object.defineProperty(obj, prop, descriptor)
缺陷:
- 无法监听数组变化 vue中通过以下8种方法监听,但不能通过vm.items[indexOfItem] = newValue监听 push()、 pop()、 shift()、 unshift()、 splice()、 sort()、 reverse()
- 只能劫持对象的属性,需要深度遍历对象
双向绑定
主要实现以下四个:
-
监听器Observer 对data数据对象变得“可监测”,vue2.0用Object.defineProperty()来劫持各个数据属性的 setter / getter;
-
监听器 Observer ,用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者
-
订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理
-
订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图
-
解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化
vue数据双向绑定的原理:vue内部把data对象里每个数据的读写转化为getter、setter,当数据变化时通知视图更新。 主要通过监听器observer、订阅器dep、订阅者watcher、解析器compile,这4个步骤实现
自己口头描述:双向绑定主要需要实现observer、dep、watcher、compile,他们分别的功能是。。。 首先要定义observer,对data中属性的setter / getter进行拦截,compile解析模板中指令,替换模板数据,初始化视图,初始化订阅者;watcher相当于每个指令,触发getter,添加到dep中;dep收集watcher,当收到属性变化通知时,通知更新函数。(我觉得赋值是在代码中赋值,就能监听到set)
口述:双向绑定原理 get :获取订阅者watcher (买家,一个订阅者相当于一个指令) set: 通知订阅器(中介),调用notify,通知订阅者watcher更新函数
源码加强分析
注意:每个data对象的属性的 getter 都持有一个 dep;源码中收集watcher会有判断保证同一数据不会被添加多次
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
这时候会做一些逻辑判断(保证同一数据不会被添加多次)后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。
生成顺序
data的初始化是在created时已经完成数据观测; props => methods =>data => computed => watch
自己构造的方法与vue生命周期的运行顺序
往往我们在开发项目时都经常用到 $refs 来直接访问子组件的方法,但是这样调用的时候可能会导致数据的延迟滞后的问题,则会出现bug。
解决方法则是推荐采取异步回调的方法,然后传参进去,严格遵守vue的生命周期就可以解决 推荐 es6 的promise。
handleAsync () {
return new Promise(resolve=>{
const res="";
resolve(res)
})
}
async handleShow() {
await this.handleAsync().then(res=>{
this.$refs.child.show(res);
})
}
vue2.0怎么对数组和对象拦截
数组:observe 方法 对象:遍历对象的key调用 defineReactive 方法,
defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter,递归调用observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。