MVVM模式
ViewModel是Vue.js的核心,它是一个Vue实例。Vue实例是作用于某一个HTML元素上的,这个元素可以是HTML的body元素,也可以是指定了id的某个元素。
当创建了ViewModel后,双向绑定是如何达成的呢?
- 首先,我们将上图中的DOM Listeners和Data Bindings看作两个工具,它们是实现双向绑定的关键。
- 从View侧看,ViewModel中的DOM Listeners工具会帮我们监测页面上DOM元素的变化,如果有变化,则更改Model中的数据;
- 从Model侧看,当我们更新Model中的数据时,Data Bindings工具会帮我们更新页面中的DOM元素。
MVVM框架三要素:数据响应式、模板引擎及其渲染
数据响应式:监听数据变化并在视图中更新
- Objext.defineProperty()
- Proxy
模板引擎:提供描述视图的模板语法
- 插值:{ {}}
- 指令:v-bind、v-on、v-model、v-for、v-if
渲染:如何将模板装换为html
- 模板 => vdom => dom
一、 数据响应式原理
数据变更能够响应在视图,就是数据响应式。
vue2利用Object.defineProperty()
来实现变更检测。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
备注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。
1.2.1 简单实现数据变更
// 响应式
const obj = {
};
function defineReactiove(obj,key,val){
//对传入的obj进行访问的拦截
Object.defineProperty(obj,key,{
get(){
console.log('get ' + key);
return val
},
set(newValue){
if(newValue !== val){
console.log('set ' + key + ":" + newValue);
val = newValue;
}
}
});
}
defineReactiove(obj,'foo','foo');
obj.foo
obj.foo = 'foooooooooooooo'
1.2.2 结合视图
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
const obj = {
};
function defineReactiove(obj,key,val){
//对传入的obj进行访问的拦截
Object.defineProperty(obj,key,{
get(){
console.log('get ' + key);
return val
},
set(newValue){
if(newValue !== val){
console.log('set ' + key + ":" + newValue);
val = newValue;
//更新函数,更新界面
update();
}
}
});
}
function update(){
app.innerText = obj.foo
}
defineReactiove(obj,'foo','');
obj.foo = new Date().toLocaleTimeString();
setInterval(()=>{
obj.foo = new Date().toLocaleTimeString();
},1000);
</script>
</body>
</html>
1.2.3 遍历需要响应化的对象
// 响应式
// const obj = {};
function defineReactive(obj, key, val) {
//对传入的obj进行访问的拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
return val
},
set(newValue) {
if (newValue !== val) {
console.log('set ' + key + ":" + newValue);
val = newValue;
}
}
});
}
对象响应化:遍历每个key,定义getter、setter
function observe(obj) {
//不是一个obj 或者 obj 是空,不是对象,直接退出
if (typeof obj !== 'object' || obj == null) {
//希望传入的是obj
return;
}
/**
* Object.keys()
* 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
* // array like object
* var obj = { 0: 'a', 1: 'b', 2: 'c' };
* console.log(Object.keys(obj)); // console: ['0', '1', '2']
*
*
*/
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// defineReactive(obj,'foo','foo');
// obj.foo
// obj.foo = 'foooooooooooooo'
const obj = {
foo: 'foo', bar: 'bar', baz: {
a: 1 } };
//遍历做响应化处理
observe(obj);
obj.foo
obj.foo = "fooooooooooooo"
obj.bar
obj.bar = "barrrrrrrrrrrr"
obj.baz.a = 10 //嵌套对象 no ok
1.2.4 解决嵌套对象问题
function defineReactive(obj, key, val) {
//递归
observe(val);
......
}
1.2.5 如果添加/删除了新属性无法检测
obj.dong = 'dong'
obj.dong //并没有get信息
function set(obj,key,val){
defineReactive(obj,key,val);
}
obj.dong = 'dong' //添加了新属性无法检测
set(obj,'dong','dongggg');
obj.dong //get
defineProperty() 对数组无效
分析:改变数组方法只有7个
解决方案:替换数组实例的原型方法,让她们在修改数组的同时还可以通知更新
1.2.6 完整代码
// 响应式
// const obj = {};
//响应式处理
function defineReactive(obj, key, val) {
//递归:解决嵌套对象问题
observe(val);
//对传入的obj进行访问的拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
return val
},
set(newValue) {
if (newValue !== val) {
console.log('set ' + key + ":" + newValue);
//如果传入的newValue依然是obj,需要做响应化处理
observe(newValue); //新值是对象的情况
val = newValue;
}
}
});
}
//遍历需要响应化的对象
function observe(obj) {
//不是一个obj 或者 obj 是空,不是对象,直接退出
if (typeof obj !== 'object' || obj == null) {
//希望传入的是obj
return;
}
/**
* Object.keys()
* 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
* // array like object
* var obj = { 0: 'a', 1: 'b', 2: 'c' };
* console.log(Object.keys(obj)); // console: ['0', '1', '2']
*
*
*/
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function set(obj,key,val){
defineReactive(obj,key,val);
}
//----------------------------------------------------------------
//响应化处理
// defineReactive(obj,'foo','foo');
// obj.foo
// obj.foo = 'foooooooooooooo'
const obj = {
foo: 'foo', bar: 'bar', baz: {
a: 1 } };
//遍历做响应化处理
observe(obj);
obj.foo
obj.foo = "fooooooooooooo"
obj.bar
obj.bar = "barrrrrrrrrrrr"
// obj.baz.a = 10 //嵌套对象 no ok ==> 递归
obj.baz = {
a:100}
obj.baz.a = 100000 //赋的值是对象 no ok
obj.dong = 'dong' //添加了新属性无法检测
set(obj,'dong','dongggg');
obj.dong //get
二、 Vue中的数据响应化
2.1 目标代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title>
</head>
<body> <div id="app"> <p>{
{counter}}</p> </div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
counter: 1
},
});
setInterval(() => {
app.counter++
}, 1000);
</script>
</body>
</html>
2.2 原理分析
1.new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
2.同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
3.同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4.由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
5.将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
劫持监听所有属性
/----> Observer --------------- 通知变化 -----------------------> Dep --------------------
/ ^ |
/ | |
/ | |
new MVVM() 添加订阅者 通知变化
\ | |
\ | |
\ | |
\----> Compiler ---------- 订阅数据变化,绑定更新函数 -------------> Watcher <-------------|
解析指令 \ /
\ /
\ /
初始化视图 更新视图
\ /
\------------> Updater <------------------/
设计类型介绍
- KVue:框架构造函数
- Observer:执行数据响应化(分辨数据是对象还是数组)
- Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
- Watcher:执行更新函数(更新dom)
- Dep:管理多个Watcher,批量更新
2.3 KVue
框架构造函数:执行初始化
2.3.1 执行初始化,对data执行响应化处理,kvue.js
/**
* 响应式处理
* @param {*} obj
* @param {*} key
* @param {*} val
*/
function defineReactive(obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
get() {
console.log("get",key);
return val
},
set(newValue) {
if (newValue !== val) {
console.log('set ' + key + ":" + newValue);
observe(newValue);
val = newValue;
}
}
});
}
//------------------------------------------------------------------------------------------------
/**
* 2.1遍历需要响应化的对象
* 在响应化的过程中,只是创建了一个Observer实例
* @param {*} obj
*/
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return;
}
//创建 Observer 实例 -- 判断传入的是对象还是数组
new Observer(obj);
}
//--------------------------补充start----------------------------------------------------------------------
补充:
Object.defineProperty
https://segmentfault.com/a/1190000007434923
getter/setter
当设置或获取对象的某个属性的值的时候,可以提供getter/setter方法。
getter 是一种获得属性值的方法
setter是一种设置属性值的方法。
在特性中使用get/set属性来定义对应的方法。
var obj = {
};
var initValue = 'hello';
Object.defineProperty(obj,"newKey",{
get:function (){
//当获取值的时候触发的函数
return initValue;
},
set:function (value){
//当设置值的时候触发的函数,设置的新值通过参数value拿到
initValue = value;
}
});
//获取值
console.log( obj.newKey ); //hello
//设置值
obj.newKey = 'change value';
console.log( obj.newKey ); //change value
******************
Object.keys
https://segmentfault.com/a/1190000009986807
获取对象的所有属性
它返回一个数组,就可以结合forEach方法遍历对象
// 1.对象
var a = {
a : 123,
b : 'asd',
c : function() {
console.log( 'haha' );
}
};
console.log( Object.keys( a ) ); // [ 'a', 'b', 'c' ]
//----------------------------补充end--------------------------------------------------------------------
/**
* 0.用户的 new KVueDe 时候
* 传进来选项,将data拿出来,做响应化处理
*/
//创建 KVue 构造函数
class KVue {
constructor(options){
//1.保存选项
this.$options = options;
this.$data = options.data;
//2.响应化处理
observe(this.$data);
}
}
//------------------------------------------------------------------------------------------------
/**2.2 根据对象的类型决定如何做响应化
*在 Observer 实例,要做的是:
*判断 类型,如果是对象,就做walk操作
*对所有的key执行拦截
*/
class Observer{
constructor(value){
//2.2.1 保存value
this.value = value
//2.2.2 判断其类型
if(typeof value === 'object'){
this.walk(value);//遍历
}
}
//对象数据的响应化
//劫持监听所有属性
walk(obj){
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
//数组数据响应化
}
2.3.2 为$data做代理
/**
* 3.代理函数
* 方便用户直接访问 $data 中的数据
* @param {*} vm 当前的框架实例
* @param {*} key 代理的key
*/
function proxy(vm,sourceKey){
Object.keys(vm[sourceKey]).forEach(key=>{
//将 $data 中的 key 代理到 vm 属性中
Object.defineProperty(vm,key,{
get(){
//当获取值的时候触发的函数
return vm[sourceKey][key]
},
set(newVal){
//当设置值的时候触发的函数,设置的新值通过参数value拿到
vm[sourceKey][key] = newVal;
}
})
});
}
class KVue {
constructor(options){
//1.保存选项
this.$options = options;
this.$data = options.data;
//2.响应化处理
observe(this.$data);
//3.代理
//没有代理的话:app.$data.counter++ 也是可以的
//实现 vue中访问 app.counter++ 一样
proxy(this,'$data');//代理的目标
}
}
以上是:
2.4 编译 - Compile
编译模板中vue模板特殊语法,初始化视图、更新视图
k-text
/-----> 处理textContent
/
/
/ k-html
/---- K开头 ---------> 处理innerhtml
/ \
/ \
/ \ k-model
/----> 编译节点 ----> 遍历属性 \------> 监听input
/ \
/ \
/ \
获取dom ----> 遍历子元素 \---- at开头 ----> 绑定click
\
\
\ {
{
xx}}
\----> 编译文本
初始化视图
2.4.1 根据节点类型编译,compile.js
//--------------补充start---------------------
补充:
document.querySelector("#app")
指定一个或多个匹配元素的 CSS 选择器。 可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。
返回匹配指定选择器的第一个元素
----------------------------------------------------------------------------------------------
Array.from(childNodes)
from() 方法的作用是:从类数组或迭代对象创建一个新的、浅拷贝的数组示例。
----------------------------------------------------------------------------------------------
https://developer.mozilla.org/zh-CN/docs/Web/API/Node
node.childNodes
返回指定节点的所有子节点,包括节点元素和文本元素
https://www.nhooo.com/jsref/elem_childnodes.html
var len = document.querySelector("div").childNodes.length;
var nodes = document.querySelector("div").childNodes;
nodes[1].style.backgroundColor = "coral";
*******************************************************
node.nodeType
返回以数字值返回指定节点的节点类型。
https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
1 Element 代表元素
3 Text 代表元素或属性中的文本内容。
节点类型 - 返回值
对于每种节点类型,nodeName 和 nodeValue 属性的返回值:
Node.childNodes 返回一个包含了该节点所有子节点的实时的NodeList
Node.nodeName
Node.nodeType
Node.nodeValue 返回或设置当前节点的值。
Node.textContent 返回或设置一个元素内所有子节点及其后代的文本内容。
Node.attributes 返回指定节点的属性集合
//------------------补充end---------------------
/*
递归遍历dom树
判断节点类型,如果是文本,则判断是否是 插值绑定
如果是元素,则遍历其属性判断是否是指令或时间,然后递归子元素
*/
class Compiler{
//el 是宿主元素
//vm 是KVue实例
constructor(el,vm){
//保存 KVue 实例
this.$vm = vm;
this.$el = document.querySelector(el);
if(this.$el){
//如果this.$el存在,则执行编译
//执行编译
this.compile(this.$el);
}
}
compile(el){
//遍历el树
const childNodes = el.childNodes; //子节点集合
Array.from(childNodes).forEach(node =>{
//判断是否是元素
if(this.isElement(node)){
console.log('编译元素' + node.nodeName);
}else if(this.isInter(node)){
console.log('编译插值文本 {
{}}' + node.textContent )
}
//递归子节点
if(node.childNodes && node.childNodes.length > 0 ){
this.compile(node);
}
});
}
isElement(node){
return node.nodeType === 1;
}
isInter(node){
//首先是文本标签,其次内容是{
{xxx}} 正则 // \{转义字符 由双大括号括起来 .若干字符 *若干个 匹配的内容要拿到 ()分组
// /\{\{(.*))\}\}/.test('{
{xx}}')
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
}
2.4.2 编译插值 { {xxx}},compile.js
compile(el){
......
}else if(this.isInter(node)){
// console.log('编译插值文本 {
{}}' + node.textContent )
this.compileText(node);
}
......
}
compileText(node){
// RegExp.$1 正则的构造函数,只要有分组,将来匹配的值 就会放在 RegExp.$1
// 拿到上面正则中()的内容,也就是 {
{}} 双大括号中间的内容
console.log(RegExp.$1)
node.textContent = this.$vm[RegExp.$1]
}
2.4.3 编译元素,compile.js
compile(el){
......
//判断是否是元素
if(this.isElement(node)){
console.log('编译元素' + node.nodeName);
this.compileElement(node);
}
......
}
compileElement(node){
//节点是元素
//遍历其属性列表
const nodeAttrs = node.attributes; // 返回指定节点的属性集合
Array.from(nodeAttrs).forEach(attr =>{
//规定:指令以 k-xx = "oo" 定义
const attrName = attr.name // k-xx k-text
const exp = attr.value // oo counter
console.log("attr",attr)
if(this.isDirective(attrName)){
const dir = attrName.substring(2) // xx text
//执行指令 this :Compiler 实例 里面有个方式是 text
this[dir] && this[dir](node,exp)
}
});
}
isDirective(attr){
return attr.indexOf('k-') === 0
}
// k-text
/**
* 更新元素的 textContent
* <span v-text="msg"></span>
* <!-- 和下面的一样 -->
* <span>{
{msg}}</span>
*/
//exp:counter
text(node,exp){
node.textContent = this.$vm[exp]
}
2.4.4 k-html,compile.js
//k-html
html(node,exp){
node.innerHTML = this.$vm[exp]
}
2.5 依赖收集
视图中会用到data中某key,这成为依赖。同一个key可能出现多次,每次都需要收集出来用一个Watcher来维护它们,此过程称为依赖收集。
多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知。
看下面按钮,理出一个思路
new Vue({
template:
`<div>
<p>{
{name1}}</p>
<p>{
{name2}}</p>
<p>{
{name1}}</p>
</div>`,
data:{
name1:'name1',
name2:'name2'
}
});
2.5.1 实现思路
- 1.defineReactive时为每一个key创建一个Dep实例
- 2.初始化视图时读取某个key,例如name1,创建一个watcher1
- 3.由于触发name1的getter方法,变将watcher1添加到name1对应的Dep中
- 4.当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
2.5.2 创建watcher,kvue.js
const watchers = [];//临时用于保存watcher测试
//观察者:保存更新函数,值发生变化调用更新函数
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// 临时放入watchers数组
watchers.push(this)
}
update() {
return this.updateFn.call(this.vm, this.vm[this.key]);//两个参数:指定上下文,newvalue
}
}
2.5.3 编写更新函数、创建watcher,compile.js
//调用update函数执行插值文本复制
compileText(node){
// console.log(RegExp.$1)
// node.textContent = this.$vm[RegExp.$1]
//将上面的代码抽取出去
this.update(node,RegExp.$1,'text'); //参数:node,当前的表达式,指令的名称
}
text(node,exp){
// node.textContent = this.$vm[exp]
//抽取updater函数
this.update(node,exp,'text'); //参数:node,当前的表达式,指令的名称
}
//k-html
html(node,exp){
// node.innerHTML = this.$vm[exp]
//抽取updater函数
this.update(node,exp,'html'); //参数:node,当前的表达式,指令的名称
}
/**
* 更新函数作用
* 1.初始化
* 2.创建watcher实例
* @param {*} node 当前节点
* @param {*} exp 当前表达式
* @param {*} dir 指令名字
*/
update(node,exp,dir){
//1.初始化操作
//指令对应的更新函数 xxUpdater
const fn = this[dir + 'Updater'];
fn && fn(node,this.$vm[exp])
//2.更新处理,封装一个更新函数,可以更新对应dom元素
//val哪里来的: this.updateFn.call(this.vm,this.vm[this.key]);//两个参数:指定上下文,newvalue
new Watcher(this.$vm,exp,function(val){
fn && fn(node,val); //更新函数的作用是:fn执行一遍
});//watcher实例最主要的作用是把更新函数传递进去
}
/**
*
* @param {*} node 节点
* @param {*} value 最新的值
*/
textUpdater(node,value){
node.textContent = value
}
htmlUpdater(node,value){
node.innerHTML = value
}
2.5.4 声明Dep,kvue.js
//Dep : 依赖,管理某个 key 相关所有 Watcher 实例
class Dep {
constructor() {
this.deps = [];
}
//增加
addDep(dep) {
this.deps.push(dep)
}
//通知
notify() {
this.deps.forEach(dep => dep.update());
}
}
2.5.5 创建watcher时触发getter,kvue.js
class Watcher {
constructor(vm, key, updateFn) {
......
// Dep.target 静态属性上设置为当前 Watcher实例
/**
* Dep.target 将watcher实例放在一个全局可以访问的地方
* window.target 也可以
* 不用,是因为 window不通用
* 如果运行环境没有window就会挂了
* Dep.target 放在一个类,作为静态属性来用
*/
Dep.target = this;
this.vm[this.key] //读取触发了getter
Dep.target = null //收集完就置空
}
}
2.5.6 依赖收集,创建Dep实例,kvue.js
function defineReactive(obj, key, val) {
observe(val);
//创建一个Dep和当前key一一对应
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
//依赖收集在这里 Dep.target 指的是watcher实例
Dep.target && dep.addDep(Dep.target)
......
},
set(newValue) {
if (newValue !== val) {
......
//通知更新
dep.notify(); //dep 和 key 是一对一的关系
}
}
});
}
作业
- 实现数组响应式
- 完成后续k-model、@xx