编写一个lvue.js,实现mvvm ,感兴趣的同学可以把代码拷贝到本地看一下实现思路
1 html部分(测试用)
<!-- * @Author: your name * @Date: 2020-07-05 22:21:34 * @LastEditTime: 2020-07-07 00:26:35 * @LastEditors: Please set LastEditors * @Description: In User Settings Edit * @FilePath: /justdoit/vue-手写简版源码/lvue.html --> <!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 @click="onclick">{{counter}}</p> <p l-text="counter"></p> <p l-html="desc"></p> <input l-model="inputVal" type="text"> <p>{{inputVal}}</p> </div> <script src="lvue.js"></script> <script> const app = new LVue({ el:'#app', data: { counter: 1, desc: 'lvue<span style="color:red">html绑定的span标签</span>', inputVal:"l-model双向绑定的值" }, methods: { onclick() { // this必须是组件实例 console.log(this.counter); } }, }) // setInterval(() => { // app.counter++ // }, 1000); </script> </body> </html>
2 js部分
/* * @Author: * @Date: 2020-07-05 22:23:07 * @LastEditTime: 2020-07-06 23:39:12 * @LastEditors: Please set LastEditors * @Description: In User Settings Edit * @FilePath: /justdoit/vue-手写简版源码/lvue.js * * * 执行new Vue() 的时候做了哪些事情? * 1 对data中传入的数据做数据劫持,监听所有属性 --监听器:Observer * 2 对模版执行编译,找到动态绑定的数据{{}}和指令v-bind,v-if 等,从data中获取并初始化视图 --编译器:Compile * 3 定义一个更新函数和Watcher,将来在数据变化的时候,Watcher调用更新函数来更新视图 --监听器:Watcher * 4 由于data中的值可能被绑定了多次,所以data中的每个key都需要一个管家Dep来管理多个Watcher,将来数据变化,会找到对应的Dep,通知Watcher执行更新函数 * * 上面过程涉及到的几个角色及作用: * 1 Vue:框架的构造函数 * 2 Observer:执行数据相应化(分辨数据是对象还是数组) * 3 Compile:编译模版,初始化视图,收集依赖(更新函数的实现,Watcher的创建) * 4 Watcher:执行更新函数 * 5 Dep:管理多个Watcher,批量更新 */ class LVue { constructor(options){ //options传入的是vue实例 // 保存选项 this.$options = options this.$data = options.data; // this.$$data = options.data // 响应化处理 objserve(this.$data) // 代理$data中属性 proxy(this) //this =>vm // 执行编译 new Compile(this.$options.el,this) } } // 将来用户可能直接通过vm.xxx去访问vm.$data中的数据,这里做个方便用户操作的方法,将$data中的值遍历出来直接放到vm上,方便用户使用 function proxy(vm){ Object.keys(vm.$data).forEach(key=>{ Object.defineProperty(vm,key,{ get(){ return vm.$data[key] }, set(newVal){ vm.$data[key] = newVal } }) }) } // 将来每一个响应式对象都会伴生一个Observer实例 class Observer{ constructor(value){ this.value = value; // 判断value是obj还是数组 this.walk(value) } walk(obj){ Object.keys(obj).forEach(key =>defineReactive(obj,key,obj[key])) } } function defineReactive(obj,key,val){ // val可能还是个对象,需要递归一下 objserve(val) // 每执行一次defineReactive就创建一个Dep实例 const dep = new Dep() Object.defineProperty(obj,key,{ get(){ Dep.target && dep.addDep(Dep.target) return val }, set(newVal){ if(newVal !== val){ val = newVal // 如果改变的是个对象,还需要调用一下 objserve(newVal) console.log('set', newVal); // 在这里已经监听到了数据的变化,后续可以做一些更新视图的操作 dep.notify() } } }) } // Watcher:小秘书,界面中的每个依赖对应一个小秘书,更新视图用 class Watcher{ constructor(vm,key,updateFn){ this.$vm = vm this.key = key this.updateFn = updateFn // 读一次数据,触发defineReactive里面的get() Dep.target = this this.$vm[this.key] Dep.target = null } // 将来管家调用 update(){ this.updateFn.call(this.$vm,this.$vm[this.key]) } } // 如果一个对象有多个属性,循环调用defineReactive,传入每一个值去进行监听 function objserve(obj){ // 判断obj类型 if(Object.prototype.toString.call(obj)!=="[object Object]"){ return } new Observer(obj); } class Dep{ constructor(){ this.deps = [] } addDep(watcher){ this.deps.push(watcher) } notify(){ this.deps.forEach(watcher=>{ watcher.update() }) } } // 编译过程 class Compile { constructor(el,vm){ this.$vm = vm this.$el = document.querySelector(el) if(this.$el){ this.compile(this.$el) } } compile(el){ // el是根节点,需要递归遍历,看每个节点是标签还是文字 el.childNodes.forEach(node=>{ if(this.isElement(node)){ console.log("是标签",node.nodeName) this.compileElement(node) }else if(this.isInter(node)){ console.log("是插值表达式",node.textContent) this.compileText(node) } // 如果当前标签里面还存在标签,递归一下向下查找 if(node.childNodes){ this.compile(node) } }) } // 判断是否是标签 isElement(node){ return node.nodeType === 1 } // 判断是否是插值表达式 isInter(node){ return node.nodeType ===3 && /{{(.*)}}/.test(node.textContent) } compileElement(node){ // 获取节点属性 const nodeAttrs = node.attributes // 转成数组循环 console.log(nodeAttrs) Array.from(nodeAttrs).forEach(attr=>{ const attrName = attr.name //指令名字 const exp = attr.value //值 console.log('typeof--------',attrName) if(attrName.indexOf('l-')!=-1){ //如果是指令 执行对应方法 const dir = attrName.substring(2) // 执行对应的方法 this[dir] && this[dir](node,exp) }else if(attrName.startsWith('@')){ const eventName = attrName.slice(1) this.listenerEvent(node,exp,eventName) } }) } // 所有动态绑定都需要创建更新函数,以及对应watcher实例 update(node,exp,dir){ // 更新的时候fn对应一个更新函数,例如指令是text的时候对应一个 textUpdater函数,在textUpdater中做视图修改操作 const fn = this[dir+'Updater'] fn && fn(node,this.$vm[exp]) new Watcher(this.$vm,exp,function(val){ fn && fn(node,val) }) } textUpdater(node,val){ node.textContent = val } htmlUpdater(node,val){ node.innerHTML = val } // 插值文本的编译 compileText(node){ // 获取表达式的值 console.log(RegExp.$1) this.update(node,RegExp.$1,'text') } // l-text text(node,exp){ this.update(node,exp,'text') } // l-html html(node,exp){ this.update(node,exp,'html') } // l-model model(node,exp){ const tagName = node.tagName; if(tagName==='INPUT' || tagName === 'TEXTAREA'){ node.value = this.$vm[exp] if(/msie/i.test(navigator.userAgent)) { //ie浏览器 node.attachEvent("onpropertychange", this.handler(event,exp)); } else {//非ie浏览器 console.log("非ie浏览器") node.addEventListener("input",(event)=>{ this.$vm[exp] = event.target.value this.$vm.$data[exp] = event.target.value console.log(this.$vm) }, false); } }else{ console.error("v-model只能设置在input或者textarea上") } } listenerEvent(node,exp,eventName){ node.addEventListener(eventName,()=>{ this.$vm.$options.methods[exp].call(this.$vm) }) } }