Vue.js双向绑定的实现原理
解析 神奇的 Object.defineProperty这个方法了不起啊。。vue.js和avalon.js 都是通过它实现双向绑定的。。而且Object.observe也被草案发起人撤回了。。所以defineProperty更有必要了解一下了几行代码看他怎么用var a= {}Object.defineProperty(a,"b",{value:123})console.log(a.b);//123
很简单,,它接受三个参数,而且都是必填的。。传入参数
第一个参数:目标对象第二个参数:需要定义的属性或方法的名字。第三个参数:目标属性所拥有的特性。(descriptor)前两个参数不多说了,一看代码就懂,主要看第三个参数descriptor,看看有哪些取值descriptor
他又以下取值,我们简单认识一下,后面例子,挨个介绍,value:属性的值(不用多说了)writable:如果为false,属性的值就不能被重写,只能为只读了configurable:总开关,一旦为false,就不能再设置他的(value,writable,configurable)enumerable:是否能在for...in循环中遍历出来或在Object.keys中列举出来。get:一会细说set:一会细说descriptor默认值
我们再看看第一个例子var a= {}Object.defineProperty(a,"b",{value:123})console.log(a.b);//123
我们只设置了 value,别的并没有设置,但是 第一次的时候可以简单的理解为(暂时这样理解)它会默认帮我们把writable,configurable,enumerable。都设上值,而且值还都是false。。也就是说,上面代码和下面是等价的的( 仅限于第一次设置的时候)var a= {}
Object.defineProperty(a,"b",{value:123,writable:false,enumerable:false,configurable:false
})
console.log(a.b);//123
以上非常重要哦。。并且以上理解对set 和 get 不起作用哦
configurable
总开关,第一次设置 false 之后,,第二次什么设置也不行了,比如说var a= {}
Object.defineProperty(a,"b",{configurable:false
})
Object.defineProperty(a,"b",{configurable:true
})
//error: Uncaught TypeError: Cannot redefine property: b
就会报错了。。注意上面讲的默认值。。。如果第一次不设置它会怎样。。会帮你设置为false。。所以。。第二次。再设置他会怎样?。。对喽,,会报错writable
如果设置为fasle,就变成只读了。。var a = {}; Object.defineProperty(o, "b", { value : 123,writable : false });console.log(a.b); // 打印 37
a.b = 25; // 没有错误抛出(在严格模式下会抛出,即使之前已经有相同的值)
console.log(o.a); // 打印 37, 赋值不起作用。
enumerable
属性特性 enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。var a= {}
Object.defineProperty(a,"b",{value:3445,enumerable:true
})
console.log(Object.keys(a));// 打印["b"]
改为falsevar a= {}
Object.defineProperty(a,"b",{value:3445,enumerable:false //注意咯这里改了
})
console.log(Object.keys(a));// 打印[]
for...in 类似,,不赘述了set 和 get
在 descriptor 中不能 同时设置访问器 (get 和 set) 和 wriable 或 value,否则会错,就是说想用(get 和 set),就不能用(wriable 或 value中的任何一个)set 和 get ,他俩干啥用的的,var a= {}
Object.defineProperty(a,"b",{set:function(newValue){console.log("你要赋值给我,我的新值是"+newValue)},get:function(){console.log("你取我的值")return 2 //注意这里,我硬编码返回2}
})
a.b =1 //打印 你要赋值给我,我的新值是1
console.log(a.b) //打印 你取我的值//打印 2 注意这里,和我的硬编码相同的
简单来说,, 这个 “b” 赋值 或者 取值的时候会分别触发 set 和 get 对应的函数这就是实现 observe的关键啊。。下一篇,,我会分析vue的observe的实现源码,聊聊自己如何一步一步实现$watchvar a = {b: {c:1},d:1}
a.$watch("b.c",()=>console.log("哈哈哈"))
Object.keys(obj)返回参数obj可被枚举的属性:
示例一:function Pasta(grain, width, shape) {this.grain = grain;this.width = width;this.shape = shape;this.toString = function () {return (this.grain + ", " + this.width + ", " + this.shape);}}console.log(Object.keys(Pasta)); //console: []var spaghetti = new Pasta("wheat", 0.2, "circle");console.log(Object.keys(spaghetti)); //console: ["grain", "width", "shape", "toString"]示例二:var arr = ["a", "b", "c"];console.log(Object.keys(arr)); // console: ["0", "1", "2"]var obj = { 0 : "a", 1 : "b", 2 : "c"};console.log(Object.keys(obj)); // console: ["0", "1", "2"]var an_obj = { 100: "a", 2: "b", 7: "c"};console.log(Object.keys(an_obj)); // console: ["2", "7", "100"]var my_obj = Object.create({}, { getFoo : { value : function () { return this.foo } } });my_obj.foo = 1;console.log(Object.keys(my_obj)); // console: ["foo"]
Vue.js最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统。本文仅探究几乎所有Vue的开篇介绍都会提到的hello world双向绑定是怎样实现的。先讲涉及的知识点,再参考源码,用尽可能少的代码实现那个hello world开篇示例。
参考文章:https://segmentfault.com/a/1190000006599500
一、访问器属性
访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。
var obj = { };
// 为obj定义一个名为hello的访问器属性
Object.defineProperty(obj, "hello", {
get: function () {return sth},
set: function (val) {/* do sth */}
})
obj.hello // 可以像普通属性一样读取访问器属性
访问器属性的"值"比较特殊,读取或设置访问器属性的值,实际上是调用其内部特性:get和set函数。
obj.hello // 读取属性,就是调用get函数并返回get函数的返回值
obj.hello = "abc" // 为属性赋值,就是调用set函数,赋值其实是传参
get和set方法内部的this都指向obj,这意味着get和set函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略(也就是所谓的被"劫持"了)。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">var obj={}Object.defineProperty(obj,"sb",{get:function(val){console.log("get方法执行了"+val);},set:function(val){console.log("set方法执行了:"+val);}})console.log(obj.sb);obj.sb="这就是个傻B"</script></head><body></body>
</html>
二、极简双向绑定的实现
此例实现的效果是:随文本框输入文字的变化,span中会同步显示相同的文字内容;在js或控制台显式的修改obj.name的值,视图会相应更新。这样就实现了model =>view以及view => model的双向绑定,并且是响应式的。
以上就是Vue实现双向绑定的基本原理。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">window.onload = function() {window.obj = {}Object.defineProperty(obj, "sb", {get: function(val) {console.log("get方法执行了" + val);},set: function(val) {document.getElementById("sb").value=val;document.getElementById("test").innerText=val;}})document.getElementById("sb").oninput = function() {obj.sb=this.value;}}</script></head><body><input type="text" name="" id="sb" value="" /><span id="test"></span></body></html>
三、分解任务
上述示例仅仅是为了说明原理。我们最终要实现的是:
首先将该任务分成几个子任务:
1、输入框以及文本节点与data中的数据绑定
2、输入框内容变化时,data中的数据同步变化。即view => model的变化。
3、data中的数据变化时,文本节点的内容同步变化。即model => view的变化。
要实现任务一,需要对DOM进行编译,这里有一个知识点:DocumentFragment。
四、DocumentFragment
DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。
五、数据初始化绑定
以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。
六、响应式的数据绑定
再来看任务二的实现思路:当我们在输入框输入数据的时候,首先触发input事件(或者keyup、change事件),在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中的text劫持为vm的访问器属性,因此给vm.text赋值,就会触发set方法。在set方法中主要做两件事,第一是更新属性的值,第二留到任务三再说。
任务二也就完成了,text属性值会与输入框的内容同步变化:
七、订阅/发布模式(subscribe&publish)
text属性变化了,set方法触发了,但是文本节点的内容没有变化。如何让同样绑定到text的文本节点也同步变化呢?这里又有一个知识点:订阅发布模式。
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作
之前提到的,当set方法触发后做的第二件事就是作为发布者发出通知:“我是属性text,我变了”。文本节点则是作为订阅者,在收到消息后执行相应的更新操作。
八、双向绑定的实现
回顾一下,每当new一个Vue,主要做了两件事:第一个是监听数据:observe(data),第二个是编译HTML:nodeToFragement(id)。
在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。
在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
我们已经实现:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的set方法。
接下来我们要实现的是:发出通知dep.notify() => 触发订阅者的update方法 => 更新视图。
这里的关键逻辑是:如何将watcher添加到关联属性的dep中。
在编译HTML过程中,为每个与data关联的节点生成一个Watcher。Watcher函数中发生了什么呢?
首先,将自己赋给了一个全局变量Dep.target;
其次,执行了update方法,进而执行了get方法,get的方法读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法中将该watcher添加到了对应访问器属性的dep中;
再次,获取属性的值,然后更新视图。
最后,将Dep.target设为空。因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。
至此,hello world双向绑定就基本实现了。文本内容会随输入框内容同步变化,在控制器中修改vm.text的值,会同步反映到文本内容中。
完整代码:https://github.com/bison1994/two-way-data-binding
JavaScript 模板引擎实现原理解析
1、入门实例
首先我们来看一个简单模板:
<script type="template" id="template"><h2><a href="{{href}}">{{title}}</a></h2><img src="{{imgSrc}}" alt="{{title}}"></script>
其中被{{ xxx }}包含的就是我们要替换的变量。
接着我们可能通过ajax或者其他方法获得数据。这里我们自己定义了数据,具体如下:
var data = [{title: "Create a Sticky Note Effect in 5 Easy Steps with CSS3 and HTML5",href: "http://net.tutsplus.com/tutorials/html-css-techniques/create-a-sticky-note-effect-in-5-easy-steps-with-css3-and-html5/",imgSrc: "https://d2o0t5hpnwv4c1.cloudfront.net/771_sticky/sticky_notes.jpg"},{title: "Nettuts+ Quiz #8",href: "http://net.tutsplus.com/articles/quizzes/nettuts-quiz-8-abbreviations-darth-sidious-edition/",imgSrc: "https://d2o0t5hpnwv4c1.cloudfront.net/989_quiz2jquerybasics/quiz.jpg"}];
ok,现在的问题就是我们怎么把数据导入到模板里面呢?
第一种大家会想到的就是采用replace直接替换里面的变量:
template = document.querySelector('#template').innerHTML, result = document.querySelector('.result'), i = 0, len = data.length, fragment = '';for ( ; i < len; i++ ) {fragment += template.replace( /\{\{title\}\}/, data[i].title ).replace( /\{\{href\}\}/, data[i].href ).replace( /\{\{imgSrc\}\}/, data[i].imgSrc ); }result.innerHTML = fragment;
第二种的话,相对第一种比较灵活,采用的是正则替换,对于初级前端,很多人对正则掌握的并不是很好,一般也用的比较少。具体实现如下:
template = document.querySelector('#template').innerHTML, result = document.querySelector('.result'), attachTemplateToData;// 将模板和数据作为参数,通过数据里所有的项将值替换到模板的标签上(注意不是遍历模板标签,因为标签可能不在数据里存在)。 attachTemplateToData = function(template, data) {var i = 0,len = data.length,fragment = '';// 遍历数据集合里的每一个项,做相应的替换function replace(obj) {var t, key, reg;//遍历该数据项下所有的属性,将该属性作为key值来查找标签,然后替换for (key in obj) {reg = new RegExp('{{' + key + '}}', 'ig');t = (t || template).replace(reg, obj[key]);}return t;}for (; i < len; i++) {fragment += replace(data[i]);}return fragment;};result.innerHTML = attachTemplateToData(template, data);
与第一种相比较,第二种代码看上去多了,但是功能实则更为强大了。第一种我们需要每次重新编写变量名,如果变量名比较多的话,会比较麻烦,且容易出错。第二种的就没有这些烦恼。
2、模板引擎相关知识
通过上面的例子,大家对模板引擎应该有个初步的认识了,下面我们来讲解一些相关知识。
2.1 模板存放
模板一般都是放置到 textarea/input 等表单控件,或者 script 等标签中。比如上面的例子,我们就是放在 script 标签上的。
2.2 模板获取
一般都是通过ID来获取,document.getElementById(“ID”):
//textarea或input则取value,其它情况取innerHTML var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;
上面的是通用的模板获取方法,这样不管你是放在 textarea/input 还是 script 标签下都可以获取到。
2.3 模板函数
一般都是templateFun("id", data);其中id为存放模板字符串的元素id,data为需要装载的数据。
2.4 模板解析编译
模板解析主要是指将模板中 JavaScript 语句和 html 分离出来,编译的话将模板字符串编译成最终的模板。上面的例子比较简单,还没有涉及到模板引擎的核心。
2.5 模板解析编译
要指出的是,不同的模板引擎所用的分隔符可能是不一样,上面的例子用的是{{ }},而Jquery tmpl 使用的是<% %>。
3、jQuery tmpl 实现原理解析
jQuery tmpl是由jQuery的作者写的,代码短小精悍。总共20多行,功能却比我们上面的强大很多。我们先来看一看源码:
(function(){var cache = {};this.tmpl = function tmpl(str, data){var fn = !/\W/.test(str) ? cache[str] = cache[str] ||tmpl(document.getElementById(str).innerHTML) :new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};" +"with(obj){p.push('" +str.replace(/[\r\t\n]/g, " ") .split("<%").join("\t") .replace(/((^|%>)[^\t]*)'/g, "$1\r").replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'")+ "');}return p.join('');");return data ? fn( data ) : fn;}; })();
初看是不是觉得有点懵,完全不能理解的代码。没事,后面我们会对源码进行解释的,我们还是先看一下所用的模板
<ul><% for ( var i = 0; i < users.length; i++ ) { %><li><a href="<%=users[i].url%>"><%=users[i].name%></a></li><% } %></ul>
可以发现,这个模板比入门例子的模板更为复杂,因为里面还夹杂着 JavaScript 代码。JavaScript 代码采用 <% %> 包含。而要替换的变量则是用 <%= %> 分隔开的。
下面我再来对代码做个注释。不过即使看了注释,你也不一定能很快理解,最好的办法是自己实际动手操作一遍。
// 代码整个放在一个立即执行函数里面
(function(){// 用来缓存,有时候一个模板要用多次,这时候,我们直接用缓存就会很方便
var cache = {};
// tmpl绑定在this上,这里的this值得是windowthis.tmpl = function tmpl(str, data){
// 只有模板才有非字母数字字符,用来判断传入的是模板id还是模板字符串,
// 如果是id的话,判断是否有缓存,没有缓存的话调用tmpl;
// 如果是模板的话,就调用new Function()解析编译var fn = !/\W/.test(str) ? cache[str] = cache[str] ||tmpl(document.getElementById(str).innerHTML) :new Function("obj",
// 注意这里整个是字符串,通过 + 号拼接"var p=[],print=function(){p.push.apply(p,arguments);};" +"with(obj){p.push('" +str
// 去除换行制表符\t\n\r.replace(/[\r\t\n]/g, " ")
// 将左分隔符变成 \t.split("<%").join("\t")
// 去掉模板中单引号的干扰.replace(/((^|%>)[^\t]*)'/g, "$1\r")
// 为 html 中的变量变成 ",xxx," 的形式, 如:\t=users[i].url%> 变成 ',users[i].url,'
// 注意这里只有一个单引号,还不配对 .replace(/\t=(.*?)%>/g, "',$1,'")
// 这时候,只有JavaScript 语句前面才有 "\t", 将 \t 变成 ');
// 这样就可把 html 标签添加到数组p中,而javascript 语句 不需要 push 到里面。
.split("\t").join("');")
// 这时候,只有JavaScript 语句后面才有 "%>", 将 %> 变成 p.push('
// 上一步我们再 html 标签后加了 ');, 所以要把 p.push(' 语句放在 html 标签放在前面,这样就可以变成 JavaScript 语句.split("%>").join("p.push('")
// 将上面可能出现的干扰的单引号进行转义
.split("\r").join("\\'")
// 将数组 p 变成字符串。+ "');}return p.join('');");return data ? fn( data ) : fn;}; })();
上面代码中,有一个要指出的就是new Function 的使用 方法。给 new Function() 传一个字符串作为函数的body来构造一个 JavaScript函数。编程中并不经常用到,但有时候应该是很有用的。
下面是 new Function 的基本用法:
// 最后一个参数是函数的 body(函数体),类型为 string; // 前面的参数都是 索要构造的函数的参数(名字) var myFunction = new Function('users', 'salary', 'return users * salary');
最后的字符串就是下面这种形式:
var p = [],print = function() {p.push.apply(p, arguments);};with(obj) {p.push(' <ul> ');for (var i = 0; i < users.length; i++) {p.push(' <li><a href="', users[i].url, '">', users[i].name, '</a></li> ');}p.push(' </ul> ');}return p.join('');
里面的 print 函数 在我们的模板里面是没有用到的。
要指出的是,采用 push 的方法在 IE6-8 的浏览器下会比 += 的形式快,但是在现在的浏览器里面, += 是拼接字符串最快的方法。实测表明现代浏览器使用 += 会比数组 push 方法快,而在 v8 引擎中,使用 += 方式比数组拼接快 4.7 倍。所以 目前有些更高级的模板引擎会 根据 javascript 引擎特性采用了两种不同的字符串拼接方式。
下面的代码是摘自腾讯的 artTemplate 的, 根据浏览器的类型来选择不同的拼接方式。功能越强大,所考虑的问题也会更多。
var isNewEngine = ''.trim;// '__proto__' in {}var replaces = isNewEngine? ["$out='';", "$out+=", ";", "$out"]: ["$out=[];", "$out.push(", ");", "$out.join('')"];
挑战:有兴趣的可以改用 += 来实现上面的代码。
总结
模板引擎原理总结起来就是:先获取html中对应的id下得innerHTML,利用开始标签和关闭标签进行字符串切分,其实是将模板划分成两部份内容,一部分是html部分,一部分是逻辑部分,通过区别一些特殊符号比如each、if等来将字符串拼接成函数式的字符串,将两部分各自经过处理后,再次拼接到一起,最后将拼接好的字符串采用new Function()的方式转化成所需要的函数。
目前模板引擎的种类繁多,功能也越来越强大,不同模板间实现原理大同小异,各有优缺,请按需选择。
vue双向数据绑定原理探究(附demo)
传送门
双向绑定的思想
双向数据绑定的思想就是数据层与UI层的同步,数据再两者之间的任一者发生变化时都会同步更新到另一者。
双向绑定的一些方法
目前,前端实现数据双向数据绑定的方法大致有以下三种:
1.发布者-订阅者模式(backbone.js)
思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。
2.赃值检测(angular.js)
思路:通过轮询的方式检测数据变动。才特定的事件触发时进入赃值检测。
大致如下:
• DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
• XHR响应事件 ( $http )
• 浏览器Location变更事件 ( $location )
• Timer事件( $timeout , $interval )
• 执行 $digest() 或 $apply()
3.数据劫持(vue.js)
思路:使用Object.defineProperty对数据对象做属性get和set的监听,当有数据读取和赋值操作时则调用节点的指令,这样使用最通用的=等号赋值就可以触发了。
wue双向数据绑定小demo思路
① 构造一个Wue对象,定义该对象的属性el、data,创建对象的时候传相应数据,并执行init()方法。
1 2 3 4 5 | var Wue= function (params){ this .el=document.querySelector(params.el); this .data=params.data; this .init(); }; |
② Init方法中执行bindText和bindModel方法,这两个方法分别是解析dom中绑定了w-model、w-text指令的html,并作相应处理。
1 2 3 4 | init: function (){ this .bindText(); this .bindModel(); } |
③ bindText方法,把带有w-text指令的元素放进一个数组中,如:w-text=’demo’,然后令其innerHTML的值等于传进来的data[demo]。
1 2 3 4 5 6 7 8 9 | bindText: function (){ var textDOMs= this .el.querySelectorAll( '[w-text]' ), bindText; for ( var i=0;i<textDOMs.length;i++){ bindText=textDOMs[i].getAttribute( 'w-text' ); textDOMs[i].innerHTML= this .data[bindText]; } } |
④ bindModel方法,把带有w-model指令的元素(一般为form相关元素)放进一个数组中,如:w-model=’demo’,为每一个元素绑定keyup事件(兼容浏览器写法)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | bindModel: function (){ var modelDOMs= this .el.querySelectorAll( '[w-model]' ), bindModel; var _that= this ; for ( var i=0;i<modelDOMs.length;i++){ bindModel=modelDOMs[i].getAttribute( 'w-model' ); modelDOMs[i].value= this .data[bindModel]|| '' ; //数据劫持 this .defineObj( this .data,bindModel); if (document.addEventListener){ modelDOMs[i].addEventListener( 'keyup' , function (event) { console.log( 'test' ); e=event||window.event; _that.data[bindModel]=e.target.value; }, false ); } else { modelDOMs[i].attachEvent( 'onkeyup' , function (event){ e=event||window.event; _that.data[bindModel]=e.target.value; }, false ); } } } |
⑤ 使用Object.defineProperty,定义set和get方法,并在set方法中调用bindText方法。这是利用了一旦w-model的值在input中被改变,会自动执行set方法,所以只有在这个方法中调用更新w-text的方法即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | defineObj: function (obj,prop,value){ var val=value|| '' ; var _that= this ; try { Object.defineProperty(obj,prop,{ get: function (){ return val; }, set: function (newVal){ val=newVal; _that.bindText(); } }) } catch (err){ console.log( 'Browser not support!' ) } } |
⑥使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | html:<br><h3>双向数据绑定demo</h3> <div id= "wrap" > <input type= "text" w-model= 'demo' > <h5 w-text= 'demo' ></h5> </div><br>js: <script src= '../js/wue.js' ></script> <script> new Wue({ el: '#wrap' , data:{ demo: 'winty' } }) </script> |
完整demo戳这里!