vue源码分析(一)

430 阅读12分钟

简介

vue 介绍

我们都知道目前使用vue已经是前端开发的主流框架之一,经过大家的经常使用越来越多的的人也渐渐的想要深入学习vue的原理,这篇文章将由我带大家学习vue的源码,vue有很多属性比如data,props,computed,watch等,那么这些属性是怎么实现的呢,接下来我会慢慢的带大家实现一下vue.js的实现原理。vue 本质上就是个函数,我们知道vue是MVVM框架这里就不介绍这个框架了,这里大致说一下它的实现原理 通过Object.defineProperty对数据进行劫持/代理,再使用数据的时候就会触发数据的 get 方法,修改数据的时候触发 set 方法,从而对数据进行劫持,再根据观察者模式(发布订阅者模式改造版,只不过订阅者也可以向发布者发送信息,所以两者都称为发布者)。这里我们需要知道Object.defineProperty的功能和发布-订阅设计模式的设计思维。

注意:开发的时候的使用脚手架vue-cli帮我们快速生成一个vue项目,我们可以在项目中使用以.vue后缀名结尾的文件进行页面和逻辑的编写,这是因为vue的脚手架搭配了webpackwebpack中使用vue-loader可以帮我们把.vue文件编译为浏览器识别的html+css+js文件。所以vue的本质还是Vue的实现。

vue 在单页面文件中使用

vue 在 html 的引入以及使用。vue 本身是一个构造函数,采用 new 的方式创建实例,然后进行传入配置项,初始化 vue 实例的状态。接着我们可以在页面中使用vue语法。

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<title>vue源码分析</title>
		<script src="./myVue.js"></script>
	</head>
	<body>
		<div
			id="app"
			style="color: red;background-color: aquamarine;font-size: 14px;"
		>
			<span style="color: yellow;">{{a}}</span>
			<div class="text">2332{{b}}</div>
			<div class="app"></div>
		</div>
		<script>
			// 对vue实例传入配置
			let vm = new Vue({
				el: "#app",
				data() {
					return {
						a: 99999,
						c: {
							test: 12,
							str: "qwe",
						},
						hobby: [
							{
								label: "数组",
							}
						],
					};
				},
			});
			vm.c.test = 1234;
			vm.hobby[0].ttt = 123;
			vm.hobby.push(4444);
			console.log("vm==", vm.hobby);
		</script>
	</body>
</html>

如下图我们可以看到vue的工作流程

vue2.png

一 初始化

vue 有多个配置项,比如:data,props,watch,computed 等配置,在初始化 vue 项目的时候,需要对这些配置项进行初始化,还有就是 vue 本身的方法和属性初始化,所以也是初始化 vue 的状态。初始化mount,mount,emit,$on 等方法,还有_c,_s,_v 等render 函数组成函数。 createElementVNode 和 createTextVNode会在生成虚拟 DOM 函数中介绍。

如下我们开始初始化vue的属性和方法

function Vue() {
	this._init(options);
}
initMixin(Vue);
initLifeCycle(Vue)

initMixin方法拓展vue原型方法,初始化vue原型方法

function initMixin(Vue) {
   //_init方法获取配置传入vue的配置()
   Vue.prototype._init = function (options) {
   	//vue默认以$开头的为自己的属性
   	const vm = this;
   	vm.$options = options;
   	//初始化状态
   	initState(vm);
   	if (options.el) {
   		vm.$mount(options.el);
   	}
   };
   Vue.prototype._c=function(){
   	//详细代码可以去拉取仓库查看
   }
   //初始化$mount方法,用于挂载模板。el为传递的模板的css选择器
   Vue.prototype.$mount = initMount
}

initLifeCycle方法是初始化生命周期,其中_update方法就是vue执行更新操作的时候调用的函数,他会将render函数执行的结果也就是虚拟DOM保存起来,在下一次更新的时候采用diff算法

//初始化生命周期
function initLifeCycle(Vue) {
	//执行更新操作,传入的是虚拟DOM,与旧的虚拟DOM相比,更新变化的部分
	Vue.prototype._update = function (vnode) {
		this.$el = patch(this.$el, vnode); //patch既有初始化的功能又有更新功能
	};
	//渲染虚拟DOM
	//ast语法树渲染太好性能,所以转成render函数之后,每次更新只执行render函数即可,无需再执行ast转化过程
	Vue.prototype._render = function () {
		const vm = this;
		//修改render函数的this指向vm
		return vm.$options.render.call(this);
	};
}

initState用来初始化传入vue配置项,即new Vue(option),初始化option配置项

function initState(vm) {
	const ops = vm.$options;
	// 初始化prop属性
	if (ops.props) {
		initProps();
	}
	// 初始化data属性
	if (ops.data) {
		initData(vm);
	}
	// 初始化computed属性
	if (ops.computed) {
		initComputed(vm);
	}
	// 初始化watch属性
	if (ops.watch) {
		initWatch(vm);
	}
}

初始化 data 响应式数据

初始化 vue 的 data 的数据时,因为 data 可能为对象或者函数,所以需要先获取真正的数据,data 中的对象(object)和基础类型(number,string 等)使用(Object.defineProperty)数据进行劫持,然后将数据绑定在 vm 的实例属性_data 上,即 vm._data 等于配置项的 data,至此 data 配置项的数据和 vm 实例实现绑定。 observe 函数主要用于对数据进行劫持之前会先判断 vue 数据是否已经为响应式,然后再使用 Observe 来设置数据,如果是数组则采用重写数组的方法来将数组劫持,如果是对象则采用 Object.defineProperty 对数据进行劫持。

function initData(vm) {
	let data = vm.$options.data;
	data = typeof data === "function" ? data.call(this) : data; //data可能为函数或者对象
	vm._data = data; //在vm上绑定data,用于绑定data配置的数据
	observe(data); //对数据进行劫持
	for (let key in data) {
		proxy(vm, "_data", key); //将数据代理到vm实例上,使用this来访问变量如this.name,并且只代理data数据的第一层
	}
}
//vue2的响应式原理,对数据的劫持
function observe(data) {
	if (typeof data !== "object" || data === null) {
		return; //data不是对象则不劫持
	}
	if (data.__ob__ instanceof Observe) {
		return data.__ob__;
	}
	// 对数据进行劫持操作
	return new Observe(data);
}

数据劫持

使用数据的劫持,首先在数据初始化的时候会给 data 绑定一个 ob 的属性,该属性指向 vm。有两个作用:1 是可以判断数据是否已经被劫持,2 是对数组的方法进行劫持的时候,数组的元素可能为对象,因此也需要回调观察者的方法进行递归观察

注意:数组本身的引用已经被劫持了,所以调用数组的时候会触发 setter,但是对数组内部的元素却没有劫持,因为元素的调用并不会触发 setter 或者 getter。因此数组元素变化是采用重写数组的方法实现的。 dep属性是用来进行依赖收集的,下面会有依赖收集的介绍

Observe表示一个劫持数组的操作类,并且在劫持数据的时候会递归调用,所以每个被劫持的数据身上都会有一个收集器this.dep 用来收集页面上使用该数据的地方(也就是vue组件)

//观察对象是否被劫持
class Observe {
	constructor(data) {
		//挂载this到data的属性上,有两个作用:1是可以判断数据是否已经被劫持,2是对数组的方法进行劫持的时候,
		// 数组的元素可能为对象,因此也需要回调观察者的方法

		//数组只是重写方法,没有进行依赖收集
		//需要对数组的对象元素进行劫持,所以对数组和对象增加一个dep依赖收集
		this.dep = new Dep();//依赖收集
		Object.defineProperty(data, "__ob__", {
			value: this,//挂载this到vm.data上
			enumerable: false, //不可枚举,不可遍历
		});

		if (Array.isArray(data)) {
			// reDefineArray重写数组方法
			data.__proto__ = reDefineArray();
			this.observeArray(data);
		} else {
			this.walk(data);
		}
	}
	walk(data) {
		//循环对象属性来劫持对象的引用类型属性
		Object.keys(data).forEach((key) => defineReactive(data, key, data[key]));
	}
	//监测数组的变化
	observeArray(data) {
		data.forEach((item) => observe(item));
	}
}

对对象数据进行劫持

采用的就是Object.defineProperty对数据进行劫持,当数据进行get和set操作的时候就会触发该函数,并且我们会递归遍历对象,是对象上的所有引用类型都被劫持。这也是vue性能不好的地方之一。

//对对象数据进行劫持
function defineReactive(target, key, value) {
	//这里会递归调用,也会对每个属性绑定dep依赖收集器
	observe(value); 
	Object.defineProperty(target, key, {
		get() {
			//取值的时候
			return value;
		},
		set(newValue) {
			if (newValue === value) return;
			value = newValue;
		},
	});
}

重写数组的七个方法来对数组改变数组的七个方法进行劫持

大家可能会有疑问,为什么数组不能采用Object.defineProperty对数据进行劫持,因为Object.defineProperty只对对象的属性进行劫持,对数组的元素不能劫持。为什么重写数组方法会对数组数据进行劫持?并且还会更新页面呢? 我们看一下源码

function reDefineArray() {
	let prototype = Array.prototype;
	let newArrayProto = Object.create(prototype); //复制原型对象
	let methods = ["push", "pop", "shift", "unshift", "sort", "splice"];
	methods.forEach((item) => {
		newArrayProto[item] = function (...args) {
			const result = prototype[item].call(this, ...args); //内部调用原来的方法,函数的劫持,切片原理
			let inserted;
			let ob = this.__ob__; //获取observe实例对象,调用数组方法
			switch (item) {
				case "push":
				case "unshift":
					inserted = args;
					break;
				case "splice":
					inserted = args.slice(2);
				default:
					break;
			}
			if (inserted) {
				ob.observeArray(inserted);
			}
			ob.dep.notify(); //数组变化了,通知对应的watcher实现更新逻辑
			return result;
		};
	});
	return newArrayProto;
}

从上面的代码我们可以看到对改变数组的七个方法进行重写,并不是对其方法逻辑进行重写,本质还是调用数组原型上的方法,而是在调用这几个方法的添加一些操作,首先inserted表示有数组的七个操作方法,当使用这七个方法的时候就会调用observeArray方法遍历数组元素,然后对引用类型进行劫持,这样就是为什么如果数组元素是引用类型还会具有响应式,如:this.arr数组的第一个元素是引用类型时this.arr[0].name="change"或者this.arr[0]=123也会引起页面的更新。但是如果数组元素是基本数据类型时数组元素不具有响应式,也就是this.arr[0]=123数组元素改变不会更新页面。ob.dep会通知页面更新,这个属性会在下面的观察者模式中介绍,这里的主要作用就是当数组调用这七个方法时会更新页面。

proxy 代理函数

proxy 代理方法:代理_data/data 的数据,让 vm 可以直接访问即 this 来访问,如 this.a,并且只访问 data 对象的第一层属性

//对data上的数据依次进行代理
function proxy(vm, target, key) {
	Object.defineProperty(vm, key, {
		get() {
			return vm[target][key];
		},
		set(newValue) {
			vm[target][key] = newValue;
		},
	});
}

二 生成虚拟DOM

模板解析,即获取模板的内容(html 的字符串)生成虚拟 DOM,当数据变化后比较新旧的虚拟 DOM 的差异,进而更新差异的部分,将差异的部分转变为真实 DOM,即将 template 语法转变成 render 函数,在init 方法中初始化$mount 方法

1 初始化$mount 方法

initMount方法主要使用来挂载传入的DOM元素组成的字符串

 function initMount(el) {
	const vm = this;
	el = document.querySelector(el);
	const ops = vm.$options;
	//先查看是否有render配置项,如果没有render函数,则使用template
	if (!ops.render) {
		let template;
		//查看是否配置项中是否有template配置项,没有的话查找el选择器对应的DOM元素
		if (!ops.template && el) {
			template = el.outerHTML; //获取DOM字符串,可以打印出来看看,这是一个包含空格与换行的字符串
		} else {
			if (el) {
				template = ops.template;
			}
		}
		if (template) {
			//编译模板方法,之后生成render函数
			const render = complieToFunction(template);
			ops.render = render;
		}
	}
	//最终可以使用render函数
	mountComponent(vm, el);
};

页面渲染函数,将会在下述的观察者watcher中执行

// 组件的挂载方法
function mountComponent(vm, el) {
	//这里的el是querySelector处理的
	vm.$el = el;
	//根据生成的虚拟dom创建真实的dom
	const updateComponent = () => {
		vm._update(vm._render());
	};

	//初次渲染和后续更新都选择updateComponent方法

	const watcher = new Watcher(vm, updateComponent, true);
}

2 生成 render 函数

生成 render 的过程可分为三步。1.将模板字符解析为 ast 抽象语法树,2.遍历 ast 抽象语法树生成 render 函数。 complieToFunction 函数的作用就是将获取的模板字符串(包含模板语法,如:<div style={background-color='red'}>{{a}}</div>等)转变成一个树形结构的对象,注意这里的对象不是虚拟DOM,它只是对vue模板语法的描述。如下:

{
	tag:'div',
	type: ELEMENT_TYPE,
	children: [
			{
				text: "{{a}}",//文本的值
				type: "text",//文本节点
				parent:{...}
			}
	],
	attrs:[
		{
			name:"background-color",
			value:"red"
		}
	]
	parent: null,
}

然后再返回对应的render函数。

// 解析模板函数complie
function complieToFunction(template) {
	//将template转化为ast语法树,也就是虚拟DOM
	let ast = parseHTML(template);
	//将虚拟DOM挂载属性和解析模板语法后生成模板字符串,
	// 模板解析的内容有,包括双花括号语法{{name}},v-bind指令等
	let code = codeGen(ast);
	code = `with(this){return ${code}}`; //with函数就是将vm实例传入进去,然后再进行解析的时候,将{{}}中的变量填入data中的值
	//模板解析引擎的实现原理 with+new Function
	let render = new Function(code); //将解析好的模板字符串进行函数化,也就是render函数
	return render;
}
模板解析-compile

将包含 vue 语法的模板字符串解析生成 ast 抽象语法树,简单来说就是一个对象,其中包含对DOM元素的描述信息,因为还包含vue的模板语法所以还不是虚拟DOM,因为vue中的模板解析所用到的正则表达式和内容比较多,这里我只实现了模板解析只对标签的style属性,vue的模板语法{{}}进行解析

点击查看代码
//解析DOM字符串模板,生成虚拟DOM
function parseHTML(html) {
	//html标签的第一个字符为<
	//将template转化成ast语法树
	//通过正则表达式来匹配
	const ELEMENT_TYPE = "element";
	const TEXT_TYPE = "text";
	const stack = []; //元素栈
	let currentParent; //指向栈中的最后一个元素
	let root; //根元素
	const ncname = `[a-zA-Z][\\-\\.0-9_a-zA_Z]*`; //匹配标签名
	const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //带命名空间的标签
	const startTagOpen = new RegExp(`^<${qnameCapture}`); //匹配一个标签名<xxxx开始标签
	const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //匹配的是</xxx>最终匹配的结束标签
	//匹配css属性的正则表达式
	const attribute =
		/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
	const startTagClose = /^\s*(\/?)>/; //匹配自闭合标签如<br />
	//vue3采用的不是正则,采用模板解析

	//创建元素抽象语法树模型
	function createASTElement(tag, attrs) {
		return {
			tag,
			type: ELEMENT_TYPE,
			children: [],
			attrs,
			parent: null,
		};
	}

	//解析开始标签
	function parseStartTag() {
		const startMatch = html.match(startTagOpen);
		if (startMatch) {
			const match = {
				tagName: startMatch[1], //标签名
				attrs: [], //标签内部的键值对属性对象
			};
			//删除匹配的标签信息
			advance(startMatch[0].length);
			//如果不是开始标签就一直匹配下去
			let attr;
			let end;
			while (
				!(end = html.match(startTagClose)) &&
				(attr = html.match(attribute))
			) {
				end = html.match(startTagClose);
				advance(attr[0].length);
				match.attrs.push({
					name: attr[1],
					value: attr[3] || attr[4] || attr[5] || true,
				});
			}
			if (end) {
				advance(end[0].length);
			}
			return match;
		}
		return false;
	}

	//边解析字符串,边去掉解析过的字符串,直至字符串解析完
	function advance(n) {
		html = html.substring(n);
	}
	//一个标签的开始
	function start(tag, attrs) {
		let node = createASTElement(tag, attrs);
		if (!root) {
			root = node;
		}
		if (currentParent) {
			node.parent = currentParent;
			currentParent.children.push(node);
		}
		stack.push(node);
		currentParent = node;
	}
	//一个标签里面的文本内容
	function handlerText(text) {
		// text
		text = text.replace(/\s/g, ""); //空格转为空字符
		text &&
			currentParent.children.push({
				type: TEXT_TYPE,
				text,
				parent: currentParent,
			});
	}
	//一个标签的结尾
	function end(tag) {
		let node = stack.pop();
		// if(node!==tag)校验标签
		currentParent = stack[stack.length - 1];
	}

	while (html) {
		let textEnd = html.indexOf("<"); //如果indexOf中的索引是0,则说明是个标签
		if (textEnd === 0) {
			const startTagMatch = parseStartTag();
			if (startTagMatch) {
				start(startTagMatch.tagName, startTagMatch.attrs);
				continue;
			}
			let endTagMatch = html.match(endTag);
			if (endTagMatch) {
				advance(endTagMatch[0].length);
				end(endTagMatch[1]);
				continue;
			}
		}
		if (textEnd > 0) {
			let text = html.substring(0, textEnd);
			if (text) {
				handlerText(text);
				advance(text.length);
			}
		}
	}
	return root;
}

生成 render 函数

其中 render 函数会包含_c,_s,_v(_c,_s,_v等函数是定义在在vue原型上的方法,用来转换语法) 等函数,这些函数在 vue 状态初始化的时候有介绍,就是将 ast 抽象语法树解析成虚拟 DOM,可以前去查看。主要作用就是生成虚拟 DOM 的时候会将元素节点和文本解析成对应的 DOM 节点,也会解析模板字符中变量,也就是将 data 中的变量填入 DOM 元素中,也就是 MVVM 结构中的 model->viewModel->view 的过程。 如下图

render函数.awebp

点击查看代码详情
//属性
function genChildren(children) {
	return children.map((child) => gen(child)).join(",");
}
//模板语法解析
function codeGen(ast) {
	let children = genChildren(ast.children);
	let code = `_c('${ast.tag}',${
		//_c函数是将html标签在render函数执行的时候转为虚拟DOM的标签
		ast.attrs.length > 0 ? genProps(ast.attrs) : "null"
	}${ast.children.length ? `,${children}` : ""}
  )`;
	return code;
}

function gen(node) {
	const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配模板中的{{}}的变量名称
	if (node.type === "element") {
		return codeGen(node);
	} else {
		//
		let text = node.text;
		if (!defaultTagRE.test(text)) {
			return `_v(${JSON.stringify(text)})`; //_v函数是将标签的文本内容在render函数执行的时候转为虚拟DOM的标签文本内容
		} else {
			let tokens = [];
			defaultTagRE.lastIndex = 0;
			let lastIndex = 0;
			while ((match = defaultTagRE.exec(text))) {
				let index = match.index;
				if (index > lastIndex) {
					tokens.push(JSON.stringify(text.slice(lastIndex, index)));
				}
				tokens.push(`_s(${match[1].trim()})`); //匹配模板中的{{}}的变量名称,放到_s函数下,并在render函数执行的过程中将变量转为真正的值
				lastIndex = index + match[0].length;
				if (lastIndex < text.length) {
					tokens.push(text.slice(lastIndex));
				}
				return `_v(${tokens.join("+")})`;
			}
		}
	}
}

//遍历标签属性,生成render函数
function genProps(attrs) {
	let str = "";
	for (let i = 0; i < attrs.length; i++) {
		let attr = attrs[i];
		if (attr.name === "style") {
			let obj = {};
			attr.value.split(";").forEach((item) => {
				let [key, value] = item.split(":");
				if (key && value) {
					obj[key] = value;
				}
			});
			attr.value = obj;
		}
		str += `${attr.name}:${JSON.stringify(attr.value)},`;
	}
	return `{${str.slice(0, -1)}}`;
}

3 生成虚拟 DOM

render 函数执行后就会产生虚拟 DOM,每次数据依赖变化就会触发render函数执行,执行的虚拟DOM会保存在内存中,当遇到更新操作的时候会采用diff算法进行新旧的虚拟DOM对比,更新与旧虚拟DOM不一样的地方。

//ast做的是语法层面的转化,他描述的vue模板语法本身
//我们虚拟dom是描述的是dom元素,可以增加自定义属性,比如vm即为模板的根节点
function vnode(vm, tag, key, data, children, text) {
	return {
		vm,
		tag,
		key,
		data,
		children,
		text,
	};
}

将html标签DOM描述转为虚拟DOM的方法

function createElementVNode(vm, tag, data, ...children) {
	if (!data) {
		data = {};
	}
	let key = data.key;
	if (key) {
		delete data.key;
	}
	return vnode(vm, tag, key, data, children);
}

将虚拟的文本节点描述转为虚拟DOM

function createTextVNode(vm, text) {
	return vnode(vm, undefined, undefined, undefined, undefined, text);
}

渲染页面

patch方法

patch方法用来根据虚拟DOM生成真实DOM的方法

点击查看代码
//patch算法
function patch(oldVNode, vnode) {
	const isRealElement = oldVNode.nodeType;
	// 如果是初次渲染不用进行diff比较
	if (isRealElement) {
		let ele = oldVNode;
		let parentEle = ele.parentNode;
		let newEle = creatEle(vnode);
		// 获取到id为app元素并且将虚拟DOM生成的真实DOM替换为app的兄弟节点,再删除app节点
		document.querySelector("#app").parentNode.insertBefore(newEle, ele.nextSibling);
		document.querySelector("#app").parentNode.removeChild(ele);
		return newEle;
	} else {
		//diff算法
	}
}
//创建真实dom
function creatEle(vnode) {
	let { tag, data, children, text } = vnode;
	if (typeof tag === "string") {
		vnode.el = document.createElement(tag);
		patchProps(vnode.el, data);
		children.forEach((child) => {
			vnode.el.appendChild(creatEle(child));
		});
	} else {
		vnode.el = document.createTextNode(text);
	}
	return vnode.el;
}

//给真实DOM添加属性
function patchProps(el, props) {
	for (let key in props) {
		if (key === "style") {
			for (let styleName in props.style) {
				el.style[styleName] = props.style[styleName];
			}
		} else {
			el.setAttribute(key, props[key]);
		}
	}
}

依赖收集

dep收集器

```js
 let depId=0

class Dep { constructor() { this.id = depId++; //用于标识dep收集器 this.subs = []; //存放属性对应的Watcher } depend() { //这里不希望放重复的watcher,而且刚才只是一个单向的关系dep->watcher //watcher记录dep Dep.target.addDep(this); //让watcher记住dep } //收集watcher addSub(watcher) { this.subs.push(watcher); } //数据更新的时候触发set方法,从而触发Dep的notify方法,通知各个依赖的页面Watcher,然后更新视图 notify() { this.subs.forEach((watcher) => { watcher.update(); }); } } ```

页面Watcher

watcher的作用是当页面的数据是data中的数据时就让data中的数据的dep依赖收集器属性收集页面渲染的watcher

let watcherId = 0;
//不同的组件拥有不同的Watcher,所以用id来区分
 class Watcher {
 		// isRender用于标识是否是一个渲染Watcher
 		constructor(vm, fn, options, callback) {
 			this.id = watcherId++;//标识watcherDOM
 			this.renderWatcher = options; //是一个渲染Watcher
 			// fn就是updateComponent更新页面DOM的操作,里面将render方法生成虚拟DOM生成真实DOM
 			//getter意味着调用这个函数可以发生取值操作
 			if (typeof fn === "string") {
 				this.getter = function () {
 					return vm[fn];
 				};
 			} else {
 				this.getter = fn;
 			}
 			this.callback = callback; //watch的回调函数
 			this.deps = []; //后续实现计算属性要用到
 			this.depsId = new Set();//Set数据结构会去重
 			this.lazy = options.lazy;//后续实现计算属性要用到
 			this.dirty = this.lazy;//后续实现计算属性要用到
 			this.value = this.lazy ? undefined : this.get();//后续实现计算属性要用到
 			this.user = options.user; //获取user
 			this.vm = vm;
 		}

 		//收集dep,当页面上的数据改变的时候,通知数据进行改变
 		addDep(dep) {
 			//一个组件有多个属性
 			let id = dep.id;
 			if (!this.depsId.has(id)) {
 				this.deps.push(dep);
 				this.depsId.add(id);
 				dep.addSub(this); //Watcher已经记住dep了,而且已经去重了,让dep来收集watcher
 			}
 		}

 		//1.当我们创建渲染watcher的时候我们会把watcher放到Dep.target上
 		//调用_render()会取值,走到get上
 		get() {
 			pushTarget(this); //静态属性只有一份
 			const value = this.getter.call(this.vm); //会去vm上取值
 			popTarget(this); //渲染完毕后清空
 			return value;
 		}
 //  设计计算属性的用到该方法,将数据去脏
 		evaluate() {
 			this.value = this.get(); //获取用户函数的返回值,并且还要标识为脏
 			this.dirty = false;
 		}
 		depend() {
 			for (let i = 0; i < this.deps.length; i++) {
 				this.deps[i].depend(); //让计算属性watcher也收集渲染watcher
 			}
 		}
 		update() {
 			// this.get(); //重新更新渲染
 			//因为每次修改数据都会触发更新,所以我们直接让多个数据修改完之后再进行一次更新
 			if (this.lazy) {
 				//如果计算属性的依赖发生了变化,就标识计算属性是脏值
 				this.dirty = true;
 			} else {
 				queueWatcher(this); //把当前的Watcher暂存起来
 			}
 		}
 		run() {
 			let oldValue = this.value;
 			let newValue = this.get();
 			if (this.user) {
 				this.callback.call(this.vm, newValue, oldValue);
 			}
 		}
 	}

观察者模式

vue所实现的是每当有数据改变的时候依赖数据的页面也会随之更新,这个时候采用的是Object.defineProperty对数据进行劫持,并且每个数据劫持的时候,都会将依赖该数据的页面渲染的watcher进行收集起来放在Dep容器里面,可以在Observe类中看见dep收集器属性,也就是说当数据改变的时候会触发set方法,这时候就会遍历收集器Dep通知的Watcher。同时Watcher也会收集对应的Dep也就是data数据,当页面上的视图数据改变的时候可以通知dep收集器去更新对应的数据,也就是大名鼎鼎的双向绑定原理。

至此我们可以在html文件中添加vue的模板语法{{}},将数据渲染到页面上,并且数据变化的时候可以更新页面。

最后:本文主要对Vue的设计原理作了基本的介绍,本文页面展示的代码不全。详细代码可以去 我的vue源码 查看,虽然目前还没有完全实现vue,后续我会继续实现更新。