Vue双向数据绑定代码解读
Vue核心基础-CSDN博客
数据双向绑定原理_哔哩哔哩_bilibili
原理示意图
前置知识
reduce()方法
用于链式获取对象的属性值
Object.defineProperty()方法
Object.defineProperty(obj, prop, descriptor)
obj
:要定义属性的对象。prop
:要定义或修改的属性的名称或 Symbol。descriptor
:将被定义或修改的属性描述符。
属性描述符(Descriptor)
属性描述符对象可以包含以下属性之一或多个:
- value:属性的值(对于 getter 和 setter 属性,该属性会被忽略)。
- writable:当且仅当该属性的值为
true
时,属性的值才可以被[[Set]]
操作改变(即可以重新赋值)。默认为false
。 - enumerable:当且仅当该属性的值为
true
时,该属性才会出现在对象的枚举属性中(例如,通过for...in
循环或Object.keys()
方法)。默认为false
。 - configurable:当且仅当该属性的值为
true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false
。 - get:一个给属性提供 getter 的方法,如果没有 getter 则为
undefined
。当访问该属性时,会调用此 getter 方法,执行时不传入任何参数,但是会传入this
对象(即该属性的宿主对象)。 - set:一个给属性提供 setter 的方法,如果没有 setter 则为
undefined
。当属性值修改时,会调用此 setter 方法。该方法将接受唯一参数,即被赋予的新值。
const object1 = {}; Object.defineProperty(object1, 'property1', { value: 42, writable: false
}); console.log(object1.property1); // 输出:42 // 尝试修改属性值
object1.property1 = 100;
console.log(object1.property1); // 输出:42,因为 writable 为 false // 尝试删除属性
delete object1.property1;
console.log(object1.property1); // 输出:42,因为 configurable 为默认值 false,所以属性不能被删除
const object2 = { _value: 42
}; ------------------------------------------------------------------------------------------
Object.defineProperty(object2, 'value', { get() { return this._value; }, set(newValue) { if (newValue < 0) { throw new Error('值必须大于或等于0'); } this._value = newValue; }
}); console.log(object2.value); // 输出:42 object2.value = 100;
console.log(object2.value); // 输出:100 // 尝试设置无效值
object2.value = -1; // 抛出错误:值必须大于或等于0
发布订阅者模式
Javascript常见设计模式-CSDN博客
流程
数据劫持(递归实现深层次)
1. 初始化过程
当 Vue 实例被创建时,它会通过 Vue.options.data
函数(或组件的 data
函数)获取到初始数据对象。然后,Vue 会遍历这个对象的所有属性,并使用 Object.defineProperty()
将它们转换为 getter/setter。
2. 递归转换
对于对象中的每个属性,Vue 会检查其值是否为对象或数组。如果是,Vue 会递归地调用一个内部函数(如 observe
),以确保这个对象或数组中的所有属性也被转换为响应式。
- 对象:对于对象,Vue 会遍历其所有属性,并对每个属性应用
Object.defineProperty()
。 - 数组:对于数组,Vue 不能直接通过
Object.defineProperty()
拦截数组索引的访问,因为数组的长度是动态的。Vue 通过修改数组原型上的方法(如push
、pop
、shift
、unshift
、splice
、sort
、reverse
)来实现对数组操作的拦截。
function defineReactive(obj, key, val) { // 递归地将对象属性转换为 getter/setter observe(val); // 使用 Object.defineProperty 拦截属性的访问 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // 这里可以添加依赖收集的逻辑(省略) return val; }, set: function reactiveSetter(newVal) { if (newVal === val) return; // 这里可以添加派发更新的逻辑(省略) // 如果 newVal 是对象或数组,则进行递归劫持 observe(newVal); val = newVal; } });
} function observe(value) { if (!isObject(value) || value instanceof VNode) { return; } // 数组需要特殊处理,因为不能拦截索引的访问 if (Array.isArray(value)) { // 这里可以扩展为修改数组原型方法(省略) // 或者使用 Object.defineProperty 对数组的长度进行劫持(但通常不推荐) // 这里只是简单处理,不展开 } else { // 遍历对象的所有属性 Object.keys(value).forEach(function (key) { defineReactive(value, key, value[key]); }); }
} function isObject(value) { // 简单的类型检查 return value !== null && typeof value === 'object';
}
代理数据劫持
Object.keys(this.$data).forEach((key)=>{Object.defineProperty(this,key,{enumerable:true,configurable:true,get(){return this.$data[key]},set(vaue){this.$data[key]=value}
})
})
模板编译
Vue 2 的模板编译流程是一个复杂但有序的过程,它涉及到将 Vue 模板(通常是 HTML 字符串,可能包含 Vue 特有的指令和插值表达式)转换成高效的渲染函数(render function)。这个过程主要在 Vue 的内部实现,特别是通过 vue-template-compiler
包来完成。以下是 Vue 2 模板编译的大致流程:
- 解析模板(Parse):
- 将模板字符串转换为抽象语法树(AST)。AST 是一种树状的数据结构,用于表示源代码的语法结构。
- 在这一步,Vue 会识别出模板中的所有元素、属性、指令(如
v-bind
、v-model
)、插值表达式(如{{ message }}
)等。
- 优化 AST(Optimize):
- 对 AST 进行静态分析,标记出哪些部分是静态的(在多次渲染中不会改变),哪些部分是动态的。
- Vue 会利用这些静态信息来优化渲染过程,比如通过静态提升(hoisting)来避免不必要的DOM操作。
- 生成渲染函数(Generate):
- 将优化后的 AST 转换成 JavaScript 渲染函数。这个函数是一个纯 JavaScript 函数,它接收组件的上下文(如 props、data、computed、methods 等)作为参数,并返回一个虚拟 DOM(VNode)树。
- 渲染函数是 Vue 组件渲染过程的核心,它会在组件的每次更新时被调用,并生成新的 VNode 树。
- 挂载或更新 DOM:
- Vue 的运行时(runtime)会接收渲染函数生成的 VNode 树,并将其与实际的 DOM 进行比较(使用虚拟 DOM 的 diff 算法)。
- 根据比较结果,Vue 会最小化地进行 DOM 更新,以达到高效渲染的目的。
需要注意的是,这个过程是在 Vue 组件的编译阶段完成的,而不是在运行时。当你使用 Vue 的单文件组件(.vue 文件)或直接在 JavaScript 中定义模板时,Vue 的构建工具(如 webpack、Vue CLI)会在构建过程中调用 vue-template-compiler
来编译模板。
简化版的编译过程
下面是一个非常简化的模拟过程,说明Vue是如何处理{{ }}
插值表达式的:
-
解析模板:首先,模板(HTML字符串)会被解析成一个抽象语法树(AST)。在这个过程中,Vue会识别出模板中的所有Vue特有的指令和
{{ }}
插值表达式。 -
转换AST:然后,Vue会遍历这个AST,并将
{{ }}
插值表达式转换为特定的代码块。对于每个{{ }}
插值,Vue会生成一个JavaScript表达式,该表达式在组件的渲染过程中会被计算,并用于替换原始的{{ }}
文本。 -
生成渲染函数:最后,Vue会将转换后的AST转换成一个JavaScript渲染函数。这个渲染函数会基于组件的状态(如数据、计算属性等)来生成最终的HTML字符串。
示例:模拟处理{{ }}
插值
虽然Vue的内部实现要复杂得多,但我们可以模拟一个非常简单的处理过程:
function compileTemplate(template) { // 假设template是一个简单的字符串,我们手动替换{{ }}内的内容 // 实际应用中,你会使用正则表达式或更复杂的解析器来解析模板 let code = template.replace(/\{\{ (.*?)\}\}/g, (_, expr) => { // 这里expr是`{{ }}`内的表达式 // 在Vue中,这个表达式会被转换成类似`_s(this.expr)`的JavaScript代码 // 这里我们简单地返回表达式本身,实际应用中你需要根据组件状态计算这个值 return `_s(${expr})`; // 假设_s是一个将值转换为字符串的函数 }); // 这里的code只是一个字符串示例,并不是真正的渲染函数 // 在Vue中,这个字符串会被转换成JavaScript代码,并生成渲染函数 console.log(code); // 注意:这里只是为了演示,并没有真正执行任何渲染逻辑
} // 示例模板
let template = `<div>{{ message }}</div>`; // 编译模板
compileTemplate(template);
// 输出: "<div>_s(message)</div>"
// 注意:这里的输出只是为了说明如何替换{{ }},并不是Vue实际生成的渲染函数
正则表达式-CSDN博客
发布(set中)与订阅(get中)
// 定义一个函数来观察一个对象,使其属性变为响应式
function observe(data) { // 遍历对象的所有键 Object.keys(data).forEach(key => { let internalValue = data[key]; // 获取当前属性的值 const dep = new Dep(); // 为每个属性创建一个依赖实例 // 使用Object.defineProperty来定义属性的getter和setter Object.defineProperty(data, key, { enumerable: true, // 属性可枚举 configurable: true, // 属性可配置 get() { dep.depend(); // 访问属性时,收集依赖 return internalValue; // 返回属性的值 }, set(newVal) { if (newVal === internalValue) return; // 如果新值等于旧值,则不执行任何操作 internalValue = newVal; // 更新属性值 dep.notify(); // 通知所有依赖此属性的观察者,属性已更改 } }); });
} // 定义一个依赖类,用于收集依赖和通知观察者
class Dep { constructor() { this.subscribers = []; // 存储所有依赖此属性的观察者 } // 依赖收集方法,将当前活动的观察者添加到依赖的订阅者列表中 depend() { if (Dep.target) { this.subscribers.push(Dep.target); } } // 通知所有订阅者(观察者)更新 notify() { this.subscribers.forEach(sub => sub.update()); } // 静态属性,用于存储当前活动的观察者 static target = null;
} // 定义一个观察者构造函数,用于观察Vue实例上的表达式
function watcher(vm, exp, cb) { // 设置当前活动的观察者为当前watcher实例 Dep.target = this; this.vm = vm; // 绑定Vue实例 this.exp = exp; // 绑定要观察的表达式 this.cb = cb; // 绑定回调函数 // 触发getter,进行依赖收集 this.value = vm[exp]; // 清除当前活动的观察者,避免污染后续操作 Dep.target = null;
} // 定义watcher实例的update方法,用于在数据变化时执行回调函数
watcher.prototype.update = function() { const newValue = this.vm[this.exp]; // 获取最新值 if (newValue !== this.value) { // 如果新值不等于旧值 this.cb(newValue); // 执行回调函数,并传入新值 this.value = newValue; // 更新旧值 }
}; // 注意:上述代码仅为演示Vue响应式系统的一部分,并未完全模拟Vue的全部功能。
// 在Vue中,watcher的创建和管理、以及Dep的target的设置和清除通常是通过Vue的内部机制来完成的。
输入框中数据改变
(涉及到解析模板指令v-model)
<template> <div> <input :value="message" @input="updateMessage" placeholder="edit me"> <p>Message is: {{ message }}</p> </div>
</template> <script>
export default { data() { return { message: '' } }, methods: { updateMessage(event) { // 你可以从event.target.value获取到输入框的值 this.message = event.target.value; } }
}
</script>
不是很了解的:模板的编译解析
在Vue.js 2.x中,模板编译和解析是Vue内部的一个复杂过程,它主要负责将Vue模板(HTML字符串或模板文件)转换成渲染函数(render function)。这个过程并不是直接通过DOM操作完成的,而是利用了JavaScript的字符串处理、正则表达式以及Vue的编译系统内部逻辑。然而,理解这个过程涉及到的一些基本概念和工具是有帮助的。
模板编译与解析的概述
Vue模板编译主要发生在Vue的初始化阶段,它涉及到以下几个步骤:
- 解析模板:将模板字符串转换成AST(抽象语法树)。
- 优化AST:静态内容提升等优化操作。
- 生成代码:将AST转换成渲染函数代码字符串。
- 编译成函数:使用
new Function()
将渲染函数代码字符串编译成可执行函数。常用到的DOM操作方法(间接相关)
虽然Vue的模板编译过程不直接操作DOM,但Vue的渲染函数和虚拟DOM系统最终会操作DOM。不过,在模板编译阶段,我们讨论的是字符串处理和JavaScript操作,而非直接的DOM操作。然而,了解Vue如何与DOM交互是有帮助的:
createElement
、appendChild
、removeChild
等(Vue内部通过虚拟DOM模拟这些操作)。常用到的正则表达式
在Vue模板编译的上下文中,正则表达式通常用于解析模板字符串,提取指令(如
v-bind
、v-model
等)、插值表达式({{ }}
)等。不过,Vue的源代码中这些正则表达式是高度定制的,且随着版本更新而变化。这里提供一个简化的例子,说明如何可能使用正则表达式来识别插值表达式:// 简化的正则表达式,用于匹配插值表达式 const interpolationRE = /\{\{([^}]+)\}\}/g; let template = 'Hello, {{ name }}!'; let matches = template.match(interpolationRE); if (matches) { console.log(matches[1]); // 输出: name }
常用到的JavaScript方法
在Vue模板编译过程中,会大量使用JavaScript的字符串和数组方法,以及对象操作:
- 字符串方法:
replace()
,split()
,trim()
,indexOf()
,substring()
等,用于处理和转换模板字符串。- 数组方法:
map()
,filter()
,reduce()
,forEach()
等,用于遍历和处理AST节点。- 对象方法:
hasOwnProperty()
,Object.keys()
,Object.assign()
,Object.create()
等,用于操作对象属性和原型链。- 正则表达式相关方法:
exec()
,test()
,match()
等,用于在模板字符串中查找和匹配特定的模式。总结
Vue的模板编译和解析是一个复杂的过程,它涉及到JavaScript的多个方面,包括字符串处理、正则表达式、数组和对象操作等。然而,这个过程主要发生在Vue内部,开发者通常不需要直接处理。理解Vue模板编译的基本原理和目的,有助于更好地理解和使用Vue框架。