双向绑定的核心原理
核心是采用数据劫持结合发布者订阅者模式,通过Object.defineProperty()对每个属性的get和set进行拦截。在数据发生变化的时候发布消息给订阅者,触发相应的监听回调。
仅仅使用Object,defineProperty()就能完成一个简单的双向绑定,但是效率比较低。观察者模式让双向绑定更有效率,它是一对多的模式,一指的是修改的一处数据,多是凡是用了这个数据的地方都更新。数据劫持就是利用Object.defineProperty()读操作数据的set,get。
极简的双向绑定
Object.defineProperty()有三个参数,第一个是属性所在的对象。第二个是要操作的属性,第三个是被操作的属性的特性,是一个对象{},一般是两个get:读取属性时触发,set写入属性时触发。
<input type="text" id="inputtext">
<p id="ptext"></p>
let obj={}
Object.defineProperty(obj,'val',{
get:function(){},
set:function(_val){
document.getElementById("inputtext").value=_val
document.getElementById('ptext').innerHTML=_val
}
})
document.addEventListener("keyup",function(e){
obj.val=e.target.value
})
简单的发布订阅模式
1.Dep类
负责进行依赖收集
首先要有一个数组,存放订阅信息
其次提供一个向数组中追加订阅信息的方法
然后提供一个循环,循环触发数组中的每个订阅信息
2.watcher类
负责订阅一些事件
//收集依赖/收集订阅者
class Dep{
constructor(){
//存放订阅者的数组
this.subs=[]
}
//将订阅者存放到数组中
add(watcher){
this.subs.push(watcher)
}
//发布通知的方法
notify(){
this.subs.forEach(watcher=>{
return watcher.update()
})
}
}
//订阅者的类
class Watcher{
//接收一个回调函数,将回调函数挂载到自己身上
constructor(callback){
this.callback=callback
}
//触发回调的方法
update(){
this.callback()
}
}
let w1=new Watcher(()=>{console.log('第一个订阅者')})
let w2=new Watcher(()=>{console.log('第二个订阅者')})
let dep=new Dep()
dep.add(w1)
dep.add(w2)
//只要我们为vue中data的数据重新赋值,vue会监听到,并把数据的变化通知到订阅者,订阅者(DOM元素)会根据最新的数据,更新内容
dep.notify()
Object.defineProperty()
let obj={
name:'lisa',
age:23
}
Object.defineProperty(obj,'name',{
enumerable:true, //当前属性允许被循环
configurable:true,//当前属性允许被配置
get(){ //getter
console.log("name属性被访问")
},
set(newVal){ //setter
console.log("name属性被修改为"+newVal)
}
})
obj.name //访问
obj.name="zhao" //修改
实现数据劫持
<div id="app">
姓名是:{{name}}
年龄:{{age}}
</div>
const vm = new Vue({
el: '#app',
data: {
name: 'lisa',
age: 21,
info: {
a: "a1",
b: "b1"
}
},
})
console.log(vm)
class Vue {
constructor(options) {
this.$data = options.data
//调用数据劫持的方法
Observer(this.$data)
//属性代理,不使用代理时,访问数据this.$data.xx
//使用代理后访问this.xxx
Object.keys(this.$data).forEach(key=>{
//为this添加了key属性
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get(){
return this.$data[key]
},
set(newVal){
this.$data[key]=newVal
}
})
})
}
}
//定义一个数据劫持的方法
function Observer(obj) {
//递归的终止条件
if (!obj || typeof obj !== "object") return
//获取obj的属性并添加set,get
Object.keys(obj).forEach((key) => {
let value = obj[key]
//把value这个子节点进行递归
Observer(value)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//属性被访问时,返回value
get() {
console.log(`有人获取了${key}的值`)
return value
},
set(newVal) {
value = newVal
//给属性重新赋值后,新的属性值没有了get和set,需要重新递归
Observer(value)
}
})
})
}
模板编译
创建文档碎片,提高DOM操作的性能
在浏览器中,一旦把节点插入到document.body(或其他节点)中,页面就会更新并反映出这种变化。但是当要向document添加大量数据时,这个过程就可能会十分缓慢。
为了解决这个问题,我们可以引入createDocumentFragment()方法,它的作用是创建一个文档碎片,把要插入的新节点先附加在它上面,然后再一次性添加到document中
function Compile(el,vm){
//获取el对应的DOM元素
vm.$el=document.querySelector(el)
//创建文档碎片
const fragment=document.createDocumentFragment()
//当存在子节点时,将节点加入到文档碎片中
while(childNode=vm.$el.firstChild){
fragment.appendChild(childNode)
}
//进行模板编译
replace(fragment);
//将文档碎片中的节点加入到DOM节点中
vm.$el.appendChild(fragment)
//创建一个对模板进行编译的方法
function replace(node){
//定义匹配插值表达式的正则
const regMustache=/\{\{\s*(\S+)\s*\}\}/
//判断如果为纯文本节点,则进行文本的替换
if(node.nodeType ===3){
const text=node.textContent
//数组第一个值为替换前的值,第二个值为替换后的值
const exec=regMustache.exec(text)
console.log(exec)
//如果内容存在(空格也为一个文本节点)
if(exec){
//对值进行分割,再拿到最下面的值
let value=exec[1].split('.').reduce((newObj,k)=>{
return newObj[k]
},vm)
node.textContent=text.replace(regMustache,value)
}
return
}
//不是纯文本节点,可能是DOM元素需要进行递归处理
node.childNodes.forEach(child=>{
replace(child)
})
}
}
创建Dep累进行依赖收集
class Dep{
constructor(){
this.subs=[]
}
addSub(watcher){
this.subs.push(watcher)
}
//负责通知每个watcher的方法
notify(){
this.subs.forEach(watcher=>
watcher.update()
)
}
}
完整的双向绑定
<div id="app">
<h3>姓名是:{{name}}</h3>
<h3>年龄:{{age}}</h3>
<h3>a的值是:{{info.a}}</h3>
姓名是:<input type="text" v-model="name">
</div>
const vm = new Vue({
el: '#app',
data: {
name: 'lisa',
age: 21,
info: {
a: "a1",
b: "b1"
}
},
})
class Vue {
constructor(options) {
this.$data = options.data
//调用数据劫持的方法
Observer(this.$data)
//属性代理,不使用代理时,访问数据this.$data.xx
//使用代理后访问this.xxx
Object.keys(this.$data).forEach(key=>{
//为this添加了key属性
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get(){
return this.$data[key]
},
set(newVal){
this.$data[key]=newVal
}
})
})
//调用模板编译的函数
Compile(options.el,this)
}
}
//定义一个数据劫持的方法
function Observer(obj) {
//递归的终止条件
if (!obj || typeof obj !== "object") return
const dep=new Dep()
//获取obj的属性并添加set,get
Object.keys(obj).forEach((key) => {
let value = obj[key]
//把value这个子节点进行递归
Observer(value)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//属性被访问时,返回value
get() {
// console.log(`有人获取了${key}的值`)
//将watcher的实例放到dep.subs数组中
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
value = newVal
//给属性重新赋值后,新的属性值没有了get和set,需要重新递归
Observer(value)
//通知每个订阅者更新自己的文本
dep.notify()
}
})
})
}
//对HTML结构进行模板编译的方法
function Compile(el,vm){
//获取el对应的DOM元素
vm.$el=document.querySelector(el)
//创建文档碎片
const fragment=document.createDocumentFragment()
//当存在子节点时,将节点加入到文档碎片中
while(childNode=vm.$el.firstChild){
fragment.appendChild(childNode)
}
//进行模板编译
replace(fragment);
//将文档碎片中的节点加入到DOM节点中
vm.$el.appendChild(fragment)
//创建一个对模板进行编译的方法
function replace(node){
//定义匹配插值表达式的正则
const regMustache=/\{\{\s*(\S+)\s*\}\}/
//判断如果为纯文本节点,则进行文本的替换
if(node.nodeType ===3){
const text=node.textContent
//数组第一个值为替换前的值,第二个值为替换后的值
const exec=regMustache.exec(text)
//如果内容存在(空格也为一个文本节点)
if(exec){
//对值进行分割,再拿到最下面的值
let value=exec[1].split('.').reduce((newObj,k)=>{
return newObj[k]
},vm)
node.textContent=text.replace(regMustache,value)
//在这个时候,创建watcher类的实例
new Watcher(vm,exec[1],(newVal)=>{
node.textContent=text.replace(regMustache,newVal)
})
}
//终止递归的条件
return
}
//判断是否为一个元素,并且是否为input输入框
if(node.nodeType ===1 && node.tagName.toUpperCase() =="INPUT"){
//得到当前元素的所有属性节点
const attrs=Array.from(node.attributes)
const findResult=attrs.find(x=>x.name ==="v-model"
)
if(findResult){
//获取到当前v-model属性的值,v-modal="name",v-modal="info.a"
const expStr=findResult.value
const value=expStr.split(".").reduce((newObj,k)=>newObj[k],vm)
node.value=value
//创建watcher的实例
new Watcher(vm,expStr,(newValue)=>{
node.value=newValue
})
//监听文本框的input输入事件,拿到文本框最新的值,更新到vm上
node.addEventListener('input',(e)=>{
const keyArr=expStr.split('.')
const obj=keyArr.slice(0,keyArr.length-1).reduce((newObj,k)=>newObj[k],vm)
console.log(obj)
obj[keyArr[keyArr.length-1]]=e.target.value
})
}
}
//不是纯文本节点,可能是DOM元素需要进行递归处理
node.childNodes.forEach(child=>{
replace(child)
})
}
}
//收集依赖/收集订阅者
class Dep{
constructor(){
this.subs=[]
}
addSub(watcher){
this.subs.push(watcher)
}
//负责通知每个watcher的方法
notify(){
this.subs.forEach(watcher=>
watcher.update()
)
}
}
//订阅者的类
class Watcher{
//cb回调函数中,记录着当前watcher如何更新自己的文本内容,
//但是,知道如何更新自己还不行,还必须拿到最新的数据
//因此,还需要在new Watcher期间,把vm也传进来(因为vm中保存着最新的数据)
//除此之外,还需要知道在vm的数据中,哪个才是自己需要的数据
//因此,必须在new Watcher期间,指定watcher对应数据的名字
constructor(vm,key,cb){
this.vm=vm
this.key=key
this.cb=cb
//下面3行负责把创建的watcher实例存放到subs数组中
Dep.target=this //this指向watcher实例
//取值目的是去执行get()
key.split(".").reduce((newObj,key)=>newObj[key],vm)
Dep.target=null
}
//watcher的实例,需要有update函数,从而让发布者能够通知我们进行更新
update(){
const value=this.key.split(".").reduce((newObj,key)=>newObj[key],this.vm)
this.cb(value)
}
}