vue双向绑定原理
首先看一下我们正常导入vue.js之后书写的代码
<div class="app">
<input type="text" v-model="name">
<span>姓名:{{ name }}</span>
<input type="text" v-model="body.age">
<span>年龄:{{ body.age }}</span>
</div>
<script>
const vm = new Vue({
el: '#app', // 绑定的根节点
data: { // 数据
name:'小明',
body: {
age: 20
}
}
})
</script>
然后我们就来自己大概写一下 vue如何实现双向绑定的
1、首先新建一个Vue实例
<script>
// 首先我们先新建一个Vue实例
class Vue {
constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
// vue的源码也是$data,这里我们也使用$data
this.$data = obj_instance.data;
}
}
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
name:'小明',
body: {
age: 20
}
}
})
console.log(vm);
</script>
2、双向绑定,我们当然需要监听data中的数据
// 首先我们先新建一个Vue实例
class Vue {
constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
// vue的源码也是$data,这里我们也使用$data
this.$data = obj_instance.data;
// 在这里,我们初始化实例的时候,需要监听data数据,我们用Observer来监听数据
Observer(this.$data)
}
}
// 监听数据变化,我们需要用到js的原生 api
function Observer(data_instance){
// 遍历对象 Object.keys 可以遍历对象的key 返回key数组
console.log(Object.keys(data_instance)); // => [name,body]
}
3、数据劫持
// 监听数据变化,我们需要用到js的原生 api Object.defineProperty 进行数据劫持
function Observer(data_instance){
// 遍历对象 Object.keys 可以遍历对象的key 返回key数组
Object.keys(data_instance).forEach(key=>{
// Object.defineProperty(操作对象, 操作属性, {
// enumerable: true, // 数据是否可以被枚举
// configurable: true, // 属性描述符是否可以被改变
// get(){ }, // 属性被访问的时候触发
// set(){ } // 属性改变的时候触发
// })
let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get(){
console.log(`访问了属性${key} => 值 ${value}`);
return value
},
set(newValue){
console.log(`属性${key}的值被修改为:${newValue}`);
value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
}
})
})
}
此时就有个问题了,这样我们只能访问到第一层,此时用个递归就可解决
注意代码
// 监听数据变化,我们需要用到js的原生 api Object.defineProperty 进行数据劫持
function Observer(data_instance){
if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在 是否为object 主要作用于递归的时候
return
}
// 遍历对象 Object.keys 可以遍历对象的key 返回key数组
Object.keys(data_instance).forEach(key=>{
// Object.defineProperty(操作对象, 操作属性, {
// enumerable: true, // 数据是否可以被枚举
// configurable: true, // 属性描述符是否可以被改变
// get(){ }, // 属性被访问的时候触发
// set(){ } // 属性改变的时候触发
// })
let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
Observer(value) // 递归
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get(){
console.log(`访问了属性${key} => 值 ${value}`);
return value
},
set(newValue){
console.log(`属性${key}的值被修改为:${newValue}`);
value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
}
})
})
}
很多人听到递归就头皮发麻,但是这里看上去应该比较简单
不过这样递归又会出现一个问题,就是比如name属性,原本的value是string , 现在改为object就会出现问题,这个细节很多人都会忽略,如果此时你重新赋值为ojbect, 从控制台可以看出此object没有get set
此时只要在set 中添加 Observer 数据监听
function Observer(data_instance){
if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在 是否为object 主要作用于递归的时候
return
}
// 遍历对象 Object.keys 可以遍历对象的key 返回key数组
Object.keys(data_instance).forEach(key=>{
// Object.defineProperty(操作对象, 操作属性, {
// enumerable: true, // 数据是否可以被枚举
// configurable: true, // 属性描述符是否可以被改变
// get(){ }, // 属性被访问的时候触发
// set(){ } // 属性改变的时候触发
// })
let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
Observer(value) // 递归
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get(){
console.log(`访问了属性${key} => 值 ${value}`);
return value
},
set(newValue){
console.log(`属性${key}的值被修改为:${newValue}`);
value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
Observer(newValue)
}
})
})
}
目前数据监听到此为止了,下面我们肯定需要把更改的数据让页面同时也需要跟着更改,但是需要考虑的是 我们不能数据变一次,页面就跟着改一次,这样太消耗性能了。继续往下走吧
4、我们下面创建一个数据解析的函数 compile
// html 模板解析 element:dom里挂载的元素 vm:vue实例
function Compile(element, vm){
vm.$el = document.querySelector(element) // 获取挂载元素
const fragment = document.createDocumentFragment() // 创建文档碎片
let child;
while (child = vm.$el.firstChild) { // 遍历所有节点 将节点放入文档碎片中 (相当于一个临时空间)
fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
}
// 此时我们需要解释 fragment
fragment_compile(fragment)
function fragment_compile(node){
const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }} 模版语法
// 首先做个判断,看一下node类型
if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
// console.log(node);
// console.log(node.nodeValue);
const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
// console.log(reg_regex);
const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
const value = arr.reduce((total, current) => total[current], vm.$data)
console.log(value);
node.nodeValue = node.nodeValue.replace(pattern, value) // 将data中的数据 替换到 元素中
console.log(node.nodeValue);
}
return
}
// 如果不是文本类型,我们需要再去遍历
node.childNodes.forEach(child => {
fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
})
}
vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
}
此时已经可以在页面中显示了(当然是需要调用 ),大家可以看到页面已经没有{{}}了,这里提醒下大家好好学习下正则,递归,es6
// 首先我们先新建一个Vue实例
class Vue {
constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
// vue的源码也是$data,这里我们也使用$data
this.$data = obj_instance.data;
// 在这里,我们初始化实例的时候,需要监听data数据,我们用Observer来监听数据
Observer(this.$data)
// 调用html数据解析
Compile(obj_instance.el, this)
}
}
下面,我们就需要添加大家常听到的发布者,订阅者了
5、首先先创建个Dependency 收集与通知订阅者
// 收集与通知订阅者 类
class Dependency{
constructor(){
this.subscribes = []; // 存入订阅者的信息
}
addSub(sub){ // 添加 订阅者
this.subscribes.push(sub)
}
notify(){ // 通知订阅者的方法
this.subscribes.forEach(sub => sub.update())
}
}
然后创建订阅者
// 订阅者 类
class Watcher{
constructor(vm, key, callback){
this.vm = vm // vue实例
this.key = key // vue实例对应的属性
this.callback = callback // 记录如何更新文本内容
// 临时属性,触发getter
Dependency.temp = this
key.split('.').reduce((total, current) => total[current], vm.$data)
Dependency.temp = null // 防止订阅者多次加入到subscribes中
}
update(){
const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data)
this.callback(value)
}
}
订阅者在哪调用创建呢呢
// html 模板解析 element:dom里挂载的元素 vm:vue实例
function Compile(element, vm){
vm.$el = document.querySelector(element) // 获取挂载元素
const fragment = document.createDocumentFragment() // 创建文档碎片
let child;
while (child = vm.$el.firstChild) { // 遍历所有节点 将节点放入文档碎片中 (相当于一个临时空间)
fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
}
// 此时我们需要解释 fragment
fragment_compile(fragment)
function fragment_compile(node){
const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }} 模版语法
// 首先做个判断,看一下node类型
if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
// console.log(node);
// console.log(node.nodeValue);
const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
const node_value = node.nodeValue
const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
const value = arr.reduce((total, current) => total[current], vm.$data)
node.nodeValue = node_value.replace(pattern, value) // 将data中的数据 替换到 元素中
// 创建订阅者
new Watcher(vm, reg_regex[1], newValue=>{
node.nodeValue = node_value.replace(pattern, newValue) // 将data中的数据 替换到 元素中
})
}
return
}
// 如果不是文本类型,我们需要再去遍历
node.childNodes.forEach(child => {
fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
})
}
vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
}
还需要通知订阅者
// 监听数据变化,我们需要用到js的原生 api Object.defineProperty 进行数据劫持
function Observer(data_instance){
if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在 是否为object 主要作用于递归的时候
return
}
const dependency = new Dependency()
// 遍历对象 Object.keys 可以遍历对象的key 返回key数组
Object.keys(data_instance).forEach(key=>{
// Object.defineProperty(操作对象, 操作属性, {
// enumerable: true, // 数据是否可以被枚举
// configurable: true, // 属性描述符是否可以被改变
// get(){ }, // 属性被访问的时候触发
// set(){ } // 属性改变的时候触发
// })
let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
Observer(value) // 递归
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get(){
console.log(`访问了属性${key} => 值 ${value}`);
// 订阅者加入subscribes
Dependency.temp && dependency.addSub(Dependency.temp)
return value
},
set(newValue){
console.log(`属性${key}的值被修改为:${newValue}`);
value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
Observer(newValue) // 这里不用担心 啊, 不是对象就会直接被return
dependency.notify() // 通知 遍历自己的数组
}
})
})
}
上面核心的内容已经实现了,下面只需要将有v-model 属性的input标签进行处理
function Compile(element, vm){
vm.$el = document.querySelector(element) // 获取挂载元素
const fragment = document.createDocumentFragment() // 创建文档碎片
let child;
while (child = vm.$el.firstChild) { // 遍历所有节点 将节点放入文档碎片中 (相当于一个临时空间)
fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
}
// 此时我们需要解释 fragment
fragment_compile(fragment)
function fragment_compile(node){
const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }} 模版语法
// 首先做个判断,看一下node类型
if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
// console.log(node);
// console.log(node.nodeValue);
const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
const node_value = node.nodeValue
const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
const value = arr.reduce((total, current) => total[current], vm.$data)
node.nodeValue = node_value.replace(pattern, value) // 将data中的数据 替换到 元素中
// 创建订阅者
new Watcher(vm, reg_regex[1], newValue=>{
node.nodeValue = node_value.replace(pattern, newValue) // 将data中的数据 替换到 元素中
})
}
return
}
if(node.nodeType === 1 && node.nodeName === 'INPUT'){ // 找到input标签中有v-modle属性的
// const attr = node.attributes
// console.log(attr);
const attr = Array.from(node.attributes)
console.log(attr);
attr.forEach(val => {
if(val.nodeName === 'v-model'){
console.log(val.nodeValue);
const value = val.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
console.log(value);
node.value = value
new Watcher(vm, val.nodeValue, newValue => {
node.value = newValue
})
node.addEventListener('input', e => {
const arr1 = val.nodeValue.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce((total, current) => {
console.log(total, current);
return total[current]
}, vm.$data)
console.log(arr1, arr2, final);
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
// 如果不是文本类型,我们需要再去遍历
node.childNodes.forEach(child => {
fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
})
}
vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
}
ps:最后附上全部代码
<!--
* @Author: Qi
* @version: v1.2
* @Date: 2022-10-30 14:39:45
* @LastEditors: Qi
* @LastEditTime: 2022-10-30 17:27:08
* @Descripttion:
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue 双向绑定</title>
</head>
<body>
<div id="app">
<input type="text" v-model="name">
<span>姓名:{{ name }}</span>
<input type="text" v-model="body.age">
<span>年龄:{{ body.age }}</span>
</div>
<script>
// 首先我们先新建一个Vue实例
class Vue {
constructor(obj_instance){ // 我们的vue实例接收的是一个json对象
// vue的源码也是$data,这里我们也使用$data
this.$data = obj_instance.data;
// 在这里,我们初始化实例的时候,需要监听data数据,我们用Observer来监听数据
Observer(this.$data)
Compile(obj_instance.el, this)
}
}
// 监听数据变化,我们需要用到js的原生 api Object.defineProperty 进行数据劫持
function Observer(data_instance){
if(!data_instance || typeof data_instance !== 'object'){ // 这里需做个判断 data_instance 是否存在 是否为object 主要作用于递归的时候
return
}
const dependency = new Dependency()
// 遍历对象 Object.keys 可以遍历对象的key 返回key数组
Object.keys(data_instance).forEach(key=>{
// Object.defineProperty(操作对象, 操作属性, {
// enumerable: true, // 数据是否可以被枚举
// configurable: true, // 属性描述符是否可以被改变
// get(){ }, // 属性被访问的时候触发
// set(){ } // 属性改变的时候触发
// })
let value = data_instance[key] // 这里先存一下,为了首次绑定的时候返回数据
Observer(value) // 递归
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get(){
console.log(`访问了属性${key} => 值 ${value}`);
// 订阅者加入subscribes
Dependency.temp && dependency.addSub(Dependency.temp)
return value
},
set(newValue){
console.log(`属性${key}的值被修改为:${newValue}`);
value = newValue // 这里不需要return , 只需要把新的值赋值给vlaue
Observer(newValue) // 这里不用担心 啊, 不是对象就会直接被return
dependency.notify() // 通知 遍历自己的数组
}
})
})
}
// html 模板解析 element:dom里挂载的元素 vm:vue实例
function Compile(element, vm){
vm.$el = document.querySelector(element) // 获取挂载元素
const fragment = document.createDocumentFragment() // 创建文档碎片
let child;
while (child = vm.$el.firstChild) { // 遍历所有节点 将节点放入文档碎片中 (相当于一个临时空间)
fragment.append(child) // 此时,你会发现页面中啥也没有了,大家可以去了解下createDocumentFragment
}
// 此时我们需要解释 fragment
fragment_compile(fragment)
function fragment_compile(node){
const pattern = /\{\{\s*(\S+)\s*\}\}/g; // 匹配{{ xxx }} 模版语法
// 首先做个判断,看一下node类型
if(node.nodeType === 3){ // nodeType 等于 3 是文本类型
// console.log(node);
// console.log(node.nodeValue);
const reg_regex = pattern.exec(node.nodeValue) //匹配模板语法 ,这里正则就不多说了,不明白的小伙伴可要多去学习下正则了
if(reg_regex){ // 匹配完之后,可以打印下 控制台看下
const node_value = node.nodeValue
const arr = reg_regex[1].split('.') // 这里为啥要转数组,因为我们要更换data里的数据,而且模板语法中比如:body.age ,它实际上是string类型,转成数组之后用链式获取的方式
const value = arr.reduce((total, current) => total[current], vm.$data)
node.nodeValue = node_value.replace(pattern, value) // 将data中的数据 替换到 元素中
// 创建订阅者
new Watcher(vm, reg_regex[1], newValue=>{
node.nodeValue = node_value.replace(pattern, newValue) // 将data中的数据 替换到 元素中
})
}
return
}
if(node.nodeType === 1 && node.nodeName === 'INPUT'){ // 找到input标签中有v-modle属性的
// const attr = node.attributes
// console.log(attr);
const attr = Array.from(node.attributes)
console.log(attr);
attr.forEach(val => {
if(val.nodeName === 'v-model'){
console.log(val.nodeValue);
const value = val.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
console.log(value);
node.value = value
new Watcher(vm, val.nodeValue, newValue => {
node.value = newValue
})
node.addEventListener('input', e => {
const arr1 = val.nodeValue.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce((total, current) => {
console.log(total, current);
return total[current]
}, vm.$data)
console.log(arr1, arr2, final);
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
// 如果不是文本类型,我们需要再去遍历
node.childNodes.forEach(child => {
fragment_compile(child) // 这边需要用到递归,这样可以遍历所有的子节点
})
}
vm.$el.appendChild(fragment) // 将文档碎片添加到$el中
}
// 收集与通知订阅者 类
class Dependency{
constructor(){
this.subscribes = []; // 存入订阅者的信息
}
addSub(sub){ // 添加 订阅者
this.subscribes.push(sub)
}
notify(){ // 通知订阅者的方法
this.subscribes.forEach(sub => sub.update())
}
}
// 订阅者 类
class Watcher{
constructor(vm, key, callback){
this.vm = vm // vue实例
this.key = key // vue实例对应的属性
this.callback = callback // 记录如何更新文本内容
// 临时属性,触发getter
Dependency.temp = this
key.split('.').reduce((total, current) => total[current], vm.$data)
Dependency.temp = null // 防止订阅者多次加入到subscribes中
}
update(){
const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data)
this.callback(value)
}
}
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
name:'小明',
body: {
age: 20
}
}
})
console.log(vm);
</script>
</body>
</html>