Vue响应式原理探究及源码实现

Vue响应式原理

一.简介

Vue响应式的原理,其实就是基于ES5的Object静态方法: Object.defineProperty() 对这个方法做劫持,还有说法是代理,劫持数据的setter和getter.然后结合发布订阅模式,在数据发生变化的时候,通知页面进行更新

注意:由于 ES5的Object.defineProperty()方法不支持IE8,所以我们的Vue不兼容IE8以下版本

二.实现响应式源码

1. Object.defineProperty()介绍

MDN:方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, property, descriptor)

  • obj: 要在其定义属性的对象
  • property 要定义或修改的属性的名字
  • descriptor 将被定义或修改的属性描述符(对象)

descriptor描述符有好几个选项可以设置的,我们这里只讲对象属性访问器 Getter和Setter

    <!-- 
       浏览器控制台
         obj.name = 'zhangssan' ====> 监听到对象在设置新值
         obj.name               ====> 监听到对象在取值
    -->
    <script>
        var obj = {
            name: 'liuqiao',
            age: 27
        }

	   //返回这个对象
       var newObj = Object.defineProperty(obj, 'name', {
            get: function () {
                console.log('监听到对象在取值');
                return name;
            },
            set: function (newValue) {
                console.log('监听到对象在设置新值');
            }
        });

        console.log(newObj);
    </script>

上述代码:我们定义了一个对象,然后通过Object.defineProperty()做数据劫持 劫持get和set方法,当我们改变对象中的一个值的时候, set方法会监听到,数据的变化,当取值的时候,get方法会监听到在取数据

在这里插入图片描述

2.实现Vue响应式源码

要实现响应式源码,我们需要构造三个类,负责各自的功能:

1.Vue类 创建Vue的构造函数

2.Compile类 创建专职页面解析的构造函数

3.Watch类 发布订阅模式,监听数据的改变和页面的变化

扫描二维码关注公众号,回复: 11355705 查看本文章
2.1 Object.keys()

返回指定对象中所有可枚举属性组成的数组

var data={name:'liuqiao',age:'27'};
var newObj=Object.keys(data)
console.log(newObj); //['name','age']
2.2 Object.values()

方法返回一个数组,成员是参数对象自身的所有可枚举属性的值(与Object.keys配套)

var data={name:'liuqiao',age:'27'};
var newObj=Object.values(data)
console.log(newObj); //['liuqiao','27']
2.3 Vue类的创建(创建一个Vue的构造函数)

支持响应式的核心源码,都依赖于Object.defineProperty()方法

  		/**
        *伪代码
        * var vm=new Vue({
        *     el:"#app",
        *     data:{
        *         name:'liuqiao',
        *         age:27
        *     }
        * });
        */

        //创建一个Vue的构造函数或者类
        class Vue {
            //构造函数
            constructor(optinons) {
                // 实例化Vue时传的是对象
                this.$el = document.querySelector(optinons.el);
                this.$data = optinons.data;

                //实现代理属性,使得data中的数据能被劫持到
                this.observer(this.$data);

                //生成一个Watch实例
                this._ev = new Watch();

                // 解析
                new Compile(this.$el, this);
                
            }

            /**
             * 将传递过来的对象中的属性直接绑定到实例对象上
             *
             * observer(data)
             */
            observer(data) {
                /*
                *  我们需要遍历data这个对象 data:{name:'liuqiao',age:27}
                *  可以使用Object.keys()
                */
                Object.keys(data).forEach(key => {
                    // this  当前对象
                    // key  属性的名字
                    // {}   将被定义或修改的属性描述符(对象)
                    Object.defineProperty(this, key, {
                        // 使用getter和setter监听数据的变化,也就是数据劫持
                        get() {
                            console.log("监听:正在进行取值操作");
                            //返回正在操作的数据
                            return data[key];
                        },
                        set(value) {
                            console.log("监听:正在进行赋值操作");
                            data[key] = value;
                            
                            // 数据更新了, 触发事件
                            this._ev.$emit(key)
                        }
                    });
                });
            }
        }
2.4 Compile类创建(创建一个页面解析的构造函数)

实现了响应式,我们做一个页面解析的操作,比如将 {{}} 双花括号解析需要成输出的值

   //创建一个Compile的构造函数(专职于页面解析工作)
        class Compile {
            /**
             *
             * @param {DocumentFragment} el DOM 对象
             * @param {Vue} vm Vue 实例对象
             */
            constructor(el, vm) {
                // 将vm绑定到当前Compile的实例对象上
                this.vm = vm;
                this.compile(el);
            }

            compile(el) {
                //遍历el这个Dom对象子节点
                el.childNodes.forEach(node => {
                    //得到所有的文本节点,在Dom中,文本节点的nodeType==3
                    //并且处理节点的文本内容,内容类似 {{ name }} 正则处理
                    if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)) {
                        console.log(node.textContent);
                        // 获取到匹配到的文本节点之后,直接将内容修改
                        // {{name}}  ===>liuqiao
                        // {{ age}}  ===>28

                        // 1.得到{{}}表达式
                        //RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
                        let exp = RegExp.$1.trim();
                        // 2.将对应的exp数据设置到当前节点的textContent上
                        node.textContent = this.vm[exp];
                        console.log(node.textContent);
                        console.log(this.vm);

                        // 3. 监听 this.vm === new Vue 的实例
                        this.vm._ev.$on(exp, () => {
                            node.textContent = this.vm[exp]
                        })
                    }

                    //递归遍历所有的子节点,找完为止
                    if (node.childNodes) {
                        this.compile(node);
                    }
                });
            }
        }
2.5 Watch类创建 (创建一个监听数据数据变化通知页面的构造函数)
		//创建一个Watch类 监听数据变化
        class Watch {
            constructor() {
                //存储消息
                this.dep = {};
            }

            //订阅消息
            /**
            * @param {eventName} 事件名字
            * @param {callback} 回调函数
            */
            $on(eventName, callback) {

                if (!this.dep[eventName]) {
                    this.dep[eventName] = [callback];
                }
                else {
                    this.dep[eventName].push(callback);
                }
            }

            //发布消息
            /**
            * @param {eventName} 事件名字
            * @param {payload} 发布消息内容
            */
            $emit(eventName, payload) {
                if (this.dep[eventName]) {
                    this.dep[eventName].forEach(cb => {
                        cb(payload);
                    });
                }
            }
        }
2.6 测试源码
 	   var vm = new Vue({
            el: '#app',
            data: {
                name: 'liuqiao',
                age: '27'
            }
        });

浏览器控制台,修改vm.name的值,会发现页面也跟着变化了,我们的Vue响应式源码就完成了,当然了,核心的内容思想是这样子的,只是比较简易版的响应式源码

三.面试中的Vue响应式原理

面试中,经常会有面试官问到这个Vue的响应式原理,我自己总结了一下,可以这么回答:

Vue之所以具有响应式原理,是因为我们的Vue对象在创建的时候,使用了Object.defineProperty()这样的一个ES5的的Object静态方法.把data数据中的所有数据进行遍历,进行劫持,然后进行getter和setter的封装,当我们Vue实例上的data数据发生变化的时候,就会触发setter的操作,在setter的操作中,我们可以触发监听器去更新真实的dom.

这个阶段是发生在Vue生命周期的beforeCreate和created之间的.

四,扩展Vue3响应式写法

Vue3的响应式写法,是基于es6的proxy代理器,可以拦截作用

Proxy: 在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截

 		 var data = {
            name: 'liuqiao',
            age: 27
        }

        var newObj = new Proxy(data, {
            // 重写数据以在中间创建一个代理
            get: function (obj, key) {
                console.log('监听到对象在取值');
                return name;
            },
            set: function (obj, key, newVal) {
                console.log('监听到对象在设置新值');
            }
        });
        console.log(newObj);

猜你喜欢

转载自blog.csdn.net/liuqiao0327/article/details/106741176