前言
数据变化反应到视图-指令操作,首先我们需要了解一下数据响应式,建议看之前先了解 Object.defineProperty属性;
实现数据响应式
- 对象属性拦截(vue2) Object.defineProperty(obj, key, {})// 原生js方法
- 对象整体代理(vue3) Proxy
- 注意: 对象属性拦截,不管使用哪种方式,道理都是相通的
- Object.defineProperty 是劫持对象属性,Proxy 是劫持对象整体
- 所以说 Proxy 比 Object.defineProperty 方便快捷的多,也是vue3性能提升的关键
实现 vue指令 v-text
v-text 声明式的指令版本实现:
目标: 一旦data中的属性发生变化之后,标记的v-text的dom标签的文本内容会立刻得到更新;
核心: 不管是指令也好还是插值表达式也好,它们都是数据和视图之间建立关联的“标识”,所以本质就是通过一定的手段找到符合标识的dom元素,然后把数据放上去,每当数据发生变化,就重新执行一遍放置数据的操作;
实现步骤:
- 先通过标识查找把数据放到对应的dom上显示出来;
- 数据变化之后再次执行将最新的值放到对应的dom上;
html代码:
<!-- v-text 声明式的指令版本实现 -->
<div id="orange">
<p v-text="name1" class="pppp"></p>
<p v-text="name2" class="pppp"></p>
</div>
js代码:
// 1. 通过标识查找把数据放到对应的dom上显示出来
function init() {
let app = document.getElementById('orange'); // 根节点
// 拿到所有子节点
const childNodes = app.childNodes || []; // 所有类型的节点,包括文本节点和标签节点
console.log('childNodes: ', childNodes)
// 筛选标签(元素)节点,nodeType为1的元素节点;
childNodes.forEach(node => {
if (node.nodeType === 1) { // 进行筛选v-text 属性:
// 获取该节点的所有属性(是map结构)
const attrs = node.attributes;
console.log('attrs: ', attrs) // attrs: { 0: v-text, 1: class, v-text: v-text, class: class, length: 2 }
// 将类数组转换为真正的数组 Array.from(map)
Array.from(attrs).forEach(attr => {
console.log('attr: ', attr) // attr: v-text="name", attr: v-text="class="pppp"
const nodeName = attr.nodeName;
const nodeValue = attr.nodeValue;
// nodeName -> "v-text" 就是我们需要找的标识
// nodeValue -> "name" ,也就是data中对应的数据key
// 把data中的数据,放到满足标识上的dom上
if (nodeName === 'v-text') {
node.innerText = data[nodeValue];
}
})
}
})
}
let data = { name1: '小熊猫', name2: '小熊猫' };
init();
到此我们就已经实现了第一步 v-text 初始赋值,所以接下来就是需要利用 Object.defineProperty 实现响应式;
js代码:
// 2. 数据变化之后再次执行将最新的值放到对应的dom上;
Object.keys(data).forEach(key => {
let value = data[key]; // 利用value闭包变量存储值
Object.defineProperty(data, key, { // 数据劫持
get() {
return value
},
set(newValue) {
value = newValue;
init(); // 每次set值的时候将会调用此函数
}
})
})
// 测试是否响应式
let num = 0;
var intervalId = window.setInterval(() => {
if (++num === 100) {
//清除Interval的定时器,传入变量名(创建Interval定时器时定义的变量名)
clearInterval(intervalId);
}
data.name1 += `-${num}`;
data.name2 += `-${num}`;
}, 1000)
此时我们已经基本完成了vue的 v-text 的指令,其实主要是数据劫持,然后根据数据劫持去实现响应式操作(dom操作)。
最后,自己把代码整理封装了一遍,做了一些小优化,可以康康。
v-text 全部代码
<!-- v-text 声明式的指令版本实现 -->
<div id="orange">
<p v-text="name1" class="pppp"></p>
<p v-text="name2" class="pppp"></p>
</div>
// 1. 初始化数据
let data = { name1: '小熊猫', name2: '狗汤圆' };
// 2. 查找v-text对应指令的全部节点
const instructionNodes = findIdentityNode('#orange', 'v-text'); // 查找标识节点
// 3. 数据响应式 VM, 数据绑定
Object.keys(data).forEach(key => {
compile(instructionNodes, key, data[key]); // 编译数据到dom上
// 数据响应式,数据劫持
hijackObjectProperties(data, key, data[key], (newVale) => {
compile(instructionNodes, key, newVale); // 编译数据到dom上
})
})
// 测试是否响应式
let num = 0;
var intervalId = window.setInterval(() => {
if (++num === 100) {
//清除Interval的定时器,传入变量名(创建Interval定时器时定义的变量名)
clearInterval(intervalId);
}
data.name1 += `-${num}`;
data.name2 += `-${num}`;
}, 1000)
/**
* @description: 查找标识(指令)节点
* @param {*} el 父节点 例如 #app
* @param {*} attName 元素属性名称(标识、指令) 例如v-text
* @return {Array} 有此标识节点
*/
function findIdentityNode(el, attName) {
const instructionNodes = []; // 查找有该指令的元素
let app = document.querySelector(el); // 根节点
// 拿到所有子节点
const childNodes = app.childNodes || []; // 所有类型的节点,包括文本节点和标签节点
console.log('childNodes: ', childNodes)
// 筛选标签(元素)节点,nodeType为1的元素节点;
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 进行筛选v-text 属性:
// 获取该节点的所有属性(是map结构)
//例如: attrs: { 0: v-text, 1: class, v-text: v-text, class: class, length: 2 }
const attrs = node.attributes;
// console.log('attrs: ', attrs)
// 将类数组转换为真正的数组 Array.from(map)
Array.from(attrs).forEach(attr => {
// attr 的值例如 v-text="name" class="pppp"
// console.log('attr: ', attr)
const nodeName = attr.nodeName;
const nodeValue = attr.nodeValue;
// nodeName -> "v-text" 就是我们需要找的标识
// nodeValue -> "name" ,也就是data中对应的数据key
// 把data中的数据,放到满足标识上的dom上
if (nodeName === attName) {
instructionNodes.push({node, attName, attValue: nodeValue});
}
})
}
})
return instructionNodes
}
/**
* @description: 劫持对象属性
* @param {*} obj 对象
* @param {*} key 属性名称
* @param {*} value 属性值
* @param {*} setCallbackFn set函数触发的的回调函数
* @param {*} getCallbackFn get函数触发时的回调函数
* @return {undefined}
*/
function hijackObjectProperties(obj, key, value, setCallbackFn, getCallbackFn) {
// 利用闭包去存储值 value
Object.defineProperty(obj, key, {
get() {
getCallbackFn && getCallbackFn(value); // 回调函数,去执行要做的事情
return value
},
set(newValue) {
if (value === newValue) { // 此处作一个优化,相同的值不重复触发setCallbackFn回调函数
return
}
value = newValue;
setCallbackFn && setCallbackFn(value); // 回调函数,去执行要做的事情
}
})
}
/**
* @description: 编译数据到dom
* @param {*} nodes 要编译dom节点
* @param {*} key 属性名,相当于变量名称
* @param {*} value 属性名称对应的属性值
* @return {*}
*/
function compile(nodes, key, value) {
nodes.filter(item => item.attValue === key).forEach(item => (item.node.innerText = value ));
}
实现 vue指令 v-model
v-model 声明式的指令版本实现:
v-model 双向绑定实现思路:
- M -> V 在v-text 中我们已经实现了,所以就只是把指令名替换以及操作dem的api换一下,因为v-model的实现是使用的input;
- V -> M 监听input事件,事件触发时的回调函数拿到当前最新输入框的值,直接赋值即可触发数据响应;
html代码:
<!-- v-text\ v-model 声明式的指令版本实现 -->
<div id="orange">
<p v-text="name1" class="pppp"></p>
<p v-text="name2" class="pppp"></p>
<input type="text" v-model="name1">
<input type="text" v-model="name2">
</div>
js代码:
与之前v-text代码有些改动:
- 查找标识(指令)节点 findIdentityNode(el, attrNames) 函数将单个指令参数改为数组,改为支持多个指令的查找;
- 编译指令数据到dom instructCompile(nodes, key, value, data)函数增加对v-model的处理,增加了data参数,用于在v-model指令中 (V -> M)处理的赋值;
// 1. 初始化数据
let data = { name1: '小熊猫', name2: '狗汤圆' };
// 2. 查找v-text\v-model对应指令的全部节点
const instructionNodes = findIdentityNode('#orange', ['v-text', 'v-model']); // 查找标识节点
// 3. 数据响应式 VM, 数据绑定
Object.keys(data).forEach(key => {
instructCompile(instructionNodes, key, data[key], data); // 首次编译数据到dom
// 数据响应式,数据劫持
hijackObjectProperties(data, key, data[key], (newVale) => {
instructCompile(instructionNodes, key, data[key], data); // 数据更改编译数据到dom
})
})
/**
* @description: 查找标识(指令)节点
* @param {*} el 父节点 例如 #app
* @param {Array} attrNames 元素属性名称(标识、指令) 例如v-text
* @return {Array} 有此标识节点
*/
function findIdentityNode(el, attrNames) {
const instructionNodes = []; // 查找有该指令的元素
let app = document.querySelector(el); // 根节点
// 拿到所有子节点
const childNodes = app.childNodes || []; // 所有类型的节点,包括文本节点和标签节点
// console.log('childNodes: ', childNodes)
// 筛选标签(元素)节点,nodeType为1的元素节点;
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 进行筛选v-text 属性:
// 获取该节点的所有属性(是map结构)
//例如: attrs: { 0: v-text, 1: class, v-text: v-text, class: class, length: 2 }
const attrs = node.attributes;
// 将类数组转换为真正的数组 Array.from(map)
Array.from(attrs).forEach(attr => {
const nodeName = attr.nodeName;
const nodeValue = attr.nodeValue;
// nodeName -> "v-text" 就是我们需要找的标识
// nodeValue -> "name" ,也就是data中对应的数据key
// 把data中的数据,放到满足标识上的dom上
if (attrNames.includes(nodeName)) {
instructionNodes.push({node, nodeName, attValue: nodeValue});
}
})
}
})
return instructionNodes
}
/**
* @description: 劫持对象属性
* @param {*} obj 对象
* @param {*} key 属性名称
* @param {*} value 属性值
* @param {*} setCallbackFn set函数触发的的回调函数
* @param {*} getCallbackFn get函数触发时的回调函数
* @return {undefined}
*/
function hijackObjectProperties(obj, key, value, setCallbackFn, getCallbackFn) {
// 利用闭包去存储值 value
Object.defineProperty(obj, key, {
get() {
getCallbackFn && getCallbackFn(value); // 回调函数,去执行要做的事情
return value
},
set(newValue) {
if (value === newValue) { // 此处作一个优化,相同的值不重复触发setCallbackFn回调函数
return
}
value = newValue;
setCallbackFn && setCallbackFn(value); // 回调函数,去执行要做的事情
}
})
}
/**
* @description: 编译指令数据到dom
* @param {*} nodes 要编译dom节点
* @param {*} key 属性名,相当于变量名称
* @param {*} value 属性名称对应的属性值
* @param {*} data 数据
* @return {*}
*/
function instructCompile(nodes, key, value, data) {
nodes.filter(item => item.attValue === key).forEach(item => {
const nodeName = item.nodeName; // 指令名称
if (nodeName === 'v-text') {
item.node.innerText = value;
}
if (nodeName === 'v-model') {
// 调用dom操作给input绑定数据
item.node.value = value;
const boundEventListenered = item.node['_boundEventListenered']; // 是否已绑定监听事件
if (!boundEventListenered) {
// 监听input事件,在事件回调中拿到最新的输入值,赋值给绑定的属性
item.node.addEventListener('input', (e) => {
data[key] = e.target.value;
})
item.node['_boundEventListenered'] = true; // 标记已绑定事件
}
}
});
}
最终实现: