• Vue2.0 —— 由设计模式切入,实现响应式原理


    Vue2.0 —— 由设计模式切入,实现响应式原理

    《工欲善其事,必先利其器》

    既然点进来了,麻烦你看下去,希望你有不一样的收获。

    大家好,我是vk,好久不见,今天我们一起来盘一盘关于 Vue2.0 的响应式原理。注意,响应式才是 Vue 的核心,而双向绑定则是指 v-model 指令。所以一般面试时候面试官都会问你,能否讲讲响应式原理,或者简单实现一个数据的双向绑定。而不是让你实现一个双向绑定原理。

    所以这时候,如果面试官问你,能否简单实现一下 Vue 的响应式?

    你应该答:好的,等我10分钟,我先去看一下 vk 哥写的文章。(狗头)

    所谓MVVM框架,即是数据驱动视图模型,分为 ModelViewViewModel。目前市场上三大框架,只有 Vue 是百分之百应用了 MVVM 框架,所以它的核心实现和思想值得我们学习。

    有的小年轻这时候就要肝了,你个辣鸡,Vue3.0 都出了那么久了,现在才来讲 Vue2.0 ,有个屁用?出来混,是要讲…
    我只能说,小年轻,你还小,有些事,你不懂…
    开玩笑
    好的,其实一方面是由于自己个人原因,没办法经常写文章。其次,不管是 Vue2.0 还是 3.0,只要是源码,它就有学习的价值。不是说,你用什么,就学什么;而是,我想学什么,我就去研究什么。这是一个主观的意向,我的建议就是不要被动的去学习,那样成长是很缓慢的,而且很容易记不住。

    另外,更完这篇 2.0 的原理,下一篇就研究更新 3.0 的原理,这样一有对比,岂不美哉?(狗头)

    一、分析

    我们都知道,Vue 在改变数据时,会自动刷新页面的 DOM。同样,我们在页面输入数据,VueModel 层数据也会随之变化,这就是 Vue 的特性 —— 响应式原理。

    最经典的例子就是输入框输入数据和其数据回显,任何一个地方改变数据,对应的数据都会发生改变,实现了数据的双向绑定。那么这到底是怎么做到的呢?我们来看一张图:
    vue双向绑定原理

    通过上面官方的图解我们可以理解并得到以下几点结论,我先把结论给你放出来了,尝试理解一下。如果不懂,没关系,我们后面会继续剖析它的原理乃至实现它:

    1. 组件实例化虚拟 DOM 时,如果需要访问我们 Data 中的数据 a,那么我们就会先 new Watcher 一个实例,在 Watcher 实例中获取这个 a 属性的值,并进行观察,这个过程就叫 Touch 我们的 getter 以获取数据。此时设置 Dep.target = this,即指向该 Watcher,保持全局唯一性。
    2. 根据 Dep.target = this 的全局唯一性,我们使用 Object.defineProperty 对数据进行拦截,设置我们的 gettersetter ,如上图。此时 getter 里面,若 Dep.targettrue ,我们通知收集器 Dep 把当前 this (即当前 Watcher )收集起来,以通知更新备用,同时返回该属性的值。
    3. 这时候再回到 Watcher,值返回以后,为防止其他依赖(即其他 Watcher)触发 getter的同时把我们这个 Watcher 又收集回去,我们需要把 Dep.target 设置为 null 。这就避免了不停的绑定 WatcherDep,造成代码死循环。

    鉴于 DepWatcher 两者之间这种微妙的关系,其实我们可以发现,这就是典型的应用了 —— 发布/订阅者设计模式

    设计模式的本质就是使代码解耦,实现低耦合,形成代码的高可读性、高可重用性和高度扩展性;
    这些特点对于一个库或者框架来说显得尤为重要。

    以上就是响应式的收集依赖的过程了,这时候你千万不要懵,好戏才刚刚开始,我们开始剖析 —— 响应式

    二、理解 Object.defineProperty

    先通过几段了解一下 Object.defineProperty 这个 API:

    const data = {}
    cosnole.log(data)
    // 输出 {Prototype: object}
    
    • 1
    • 2
    • 3

    我们根据输出的结果,可以看到 data 里面现在只有一个原型对象。当我们为 data 添加或修改属性时:

    let name = '张三'
    data.name = name
    console.log(data)
    // 输出 {name: '张三', Prototype: object},这个也是很明显就可以理解的
    
    • 1
    • 2
    • 3
    • 4

    对于添加或修改对象属性,有时候我们也可以用到 Object.defineProperty 这个 API。先看一下官方的定义:
    定义
    那我们按照 MDN 文档,应用一下:

    const data = {}
    let person = '张三'
    Object.defineProperty(data, "name", {
    	get: function() {
    		console.log("get")
    		return person
    	},
    	set: function(newValue) {
    		console.log("set")
    		value = newValue
    	}
    })
    console.log(data)
    // 输出 {name: '张三', get: function() {}, set: function(newValue) {}, Prototype: object}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    通过该 API 新增对象属性,我们可以观察到,跟直接添加对象属性相比较,多了 gettersetter 两个内置函数,分别用来拦截调用属性值和修改操作属性。

    // 根据上面的 API,这时我们来修改 data 的属性
    data.name = "李四"
    console.log(data.name)
    // 输出 set,说明修改属性值触发了 set 方法
    // 输出 get,说明调用属性值触发了 get 方法
    // 输出 李四,说明属性值已被修改
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这说明,Object 的可能性一下子就被打开了,利用这个 API 可以达到我们前面提到的设计模式的特点。

    插一句题外话,在 Vue2.0 发布的时候,Proxy 其实已经诞生了。很多人疑惑为什么尤大大不使用 Proxy?其实是因为当时的前端环境还并没有完全支持这个 API。很多浏览器除了几个主流的,基本上都还没有适配上。所以,尤大大为了用户群体考虑,选择了 Object.defineProperty,而放弃了 Proxy。直到今天,前端环境对 Proxy 友好了,Vue3.0 也就天然适配了 Proxy。

    由此可见,我们可以往 Object.defineProperty 里面添加很多东西,例如:数据的监听、数据的加工、数据的计算、数据的判断等等非常非常多的工作。这也被业界称之为非常经典的 —— 《Vue的数据劫持》。

    但是,该API有弊端。
    对于已进行数据劫持的对象,他在新增属性时候,并不会为新属性绑定 settergetter
    对于已进行数据劫持的对象,他在删除属性的时候,并不会触发 setter

    三、浅析 Vue2.0 的响应式

    通过上面的分析我们大概了解到,Vue 的数据劫持是怎么实现的。

    这个时候其实很重要昂,数据劫持只是响应式的其中一环罢了。不过现在需要继续摸索,层层递进,我带你模拟一下响应式的简易的过程(由于篇幅原因,就不做太多的引导了,直接全部代码展示,希望你多跟着敲几遍,把它理解透):

    1. 我们需要一个入口,来传入以及分析数据类型,创建 observe.js 文件:
    import Observer from "Observer.js";
    // 监听对象属性
    export default function observe(value) {
    	// 判断是基本数据类型或者引用数据类型 object
    	if (typeof value != "object") return;
    	let ob;
    	// 判断这个对象或属性是否携带有响应式的标识
    	if (typeof value.__ob__ != "undefined") {
    		ob = value.__ob__;
    	} else {
    		ob = new Observer(value);
    	}
    	return ob;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 再次细化颗粒度,精确到每个对象属性或其子属性,给属性赋予拦截操作,类(class)是不二之选。不过在这之前,我们需要新增一个工具函数文件,用来给对象或其属性添加 __ob__ 响应式标识。创建 utils.js 文件:
    /**
     * @param {Object} obj 需要绑定的对象
     * @param {String} key 需要绑定的属性名
     * @param {Any} value 需要绑定的属性值
     * @param {Boolean} enumerable 绑定的属性是否可枚举
    */
    export default function def(obj, key, value, enumerable) {
    	Object.defineProperty(obj, key, {
    		value,
    		enumerable,
    		writable: true,
    		configurable: true
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 创建 Observer.js 文件,设置颗粒度拦截:
    import { def } from "utils.js";
    import observe from "observe.js";
    import defineReactive$$1 from "defineReactive.js";
    export default class Observer {
    	constructor(value) {
    		// 绑定响应式标识 __ob__ 属性
    		def(value, "__ob__", this., false);
    		// 实现拦截
    		this.walk(value);
    	}
    	walk(data) {
    		for (let k in data) {
    			defineReactive$$1(data, k);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 创建 defineReactive.js 文件,实现数据的拦截:
    /**
     * defineReactive.js
     * 该函数用于实现数据劫持
     * @param {Object} data 需要实现数据劫持的对象
     * @param {String} key 被劫持对象的属性名
     * @param {Any} value 被劫持对象的属性值
     * @return {Any} value 被劫持对象添加监听后的属性值
    */
    export default function defineReactive$$1(target, key, value) {
    	if (arguments.length == 2) value = target[key]; 
    	// 深度遍历监听,因为对象的子属性也可能是一个 object
    	observer(value);
    	
    	// 调用核心 API
    	Object.defineProperty(target, key, {
    		enumerable: true,
    		configurable: true,
    		get() {
    			return value;
    		},
    		set(newValue) {
    			if (newValue !== value) {
    				value = newValue;
    				// 同样的也是深度遍历监听,判断新的属性值是否也是 object
    				observer(newValue);
    				// 打印一下,方便我们后面监听数组
    				console.log("视图更新");
    			}
    		}
    	})
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    然后,利用 observer 测试监听一个对象:

    const obj = {
    	name: '张三',
    	age: 20
    }
    observer(obj);
    console.log(obj);
    // 输出 {name: '张三', age: 20, get age fn(), set age fn(), get name fn(), set name fn(), Prototype: object}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    数据劫持
    我们观察到对象分别添加了 age > getterage > settername > gettername > setter,说明我们针对 obj 的数据劫持已经成功监听到了。

    不过,接下来,我们还需要测试一下,当我们分别新增属性、修改属性和删除属性,是否会触发视图刷新函数:

    // 测试新增属性
    obj.idcard = 123456
    
    // 测试修改属性
    obj.age = 18
    
    // 测试删除属性
    delete obj.age
    console.log(obj)
    // 输出 视图更新
    // 输出 {name: '张三', idcard: 123456, get name fn(), set name fn(), Prototype: object}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    现在我们可以观察到,执行完语句的对象 obj,只剩下了 name > gettername > setter。而且,触发视图刷新的,是我们在修改 age 属性的过程中触发的。这就说明,新增的属性,并不会触发视图刷新,也不会被劫持数据监听。删除属性,也不会触发视图刷新。
    测试新增和删除属性

    看到这里,我相信你应该已经挺兴奋的了。因为你已经距离能自己手动实现一个双向绑定不远了。但,我希望细心的朋友可以发现,整个数据劫持的过程中,利用 setter 来触发视图刷新,这种设计手法相当于什么?
    没错,它就是我们平常所了解的 —— 观察者模式

    这时候,有人问了:你这写的不严谨。你的属性都是基本数据类型,根本没提到引用数据类型数组要怎么处理啊!你个辣鸡!!!
    啊啊啊啊啊
    小伙子,我很佩服你的勇气。

    紧接着,我们继续测试,如果对象属性的值是引用类型的情况下,observe 的表现如何:

    const obj = {
    	name: '张三',
    	age: 20,
    	hobby: ['唱', '跳', 'rap'],
    	address: {
    		province: '广东省',
    		city: '深圳市',
    		district: '福田区'
    	}
    }
    
    observe(obj);
    
    obj.address.district = '南山区'
    obj.hobby.push('篮球')
    
    console.log(obj);
    // 输出 视图更新
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    咦?!为什么只输出了一个视图刷新???明明 hobby 也生成了 settergetter 啊!不应该是刷新两次吗???
    自闭了
    原因就是,虽然我们封装的 defineReactive$$1 可以监听到这个属性值,但是,并不具备监听数组更新的能力。

    得,又是一个坑。

    其实,Vue2.0 通过这个API实现响应式,还是不尽如人意的。
    但是,尤大大还是为我们提供了 Vue.setVue.delete ,供我们新增属性,删除属性。

    那咋办呢?总不能写一半去跟面试官说,剩下的你来?

    我们可以通过改写 Object.defineProperty 来拦截数组及其属性,但是我们并不知道数组一开始的长度是多少。因此,为了性能着想,尤大大可以说是另辟蹊径,开辟了一个新思路。
    继续处理
    这时候就大胆一点啦,我们的思维不妨狂野一点。都自己手动实现响应式原理了,不如再动动脑筋,发散一下思维,接着处理一下 Array的原型 :

    // 重新定义数组原型
    import { def } from "utils.js";
    // 复制 Array 的原型
    const arrayPrototype = Array.prototype;
    // 重塑新的 Array 原型
    const arrayMethods = Object.create(arrayPrototype);
    // 列举出影响属性变化以及需要改写原型的方法名
    const methodsNeedChange = ["push","pop","shift","unshift","splice","reverse","sort"];
    // 遍历方法名
    methodsNeedChange.forEach(methodName => {
    	let original = arrayPrototype[methodName];
    	def(arrayMethods, methodName, function() {
    		//这个this是指调用该方法的实例对象,即数组对象arr
            const result = original.apply(this, arguments);
            //把类数组对象变为数组,从而在Observe中判断为数组,从而实现对元素的监视
            const args = [...arguments];
            //push , unshift, splice能增加新项,故也要变为observe
            const ob = this.__ob__;
            let inserted = []; // 保存新增的数组元素,用于设置响应式
            switch(methodName) {
                case 'push':
                case 'unshift':
                    inserted = args; //指def形参的第三个function的参数
                    break;
                case 'splice':
                    inserted = args.slice(2); //slice(start,end,newvalue) 开始结束时全闭区间
                    break;
            }
            //判断inserted是否为空,让新增的项也成为响应式
            if (inserted) {
                //  ob就是OBserve类的实例对象 
                ob.arrayOberver(inserted)
            }
            // 工具函数的第三个参数是值,所以我们把方法处理完的结果返回即可
            return result;
    	}, false)
    })
    
    // 修改Observer类
    export default class Observer {
    	constructor(value) {
    		// 绑定响应式标识 __ob__ 属性
    		def(value, "__ob__", this, false);
    		// 判断是否为数组
    		if (Array.isArray(value)) {
    			// 实现拦截
                Object.setPrototypeOf(value, arrayMethods);
                // 数组的子项也可能是数组, 故也要调用 observe
                this.arrayOberver(value);
            } else {
                this.walk(value);
            }
    	}
    	walk(data) {
    		for (let k in data) {
    			defineReactive$$1(data, k);
    		}
    	}
    	arrayOberver(arr) {
            for(let i = 0, l = arr.length; i < l; i++) {
                observe(arr[i]);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    这样,我们就能实现,既能劫持属性值为数组的变化,又不影响原来对数组的响应式的监听。

    是吧,看到这里,你就会发现该API其实很拉垮,需要不断完善,才能勉强实现响应式。反之你也可以理解的尤大大的思维是有多么狂野。
    但在 Vue3.0 中,改用了 proxy 处理响应式,实现了更完美的响应式。

    四、实现 Vue2.0 的双向绑定原理

    经过前面的分析,我相信你现在应该对实现2.0版本的双向绑定应该有思路了,让我们梳理一下:

    1. 组件挂载的时候,必须先遍历 data 对属性进行数据劫持;
    2. 生成 Dep 调度中心,准备收集观察者依赖;
    3. 利用 getter 函数埋入观察者;
    4. 利用 setter 函数通知 Watcher 更新数据;
    5. 如果 Model 层数据变动,利用调度中心通知观察者更新视图;
    6. 如果 View 层操控数据,利用调度中心通知观察者更新 data 对应的属性。

    这里插一句,这篇文章代码部分是在 node 环境下示例的,也就是我可以使用 import 和 export 的关键。因为在这种开发环境下我的工作模式可以很单一,每一个文件都有它们自己的职责,而且每一个文件也只会注重自己需要做的事情。当然 Vue 源码也是这么做的,除了几份编译版的代码,但也是使用 rollup 打包出来的。

    OK,现在我们从头到尾,循序渐进的,完整的实现一下整个响应式的过程:

    1. 实现数据劫持
    2. 处理数组原型
    3. DepWatcher 收集依赖
    4. 修改数据更新视图
    • 第一,需要一个入口判断数据类型:
    import Observer from "Observer.js";
    /**
     * observe.js
     * 此方法用于判断数据是否为对象以及挂载拦截
     * @param {Object} value 需要监听的对象
     * @return {Object} ob 响应式的标识
    */
    
    export default function observe(value) {
    	// 判断是基本数据类型或者引用数据类型 object
    	if (typeof value != "object") return;
    	let ob;
    	// 判断这个对象或属性是否携带有响应式的标识
    	if (typeof value.__ob__ != "undefined") {
    		ob = value.__ob__;
    	} else {
    		ob = new Observer(value);
    	}
    	return ob;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 第二,实现工具函数,添加响应式标识:
    /**
     * utils.js
     * 该工具函数用于给对象属性绑定属性
     * @param {Object} obj 需要绑定的对象
     * @param {String} key 需要绑定的属性名
     * @param {Any} value 需要绑定的属性值
     * @param {Boolean} enumerable 绑定的属性是否可枚举
    */
    export const function def(obj, key, value, enumerable) {
    	Object.defineProperty(obj, key, {
    		value,
    		enumerable,
    		writable: true,
    		configurable: true
    	});
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 第三,细化颗粒度,为数据挂载数据拦截:
    // Observer.js
    import { def } from "utils.js";
    import Dep from "Dep.js";
    import { arrayMethods } from "array.js";
    import observe from "observe.js";
    import defineReactive$$1 from "defineReactive.js";
    
    export default class Observer {
    	constructor(value) {
    		// 绑定响应式标识 __ob__ 属性
    		def(value, "__ob__", this, false);
    		// 埋入该属性的收集者
    		this.dep = new Dep();
    		// 判断是否为数组
    		if (Array.isArray(value)) {
    			// 实现拦截
                Object.setPrototypeOf(value, arrayMethods);
                // 数组的子项也可能是数组, 故也要调用 observe
                this.arrayOberver(value);
            } else {
                this.walk(value);
            }
    	}
    	walk(data) {
    		for (let k in data) {
    			defineReactive$$1(data, k);
    		}
    	}
    	arrayOberver(arr) {
            for(let i = 0, l = arr.length; i < l; i++) {
                observe(arr[i]);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 第四,实现数据拦截:
    /**
     * defineReactive.js
     * 该函数用于实现数据劫持
     * @param {Object} data 需要实现数据劫持的对象
     * @param {String} key 被劫持对象的属性名
     * @param {Any} value 被劫持对象的属性值
     * @return {Any} value 被劫持对象添加监听后的属性值
    */
    import Dep from "Dep.js";
    import observe from "observe.js";
    
    export default function defineReactive$$1(data, key, value) {
    	// 判断参数有没有传入 value,如果没有传入则需要给其赋值,否则报错
    	if (arguments.length == 2) value = data[key];
    	// 埋入收集依赖执行过程的收集者,用于触发收集或触发更新
    	const dep = new Dep();
    	// 其子属性也有可能是对象,故也要监听
    	let childOb = observe(value);
    	Object.defineProperty(data, key {
    		enumerable: true,
    		configurable: true,
    		get() {
    			// Watcher 获取属性值的时候判断 Dep.target
    			if (Dep.target) {
    				// 添加到需要通知更新的数组里面
    				dep.depend();
    				if (childOb) {
    					// 如果子属性有响应式也要添加
    					childOb.dep.depend();
    				}
    			}
    			return value;
    		},
    		set(newValue) {
    			if (newValue == value) return;
    			value = newValue;
    			// 新值子属性也可能是对象,故也要监听
    			childOb = observe(newValue); 
    			// 更新值的时候,通知 Watcher 更新
    			dep.notify();
    		}
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 第五,处理数组原型:
    // 重新定义数组原型
    import { def } from "utils.js";
    
    // 复制 Array 的原型
    const arrayPrototype = Array.prototype;
    // 重塑新的 Array 原型
    export const arrayMethods = Object.create(arrayPrototype);
    // 列举出影响属性变化以及需要改写原型的方法名
    const methodsNeedChange = ["push","pop","shift","unshift","splice","reverse","sort"];
    // 遍历方法名
    methodsNeedChange.forEach(methodName => {
    	let original = arrayPrototype[methodName];
    	def(arrayMethods, methodName, function() {
    		// 这个this是指调用该方法的实例对象,即数组对象arr
            const result = original.apply(this, arguments);
            // 把类数组对象变为数组,从而在Observe中判断为数组,从而实现对元素的监视
            const args = [...arguments];
            // push , unshift, splice能增加新项,故也要变为observe
            const ob = this.__ob__;
            let inserted = []; // 保存新增的数组元素,用于设置响应式
            switch(methodName) {
                case 'push':
                case 'unshift':
                    inserted = args; //指def形参的第三个function的参数
                    break;
                case 'splice':
                    inserted = args.slice(2); //slice(start,end,newvalue) 开始结束时全闭区间
                    break;
            }
            // 判断inserted是否为空,让新增的项也成为响应式
            if (inserted) {
                //  ob就是OBserve类的实例对象 
                ob.arrayOberver(inserted)
            }
            // push , unshift, splice能增加新项,故需要通知 Watcher 更新
            ob.dep.notify();
            // 工具函数的第三个参数是值,所以我们把方法处理完的结果返回即可
            return result;
    	}, false)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 第六,实现 Dep 类:
    // 定义起始ID
    let depid = 0;
    
    export default class Dep {
    	constructor() {
    		// 自增ID
    		this.id = depid++;
    		this.subs = [];
    	}
    	addSubs(sub) {
    		this.subs.push(sub);
    	}
    	depend() {
    		if (Dep.target) {
    			this.addSubs(Dep.target);
    		}
    	}
    	notify() {
    		const subs = this.subs.slice();
    		for(let i = 0, l = subs.length; i < l; i++) {
    			// 循环通知 Watcher 更新
    			subs[i].update();
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 第七,实现 Watcher 类:
    import Dep from "Dep.js";
    /**
     * Watcher.js
     * 该类用于实例观察者依赖
     * @param {Node} node 依赖的对象属性
     * @param {String} key 对象数据的属性名
     * @param {Object} vm 实例化的 Vue 数据对象
    */
    // 定义起始ID
    let watchId = 0;
    
    export default class Watcher {
    	constructor(node, key, vm) {
    		// 自增ID
    		this.id = watchId++;
    		// 实例化的时候指向自己,方便 Dep 收集依赖
    		Dep.target = this;
    		// 依赖的对象属性,用于判断数据以什么方式更新
    		this.node = node;
    		this.key = key;
    		// vm 实例化的 Vue 对象,包含 data
    		this.vm = vm;
    		// 获取实例化的属性值,驱动 Dep 收集依赖
    		this.getValue();
    	}
    	getValue() {
    		try {
                this.value = this.vm.$data[this.key];
            } finally {
            	// 设置为 null,防止死循环
                Dep.target = null;
            }
    	}
    	update() {
            this.getAndInvoke();
        }
        getAndInvoke() {
            this.getValue();
            if (this.node.nodeType === 1) {
                this.node.value = this.value;
            } else if (this.node.nodeType === 3) {
                this.node.textContent = this.value;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 第八,模拟虚拟 DOM
    import Watcher from "Watcher.js";
    
    /**
     * render.js
     * 该方法用于实现虚拟 DOM 和挂载节点
     * @param {DOM} el 需要挂载的节点
     * @param {Object} vm 实例化的 Vue 数据对象
    */
    export default function nodeToFragment(el, vm) {
    	// 创建文档碎片
        let fragment = document.createDocumentFragment();
        let child;
        // 循环生成文档碎片
        while(child = el.firstChild) {
            compiler(child, vm); // 模板编译
            fragment.appendChild(child); // 将节点添加到文档碎片中
        }
        // 挂载文档
        el.appendChild(fragment);
    }
    
    function compiler(node, vm) {
        // 每个节点都有个节点类型属性 nodeType 对应的值分别是 1.元素 2.文本 8.注释 9.根节点
        if (node.nodeType === 1) {
            // 如果是元素节点
            // 遍历所有的属性,判断是否有 v-model 指令
            [...node.attributes].forEach(item => {
                if (/^v-/.test(item.nodeName)) {
                    new Watcher(node, item.nodeValue, vm);
                    // nodeName 就是属性名
                    node.value = vm.$data[item.nodeValue];
                    node.addEventListener('input', () => {
                        console.log(vm.$data[item.nodeValue]);
                        vm.$data[item.nodeValue] = node.value;
                    })
                }
            });
            // 元素节点还可能有很多子节点或孙子节点,因此需要递归处理
            [...node.childNodes].forEach(item => {
                compiler(item, vm);
            })
        } else if (node.nodeType === 3) {
            // 如果是文本节点
            // 检测该文本中是否包含胡须语法
            if (/\{\{\w+\}\}/.test(node.textContent)) {
                // 将胡须语法换为数据
                node.textContent = node.textContent.replace(/\{\{(\w+)\}\}/, function(a, b) {
                    new Watcher(node, b, vm);
                    return vm.$data[b];
                })
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 第九,封装 Vue 函数
    import observe from "observe.js";
    import nodeToFragment from "render.js";
    
    /**
     * core.js
     * 该方法用于生成 Vue 实例
     * @param {Object} options 实例化所需的参数
    */
    export default function Vue(options) {
    	this.$data = options.data;
        this.$el = document.querySelector(options.el);
        observe(this.$data);
        nodeToFragment(this.$el, this);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 第十,例子:
    DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>vue2实现双向绑定title>
        head>
        <body>
            <div id="app">
                <input type="text" v-model="name">
                <h2>{{name}}h2>
            div>
            <script src="xuni/bundle.js">script>
            <script>
                const vm = new Vue({
                    data: { name: 'vk是铁憨憨', hobby: ['唱','跳','rap'] },
                    el: '#app'
                })
            script>
        body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    看一下效果:

    双向绑定的效果

    • 搞个定时器,修改 data
    DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>vue2实现双向绑定title>
        head>
        <body>
            <div id="app">
                <input type="text" v-model="name">
                <h2>{{name}}h2>
            div>
            <script src="xuni/bundle.js">script>
            <script>
                const vm = new Vue({
                    data: { name: 'vk是铁憨憨', hobby: ['唱','跳','rap'] },
                    el: '#app'
                })
                setTimeout(() => {
    				vm.$data.name = "vk是大帅逼";
    			}, 3000)
            script>
        body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 看下效果如何:

    改变data数据的效果
    最后,感谢你的阅读,码字真的很辛苦,给个三连吧!!!

    代码已上传至码云,有需要的小伙伴自行下载吧 —— 《下载地址》

    参考文献

  • 相关阅读:
    利用邮件营销提升电商营业额的策略与方法
    C++ 捕获程序异常奔溃minidump
    使用Python批量发送个性化邮件
    【开发】微服务整合Sentinel
    slam资料汇总
    Jenkins CLI二次开发工具类
    Nacos win10 安装配置教程
    【历史上的今天】12 月 6 日:微波炉问世;多媒体格式 Mkv 诞生;日立环球存储科技公司成立
    Java给Excel设置单元格格式
    一、python基础语法
  • 原文地址:https://blog.csdn.net/LizequaNNN/article/details/124988514