zoukankan      html  css  js  c++  java
  • 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)
            })
        }
    }
  • 相关阅读:
    mysql 数据库 分表后 怎么进行分页查询?Mysql分库分表方案?
    mysql分库分区分表
    Mysql分表和分区的区别、分库和分表区别
    shell 浮点数和整数比较大小
    Domino's Pizza 点餐
    Long John Silver's 点餐
    韩国bibigo饺子做煎饺到方法
    其他的知名餐饮
    KFC 点餐
    Macdonald 点餐
  • 原文地址:https://www.cnblogs.com/panda-programmer/p/13258567.html
Copyright © 2011-2022 走看看