Предисловие:Март и апрель — пиковый сезон найма, и я полагаю, что многих студентов, которые проходят собеседование на позиции фронтенда, спрашивают, каков принцип Vue? Эта статья научит вас, как реализовать основные функции фреймворка Vue самым простым способом. Чтобы снизить ваши затраты на обучение, я научу вас фреймворку Vue самым простым способом.
1. Подготовка
Мы надеемся, что те из вас, кто готов читать эту статью, должны обладать следующими навыками:
- Знаком с синтаксисом ES6.
- Изучите тип узла HTML DOM
- знакомый
Object.defineProperty()использование метода - Базовое использование регулярных выражений. (например, группировка)
Во-первых, мы создаем файл HTML в соответствии со следующим кодом.В этой статье в основном рассказывается, как реализовать следующие функции.
<script src="../src/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 解析插值表达式 -->
<h2>title 是 {{title}}</h2>
<!-- 解析常见指令 -->
<p v-html='msg1' title='混淆属性1'>混淆文本1</p>
<p v-text='msg2' title='混淆属性2'>混淆文本2</p>
<input type="text" v-model="something">
<!-- 双向数据绑定 -->
<p>{{something}}</p>
<!-- 复杂数据类型 -->
<p>{{dad.son.name}}</p>
<p v-html='dad.son.name'></p>
<input type="text" v-model="dad.son.name">
<button v-on:click='sayHi'>sayHi</button>
<button @click='printThis'>printThis</button>
</div>
</body>
let vm = new Vue({
el: '#app',
data: {
title: '手把手教你撸一个vue框架',
msg1: '<a href="#">应该被解析成a标签</a>',
msg2: '<a href="#">不应该被解析成a标签</a>',
something: 'placeholder',
dad: {
name: 'foo',
son: {
name: 'bar',
son: {}
}
}
},
methods: {
sayHi() {
console.log('hello world')
},
printThis() {
console.log(this)
}
},
})
Теперь, когда подготовка завершена, давайте вместе реализуем основные функции фреймворка Vue!
Идеи реализации MVVM
Все мы знаем, что vue — это прогрессивный фреймворк, основанный на шаблоне проектирования MVVM. Итак, как реализовать инфраструктуру MVVM в JavaScript? Есть три основные идеи для реализации MVVM framework:
- backbone.js
Модель издатель-подписчик обычно реализует привязку данных и представлений через публикации и подписки.
- Angular.js
Angular.js определяет, следует ли обновлять представление, сравнивая, изменились ли данные с помощью мониторинга грязных значений. Это похоже на отслеживание того, изменились ли данные с помощью опроса таймера.
- Vue.js
Vue.js использует перехват данных в сочетании с шаблоном издатель-подписчик. До vue2.6 было черезObject.defineProperty()Чтобы перехватить методы установки и получения каждого свойства, публикуйте сообщения подписчикам при изменении данных и запускайте соответствующие обратные вызовы. Это также основная причина, по которой браузеры ниже IE8 не поддерживают vue.
Идеи реализации Vue
- Реализуйте анализатор шаблона компиляции, который может разобрать направления и интерполяционные выражения в шаблоне и назначать соответствующие операции
- Реализуйте прослушиватель данных Observer, который может отслеживать все свойства объекта данных (данных).
- Реализуйте прослушиватель Watcher. Говорите о результатах синтаксического анализа Compile, подключайтесь к объектам, наблюдаемым наблюдателем, устанавливайте отношения, получайте уведомления, когда наблюдатель наблюдает изменения в объектах данных, и обновляйте DOM.
- Создайте общедоступный объект записи (Vue), получите конфигурацию инициализации и скоординируйте модули Compile, Observer и Watcher, то есть Vue.
Описанный выше процесс показан на следующем рисунке:
2. Файл записи Vue
Разобравшись с логикой, мы обнаружим, что то, что мы хотим сделать в этом входном файле, очень простое:
- Подключить данные и методы к корневому экземпляру;
- Используйте модуль Observer для прослушивания изменений во всех свойствах данных.
- Если есть точка монтирования, используйте модуль Compile для компиляции всех инструкций и выражений интерполяции под точкой монтирования.
/**
* vue.js (入口文件)
* 1. 将data,methods里面的属性挂载根实例中
* 2. 监听 data 属性的变化
* 3. 编译挂载点内的所有指令和插值表达式
*/
class Vue {
constructor(options={}){
this.$el = options.el;
this.$data = options.data;
this.$methods = options.methods;
debugger
// 将data,methods里面的属性挂载根实例中
this.proxy(this.$data);
this.proxy(this.$methods);
// 监听数据
// new Observer(this.$data)
if(this.$el) {
// new Compile(this.$el,this);
}
}
proxy(data={}){
Object.keys(data).forEach(key=>{
// 这里的this 指向vue实例
Object.defineProperty(this,key,{
enumerable: true,
configurable: true,
set(value){
if(data[key] === value) return
return value
},
get(){
return data[key]
},
})
})
}
}
3. Скомпилировать модуль
Главное, что делает компиляция, это парсить инструкции (узлы атрибутов) и выражения интерполяции (текстовые узлы), заменять переменные в шаблоне данными, затем инициализировать представление страницы рендеринга и привязывать узел, соответствующий каждой инструкции, к функции обновления , добавьте подписчиков, которые прослушивают данные, после изменения данных получают уведомление и обновляют представление.
Поскольку процесс обхода и синтаксического анализа имеет несколько операций на узле dom, это приведет к сбою страницы.Перекомпоновать и перекраситьПроблема в том, что для повышения производительности и эффективности нам лучше всего разбирать инструкции и выражения интерполяции в памяти, поэтому нам нужно пройти весь контент под точкой монтирования и сохранить его в DocumentFragments.
DocumentFragments — это узлы DOM. Они не являются частью основного дерева DOM. Обычно используется для создания фрагмента документа, добавления элементов к фрагменту документа, а затем добавления фрагмента документа к дереву DOM. Поскольку фрагмент документа находится в памяти, а не в дереве DOM, вставка дочерних элементов во фрагмент документа не вызывает переформатирование страницы (вычисление положения и геометрии элемента). Таким образом, использование фрагментов документа обычно приводит к повышению производительности.
Итак, нам нуженnode2fragment()метод для обработки вышеуказанной логики.
Реализуйте node2fragment и сохраните все узлы в точке монтирования в DocumentFragment.
node2fragment(node) {
let fragment = document.createDocumentFragment()
// 把el中所有的子节点挨个添加到文档片段中
let childNodes = node.childNodes
// 由于childNodes是一个类数组,所以我们要把它转化成为一个数组,以使用forEach方法
this.toArray(childNodes).forEach(node => {
// 把所有的字节点添加到fragment中
fragment.appendChild(node)
})
return fragment
}
this.toArray()Это метод класса, который я инкапсулирую для преобразования массива класса в массив. Способ реализации тоже очень простой, я использовал самые распространенные приемы в разработке:
toArray(classArray) {
return [].slice.call(classArray)
}
Разобрать узлы во фрагменте
Следующее, что нам нужно сделать, это разобрать узлы во фрагменте:compile(fragment).
Логика этого метода также очень проста, нам нужно рекурсивно обойти все дочерние узлы фрагмента и судить по типу узла, если это текстовый узел, то он будет разобран по интерполяционному выражению, а если это узел атрибута, он будет разобран в соответствии с инструкцией. При анализе узла атрибута мы должны дополнительно судить: определяется ли онv-команду в начале или специальные символы, такие как@,:команду в начале.
// Compile.js
class Compile {
constructor(el, vm) {
this.el = typeof el === "string" ? document.querySelector(el) : el
this.vm = vm
// 解析模板内容
if (this.el) {
// 为了避免直接在DOM中解析指令和差值表达式所引起的回流与重绘,我们开辟一个Fragment在内存中进行解析
const fragment = this.node2fragment(this.el)
this.compile(fragment)
this.el.appendChild(fragment)
}
}
// 解析fragment里面的节点
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node => {
// 如果是元素节点,则解析指令
if (this.isElementNode(node)) {
this.compileElementNode(node)
}
// 如果是文本节点,则解析差值表达式
if (this.isTextNode(node)) {
this.compileTextNode(node)
}
// 递归解析
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
}
Логика, которая обрабатывает инструкции по разбору: CompileUtils
Следующее, что нам нужно сделать, это проанализировать инструкцию и уведомить представление о результате анализа.
Когда данные изменяются, объект-наблюдатель используется для отслеживания изменений данных expr, и как только данные изменяются, выполняется функция обратного вызова.
new Watcher(vm,expr,callback)// Используйте Watcher, чтобы вернуть проанализированный результат в представление.
Мы можем инкапсулировать всю логику для обработки прагм и выражений интерполяции вcompileUtilобъекты находятся под управлением.
Есть два подводных камня, на которые нужно обратить внимание:
- В случае сложных данных, таких как выражения интерполяции:
{{dad.son.name}}или<p v-text='dad.son.name'></p>Мы получилиv-textЗначением свойства является строкаdad.son.name, мы не можем пройтиvm.$data['dad.son.name']получить данные, но передатьvm.$data['dad']['son']['name']форму для получения данных. Следовательно, если данные представляют собой сложные данные, нам необходимо реализоватьgetVMData()а такжеsetVMData()способ получения и изменения данных. - В Vue, это в способе в методах указывает на экземпляр VUE, поэтому когда мы пройдем
v-onКогда инструкция привязывает метод к узлу, нам нужно привязать этот метод метода как экземпляр vue.
// Compile.js
let CompileUtils = {
getVMData(vm, expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
},
setVMData(vm, expr,value) {
let data = vm.$data
let arr = expr.split('.')
arr.forEach((key,index) => {
if(index < arr.length -1) {
data = data[key]
} else {
data[key] = value
}
})
},
// 解析插值表达式
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
let expr = RegExp.$1
node.textContent = txt.replace(reg, this.getVMData(vm, expr))
new Watcher(vm, expr, newValue => {
node.textContent = txt.replace(reg, newValue)
})
}
},
// 解析v-text
text(node, vm, expr) {
node.textContent = this.getVMData(vm, expr)
new Watcher(vm, expr, newValue => {
node.textContent = newValue
})
},
// 解析v-html
html(node, vm, expr) {
node.innerHTML = this.getVMData(vm, expr)
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue
})
},
// 解析v-model
model(node, vm, expr) {
let that = this
node.value = this.getVMData(vm, expr)
node.addEventListener('input', function () {
// 下面这个写法不能深度改变数据
// vm.$data[expr] = this.value
that.setVMData(vm,expr,this.value)
})
new Watcher(vm, expr, newValue => {
node.value = newValue
})
},
// 解析v-on
eventHandler(node, vm, eventType, expr) {
// 处理methods里面的函数fn不存在的逻辑
// 即使没有写fn,也不会影响项目继续运行
let fn = vm.$methods && vm.$methods[expr]
try {
node.addEventListener(eventType, fn.bind(vm))
} catch (error) {
console.error('抛出这个异常表示你methods里面没有写方法\n', error)
}
}
}
В-четвертых, модуль Observer
На самом деле в модуле Observer нам нечего делать, это предоставитьwalk()метод, рекурсивно перехваченныйvm.$dataВсе данные в контроле, перехватывающие сеттеры и геттеры. Если данные изменяются, опубликуйте уведомление, чтобы все подписчики могли обновить содержимое и изменить представление.
Следует отметить, что если установленное значение является объектом, нам необходимо убедиться, что объект также является реактивным.
В коде описано:walk(aObjectValue). Что касается реализации реактивных объектов, мы используем следующий подход:Object.defineProperty()
Полный код выглядит следующим образом:
// Observer.js
class Observer {
constructor(data){
this.data = data
this.walk(data)
}
// 遍历walk中所有的数据,劫持 set 和 get方法
walk(data) {
// 判断data 不存在或者不是对象的情况
if(!data || typeof data !=='object') return
// 拿到data中所有的属性
Object.keys(data).forEach(key => {
// console.log(key)
// 给data中的属性添加 getter和 setter方法
this.defineReactive(data,key,data[key])
// 如果data[key]是对象,深度劫持
this.walk(data[key])
})
}
// 定义响应式数据
defineReactive(obj,key,value) {
let that = this
// Dep消息容器在Watcher.js文件中声明,将Observer.js与Dep容器有关的代码注释掉并不影响相关逻辑。
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable: true,
get(){
// 如果Dep.target 中有watcher 对象,则存储到订阅者数组中
Dep.target && dep.addSub(Dep.target)
return value
},
set(aValue){
if(value === aValue) return
value = aValue
// 如果设置的值是一个对象,那么这个对象也应该是响应式的
that.walk(aValue)
// watcher.update
// 发布通知,让所有订阅者更新内容
dep.notify()
}
})
}
}
5. Модуль наблюдателя
Роль наблюдателя заключается в том, чтобы связать результаты анализа компиляции с объектами, наблюдаемыми наблюдателем, для установления связи.Когда данные, наблюдаемые наблюдателем, изменяются, получать уведомления (dep.notify) сообщает Watcher, что Watcher обновляет DOM через Compile. Это включает в себя идею модели издатель-подписчик.
Watcher — это мост, соединяющий Compile и Observer.
В конструктор Watcher нам нужно передать три параметра:
-
vm: экземпляр vue -
expr: Имя данных в vm.$data (ключ) -
callback: Функция обратного вызова, которая выполняется при изменении данных.
Обратите внимание, что для того, чтобы получить объект глубоких данных, здесь нам нужно обратиться к объявленному ранееgetVMData()метод.
Определить наблюдателя
constructor(vm,expr,callback){
this.vm = vm
this.expr = expr
this.callback = callback
//
this.oldValue = this.getVMData(vm,expr)
//
}
Предоставьте метод update() для обновления страницы при обновлении данных.
Когда мы должны обновить страницу?
Мы должны реализовать метод обновления в Watcher для сравнения нового значения со старым значением. При изменении данных выполняется функция обратного вызова.
update() {
// 对比expr是否发生改变,如果改变则调用callback
let oldValue = this.oldValue
let newValue = this.getVMData(this.vm,this.expr)
// 变化的时候调用callback
if(oldValue !== newValue) {
this.callback(newValue,oldValue)
}
}
Ассоциированный наблюдатель и компилятор
vm.msgКогда значение expr необходимо повторно отобразить, нам также нужно прослушивать изменения значения expr через Watcher.
// compile.js
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
let expr = RegExp.$1
node.textContent = txt.replace(reg, this.getVMData(vm, expr))
// 侦听expr值的变化。当expr的值发生改变时,执行回调函数
new Watcher(vm, expr, newValue => {
node.textContent = txt.replace(reg, newValue)
})
}
},
Итак, когда мы должны вызывать метод обновления и запускать функцию обратного вызова?
Поскольку выше мы реализовали адаптивные данные в Observer, метод set неизбежно будет запускаться при изменении данных. Поэтому, когда мы запускаем метод set, нам также необходимо вызвать метод watcher.update, вызвать функцию обратного вызова и изменить страницу.
// observer.js
defineReactive(obj,key,value) {
...
set(aValue){
if(value === aValue) return
value = aValue
// 如果设置的值是一个对象,那么这个对象也应该是响应式的
that.walk(aValue)
watcher.update
}
}
Итак, вопрос в том, когда мы анализируем разные инструкции, у нас появляется много новых Наблюдателей, так какой же метод обновления Наблюдателя здесь следует вызывать? Как уведомить всех Наблюдателей об изменении данных?
Итак, появляется новая концепция: модель издатель-подписчик.
Что такое шаблон издатель-подписчик?
Шаблон издатель-подписчик также называется шаблоном наблюдателя. Он определяет отношение зависимости «один ко многим», то есть при изменении состояния объекта все объекты, которые зависят от него, будут уведомлены и автоматически обновлены, что решает функциональную связь между объектом-субъектом и наблюдателем.
Здесь мы используем общедоступную учетную запись WeChat в качестве примера, чтобы проиллюстрировать эту ситуацию.
Например, если один из наших классов подписывается на официальный счет, то все в этом классе являются подписчиком, а официальный счет - издатель. Если однажды официальная учетная запись считает, что содержание статьи неверно и необходимо изменить опечатки (изменить данные в VM. $ Данные), необходимо ли уведомить каждого подписчика? Это не может быть, что статья о академическом комитете изменилась, но статья лидера класса не изменилась. В этом процессе издателя не нужно заботиться, кто подписывается на него, но нужно только для толкания этого обновленного сообщения всем подписчикам (уведомлять).
Таким образом, есть два процесса, связанные здесь:
- Добавьте подписчиков:
addSub(watcher) - Всплывающие уведомления:
notify(){ sub.update() }
В этом процессе роль издателя — это объект, от которого зависит каждый подписчик.
Определяем класс в Watcher: Dep (зависимый контейнер). Каждый раз, когда мы создаем нового Watcher, мы добавляем подписчиков в Dep. Как только данные наблюдателя изменятся, уведомите Dep, чтобы инициировать уведомление (notify), и выполните функцию обновления, чтобы изменить DOM.
// watcher.js
// 订阅者容器,依赖收集
class Dep {
constructor(){
// 初始化一个空数组,用来存储订阅者
this.subs = []
}
// 添加订阅者
addSub(watcher){
this.subs.push(watcher)
}
// 通知
notify() {
// 通知所有的订阅者更改页面
this.subs.forEach(sub => {
sub.update()
})
}
}
Далее наша идея очень понятна, то есть каждый раз, когда новый Watcher новый, он сохраняется в контейнере Dep. Собирается связать Депа с Наблюдателем. Мы можем добавить цель атрибута класса в Dep для хранения объекта Watcher, то есть нам нужно присвоить это Dep.target в конструкторе Watcher.
- Сначала мы войдем в Observer, чтобы перехватить сообщение данных в data, здесь мы введем метод get в Observer;
- После угона оценим, существует ли el, и если да, то скомпилируем интерполяционное выражение и введем Compile;
- Если перехваченное сообщение с данными изменится в это время, наблюдатель в усах будет прослушивать изменение данных;
- В конструкторе Watcher передайте
this.oldValue = this.getVMData(vm, expr)Метод один раз войдет в метод get в Observer, а затем программа будет выполнена.
Так что найти время добавления подписчиков нам не составит труда.Код такой:
- Добавьте Watcher в массив подписчиков, и если данные изменятся, отправьте уведомление всем подписчикам.
// Observer.js
// 定义响应式数据
defineReactive(obj,key,value) {
// defineProperty 会改变this指向
let that = this
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable: true,
get(){
// 如果Dep.target存在,即存在watcher 对象,则存储到订阅者数组中
// debugger
Dep.target && dep.addSub(Dep.target)
return value
},
set(aValue){
if(value === aValue) return
value = aValue
// 如果设置的值是一个对象,那么这个对象也应该是响应式的
that.walk(aValue)
// watcher.update
// 发布通知,让所有订阅者更新内容
dep.notify()
}
})
}
- После сохранения Watcher в контейнере Dep установите Dep.target пустым, чтобы Watcher можно было сохранить в следующий раз.
// Watcher.js
constructor(vm,expr,callback){
this.vm = vm
this.expr = expr
this.callback = callback
Dep.target = this
// debugger
this.oldValue = this.getVMData(vm,expr)
Dep.target = null
}
Полный код Watcher.js выглядит следующим образом:
// Watcher.js
class Watcher {
/**
*
* @param {*} vm 当前的vue实例
* @param {*} expr data中数据的名字
* @param {*} callback 一旦数据改变,则需要调用callback
*/
constructor(vm,expr,callback){
this.vm = vm
this.expr = expr
this.callback = callback
Dep.target = this
this.oldValue = this.getVMData(vm,expr)
Dep.target = null
}
// 对外暴露的方法,用于更新页面
update() {
// 对比expr是否发生改变,如果改变则调用callback
let oldValue = this.oldValue
let newValue = this.getVMData(this.vm,this.expr)
// 变化的时候调用callback
if(oldValue !== newValue) {
this.callback(newValue,oldValue)
}
}
// 只是为了说明原理,这里偷个懒,就不抽离出公共js文件了
getVMData(vm,expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
}
}
class Dep {
constructor(){
this.subs = []
}
// 添加订阅者
addSub(watcher){
this.subs.push(watcher)
}
// 通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
На данный момент мы реализовали основные функции фреймворка Vue.
В этой статье только простейшим образом моделируются основные функции фреймворка vue, поэтому многие детали и качество кода определенно будут принесены в жертву, пожалуйста, простите меня.
В тексте неизбежно будут какие-то неточности.Добро пожаловать на поправку.Если интересно,можете пообщаться между собой.