Vue Advanced Guide-01 Рукописный исходный код Vue для анализа исходного кода Vue

Vue.js

предисловие

Теперь на фронтенд-интервью Vue спросит о принципе отзывчивости и о том, как его реализовать.Если вы просто ответите, что перехвата свойств через Object.defineProperty() может быть недостаточно.

В этой статье используются учебные документы и видеоуроки.实现手写Простой исходный код Vue реализует двустороннюю привязку данных, инструкции по синтаксическому анализу и т. д.

Адрес источника

Справочная статья

Несколько способов добиться двусторонней привязки

В настоящее время несколько основных фреймворков mvc (vm) реализуют одностороннюю привязку данных, а двусторонняя привязка данных, как я понимаю, представляет собой не что иное, как добавление ввода к элементам ввода (input, textare и т. д.) на основе односторонней привязки данных. путь привязки.Событие изменения (ввода) используется для динамического изменения модели и представления.Это не очень продвинутый, поэтому не нужно слишком беспокоиться о реализации односторонней или двусторонней привязки.

Существуют примерно следующие способы реализации привязки данных:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(Vue.js)

  • Шаблон издатель-подписчик

Как правило, привязка данных и попыток осуществляется с помощью sub и pub, более подробный метод данных обычно vm.set('property', value) Ведь этот метод сейчас слишком низкий, мы предпочитаем обновлять данные через vm.property=value, и автоматически обновлять представление при этом, поэтому есть следующие два метода.

  • проверка грязного значения

Angular.js использует обнаружение грязных значений, чтобы сравнить, изменились ли данные, чтобы решить, следует ли обновлять представление. Самый простой способ — использовать setInterval() для периодического опроса для обнаружения изменений данных. Конечно, Google не будет таким низким. обнаружение значения вводится, когда событие запускается, примерно следующим образом

* DOM事件,臂如用户输入文本,点击按钮等(ng-click)
* XHR响应事件($http)
* 浏览器location变更事件($location)
* Timer事件($timeout, $interval)
* 执行$diaest()或¥apply()
  • захват данных

Vue.js сочетает в себе модель издатель-подписчик через перехват данных, черезObject.defineProperty()захватить каждое имуществоsetter,getter, публиковать сообщения подписчикам при изменении данных и запускать соответствующие обратные вызовы прослушивателя.

Реализация исходного кода Vue

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
		<script type="text/javascript" src="./compile.js"></script>
		<script type="text/javascript" src="./observe.js"></script>
		<script type="text/javascript" src="./myvue.js"></script>
	</head>
	<body>
		<div id="app">
			<h2>{{person.name}} -- {{person.age}}</h2>
			<h3>{{person.sex}}</h3>
			<ul>
				<li>1</li>
				<li>2</li>
				<li>3</li>
			</ul>
			<div v-text="msg"></div>
			<div>{{msg}}</div>
			<div v-text="person.name"></div>
			<div v-html="htmlStr"></div>
			<input type="text" v-model="msg" />
			<button type="button" v-on:click="btnClick">v-on:事件</button>
			<button type="button" @click="btnClick">@事件</button>
		</div>
		<script type="text/javascript">
			let vm = new Myvue({
				el: '#app',
				data: {
					person: {
						name: '只会番茄炒蛋',
						age: 18,
						sex: '男'
					},
					msg: '学习MVVM实现原理',
					htmlStr: '<h1>我是html指令渲染的</h1>'
				},
				methods: {
					btnClick() {
						console.log(this.msg)
					}
				}
			})
		</script>
	</body>
</html>

Шаг 1. Реализовать парсер инструкций (компилировать)

Главное, что делает компиляция, — парсит инструкции шаблона, заменяет переменные в шаблоне данными, затем инициализирует и отрисовывает представление страницы, привязывает функцию обновления к узлу, соответствующему каждой инструкции, и добавляет подписчиков для мониторинга данных. Как только данные изменятся, получите уведомление, обновите представление

myvue.js

// 工具类根据指令执行对应方法
const compileUtils = {
	/*
	 * node 当前元素节点
	 * expr 当前指令的value
	 * vm 当前Myvue实例, 
	 * eventName 当前指令事件名称
	 */

	// 由于指令绑定的属性有可能是原始类型,也有可能是引用类型, 因此要取到最终渲染的值
	getValue(expr, vm) {
		// reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
		return expr.split('.').reduce((data, currentVal) => {
			return data[currentVal]
		}, vm.$data)
	},
	// input双向数据绑定
	setValue(expr, vm, inputVal) {
		// reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
		return expr.split('.').reduce((data, currentVal) => {
			// 将当前改变的值赋值
			data[currentVal] = inputVal
			console.log(data);
		}, vm.$data)
	},

	// 处理{{person.name}}--{{person.age}}这种格式的数据,不更新值的时候会全部替换了
	getContentVal(expr, vm) {
		return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
			// 获取{{}}中的属性
			return this.getValue(args[1], vm)
		})

	},
	// 这里简单就封装了几个指令方法
	text(node, expr, vm) {
		let value;
		// 处理{{}}的格式
		if (expr.indexOf('{{') !== -1) {
			value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
				// 绑定观察者
				new Watcher(vm, args[1], (newValue) => {
					// 处理{{person.name}}--{{person.age}}这种格式的数据,不然更新值的时候会全部替换了
					this.upDater.textUpDater(node, this.getContentVal(expr, vm))
				})
				// 获取{{}}中的属性
				return this.getValue(args[1], vm)
			})
		} else {
			new Watcher(vm, expr, (newValue) => {
				this.upDater.textUpDater(node, newValue)
			})
			// 获取当前要节点要更新展示的值
			value = this.getValue(expr, vm)
		}
		// 更新的工具类
		this.upDater.textUpDater(node, value)
	},
	html(node, expr, vm) {
		const value = this.getValue(expr, vm)
		// 绑定观察者
		new Watcher(vm, expr, (newValue) => {
			this.upDater.htmlUpDater(node, newValue)
		})
		// 更新的工具类
		this.upDater.htmlUpDater(node, value)
	},
	model(node, expr, vm) {
		const value = this.getValue(expr, vm)
		// 绑定观察者
		new Watcher(vm, expr, (newValue) => {
			this.upDater.modelUpDater(node, newValue)
		})
		node.addEventListener('input', (e) => {
			// 设置值
			this.setValue(expr, vm, e.target.value)
		})
		// 更新的工具类
		this.upDater.modelUpDater(node, value)
	},
	on(node, expr, vm, eventName) {
		// 获取当前指令对应的方法
		const fn = vm.$options.methods && vm.$options.methods[expr]
		// console.log(fn);
		node.addEventListener(eventName, fn.bind(vm), false)
	},
	// 更新的工具类
	upDater: {
		// v-text指令的更新函数
		textUpDater(node, value) {
			node.textContent = value
		},
		// v-html指令的更新函数
		htmlUpDater(node, value) {
			node.innerHTML = value
		},
		// v-model指令的更新函数
		modelUpDater(node, value) {
			node.value = value
		}
	}
}

// Myvue
class Myvue {
	constructor(options) {
		this.$el = options.el;
		this.$data = options.data;
		this.$options = options;
		if (this.$el) {
			// 1.实现一个数据观察者
			new Observe(this.$data)

			// 2.实现一个指令解析器
			new Compile(this.$el, this)

			// 3.实现this代理, 访问数据可以直接通过this访问
			this.proxyData(this.$data)
		}
	}
	proxyData(data) {
		for (const key in data) {
			Object.defineProperty(this, key, {
				get() {
					return data[key]
				},
				set(newValue) {
					data[key] = newValue
				}
			})
		}
	}
}

compile.js

// 指令解析器
class Compile {
	constructor(el, vm) {
		// 判断当前传入的el是不是一个元素节点
		// document.querySelector返回与指定的选择器组匹配的元素的后代的第一个元素。
		this.el = this.isElementNode(el) ? el : document.querySelector(el)
		this.vm = vm
		// 1.匹配节点内容及指令替换相应的内容, 因为每次匹配替换会导致页面回流和重绘, 所以使用文档碎片对象
		// 获取文档碎片对象, 放入内存中会减少页面的回流和重绘
		const fragment = this.node2Fragment(this.el)

		// 2.编译模版
		this.compile(fragment)

		// 3.追加子元素到根元素
		this.el.appendChild(fragment)

	}

	// 判断是否是元素节点
	isElementNode(node) {
		return node.nodeType === 1
	}

	// 将当前根元素中的所有子元素一层层取出来放到文档碎片中, 以减少页面回流和重绘
	node2Fragment(el) {
		// 创建文档碎片对象
		const fragment = document.createDocumentFragment()
		let firstChild;
		// 将当前el节点对象的所有子节点追加到文档碎片对象中
		while (firstChild = el.firstChild) {
			fragment.appendChild(firstChild)
		}
		return fragment
	}

	// 编译模版, 解析指令
	compile(fragment) {
		// 1.获取到所有的子节点, 当前获取的子节点数组是一个伪数组, 需要转为数组
		const childNodes = [...fragment.childNodes]
		childNodes.forEach(child => {
			// 判断当前节点是元素节点还是文本节点
			if (this.isElementNode(child)) {
				// 编译元素节点
				this.compileElement(child)
			} else {
				// 编译文本节点
				this.compileText(child)
			}
			// 递归遍历当前节点时候还有子节点对象
			if (child.childNodes && child.childNodes.length) {
				this.compile(child)
			}
		})

	}

	// 编译元素节点
	compileElement(node) {
		// 根据不同指令属性, 编译模版信息
		const attributes = [...node.attributes];
		attributes.forEach(attr => {
			// 通过解构将指令的name和value获取到
			const {
				name,
				value
			} = attr
			// 判断当前属性是指令还是原生属性
			if (this.isDirective(name)) {
				// 截取指令, 不需要v-
				const directive = name.split('-')[1]
				// 由于指令格式有 v-text v-html v-bind:属性 v-on:事件等等, 按照 : 再次分割
				const [dirName, eventName] = directive.split(':')
				// 更新数据, 数据驱动视图
				compileUtils[dirName](node, value, this.vm, eventName)
				// 删除有指令的标签上的属性
				node.removeAttribute('v-' + directive)
			} else if (this.isEventName(name)) { // 判断指令是以@开头绑定的事件
				// 截取指令, 不需要@, 这里就省略处理里 @click.stop.prevent等事件修饰符, 原理不难
				const eventName = name.split('@')[1]
				// 更新数据, 数据驱动视图
				compileUtils['on'](node, value, this.vm, eventName)
			}
		})
	}

	// 编译文本节点
	compileText(node) {
		// node.textContent获取文本并且匹配{{}} 模版字符串类型的
		const content = node.textContent
		if (/\{\{(.+?)\}\}/.test(content)) {
			compileUtils['text'](node, content, this.vm)
		}
	}

	// 判断当前属性是指令还是原生属性
	isDirective(attrName) {
		// startsWith() 方法用来判断当前字符串是否以另外一个给定的子字符串开头,并根据判断结果返回 true 或 false。
		return attrName.startsWith('v-')
	}

	// 判断指令是以@开头绑定的事件
	isEventName(attrName) {
		return attrName.startsWith('@')
	}
}

Шаг 2. Реализуйте прослушиватель данных (наблюдатель)

Используйте Obeject.defineProperty() для отслеживания изменений свойств, затем рекурсивно просматривайте объекты данных, которые необходимо наблюдать, включая свойства объектов вложенных свойств, добавляйте сеттеры и геттеры. В этом случае присвоение значения этому объекту вызовет setter , то вы сможете отслеживать изменения данных.

observer.js

// 数据劫持
class Observe {
	constructor(data) {
		this.observe(data)
	}
	// 使用object.defineProperty监听对象, 数组暂时不考虑,太复杂
	observe(data) {
		if (data && typeof data === 'object') {
			// console.log(data);
			Object.keys(data).forEach(key => {
				this.defineReactive(data, key, data[key])
			})
		}
	}

	// 劫持属性
	defineReactive(obj, key, value) {
		// 递归遍历
		this.observe(value)
		// 创建依赖收集器
		const dep = new Dep()
		// console.log(dep);
		Object.defineProperty(obj, key, { // obj为已有对象, key为属性, 第三个参数为属性描述符
			enumerable: true, // enumerable:是否可以被枚举(for in),默认false
			configurable: false, // 是否可以被删除,默认false
			// 获取
			get() {
				// console.log(dep.target);
				// 订阅数据变化时, 往Dep中添加观察者
				Dep.target && dep.addSub(Dep.target)
				return value
			},
			// 设置
			set: (newValue) => {
				// 这里要注意新设置的值也需要劫持他的属性
				this.observe(newValue)
				if (newValue !== value) {
					value = newValue
				}
				// 通知订阅器找到对应的观察者,通知观察者更新视图
				dep.notify()
			}
		})
	}
}

Часть 3. Реализация Watcher для обновления представления

При инициализации экземпляра myvue передайте объект. Добавьте наблюдателя, когда defineProperty() получает свойство, и запускайте notify(), когда set изменяет свойство, чтобы вызвать метод upDate для обновления представления.

// 观察者
class Watcher {
	constructor(vm, expr, cb) {
		this.vm = vm
		this.expr = expr
		this.cb = cb
		// 存储旧值
		this.oldValue = this.getOldValue()
	}
	// 获取旧值
	getOldValue() {
		// 在获取旧值的时候将观察者挂在到Dep订阅器上
		Dep.target = this
		const oldValue = compileUtils.getValue(this.expr, this.vm)
		// 销毁Dep上的观察者
		Dep.target = null
	}

	// 更新视图
	upDate() {
		// 获取新值
		const newValue = compileUtils.getValue(this.expr, this.vm)
		if (newValue !== this.oldValue) {
			this.cb(newValue)
		}
	}
}

// 订阅器
class Dep {
	constructor() {
		this.subs = []
	}
	// 收集观察者
	addSub(watcher) {
		this.subs.push(watcher)
	}
	// 通知观察者去更新视图
	notify() {
		this.subs.forEach(watcher => {
			watcher.upDate()
		})
	}
}

Вопросы для интервью - объясните свое понимание реактивного принципа MVVM.

Vue использует перехват данных для взаимодействия с моделью издатель-подписчик.Он использует Object.defineProperty to() для захвата методов получения и установки каждого свойства.Когда данные изменяются, он публикует сообщение сборщику зависимостей, чтобы уведомить наблюдателя и сделать так, чтобы Соответствующая функция обратного вызова для обновления представления.

В частности: MVVM действует как точка входа для привязки, интегрирует Observe, Compil и Watcher, отслеживает изменения модели через Observe, анализирует и компилирует инструкции шаблона через Compil и, наконец, использует Watcher для создания коммуникационного моста между Observe и Compil, чтобы для достижения изменения данных => обновление представления, изменение представления взаимодействия (ввод) => эффект двусторонней привязки изменения модели данных.

Суммировать

Эта статья в основном посвящена几种实现双向绑定的做法,实现Observer,实现Compile,实现Watcher,实现MVVMЭти модули иллюстрируют принцип и реализацию двусторонней привязки. И в соответствии с процессом идеи я подробно объяснил некоторые идеи и ключевые моменты содержания.Конечно, должно быть много недостатков, но вы должны иметь более глубокое понимание того, как реализовать двустороннюю привязку данных.

Эта статья также подытоживается просмотром статьи об анализе исходного кода Vue и соответствующих видеороликов станции B. Как говорится, хорошая память не так хороша, как плохой текст.Даже если вы скопируете ее, вы будете более впечатленный.

Наконец, спасибо за чтение!

❤️ Сделайте себе одолжение после прочтения

Если вы найдете этот контент вдохновляющим, я хотел бы пригласить вас сделать мне небольшую услугу:

1.подобно, чтобы больше людей могли увидеть этот контент.

2.Обратите внимание на паблик-аккаунт "Tomato Science Front End", Я буду регулярно обновлять и публиковать информацию о внешнем интерфейсе и опыт реализации проекта для вашего ознакомления.