在说vue双向绑定原理前,先了解下要用到那些知识点:文档碎片DocumentFragment、Object.defineProperty。首先简单介绍下这两个知识点。
DocumentFragment
DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。
<body><ul></ul>
<script>
let a = document.querySelector('ul')
let fragMent = document.createDocumentFragment()
let arr = [1,2,3] arr.forEach((item)=>{
let li = document.createElement('li')
li.innerText = item
fragMent.appendChild(li)
})
a.appendChild(fragMent)</script></body>
页面效果
Object.defineProperty
**Object.defineProperty()** 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
示例1:给obj添加属性
let obj = {}
// 给obj添加一个叫‘xyz’的name属性
Object.defineProperty(obj,'name',{
value:'xyz'
})
console.log(obj);
示例2:
let obj = {}
Object.defineProperty(obj,'name',{
get:()=>{
console.log('获取到了');
return obj[name]
},
set:(newValue)=>{
console.log('修改了');
obj[name] = newValue
}})
obj.name = 'zxy'
console.log(obj.name);
先了解这两个我们在继续接下来的
Observe
定义一个类给对象的每个属性都添加上definProperty
class Observe{
// 把要观察的对象传进来
constructor(obj) {
this.query(obj)
}
query(data){
// 看有没有值,并且是否是个对象
if (data && typeof data === 'object'){
//遍历这个对象
for (let key in data){
this.addQuery(data,key,data[key])
}
}
}
// 定义这个方法专门为对象的每个属性绑定defineProperty
addQuery(data,key,value){
// 是否属性的值为对象
this.query(value)
// 给对象的属性绑定defineProperty
Object.defineProperty(data,key,{
// get是获取属性值
get() {
console.log('获取到了');
return value
},
// set是修改属性
set:(v) => {
// 如果新的属性值不等于旧值
if (value !== v){
// 看修改的值是否为对象
this.query(v)
value = v
}
}
})
}
}
let obj = {
name: 'xyz'
}
new Observe(obj)
console.log(obj.name);
接下来我们就写出一个自己的vue
定义自己的ZUE
HTML结构
<body>
<div id="app">
<input type="text" v-model = 'name'>
<p>{{name}}</p>
</div>
<script>
let zue = new Zue({
el:"#app",
data:{
name: 'xyz'
}
})
</script>
</body>
通过new来创建vue实例,说明vue也是一个类
class Zue{
constructor(vm) {
// 因为vue创建的时候指定的区域可以是一个ID名称,也可以是一个DOM元素
if (this.isElement(vm.el)){
this.$el = vm.el
}else {
this.$el = document.querySelector(vm.el)
}
this.$data = vm.data
//在vue中我们可以通过this.的方法获取data中的数据。
this.proxyData()
// 先判断渲染区域有没有拿到
if (this.$el){
// 给所有的数据添加监听,用到了上面的Observe
new Observe(this.$data)
}
}
//我们将data中的数据绑定到this上
proxyData(){
for (let key in this.$data){
Object.defineProperty(this,key,{
get: () =>{
return this.$data[key]
}
})
}
}
//判断是否为一个元素节点
isElement(node){
return node.nodeType === 1
}
}
到这一步我们就可以通过data来拿到元素和data中的数据了
Render
//专门负责渲染
class Render{
constructor(vm) {
this.vm = vm
//1、将网页中的元素放到内存中渲染
let fragment = this.nodeFragment(this.vm.$el)
//2、利用指定的数据替换元素中的
// console.log(fragment);
this.forFragment(fragment)
//3、将编译好的元素重新渲染到页面上
this.vm.$el.appendChild(fragment)
}
nodeFragment(app){
//创建一个文档碎片把元素装进去
let newNode = document.createDocumentFragment()
let node = app.firstChild
while (node){
// 只要把界面元素放到文档碎片中,这个元素就会从界面上消失,放到内存中
newNode.appendChild(node)
node = app.firstChild
}
// 将元素全部放完后返回文档碎片对象
return newNode
}
forFragment(nodes){
// 利用文档碎片的方法childNodes拿出所有的元素,但是这是个伪数组。
// 利用扩展运算符转换一下
let nodeList = [...nodes.childNodes]
nodeList.forEach(node => {
// 判断是否是一个元素
if (this.vm.isElement(node)){
// 处理元素的方法
this.handleElement(node)
// 可能包含子元素,要递归调用遍历子元素
this.forFragment(node)
}else {
// 处理文本的方法
this.handleText(node)
}
})
}
// 处理元素的方法
handleElement(node){
let allAttr = [...node.attributes]
allAttr.forEach(attr => {
//结构赋值
let {name,value} = attr
// console.log(name, value,'----');
// 判断是否为v-开头
if (name.startsWith('v-')){
// 这里进一步分割是看如果有v-on:click的情况
let [a,b] = name.split(':')
// console.log(a, b);
let [,command] = a.split('-')
ZueMethod[command](node,value,this.vm,b)
}
})
}
// 处理文本的方法
handleText(node){
let nodeText = node.textContent
// console.log(nodeText);
// 利用正则匹配到{{}}
let res = /\{\{.+?\}\}/gi
if (res.test(nodeText)){
//ZueMethod专门处理这些指令的。等会下面实现
ZueMethod['textMethod'](node,nodeText,this.vm)
}
}
}
在实现ZueMethod对象之前,先了解下数据驱动视图。怎么实现对象(model)和视图(view)的自动更新。
简单说下流程:
-
我们定义的Observe进行数据劫持
-
然后在需要订阅的地方(如:模版编译),添加观察者(Watcher)
-
在Observe的get方法中添加订阅
-
在Observe的set方法中发布订阅
Watcher观察者
//观察者class Watcher{
constructor(vm,value,fn) {
// vm就是当前实例,value就是指令赋的值,fn回调函数
this.vm = vm
this.value = value
this.fn = fn
this.oldValue = this.getOldValue()
}
// 获取旧的值保存起来
getOldValue(){
// 实例化后当前this指向当前实例
Dep.target = this
let oldValue = ZueMethod.getVlaue(this.value,this.vm)
Dep.target = null
return oldValue
}
//获取新的值进行比较
update(){
let newValue = ZueMethod.getVlaue(this.value,this.vm)
if (newValue !== this.oldValue){
this.fn(newValue,this.oldValue)
}
}
}
Dep订阅
//订阅
class Dep{
constructor() {
this.sub = []
}
addSub(watcher){
this.sub.push(watcher)
}
notify(){
this.sub.forEach(watch => watch.update())
}
}
之前说的ZueMethod就是编译模板的地方,也就是添加观察者的地方
添加观察者
let ZueMethod = {
//这里是如果对象中的属性值是对象进行处理,比如a.b.c
getVlaue(value,vm){
return value.split('.').reduce((a,b) => {
return a[b.trim()]
},vm.$data)
},
// 对模板中的数据进行处理
getText(value,vm){
let res = /\{\{(.+?)\}\}/gi
// 这里是如果插值语法进行处理
let val = value.replace(res,(...args) =>{
console.log(args);
return this.getVlaue(args[1],vm)
})
console.log(val);
return val
},
// 获取新的值
setValue(vm,value,newValue){
value.split('.').reduce((a,b,index,arr) => {
if (index === arr.length-1){
a[b.trim()] = newValue
}
return a[b.trim()]
},vm.$data)
},
// v-model
model:function (node,value,vm){
// 添加监听
new Watcher(vm,value,(newValue,oldValue) => {
// console.log('执行了',newValue);
node.value = newValue
})
let newValue = this.getVlaue(value,vm)
node.value = newValue
// 获取最新的数据
node.addEventListener('input', (e) =>{
let newValue = e.target.value
this.setValue(vm,value,newValue)
})
},
//v-html
html:function (node,value,vm){
new Watcher(vm,value,(newValue,oldValue) => {
node.innerHTML = newValue
})
let val = this.getVlaue(value,vm)
node.innerHTML = val
},
// v-text
text:function (node,value,vm){
new Watcher(vm,value,(newValue,oldValue) => {
node.innerText = newValue
})
let val = this.getVlaue(value,vm)
node.innerText = val
},
//v-on
on:function (node,value,vm,b){
node.addEventListener(b, (e) =>{
vm.$methods[value].call(vm,e)
})
},
// 处理模板中数据的方法
textMethod:function (node,value,vm){
// console.log(value);
let res = /\{\{(.+?)\}\}/gi
let val = value.replace(res,(...args) => {
// console.log(args);
new Watcher(vm,args[1],(newValue,oldValue)=>{
node.textContent = this.getText(value,vm)
})
return this.getVlaue(args[1],vm)
})
node.textContent = val
}
}
哈哈,代码比较多,大家看一个指令就行了。其他的大差不差
这时候我们的vue就基本实现完成了,但是还有个最重要的!就是我们添加订阅和发布订阅了!
在definEProperty中,我们获取数据会触发get方法,修改数据会触发set方法。所以我们可以在get中添加订阅,set中发布订阅
小小的改动下Observe中defineProperty的get和set方法
get() {
// 添加订阅。我们在Watcher给Dep绑定了一个target属性
// 在编译模板时,Watcher进行实例化,添加了观察者。那么this就指向当前实例
Dep.target && dep.addSub(Dep.target)
return value},
set:(v) => {
if (value !== v){
// console.log(v);
// 修改的值是否为对象
this.query(v)
value = v
// 数据改变时发布订阅
dep.notify()
}
}
完整版
将以上组合在一起。差不多就是个简化版的vue啦
具体代码如下,有点多==
let ZueMethod = {
getVlaue(value,vm){
return value.split('.').reduce((a,b) => {
return a[b.trim()]
},vm.$data)
},
getText(value,vm){
let res = /\{\{(.+?)\}\}/gi
let val = value.replace(res,(...args) =>{
return this.getVlaue(args[1],vm)
})
return val
},
setValue(vm,value,newValue){
value.split('.').reduce((a,b,index,arr) => {
if (index === arr.length-1){
a[b.trim()] = newValue
}
return a[b.trim()]
},vm.$data)
},
model:function (node,value,vm){
new Watcher(vm,value,(newValue,oldValue) => {
node.value = newValue
})
let newValue = this.getVlaue(value,vm)
node.value = newValue
node.addEventListener('input', (e) =>{
let newValue = e.target.value
this.setValue(vm,value,newValue)
})
},
html:function (node,value,vm){
new Watcher(vm,value,(newValue,oldValue) => {
node.innerHTML = newValue
})
let val = this.getVlaue(value,vm)
node.innerHTML = val
},
text:function (node,value,vm){
new Watcher(vm,value,(newValue,oldValue) => {
node.innerText = newValue
})
let val = this.getVlaue(value,vm)
node.innerText = val
},
on:function (node,value,vm,b){
node.addEventListener(b, (e) =>{
vm.$methods[value].call(vm,e)
})
},
textMethod:function (node,value,vm){
let res = /\{\{(.+?)\}\}/gi
let val = value.replace(res,(...args) => {
new Watcher(vm,args[1],(newValue,oldValue)=>{
node.textContent = this.getText(value,vm)
})
return this.getVlaue(args[1],vm)
})
node.textContent = val
}
}
class Zue{
constructor(vm) {
if (this.isElement(vm.el)){
this.$el = vm.el
}else {
this.$el = document.querySelector(vm.el)
}
this.$data = vm.data
this.$methods = vm.methods
this.proxyData()
if (this.$el){
new Observe(this.$data)
new Render(this)
}
}
proxyData(){
for (let key in this.$data){
Object.defineProperty(this,key,{
get: () =>{
return this.$data[key]
}
})
}
}
isElement(node){
return node.nodeType === 1
}
}
//专门负责渲染
class Render{
constructor(vm) {
this.vm = vm
let fragment = this.nodeFragment(this.vm.$el)
this.forFragment(fragment)
this.vm.$el.appendChild(fragment)
}
nodeFragment(app){
let newNode = document.createDocumentFragment()
let node = app.firstChild
while (node){
newNode.appendChild(node)
node = app.firstChild
}
return newNode
}
forFragment(nodes){
let nodeList = [...nodes.childNodes]
nodeList.forEach(node => {
if (this.vm.isElement(node)){
this.handleElement(node)
this.forFragment(node)
}else {
this.handleText(node)
}
})
}
handleElement(node){
let allAttr = [...node.attributes]
allAttr.forEach(attr => {
let {name,value} = attr
if (name.startsWith('v-')){
let [a,b] = name.split(':')
let [,command] = a.split('-')
ZueMethod[command](node,value,this.vm,b)
}
})
}
handleText(node){
let nodeText = node.textContent
let res = /\{\{.+?\}\}/gi
if (res.test(nodeText)){
ZueMethod['textMethod'](node,nodeText,this.vm)
}
}
}
class Observe{
constructor(obj) {
this.query(obj)
}
query(data){
if (data && typeof data === 'object'){
for (let key in data){
this.addQuery(data,key,data[key])
}
}
}
addQuery(data,key,value){
this.query(value)
let dep = new Dep()
Object.defineProperty(data,key,{
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set:(v) => {
if (value !== v){
this.query(v)
value = v
dep.notify()
}
}
})
}
}
//发布订阅
class Dep{
constructor() {
this.sub = []
}
addSub(watcher){
this.sub.push(watcher)
}
notify(){
this.sub.forEach(watch => watch.update())
}
}
//观察者
class Watcher{
constructor(vm,value,fn) {
this.vm = vm
this.value = value
this.fn = fn
this.oldValue = this.getOldValue()
}
getOldValue(){
Dep.target = this
let oldValue = ZueMethod.getVlaue(this.value,this.vm)
Dep.target = null
return oldValue
}
update(){
let newValue = ZueMethod.getVlaue(this.value,this.vm)
if (newValue !== this.oldValue){
this.fn(newValue,this.oldValue)
}
}
}
我表达的可能不是太好,不太容易理解,也可能有错误的地方我没检查出来。希望大家多多包含😊