大前端-vue响应式原理

215 阅读6分钟

数据驱动

数据响应式:

  1. 数据模型仅仅是普通的javascript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率

双向绑定:

  1. 数据改变,视图改变;视图改变,数据也随之改变
  2. 我们可以使用v-model在表单元素上创建双向绑定

数据驱动是vue最独特的特性之一:

  1. 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图的

数据响应式的核心原理

Vue2.x中是基于Object.defineProperty实现的;

let data = {
    msg:'hello',
    count:10,
};
let vm = {};
Object.keys(data).forEach((v) => {
    Object.defineProperty(vm,v,{
        enumerable:true,
        configurable:true,
        get(){
            console.log('get');
             return data[v]
        },
        set(value){
            console.log('set:' + value);
            if(value === data[v]) return;
            data[v] = value;
            document.getElementById('app').innerHTML = value
        }
    })
})
vm.msg = 'hello world';
console.log(vm.msg)

Vue3.x中是基于proxy来实现的;

let data = {
    msg:'hello',
    count:10
};

let vm = new Proxy(data,{
    get(target,key){
        console.log('get:' + key);
        return target[key];
    },
    set(target,key,value){
        if(value === target[key])return;
        console.log('set:'+key);
        target[key] = value;
        document.querySelector('#app').innerHTML = value;
    }
})

vm.msg = 'hello world';
console.log(vm.msg)

发布订阅模式

发布订阅模式:

  • 订阅者
  • 发布者
  • 信号中心 我们假定,存在着一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行,这就叫做“发布/订阅模式”(publish-subscribe pattern)
class EventEmmit{
    constructor(){
        this.subs = {}
    }

    $on(type,fn){
        this.subs[type] = this.subs[type] || [];
        this.subs[type].push(fn);
    }

    $emit(type){
        if(this.subs[type]){
            this.subs[type].forEach((fn) => fn())
        }
    }
}

let em = new EventEmmit();

em.$on('click',() => {
    console.log('click1')
})

em.$on('click',() => {
    console.log('click2')
})

em.$emit('click');

观察者模式

观察者(订阅者) -- Watcher

  1. update():当事件发生时,具体要做的事情 目标(发布者) -- Dep
  2. subs数组:存储所有的观察者
  3. addSub():添加观察者
  4. notify():当事件发生,调用所有观察者的update()方法 没有事件中心
class Dep{
    constructor(){
        this.subs = []
    }

    addSubs(sub){
        if(sub && sub.update){
            this.subs.push(sub)
        }
    }

    notify(){
        this.subs.forEach((sub) => sub.update())
    }
}

class Watcher{
    update(){
        console.log('watcher')
    }
}

let dep = new Dep();
let watch = new Watcher();

dep.addSubs(watch);
dep.notify();

总结:

  • 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所有观察者模式的订阅者与发布者直接是存在依赖的;
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在

2021-04-19_230035.png

vue响应式原理模拟

vue整体结构:

2021-04-20_125014.png

  • Vue:把data中的成员注入到vue实例,并且把data中的成员转成getter/setter
  • Observer:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知Dep;
  • Compiler:解析每个元素中的指令/插值表达式,并替换成相应的数据
  • Dep:添加观察者(watcher),当数据变化时通知所有观察者
  • Watcher:数据变化更新视图 Vue:
  • 功能:
  1. 负责接收初始化的参数(选项)
  2. 负责把data中的属性注入到Vue实例,转换成getter/setter
  3. 负责调用observer监听data中所有属性的变化
  4. 负责调用compiler解析指令/插值表达式
  • 结构:

2021-04-20_125807.png

  • 实现
  1. 构造器:(1)创建vue实例;(2)保存options参数;(3)挂载传入的data数据,调用this._proxyData方法将data转换为响应式数据(4)根据传入的el参数挂载data转换为响应式数据(4)根据传入的el参数挂载el;(5)调用Observer对象,传入this.$data,监听数据的变化;(6)调用Compiler对象,传入当前的vue实例,解析指令和插值表达式;
  2. _proxyData方法:接收vue实例中的$data对象,遍历对象中的属性,将每个属性用Object.defineProperty转换成getter和setter,并挂载到当前Vue实例上;
class Vue {
    constructor(options) {
        // 通过属性保存选项中的数据
        this.$options = options || {};
        this.$data = options.data || {};
        // 判断options.el是选择器还是document对象
        this.$el =
            typeof options.el === 'string'
                ? document.querySelector(options.el)
                : options.el;
        // 把data中的成员转换成getter/setter,注入到vue实例中
        this._proxyData(this.$data);
        // 调用Observer对象 监听数据得变化
        new Observer(this.$data);
        // 调用Compiler对象,解析指令和插值表达式
        new Compiler(this)
    }

    _proxyData(data) {
        // 遍历data中的所有属性
        Object.keys(data).forEach((key) => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return data[key];
                },
                set(value) {
                    if (value === data[key]) return;
                    data[key] = value;
                },
            });
        });
    }
}

Observer:

  • 功能
  1. 负责把data选项中的属性转换成响应式数据
  2. data中的某个属性也是对象,把该属性转换成响应式数据
  3. 数据变化发送通知
  • 结构

2021-04-20_131559.png

  • 实现
  1. 构造器:接收传入的data对象,并调用walk方法
  2. walk方法:判断传入的参数是不是对象,如果是对象就遍历对象中的所有属性并调用defineReactive方法,将每个属性转换成getter/setter;
  3. defineReactive方法:接收三个参数:对象,属性,和当前属性的值;第三个参数要传value是因为如果此处直接取obj[key]就好发生对象的循环调用,造成堆栈溢出;如果传入的value是对象,则递归调用walk方法,把value对象内部的属性也转换成getter/setter;调用DEP对象,收集依赖并发送通知;在get中收集依赖;在set中,如果新的value是对象,则调用walk将新的value也转换成getter/setter,并发送通知更新视图;
class Observer {
    constructor(data) {
        this.walk(data);
    }

    walk(data) {
        // 判断data是不是对象,不是对象就什么都不做
        if (!data || typeof data !== 'object') return;
        // 遍历data对象的所有属性
        Object.keys(data).forEach((key) => {
            this.defineReactive(data, key, data[key]);
        });
    }
    // 第三个参数要穿value是因为如果此处直接取obj[key]就会发生对象的循环调用,造成堆栈溢出
    // 此处会形成闭包,所有value的值并不会被垃圾回收机制处理掉
    defineReactive(obj, key, value) {
        let that = this;
        // 负责收集依赖并发送通知
        let dep = new Dep();
        // 如果value是对象,则把value内部的属性也转换成响应式数据
        this.walk(value);
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 收集依赖
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue) {
                if (newValue === value) return;
                value = newValue;
                // 如果newValue是对象,则把newValue内部的属性转换成响应式数据
                that.walk(newValue);
                // 发送通知
                dep.notify()
            },
        });
    }
}

Compiler:

  • 功能
  1. 负责编译模板,解析指令/插值表达式
  2. 负责页面的首次渲染
  3. 当数据变化后重新渲染视图
  • 结构

compiler.png

  • 实现
  1. 构造器:保存传入的vue实例;保存vue实例中的$el;调用compiler()方法,传入el,编译模板;
  2. compile方法:编译模板,处理文本节点和元素节点;通过el.childNodes获取el下面的所有子节点;遍历所有的子节点,如果是文本节点,则调用compileText方法处理文本节点,如果是元素节点,则调用compileElement方法处理元素节点;如果当前node节点下面还有子节点,则递归调用compile方法,继续处理
  3. isDirective方法:判断传入的attrName属性是不是指令;vue的指令都是以v-开头的,,所有判断属性是否以v-开头;
  4. isTextNode方法:判断传入的node节点是不是文本节点;node属性中有一个nodeType字段,nodeType=3则是文本节点;
  5. isElementNode方法:判断传入的node节点是不是元素节点;nodeType=1是元素节点;
  6. compileElement方法:接收node节点,获取node节点的所有属性,判断是否是vue指令,如果是vue指令则调用相应方法执行指令任务;
  7. compileText方法:接收文本节点,定义正则表达式,处理插值表达式,调用watcher对象,创建观察者,当数据改变时,更新视图;
  8. update方法:接收node节点,当前属性和当前属性的值,根据属性名称去调用相应的指令处理方法,调用时使用call方法,将this指向当前的compile实例;
  9. textUpdate方法:处理v-text指令,将当前node节点的textContent值改为value,并创建watch对象,监听数据变化更新视图;
  10. modelUpdate方法:处理v-model指令,将当前node节点的value值改为传入的value,并为当前节点监听input事件,实现双向数据绑定;然后创建watch监听,监听数据变化更新视图;
class Compiler {
    constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
        this.compile(this.el);
    }
    // 编译模板,处理文本节点和元素节点
    compile(el) {
        // 获取el下面的所有子节点
        let childNodes = Array.from(el.childNodes);
        childNodes.forEach((node) => {
            // 处理文本节点
            if (this.isTextNode(node)) {
                this.compileText(node);
            }
            // 处理元素节点
            if (this.isElementNode(node)) {
                this.compileElement(node);
            }
            // 判断node节点是否有子节点,如果有子节点,要递归调用compiler
            if (node.childNodes && node.childNodes.length) {
                this.compile(node);
            }
        });
    }
    // 编译元素节点,处理指令
    compileElement(node) {
        // console.dir(node.attributes);
        // 获取元素节点所以的属性
        let attributes = Array.from(node.attributes);
        // 遍历所有属性
        attributes.forEach((attr) => {
            // 判断属性是不是指令(是不是以v-开头)
            if (this.isDirective(attr.name)) {
                let attrName = attr.name.substr(2);
                let attrValue = attr.value;
                this.update(node, attrValue, attrName);
            }
        });
    }
    update(node, key, attrName) {
        let updateFn = this[attrName + 'Update'];
        // 调用call方法将updateFn执行时的this指向当前compiler对象
        updateFn && updateFn.call(this,node, this.vm[key],key);
    }
    // 处理v-text指令
    textUpdate(node, value,key) {
        node.textContent = value;
        new Watcher(this.vm,key,(newValue) => {
            node.textContent = newValue;
        })
    }
    // 处理v-model指令
    modelUpdate(node, value,key) {
        node.value = value;
        new Watcher(this.vm,key,(newValue) => {
            node.value = newValue;
        })
        //给node添加input事件,实现双向数据绑定
        node.addEventListener('input',() => {
            this.vm[key] = node.value;
        })
    }
    // 编译文本节点,处理差值表达式
    compileText(node) {
        // 定义正则表达式,匹配差值表达式 {{  }}
        let reg = /\{\{(.+?)\}\}/;
        // 获取文本节点的内容
        let value = node.textContent;
        if (reg.test(value)) {
            let key = RegExp.$1.trim();
            node.textContent = value.replace(reg, this.vm[key]);

            new Watcher(this.vm,key,(newValue) => {
                node.textContent = newValue;
            })
        }
    }
    // 判断元素属性是否是指令
    isDirective(attrName) {
        // 判断属性是否以v-开头,vue中的指令都是以v-开头的
        return attrName.startsWith('v-');
    }
    // 判断节点是否是文本节点
    isTextNode(node) {
        // node属性中的nodeType = 3是文本节点
        return node.nodeType === 3;
    }
    // 判断节点是否是元素节点
    isElementNode(node) {
        // node属性中的nodeType = 1是元素节点
        return node.nodeType === 1;
    }
}

Dep:

  • 功能
  1. 收集依赖,添加观察者(watcher)
  2. 通知所有观察者
  • 结构

dep.png

  • 实现
  1. 构造器:创建subs数组,存储所有的观察者
  2. addSub方法:添加观察者
  3. notify方法:发送通知
class Dep{
    constructor(){
        // 存储所有的观察者
        this.subs = []
    }
    // 添加观察者
    addSub(sub){
        if(sub && sub.update){
            this.subs.push(sub)
        }
    }
    // 发送通知
    notify(){
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

Watcher:

watcher1.png

  • 功能
  1. 当数据变化触发依赖,dep通知所有的Watcher实例更新视图
  2. 自身实例化的时候往Dep
  • 结构

watcher.png

  • 实现
  1. 构造器:结受vue实例,key和一个回调函数,并保存到当前实例中;把当前的watcher对象记录到Dep类的target属性上;触发get方法保存oldValue;添加完成之后清空Dep的target属性,防止重复添加;
  2. update方法:当数据发送变化时,调用回调函数更新视图;
class Watcher{
    constructor(vm,key,cb){
        this.vm = vm;
        // data中的属性名称
        this.key = key;
        // 回调函数
        this.cb = cb;
        // 把watcher对象记录到Dep类的静态属性target
        Dep.target = this;
        // 触发get方法,在get方法中会调用addSub;
        this.oldValue = vm[key];
        // 添加完之后清空Dep的target属性
        Dep.target = null;
    }
    // 当数据发送变化时更新视图
    update(){
        let newValue = this.vm[this.key];
        if(newValue === this.oldValue) return;
        this.cb(newValue);
    }
}

拉钩教育 -- 大前端学习笔记