手写vue---部分实现

let CompileUtil = {
    getValue(vm, value){
        // 切割 time.h ==> time h
        return value.split('.').reduce((data, currentKey) => {
            // 第一次执行:data = $data, currentKey = time
            // 第二次执行:data = time, currentKey = h
            // 使用trim()的原因:插值{{}}内的内容 无空格{{name}} 如果有空格{{  name  }},有空格的时候会把空格包含在内容,就获取不到对应的数据
            return data[currentKey.trim()] 
        }, vm.$data)
    },
    getContent(vm, value){
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args)=>{
            console.log('args', args);
            return this.getValue(vm, args[1])
        })
        // console.log('{{val}}', val);
        return val
    },
    setValue(vm, attr, newValue){
        attr.split('.').reduce((data, currentAttr,index, arr)=>{
            if(index === arr.length - 1){
                data[currentAttr] = newValue
            }
            return data[currentAttr]
        },vm.$data)
    },
    model: function(node, value, vm){
        // 第二步:在第一步渲染的时候,就给所有的属性添加观察者
        new Watcher(vm, value, (newValue, oldValue)=>{
            // 更新数据
            node.value = newValue
        })
        // v-model = "time.h" vm.$data[time.h]无法识别,vm.$data[time] time[h]
        let val = this.getValue(vm, value)
        node.value = val

        // 上面是通过数据修改界面

        // 界面驱动数据更新
        /***
         * v-model
         *      input 输入事件
         */
        node.addEventListener('input', (e)=>{
            let newValue = e.target.value
            this.setValue(vm, value, newValue)
        })
    },
    html: function(node, value, vm){
        new Watcher(vm, value, (newValue, oldValue)=>{
            // 更新数据
            node.innerHTML = newValue
        })
        let val = this.getValue(vm, value)
        node.innerHTML = val
    },
    text: function(node, value, vm){
        new Watcher(vm, value, (newValue, oldValue)=>{
            // 更新数据
            node.innerText = newValue
        })
        let val = this.getValue(vm, value)
        node.innerText = val
    },
    content: function(node, value, vm){
        // value {{name}} --- name  --- $data[name]
        // let val = this.getContent(vm, value)
        let reg = /\{\{(.+?)\}\}/gi
        let val = value.replace(reg, (...args)=>{ // 这一层使用replace是因为value获取到的时候{{xxx}},需要将xxx去进行监听
            // 内层是为了保证数据完整性
            new Watcher(vm, args[1], (newValue, oldValue)=>{
                // console.log('{{{{{{', value);
                // 更新数据
                node.textContent = this.getContent(vm, value) // 如果监听到数据发生变化,回调函数执行,使用getContent的原因是:当有多个{{}},改变其中一个{{}}直接赋值将会覆盖其他的{{}}
            })
            // 上面是watcher进行监听所有的属性
            return this.getValue(vm, args[1])
        })
        node.textContent = val
    },
    on: function(node, value, vm, type){
        node.addEventListener(type, (e)=>{
            vm.$methods[value].call(vm, e) // 通过call改变this
        })
    }
}
class Sue {
    constructor(options){
        // 1.保存创建时候传递的数据
        if(this.isElement(options.el)){
            this.$el = options.el;
        }else{
            this.$el = document.querySelector(options.el)
        }
        this.$data = options.data
        this.proxyData()
        this.$methods = options.methods
        this.$computed = options.computed
        /**
         * 将computed中的方法添加到$data中,
         * 只有这样将来在渲染的时候才能从$data中获取到computed中定义的计算属性
         */
        this.computed2data()
        // 2.根据指定的区域和数据去编译渲染界面
        if(this.$el){ // this.$el 存在才去编译渲染
            // 第一步:给外界传入的所有数据都添加get/set方法,这样就可以监听数据的变化了
            new Observer(this.$data)
            new Compiler(this)
        }
    }
    computed2data(){
        for(let key in this.$computed){
            Object.defineProperty(this.$data, key, {
                get:()=>{
                    return this.$computed[key].call(this)
                }
            })
        }
    }
    /**
     * 数据代理:数据拦截
     *     this.xxx 数据保存在vue实例上才可以获取
     *     根据 Object.defineProperty在vue实例上增加属性
     */
    proxyData(){
        for(let key in this.$data){
            Object.defineProperty(this, key, {
                get:()=>{
                    return this.$data[key]
                }
            })
        }
    }
    // 判断是否是一个元素
    isElement(node){
        return node.nodeType === 1;
    }
}
class Compiler {
    constructor(vm){
        this.vm = vm;
        // 1.将网页上的元素放到内存中
        let fragment = this.node2fragment(this.vm.$el);
        // 2.利用指定的数据编译内存中的元素
        this.buildTemplate(fragment)
        // 3.将编译好的内容重新渲染到网页上
        this.vm.$el.appendChild(fragment)   
    }
    node2fragment(app){
        // 1.创建一个空的文档碎片对象
        let fragment = document.createDocumentFragment();
        // 2.编译循环获取每一个元素
        let node = app.firstChild;
        while(node){
             // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
            fragment.appendChild(node)
            node = app.firstChild
        }
        // 3.返回储存了所有元素的文档碎片对象
        return fragment
    }
    buildTemplate(fragment){
        // console.log('childNodes', fragment.childNodes); // 伪数组
        let nodeList = [...fragment.childNodes] // 伪数组转为数组
        nodeList.forEach(node=>{
            // 需要判断当前遍历到的节点是一个元素还是一个文本
            // 如果是一个元素, 我们需要判断有没有v-model属性
            // 如果是一个文本, 我们需要判断有没有{{}}的内容
            if(this.vm.isElement(node)){
                // 是一个元素
                this.buildElement(node)
                // 处理子元素(处理后代)
                this.buildTemplate(node)
            }else{
                // 不是一个元素
                this.buildText(node)
            }
        })
    }
    // 专门处理元素的方法
    buildElement(node){
        let attrs = [...node.attributes] // attributes:节点的属性集合,node.attributes 伪数组 
        // console.log('attrs', attrs);
        attrs.forEach(attr => {
            /**
             * v-model='xxx'  name=v-model  value = xxx
             * v-on:click='myFn' name=v-on:click  value = myFn
             */
            let {name,value} = attr;
            // console.log(name, value);
            // v-开头是vue的指令
            if(name.startsWith('v-')){
                // let [, directive] = name.split('-')
                // console.log('Vue指令');
                let [directiveName, directiveType] = name.split(':') // ['v-on','click']
                let [, directive] = directiveName.split('-')
                CompileUtil[directive](node, value, this.vm, directiveType)
            }
        })
    }
    // 专门用来处理文本的
    buildText(node){
        let content = node.textContent; // textContent 属性表示一个节点及其后代的文本内容
        // 匹配插值{{ }}
        let reg = /\{\{.+?\}\}/gi
        if(reg.test(content)){
            // console.log('是{{}}的文本', content);
            CompileUtil['content'](node, content, this.vm)
        }
    }
}
// 监听数据变化
class Observer {
    constructor(data){
        this.observer(data)
    }
    observer(obj){
        if(obj && typeof obj === 'object'){
            for(let key in obj){
                this.defineReactive(obj, key, obj[key])
            }
        }
    }
    defineReactive(obj, attr, value){
        this.observer(value)
        // 第三步:将当前属性的所有观察者对象都放到当前属性的发布订阅对象中管理起来
        let dep = new Dep(); // 创建了属于当前属性的发布订阅对象
        Object.defineProperty(obj, attr, {
            get(){
                Dep.target && dep.addSub(Dep.target)
                // console.log('Dep.target', Dep.target);
                return value
            },
            set:(newValue)=>{
                if(value !== newValue){
                    console.log('监听到数据变化newValue', newValue);
                    // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
                    this.observer(newValue)
                    value = newValue
                    dep.notify()
                }
            }
        })
    }
}
// 数据变化之后更新UI界面,可以使用发布订阅模式来实现
// 先定义一个观察者类,再定义一个发布订阅类,然后再通过发布订阅的类来管理观察者类
// 发布订阅类
class Dep {
    constructor() {
        // 这个数组就是专门用于管理某个属性所有的观察者对象的
        this.subs = []
    }
    // 订阅观察的方法(把观察者添加到数组中)
    addSub(watcher){
        this.subs.push(watcher)
    }
    // 发布订阅的方法(执行数组中所有观察者的更新方法)
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
// 观察者类
class Watcher {
    /***
     * vm vue实例
     * attr 观察的属性
     * cb 回调函数
     */
    constructor(vm, attr, cb){
        this.vm = vm
        this.attr = attr
        this.cb = cb
        // 在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue()
    }
    getOldValue() {
        Dep.target = this;
        let oldValue = CompileUtil.getValue(this.vm, this.attr)
        Dep.target = null;
        return oldValue
    }
    // 定义一个更新的方法,用于判断新值和旧值是否相同
    update(){
        let newValue = CompileUtil.getValue(this.vm, this.attr)
        if(this.oldValue !== newValue){
            this.cb(newValue, this.oldValue)
        }
    }
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./js/Sue.js"></script>
</head>

<body>
    <div id="app">
        <input type="text" v-model="name">
        <input type="text" v-model="time.h">
        <input type="text" v-model="time.m">
        <input type="text" v-model="name">
        <div v-html="html"></div>
        <div v-text="text"></div>
        <p>{{ time.h }} --- {{time.m}}</p>
        <div v-model="name" class="ss">123</div>
        <a href="">123</a>
        <div v-on:click="myFn">点击</div>
        <div>{{getName}}</div>
    </div>
    <script>
        /*
            1.要想使用Vue必须先创建Vue的实例, 创建Vue的实例通过new来创建, 所以说明Vue是一个类
            所以我们要想使用自己的Vue,就必须定义一个名称叫做Vue的类
            2.只要创建好了Vue的实例, Vue就会根据指定的区域和数据, 去编译渲染这个区域
            所以我们需要在自己编写的Vue实例中拿到数据和控制区域, 去编译渲染这个区域
            注意点: 创建Vue实例的时候指定的控制区域可以是一个ID名称, 也可以是一个Dom元素
            注意点: Vue实例会将传递的控制区域和数据都绑定到创建出来的实例对象上
                    $el/$data
    * */
        let vue = new Sue({
            el: "#app",
            data: {
                name: 'sss',
                time: {
                    h: '小时',
                    m: "秒",
                },
                age: 18,
                html: `<div>我是标签div</div>`,
                text: `<span>我是标签span</span>`,
            },
            methods: {
                myFn() {
                    alert('myFn被执行了')
                    console.log('this', this);
                    console.log('this.xxx', this.time.h);
                    /**
                     * this.xxx 数据保存在vue实例上才可以获取
                    */
                }
            },
            computed: {
                getName() {
                    return this.name + '666';
                }
            }
        })
    </script>
</body>

</html>

学习记录❥(^_-)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值