In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-16 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/02 Report--
This article introduces the knowledge of "how to understand two-way binding of JavaScript data". In the operation of practical cases, many people will encounter such a dilemma, so let the editor lead you to learn how to deal with these situations. I hope you can read it carefully and be able to achieve something!
Implementation principle of template engine
For page rendering, it is generally divided into server-side rendering and browser-side rendering. Generally speaking, server-side html page rendering is faster and more conducive to SEO, but browser-side rendering is more conducive to improve development efficiency and reduce maintenance costs. It is a related and comfortable front-end cooperation mode. The back-end provides interfaces, and the front-end does view and interaction logic. The front end requests data through Ajax and then splices html strings or uses js template engine or data-driven framework such as Vue for page rendering.
Before the advent of frameworks like ES6 and Vue, the way the front end bound data was to dynamically concatenate html strings and js template engines. The template engine separates the data from the view, and the template corresponds to the view, focusing on how to display the data, the data prepared outside the template, and which data can be displayed. The working principle of template engine can be simply divided into two steps: template parsing / compiling (Parse / Compile) and data rendering (Render). Today, there are three ways of front-end templates:
String-based templating (string-based parse and compile procedures)
Dom-based templating (Dom-based link or compile process)
Living templating (string-based parse and dom-based compile procedures)
String-based templating
String-based template engine, in essence, is still the form of string splicing, but the general library has been encapsulated and optimized, providing more convenient syntax to simplify our work. The basic principles are as follows:
Typical libraries:
Art-template
Mustache.js
DoT
In a previous article, I introduced the implementation of js template engine. Interested friends can take a look at this: JavaScript Advanced Learning (1)-- A simple js template engine implementation based on regular expressions. In this article, we use regular expressions to implement a simple js template engine, which uses regular matching to find out the content between {{}} in the template, and then replace it with the data in the model, so as to achieve view rendering.
Var template = function (tpl, data) {var re = / {{(. +?)} / g, cursor = 0, reExp = / (^ ()? (var | if | for | else | case | {|} |;)) (. *) / g, code = 'var r = [];\ n' / / parse the escape of html function parsehtml (line) {/ / single and double quotation marks, replace the newline character with a space, and remove the space line = line.replace (/ ('| ") / g,'\\ $1'). Replace (/\ ntag g,'). Replace (/ (^\ s +) | (\ s + $) / g,"); code + = 'r.push ("' + line +'");\ n' } / / parse the js code function parsejs (line) {/ / remove the space line = line.replace (/ (^\ s +) | (\ s +) / g, "); code + = line.match (reExp)? Line +'\ n': 'r.push (' + 'this.' + line +');\ ncompiler;} / compile template while ((match = re.exec (tpl))! = = null) {/ / start tag {{content before and content after closing tag}} parsehtml (tpl.slice (cursor, match.index)) / / parsejs (match [1]) between the start tag {{and the end tag}}; / / move the pointer cursor = match.index + match [0] .length after each match;} / * the content parsehtml (tpl.substr (cursor, tpl.length-cursor)); code + = 'return r.join (");' Return new Function (code.replace (/ [\ r\ t\ n] / g,') .apply (data);}
Source code: http://jsfiddle.net/zhaomenghuan/bw468orv/embedded/
Now that ES6 supports template strings, we can implement similar functions with relatively simple code:
Const template = data = > `
Name: ${data.name}
Age: ${data.profile.age}
${data.skills.map (skill = > `$ {skill} `) .join ('')} `const data = {name: 'zhaomenghuan', profile: {age: 24}, skills: [' html5', 'javascript',' android']} document.body [XSS _ clean] = template (data)
Dom-based templating
Dom-based templating is from the DOM point of view to achieve data rendering, we traverse the DOM tree, extract attributes and DOM content, and then write the data to the DOM tree, so as to achieve page rendering. A simple example is as follows:
Function MVVM (opt) {this.dom = document.querySelector (opt.el); this.data = opt.data | | {}; this.renderDom (this.dom);} MVVM.prototype = {init: {sTag:'{{', eTag:'}}'}, render: function (node) {var self = this; var sTag = self.init.sTag; var eTag = self.init.eTag Var matchs = node.textContent.split (sTag); if (matchs.length) {var ret =''; for (var I = 0; I
< matchs.length; i++) { var match = matchs[i].split(eTag); if (match.length == 1) { ret += matchs[i]; } else { ret = self.data[match[0]]; } node.textContent = ret; } } }, renderDom: function(dom) { var self = this; var attrs = dom.attributes; var nodes = dom.childNodes; Array.prototype.forEach.call(attrs, function(item) { self.render(item); }); Array.prototype.forEach.call(nodes, function(item) { if (item.nodeType === 1) { return self.renderDom(item); } self.render(item); }); } } var app = new MVVM({ el: '#app', data: { name: 'zhaomenghuan', age: '24', color: 'red' } }); 源代码:http://jsfiddle.net/zhaomenghuan/6e3yg6Lq/embedded/ 页面渲染的函数 renderDom 是直接遍历DOM树,而不是遍历html字符串。遍历DOM树节点属性(attributes)和子节点(childNodes),然后调用渲染函数render。当DOM树子节点的类型是元素时,递归调用遍历DOM树的方法。根据DOM树节点类型一直遍历子节点,直到文本节点。 render的函数作用是提取{{}}中的关键词,然后使用数据模型中的数据进行替换。我们通过textContent获取Node节点的nodeValue,然后使用字符串的split方法对nodeValue进行分割,提取{{}}中的关键词然后替换为数据模型中的值。 DOM 的相关基础 注:元素类型对应NodeType 元素类型NodeType元素1属性2文本3注释8文档9 childNodes 属性返回包含被选节点的子节点的 NodeList。childNodes包含的不仅仅只有html节点,所有属性,文本、注释等节点都包含在childNodes里面。children只返回元素如input, span, script, div等,不会返回TextNode,注释。 数据双向绑定实现原理 js模板引擎可以认为是一个基于MVC的结构,我们通过建立模板作为视图,然后通过引擎函数作为控制器实现数据和视图的绑定,从而实现实现数据在页面渲染,但是当数据模型发生变化时,视图不能自动更新;当视图数据发生变化时,模型数据不能实现更新,这个时候双向数据绑定应运而生。检测视图数据更新实现数据绑定的方法有很多种,目前主要分为三个流派,Angular使用的是脏检查,只在特定的事件下才会触发视图刷新,Vue使用的是Getter/Setter机制,而React则是通过 Virtual DOM 算法检查DOM的变动的刷新机制。 本文限于篇幅和内容在此只探讨一下 Vue.js 数据绑定的实现,对于 angular 和 react 后续再做说明,读者也可以自行阅读源码。Vue 监听数据变化的机制是把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Vue 2.x 对 Virtual DOM 进行了支持,这部分内容后续我们再做探讨。 引子 为了更好的理解Vue中视图和数据更新的机制,我们先看一个简单的例子: var o = { a: 0 } Object.defineProperty(o, "b", { get: function () { return this.a + 1; }, set: function (value) { this.a = value / 2; } }); console.log(o.a); // "0" console.log(o.b); // "1" // 更新o.a o.a = 5; console.log(o.a); // "5" console.log(o.b); // "6" // 更新o.b o.b = 10; console.log(o.a); // "5" console.log(o.b); // "6" 这里我们可以看出对象o的b属性的值依赖于a属性的值,同时b属性值的变化又可以改变a属性的值,这个过程相关的属性值的变化都会影响其他相关的值进行更新。反过来我们看看如果不使用Object.defineProperty()方法,上述的问题通过直接给对象属性赋值的方法实现,代码如下 var o = { a: 0 } o.b = o.a + 1; console.log(o.a); // "0" console.log(o.b); // "1" // 更新o.a o.a = 5; o.b = o.a + 1; console.log(o.a); // "5" console.log(o.b); // "6" // 更新o.b o.b = 10; o.a = o.b / 2; o.b = o.a + 1; console.log(o.a); // "5" console.log(o.b); // "6" 很显然使用Object.defineProperty()方法可以更方便的监听一个对象的变化。当我们的视图和数据任何一方发生变化的时候,我们希望能够通知对方也更新,这就是所谓的数据双向绑定。既然明白这个道理我们就可以看看Vue源码中相关的处理细节。 Object.defineProperty() Object.defineProperty()方法可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。 语法:Object.defineProperty(obj, prop, descriptor) 参数: obj:需要定义属性的对象。 prop:需被定义或修改的属性名。 descriptor:需被定义或修改的属性的描述符。 返回值:返回传入函数的对象,即***个参数obj 该方法重点是描述,对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个拥有可写或不可写值的属性。存取描述符是由一对 getter-setter 函数功能来描述的属性。描述符必须是两种形式之一;不能同时是两者。 数据描述符和存取描述符均具有以下可选键值: configurable:当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false。 enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 数据描述符同时具有以下可选键值: value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。 writable:当且仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false。 存取描述符同时具有以下可选键值: get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为undefined。 set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。 我们可以通过Object.defineProperty()方法精确添加或修改对象的属性。比如,直接赋值创建的属性默认情况是可以枚举的,但是我们可以通过Object.defineProperty()方法设置enumerable属性为false为不可枚举。 var obj = { a: 0, b: 1 } for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 结果: "obj.a = 0" "obj.b = 1" 我们通过Object.defineProperty()修改如下: var obj = { a: 0, b: 1 } Object.defineProperty(obj, 'b', { enumerable: false }) for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 结果: "obj.a = 0" 这里需要说明的是我们使用Object.defineProperty()默认情况下是enumerable属性为false,例如: var obj = { a: 0 } Object.defineProperty(obj, 'b', { value: 1 }) for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 结果: "obj.a = 0" 其他描述属性使用方法类似,不做赘述。Vue源码core/util/lang.jsS中定义了这样一个方法: /** * Define a property. */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } Object.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性) 语法:Object.getOwnPropertyDescriptor(obj, prop) 参数: obj:在该对象上查看属性 prop:一个属性名称,该属性的属性描述符将被返回 返回值:如果指定的属性存在于对象上,则返回其属性描述符(property descriptor),否则返回 undefined。可以访问"属性描述符"内容,例如前面的例子: var o = { a: 0 } Object.defineProperty(o, "b", { get: function () { return this.a + 1; }, set: function (value) { this.a = value / 2; } }); var des = Object.getOwnPropertyDescriptor(o,'b'); console.log(des); console.log(des.get); Vue源码分析 本次我们主要分析一下Vue 数据绑定的源码,这里我直接将 Vue.js 1.0.28 版本的代码稍作删减拿过来进行,2.x 的代码基于 flow 静态类型检查器书写的,代码除了编码风格在整体结构上基本没有太大改动,所以依然基于 1.x 进行分析,对于存在差异的部分加以说明。 监听对象变动 // 观察者构造函数 function Observer (value) { this.value = value this.walk(value) } // 递归调用,为对象绑定getter/setter Observer.prototype.walk = function (obj) { var keys = Object.keys(obj) for (var i = 0, l = keys.length; i < l; i++) { this.convert(keys[i], obj[keys[i]]) } } // 将属性转换为getter/setter Observer.prototype.convert = function (key, val) { defineReactive(this.value, key, val) } // 创建数据观察者实例 function observe (value) { // 当值不存在或者不是对象类型时,不需要继续深入监听 if (!value || typeof value !== 'object') { return } return new Observer(value) } // 定义对象属性的getter/setter function defineReactive (obj, key, val) { var property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 保存对象属性预先定义的getter/setter var getter = property && property.get var setter = property && property.set var childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val console.log("访问:"+key) return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val if (newVal === value) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } // 对新值进行监听 childOb = observe(newVal) console.log('更新:' + key + ' = ' + newVal) } }) } 定义一个对象作为数据模型,并监听这个对象。 let data = { user: { name: 'zhaomenghuan', age: '24' }, address: { city: 'beijing' } } observe(data) console.log(data.user.name) // 访问:user // 访问:name data.user.name = 'ZHAO MENGHUAN' // 访问:user // 更新:name = ZHAO MENGHUAN 效果如下:Listen for array changes
Above, we convert all the properties of the object to getter/setter through Object.defineProperty to change the listening object, but it is impossible to listen to array objects through Object.defineProperty. Vue contains a set of ways to observe the variation of the array, so they will also trigger view updates.
Const arrayProto = Array.prototype const arrayMethods = Object.create (arrayProto) function def (obj, key, val, enumerable) {Object.defineProperty (obj, key, {value: val, enumerable:! enumerable, writable: true, configurable: true})} / array ['push',' pop', 'shift',' unshift', 'splice',' sort', 'reverse'] .forEach (function (method) {/ / cache array primitive method var original = arrayProto [method] def (arrayMethods, method) Function mutator () {var I = arguments.length var args = new Array (I) while (iMury -) {args [I] = arguments [I]} console.log ('array change') return original.apply (this, args)})})
Vue.js 1.x added $set and $remove methods to the Array.prototype prototype object, removed them after 2.x, and replaced them with global API Vue.set and Vue.delete, which we'll examine later.
Define an array as the data model and call seven mutated methods to listen on the array.
Let skills = ['JavaScript',' Node.js', 'html5'] / / prototype pointer points to array objects with mutated methods skills.__proto__ = arrayMethods skills.push (' java') / / array changes skills.pop () / / array changes
The effect is as follows:
We point the prototype pointer of the array we need to listen to to the array object we define, so that our array can listen for changes and track the array when calling the variation methods of the above seven arrays.
As for the _ _ proto__ attribute, it is officially added to the specification in ES2015. The standard clearly stipulates that only browsers must deploy this attribute, and other running environments do not necessarily need to deploy it, so Vue first determines that when the _ _ proto__ attribute exists, it points the prototype pointer _ _ proto__ to the array object with mutation method, and when it does not exist, it directly hangs the mutation method on the object to be tracked.
We can add listening to the array in the above Observer observer constructor. The source code is as follows:
Const hasProto ='_ proto__' in {} const arrayKeys = Object.getOwnPropertyNames (arrayMethods) / / Observer constructor function Observer (value) {this.value = value if (Array.isArray (value)) {var augment = hasProto? ProtoAugment: copyAugment augment (value, arrayMethods, arrayKeys) this.observeArray (value)} else {this.walk (value)}} / / observe each item in the array Observer.prototype.observeArray = function (items) {for (var I = 0, l = items.length; I)
< l; i++) { observe(items[i]) } } // 将目标对象/数组的原型指针__proto__指向src function protoAugment (target, src) { target.__proto__ = src } // 将具有变异方法挂在需要追踪的对象上 function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] def(target, key, src[key]) } } 原型链 对于不了解原型链的朋友可以看一下我这里画的一个基本关系图: 原型对象是构造函数的prototype属性,是所有实例化对象共享属性和方法的原型对象; 实例化对象通过new构造函数得到,都继承了原型对象的属性和方法; 原型对象中有个隐式的constructor,指向了构造函数本身。 Object.create Object.create 使用指定的原型对象和其属性创建了一个新的对象。 const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) 这一步是通过 Object.create 创建了一个原型对象为Array.prototype的空对象。然后通过Object.defineProperty方法对这个对象定义几个变异的数组方法。有些新手可能会直接修改 Array.prototype 上的方法,这是很危险的行为,这样在引入的时候会全局影响Array 对象的方法,而使用Object.create实质上是完全了一份拷贝,新生成的arrayMethods对象的原型指针__proto__指向了Array.prototype,修改arrayMethods 对象不会影响Array.prototype。 基于这种原理,我们通常会使用Object.create 实现类式继承。 // 实现继承 var extend = function(Child, Parent) { // 拷贝Parent原型对象 Child.prototype = Object.create(Parent.prototype); // 将Child构造函数赋值给Child的原型对象 Child.prototype.constructor = Child; } // 实例 var Parent = function () { this.name = 'Parent'; } Parent.prototype.getName = function () { return this.name; } var Child = function () { this.name = 'Child'; } extend(Child, Parent); var child = new Child(); console.log(child.getName()) 发布-订阅模式 在上面一部分我们通过Object.defineProperty把对象的属性全部转为 getter/setter 以及 数组变异方法实现了对数据模型变动的监听,在数据变动的时候,我们通过console.log打印出来提示了,但是对于框架而言,我们相关的逻辑如果直接写在那些地方,自然是不够优雅和灵活的,这个时候就需要引入常用的设计模式去实现,vue.js采用了发布-订阅模式。发布-订阅模式主要是为了达到一种"高内聚、低耦合"的效果。 Vue的Watcher订阅者作为Observer和Compile之间通信的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。 /** * 观察者对象 */ function Watcher(vm, expOrFn, cb) { this.vm = vm this.cb = cb this.depIds = {} if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = this.parse_Expression(expOrFn) } this.value = this.get() } /** * 收集依赖 */ Watcher.prototype.get = function () { // 当前订阅者(Watcher)读取被订阅数据的***更新后的值时,通知订阅者管理员收集当前订阅者 Dep.target = this // 触发getter,将自身添加到dep中 const value = this.getter.call(this.vm, this.vm) // 依赖收集完成,置空,用于下一个Watcher使用 Dep.target = null return value } Watcher.prototype.addDep = function (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this) this.depIds[dep.id] = dep } } /** * 依赖变动更新 * * @param {Boolean} shallow */ Watcher.prototype.update = function () { this.run() } Watcher.prototype.run = function () { var value = this.get() if (value !== this.value) { var oldValue = this.value this.value = value // 将newVal, oldVal挂载到MVVM实例上 this.cb.call(this.vm, value, oldValue) } } Watcher.prototype.parseExpression = function (exp) { if (/[^\w.$]/.test(exp)) { return } var exps = exp.split('.') return function(obj) { for (var i = 0, len = exps.length; i < len; i++) { if (!obj) return obj = obj[exps[i]] } return obj } } Dep 是一个数据结构,其本质是维护了一个watcher队列,负责添加watcher,更新watcher,移除watcher,通知watcher更新。 let uid = 0 function Dep() { this.id = uid++ this.subs = [] } Dep.target = null /** * 添加一个订阅者 * * @param {Directive} sub */ Dep.prototype.addSub = function (sub) { this.subs.push(sub) } /** * 移除一个订阅者 * * @param {Directive} sub */ Dep.prototype.removeSub = function (sub) { let index = this.subs.indexOf(sub); if (index !== -1) { this.subs.splice(index, 1); } } /** * 将自身作为依赖添加到目标watcher */ Dep.prototype.depend = function () { Dep.target.addDep(this) } /** * 通知数据变更 */ Dep.prototype.notify = function () { var subs = toArray(this.subs) // stablize the subscriber list first for (var i = 0, l = subs.length; i < l; i++) { // 执行订阅者的update更新函数 subs[i].update() } } 模板编译 compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。 这种实现和我们讲到的Dom-based templating类似,只是更加完备,具有自定义指令的功能。在遍历节点属性和文本节点的时候,可以编译具备{{}}表达式或v-xxx的属性值的节点,并且通过添加 new Watcher()及绑定事件函数,监听数据的变动从而对视图实现双向绑定。 MVVM实例 在数据绑定初始化的时候,我们需要通过new Observer()来监听数据模型变化,通过new Compile()来解析编译模板指令,并利用Watcher搭起Observer和Compile之间的通信桥梁。 /** * @class 双向绑定类 MVVM * @param {[type]} options [description] */ function MVVM(options) { this.$options = options || {} // 简化了对data的处理 let data = this._data = this.$options.data // 监听数据 observe(data) new Compile(options.el || document.body, this) } MVVM.prototype.$watch = function (expOrFn, cb) { new Watcher(this, expOrFn, cb) } 为了能够直接通过实例化对象操作数据模型,我们需要为MVVM实例添加一个数据模型代理的方法: MVVM.prototype._proxy = function (key) { Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () =>This._data [key], set: (val) = > {this._ data [key] = val}})}
So far, we can use a small example to illustrate the content of this article:
{{user.name}}
{{modelValue}}
Let vm = new MVVM ({el:'# app', data: {modelValue:', user: {name: 'zhaomenghuan', age:' 24'}, address: {city: 'beijing'} Skills: ['JavaScript',' Node.js', 'html5']}) vm.$watch (' modelValue', val = > console.log (`watch modelValue: ${val} `)) "how to understand two-way binding of JavaScript data" ends here Thank you for your reading. If you want to know more about the industry, you can follow the website, the editor will output more high-quality practical articles for you!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.