vue简版源码实现

编写一个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)
        })
    }
}
原文地址:https://www.cnblogs.com/panda-programmer/p/13258567.html