前端从业人员技术贴

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

2019-07-18  本文已影响51人  你都如何回忆我_z

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(){}
}

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);
        });
    }
}

}

上一篇下一篇

猜你喜欢

热点阅读