整体架构与核心思路
Vue 的双向绑定原理基于数据劫持、观察者模式等,通过多个核心模块协同工作,实现数据变化驱动视图更新,以及视图交互(如表单输入等)反向更新数据的效果,整体遵循 MVVM(Model-View-ViewModel)的设计模式。核心思路是对数据进行监听,解析模板指令,在数据和视图之间建立关联,当数据变动时能及时通知相关部分更新视图,反之亦然。
实现过程
-
Observe(监听器)
-
功能:负责监听数据对象的属性变化。通过递归遍历数据对象的属性,使用
Object.defineProperty
对每个属性进行劫持,创建对应的Dep
实例来管理依赖关系。 -
实现细节:
- 在构造函数中接收要监听的数据对象,调用
observe
方法开启监听。 observe
方法判断传入的数据是否为对象,若是则循环其属性,针对每个属性调用defineReactive
方法进行处理。defineReactive
方法为每个属性创建Dep
实例,先递归监听属性值(如果属性值也是对象),然后通过Object.defineProperty
定义属性的get
和set
方法。在get
方法中,若存在全局的Dep.target
(即当前正在计算的Watcher
实例),则将其添加到对应Dep
实例的依赖列表subs
中;在set
方法中,当属性值更新时,调用dep.notify
通知所有依赖该属性的Watcher
实例进行更新。
- 在构造函数中接收要监听的数据对象,调用
class Observe { constructor(data) { this.data = data; this.observe(data); } observe(data) { //如果不是对象就退出监听 递归退出条件 if (data && typeof data === "object") { //循环data取值给每一个key 添加 监听 Object.keys(data).forEach((key) => { this.defineReactive(data, key, data[key]); }); } } defineReactive(data, key, val) { const dep = new Dep() //递归监听子对象 this.observe(val); // 给data每一个key值设置监听 Object.defineProperty(data, key, { get() { // 在watcher里面get方法里面会把,Dep.target静态方法添加Watcher实例 // 此时 全局只有一个Dep.target 如果有就添加到当前的dep的subs队列里面 // dep实例存储watcher Dep.target && dep.addSub(Dep.target); return val; }, set(newVal) { //更新赋值 val = newVal; // 有更新就通知 dep.notify() }, }); } }
-
-
Compile(解析器)
-
功能:扫描和解析 DOM 元素节点上的指令,将模板中的数据占位符(如
{{}}
插值表达式、v-
指令等)与数据进行绑定,并关联相应的更新函数,使数据能正确渲染到视图以及视图变化能反馈到数据上。 -
实现细节:
- 构造函数接收
vm
(Vue 实例),获取对应的 DOM 元素(支持传入选择器字符串或直接的 DOM 节点),将其转换为文档碎片fragment
进行后续解析,避免频繁操作 DOM。 node2Fragment
方法通过循环将传入 DOM 元素的子节点剪切到新创建的文档碎片中,方便后续统一处理后再添加回 DOM。compileElement
方法遍历文档碎片中的子节点,根据节点类型(文本节点或元素节点)分别调用compileText
或compileAttrs
方法进行处理,若子节点还有子节点则递归调用自身继续解析。compileAttrs
方法遍历元素节点的属性,判断是否为指令(以v-
或@
开头),针对不同指令(如v-text
、v-on:click
、v-model
等)进行不同的处理,例如创建Watcher
实例关联数据和视图更新逻辑,或者绑定事件监听等。compileText
方法解析文本节点中的插值表达式,提取表达式内容并创建Watcher
实例,当数据变化时更新文本节点的内容。同时,parseText
方法用于格式化插值表达式,将其转换为可计算求值的形式。
- 构造函数接收
class Compile { constructor(vm) { this.$vm = vm; const el = vm.$el this.$el = isElementNode(el) ? el : document.querySelector(el); if (this.$el) { const fragment = this.node2Fragment(this.$el) // 解析文档碎片 this.compileElement(fragment) this.$el.appendChild(fragment) } } node2Fragment(el) { // 创建新的文档碎片 只需要更新一次dom let fragment = document.createDocumentFragment(), child // 剪切 firstChild 到文档碎片里面 while ((child = el.firstChild)) { fragment.appendChild(child) } return fragment } compileElement(node) { const childNodes = node.childNodes, that = this; // 类数组转换 [].slice.call(childNodes).forEach(node => { //<div v-text="msg">{{msg}}</div> // 如果是文字节点 {{msg}} if (isTextNode(node)) { const reg = /\{\{(.+?)\}\}/; const text = node.textContent if (reg.test(text)) { this.compileText(node) } } else if (isElementNode(node)) { // 如果是元素节点 <div></div> this.compileAttrs(node) } // 如果还有子节点 就递归执行 if (node.childNodes && node.childNodes.length) { that.compileElement(node) } }) } compileAttrs(node) { const nodeAttrs = node.attributes; [].slice.call(nodeAttrs).forEach(attr => { //<div v-text="msg">{{msg}}</div> const attrName = attr.name //'v-text' // 如果是指令 if (isDirective(attrName)) { const exp = attr.value let dir = attrName.substring(2) // 分析指令 并执行内容 if (dir === 'text') { new Watcher(exp, this.$vm, (newVal) => { node.textContent = newVal }) } else if (dir === 'on:click') { const fn = this.$vm[exp] document.addEventListener('click', fn.bind(this.$vm), false) } else if (dir === 'model') { node.addEventListener('input', (event) => { this.$vm[exp] = event.target.value }) new Watcher(exp, this.$vm, (newVal) => { node.value = newVal }) } } }) } compileText(node) { // 解析文本节点 let text = node.textContent.trim() const exp = this.parseText(text) new Watcher(exp, this.$vm, (newValue) => { node.textContent = newValue }) } // 格式化插值表达式 parseText(text) { const reg = /\{\{(.+?)\}\}/g; const price = text.split(reg) const match = text.match(reg) return price.map(item => { if (match && match.indexOf('{{' + item + "}}") > -1) { return "(" + item + ")" } else { return "'" + item + "'" } }).join('+') } } // 判断节点 const isDirective = attr => attr.indexOf("v-") === 0 || attr.indexOf("@") === 0; const isElementNode = node => node.nodeType === 1; const isTextNode = node => node.nodeType === 3; ```
-
-
Watcher(观察者)
-
功能:作为连接
Observer
和Compile
的桥梁,订阅数据属性的变化,当属性变动时接收通知并执行对应的回调函数,从而更新视图。 -
实现细节:
- 构造函数接收表达式
exp
(对应数据属性或插值表达式等)、vm
(Vue 实例)以及回调函数cb
,在实例化时调用update
方法进行初始化更新。 get
方法先将自身设置为全局的Dep.target
,然后通过computedExp
方法计算表达式的值,计算过程中会触发对应数据属性的get
方法,从而建立依赖关系(将自身添加到对应Dep
的subs
列表中),最后再将Dep.target
置空并返回计算出的值。update
方法获取表达式最新的值,并调用传入的回调函数cb
,将新值传递进去,以实现视图更新。computedExp
方法利用new Function
创建一个函数,在指定的vm
作用域下计算表达式的值。
- 构造函数接收表达式
let _uid = 0 class Watcher { constructor(exp,vm,cb) { this.exp =exp this.vm =vm this.cb =cb this.uid = _uid++ this.update() } get() { // 把自己添加到dep里面 在Observe.defineReactive 里面初始化监听对象的时候get() 方法添加到dep实例的subs队列里面 Dep.target = this // 计算插值表达式 const newVal = Watcher.computedExp(this.exp, this.vm) Dep.target = null // 删除节点 return newVal } update() { // 获取插值表达式的值 callback给node节点 const exp = this.get() this.cb && this.cb(exp) } staticcomputedExp(exp,vm) { // 实现插值表达式里面的内容 const fn = new Function('vm', 'with(vm){return ' +exp+ '}') return fn(vm) } }
-
-
Dep(依赖收集器)
-
功能:用于收集依赖(即
Watcher
实例),管理数据属性与观察者之间的关系,在数据变化时负责通知所有依赖该属性的Watcher
实例进行更新。 -
实现细节:
- 构造函数初始化一个空对象
subs
用于存放Watcher
实例,通过addSub
方法将Watcher
实例添加到subs
中,以Watcher
的唯一标识uid
作为属性名进行存储。 notify
方法遍历subs
中的所有Watcher
实例,调用每个实例的update
方法,实现批量通知更新的操作。
- 构造函数初始化一个空对象
class Dep { constructor() { // 存放所有的watcher this.subs = {} } addSub(watcher) { // 把watcher 添加到subs集合 this.subs[watcher.uid] =watcher } notify() { // 循环通知更新 for (const uid in this.subs) { this.subs[uid].update() } } }
-