自己动手写一个简易版本Vue(二)

Vue由于其高效的性能和灵活入门简单、轻量的特点下变得火热。在当今前端越来越普遍的使用,今天来动手写一下Vue

主要实现
1.构建Vue实例查找指令和模板
2.数据驱动界面更新
3.界面驱动数据更新
4.事件相关指令
5.Vuex基本实现
6.Vue-Router基本实现

上个章节讲到构建Vue实例,和遍历模板和指令。这个章节开始讲数据驱动界面更新。讲解之前先看下流程图。我们得监听所有属性set,get方法。然后让订阅者(Dep)发布通知,让观察者(Watcher)更新视图。

Vue流程图.png

如果要监听所有属性的set,和get方法。在Vue2.x版本中使用的是Object.defineProperty

    1.defineProperty方法的特点
      可以直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
    2.defineProperty用法
      obj: 需要操作的对象
      prop: 需要操作的属性
      descriptor: 属性描述符
      Object.defineProperty(obj, prop, descriptor)
    * */
    let obj = {name: 'zs'};
    // 需求: 给obj对象动态新增一个name属性, 并且age属性的取值必须是18
    Object.defineProperty(obj, 'age', {
        // 可以通过value来告诉defineProperty方法新增的属性的取值是什么
        value: '18',
        // 默认情况下通过defineProperty新增的属性的取值是不能修改的
        // 如果想修改, 那么就必须显示的告诉defineProperty方法
        // writable: true
        writable: true,
        // 默认情况下通过defineProperty新增的属性是不能删除的
        // 如果想删除, 那么就必须显示的告诉defineProperty方法
        configurable: true,
        // 默认情况下通过defineProperty新增的属性是不能迭代的
        // 如果想迭代, 那么就必须显示的告诉defineProperty方法
        enumerable: true
    });
    for(let key in obj){
        console.log(key, obj[key]);
    }

允许结果:


Snip20210725_2.png

看完基本用法后,再看如果添加set,get


    /*
    1.defineProperty方法
    defineProperty除了可以动态修改/新增对象的属性以外
    还可以在修改/新增的时候给该属性添加get/set方法
    2.defineProperty get/set方法特点
    只要通过defineProperty给某个属性添加了get/set方法
    那么以后只要获取这个属性的值就会自动调用get, 设置这个属性的值就会自动调用set
    3.注意点:
    如果设置了get/set方法, 那么就不能通过value直接赋值, 也不能编写writable:true
    * */
    let obj = {};
    let oldValue = 'zs';
    Object.defineProperty(obj, 'name', {
        // value: oldValue,
        // writable: true,
        configurable: true,
        enumerable: true,
        get(){
            console.log("get方法被执行了");
            return oldValue;
        },
        set(newValue){
            if(oldValue !== newValue){
                console.log("set方法被执行了");
                oldValue = newValue;
            }
        }
    });
    console.log(obj.name);
    obj.name = 'ls';

允许结果:


Snip20210725_3.png

如果要监听对象的所有属性,可以使用遍历。为了方便今后使用,我们定义一个类,专门用于监听对象属性。

 /*
    需求: 快速监听对象中所有属性的变化
    * */
    let obj = {
        name: 'zs',
        age: 18,
        friend:{
            name : 'ls'
        }
    };
    class Observer{
        // 只要将需要监听的那个对象传递给Observer这个类
        // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
        constructor(data){
            this.observer(data);
        }
        observer(obj){
            if(obj && typeof obj === 'object'){
                // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
                for(let key in obj){
                    this.defineRecative(obj, key, obj[key])
                }
            }
        }
        // obj: 需要操作的对象
        // attr: 需要新增get/set方法的属性
        // value: 需要新增get/set方法属性的取值
        defineRecative(obj, attr, value){
            // 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
            this.observer(value);
            Object.defineProperty(obj, attr, {
                get(){
                    return value;
                },
                set:(newValue)=>{
                    if(value !== newValue){
                        // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
                        this.observer(newValue);
                        value = newValue;
                        console.log('监听到数据的变化, 需要去更新UI');
                    }
                }
            })
        }
    }
    new Observer(obj);
    obj.name = '王五';
    obj.friend.name = '赵六';
    console.log(obj.name);
    console.log(obj.friend.name);

运行结果:


Snip20210725_5.png

准备工作做完后,再对上个章节代码进行改进,把刚写好的类放到Lue.js文件中,在编译模版之前,先将data的数据都监听

  // 2.根据指定的区域和数据去编译渲染界面
  class Lue {
    constructor(options){
        // 1.保存创建时候传递过来的数据
        if(this.isElement(options.el)){
            this.$el = options.el;
        }else{
            this.$el = document.querySelector(options.el);
        }
        this.$data = options.data;
        // 2.根据指定的区域和数据去编译渲染界面
        if(this.$el){
            // 第一步: 给外界传入的所有数据都添加get/set方法
            //         这样就可以监听数据的变化了
            new Observer(this.$data);
            new Compiler(this);
        }
    }
    // 判断是否是一个元素
    isElement(node){
        return node.nodeType === 1;
    }
}

运行结果如下:


Snip20210725_6.png


接着我们还需要,使用发布订阅模式。先定义一个发布订阅类(Dep),观察者类(Watcher),然后通过发布订阅的类管理观察类

class Dep {
    constructor(){
        // 这个数组就是专门用于管理某个属性所有的观察者对象的,name属性可能有三个观察者,因为模板中三次用到了name
        this.subs = [];
    }
    // 订阅观察的方法
    addSub(watcher){
        this.subs.push(watcher);
    }
    // 发布订阅的方法
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
class Watcher {
    constructor(vm, attr, cb){
        this.vm = vm;
        this.attr = attr;
        this.cb = cb;
        // 在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue();
    }
    getOldValue(){
        Dep.target = this;
        let oldValue = CompilerUtil.getValue(this.vm, this.attr);
        Dep.target = null;
        return oldValue;
    }
    // 定义一个更新的方法, 用于判断新值和旧值是否相同
    update(){
        let newValue = CompilerUtil.getValue(this.vm, this.attr);
        if(this.oldValue !== newValue){
            this.cb(newValue, this.oldValue);
        }
    }
}

再到CompilerUtil类的几个指令方法中添加观察者

model: function (node, value, vm) { // value=name  value=time.h
        // 第二步: 在第一次渲染的时候, 就给所有的属性添加观察者
        new Watcher(vm, value, (newValue, oldValue)=>{
            node.value = newValue;
            // debugger;
        });
        let val = this.getValue(vm, value);
        node.value = val;
       //input值发生变化时,重新设置下值
        node.addEventListener('input', (e)=>{
            let newValue = e.target.value;
            this.setValue(vm, value, newValue);
        })
    },

最后在属性发生变化时,通知观察者.再到Observer类

defineRecative(obj, attr, value){
        // 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
        this.observer(value);
        // 第三步: 将当前属性的所有观察者对象都放到当前属性的发布订阅对象中管理起来
        let dep = new Dep(); // 创建了属于当前属性的发布订阅对象
        Object.defineProperty(obj, attr, {
            get(){
                Dep.target && dep.addSub(Dep.target);
                // debugger;
                return value;
            },
            set:(newValue)=>{
                if(value !== newValue){
                    // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
                    this.observer(newValue);
                    value = newValue;
                    dep.notify();
                    console.log('监听到数据的变化, 需要去更新UI');
                }
            }
        })
    }

数据驱动界面更新就完成了

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 前言1.此版本是vue1.0的版本,watcher的监听粒度为页面级(出现多少引用数据就有多少个whater,2....
    myth小艾阅读 1,753评论 0 8
  • 本文是lhyt本人原创,希望用通俗易懂的方法来理解一些细节和难点。转载时请注明出处。文章最早出现于本人github...
    lhyt阅读 2,296评论 0 4
  • 数据发生变化后,会重新对页面渲染,这就是Vue响应式 响应式图例 2 想完成这个过程,我们需要做些什么 侦测数据的...
    南城夏季阅读 305评论 0 0
  • Vue由于其高效的性能和灵活入门简单、轻量的特点下变得火热。在当今前端越来越普遍的使用,今天来动手写一下Vue 一...
    李徐安阅读 1,241评论 0 3
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,834评论 28 54

友情链接更多精彩内容