美文网首页前端从业人员技术贴
前端 实现一个简易版的vue,了解vue的运行机制

前端 实现一个简易版的vue,了解vue的运行机制

作者: 你都如何回忆我_z | 来源:发表于2019-07-18 20:58 被阅读51次

    HTMl代码结构

     <div id="wrap">
        <p v-html="test"></p>
        <input type="text" v-model="form">
        <div>
            多层对象渲染
            <div>{{aaa.bb}}</div>
        </div>
        <div>计算属性: <span>{{newform}}</span></div>
        <button @click="add">
            ++++
        </button>
        <button @click="sub">
            ---
        </button>
        单层对象渲染{{form}}
        <div>函数渲染{{fnForm()}}</div>
    </div>
    

    js调用代码

    new Vue({
        el: '#wrap',
        data: {
            form: 0,
            test: '<strong>我是粗体</strong>',
            aaa: {
                bb: '123'
            }
        },
        computed: {
            newform() {
                return this.form * 2;
            }
        },
        methods: {
            add() {
                this.form++;
            },
            fnForm() {
                return this.form;
            },
            sub() {
                this.form--;
            }
        },
        created() {
            console.log(this, 'vue');
        }
    });
    

    vue结构

    class Vue{
        constructor(){}
        observer(){} 
        compile(){}
        dealComputed(){}
       render(){}
    }
    class Watcher{
        constructor(){}
        update(){}
    }
    
    • Watcher 渲染视图的依赖 (局部更新)
    • Vue constructor 构造函数主要是数据的初始化
    • observer 劫持监听所有数据
    • compile 编译dom
    • dealComputed computed处理
    • render 解析{{}}

    vue 初始化

      class Vue {
           constructor(options = {}) {
           this.$el = document.querySelector(options.el);
           this.data = options.data;
           this.callerName = '';
           // 依赖收集器: 存储依赖的回调函数
           this.caller = {};
           this.methods = options.methods;
           //  依赖收集器: 存储依赖的渲染函数
           this.watcherTask = {};
           // 计算属性
           this.observer(this.data);
    
           // 异步任务集合
           this.taskList = new Set([]);
           this.timeId = 0;
           this.dealComputed(options.computed);
           this.compile(this.$el); // 解析dom
           // 监听的任务队列
           options.created.bind(this)();
    }
    

    observer 劫持监听

    observer(data) {
        let that = this;
        Object.keys(data).forEach(key => {
            let value = data[key];
            this.watcherTask[key] = new Set([]);
            Object.defineProperty(this, key, {
                configurable: false,
                enumerable: true,
                get() {
                    if (that.callerName) {
                        that.addCallback(key);
                    }
                    return value;
                },
                set(newValue) {
                    if (newValue !== value) {
                        value = newValue;
                        if (that.caller[key]) {
                            that.caller[key].forEach(name => {
                                // 放到任务列表中,避免无用的重复执行
                                /**
                                 *  这里为什么要写name进去呢, 就是为了callback执行的时候  可以从wather函数中找到 对应dom的渲染函数
                                 * 思路是 computer里的某一个函数所依赖的data的值一旦发生改变
                                 * 在setter函数里重新调用computer函数, 去更新值
                                 * 更新完后 再去wather里找到指定dom的渲染函数, 渲染到页面
                                 */
                                that.taskList.add({
                                    type: name,
                                    fn: that[name]
                                });
                            });
                        }
                        that.toExecTask(key);
                    }
                }
            });
        });
    }
    

    compile 编译dom
    简单实现了一下 双向绑定 v-model v-html @click

    compile(el) {
        let nodes = el.childNodes;
        for (let i = 0; i < nodes.length; i++) {
            let node = nodes[i];
            // 区分文本节点和元素节点
            if (node.nodeType === 3) {
                /**
                 * 如果是文本节点 则直接取出 调用render函数渲染
                 * 取出文本节点的内容
                 */
                let text = node.textContent.trim();
                if (!text) {
                    continue;
                }
                this.render(node, 'textContent');
            }
            else if (node.nodeType === 1) {
                /**
                 * 元素节点  检测节点内是否还有嵌套的子节点
                 * 如果有 就递归再去执行
                 */
                if (node.childNodes.length > 0) {
                    this.compile(node);
                }
                // 编译v-model
                let vmFlag = node.hasAttribute('v-model');
                if (vmFlag && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
                    // 编辑时候input 自执行函数  
                    node.addEventListener('input', (() => {
                        // 获取v-model 绑定的key
                        let key = node.getAttribute('v-model');
                        node.removeAttribute('v-model');
                        // 渲染视图  并添加到监听的任务队列
                        this.watcherTask[key].add(new Watcher(node, this, key, 'value'));
    
                        return () => {
                            this.data[key] = node.value;
                            // console.log(this.data[key]);
                        }
                    })());
                }
    
                // 编译v-html
                if (node.hasAttribute('v-html')) {
                    let key = node.getAttribute('v-html');
                    node.removeAttribute('v-html');
                    this.watcherTask[key].add(new Watcher(node, this, key, 'innerHTML'));
                }
    
    
               // 编译@click
                if (node.hasAttribute('@click')) {
                    let key = node.getAttribute('@click');
                    node.removeAttribute('@click');
                    node.addEventListener('click', () => {
                        if (this.methods[key]) {
                            this.methods[key].bind(this)();
                        }
                    });
                }
                this.render(node, 'innerHTML');
            }
        }
    }
    

    render 函数 解析{{}}

    render(node, type) {
        let reg = /\{\{(.*?)\}\}/g;
        // 取出文本
        let txt = node.textContent;
        let flag = reg.test(txt);
        if (flag) {
            node.textContent = txt.replace(reg, ($1, $2) => {
                // 是否是函数
                let tpl = null;
                if ($2.includes('(') || $2.includes(')')) {
                    //函数
                    // 未完成 - 函数欠缺收集依赖(回头补上)
                    let key = $2.replace(/[(|)]/g, '');
                    return this.methods[key] ? this.methods[key].bind(this)() : '';
                } else {
                    // data
                    let tpl = this.watcherTask[$2] = this.watcherTask[$2] || new Set([]);
                    tpl.add(new Watcher(node, this, $2, type));
                }
                console.log($2);
                // 处理对象 例如 {{data.a}}
                let valArr = $2.split('.');
                // 如果是 {{}}里是对象嵌套的值
                if (valArr.length > 1) {
                    let v = null;
                    valArr.forEach(key => {
                        v = !v ? this[key] : v[key];
                    });
                    return v;
                }
                // 如果是 {{}}里 不是对象嵌套的值
                return this[$2];
            });
        }
    }
    

    execTask函数 执行 taskList里收集的依赖函数更新数据 然后执行watcherTask 队列里的 渲染函数 更新视图

    execTask(key) {
        this.taskList.forEach(item => {
            item.fn.bind(this)();
            if (item.type) {
                let key = item.type.replace(/_computed_/g, '');
                // 渲染该dom computed
                this.watcherTask[key].forEach(task => {
                    task.update();
                });
            }
        });
        this.taskList = new Set([]);
        
        console.log(this.watcherTask, 'key', key);
        // 渲染该dom
        this.watcherTask[key].forEach(task => {
            task.update();
        });
    }
    

    computed 计算属性

    dealComputed(computed) {
        Object.keys(computed).forEach(key => {
            // 回调函数名
            let computedCallbackName = '_computed_' + key;
            // 回调函数值
            let fn = (() => {
                this[key] = computed[key].bind(this)();
            });
            this[computedCallbackName] = fn;
            // 读取值之前设置callerName
            this.callerName = computedCallbackName;
            fn();
        });
    }
    

    简单实现了 nextTick, 并且对 多次修改data下的值 进行依赖合并调用
    例如: this.a+ 1
    this.a+ 1
    this.a+ 1
    如上接连三次修改this.a的值 这样就会导致setter函数被触发三次, 重复去执行其依赖操作, 所以每次调用依赖队列 都将其放到 异步队列中操作

    // 向特定字段下加入依赖它的回调函数
    addCallback(key) {
        if (!this.caller[key]) {
            this.caller[key] = new Set([]);
        }
        this.caller[key].add(this.callerName);
    }
     $nextTick(cb) {
           this.timeId = setTimeout(cb.bind(this), 0);
    }
    toExecTask(key) {
        if (!this.timeId) {
            this.$nextTick(() => {
                this.timeId = 0;
                this.execTask(key);
            });
        }
    }
    

    Watcher 渲染函数

    class Watcher {
        constructor(el, vm, value, type) {
            this.el = el;
            this.vm = vm;
            this.value = value;
            this.type = type;
            this.update();
        }
       update() {
            this.el[this.type] = this.vm[this.value];
        }
    }
    

    以下是源代码 vue.js 直接在index.html 中引入就好 <script src="./vuea.js"></script>

    // 更细视图操作
    class Watcher {
        constructor(el, vm, value, type) {
            this.el = el;
            this.vm = vm;
            this.value = value;
            this.type = type;
            this.update();
        }
        update() {
                this.el[this.type] = this.vm[this.value];
        }
    }
    
    class Vue {
    constructor(options = {}) {
        this.$el = document.querySelector(options.el);
        this.data = options.data;
        this.callerName = '';
        // 依赖收集器: 存储依赖的回调函数
        this.caller = {};
        this.methods = options.methods;
        this.watcherTask = {};
        // 初始化劫持监听的所有数据
        // 计算属性
        this.observer(this.data);
    
        // 异步任务集合
        this.taskList = new Set([]);
        this.timeId = 0;
        this.dealComputed(options.computed);
        this.compile(this.$el); // 解析dom
        // 监听的任务队列
        options.created.bind(this)();
    }
    // 向特定字段下加入依赖它的回调函数
    addCallback(key) {
        if (!this.caller[key]) {
            this.caller[key] = new Set([]);
        }
        this.caller[key].add(this.callerName);
    }
    observer(data) {
        let that = this;
        Object.keys(data).forEach(key => {
            let value = data[key];
            this.watcherTask[key] = new Set([]);
            Object.defineProperty(this, key, {
                configurable: false,
                enumerable: true,
                get() {
                    if (that.callerName) {
                        that.addCallback(key);
                    }
                    return value;
                },
                set(newValue) {
                    if (newValue !== value) {
                        value = newValue;
                        if (that.caller[key]) {
                            that.caller[key].forEach(name => {
                                // 放到任务列表中,避免无用的重复执行
                                /**
                                 *  这里为什么要写name进去呢, 就是为了callback执行的时候  可以从wather函数中找到 对应dom的渲染函数
                                 * 思路是 computer里的某一个函数所依赖的data的值一旦发生改变
                                 * 在setter函数里重新调用computer函数, 去更新值
                                 * 更新完后 再去wather里找到指定dom的渲染函数, 渲染到页面
                                 */
                                that.taskList.add({
                                    type: name,
                                    fn: that[name]
                                });
                            });
                        }
                        that.toExecTask(key);
                    }
                }
            });
        });
    }
    // 编译
    compile(el) {
        let nodes = el.childNodes;
        for (let i = 0; i < nodes.length; i++) {
            let node = nodes[i];
            // 区分文本节点和元素节点
            if (node.nodeType === 3) {
                /**
                 * 如果是文本节点 则直接取出 调用render函数渲染
                 * 取出文本节点的内容
                 */
                let text = node.textContent.trim();
                if (!text) {
                    continue;
                }
                this.render(node, 'textContent');
            }
            else if (node.nodeType === 1) {
                /**
                 * 元素节点  检测节点内是否还有嵌套的子节点
                 * 如果有 就递归再去执行
                 */
                if (node.childNodes.length > 0) {
                    this.compile(node);
                }
                // 编译v-model
                let vmFlag = node.hasAttribute('v-model');
                if (vmFlag && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
                    // 编辑时候input 自执行函数  
                    node.addEventListener('input', (() => {
                        // 获取v-model 绑定的key
                        let key = node.getAttribute('v-model');
                        node.removeAttribute('v-model');
                        // 渲染视图  并添加到监听的任务队列
                        this.watcherTask[key].add(new Watcher(node, this, key, 'value'));
    
                        return () => {
                            this.data[key] = node.value;
                            // console.log(this.data[key]);
                        }
                    })());
                }
    
                // 编译v-html
                if (node.hasAttribute('v-html')) {
                    let key = node.getAttribute('v-html');
                    node.removeAttribute('v-html');
                    this.watcherTask[key].add(new Watcher(node, this, key, 'innerHTML'));
                }
    
    
               // 编译@click
                if (node.hasAttribute('@click')) {
                    let key = node.getAttribute('@click');
                    node.removeAttribute('@click');
                    node.addEventListener('click', () => {
                        if (this.methods[key]) {
                            this.methods[key].bind(this)();
                        }
                    });
                }
                this.render(node, 'innerHTML');
            }
        }
    }
    // 计算属性
    dealComputed(computed) {
        Object.keys(computed).forEach(key => {
            // 回调函数名
            let computedCallbackName = '_computed_' + key;
            // 回调函数值
            let fn = (() => {
                this[key] = computed[key].bind(this)();
            });
            this[computedCallbackName] = fn;
            // 读取值之前设置callerName
            this.callerName = computedCallbackName;
            fn();
        });
    }
    // 解析双括号 
    render(node, type) {
        let reg = /\{\{(.*?)\}\}/g;
        // 取出文本
        let txt = node.textContent;
        let flag = reg.test(txt);
        if (flag) {
            node.textContent = txt.replace(reg, ($1, $2) => {
                // 是否是函数
                let tpl = null;
                if ($2.includes('(') || $2.includes(')')) {
                    //函数
                    // 欠缺收集依赖
                    let key = $2.replace(/[(|)]/g, '');
                    return this.methods[key] ? this.methods[key].bind(this)() : '';
                } else {
                    // data
                    let tpl = this.watcherTask[$2] = this.watcherTask[$2] || new Set([]);
                    tpl.add(new Watcher(node, this, $2, type));
                }
                console.log($2);
                // 处理对象 例如 {{data.a}}
                let valArr = $2.split('.');
                // 如果是 {{}}里是对象嵌套的值
                if (valArr.length > 1) {
                    let v = null;
                    valArr.forEach(key => {
                        v = !v ? this[key] : v[key];
                    });
                    return v;
                }
                // 如果是 {{}}里 不是对象嵌套的值
                return this[$2];
            });
        }
    }
    // 执行并清空任务队列
    execTask(key) {
        this.taskList.forEach(item => {
            item.fn.bind(this)();
            if (item.type) {
                let key = item.type.replace(/_computed_/g, '');
                // 渲染该dom computed
                this.watcherTask[key].forEach(task => {
                    task.update();
                });
            }
        });
        this.taskList = new Set([]);
        
        console.log(this.watcherTask, 'key', key);
        // 渲染该dom
        this.watcherTask[key].forEach(task => {
            task.update();
        });
        
    }
    $nextTick(cb) {
        this.timeId = setTimeout(cb.bind(this), 0);
    }
    toExecTask(key) {
        if (!this.timeId) {
            this.$nextTick(() => {
                this.timeId = 0;
                this.execTask(key);
            });
        }
    }
    

    }

    相关文章

      网友评论

        本文标题:前端 实现一个简易版的vue,了解vue的运行机制

        本文链接:https://www.haomeiwen.com/subject/tpixlctx.html