(精华2020年5月17日更新) vue实战篇 手写vue底层源码

MYvue.js 主要作用监听属性变化

class MYvue {
    constructor(options) {
        this.$options = options;
        this.$data = options.data;
        //数据劫持
        this.observe(this.$data);
        this.$el = options.el;
        //包含Watcher创建
        new Complie(options.el, this)

    }
    //数据劫持
    observe(data) {
        if (!data || typeof data != 'object') {
            return;
        }
        Object.keys(data).forEach((key) => {
            //让数据可观测
            //this.$data.test 改变
            this.defineReactive(data, key, data[key]);
            //this.test 代理改变
            this.proxyData(key);
        })
    }
    //让数据可观测
    defineReactive(obj, key, value) {
        var dept = new Dep();
        //value值为对象递归遍历
        this.observe(value);

        Object.defineProperty(obj, key, {
            get() {
                //属性被读取了,将来需要添加订阅
                console.log('我被读取了');
                //将Dep.target(当前的watcher对象存入Dep的deps)
                Dep.target && dept.addDep(Dep.target)
                return value;
            },
            set(newVal) {
                if (newVal == value) {
                    return;
                }
                value = newVal;
                console.log(key + '属性更行了,他更新的值是' + newVal);
                //如果我被改变, 我将来会在这里通知的
                dept.notify(); //变化的数据,让watcher的update方法执行
            },
            enumerable: true,
            configurable: true
        })
    }

    //代理data中的属性到vue实例上
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal;
            }
        })
    }
}

//Dep 用来管理wather,管理者的角色
class Dep {
    constructor() {
        //存放所有的依赖(watcher),一个watcher对应一个属性
        this.deps = [];
    }
    //收集订阅者
    addDep(sub) {
        this.deps.push(sub);
    }
    //通知订阅更新
    notify() {
        this.deps.forEach((sub) => {
            //你要更新了
            sub.update() //update是watchr里面的一个函数
        })
    }
}

//监听器对象
class Watcher {
    constructor(vm, key, cb) {
        // vm :Vue实例化对象
        // key:  需要监听的属性
        // cb: 是Watccher绑定的更新函数
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        Dep.target = this;//this指Watcher本身

        this.vm[key] // 触发getter ,添加依赖
        Dep.target = null;

    }
    update() {
        console.log('你的属性要更新了');
        this.cb.call(this.vm, this.vm[this.key]);
    }
}

Complie.js 把属性变化重新渲染html

class Complie {
    constructor(el, vm) {
        //遍历节点
        this.$el = document.querySelector(el);
        this.$vm = vm;
        //编译
        if (this.$el) {
            //转换内容为片段fragment
            this.$fragment = this.node2Fragment(this.$el);

            //执行编译
            this.replaceTemplate(this.$fragment)
            //将编译完的html结果追加到$el
            this.$el.appendChild(this.$fragment);
        }
    }
    node2Fragment(el) {
        // createDocumentFragment 用来创建虚拟dom节点
        var frag = document.createDocumentFragment();
        let child;
        //讲el中所有的元素搬家到frag
        while (el.firstChild && (child = el.firstChild)) {
            frag.appendChild(child);
        }
        return frag;
    }
    //对el立面的内容进行替换
    // 对el里面的内容进行替换
    replaceTemplate(frag) {
        // console.log('frag.childNodes',frag.childNodes);
        var childNodes = Array.from(frag.childNodes);
        if (childNodes.length == 0) return;
        childNodes.forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}}

            // 只读属性 Node.nodeType 表示的是该节点的类型   
            // nodeType 属性可用来区分不同类型的节点,比如 元素, 文本 和 注释。 
            // 即是文本节点又有大括号的情况{{}}
            if (node.nodeType === 3 && reg.test(txt)) {
                // console.log(RegExp.$1); // 匹配到的第一个分组 如: a.b, c
                let arr = RegExp.$1.split('.');
                let valTest = this.$vm;
                arr.forEach(key => {
                    valTest = valTest[key]; // 如this.a.b
                });
                // 用trim方法去除一下首尾空格
                node.textContent = txt.replace(reg, valTest).trim();

                // 监听变化,第二步加的
                // 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参
                new Watcher(this.$vm, RegExp.$1, newVal => {
                    //这里是有属性改变的时候会更新数据
                    node.textContent = txt.replace(reg, newVal).trim();
                });
            }

            // v-modle数据的双向绑定
            if (node.nodeType === 1) { // 元素节点
                let nodeAttr = Array.from(node.attributes); // 获取dom上的所有属性,是个类数组
                nodeAttr.length > 0 && nodeAttr.forEach(attr => {
                    let name = attr.name; // v-model  type
                    let exp = attr.value; // c        text
                    if (name.includes('v-')) {
                        node.value = this.$vm[exp]; // this.c 为 2
                    }
                    // oninput .onclick
                    node.addEventListener('input', e => {
                        let newVal = e.target.value;
                        // 相当于给this.c赋了一个新值
                        // 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
                        this.$vm[exp] = newVal;
                    });
                });
            }

            // 如果还有子节点,继续递归replaceTemplate
            if (node.childNodes && node.childNodes.length) {
                this.replaceTemplate(node);
            }
        });
    }
}

使用

<!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>
<script src="./MYvue.js"></script>
<script src="./compile.js"></script>
<body>
    <div id="app">
        <h1>{{song}}</h1>
        <p>主打歌为{{album.theme}} </p>
        <p> <span>作词人为{{singer}}等人。</span><em>qq</em></p>
        <input type="text" v-model="song">
    </div>
    <script>
        // 写法和Vue一样
        let MYvue = new MYvue({
            el: '#app',
            data: { 
                song: '飞啊飞啊',
                album: {
                    name: '一眼万年',
                    theme: '天外飞仙'
                },
                singer: '胡歌'
            }
        });
    </script>
</body>
</html>

你可能感兴趣的:((持续更新)vue实战篇)