vue双向数据绑定原理与实现
Object.defineProperty
语法:Object.defineProperty(obj,prop,descriptor)
obj:目标对象
prop:需要定义的属性和方法的名称
descriptor:目标属性所拥有的特性
第三个参数对应为对象,可供定义的属性列表
value:属性的值
writable:如果为false,属性的值就不能被重写。
get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
set:一旦目标属性被赋值,就会调回此方法。
configurable: 如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化。
enumerable: 是否能在for…in循环中遍历出来或在Object.keys中列举出来
function text(){
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77;
// throws an error in strict mode
console.log(object1.property1);
// expected output: 42
}
function text(){
const object1 = {
_property1: 42
};
Object.defineProperty(object1, 'property1', {
get:function(){
return object1._property1
},
set:function(newValue){
this._property1 = newValue
console.log("set"+newValue)
}
});
object1.property1 = 77;
// throws an error in strict mode
console.log(object1.property1);
// expected output: 42
}
注意在你的对象中(第一个参数),不要存在与需要定义的属性和方法的名称(第二个参数)相同的名称,否则会引起栈溢出(当你在对象声明同名属性,已经调用getset,在Object.defineProperty重复声明getset会引起栈溢出)
发布-订阅模式
发布—订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
//发布者
var pub = {
publish: function(){
dep.notify();
}
}
//三个订阅者
var sub1 = { update: function(){console.log(1)} }
var sub2 = { update: function(){console.log(2)} }
var sub3 = { update: function(){console.log(3)} }
//主题对象
function Dep(){
this.subs = [sub1, sub2, sub3]
}
//主题对象的原型方法 让订阅者响应
Dep.prototype.notify = function(){
this.subs.forEach(function (sub) {
sub.update();
})
}
var dep = new Dep()
//发布者发布消息 主题对象执行notify 触发订阅者执行update
pub.publish()
原理
vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式的方式来实现
数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变
实现简单的双向数据绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webpack-template</title>
</head>
<body>
<div id="main-con">
<input type="text" id="a">
<span id="b"></span>
</div>
</body>
<script>
var obj = {}
var val = 'hello'
Object.defineProperty(obj,'val',{
get: function(){
return val
},
set: function(newValue){
val = newValue
document.getElementById('b').innerHTML = val
}
});
document.addEventListener('keyup',function(e){
obj.val = e.target.value
})
</script>
</html>
DocuemntFragment(碎片化文档)
当每个节点都插入到文档当中都会引发一次浏览器的回流
浏览器的回流与重绘
回流必将引起重绘,重绘不一定会引起回流
首先
1.浏览器使用流式布局模型 (Flow Based Layout)。
2.浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了Render Tree。
有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
3.由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。
回流 (Reflow)
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
重绘 (Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
避免频繁操作DOM,提高浏览器性能
创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
内容绑定
function compile(node, vm){
//正则 检测{{}}表达式 并且可通过()提取参数
var reg = /\{\{(.*)\}\}/
//判断元素节点
if (node.nodeType === 1) {
//返回该元素所有属性节点的一个实时集合 字符串形式的名/值对
var attr = node.attributes;
//遍历节点全部属性,看看那个属性绑定了data
for (let i = 0; i < attr.length; i++) {
//指定属性绑定data 解析为h5
if(attr[i].nodeName == 'v-model'){
//获得指定的变量名称
const name = attr[i].nodeValue;
//把data值赋给该node
node.value = vm.data[name]
//删除此属性
node.removeAttribute('v-model')
}
}
}
//判断文本节点
if(node.nodeType === 3){
//判断是否符合{{}}表达式
if(reg.test(node.nodeValue)){
//通过RegExp.$1取得变量或者表达式
var name = RegExp.$1
//剔除空格
name = name.trim()
//把data值赋给该node
node.nodeValue = vm.data[name]
}
}
}
//通过DocuemntFragment(碎片化文档)减少回流
function nodeToFragment(node,vm){
var fragment = document.createDocumentFragment();
var child ;
while (child = node.firstChild) {
compile(child,vm)
fragment.appendChild(child)
}
return fragment
}
//模拟vue
function Vue (options){
this.data = options.data
var id = options.el
var dom = nodeToFragment(document.getElementById(id),this)
document.getElementById(id).appendChild(dom)
}
//vue实例
var vm = new Vue({
el: 'app',
data: {
text: 'hello'
}
})
view => model
//监听函数
function defineReactive(obj, key, val){
Object.defineProperty(obj, key, {
set: function(newVal){
val = newVal
console.info('new',val)
},
get: function(){
return val
}
})
}
//观察者
function observe(obj,vm){
//实现对实例的每一个属性都进行监听
for(let key of Object.keys(obj)){
defineReactive(vm, key, obj[key]);
}
}
function compile(node, vm){
//正则 检测{{}}表达式 并且可通过()提取参数
var reg = /\{\{(.*)\}\}/
//判断元素节点
if (node.nodeType === 1) {
//返回该元素所有属性节点的一个实时集合 字符串形式的名/值对
var attr = node.attributes;
//遍历节点全部属性,看看那个属性绑定了data
for (let i = 0; i < attr.length; i++) {
//指定属性绑定data 解析为h5
if(attr[i].nodeName == 'v-model'){
//获得指定的变量名称
const name = attr[i].nodeValue;
// //把data值赋给该node
// node.value = vm.data[name]
node.addEventListener('input',function(e){
vm[name] = e.target.value
})
node.value = vm[name]
//删除此属性
node.removeAttribute('v-model')
}
}
}
//判断文本节点
if(node.nodeType === 3){
//判断是否符合{{}}表达式
if(reg.test(node.nodeValue)){
//通过RegExp.$1取得变量或者表达式
var name = RegExp.$1
//剔除空格
name = name.trim()
//把data值赋给该node
// node.nodeValue = vm.data[name]
node.value = vm[name]
}
}
}
//通过DocuemntFragment(碎片化文档)减少回流
function nodeToFragment(node,vm){
var fragment = document.createDocumentFragment();
var child ;
while (child = node.firstChild) {
compile(child,vm)
fragment.appendChild(child)
}
return fragment
}
//模拟vue
function Vue (options){
this.data = options.data
var data = this.data
observe(data, this)
var id = options.el
var dom = nodeToFragment(document.getElementById(id),this)
document.getElementById(id).appendChild(dom)
}
//vue实例
var vm = new Vue({
el: 'app',
data: {
text: 'hello'
}
})
model => view
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webpack-template</title>
</head>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
{{text}}
</div>
</body>
<script>
//模拟vue
function Vue (options){
this.data = options.data
var data = this.data
observe(data, this)
var id = options.el
var dom = nodeToFragment(document.getElementById(id),this)
document.getElementById(id).appendChild(dom)
}
//观察者
function observe(obj,vm){
//实现对实例的每一个属性都进行监听
for(let key of Object.keys(obj)){
defineReactive(vm, key, obj[key]);
}
}
//监听函数
function defineReactive(obj, key, val){
//为每一个属性生成一个主题对象
var dep = new Dep()
//对每个data数据进行监听
Object.defineProperty(obj, key, {
set: function(newVal){
val = newVal
console.info('new',val)
//当数据变化时 更新通知
dep.notify()
},
get: function(){
if(Dep.target){
dep.addSub(Dep.target)
}
return val
}
})
}
//dep构造函数 主题对象
function Dep(){
this.subs = []
}
//dep原型方法
Dep.prototype = {
addSub(sub){
this.subs.push(sub)
},
notify(){
this.subs.forEach(function(sub) {
sub.update();
})
}
}
//通过DocuemntFragment(碎片化文档)减少回流
function nodeToFragment(node,vm){
var fragment = document.createDocumentFragment();
var child ;
while (child = node.firstChild) {
compile(child,vm)
fragment.appendChild(child)
}
return fragment
}
//编译函数 解析为h5
function compile(node, vm){
//正则 检测{{}}表达式 并且可通过()提取参数
var reg = /\{\{(.*)\}\}/
//判断元素节点
if (node.nodeType === 1) {
//返回该元素所有属性节点的一个实时集合 字符串形式的名/值对
var attr = node.attributes;
//遍历节点全部属性,看看那个属性绑定了data
for (let i = 0; i < attr.length; i++) {
//指定属性绑定data 解析为h5
if(attr[i].nodeName == 'v-model'){
//获得指定的变量名称
const name = attr[i].nodeValue;
// //把data值赋给该node
// node.value = vm.data[name]
node.addEventListener('input',function(e){
vm[name] = e.target.value
})
node.value = vm[name]
//删除此属性
node.removeAttribute('v-model')
}
}
}
//判断文本节点
if(node.nodeType === 3){
//判断是否符合{{}}表达式
if(reg.test(node.nodeValue)){
//通过RegExp.$1取得变量或者表达式
var name = RegExp.$1
//剔除空格
name = name.trim()
//把data值赋给该node
// node.nodeValue = vm.data[name]
// node.value = vm[name]
//会为每个与数据绑定相关的节点生成一个订阅者 watcher,watcher 会将自己添加到相应属性的 dep 容器中
new Watcher(vm, node, name)
}
}
}
//Watcher构造函数 监听者
function Watcher(vm, node, name){
//将自己赋给了一个全局变量 Dep.target
Dep.target = this
//获得vue实例
this.vm = vm
//获得节点
this.node = node
//获得属性名
this.name = name
//实例化时执行update
this.update()
Dep.target = null
}
//Watcher原型方法
Watcher.prototype = {
update(){
//通过update 执行get
this.get()
//更改节点内容
this.node.nodeValue = this.value
},
//get 的方法读取了 vm 的访问器属性
//从而触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中
get(){
this.value = this.vm[this.name]
}
}
//vue实例
var vm = new Vue({
el: 'app',
data: {
text: 'hello'
}
})
</script>
</html>
这样我们基本实现了vue的双向绑定,光看有点难以理解,建议大家自己敲一遍。
源码借鉴于:https://www.jianshu.com/p/e7ebb1500613