即了解了底层实现原理和过程,又锻炼编程能力
第一部 实现html模板 模拟vue 传数据new出来实例
<!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 class="box">
年龄{{gpc.age}}----
姓名 {{gpc.name}}
<ul>
<li>郭大爷</li>
<li>{{gpc.name}}</li>
</ul>
<input type="text" v-model="message">
</div>
</div>
</body>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
let vm = new mvvm({
el: "#app",
data: {
message: "hello gpc",
gpc:{
name: "郭大爷",
age:222
}
}
})
</script>
</html>
第二部 创建构造函数 mvvm.js
class mvvm {
constructor(options) {
/**一上来,先把可用的东西挂载到实例上, */
this.$el = options.el;
this.$data = options.data;
if (this.$el) {
//数据劫持 把所有数据都set get化
new Observer(this);
//数据代理 this.$data.msg获取数据 变成 this.msg
this.proxyData(this.$data);
//用数据和元素编译
new Compile(this.$el, this);
}
}
//代理的函数
proxyData(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newValue){
data[key] = newValue;
}
})
})
}
}
第三部 实现数据劫持 定义Observer.js
//要将数据--劫持 先获取到data 和 key 和 value
Object.keys(data).forEach(key=>{
this.defineReactive(data,key,data[key]);
//递归继续劫持
this.observer(data[key]);
})
}
//定义响应式
defineReactive(obj,key,value){
let that = this;
//每个比变化的数据,都会对应一个数组,这个数组存放的是所有更新的操作
let dep = new Dep();
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){ //取值的时候
Dep.target && dep.addSub(Dep.target)
return value;
},
set(newValue){ //当给data属性中设置的时候
if(newValue!=value){
that.observer(newValue); //如果是对象继续劫持
value=newValue;
dep.notify(); //当改变值的时候,通知所有人,数据变化了
}
}
})
}
}
class Dep{
constructor(){
//发布订阅者模式
this.subs = [];
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
第四步 实现数据的编译 compile.js
class Compile {
constructor(el, vm) {
//编译的方法也写成一个构造函数
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) { /**如果元素存在就开始编译dom*/
//一、先把dom放入内存中,内存中操作dom性能比较友好
//二、通过fragment 把dom放入内存中
let fragment = this.node2fragment(this.el);
//编译放入内存中的元素节点和文字节点
this.compile(fragment);
//三、 编译完了,再放入页面中
this.el.appendChild(fragment)
}
}
/**专门写一些辅助的方法 */
isElementNode(node) {
return node.nodeType === 1;
}
/**核心方法 */
//编译元素
compileElement(node) {
let attr = node.attributes;
Array.from(attr).forEach(attr => {
if (attr.name.includes('v-')) {
//带v-model v-html的
//取到对应的值放入节点中
let expr = attr.value
// console.log('编译元素 v-model 类型的')
let [, type] = attr.name.split('-');
CompileUtil[type](node,this.vm, expr);
}
})
}
//编译文本
compileText(node) { //node是传进来的
//正则找到带插值表达式的
let expr = node.textContent;
let reg = /\{\{([^{|}]+)\}\}/g;
if (reg.test(expr)) {
//带花括号的才编译文本
//node this.vm.$data text
CompileUtil['text'](node,this.vm,expr);
}
}
//总的编译函数
compile(fragment) {
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
//1是元素 2 是属性 3 是文本
if (this.isElementNode(node)) {
//编译元素,只是编译带v-的
this.compileElement(node);
this.compile(node)
} else {
//编译文本只编译{{}}
this.compileText(node);
}
})
}
//抽取虚拟dom
node2fragment(el) { //需要将el中所有内容放入到内存中
//文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
//这个循环条件太优秀了
fragment.appendChild(firstChild);
}
return fragment; //返回内存中的节点
}
}
//编译替换 单对象
CompileUtil = {
//获取值
getVal(vm, expr) {
let arr = expr.split('.');
// 下一个、的结果, 溢奶 上一个的结果
return arr.reduce((prev,item)=>{
return prev[item]
},vm.$data)
},
//提取文字
getTextVal(vm,str){
return str.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
return this.getVal(vm,arguments[1])
})
},
text(node, vm, expr) { //文本处理
let updateFn = this.updater['textUpdater'];
//提取文字分割成数组()
//如果是多个 分多次扔进去处理
let value=this.getTextVal(vm,expr);
// 这个函数只执行一次,但是正则里的new watcher
// 需要监听2次 a一次 和 b一次
expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
new Watcher(vm,arguments[1],(newValue)=>{
//当a发生变化时,需要获取2次值,a的值和b的值 来更新节点中的值
//这里为什么不可以用回调函数里面的参数newValue
//应为newValue时变化的那个值,不能把a的新值给节点,从而丢失b的老值
updateFn && updateFn(node,this.getTextVal(vm,expr));
})
})
//如果一个文本节点里有多个插值表达式,需要监听-值{{a}} {{b}}
//初始编译模板的时候就加一个监听,getset ,如果数据变化了,调用watch的callback方法的回调函数
//按数组 this.data.gpc.name格式 提取data里面的内容
updateFn && updateFn(node,value);
},
setValue(vm,expr,newValue){
expr = expr.split('.');
expr.reduce((prev,item,curindex)=>{
if(curindex===expr.length-1){
return prev[item] = newValue;
}
},vm.$data)
},
model(node, vm, expr) {//输入框处理
//调用Model处理函数
let updateFn = this.updater['modeUpdater'];
//编译v-model的时候加上监听
new Watcher(vm,expr,(newValue)=>{
//这个回调函数在什么时机调用呢,当数据发送改变时
//数据改变时触发get set
updateFn && updateFn(node,this.getVal(vm,expr));
})
//添加一个事件
node.addEventListener('input',e=>{
let newValue = e.target.value;
this.setValue(vm,expr,newValue)
})
updateFn && updateFn(node,this.getVal(vm,expr));
},
updater: {
textUpdater(node, value) { //文本更新
node.textContent = value;
},
modeUpdater(node, value) { // 输入框更新
node.value = value
}
}
}
第五部 加入观察这 watcher.js
//观察者的目的 就是给需要变化的那个元素增加一个观察者,
当数据变化后监听 对应的方法
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
//刚初始化就获取值,此时的是老的值
this.value = this.get()
}
getVal(vm,expr){
expr = expr.split('.');
return expr.reduce((prev,next)=>{
return prev[next]
},vm.$data)
}
get(){
// Dep.target = this; //this就是new watcher的实例
Dep.target = this;
// replace(/^\s*|\s*$/g , ""); 匹配空格字符串
let value = this.getVal(this.vm,this.expr);
Dep.target = null;
return value;
}
//对外暴露的方法
update(){
//值变化 了再取 就是新值啦
let newValue = this.getVal(this.vm,this.expr);
let oldValue = this.value;
if(newValue!==oldValue){
this.cb(newValue);// 对应watch的callback方法
}
}
}