Vue双向绑定原理
上图来源于Vue官网:https://v2.cn.vuejs.org/v2/guide/reactivity.html
我们希望 页面变化数据变化 数据变化页面变化
需要实现 永远根据数据的变化去渲染页面 在页面改变数据也能收到通知去修改数据
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者
2.实现一个订阅者Watcher,每个Watcher都绑定一个更新函数,Watcher可以收到属性的变化通知并执行相应的函数,从而更新视图
3.实现一个消息订阅器 Dep ,主要收集订阅者,当 Observe监听到发生变化,就通知Dep 再去通知Watcher去触发更新。
4.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,若节点存在指令,则Compile初始化这类节点的模板数据(使其显示在视图上),以及初始化相应的订阅者。
//html中部分代码
<body>
<div id="app">
<span>{{name}}</span>
<input type="text" v-model="name" />
<span>更多{{ more.like }}</span>
<input type="text" v-model="more.like" />
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el:'#app',
data: {
name:'小胡子',
more: {
like: '羽毛球'
}
}
})
console.log(vm)
</script>
</body>
</html>
在vue.js文件中模拟vue双向数据绑定
1.创建Vue类(或创建构造函数)
创建一个Vue实例的时候需要传入一个对象,所以constructor 传入 obj_instance 形参
这个对象告知挂载到哪个元素上,并且设置数据。
需要把对象里面的data属性赋值给实例里面的$data属性
class Vue {
constructor(obj_instance) {
this.$data = obj_instance.data
// 创建实例时监听
Oberver(this.$data)
Compile(obj_instance.el, this)
}
}
2.实现数据监听 Oberver(this.$data)
初始化实例的时候就需要调用Oberver(this.$data)
对象属性发生变化了需要 通知 更改后的属性更新到DOM节点里去
使用Object.defineProperty进行数据劫持-监听实例里面的数据
//Object.defineProperty 使用方式
/* Object.defineProperty(操作对象,操作属性,{
enumerable:true, //属性可以枚举
configurable:true, //属性描述符可以被改变
get(){},
set(){}
})*/
data里面每一个属性都需要监听,使用Object.keys(data_instance) 获取对象里面的每一个属性,返回的是一个数组
//需要对每个key进行监听
Object.keys(data_instance).forEach(key =>{
let value = data_instance[key]
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
//访问值的时候会调用get函数
//不设置getter函数对象就没有值
//不能直接在函数中返回属性对应的值,没有使用Object.defineProperty之前属性里面的值是没有被修改的,使用后就会被修改,使用我们在Object.defineProperty之前应该把属性存起来,再在getter函数中返回
get() {
return value
},
//设置setter函数 不设置修改不会成功
set(newValue) {
//newValue 新传入的值
// 赋新值
value= newValue
}
})
})
属性值是可能一个对象,对象下面可能还有对象,我们就需要重复调用 Oberver(value) 方法 递归方式
//递归出口 没有子属性或没检测到对象都需要终止
if (!data_instance || typeof data_instance !== 'object') return
把一个字符串修改为对象,我们发现它没有get 和 set 所以需要 在set中调用 Oberver(newValue) 不是对象就结束,是就进行数据劫持的操作
//数据监听完整代码
function Oberver(data_instance) {
if (!data_instance || typeof data_instance !== 'object') return
const dependency = new Dependency()
Object.keys(data_instance).forEach(key => {
let value = data_instance[key]
Oberver(value) //递归 - 子属性数据劫持
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get() {
Dependency.temp && dependency.addSub(Dependency.temp)
return value
},
set(newValue) {
value = newValue
Oberver(newValue)
dependency.notity()
}
})
})
}
3.HTML模板解析 - 替换内存
获取页面元素——应用Vue数据——渲染页面
上面的流程 获取一次元素渲染一次页面会频繁的操作DOM节点
所以我们需要新增一个步骤
获取页面元素——放入临时内存区域——应用Vue数据——渲染页面
//模板解析函数
//需要两个参数 element:Vue实例挂载的元素 vm:Vue实例(this)
function Compile(element, vm){
//获取元素保存到实例的$el
vm.$el = document.querySelector(element)
// 放入临时内存
// fragment临时接受dom元素
//创建文档碎片
const fragment = document.createDocumentFragment()
//使用while 循环添加到fragment
let child;
//循环时先把页面的子节点赋值给child变量
while (child = vm.$el.firstChild) {
fragment.append(child)
}
//所有节点都添加到fragment后 修改节点
//创建一个修改fragment节点的函数
fragment_compile(fragment)
function fragment_compile(node){
}
}
修改 fragment 的 fragment_compile(fragment) 函数实现
// 替换文档碎片内容
function fragment_compile(node) {
// 转义花括号 \{\{\}\}
// 匹配前后空格 \s* \s*
// 核心字符 \S+
const pattern = /\{\{\s*(\S+)\s*\}\}/
//只需要修改文本节点 文本节点为3 我们就进行数据替换 并且返回
// 如果不是就继续循环一下文档碎片的所有子节点
if(node.nodeType === 3){
const xxx = node.nodeValue
// 过滤掉其他空节点
const result_regex = pattern.exec(node.nodeValue)
//如果是文本且是我们要的节点
//可以用vue实例访问对应的值
if(result_regex){
const arr = result_regex[1].split('.')
//需要链式获取子属性
const value = arr.reduce(
(total, current) => total[current],vm.$data
)
node.nodeValue = xxx.replace(pattern, value)
// 创建订阅者
new Watcher(vm, result_regex[1], newValue => {
node.nodeValue = xxx.replace(pattern, newValue)
})
}
//递归出口
return
}
// v-model 实现
if(node.nodeType === 1 && node.nodeName === 'INPUT'){
const attr = Array.from(node.attributes);
attr.forEach(i => {
if(i.nodeName === 'v-model'){
const value = i.nodeValue.split('.').reduce(
(total, current) => total[current], vm.$data
)
node.value = value
new Watcher(vm, i.nodeName, newValue => {
node.value = newValues
})
node.addEventListener('input', e => {
console.log(i)
// const name = i.nodeValue
// vm.$data[name] = e.target.value
const arr1 = i.nodeValue.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
console.log(arr1)
console.log(arr2)
const final = arr2.reduce(
(total, current) => total[current],vm.$data
)
console.log(final)
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
// 节点里面可能还有节点文本节点就遍历解析
node.childNodes.forEach(child => fragment_compile(child))
}
//将文本碎片添加到vm里的$el属性里面
vm.$el.appendChild(fragment)
function Compile(element, vm){
vm.$el = document.querySelector(element)
const fragment = document.createDocumentFragment()
let child;
while (child = vm.$el.firstChild) {
fragment.append(child)
}
fragment_compile(fragment)
function fragment_compile(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/
if(node.nodeType === 3){
const xxx = node.nodeValue
const result_regex = pattern.exec(node.nodeValue)
if(result_regex){
const arr = result_regex[1].split('.')
const value = arr.reduce(
(total, current) => total[current],vm.$data
)
node.nodeValue = xxx.replace(pattern, value)
new Watcher(vm, result_regex[1], newValue => {
node.nodeValue = xxx.replace(pattern, newValue)
})
}
return
}
// 元素节点 节点类型为1
if(node.nodeType === 1 && node.nodeName === 'INPUT'){
const attr = Array.from(node.attributes);
attr.forEach(i => {
if(i.nodeName === 'v-model'){
const value = i.nodeValue.split('.').reduce(
(total, current) => total[current], vm.$data
)
node.value = value
new Watcher(vm, i.nodeName, newValue => {
node.value = newValues
})
node.addEventListener('input', e => {
const arr1 = i.nodeValue.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce(
(total, current) => total[current],vm.$data
)
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
node.childNodes.forEach(child => fragment_compile(child))
}
vm.$el.appendChild(fragment)
}
4.实现发布者订阅模式
// 依赖- 收集和通知订阅者
class Dependency {
constructor(){
//保存订阅者信息
this.subscribers = [];
}
// 添加订阅者
addSub(sub) {
this.subscribers.push(sub)
}
notity() {
// update 调用自身方法更新
this.subscribers.forEach(sub => sub.update())
}
}
订阅者实现
//订阅者
class Watcher {
constructor(vm, key, callback){
this.vm = vm
this.key = key
this.callback = callback
// 临时属性 - 触发getter
Dependency.temp = this;
key.split('.').reduce((total, current) => total[current],vm.$data)
Dependency.temp = null
}
// 回调函数记录如何更新文本内容
update() {
const value = this.key.split('.').reduce((total, current) => total[current],this.vm.$data)
this.callback(value)
}
}
以上内容代码是根据视频总结:https://www.bilibili.com/video/BV1934y1a7MN?spm_id_from=333.788.top_right_bar_window_history.content.click&vd_source=d69c710a9302f968dc33b3bb707e83bc
参考文章:https://blog.csdn.net/weixin_52092151/article/details/119810514