Vue.js 响应式原理
Posted X可乐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue.js 响应式原理相关的知识,希望对你有一定的参考价值。
文章目录
整体分析
- Vue
- 目标:将 data 数据注入到 Vue 实例,便于方法内操作
- Observer(发布者)
- 目标:数据劫持,监听数据变化,并在变化时通知 Dep
- Dep(消息中心)
- 目标:存储订阅者以及管理消息的发送
- Watcher(订阅者)
- 目标:订阅数据变化,进行试图更新
- Compiler
- 目标:解析模板中的指令与插值表达式,并替换成相应的数据
Vue 类
- 功能
- 接受配置信息
- 将 data 的属性转换成 Getter、Setter,并注入到 Vue 实例中
- *监听 data 中所有属性的变化,设置成响应式数据
- *调用解析功能(解析模板内的插值表达式、指令等)
class Vue
constructor (options)
// 1. 存储属性
this.$options = options ||
this.$data = options.data ||
// 判断 el 值的类型,并进行相应处理
const el = options
this.$el = typeof el === 'string' ? document.querySelector(el) : el
// 2. 将 dta 属性注入到 Vue 实例中
_proxyData(this, this.$data)
// *3. 创建 Observer 实例监视 data 的属性变化
new Observer(this.$data)
// *4. 调用 Compiler
new Compiler(this)
// 将 data 的属性注入到 Vue 实例
function _proxyData (target, data)
Object.keys(data).forEach(key =>
Object.defineProperty(target, key,
enumerable: true,
configurable: true,
get ()
return data[key]
,
set (newValue)
data[key] = newValue
)
)
Observer 类
- 功能
- 通过数据劫持方式监视 data 中的属性变化,变化时通知消息中心 Dep
- 需要考虑 data 的属性也可能为对象,也要转换成响应式数据
class Observer
// 接受传入的对象,将这个对象的属性转换为 Getter / Setter
constructor(data)
this.data = data
// 遍历数据
this.walk(data)
// 封装用于数据便利的方法
walk(data)
// 将遍历后的属性,都转换为 Getter/Setter
Object.keys(data).forEach(key => this.convert(key, data[key]))
// 封装用于将对象转换为响应式数据的方法
convert(key, value)
defineReactive(this.data, key, value)
// 用于为对象定义一个响应式的属性
function defineReactive(data, key, value)
// 创建消息中心
const dep = new Dep()
// 检测是否为对象,如果是,创建一个新的 observer 实例进行管理
observer(value)
// 进行数据劫持
Object.defineProperty(data, key,
enumerable: true,
configurable: true,
get()
console.log('获取了属性')
// * 在触发 Getter 时添加订阅者
Dep.target && dep.addSub(Dep.target)
return value
,
set(newValue)
console.log('设置了属性')
if (value === newValue) return
value = newValue
observer(value)
// * 数据变化时,通知消息中心
dep.notify(0)
)
function observer(value)
if (typeof value === 'object' && value !== null)
return new Observer(value)
Dep 类
-
Dep 是 Dependency 的简写,含义 “依赖”,指的是 Dep 用于收集与管理订阅者与发布者之间的依赖关系
-
功能:
- *为每个数据收集顶硬的依赖,存储依赖
- 添加并存储订阅者
- 数据变化时,通知所有观察者
class Dep
constructor ()
// 存储订阅者
this.subs = []
// 添加订阅者
addSub (sub)
if (sub && sub.update)
this.subs.push(sub)
// 通知订阅者的方法
notify ()
// 遍历订阅者,并执行更新功能
this.subs.forEach(sub =>
sub.update()
);
Watcher 类
- 功能:
- 实例化 Watch 时,往 dep 对象中添加自己
- 当数据变化触发 dep,dep 通知所有对应的 Watcher 实例更新视图
class Watcher
constructor (vm, key, cb)
// 当前 Vue 实例
this.vm = vm
// 订阅的属性名
this.key = key
// 数据变化后,要执行的回调
this.cb= cb
// 触发 Getter 前,将当前订阅者实例存储给 Dep 类
Dep.target = this
// 记录属性更改之前的值,用于进行更新状态检测(导致了属性 Getter 的触发)
this.oldValue = vm[key]
// 操作完毕后,清除 target ,用于存储下一个 Watch 实例
Dep.target = null
// 封装数据变化时,更新视图的功能
update ()
const newValue = this.vm[this.key]
if (newValue === this.oldValue) return
// 数据改变,调用更新后的回调
this.cb(newValue)
Compiler 类
- 功能:
- 进行编译模板,并解析内部指令与插值表达式
- 进行页面的首次渲染
- 数据变化后,重新渲染视图
class Compiler
constructor(vm)
this.vm = vm
this.el = vm.$el
// 初始化模板编译方法
this.compile(this.el)
// 基础模板方法
compile(el)
const childNodes = el.childNodes
Array.from(childNodes).forEach(node =>
// 检测节点类型(文本节点、元素节点)
if (isTextNode(node))
// 编译文本节点内容
this.compileText(node)
else if (isElementNode(node))
// 编译元素节点内容
this.compileElement(node)
// 检测当前节点是否存在子节点
if (node.childNodes && node.childNodes.length)
this.compile(node)
);
// 封装文本节点编译方法
compileText(node)
const reg = /\\\\(.+?)\\\\/g
// 去除内容中不必要的空格与换行
const value = node.textContent.replace(/\\s/g, '')
// 声明数据存储多段文本
const tokens = []
// 记录已经操作过的位置的索引
let lastIndex = 0
// 记录当前提取内容的初始索引
let index
let result
while (result = reg.exec(value))
// 本次提取内容的初始索引
index = result.index
// 处理普通文本
if (index > lastIndex)
// 将中间部分的内容存储到 tokens 中
tokens.push(value.slice(lastIndex, index))
// 处理插值表达式内容(去除空格的操作可省略)
const key = result[1].trim()
// 根据 key 获取对应属性值,存储到 tokens
tokens.push(this.vm[key])
// 更新 lastIndex
lastIndex = index + result[0].length
// 创建订阅者,Watcher 实时订阅数据变化
const pos = tokens.length - 1
new Watcher(this.vm, key, newValue =>
// 数据变化,修改 tokens 中的对应数据
tokens[pos] = newValue
node.textContent = tokens.join('')
)
if (tokens.length)
// 初始页面初始渲染
node.textContent = tokens.join('')
// 封装元素节点编译方法
compileElement(node)
// 获取属性节点
Array.from(node.attributes).forEach(attr =>
// 保存属性名称,并检测属性的功能
let attrName = attr.name
if (!isDirective(attrName)) return
// 获取指令的具体名称
attrName = attrName.slice(2)
// 获取指令的值,代表响应式数据的名称
let key = attr.value
// 封装 update 方法,用于进行不同指令的功能分配
this.update(node, key, attrName)
)
// 用于进行指令分配的方法
update(node, key, attrName)
// 名称处理
let updateFn = this[attrName + 'Updater']
// 检测并调用
updateFn && updateFn.call(this, node, key, this.vm[key])
// v-text 处理
textUpdater(node, key, value)
// 给元素设置内容
node.textContent = value
// 订阅数据变化
new Watcher(this.vm, key, newValue =>
node.textContent = newValue
)
// v-model 处理
modelUpdater(node, key, value)
// 给元素设置内容
node.value = value
// 订阅数据变化
new Watcher(this.vm, key, newValue =>
node.value = newValue
)
// 监听 input 事件,实现双向绑定
node.addEventListener('input', () =>
this.vm[key] = node.value
)
// 判断节点是否为元素节点
function isElementNode(node)
return node.nodeType === 1
// 判断节点是否为文本节点
function isTextNode(node)
return node.nodeType === 3
// 判断属性名是否为指令
function isDirective(attrName)
return attrName.startsWith('v-')
功能总结
- Vue 类
- 把 data 的属性注入到 Vue 实例
- 调用Observer 实现数据响应式处理
- 调用 Compiler 编译模板
- Observer
- 将 data 的属性转换成 Getter/Setter
- 为 Dep 添加订阅者 Watcher
- 数据变化时发送通知 Dep
- Dep
- 收集依赖,添加订阅者(watcher)
- 通知订阅者
- Watcher
- 编译模板时创建订阅者,订阅数据变化
- 接到 Dep 通知时,调用 Compiler 中的模板功能更新视图
- Compiler
- 编译模板,解析指令与插值表达式
- 负责页面首次渲染与数据变化后重新渲染
以上是关于Vue.js 响应式原理的主要内容,如果未能解决你的问题,请参考以下文章