Зная, что ничего не поделаешь, и будь уверен
—— —— >
предисловие
Это серия статей об исходном коде Vue, рекомендуется начать с первой статьиСерия исходного кода Vue (1): правильная позиция интерпретации исходного кода VueНачните читать. Статья представляет собой процесс моего личного изучения исходного кода, я делюсь им здесь и надеюсь, что он будет полезен всем.
Эта статья является предыдущей статьей:Серия исходного кода Vue (3): принцип реагирования на данныедобавка. Основное содержание — это принцип и разница в скорости реагирования на данные между Vue2.X и Vue3.X, дополняющие режим публикации-подписки и режим наблюдателя, и, наконец, минимальная скорость реагирования на данные.
В предыдущей статье мы говорили о методе адаптивного анализа исходного кода:defineReactive(), который является основным кодом Vue для адаптивной обработки. Для тех, кто не помнит, можете нажатьСерия исходного кода Vue (3): принцип реагирования на данныеОглядываясь назад, даdefineReactive()Объяснение есть в статьекодовый блок 7.
Далее введите содержание статьи 👇 👇
Ядро VueJs включает в себя набор «отзывчивых систем». «Реактивный» означает, что при изменении данных Vue уведомит об этом код, использующий эти данные. Например, данные используются при рендеринге представления, и при изменении данных представление автоматически обновляется.
Далее мы пойдем от мелкого к глубокому, чтобы понятьVue2.Xа такжеVue3.Xответный принцип.
Принцип отзывчивости Vue2.X
Сначала поговоримVue2.XПринцип отзывчивости.
Люди много не говорят, просто переходите к коду 😂 😂
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title> Vue2.X 单属性的数据响应式 </title>
</head>
<body>
<div id="app"></div>
<script>
// 模拟Vue中的data选项
let data = {
msg: 'hello world'
}
// 模拟Vue的实例
let vm = {}
// 数据劫持:当访问或者设置vm中的成员时,做一些劫持后操作
Object.defineProperty(vm, 'msg', {
// 当获取值的时候执行
get () {
console.log('get: ', data.msg)
return data.msg
},
// 当设置值的时候执行
set (newValue) {
console.log('set: ', newValue)
if (newValue === data.msg) {
return
}
data.msg = newValue
// 数据更改时更新DOM的值
document.querySelector('#app').textContent = data.msg
}
})
// 测试一下 o(* ̄︶ ̄*)o
vm.msg = 'Hello VueJs'
console.log(vm.msg)
</script>
</body>
</html>
Запустив код, мы видим, что изменениеvm.msgзначение, вызывающее перехват данных
Вы также можете попробовать изменить его, чтобы узнать, как вызвать перехват данных.
Приведенное выше поддерживает отзывчивость только одного свойства. Что, если вы хотите поддерживать несколько свойств? код напрямую.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title> Vue2.X 多属性的数据响应式 </title>
</head>
<body>
<div id="app"></div>
<script>
// 模拟Vue中的data选项
let data = {
msg: 'hello vue',
value: 7
}
// 模拟Vue的实例
let vm = {}
proxyData(data)
function proxyData(data) {
// 遍历data对象中的所有属性
Object.keys(data).forEach(key => {
// 把data中的属性,转换成vm的setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('get: ', key, data[key])
return data[key]
},
set (newValue) {
console.log('set: ', key, newValue)
if (newValue === data[key]) {
return
}
data[key] = newValue
// 数据更改,使DOM的值更新
document.querySelector('#app').textContent = data[key]
}
})
})
}
// 测试一下 o(* ̄︶ ̄*)o
vm.msg = 'Hello Vue'
console.log(vm.msg)
</script>
</body>
</html>
Попробуйте посмотреть, поддерживает ли он отзывчивость с несколькими свойствами.
Принцип отзывчивости Vue3.X
Отзывчивая реализация Vue3.X и Vue2.X отличается.Vue3.X реализован с использованием метода Proxy в ES6. Некоторые из друзей здесь могут быть не слишком знакомы с методом прокси, поэтому давайте сначала кратко поговорим о нем:
ProxyОбъекты используются для создания прокси для объекта, что позволяет перехватывать и настраивать основные операции (такие как поиск свойств, назначение, перечисление, вызовы функций и т. д.).
грамматика
const p = new Proxy(target, handler)
параметр
-
target
: нужно использоватьProxy
Обернутый целевой объект (может быть объект любого типа, включая собственные массивы, функции или даже другой прокси). -
handler
: объект, который обычно имеет функции в качестве атрибутов, и функции в каждом атрибуте определяют прокси при выполнении различных операций.p
поведение.
Если вы хотите узнать больше, вы можете нажатьПодробное объяснение MDNУзнать больше.
как использоватьProxyДля реализации адаптивного принципа Vue3.X мы пока много не болтаем, просто переходим к коду 😂 😂
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue3.X 数据响应式</title>
</head>
<body>
<div id="app"> </div>
<script>
// 模拟Vue中的data选项
let data = {
msg: 'hello vue',
value: 7
}
// 模拟Vue实例
let vm = new Proxy(data, {
// 执行代理行为的函数 当访问vm的成员会执行
get (target, key) {
console.log('get, key: ', key, target[key])
return target[key]
},
// 当设置vm的成员会执行
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
// 测试一下 o(* ̄︶ ̄*)o
vm.msg = 'Hello Vue'
console.log(vm.msg)
</script>
</body>
</html>
Попробуйте и посмотрите, как это работает.
👌🏻 идеально! Давайте посмотрим на разницу, хотя уже очень ясно, но и суммировать.
Отзывчивые принципы Vue2.X и Vue3.X
Сначала Vue 2.x
- Основной принцип: Object.defineProperty
- Непосредственно контролировать свойства
- Браузер, совместимый с IE8 или выше (не совместим с IE8)
Вслед за Vue3.X
- Основной принцип: прокси
- Слушайте непосредственно объекты, а не свойства
- Новый метод в ES6, не поддерживает браузер IE
Шаблон публикации-подписки и шаблон наблюдателя
Далее давайте углубимся в принцип отзывчивости Vue, затем нам нужно поговорить о режиме публикации-подписки и режиме наблюдателя.
модель публикации-подписки
Что такое модель публикации-подписки?
Основываясь на центре событий, получите уведомление целевого подписчика, вам необходимо подписаться на событие, объект, который инициировал событие, издатель, издатель, инициировав события, уведомить отдельных подписчиков.
Например 🌰 : Вы все подписались на официальный аккаунт? Например: разработчики WeChat, подборки Qiwu и т. д. Здесь задействованы две роли: официальный аккаунт (центр событий) и все, кто подписался на официальный аккаунт (подписчики). Тогда, когда автор официального аккаунта опубликует статью, новости получат все, кто подпишется на официальный аккаунт.Тут другая роль: автор (издатель) официального аккаунта.
Шина событий в vue — это используемая модель публикации-подписки.
Далее буду симулировать и реализовывать пользовательские события во Vue, людей, которые говорят безжалостно, пока не так много, поэтому перейду непосредственно к коду.
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发布订阅模式</title>
</head>
<body>
<script>
// 事件触发器
class EventEmitter {
// 事件中心
constructor () {
// 创建的对象原型属性为null
this.subs = Object.create(null)
}
// 注册事件
$on (eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// 触发事件
$emit (eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler()
})
}
}
}
// 测试一下 o(* ̄︶ ̄*)o
let em = new EventEmitter()
// 注册事件(订阅消息)
em.$on('click', () => {
console.log('click1')
})
em.$on('click', () => {
console.log('click2')
})
// 触发事件(发布消息)
em.$emit('click')
</script>
</body>
</html>
Проверьте это, вы также можете скопировать код, чтобы испытать его:
Легко ли понять 😆 😆 😆
Шаблон наблюдателя
Целевой объект и объект-наблюдатель взаимозависимы.Наблюдатель наблюдает за состоянием объекта.Если состояние объекта изменится, он уведомит всех наблюдателей, зависящих от объекта.
По сравнению с режимом публикации-подписки в режиме наблюдателя на один центр событий меньше, а подписчик и издатель не связаны напрямую.
- целевой объект[Subject]: собственный метод: [добавить/удалить/уведомить] Observer;
- объект наблюдателя[Observer]: Собственный метод: получить уведомление об изменении состояния субъекта и обработать его;
цель[Subject]При изменении состояния уведомить все наблюдаемые объекты [Observer].
Реагирующее изменение данных в Vue — это режим наблюдателя.Как мы уже узнали из предыдущей статьи об анализе исходного кода, каждый адаптивный атрибут имеет dep, а dep хранит наблюдатель, который зависит от этого атрибута (наблюдатель — это функция, которая наблюдает за изменениями данных). , если данные изменятся, dep уведомит всех наблюдателей о вызове метода update. Следовательно, наблюдатель должен быть собран целевым объектом, чтобы уведомить всех наблюдателей, которые от него зависят. Некоторые друзья здесь могут спросить: почему dep хранится в наблюдателе? Причина в том, что выполняющемуся в данный момент наблюдателю необходимо знать, какой dep уведомил себя в это время.
Наблюдатель (Подписчик) - Наблюдатель
- update(): что делать, когда происходит событие
Таргетинг (издатель) – отдел
- массив subs: хранить всех наблюдателей
- addSub(): добавить наблюдателя
- notify(): вызывать update() всех наблюдателей, когда происходит событие
нет центра событий
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>观察者模式</title>
</head>
<body>
<script>
// 目标者(发布者)
class Dep {
constructor () {
// 记录所有的订阅者
this.subs = []
}
// 添加订阅者
addSub (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 观察者(订阅者)
class Watcher {
update () {
console.log('update')
}
}
// 测试一下 o(* ̄︶ ̄*)o
let dep = new Dep()
let watcher = new Watcher()
let watcher1 = new Watcher()
// 添加订阅
dep.addSub(watcher)
dep.addSub(watcher1)
// 开启通知
dep.notify()
</script>
</body>
</html>
Хотя это уже очевидно, давайте проверим это.
Братья, чувствуете ли вы внезапное осознание, когда видите это? "Ха-ха... Ты чувствуешь себя немного 🌬 🐂 🍺?"
Разница между режимом публикации-подписки и режимом наблюдателя
Подведем итоги с трех точек зрения:
структурный анализ
- В режиме наблюдателя всего две роли: наблюдатель и цель (также называемая наблюдаемой)
- В модели публикации-подписки есть не только издатели и подписчики, но и центр событий (также называемый центром управления)
Проанализируйте отношения
- Наблюдатель и цель слабо связаны
- Издатели и подписчики, связи нет вообще
Анализ с точки зрения использования
- Режим наблюдателя, в основном используемый внутри одного приложения (как упоминалось выше, реагирующие изменения данных в Vue — это режим наблюдателя).
- Модель публикации-подписки больше применима к кросс-приложенным моделям, таким как наша широко используемаяПО промежуточного слоя сообщений
Поясню на картинке
Простой отклик Vue
Все готово, так что давайте начнем с нашей сегодняшней темы. Беспощадных слов еще не много, так что сразу переходим к коду.
Код приложения (index.html)
Во-первых, используйте код. Затем в соответствии с кодом функции добавляется код каждой части.
<body>
<div id="app">
<div>
<input v-model="msg" />
<span> {{ msg }} </span>
<p v-text="msg"></p>
</div>
<br>
<div>
<input v-model="value" />
<span> {{ value }} </span>
<p v-text="value"></p>
</div>
</div>
<script src="./dep.js"></script>
<script src="./watcher.js"></script>
<script src="./compiler.js"></script>
<script src="./observer.js"></script>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'hello vue',
value: 7,
},
})
</script>
</body>
класс vue "vue.js"
/**
* vue.js
*
* 属性
* - $el:挂载的dom对象
* - $data: 数据
* - $options: 传入的属性
*
* 方法:
* - _proxyData 将数据转换成getter/setter形式
*
*/
class Vue {
constructor(options) {
// 获取传入的对象 默认为空对象
this.$options = options || {}
// 获取 el (#app)
this.$el =
typeof options.el === 'string'
? document.querySelector(options.el)
: options.el
// 获取data 默认为空对象
this.$data = options.data || {}
// 调用_proxyData处理data中的属性
this._proxyData(this.$data)
// 使用Obsever把data中的数据转为响应式 并监测数据的变化,渲染视图
new Observer(this.$data)
// 编译模板 渲染视图
new Compiler(this)
}
// 把data中的属性注册到Vue
_proxyData(data) {
// 遍历data对象的所有属性 进行数据劫持
Object.keys(data).forEach((key) => {
// 把data中的属性,转换成vm的getter/setter
Object.defineProperty(this, key, {
// 可枚举(可遍历)
enumerable: true,
// 可配置(可以使用delete删除,可以通过defineProperty重新定义)
configurable: true,
// 获取值的时候执行
get() {
return data[key]
},
// 设置值的时候执行
set(newValue) {
// 若新值等于旧值则返回
if (newValue === data[key]) {
return
}
// 如新值不等于旧值则赋值
data[key] = newValue
},
})
})
}
}
Наблюдатель "observer.js"
/**
* observer.js
*
* 功能
* - 把$data中的属性,转换成响应式数据
* - 如果$data中的某个属性也是对象,把该属性转换成响应式数据
* - 数据变化的时候,发送通知
*
* 方法:
* - walk(data) - 遍历data属性,调用defineReactive将数据转换成getter/setter
* - defineReactive(data, key, value) - 将数据转换成getter/setter
*
*/
class Observer {
constructor(data) {
this.walk(data)
}
// 遍历data转为响应式
walk(data) {
// 如果data为空或者或者data不是对象
if (!data || typeof data !== "object") {
return;
}
// 遍历data转为响应式
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key])
})
}
// 将data中的属性转为getter/setter
defineReactive(data, key, value) {
// 检测属性值是否是对象,是对象的话,继续将对象转换为响应式的
this.walk(value)
// 保存一下 this
const that = this;
// 创建Dep对象 给每个data添加一个观察者
let dep = new Dep();
Object.defineProperty(data, key, {
// 可枚举(可遍历)
enumerable: true,
// 可配置(可以使用delete删除,可以通过defineProperty重新定义)
configurable: true,
// 获取值的时候执行
get() {
// 在这里添加观察者对象 Dep.target 表示观察者
Dep.target && dep.addSub(Dep.target)
return value
},
// 设置值的时候执行
set(newValue) {
// 若新值等于旧值则返回
if (newValue == value) {
return;
}
// 如新值不等于旧值则赋值 此处形成了闭包,延长了value的作用域
value = newValue;
// 赋值以后检查属性是否是对象,是对象则将属性转换为响应式的
that.walk(newValue);
// 数据变化后发送通知,触发watcher的pudate方法
dep.notify();
},
})
}
}
Компилятор "compiler.js"
/**
* compiler.js
*
* 功能
* - 编译模板,解析指令/插值表达式
* - 负责页面的首次渲染
* - 数据变化后,重新渲染视图
*
* 属性
* - el -app元素
* - vm -vue实例
*
* 方法:
* - compile(el) -编译入口
* - compileElement(node) -编译元素(指令)
* - compileText(node) 编译文本(插值)
* - isDirective(attrName) -(判断是否为指令)
* - isTextNode(node) -(判断是否为文本节点)
* - isElementNode(node) - (判断是否问元素节点)
*/
class Compiler {
constructor(vm) {
// 获取vm
this.vm = vm
// 获取el
this.el = vm.$el
// 编译模板 渲染视图
this.compile(this.el)
}
// 编译模板渲染视图
compile(el) {
// 不存在则返回
if (!el) return;
// 获取子节点
const nodes = el.childNodes;
//收集
Array.from(nodes).forEach((node) => {
// 文本类型节点的编译
if (this.isTextNode(node)) {
// 编译文本节点
this.compileText(node)
} else if (this.isElementNode(node)) {
// 编译元素节点
this.compileElement(node)
}
// 判断是否还存在子节点
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 添加指令方法 并且执行
update(node, value, attrName, key) {
// 定义相应的方法 举个例子:添加textUpdater就是用来处理v-text的
const updateFn = this[`${attrName}Updater`];
// 若存在 则调用
updateFn && updateFn.call(this, node, value, key);
}
// 用来处理v-text
textUpdater(node, value, key) {
node.textContent = value;
}
// 用来处理v-model
modelUpdater(node, value, key) {
node.value = value;
// 用来实现双向数据绑定
node.addEventListener("input", (e) => {
this.vm[key] = node.value;
});
}
// 编译元素节点
compileElement(node) {
// 获取到元素节点上面的所有属性进行遍历
Array.from(node.attributes).forEach((attr) => {
// 获取属性名
let _attrName = attr.name
// 判断是否是 v- 开头
if (this.isDirective(_attrName)) {
// 删除 v-
const attrName = _attrName.substr(2);
// 获取属性值 并赋值给key
const key = attr.value;
const value = this.vm[key];
// 添加指令方法
this.update(node, value, attrName, key);
// 数据更新之后,通过wather更新视图
new Watcher(this.vm, key, (newValue) => {
this.update(node, newValue, attrName, key);
});
}
});
}
// 编译文本节点
compileText(node) {
// . 表示任意单个字符,不包含换行符、+ 表示匹配前面多个相同的字符、?表示非贪婪模式,尽可能早的结束查找
const reg = /\{\{(.+?)\}\}/;
// 获取节点的文本内容
var param = node.textContent;
// 判断是否有 {{}}
if (reg.test(param)) {
// $1 表示匹配的第一个,也就是{{}}里面的内容
// 去除 {{}} 前后空格
const key = RegExp.$1.trim();
// 赋值给node
node.textContent = param.replace(reg, this.vm[key]);
// 编译模板的时候,创建一个watcher实例,并在内部挂载到Dep上
new Watcher(this.vm, key, (newValue) => {
// 通过回调函数,更新视图
node.textContent = newValue;
});
}
}
// 判断元素的属性是否是vue指令
isDirective(attrName) {
return attrName && attrName.startsWith("v-");
}
// 判断是否是文本节点
isTextNode(node) {
return node && node.nodeType === 3;
}
// 判断是否是元素节点
isElementNode(node) {
return node && node.nodeType === 1;
}
}
Деп "dep.js"
/**
* dep.js
*
* 功能
* - 收集观察者
* - 触发观察者
*
* 属性
* - subs: Array
* - target: Watcher
*
* 方法:
* - addSub(sub): 添加观察者
* - notify(): 触发观察者的update
*
*/
class Dep {
constructor() {
// 存储观察者
this.subs = []
}
// 添加观察者
addSub(sub) {
// 判断观察者是否存在、是否拥有update且typeof为function
if (sub && sub.update && typeof sub.update === "function") {
this.subs.push(sub);
}
}
// 发送通知
notify() {
// 触发每个观察者的更新方法
this.subs.forEach((sub) => {
sub.update()
})
}
}
Наблюдатель "watcher.js"
/**
* watcher.js
*
* 功能
* - 生成观察者更新视图
* - 将观察者实例挂载到Dep类中
* - 数据发生变化的时候,调用回调函数更新视图
*
* 属性
* - vm: vue实例
* - key: 观察的键
* - cb: 回调函数
*
* 方法:
* - update()
*
*/
class Watcher {
constructor(vm, key, cb) {
// 获取vm
this.vm = vm
// 获取data中的属性
this.key = key
// 回调函数(更新视图的具体方法)
this.cb = cb
// 将watcher实例挂载到Dep
Dep.target = this
// 缓存旧值
this.oldValue = vm[key]
// get值之后,清除Dep中的实例
Dep.target = null
}
// 观察者中的方法 用来更新视图
update() {
// 调用update的时候,获取新值
let newValue = this.vm[this.key]
// 新值和旧值相同则不更新
if (newValue === this.oldValue) return
// 调用具体的更新方法
this.cb(newValue)
}
}
Проверьте это.
Вот и все. Не стесняйтесь ставить лайки, добавлять в избранное и подписываться 🙏 🙏 .