vue2双向绑定的实现
通过四个步骤循序渐进,加深对vue2.0响应式的理解
一、极简双向绑定
html页面
<!-- QAQshfit专用模板 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>极简双向绑定</title>
<style>
</style>
<script src="./index.js"></script>
</head>
<body>
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
</body>
<script>
</script>
</html>
js
var obj = {}
Object.defineProperty(obj, 'data1', {
//data1发生改变时调用
set: function (newVal) {
console.log('set调用')
document.getElementById('a').value = newVal
document.getElementById('b').innerHTML = newVal
},
get: function () {
console.log('get调用')
}
})
document.addEventListener('keyup', function (e) {
obj.data1=e.target.value
console.log('按下')
})
/*
这里只实现了view和model简单的双向绑定,只有一个data,而不是data对象,并且没有对v-model和{{}}进行处理
*/
二、初始化渲染
html
<!-- QAQshfit专用模板 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>初始化渲染</title>
<style>
</style>
</head>
<body>
<div id="app">
<input type="text" id="a" v-model="d1">
{{d1}}
</div>
</body>
<script>
</script>
<script src="./index.js"></script>
</html>
js
/*
该过程值完成了从model到view的初始化渲染
具体过程为:
1.vue实例创建,vue方法执行。
2.vue将挂载对象的子节点进行劫持,传入vue实例
3.获取到子节点,并用compile进行处理,传入vue实例
4.将带有绑定属性的结点,将其值修改为vue中data的值
5.将处理的结点返回,再进行挂载
*/
// 用一个方法对node结点进行劫持处理
function nodeToFragment(node,vm) {
/*
DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。
*/
//DocumentFragment处理节点,速度和性能远远优于直接操作DOM。
var flag = document.createDocumentFragment()
var child;
//node.firstChild返回node的第一个子节点
//下面原理是,如果子节点存在就添加到DocumentFragment当中
while (child = node.firstChild) {
//对子节点进行处理
compile(child,vm)
flag.append(child); //劫持node的所有子节点
}
return flag;
}
//对双向绑定和单向绑定进行处理
function compile(node, vm) {
//.表示任意字符 *表示任意数量
var reg = /\{\{(.*)\}\}/
console.log('结点类型' + node.nodeType)
//结点类型为元素
if (node.nodeType === 1) {
//获取节点属性
var attr = node.attributes;
//解析属性
for (var i = 0; i < attr.length; i++) {
//如果属性为 v-model
if (attr[i].nodeName == 'v-model') {
//获取到v-model的值 即data的属性名
var name = attr[i].nodeValue
node.value = vm.data[name] //data的值赋给node
node.removeAttribute('v-model')
}
}
}
//结点类型为text
if (node.nodeType === 3) {
console.log(node.nodeValue)
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; //获取匹配的字符串
name = name.trim()
node.nodeValue = vm.data[name] //data的值赋给node
}
}
}
function Vue({ el, data } = {}) {
this.data = data
var id = el
console.log()
// 返回一个新的dom结点
var dom = nodeToFragment(document.getElementById(id), this)
//将处理完的结点重新挂载到根结点
document.getElementById(id).appendChild(dom)
}
var vm=new Vue({
el:'app',
data:{
d1:'hello world'
}
})
三、响应式数据绑定
html
<!-- QAQshfit专用模板 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>响应式数据绑定</title>
<style>
</style>
</head>
<body>
<div id="app">
<input type="text" id="a" v-model="d1">
{{d1}}
</div>
</body>
<script>
</script>
<script src="./index.js"></script>
</html>
js
/*
该过程值完成了从model到view的初始化渲染,同时增加了对data中数据的监听,即model可以监听到view的变化了
具体过程为:
1.vue实例创建,vue方法执行。
2.observe遍历data中的数据,传入defineReactive处理
3.defineReactive中为每个数据绑定set和get方法,来对data中的数据进行监听
4.vue将挂载对象的子节点进行劫持,传入data
5.获取到子节点,并用compile进行处理,传入data
6.将带有绑定属性的结点,将其值修改为vue中data的值
7.将处理的结点返回,再进行挂载
*/
//作用:给数据绑定set,和get,可以监听到model中数据的变化
//obj:data对象 key:data中的一个数据 val:view的值
function defineReactive(obj,key,val){
Object.defineProperty(obj, key, {
get: function () {
console.log('get调用')
return val
},
//data1发生改变时调用
set: function (newVal) {
console.log('set调用')
//如果view的数据发生改变则更新data中的数据
if(newVal===val) return
val=newVal
console.log(val)
}
})
}
//给每一个数据进行绑定处理
function observe(obj){
Object.keys(obj).forEach(function(key){
defineReactive(obj,key,obj[key])
})
}
// 用一个方法对node结点进行劫持处理
function nodeToFragment(node,data) {
/*
DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。
*/
//DocumentFragment处理节点,速度和性能远远优于直接操作DOM。
var flag = document.createDocumentFragment()
var child;
//node.firstChild返回node的第一个子节点
//下面原理是,如果子节点存在就添加到DocumentFragment当中
while (child = node.firstChild) {
//对子节点进行处理
compile(child,data)
flag.append(child); //劫持node的所有子节点
}
return flag;
}
//对双向绑定和单向绑定进行处理
function compile(node, data) {
//.表示任意字符 *表示任意数量
var reg = /\{\{(.*)\}\}/
console.log('结点类型' + node.nodeType)
//结点类型为元素
if (node.nodeType === 1) {
//获取节点属性
var attr = node.attributes;
//解析属性
for (var i = 0; i < attr.length; i++) {
//如果属性为 v-model
if (attr[i].nodeName == 'v-model') {
//获取到v-model的值 即data的属性名
var name = attr[i].nodeValue
node.addEventListener('input',function(e){
//给相应的data属性赋值,进而触发该属性的set方法
data[name]=e.target.value;
})
node.value = data[name] //data的值赋给node
node.removeAttribute('v-model')
}
}
}
//结点类型为text
if (node.nodeType === 3) {
console.log(node.nodeValue)
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; //获取匹配的字符串
name = name.trim()
node.nodeValue = data[name] //data的值赋给node
}
}
}
function Vue({ el, data } = {}) {
this.data = data
var id = el
observe(this.data)
// 返回一个新的dom结点
var dom = nodeToFragment(document.getElementById(id), this.data)
//将处理完的结点重新挂载到根结点
document.getElementById(id).appendChild(dom)
}
var vm=new Vue({
el:'app',
data:{
d1:'hello world'
}
})
四、双向绑定
html
<!-- QAQshfit专用模板 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>响应式数据绑定</title>
<style>
</style>
<script src="./index.js"></script>
</head>
<body>
<div id="app">
<input type="text" id="a" v-model="d1">
{{d1}}
<div>{{d2}}
<div style="height:100px;width:100px;">{{d3}}</div>
</div>
</div>
</body>
<script>
var vm=new Vue({
el:'app',
data:{
d1:'hello world',
d2:'我是外层div',
d3:'我是内层div'
}
})
</script>
</html>
js
/*
该过程值完成了从model到view的初始化渲染,同时增加了对data中数据的监听,即model可以监听到view的变化了
具体过程为:
1.vue实例创建,vue方法执行。
2.observe遍历data中的数据,传入defineReactive处理
3.defineReactive中为每个数据绑定set和get方法,来对data中的数据进行监听
4.vue将挂载对象的子节点进行劫持,传入data
5.获取到子节点,并用compile进行处理,传入data
6.将带有绑定属性的结点,将其值修改为vue中data的值
7.将处理的结点返回,再进行挂载
而在compile处理的过程中,每一个文本类型结点都实例化了一个Watcher
实例的过程中会把自己添加到Dep当中
当data中的数据发生改变,dep就会遍历并调用Watcher的更新方法
*/
//作用:给数据绑定set,和get,可以监听到model中数据的变化
//obj:data对象 key:data中的一个数据 val:view的值
function defineReactive(obj,key,val){
var dep=new Dep();
Object.defineProperty(obj, key, {
get: function () {
console.log('get调用')
if(Dep.target) dep.addSub(Dep.target)
return val
},
//data1发生改变时调用
set: function (newVal) {
console.log('set调用')
//如果view的数据发生改变则更新data中的数据
if(newVal===val) return
val=newVal
dep.notify()
console.log(val)
}
})
}
//给每一个数据进行绑定处理
function observe(obj){
Object.keys(obj).forEach(function(key){
defineReactive(obj,key,obj[key])
})
}
// 用一个方法对node结点进行劫持处理
function nodeToFragment(node,data) {
/*
DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。
*/
//DocumentFragment处理节点,速度和性能远远优于直接操作DOM。
var flag = document.createDocumentFragment()
var child;
// //node.firstChild返回node的第一个子节点
// //下面原理是,如果子节点存在就添加到DocumentFragment当中
// while (child = node.firstChild) {
// //对子节点进行处理
// compile(child,data)
// flag.append(child); //劫持node的所有子节点
// }
//对子节点进行遍历,每一个都进行处理
for(var i=0;i<node.childNodes.length;i++){
child=node.childNodes[i]
compile(child,data)
}
flag.append(child);
return flag;
}
//对双向绑定和单向绑定进行处理
function compile(node, data) {
//.表示任意字符 *表示任意数量
var reg = /\{\{(.*)\}\}/
console.log('结点类型' + node.nodeType)
//结点类型为元素
if (node.nodeType === 1) {
//获取节点属性
var attr = node.attributes;
//解析属性
for (var i = 0; i < attr.length; i++) {
//如果属性为 v-model
if (attr[i].nodeName == 'v-model') {
//获取到v-model的值 即data的属性名
var name = attr[i].nodeValue
node.addEventListener('input',function(e){
//给相应的data属性赋值,进而触发该属性的set方法
data[name]=e.target.value;
})
node.value = data[name] //data的值赋给node
node.removeAttribute('v-model')
}
}
if(node.childNodes.length>0){
var child;
for(var i=0;i<node.childNodes.length;i++){
child=node.childNodes[i]
arguments.callee(child,data)
}
}
}
//结点类型为text
if (node.nodeType === 3) {
console.log(node.nodeValue)
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; //获取匹配的字符串
name = name.trim()
console.log(data)
// node.nodeValue = data[name] //data的值赋给node
new Watcher(data,node,name)
}
}
}
function Watcher(data,node,name) {
Dep.target=this
this.name=name
this.node=node
this.vm=data
this.update()
Dep.target=null
}
Watcher.prototype={
update:function(){
this.get();
this.node.nodeValue=this.value
},
get:function(){
this.value=this.vm[this.name]
}
}
function Dep() {
this.subs=[]
}
Dep.prototype={
addSub:function(sub){
this.subs.push(sub)
},
notify:function(){
this.subs.forEach((sub)=>{
sub.update()
})
}
}
function Vue({ el, data } = {}) {
this.data = data
var id = el
observe(this.data)
// 返回一个新的dom结点
var dom = nodeToFragment(document.getElementById(id), this.data)
//将处理完的结点重新挂载到根结点
document.getElementById(id).appendChild(dom)
}