Vue响应式、双向绑定原理即代码实现

10 篇文章 0 订阅

一、Vue响应式原理图:

在这里插入图片描述

二、Vue响应式原理

Vue是采用数据劫持配合发布者-订阅者模式的方式实现响应式的。通过Object.defineProperty 来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器Dep, 去通知观察者Watcher,由Watcher执行更新视图的回调函数。
MVVM的实现整合了Observer、CompiIe和watcher三者。他通过Observer 来劫持监听model数据,通过Compile来解析编译模板指令并对数据绑定对应的观察者,最终利用Watcher搭起Observer、CompiIe之间的通信桥梁,以达到数据变化 → 视图更新;视图交互变化 → model数据变更的双向绑定效果。

三、Vue相应式原理图解

在这里插入图片描述
第 1 步:Observer内通过Object.defineProperty对数据进行劫持,并绑定getter和setter。
getter用于返回数据并通知Dep收集订阅器。
setter用于设置新值并通知Dep数据变化
第 2 步:Compile对指令(v-text、v- html、{{ }} 等)进行解析,并准备初始化页面。
第 3 步:初始化页面前需要执行3,实例化Watcher,为即将渲染到页面的数据绑定自身对应的watcher(watcher中有更新视图的回调函数)。
第 4 步:拿数据时会触发getter,getter将数据返回用于渲染页面,并执行4-1,告诉Dep收集之前为该数据绑定的watcher,然后Dep就执行4-2去收集watcher。
第 5 步:当数据变化时会触发setter,setter为数据设置新值,并执行5,告诉Dep数据变化了,Dep又通知Watcher数据变化了,又watcher执行回调去更新页面。

数据变化响应视图: 当数据变化时会触发setter,setter为数据设置新值,并执行5,告诉Dep数据变化了,Dep又通知Watcher数据变化了,又watcher执行回调去更新页面。
据变化了, Wat ( her 会执行回调更新页面。
视图变化响应数据: 当视图变化时,触发事件( 如input 标签的input 事件)对数据进行改变,数据改变会触发setter,然后又执行第5步 。

四、Vue响应式代码模拟实现(不到300行)

1.Vue入口(25行)
实例化Observer、Watcher等功能模块,并对data、methods、computed进行代理,即实现【this.数据】而不是【this.data.数据】、【this.方法】而不是【this.methods.方法】等。

// Vue入口
class MVue {
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
            // 1.实例化数据劫持监听--Object.defineProperty
            new Observer(this.$data);
            // 2.实例化指令解析器
            new Compile(this.$el, this);
            // 对data进行代理实现 this.person.name。methods、computed的代理实现雷同。
            this.proxyData(this.$data);
        }
    }
    // 对data中定义的数据进行代理,相当于挂载到了vm实力上,可以直接【this.数据】来使用,而不用【this.data.数据】
    proxyData(data) {
        for (const key in data) {
            // 对data第一层做劫持绑定get和set。this指向vm
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(newVal) {
                    data[key] = newVal;
                }
            })
        }
    }
}

2.Observer数据劫持和监听的实现(44行)
Observer主要是对数据进行劫持监听,通过Object.defineProperty对数据设置set和get,get返回数据并通知收集watcher,set设置数据并通知Dep数据变化。这里还实现了watcher的收集器Dep,Dep主要又两个功能,收集watcher和通知watcher数据变化。

// 通知watcher数据变化需更新视图 和 收集每个数据对应watcher的作用
class Dep {
    constructor() {
        this.subs = []
    }
    // 收集观察者
    addSub(watcher) {
        this.subs.push(watcher);
    }
    // 通知观察者更新视图
    notify() {
        console.log(this)
        this.subs.forEach(w => w.update());
    }
}
// 劫持监听数据
class Observer {
    constructor(data) {
        this.observer(data);
    }
    // 观察对象数据
    observer(data) {
        if (data && typeof data === 'object') {
            Object.keys(data).forEach((key) => {
                // 劫持监听数据
                this.defineReactive(data, key, data[key]);
            })
        }
    }
    // 劫持监听数据
    defineReactive(data, key, value) {
        // 递归遍历 每一层
        this.observer(value);
        // 实例化Dep存储watcher和通知watcher更新视图。
        const dep = new Dep();
        // 劫持每个属性,Object.defineProperty(目标对象,劫持的键值,键值对应的属性的特性)
        Object.defineProperty(data, key, {
            enumerable: true, // 可以被枚举(使用for...in或Object.keys())
            configurable: false, // 是否可再次被设置属性特性
            get() { // 获取属性值
                // 得到数据的同时向Dep中添加观察者订阅器(每个数据都有自己的watcher)
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newValue) => { // 修改属性,使用箭头函数获取定义上下文的this,普通函数this指向了传进来的data
                // 防止新修改的属性不被劫持监听
                this.observer(newValue);
                if (newValue !== value) {
                    value = newValue;
                    // 数据修改通知对应watcher
                    dep.notify();
                }
            }
        })
    }
}

3.Compile指令解析初始化页面的实现(137行)
Compile主要实现了对指令的解析、数据绑定watcher、初始化页面的功能。其中包含:指令解析的工具类compileUtil、初始化页面工具类updater、Compile自身。已实现的解析指令有v-text、v-html、v-model、v-bind、v-on、{{}}、:、@

// 具体指令解析操作
const compileUtil = {
    // node当前节点,expr指令值,vm实例
    text: function (node, expr, vm) { // v-text 解析
        // 插值表达式单独处理
        if (expr.indexOf('{{') !== -1 && expr.indexOf('}}') !== -1) {
            //  获取插值表达式对应的值{{person.name}}---{{person.age}}
            const value = expr.replace(/\{\{(.+?)\}\}/g, (...arg) => {
                new Watcher(vm, arg[1], () => {
                    // 插值表达式可能是复杂的,比如同时有多个,不能只处理一个就替换,所以需要单独处理
                    return updater.textUpdater(node, this.getInterpolation(expr, vm));
                })
                return (this.getVal(arg[1], vm));
            })
            // 渲染页面
            return updater.textUpdater(node, value);
        }
        // 绑定watcher,订阅数据变化
        this.bindWatch(node, expr, vm, 'text');
    },
    getInterpolation(expr, vm) {
        // 插值表达式添加watcher做单独处理
        return expr.replace(/\{\{(.+?)\}\}/g, (...arg) => {
            return (this.getVal(arg[1], vm));
        })
    },
    html: function (node, expr, vm) { // v-html 解析
        this.bindWatch(node, expr, vm, 'html');
    },
    model: function (node, expr, vm) { // v-model 解析
        this.bindWatch(node, expr, vm, 'model');
        // 视图 → 数据 → 视图
        node.addEventListener('input', (e) => {
            this.setVal(expr, vm, e.target.value)
        }, false);
    },
    bind: function (node, expr, vm, attrName) { // v-bind 和 : 解析
        // expr:指令值,  attrName:绑定的属性名
        this.bindWatch(node, expr, vm, 'bind', attrName);
    },
    on: function (node, expr, vm, eventName) { // v-on 和 @ 解析
        // expr:方法名,  eventName:事件
        // 获取事件方法
        const fun = vm.$options.methods && vm.$options.methods[expr];
        // 小细节:vue的methods中的方法this默认指向vm实例,而这里fun将被node调用,所以this是指向node节点的。
        node.addEventListener(eventName, fun.bind(vm), false)
    },
    bindWatch: function (node, expr, vm, directive, attrName) {
        // node:节点, expr:指令值(data), vm:实例, directive:指令种类, attrName:绑定属性值(v-bind和: 才有)
        // 指令触发的时候需要为其添加watcher的回调订阅
        const updaterFn = updater[directive + 'Updater'],
            value = this.getVal(expr, vm); // 获取对应的渲染函数,获取指令绑定数据
        // 触发渲染
        updaterFn && updaterFn(node, value, attrName);
        new Watcher(vm, expr, (newVal) => {
            updaterFn && updaterFn(node, newVal, attrName); // 数据更新,watcher回调,更新视图
        })
    },
    getVal: function (expr, vm) {
        // expr形式可能是msg也可能是person.name,所以需要遍历获取expr对应的vm实例data中的值
        return expr.split('.').reduce((data, current) => {
            return data[current];
        }, vm.$data)
    },
    setVal: function (expr, vm, inputVal) {
        let base = vm.$data; // 所有数据取值都是从这里开始的
        expr = expr.split('.'); // 为了遍历方便将其分割成数组
        expr.forEach((value, index) => {
            // 形如:person.son.name这种嵌套对象,只有当到达最后一层时,才将新值赋值。
            if (index < expr.length - 1) {
                base = base[value]
            } else {
                base[value] = inputVal; // 更新新值
            }
        })
    }
}
// 指令解析后更新页面
const updater = {
    // node当前节点,value当前值
    textUpdater: function (node, value) { // v-text 渲染
        // 当value不存在时  应该显示 '' 而不是undefined
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    htmlUpdater: function (node, value) { // v-html 渲染
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },
    modelUpdater: function (node, value) { // v-model 渲染
        node.value = typeof value == 'undefined' ? '' : value;
    },
    bindUpdater: function (node, value, attrName) { // v-bind 渲染
        node.setAttribute(attrName, value);
    }
}
// 指令解析器
class Compile {
    // 接收根节点对象el,和vm实例
    constructor(el, vm) {
        // 得到元素节点
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        // vm实例
        this.vm = vm;
        // 1.为了避免频繁操作dom,造成回流重绘等浪费性能问题,这里使用文档碎片存储dom
        const fragment = this.nodeFragment(this.el);
        // 2.对fragment进行模板编译,处理指令、插值表达式、事件等,有fragment就减少了dom操作
        this.compileInit(fragment);
        // 3.将文档碎片插入到dom中
        this.el.appendChild(fragment);
    }
    // 对fragment进行模板编译
    compileInit(fragment) {
        // 获取文档碎片下的所有子节点
        const childNodes = fragment.childNodes;
        [...childNodes].forEach(child => {
            if (this.isElementNode(child)) {
                // 是元素节点
                this.compileElement(child);
            } else if (this.isTextNode(child)) {
                // 是文本节点
                this.compileText(child);
            }
            if (child.childNodes && child.childNodes.length) {
                // 元素嵌套的递归
                this.compileInit(child)
            }
        })
    }
    // 编译元素节点
    compileElement(node) {
        const nodeAttr = node.attributes;
        [...nodeAttr].forEach(attr => {
            // 解构属性值和名
            const {
                name,
                value
            } = attr;
            if (this.isDirective(name)) {
                // 是否是指令    
                // directive 值为 text, html, model, on:click, bind:XXX 仅做这几种
                const [, directive] = name.split('-');
                const [dirName, eventName] = directive.split(':'); // ['text','']['html','']['model','']['on','click']……
                // 对应指令的解析事件,传递(当前节点,指令值,vm实例,事件)
                compileUtil[dirName](node, value, this.vm, eventName)
                // 删除标签上的 v-html v-text 之类的属性
                node.removeAttribute('v-' + directive);
            } else if (this.isAtSymbol(name)) { // 是否是@开头的指令
                const [, eventName] = name.split('@');
                // 执行v-on 解析
                compileUtil['on'](node, value, this.vm, eventName)
                // 删除标签上的 @开头的  属性
                node.removeAttribute('@' + eventName);
            } else if (this.isColonSymbol(name)) {
                const [, attrName] = name.split(':');
                // 执行v-bind 解析
                compileUtil['bind'](node, value, this.vm, attrName)
                // 删除标签上的 :开头的 属性
                node.removeAttribute(':' + attrName);
            }
        })
    }
    // 编译文本节点
    compileText(node) {
        // 匹配所有 带有“{{}}”进行解析
        const content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
            // 调用解析
            compileUtil['text'](node, content, this.vm)
        }
    }
    // 判断是否是以“v-”开头的属性
    isDirective = (directive) => directive.startsWith('v-');
    // 判断是否以“@”开头
    isAtSymbol = (directive) => directive.startsWith('@');
    // 判断是否以“:”开
    isColonSymbol = (directive) => directive.startsWith(':');
    // 将dom文档碎片化
    nodeFragment(el) {
        // 创建文档碎片
        const f = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            // 每次使用appendChild()方法后el节点的firstChild会被移除
            // 这不是死循环!!!
            f.appendChild(firstChild);
        }
        return f;
    }
    // 判断是不是元素节点
    isElementNode = (node) => node.nodeType === 1;
    // 判断是不是文本节点
    isTextNode = (node) => node.nodeType === 3;
}

4.Watcher订阅数据更新视图的实现(20行)
Watcher主要功能为获取数据旧值以及更新视图的功能函数。

// 观察者watcher,作用更新视图
class Watcher {
    constructor(vm, expr, callback) {
        this.vm = vm; // 实例
        this.expr = expr; // 被观察的数据
        this.callback = callback; // 回调函数更新视图
        this.oldVal = this.getOldVal() // 旧值
    }
    // 获取旧值,使用指令解析中的compileUtil中的getVal方法
    getOldVal() {
        // 观察者被创建时会收集旧值,此时将该变量的watcher实例挂到Dep上。
        Dep.target = this; // this指向Watcher的实例
        const oldVal = compileUtil.getVal(this.expr, this.vm);
        delete Dep.target; // 旧值获取完毕清除Dep上的watcher
        return oldVal;
    }
    // 更新视图 -- 新值和就值是否有变化,有执行回调更新视图
    update() {
        // 数据变化后,Dep通知watcher 更新视图,所以此处获取到的是新值。
        const newVal = compileUtil.getVal(this.expr, this.vm);
        if (newVal !== this.oldVal) {
            // 新值和就值不同时,更新视图。
            this.callback(newVal);
        }
    }
}

五、测试使用

 	<div id='app'>
        <h1>插值表达式:{{person.name}}---{{person.age}}</h1>
        <div>插值表达式:{{msg}}</div>
        <p v-text="msg"></p>
        <ul>
            <li>文本1</li>
            <li>插值表达式:{{msg}}</li>
            <li>文本2</li>
        </ul>
        <div v-html="htmlMsg"></div>
        <input type="text" v-model="person.age">
        <input type="text" v-model="msg">
        插值表达式:{{person.name}}
        <button v-on:click="handle">v-on绑定事件→点我</button>
        <button @click="handle">@绑定事件→点我</button>
        <div v-bind:title="msg">v-bind绑定title</div>
        <div :title="msg">:绑定title</div>
        {{msg}}
    </div>
    <script src="./Watcher.js"></script>
    <script src="./Observer.js"></script>
    <script src="./Compile.js"></script>
    <script src="./MVue.js"></script>
    <script>
        let vm = new MVue({
            el: '#app',
            data: {
                person: {
                    name: '张三',
                    age: 19
                },
                htmlMsg: '<strong><i>我是v-html</i></strong>',
                msg: 'vue双向绑定',
            },
            methods: {
                handle: function () {
                    console.log(this)
                    // this.person.name='李四'
                    alert('点击事件触发!我将修改数据');
                }
            }
        })
    </script>

到这里就模拟实现了vue的响应式、双向绑定,只是模拟,不要较真。。。

如果对你有帮助可以点赞👍+收藏哦~~~我们一起学前端。

以上内容均为原创,转载请注明来源!!!!

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值