目录
- 一、MVVM模型
- 二、内在
- 1. 深入响应式原理
- 2. Object.entries
- 3. 底层搭建
一、MVVM模型
MVVM,即Model 、View、ViewModel。
Model => data数据
view => 视图(vue模板)
ViewModel => vm => vue 返回的实例 => 控制中心, 负责监听Model的数据,进行改变并且控制视图的更新
vue渲染流程
1. vue 拿到挂载中的所有节点
2. vue 取data中的数据,插入到带有vue指令,特殊的vue符号中
3. vue 将数据插入成功的元素放回到页面中
二、内在
1. 深入响应式原理
如何追踪变化: 当把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty把这些 property 全部转为 getter/setter。
例:监听对象的变化
<script>
// 方法:Object.defineProperty(监听对象,对象的属性名,{配置对象})
let data = {
age:38,
}
// 定义一个变量
let _data = null;
Object.defineProperty(data,"age",{
get(){
console.log("get--data.age取值的时候触发");
return _data
},
set(val){
console.log("set--设置data.age的时候触发");
_data = val
}
})
</script>
使用Object.defineProperty方法,函数内部把属性转化为get/set,get()函数在函数取值时触发,set()函数在设值时触发,通过在外部定义变量,在get()函数内部返回该变量,在set()内部将设的值赋值给该外部变量,从而实现监听。
对象之间的关联:原对象与代理对象
<script>
// 关联:代理
let data = {
age: 38,
}
// 定义一个变量
let _data = {};
Object.defineProperty(_data, "age", {
get() {
return data.age
},
set(val) {
data.age = val
}
})
</script>
如上述代码,监听的对象是外部定义的对象,监听的属性名是另一个对象中的属性。在get()执行时,将data.age的值返回给监听对象_data,那么 _data中就会生成一个属性值age: 38,同理在set()执行时,也会将设置的值返回给监听对象 _data,从而修改 _data中的属性值。两个对象data与 _data之间是代理的关系。
vue2底层:数据代理 => 通过一个对象代理另一个对象的中的属性操作(读/写)
2. Object.entries
Object.entries将一级对象处理成键值对的形式。
let data = {
name:"Evan You",
age:"36",
sex:"man"
}
然后通过循环遍历,使用Object.defineProperty方法,把属性转化为get/set,最后代理给_data。
// 原对象
let data = {
name:"Evan You",
age:36,
sex:"man"
}
// 代理对象
let _data = {}
// 处理键值对
Object.entries(data).forEach(([key, val]) => {
// 获取data对象的键值对,交给_data代理
createObj(_data, key, val)
})
// 代理
function createObj(_data, key, val){
console.log(_data, key, val);
Object.defineProperty(_data, key, {
// 对data的每一个属性key,get获取值,set将值赋给属性,最后将属性及其对应值赋给监听对象_data
get(){
return val;
},
set(value){
val = value;
}
})
}
修改_data的值,但data的值不会受影响。
3. 底层搭建
创建一个class类Test,返回一个constructor对象,实例化Test。在constructor输出arguments可获得节点和数据。
class Test{
constructor(){
console.log(arguments);
}
}
let vm = new Test({
el:"#root",
data(){
return {
num:"32",
name:"Jordan",
country:"Ame",
work:"basketball"
}
}
})
操作:
<div id="root">
{{name}
</br><input type="text" v-model="name"> </br>
{{work}}
</div>
vue底层:
- vue 拿到挂载中的所有节点;
- vue 拿data中的数据, 插入到带有vue指令,特殊的vue符合中。Object.defineProperty监听data数据;
- vue 将数据插入成功的元素放回到页面中。
class Test {
constructor({el, data}) { // 解构赋值
// 获取节点
this.el = document.querySelector(el);
this._data = data;
// 调用方法
getDom(this.el, this._data)
}
}
// 获取节点
function getDom(node, _data){
console.log(node, _data);
}
可以通过firstChild和nextSibling获取每一个节点:
getDom函数:
function getDom(node, _data){
// console.log(node, _data);
// 文本代码拼凑,创建一个新的空白的文档片段
let frame = document.createDocumentFragment();
let child;
console.log(node.firstChild);
// 空循环,将赋值给child
while(child = node.firstChild){
// 插入到frame
frame.append(child);
console.log(child);// child获得节点
}
return frame;
}
执行结果:页面上的节点被剪切。
循环过程:每执行一次append操作,root的第一个节点就会被剪切掉,当所有节点被剪切掉时为null循环结束。
接下来,我们在进入循环之后先调用另一个函数getDom2,判断节点的类型并把原对象的数据取出来赋值给这些节点。
function getDom(node, _data){
let frame = document.createDocumentFragment();
while(child = node.firstChild){
getDom2(child, _data)
frame.append(child);
}
// 返回操作后的节点
return frame;
}
function getDom2(node, _data){
console.log(node, node.nodeType);
//正则 匹配插值符号{{}}
let reg = /\{\{(.*)\}\}/
// 节点
// if(noede.nodeType == 1){ // 元素节点,nodeType 属性返回 1
// }
if(node.nodeType == 3){ //文本节点,nodeType 属性返回 3
// 如果该文本节点匹配到{{}},返回true
if(reg.test(node.nodeValue)){ // 取出节点值,匹配{{}}
console.log(reg.test(node.nodeValue)); // 文本节点,返回true
console.dir(RegExp);
// $1为RegExp的属性,获取{{}}里面的值-> name,work
let arg = RegExp.$1.trim();
// 获取{{}}里面的值-> name,work
console.log(arg);
// 将原对象的name,work及其值赋给页面中的{{name}},{{work}}
node.nodeValue = _data[arg];
// 节点分别为:Jordan、basketball
console.log(node);
// 将数据(节点 data)存储下来
new watcher(_data, node, arg)
}
}
}
class Test {
constructor({el, data}) {
this.el = document.querySelector(el);
this._data=data;
// 获取节点
this.dom = getDom(this.el, this._data)
// 将返回的节点插入页面
this.el.append(this.dom)
}
}
输出说明:
**说明:**为了方便,html的div中将用于换行的两个删去。
对于元素节点,进行下述处理。
if(node.nodeType == 1){ // 元素节点,nodeType 属性返回 1
console.log(node, node.nodeType);
// 获取元素节点上的属性节点
console.log(node.attributes);
[...node.attributes].forEach((item) => { // 遍历属性节点
if(item.nodeName == "v-model"){
console.log(item.nodeName);
// 获取v-model的属性值-> name
let arg = item.nodeValue;
// 双向数据绑定,通过页面修改原对象
node.addEventListener("input",(ev)=>{
_data[arg] = ev.target.value
})
console.log(arg);
// 将原对象data中的name赋值(代理)到v-model的name
node.value = _data[arg];
console.log(node.value);
console.log(node);
// 将数据(节点 data)存储下来
new watcher(_data, node, arg)
}
})
}
从vue底层原理可知,在获取节点以及渲染之前,应该先进行数据监听。
数据监听第一步是启动订阅(Dep类),然后调用Object.defineProperty所有属性转为get/set,在get中调用Dep类的addSub方法存储数据,在set中调用Dep类的notify方法修改数据。此时还未获取节点,属于在后端修改数据。
在获取节点的分类节点并插入页面后,调用watcher类,该类先保存数据,并传送数据给Dep类,也进行数据的获取和修改。此时以获取节点,属于在页面修改数据。
以上两部分即是双向数据绑定。
// 订阅发布:在数据变动时,发给订阅者,触发对应的函数
class Dep {
constructor() {
// 保留数据
this.sub = []
}
addSub(val) {
this.sub.push(val)
}
notify() {
this.sub.map((item) => {
item.update()
})
}
}
//观察者 => 保存数据,以便后期进行修改
class watcher {
constructor(_data, node, arg) {
// 数据也给Dep一份,以便Dep订阅数据是否变化
Dep.target = this
// 保存数据
this._data = _data;
this.node = node;
this.arg = arg;
this.init()
}
init() {
// 用于后期进行数据修改
this.update()
// 修改数据后,清空Dep留存的数据
Dep.target = null
}
update() {
// 用于获取数据
this.get()
// 修改数据
this.node.value = this.node.nodeValue = this.value
}
get() {
// 获取数据,数据代理
this.value = this._data[this.arg]
}
}
//处理数据监听
function setdefineProperty(data, _data) { // data===_data
Object.entries(data).forEach(([key, val]) => {
createObj(_data, key, val)
});
}
//监听数据
function createObj(_data, key, val) {
//启动 订阅发布
let dep = new Dep()
// 所有属性转为get/set
Object.defineProperty(_data, key, {
get() {
// 如果Dep.target 有数据
if (Dep.target) { //没有数据不就要放进去
dep.addSub(Dep.target)
}
return val
},
set(value) {
// 数据相同不作改变
if (val == value) {
return
}
// 更改数据
val = value;
// 设置的时候修改页面数据
dep.notify()
}
})
}
双向数据绑定需在获取输入框节点的代码中加入:
// 双向数据绑定,通过页面修改原对象
node.addEventListener("input", (ev) => {
_data[arg] = ev.target.value
})
最后,最好将多余的属性和样式删除。如删除v-model。
// 删除属性
node.removeAttribute("v-model")