前言

谈起当前前端最热门的 js 框架,必少不了 Vue、React、Angular,对于大多数人来说,我们更多的是在使用框架,对于框架解决痛点背后使用的基本原理往往关注不多,近期在研读 Vue.js 源码,也在写源码解读的系列文章。和多数源码解读的文章不同的是,我会尝试从一个初级前端的角度入手,由浅入深去讲解源码实现思路和基本的语法知识,通过一些基础事例一步步去实现一些小功能。

本场 Chat 是系列 Chat 的开篇,我会首先讲解一下数据双向绑定的基本原理,介绍对比一下三大框架的不同实现方式,同时会一步步完成一个简单的mvvm示例。读源码不是目的,只是一种学习的方式,目的是在读源码的过程中提升自己,学习基本原理,拓展编码的思维方式。

模板引擎实现原理

对于页面渲染,一般分为服务器端渲染和浏览器端渲染。一般来说服务器端吐html页面的方式渲染速度更快、更利于SEO,但是浏览器端渲染更利于提高开发效率和减少维护成本,是一种相关舒服的前后端协作模式,后端提供接口,前端做视图和交互逻辑。前端通过Ajax请求数据然后拼接html字符串或者使用js模板引擎、数据驱动的框架如Vue进行页面渲染。

在ES6和Vue这类框架出现以前,前端绑定数据的方式是动态拼接html字符串和js模板引擎。模板引擎起到数据和视图分离的作用,模板对应视图,关注如何展示数据,在模板外头准备的数据, 关注那些数据可以被展示。模板引擎的工作原理可以简单地分成两个步骤:模板解析 / 编译(Parse / Compile)和数据渲染(Render)两部分组成,当今主流的前端模板有三种方式:

  • String-based templating (基于字符串的parse和compile过程)
  • Dom-based templating (基于Dom的link或compile过程)
  • Living templating (基于字符串的parse 和 基于dom的compile过程)

String-based templating

基于字符串的模板引擎,本质上依然是字符串拼接的形式,只是一般的库做了封装和优化,提供了更多方便的语法简化了我们的工作。基本原理如下:

典型的库:

  • art-template
  • mustache.js
  • doT

之前的一篇文章中我介绍了js模板引擎的实现思路,感兴趣的朋友可以看看这里:JavaScript进阶学习(一)—— 基于正则表达式的简单js模板引擎实现。这篇文章中我们利用正则表达式实现了一个简单的js模板引擎,利用正则匹配查找出模板中{{}}之间的内容,然后替换为模型中的数据,从而实现视图的渲染。

 
  1. var template = function(tpl, data) { 
  2.  
  3.   var re = /{{(.+?)}}/g, 
  4.  
  5.     cursor = 0, 
  6.  
  7.     reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,     
  8.  
  9.     code = 'var r=[];\n'
  10.  
  11.   
  12.  
  13.   // 解析html 
  14.  
  15.   function parsehtml(line) { 
  16.  
  17.     // 单双引号转义,换行符替换为空格,去掉前后的空格 
  18.  
  19.     line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,""); 
  20.  
  21.     code +='r.push("' + line + '");\n'
  22.  
  23.   } 
  24.  
  25.    
  26.  
  27.   // 解析js代码         
  28.  
  29.   function parsejs(line) {   
  30.  
  31.     // 去掉前后的空格 
  32.  
  33.     line = line.replace(/(^\s+)|(\s+$)/g,""); 
  34.  
  35.     code += line.match(reExp)? line + '\n' : 'r.push(' + 'this.' + line + ');\n'
  36.  
  37.   }     
  38.  
  39.      
  40.  
  41.   // 编译模板 
  42.  
  43.   while((match = re.exec(tpl))!== null) { 
  44.  
  45.     // 开始标签  {{ 前的内容和结束标签 }} 后的内容 
  46.  
  47.     parsehtml(tpl.slice(cursor, match.index)); 
  48.  
  49.     // 开始标签  {{ 和 结束标签 }} 之间的内容 
  50.  
  51.     parsejs(match[1]); 
  52.  
  53.     // 每一次匹配完成移动指针 
  54.  
  55.     cursor = match.index + match[0].length; 
  56.  
  57.   } 
  58.  
  59.   // 最后一次匹配完的内容 
  60.  
  61.   parsehtml(tpl.substr(cursor, tpl.length - cursor)); 
  62.  
  63.   code += 'return r.join("");'
  64.  
  65.   return new Function(code.replace(/[\r\t\n]/g, '')).apply(data); 
  66.  
  67. }  

源代码:http://jsfiddle.net/zhaomenghuan/bw468orv/embedded/

现在ES6支持了模板字符串,我们可以用比较简单的代码就可以实现类似的功能:

 
  1. const template = data => ` 
  2.  
  3.   <p>name: ${data.name}</p> 
  4.  
  5.   <p>age: ${data.profile.age}</p> 
  6.  
  7.   <ul> 
  8.  
  9.     ${data.skills.map(skill => ` 
  10.  
  11.       <li>${skill}</li> 
  12.  
  13.     `).join('')} 
  14.  
  15.   </ul>` 
  16.  
  17.   
  18.  
  19. const data = { 
  20.  
  21.   name'zhaomenghuan'
  22.  
  23.   profile: { age: 24 }, 
  24.  
  25.   skills: ['html5''javascript''android'
  26.  
  27.  
  28.   
  29.  
  30. document.body.innerHTML = template(data)  

Dom-based templating

Dom-based templating 则是从DOM的角度去实现数据的渲染,我们通过遍历DOM树,提取属性与DOM内容,然后将数据写入到DOM树中,从而实现页面渲染。一个简单的例子如下:

 
  1. function MVVM(opt) { 
  2.  
  3.   this.dom = document.querySelector(opt.el); 
  4.  
  5.   this.data = opt.data || {}; 
  6.  
  7.   this.renderDom(this.dom); 
  8.  
  9.  
  10.   
  11.  
  12. MVVM.prototype = { 
  13.  
  14.   init: { 
  15.  
  16.     sTag: '{{'
  17.  
  18.     eTag: '}}' 
  19.  
  20.   }, 
  21.  
  22.   render: function (node) { 
  23.  
  24.     var self = this; 
  25.  
  26.     var sTag = self.init.sTag; 
  27.  
  28.     var eTag = self.init.eTag; 
  29.  
  30.   
  31.  
  32.     var matchs = node.textContent.split(sTag); 
  33.  
  34.     if (matchs.length){ 
  35.  
  36.       var ret = ''
  37.  
  38.       for (var i = 0; i < matchs.length; i++) { 
  39.  
  40.         var match = matchs[i].split(eTag); 
  41.  
  42.         if (match.length == 1) { 
  43.  
  44.             ret += matchs[i]; 
  45.  
  46.         } else { 
  47.  
  48.             ret = self.data[match[0]]; 
  49.  
  50.         } 
  51.  
  52.         node.textContent = ret; 
  53.  
  54.       } 
  55.  
  56.     } 
  57.  
  58.   }, 
  59.  
  60.   renderDom: function(dom) { 
  61.  
  62.     var self = this; 
  63.  
  64.   
  65.  
  66.     var attrs = dom.attributes; 
  67.  
  68.     var nodes = dom.childNodes; 
  69.  
  70.   
  71.  
  72.     Array.prototype.forEach.call(attrs, function(item) { 
  73.  
  74.       self.render(item); 
  75.  
  76.     }); 
  77.  
  78.   
  79.  
  80.     Array.prototype.forEach.call(nodes, function(item) { 
  81.  
  82.       if (item.nodeType === 1) { 
  83.  
  84.         return self.renderDom(item); 
  85.  
  86.       } 
  87.  
  88.       self.render(item); 
  89.  
  90.     }); 
  91.  
  92.   } 
  93.  
  94.  
  95.   
  96.  
  97. var app = new MVVM({ 
  98.  
  99.   el: '#app'
  100.  
  101.   data: { 
  102.  
  103.     name'zhaomenghuan'
  104.  
  105.     age: '24'
  106.  
  107.     color: 'red' 
  108.  
  109.   } 
  110.  
  111. });  

源代码: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中视图和数据更新的机制,我们先看一个简单的例子:

 
  1. var o = { 
  2.  
  3.   a: 0 
  4.  
  5.  
  6. Object.defineProperty(o, "b", { 
  7.  
  8.   get: function () { 
  9.  
  10.     return this.a + 1; 
  11.  
  12.   }, 
  13.  
  14.   setfunction (value) { 
  15.  
  16.     this.a = value / 2; 
  17.  
  18.   } 
  19.  
  20. }); 
  21.  
  22. console.log(o.a); // "0" 
  23.  
  24. console.log(o.b); // "1" 
  25.  
  26.   
  27.  
  28. // 更新o.a 
  29.  
  30. o.a = 5; 
  31.  
  32. console.log(o.a); // "5" 
  33.  
  34. console.log(o.b); // "6" 
  35.  
  36.   
  37.  
  38. // 更新o.b 
  39.  
  40. o.b = 10; 
  41.  
  42. console.log(o.a); // "5" 
  43.  
  44. console.log(o.b); // "6"  

这里我们可以看出对象o的b属性的值依赖于a属性的值,同时b属性值的变化又可以改变a属性的值,这个过程相关的属性值的变化都会影响其他相关的值进行更新。反过来我们看看如果不使用Object.defineProperty()方法,上述的问题通过直接给对象属性赋值的方法实现,代码如下

 
  1. var o = { 
  2.  
  3.   a: 0 
  4.  
  5. }     
  6.  
  7. o.b = o.a + 1; 
  8.  
  9. console.log(o.a); // "0" 
  10.  
  11. console.log(o.b); // "1" 
  12.  
  13.   
  14.  
  15. // 更新o.a 
  16.  
  17. o.a = 5; 
  18.  
  19. o.b = o.a + 1; 
  20.  
  21. console.log(o.a); // "5" 
  22.  
  23. console.log(o.b); // "6" 
  24.  
  25.   
  26.  
  27. // 更新o.b 
  28.  
  29. o.b = 10; 
  30.  
  31. o.a = o.b / 2; 
  32.  
  33. o.b = o.a + 1; 
  34.  
  35. console.log(o.a); // "5" 
  36.  
  37. 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为不可枚举。

 
  1. var obj = { 
  2.  
  3.   a: 0, 
  4.  
  5.   b: 1 
  6.  
  7.  
  8. for (var prop in obj) { 
  9.  
  10.   console.log(`obj.${prop} = ${obj[prop]}`); 
  11.  
  12. }  

结果:

 
  1. "obj.a = 0" 
  2.  
  3. "obj.b = 1" 

我们通过Object.defineProperty()修改如下:

 
  1. var obj = { 
  2.  
  3.   a: 0, 
  4.  
  5.   b: 1 
  6.  
  7.  
  8. Object.defineProperty(obj, 'b', { 
  9.  
  10.   enumerable: false 
  11.  
  12. }) 
  13.  
  14. for (var prop in obj) { 
  15.  
  16.   console.log(`obj.${prop} = ${obj[prop]}`); 
  17.  
  18. }  

结果:

 
  1. "obj.a = 0" 

这里需要说明的是我们使用Object.defineProperty()默认情况下是enumerable属性为false,例如:

 
  1. var obj = { 
  2.  
  3.   a: 0 
  4.  
  5.  
  6. Object.defineProperty(obj, 'b', { 
  7.  
  8.   value: 1 
  9.  
  10. }) 
  11.  
  12. for (var prop in obj) { 
  13.  
  14.   console.log(`obj.${prop} = ${obj[prop]}`); 
  15.  
  16. }  

结果:

 
  1. "obj.a = 0" 

其他描述属性使用方法类似,不做赘述。Vue源码core/util/lang.jsS中定义了这样一个方法:

 
  1. /** 
  2.  
  3. * Define a property. 
  4.  
  5. */ 
  6.  
  7. export function def (obj: Object, key: string, val: any, enumerable?: boolean) { 
  8.  
  9.   Object.defineProperty(obj, key, { 
  10.  
  11.     value: val, 
  12.  
  13.     enumerable: !!enumerable, 
  14.  
  15.     writable: true
  16.  
  17.     configurable: true 
  18.  
  19.   }) 
  20.  
  21. }  

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

语法:Object.getOwnPropertyDescriptor(obj, prop)

参数:

  • obj:在该对象上查看属性
  • prop:一个属性名称,该属性的属性描述符将被返回

返回值:如果指定的属性存在于对象上,则返回其属性描述符(property descriptor),否则返回 undefined。可以访问“属性描述符”内容,例如前面的例子:

 
  1. var o = { 
  2.  
  3.   a: 0 
  4.  
  5.  
  6.              
  7.  
  8. Object.defineProperty(o, "b", { 
  9.  
  10.   get: function () { 
  11.  
  12.     return this.a + 1; 
  13.  
  14.   }, 
  15.  
  16.   setfunction (value) { 
  17.  
  18.     this.a = value / 2; 
  19.  
  20.   } 
  21.  
  22. }); 
  23.  
  24.              
  25.  
  26. var des = Object.getOwnPropertyDescriptor(o,'b'); 
  27.  
  28. console.log(des); 
  29.  
  30. console.log(des.get);  

Vue源码分析

本次我们主要分析一下Vue 数据绑定的源码,这里我直接将 Vue.js 1.0.28 版本的代码稍作删减拿过来进行,2.x 的代码基于 flow 静态类型检查器书写的,代码除了编码风格在整体结构上基本没有太大改动,所以依然基于 1.x 进行分析,对于存在差异的部分加以说明。

监听对象变动

 
  1. // 观察者构造函数 
  2.  
  3. function Observer (value) { 
  4.  
  5.   this.value = value 
  6.  
  7.   this.walk(value) 
  8.  
  9.  
  10.   
  11.  
  12. // 递归调用,为对象绑定getter/setter 
  13.  
  14. Observer.prototype.walk = function (obj) { 
  15.  
  16.   var keys = Object.keys(obj) 
  17.  
  18.   for (var i = 0, l = keys.length; i < l; i++) { 
  19.  
  20.     this.convert(keys[i], obj[keys[i]]) 
  21.  
  22.   } 
  23.  
  24.  
  25.   
  26.  
  27. // 将属性转换为getter/setter 
  28.  
  29. Observer.prototype.convert = function (key, val) { 
  30.  
  31.   defineReactive(this.value, key, val) 
  32.  
  33.  
  34.   
  35.  
  36. // 创建数据观察者实例 
  37.  
  38. function observe (value) { 
  39.  
  40.   // 当值不存在或者不是对象类型时,不需要继续深入监听 
  41.  
  42.   if (!value || typeof value !== 'object') { 
  43.  
  44.     return 
  45.  
  46.   } 
  47.  
  48.   return new Observer(value) 
  49.  
  50.  
  51.   
  52.  
  53. // 定义对象属性的getter/setter 
  54.  
  55. function defineReactive (obj, key, val) { 
  56.  
  57.   var property = Object.getOwnPropertyDescriptor(obj, key
  58.  
  59.   if (property && property.configurable === false) { 
  60.  
  61.     return 
  62.  
  63.   } 
  64.  
  65.   
  66.  
  67.   // 保存对象属性预先定义的getter/setter 
  68.  
  69.   var getter = property && property.get 
  70.  
  71.   var setter = property && property.set 
  72.  
  73.   
  74.  
  75.   var childOb = observe(val) 
  76.  
  77.   Object.defineProperty(obj, key, { 
  78.  
  79.     enumerable: true
  80.  
  81.     configurable: true
  82.  
  83.     get: function reactiveGetter () { 
  84.  
  85.       var value = getter ? getter.call(obj) : val 
  86.  
  87.       console.log("访问:"+key
  88.  
  89.       return value 
  90.  
  91.     }, 
  92.  
  93.     setfunction reactiveSetter (newVal) { 
  94.  
  95.       var value = getter ? getter.call(obj) : val 
  96.  
  97.       if (newVal === value) { 
  98.  
  99.         return 
  100.  
  101.       } 
  102.  
  103.       if (setter) { 
  104.  
  105.         setter.call(obj, newVal) 
  106.  
  107.       } else { 
  108.  
  109.         val = newVal 
  110.  
  111.       } 
  112.  
  113.       // 对新值进行监听 
  114.  
  115.       childOb = observe(newVal) 
  116.  
  117.       console.log('更新:' + key + ' = ' + newVal) 
  118.  
  119.     } 
  120.  
  121.   }) 
  122.  
  123. }  

定义一个对象作为数据模型,并监听这个对象。

 
  1. let data = { 
  2.  
  3.   user: { 
  4.  
  5.     name'zhaomenghuan'
  6.  
  7.     age: '24' 
  8.  
  9.   }, 
  10.  
  11.   address: { 
  12.  
  13.     city: 'beijing' 
  14.  
  15.   } 
  16.  
  17.  
  18. observe(data) 
  19.  
  20.   
  21.  
  22. console.log(data.user.name
  23.  
  24. // 访问:user 
  25.  
  26. // 访问:name 
  27.  
  28.   
  29.  
  30. data.user.name = 'ZHAO MENGHUAN' 
  31.  
  32. // 访问:user 
  33.  
  34. // 更新:name = ZHAO MENGHUAN  

效果如下:

监听数组变动

上面我们通过Object.defineProperty把对象的属性全部转为 getter/setter 从而实现监听对象的变动,但是对于数组对象无法通过Object.defineProperty实现监听。Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。

 
  1. const arrayProto = Array.prototype 
  2.  
  3. const arrayMethods = Object.create(arrayProto) 
  4.  
  5.   
  6.  
  7. function def(obj, key, val, enumerable) { 
  8.  
  9.   Object.defineProperty(obj, key, { 
  10.  
  11.     value: val, 
  12.  
  13.     enumerable: !!enumerable, 
  14.  
  15.     writable: true
  16.  
  17.     configurable: true 
  18.  
  19.   }) 
  20.  
  21.  
  22.   
  23.  
  24. // 数组的变异方法 
  25.  
  26. ;[ 
  27.  
  28.   'push'
  29.  
  30.   'pop'
  31.  
  32.   'shift'
  33.  
  34.   'unshift'
  35.  
  36.   'splice'
  37.  
  38.   'sort'
  39.  
  40.   'reverse' 
  41.  
  42.  
  43. .forEach(function (method) { 
  44.  
  45.   // 缓存数组原始方法 
  46.  
  47.   var original = arrayProto[method] 
  48.  
  49.   def(arrayMethods, method, function mutator () { 
  50.  
  51.     var i = arguments.length 
  52.  
  53.     var args = new Array(i) 
  54.  
  55.     while (i--) { 
  56.  
  57.       args[i] = arguments[i] 
  58.  
  59.     } 
  60.  
  61.     console.log('数组变动'
  62.  
  63.     return original.apply(this, args) 
  64.  
  65.   }) 
  66.  
  67. })  

Vue.js 1.x 在Array.prototype原型对象上添加了$set 和 $remove方法,在2.X后移除了,使用全局 API Vue.set 和 Vue.delete代替了,后续我们再分析。

定义一个数组作为数据模型,并对这个数组调用变异的七个方法实现监听。

 
  1. let skills = ['JavaScript''Node.js''html5'
  2.  
  3. // 原型指针指向具有变异方法的数组对象 
  4.  
  5. skills.__proto__ = arrayMethods 
  6.  
  7.   
  8.  
  9. skills.push('java'
  10.  
  11. // 数组变动 
  12.  
  13. skills.pop() 
  14.  
  15. // 数组变动  

效果如下:

我们将需要监听的数组的原型指针指向我们定义的数组对象,这样我们的数组在调用上面七个数组的变异方法时,能够监听到变动从而实现对数组进行跟踪。

对于__proto__属性,在ES2015中正式被加入到规范中,标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,所以 Vue 是先进行了判断,当__proto__属性存在时将原型指针__proto__指向具有变异方法的数组对象,不存在时直接将具有变异方法挂在需要追踪的对象上。

我们可以在上面Observer观察者构造函数中添加对数组的监听,源码如下:

 
  1. const hasProto = '__proto__' in {} 
  2.  
  3. const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 
  4.  
  5.   
  6.  
  7. // 观察者构造函数 
  8.  
  9. function Observer (value) { 
  10.  
  11.   this.value = value 
  12.  
  13.   if (Array.isArray(value)) { 
  14.  
  15.     var augment = hasProto 
  16.  
  17.       ? protoAugment 
  18.  
  19.       : copyAugment 
  20.  
  21.     augment(value, arrayMethods, arrayKeys) 
  22.  
  23.     this.observeArray(value) 
  24.  
  25.   } else { 
  26.  
  27.     this.walk(value) 
  28.  
  29.   } 
  30.  
  31.  
  32.   
  33.  
  34. // 观察数组的每一项 
  35.  
  36. Observer.prototype.observeArray = function (items) { 
  37.  
  38.   for (var i = 0, l = items.length; i < l; i++) { 
  39.  
  40.     observe(items[i]) 
  41.  
  42.   } 
  43.  
  44.  
  45.   
  46.  
  47. // 将目标对象/数组的原型指针__proto__指向src 
  48.  
  49. function protoAugment (target, src) { 
  50.  
  51.   target.__proto__ = src 
  52.  
  53.  
  54.   
  55.  
  56. // 将具有变异方法挂在需要追踪的对象上 
  57.  
  58. function copyAugment (target, src, keys) { 
  59.  
  60.   for (var i = 0, l = keys.length; i < l; i++) { 
  61.  
  62.     var key = keys[i] 
  63.  
  64.     def(target, key, src[key]) 
  65.  
  66.   } 
  67.  
  68. }  

原型链

对于不了解原型链的朋友可以看一下我这里画的一个基本关系图:

  • 原型对象是构造函数的prototype属性,是所有实例化对象共享属性和方法的原型对象;
  • 实例化对象通过new构造函数得到,都继承了原型对象的属性和方法;
  • 原型对象中有个隐式的constructor,指向了构造函数本身。

Object.create

Object.create 使用指定的原型对象和其属性创建了一个新的对象。

 
  1. const arrayProto = Array.prototype 
  2.  
  3. 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 实现类式继承。

 
  1. // 实现继承 
  2.  
  3. var extend = function(Child, Parent) { 
  4.  
  5.     // 拷贝Parent原型对象 
  6.  
  7.     Child.prototype = Object.create(Parent.prototype); 
  8.  
  9.     // 将Child构造函数赋值给Child的原型对象 
  10.  
  11.     Child.prototype.constructor = Child; 
  12.  
  13.  
  14.   
  15.  
  16. // 实例 
  17.  
  18. var Parent = function () { 
  19.  
  20.     this.name = 'Parent'
  21.  
  22.  
  23. Parent.prototype.getName = function () { 
  24.  
  25.     return this.name
  26.  
  27.  
  28. var Child = function () { 
  29.  
  30.     this.name = 'Child'
  31.  
  32.  
  33. extend(Child, Parent); 
  34.  
  35. var child = new Child(); 
  36.  
  37. console.log(child.getName())  

发布-订阅模式

在上面一部分我们通过Object.defineProperty把对象的属性全部转为 getter/setter 以及 数组变异方法实现了对数据模型变动的监听,在数据变动的时候,我们通过console.log打印出来提示了,但是对于框架而言,我们相关的逻辑如果直接写在那些地方,自然是不够优雅和灵活的,这个时候就需要引入常用的设计模式去实现,vue.js采用了发布-订阅模式。发布-订阅模式主要是为了达到一种“高内聚、低耦合”的效果。

Vue的Watcher订阅者作为Observer和Compile之间通信的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。

 
  1. /** 
  2.  
  3. * 观察者对象 
  4.  
  5. */ 
  6.  
  7. function Watcher(vm, expOrFn, cb) { 
  8.  
  9.     this.vm = vm 
  10.  
  11.     this.cb = cb 
  12.  
  13.     this.depIds = {} 
  14.  
  15.     if (typeof expOrFn === 'function') { 
  16.  
  17.         this.getter = expOrFn 
  18.  
  19.     } else { 
  20.  
  21.         this.getter = this.parseExpression(expOrFn) 
  22.  
  23.     } 
  24.  
  25.     this.value = this.get() 
  26.  
  27.  
  28.   
  29.  
  30. /** 
  31.  
  32. * 收集依赖 
  33.  
  34. */ 
  35.  
  36. Watcher.prototype.get = function () { 
  37.  
  38.     // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者 
  39.  
  40.     Dep.target = this 
  41.  
  42.     // 触发getter,将自身添加到dep中 
  43.  
  44.     const value = this.getter.call(this.vm, this.vm) 
  45.  
  46.     // 依赖收集完成,置空,用于下一个Watcher使用 
  47.  
  48.     Dep.target = null 
  49.  
  50.     return value 
  51.  
  52.  
  53.   
  54.  
  55. Watcher.prototype.addDep = function (dep) { 
  56.  
  57.     if (!this.depIds.hasOwnProperty(dep.id)) { 
  58.  
  59.         dep.addSub(this) 
  60.  
  61.         this.depIds[dep.id] = dep 
  62.  
  63.     } 
  64.  
  65.  
  66.   
  67.  
  68. /** 
  69.  
  70. * 依赖变动更新 
  71.  
  72.  
  73. * @param {Boolean} shallow 
  74.  
  75. */ 
  76.  
  77. Watcher.prototype.update = function () { 
  78.  
  79.     this.run() 
  80.  
  81.  
  82.   
  83.  
  84. Watcher.prototype.run = function () { 
  85.  
  86.     var value = this.get() 
  87.  
  88.     if (value !== this.value) { 
  89.  
  90.         var oldValue = this.value 
  91.  
  92.         this.value = value 
  93.  
  94.         // 将newVal, oldVal挂载到MVVM实例上 
  95.  
  96.         this.cb.call(this.vm, value, oldValue) 
  97.  
  98.     } 
  99.  
  100.  
  101.   
  102.  
  103. Watcher.prototype.parseExpression = function (exp) { 
  104.  
  105.     if (/[^\w.$]/.test(exp)) { 
  106.  
  107.         return 
  108.  
  109.     } 
  110.  
  111.     var exps = exp.split('.'
  112.  
  113.      
  114.  
  115.     return function(obj) { 
  116.  
  117.         for (var i = 0, len = exps.length; i < len; i++) { 
  118.  
  119.             if (!obj) return 
  120.  
  121.             obj = obj[exps[i]] 
  122.  
  123.         } 
  124.  
  125.         return obj 
  126.  
  127.     } 
  128.  
  129. }  

Dep 是一个数据结构,其本质是维护了一个watcher队列,负责添加watcher,更新watcher,移除watcher,通知watcher更新。

 
  1. let uid = 0 
  2.  
  3.   
  4.  
  5. function Dep() { 
  6.  
  7.     this.id = uid++ 
  8.  
  9.     this.subs = [] 
  10.  
  11.  
  12.   
  13.  
  14. Dep.target = null 
  15.  
  16.   
  17.  
  18. /** 
  19.  
  20. * 添加一个订阅者 
  21.  
  22.  
  23. * @param {Directive} sub 
  24.  
  25. */ 
  26.  
  27. Dep.prototype.addSub = function (sub) { 
  28.  
  29.     this.subs.push(sub) 
  30.  
  31.  
  32.   
  33.  
  34. /** 
  35.  
  36. * 移除一个订阅者 
  37.  
  38.  
  39. * @param {Directive} sub 
  40.  
  41. */ 
  42.  
  43. Dep.prototype.removeSub = function (sub) { 
  44.  
  45.     let index = this.subs.indexOf(sub); 
  46.  
  47.     if (index !== -1) { 
  48.  
  49.         this.subs.splice(index, 1); 
  50.  
  51.     } 
  52.  
  53.  
  54.   
  55.  
  56. /** 
  57.  
  58. * 将自身作为依赖添加到目标watcher 
  59.  
  60. */ 
  61.  
  62. Dep.prototype.depend = function () { 
  63.  
  64.     Dep.target.addDep(this) 
  65.  
  66.  
  67.   
  68.  
  69. /** 
  70.  
  71. * 通知数据变更 
  72.  
  73. */ 
  74.  
  75. Dep.prototype.notify = function () { 
  76.  
  77.     var subs = toArray(this.subs) 
  78.  
  79.     // stablize the subscriber list first 
  80.  
  81.     for (var i = 0, l = subs.length; i < l; i++) { 
  82.  
  83.         // 执行订阅者的update更新函数 
  84.  
  85.         subs[i].update() 
  86.  
  87.     } 
  88.  
  89. }  

模板编译

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

这种实现和我们讲到的Dom-based templating类似,只是更加完备,具有自定义指令的功能。在遍历节点属性和文本节点的时候,可以编译具备{{}}表达式或v-xxx的属性值的节点,并且通过添加 new Watcher()及绑定事件函数,监听数据的变动从而对视图实现双向绑定。

MVVM实例

在数据绑定初始化的时候,我们需要通过new Observer()来监听数据模型变化,通过new Compile()来解析编译模板指令,并利用Watcher搭起Observer和Compile之间的通信桥梁。

 
  1. /** 
  2.  
  3. * @class 双向绑定类 MVVM 
  4.  
  5. * @param {[type]} options [description] 
  6.  
  7. */ 
  8.  
  9. function MVVM(options) { 
  10.  
  11.     this.$options = options || {} 
  12.  
  13.     // 简化了对data的处理 
  14.  
  15.     let data = this._data = this.$options.data 
  16.  
  17.     // 监听数据 
  18.  
  19.     observe(data) 
  20.  
  21.     new Compile(options.el || document.body, this) 
  22.  
  23.  
  24.   
  25.  
  26. MVVM.prototype.$watch = function (expOrFn, cb) { 
  27.  
  28.     new Watcher(this, expOrFn, cb) 
  29.  
  30. }  

为了能够直接通过实例化对象操作数据模型,我们需要为MVVM实例添加一个数据模型代理的方法:

 
  1. MVVM.prototype._proxy = function (key) { 
  2.  
  3.     Object.defineProperty(this, key, { 
  4.  
  5.         configurable: true
  6.  
  7.         enumerable: true
  8.  
  9.         get: () => this._data[key], 
  10.  
  11.         set: (val) => { 
  12.  
  13.             this._data[key] = val 
  14.  
  15.         } 
  16.  
  17.     }) 
  18.  
  19. }  

至此我们可以通过一个小例子来说明本文的内容:

 
  1. <div id="app"
  2.  
  3.     <h3>{{user.name}}</h3> 
  4.  
  5.     <input type="text" v-model="modelValue"
  6.  
  7.     <p>{{modelValue}}</p> 
  8.  
  9. </div> 
  10.  
  11. <script> 
  12.  
  13.     let vm = new MVVM({ 
  14.  
  15.         el: '#app'
  16.  
  17.         data: { 
  18.  
  19.             modelValue: ''
  20.  
  21.             user: { 
  22.  
  23.                 name'zhaomenghuan'
  24.  
  25.                 age: '24' 
  26.  
  27.             }, 
  28.  
  29.             address: { 
  30.  
  31.                 city: 'beijing' 
  32.  
  33.             }, 
  34.  
  35.             skills: ['JavaScript''Node.js''html5'
  36.  
  37.         } 
  38.  
  39.     }) 
  40.  
  41.      
  42.  
  43.     vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`)) 
  44.  
  45. </script>  

本文目的不是为了造一个轮子,而是在学习优秀框架实现的过程中去提升自己,搞清楚框架发展的前因后果,由浅及深去学习基础,本文参考了网上很多优秀博主的文章,由于时间关系,有些内容没有做深入探讨,觉得还是有些遗憾,在后续的学习中会更多的独立思考,提出更多自己的想法。

参考文档

  • 前端模板技术面面观
  • Object.defineProperty()
  • Vue.js 源码学习笔记
  • vue早期源码学习系列
  • 解析最简单的observer和watcher
  • 剖析Vue实现原理 – 如何实现双向绑定mvvm 
作者:佚名
来源:51CTO
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 割双眼皮没去脂

    ...

    2024/4/21 14:21:18
  2. 割双眼皮埋线图片大全

    ...

    2024/4/21 14:21:17
  3. 聊聊最近几年的路径追踪技术的进展(一)

    路径追踪技术&#xff08;Path tracing&#xff0c;PT&#xff09;已经是当下工业中离线渲染使用的主流技术&#xff0c;不管是商业渲染器如皮克斯的RenderMan&#xff0c;Solid Angle的Arnold等&#xff0c;还是迪士尼的in-house渲染器Hyperion以及Weta Digital的Manuka都是基…...

    2024/4/21 14:21:16
  4. PBRT_V2 总结记录 54 Volume Scattering Processes

    There are three main processes that affect the distribution of radiance in an environment with participating media: Absorption—the reduction in radiance due to the conversion of light to another form of energy, such as heat &#xff08;吸收&#xff09; …...

    2024/4/21 14:21:15
  5. Gate学习1

    一、了解gate GATE是国际OpenGATE协作开发的高级开源软件&#xff0c;致力于医学成像和放射治疗的数值模拟。支持PET、SPECT、CT、光学成像&#xff08;荧光成像和生物发光&#xff09;以及放射治疗。 二、开始gate 1、gate使用的语言 gate不使用c语言&#xff0c;用户可以…...

    2024/4/21 14:21:15
  6. 文献阅读(一)

    Fundamentally Reading (仅作为学习过程中的记录,由于是最开始几篇相关论文,读的比较细) Efficient Reflectance Capture Using an Autoencoder 1.问题介绍 渲染方程:https://zhuanlan.zhihu.com/p/52497510?from_voters_pagetrue BRDF概念:https://www.cnblogs.com/meng…...

    2024/4/21 14:21:13
  7. win10的任务管理器显示所占内存,比实际占用内存小的原因

    我们看这个图片,我电脑内存是8g,占用31%,也就是2.5g左右,但是下面的每项加起来只是2.5g的一半。正确的看真实占用应该在资源监视器里面看。   打开资源监视器的方法:打开任务管理器 –>选择性能 –> 这页的左下角有打开资源监视器选项。 其页面如下: 这里显示的才…...

    2024/4/24 19:18:25
  8. 文献阅读 - SphereFace: Deep Hypersphere Embedding for Face Recognition

    SphereFace: Deep Hypersphere Embedding for Face Recognition W. Liu, et al., SphereFace: Deep Hypersphere Embedding for Face Recognition, CVPR (2017) 摘要 开集&#xff08;open-set protocol&#xff09;面部识别的特征要求&#xff1a;在某个测度空间&#xff08;…...

    2024/4/21 14:21:11
  9. 人脸识别的LOSS(上)

    超多分类的Softmax 2014年CVPR两篇超多分类的人脸识别论文&#xff1a;DeepFace和DeepID Taigman Y, Yang M, Ranzato M A, et al. Deepface: Closing the gap to human-level performance in face verification [C]// CVPR, 2014. DeepFace&#xff1a;4.4M训练集&#xff0c…...

    2024/4/21 14:21:10
  10. mongodb集合API

    集合上的方法 db.test.addIdIfNeeded( db.test.getSplitKeysForChunks( db.test.aggregate( db.test.getWriteConcern( db.test.bulkWrite( db.test.group( db.test.constructor db.test.groupcmd( db.test.c…...

    2024/4/21 14:21:10
  11. 【翻译】Species distribution modeling 2 数据准备

    2.准备数据 2.1 物种出现数据 向R中导入出现数据很简单。但收集、地理引用和交叉验证坐标数据是乏味的。对物种分布建模的讨论通常关注点在于对比建模方法&#xff0c;但当你要对付一些记录少或不确定的物种时&#xff0c;你的关注点恐怕应该放在提高出现数据的质量了。如果你的…...

    2024/4/21 14:21:08
  12. 割割双眼皮埋线过程图

    ...

    2024/4/21 14:21:09
  13. 割双眼皮麻药散了疼吗

    ...

    2024/4/21 14:21:06
  14. 割双眼皮六个月没消肿

    ...

    2024/4/21 14:21:05
  15. Angular只跑某一个Test Case

    Angular只&#xff08;不&#xff09;跑某一个Test Case 只跑(Only Run) Add f before describe or it, i.e. fdescribe fit只不跑(Exclude) Add x before describe or it, i.e. xdescribe xit...

    2024/4/21 14:21:04
  16. 割双眼皮流泪没事吧

    ...

    2024/4/20 15:29:08
  17. 割双眼皮了怎么热敷

    ...

    2024/4/20 15:29:07
  18. 割双眼皮了能吃蘑菇吗

    ...

    2024/4/20 15:29:06
  19. 割双眼皮了不能吃什么

    ...

    2024/4/20 3:35:02
  20. 割双眼皮两眼不一样吗

    ...

    2024/4/21 14:21:03

最新文章

  1. 5分钟快速搭建k8s集群1.29.x

    配置主机名和hosts 配置主机名 hostnamectl set-hostname node1 hostnamectl set-hostname node2 hostnamectl set-hostname node3 vim /etc/hosts 172.19.35.202 node1 172.19.35.203 node2 172.19.35.204 node3 测试 hostname ping -c 3 node2 配置时间同步 关闭防火墙 sys…...

    2024/4/25 17:27:39
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 深入浅出 -- 系统架构之微服务中Nacos的部署

    前面我们提到过&#xff0c;在微服务架构中&#xff0c;Nacos注册中心属于核心组件&#xff0c;通常我们会采用高性能独立服务器进行部署&#xff0c;下面我们一起来看看Nacos部署过程&#xff1a; 1、环境准备 因为Nacos是支持windows和Linux系统的&#xff0c;且服务器操作…...

    2024/4/23 6:26:15
  4. LeetCode-46. 全排列【数组 回溯】

    LeetCode-46. 全排列【数组 回溯】 题目描述&#xff1a;解题思路一&#xff1a;回溯。回溯三部曲解题思路二&#xff1a;0解题思路三&#xff1a;0 题目描述&#xff1a; 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案…...

    2024/4/23 6:35:45
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/25 11:51:20
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/23 13:30:22
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/23 13:28:06
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/24 18:16:28
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/23 13:27:44
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/19 11:57:53
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/23 13:29:53
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/25 16:48:44
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/25 13:39:44
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/23 22:01:21
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/23 13:29:23
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/25 0:00:17
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/25 4:19:21
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/19 11:59:23
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/19 11:59:44
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/25 2:10:52
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/24 16:38:05
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/25 13:19:01
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/23 13:27:51
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/23 13:27:19
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57