Vue由于其高效的性能和灵活入门简单、轻量的特点下变得火热。在当今前端越来越普遍的使用,今天来剖析一下Vue的深入响应式原理。


tips:转自我的博客唐益达博客,此为原创。转载请注明出处,原文链接


一、Vue对比其他框架原理

Vue相对于React,Angular更加综合一点。AngularJS则使用了“脏值检测”。

React则采用避免直接操作DOM的虚拟dom树。而Vue则采用的是 Object.defineProperty特性(这在ES5中是无法slim的,这就是为什么vue2.0不支持ie8以下的浏览器)

Vue可以说是尤雨溪从Angular中提炼出来的,又参照了React的性能思路,而集大成的一种轻量、高效,灵活的框架。

二、Vue的原理

Vue的原理可以简单地从下列图示所得出

  1. 通过建立虚拟dom树document.createDocumentFragment(),方法创建虚拟dom树。
  2. 一旦被监测的数据改变,会通过Object.defineProperty定义的数据拦截,截取到数据的变化。
  3. 截取到的数据变化,从而通过订阅——发布者模式,触发Watcher(观察者),从而改变虚拟dom的中的具体数据。
  4. 最后,通过更新虚拟dom的元素值,从而改变最后渲染dom树的值,完成双向绑定

Vue的模式是m-v-vm模式,即(model-view-modelView),通过modelView作为中间层(即vm的实例),进行双向数据的绑定与变化。

而实现这种双向绑定的关键就在于:

Object.defineProperty订阅——发布者模式浙两点。

下面我们通过实例来实现Vue的基本双向绑定。

三、Vue双向绑定的实现

3.1 简易双绑

首先,我们把注意力集中在这个属性上:Object.defineProperty。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法:Object.defineProperty(obj, prop, descriptor)

什么叫做,定义或修改一个对象的新属性,并返回这个对象呢?

  1. var obj = {};
  2. Object.defineProperty(obj,'hello',{
  3. get:function(){
  4. //我们在这里拦截到了数据
  5. console.log("get方法被调用");
  6. },
  7. set:function(newValue){
  8. //改变数据的值,拦截下来额
  9. console.log("set方法被调用");
  10. }
  11. });
  12. obj.hello//输出为“get方法被调用”,输出了值。
  13. obj.hello = 'new Hello';//输出为set方法被调用,修改了新值

输出结果如下:

可以从这里看到,这是在对更底层的对象属性进行编程。简单地说,也就是我们对其更底层对象属性的修改或获取的阶段进行了拦截(对象属性更改的钩子函数)。

在这数据拦截的基础上,我们可以做到数据的双向绑定:

  1. var obj = {};
  2. Object.defineProperty(obj,'hello',{
  3. get:function(){
  4. //我们在这里拦截到了数据
  5. console.log("get方法被调用");
  6. },
  7. set:function(newValue){
  8. //改变数据的值,拦截下来额
  9. console.log("set方法被调用");
  10. document.getElementById('test').value = newValue;
  11. document.getElementById('test1').innerHTML = newValue;
  12. }
  13. });
  14. //obj.hello;
  15. //obj.hello = '123';
  16. document.getElementById('test').addEventListener('input',function(e){
  17. obj.hello = e.target.value;//触发它的set方法
  18. })

html:

  1. <div id="mvvm">
  2. <input v-model="text" id="test"></input>
  3. <div id="test1"></div>
  4. </div>

在线演示:demo演示

在这我们可以简单的实现了一个双向绑定。但是到这还不够,我们的目的是实现一个Vue。

3.2 Vue初始化(虚拟节点的产生与编译)

3.2.1 Vue的虚拟节点容器
  1. function nodeContainer(node, vm, flag){
  2. var flag = flag || document.createDocumentFragment();
  3. var child;
  4. while(child = node.firstChild){
  5. compile(child, vm);
  6. flag.appendChild(child);
  7. if(child.firstChild){
  8. // flag.appendChild(nodeContainer(child,vm));
  9. nodeContainer(child, vm, flag);
  10. }
  11. }
  12. return flag;
  13. }

这里几个注意的点:

  1. while(child = node.firstChild)把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点。一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while。
  2. document.createDocumentFragment();是一个虚拟节点的容器树,可以存放我们的虚拟节点。
  3. 上面的函数是个迭代,一直循环到节点的终点为止。
3.2.2 Vue的节点初始化编译

先声明一个Vue对象

  1. function Vue(options){
  2. this.data = options.data;
  3. var id = options.el;
  4. var dom = nodeContainer(document.getElementById(id),this);
  5. document.getElementById(id).appendChild(dom);
  6. }
  7. //随后使用他
  8. var Demo = new Vue({
  9. el:'mvvm',
  10. data:{
  11. text:'HelloWorld',
  12. d:'123'
  13. }
  14. })

接下去的具体得初始化内容

  1. //编译
  2. function compile(node, vm){
  3. var reg = /\{\{(.*)\}\}/g;//匹配双绑的双大括号
  4. if(node.nodeType === 1){
  5. var attr = node.attributes;
  6. //解析节点的属性
  7. for(var i = 0;i < attr.length; i++){
  8. if(attr[i].nodeName == 'v-model'){
  9. var name = attr[i].nodeValue;
  10. node.value = vm.data[name];//讲实例中的data数据赋值给节点
  11. //node.removeAttribute('v-model');
  12. }
  13. }
  14. }
  15. //如果节点类型为text
  16. if(node.nodeType === 3){
  17. if(reg.test(node.nodeValue)){
  18. // console.dir(node);
  19. var name = RegExp.$1;//获取匹配到的字符串
  20. name = name.trim();
  21. node.nodeValue = vm.data[name];
  22. }
  23. }
  24. }

代码解释:

  1. 当nodeType为1的时候,表示是个元素。同时我们进行判断,如果节点中的指令含有v-model这个指令,那么我们就初始化,进行对节点的值的赋值。
  2. 如果nodeType为3的时候,也就是text节点属性。表示你的节点到了终点,一般都是节点的前后末端。我们常常在这里定义我们的双绑值。此时一旦匹配到了双绑(双大括号),即进行值的初始化。

至此,我们的Vue初始化已经完成。

在线演示:demo1

3.3 Vue的声明响应式

3.3.1 定义Vue的data的属性响应式
  1. function defineReactive (obj, key, value){
  2. Object.defineProperty(obj,key,{
  3. get:function(){
  4. console.log("get了值"+value);
  5. return value;//获取到了值
  6. },
  7. set:function(newValue){
  8. if(newValue === value){
  9. return;//如果值没变化,不用触发新值改变
  10. }
  11. value = newValue;//改变了值
  12. console.log("set了最新值"+value);
  13. }
  14. })
  15. }

这里的obj我们这定义为vm实例或者vm实例里面的data属性。

PS:这里强调一下,defineProperty这个方法,不仅可以定义obj的直接属性,比如obj.hello这个属性。也可以间接定义属性比如:obj.middle.hello。这里导致的效果就是两者的hello属性都被定义成响应式了。

用下列的observe方法循环调用响应式方法。

  1. function observe (obj,vm){
  2. Object.keys(obj).forEach(function(key){
  3. defineReactive(vm,key,obj[key]);
  4. })
  5. }

然后再Vue方法中初始化:

  1. function Vue(options){
  2. this.data = options.data;
  3. var data = this.data;
  4. -------------------------
  5. observe(data,this);//这里调用定义响应式方法
  6. -------------------------
  7. var id = options.el;
  8. var dom = nodeContainer(document.getElementById(id),this);
  9. document.getElementById(id).appendChild(dom); //把虚拟dom渲染上去
  10. }

在编译方法中v-model属性找到的时候去监听:

  1. function compile(node, vm){
  2. var reg = /\{\{(.*)\}\}/g;
  3. if(node.nodeType === 1){
  4. var attr = node.attributes;
  5. //解析节点的属性
  6. for(var i = 0;i < attr.length; i++){
  7. if(attr[i].nodeName == 'v-model'){
  8. var name = attr[i].nodeValue;
  9. -------------------------//这里新添加的监听
  10. node.addEventListener('input',function(e){
  11. console.log(vm[name]);
  12. vm[name] = e.target.value;//改变实例里面的值
  13. });
  14. -------------------------
  15. node.value = vm[name];//讲实例中的data数据赋值给节点
  16. //node.removeAttribute('v-model');
  17. }
  18. }
  19. }
  20. }

以上我们实现了,你再输入框里面输入,同时触发getter&setter,去改变vm实例中data的值。也就是说MVVM的图例中经过getter&setter已经成功了。接下去就是订阅——发布者模式。

在线演示:demo2

实现效果:

3.4 订阅——发布者模式

什么是订阅——发布者?简单点说:你微信里面经常会订阅一些公众号,一旦这些公众号发布新消息了。那么他就会通知你,告诉你:我发布了新东西,快来看。

这种情景下,你就是订阅者,公众号就是发布者

所以我们要模拟这种情景,我们先声明3个订阅者:

  1. var sub1 = {
  2. update:function(){
  3. console.log(1);
  4. }
  5. }
  6. var sub2 = {
  7. update:function(){
  8. console.log(2);
  9. }
  10. }
  11. var sub3 = {
  12. update:function(){
  13. console.log(3);
  14. }
  15. }

每个订阅者对象内部声明一个update方法来触发订阅属性。

再声明一个发布者,去触发发布消息,通知的方法::

  1. function Dep(){
  2. this.subs = [sub1,sub2,sub3];//把三个订阅者加进去
  3. }
  4. Dep.prototype.notify = function(){//在原型上声明“发布消息”方法
  5. this.subs.forEach(function(sub){
  6. sub.update();
  7. })
  8. }
  9. var dep = new Dep();
  10. //pub.publish();
  11. dep.notify();

我们也可以声明另外一个中间对象

  1. var dep = new Dep();
  2. var pub = {
  3. publish:function(){
  4. dep.notify();
  5. }
  6. }
  7. pub.publish();//这里的结果是跟上面一样的

实现效果:

到这,我们已经实现了:

  1. 修改输入框内容 => 触发修改vm实例里的属性值 => 触发set&get方法
  2. 订阅成功 => 发布者发出通知notify() => 触发订阅者的update()方法

接下来重点要实现的是:如何去更新视图,同时把订阅——发布者模式进去watcher观察者模式?

3.5 观察者模式

先定义发布者:

  1. function Dep(){
  2. this.subs = [];
  3. }
  4. Dep.prototype ={
  5. add:function(sub){//这里定义增加订阅者的方法
  6. this.subs.push(sub);
  7. },
  8. notify:function(){//这里定义触发订阅者update()的通知方法
  9. this.subs.forEach(function(sub){
  10. console.log(sub);
  11. sub.update();//下列发布者的更新方法
  12. })
  13. }
  14. }

再定义观察者(订阅者):

  1. function Watcher(vm,node,name){
  2. Dep.global = this;//这里很重要!把自己赋值给Dep函数对象的全局变量
  3. this.name = name;
  4. this.node = node;
  5. this.vm = vm;
  6. this.update();
  7. Dep.global = null;//这里update()完记得清空Dep函数对象的全局变量
  8. }
  9. Watcher.prototype.update = function(){
  10. this.get();
  11. switch (this.node.nodeType) { //这里去通过判断节点的类型改变视图的值
  12. case 1:
  13. this.node.value = this.value;
  14. break;
  15. case 3:
  16. this.node.nodeValue = this.value;
  17. break;
  18. default: break;
  19. };
  20. }
  21. Watcher.prototype.get = function(){
  22. this.value = this.vm[this.name];//这里把this的value值赋值,触发data的defineProperty方法中的get方法!
  23. }

以上需要注意的点:

  1. 在Watcher函数对象的原型方法update里面更新视图的值(实现watcher到视图层的改变)。
  2. Watcher函数对象的原型方法get,是为了触发defineProperty方法中的get方法!
  3. 在new一个Watcher的对象的时候,记得把Dep函数对象赋值一个全局变量,而且及时清空。至于为什么这么做,我们接下来看。
  1. function defineReactive (obj, key, value){
  2. var dep = new Dep();//这里每一个vm的data属性值声明一个新的订阅者
  3. Object.defineProperty(obj,key,{
  4. get:function(){
  5. console.log(Dep.global);
  6. -----------------------
  7. if(Dep.global){//这里是第一次new对象Watcher的时候,初始化数据的时候,往订阅者对象里面添加对象。第二次后,就不需要再添加了
  8. dep.add(Dep.global);
  9. }
  10. -----------------------
  11. return value;
  12. },
  13. set:function(newValue){
  14. if(newValue === value){
  15. return;
  16. }
  17. value = newValue;
  18. dep.notify();//触发了update()方法
  19. }
  20. })
  21. }

这里有一点需要注意:

在上述圈起来的地方:if(Dep.global)是在第一次new Watcher()的时候,进入update()方法,触发这里的get方法。这里非常的重要的一点!在此时new Watcher()只走到了this.update();方法,此刻没有触发Dep.global = null函数,所以值并没有清空,所以可以进到dep.add(Dep.global);方法里面去。

而第二次后,由于清空了Dep的全局变量,所以不会触发add()方法。

PS:这个思路容易被忽略,由于是参考之前一个博主的代码影响,我自己想了很多方法改变,但是在这种情景下难以实现别的更好的交互方式。

所以我暂时现在只能使用Dep的全局变量的方式,来实现Dep函数与Watcher函数的交互。(如果是ES6的模块化方法会不一样)

而后我会尽量找寻其他更好的方法来实现Dep函数与Watcher函数的交互。

紧接着在text节点和绑定了的input节点(别忘记了这个节点)new Watcher的方法来触发以上的内容:

  1. // 如果节点为input
  2. if(node.nodeType === 1){
  3. ...........
  4. ----------
  5. new Watcher(vm,node,name) // 别忘记给input添加观察者模式
  6. ----------
  7. }
  8. //如果节点类型为text
  9. if(node.nodeType === 3){
  10. if(reg.test(node.nodeValue)){
  11. // console.dir(node);
  12. var name = RegExp.$1;//获取匹配到的字符串
  13. name = name.trim();
  14. // node.nodeValue = vm[name];
  15. -------------------------
  16. new Watcher(vm,node,name);//这里到了一个新的节点,new一个新的观察者
  17. -------------------------
  18. }
  19. }

至此,vue双向绑定已经简单的实现。

3.6 最终效果

在线演示:Codepen实现Vue的demo(有时候要翻墙)

在线源码参考:demo4

下列是全部的源码,仅供参考。

HTML:

  1. <div id="mvvm">
  2. <input v-model="d" id="test">{{text}}
  3. <div>{{d}}</div>
  4. </div>

JS:

  1. var obj = {};
  2. function nodeContainer(node, vm, flag){
  3. var flag = flag || document.createDocumentFragment();
  4. var child;
  5. while(child = node.firstChild){
  6. compile(child, vm);
  7. flag.appendChild(child);
  8. if(child.firstChild){
  9. nodeContainer(child, vm, flag);
  10. }
  11. }
  12. return flag;
  13. }
  14. //编译
  15. function compile(node, vm){
  16. var reg = /\{\{(.*)\}\}/g;
  17. if(node.nodeType === 1){
  18. var attr = node.attributes;
  19. //解析节点的属性
  20. for(var i = 0;i < attr.length; i++){
  21. if(attr[i].nodeName == 'v-model'){
  22. var name = attr[i].nodeValue;
  23. node.addEventListener('input',function(e){
  24. vm[name] = e.target.value;
  25. });
  26. node.value = vm[name];//讲实例中的data数据赋值给节点
  27. node.removeAttribute('v-model');
  28. }
  29. }
  30. }
  31. //如果节点类型为text
  32. if(node.nodeType === 3){
  33. if(reg.test(node.nodeValue)){
  34. // console.dir(node);
  35. var name = RegExp.$1;//获取匹配到的字符串
  36. name = name.trim();
  37. // node.nodeValue = vm[name];
  38. new Watcher(vm,node,name);
  39. }
  40. }
  41. }
  42. function defineReactive (obj, key, value){
  43. var dep = new Dep();
  44. Object.defineProperty(obj,key,{
  45. get:function(){
  46. console.log(Dep.global);
  47. if(Dep.global){
  48. dep.add(Dep.global);
  49. }
  50. console.log("get了值"+value);
  51. return value;
  52. },
  53. set:function(newValue){
  54. if(newValue === value){
  55. return;
  56. }
  57. value = newValue;
  58. console.log("set了最新值"+value);
  59. dep.notify();
  60. }
  61. })
  62. }
  63. function observe (obj,vm){
  64. Object.keys(obj).forEach(function(key){
  65. defineReactive(vm,key,obj[key]);
  66. })
  67. }
  68. function Vue(options){
  69. this.data = options.data;
  70. var data = this.data;
  71. observe(data,this);
  72. var id = options.el;
  73. var dom = nodeContainer(document.getElementById(id),this);
  74. document.getElementById(id).appendChild(dom);
  75. }
  76. function Dep(){
  77. this.subs = [];
  78. }
  79. Dep.prototype ={
  80. add:function(sub){
  81. this.subs.push(sub);
  82. },
  83. notify:function(){
  84. this.subs.forEach(function(sub){
  85. console.log(sub);
  86. sub.update();
  87. })
  88. }
  89. }
  90. function Watcher(vm,node,name){
  91. Dep.global = this;
  92. this.name = name;
  93. this.node = node;
  94. this.vm = vm;
  95. this.update();
  96. Dep.global = null;
  97. }
  98. Watcher.prototype = {
  99. update:function(){
  100. this.get();
  101. switch (this.node.nodeType) {
  102. case 1:
  103. this.node.value = this.value;
  104. break;
  105. case 3:
  106. this.node.nodeValue = this.value;
  107. break;
  108. default: break;
  109. }
  110. },
  111. get:function(){
  112. this.value = this.vm[this.name];
  113. }
  114. }
  115. var Demo = new Vue({
  116. el:'mvvm',
  117. data:{
  118. text:'HelloWorld',
  119. d:'123'
  120. }
  121. })

四、回顾

我们再来通过一张图回顾一下整个过程:

从上可以看出,大概的过程是这样的:

  1. 定义Vue对象,声明vue的data里面的属性值,准备初始化触发observe方法。
  2. 在Observe定义过响应式方法Object.defineProperty()的属性,在初始化的时候,通过Watcher对象进行addDep的操作。即每定义一个vue的data的属性值,就添加到一个Watcher对象到订阅者里面去。
  3. 每当形成一个Watcher对象的时候,去定义它的响应式。即Object.defineProperty()定义。这就导致了一个Observe里面的getter&setter方法与订阅者形成一种依赖关系。
  4. 由于依赖关系的存在,每当数据的变化后,会导致setter方法,从而触发notify通知方法,通知订阅者我的数据改变了,你需要更新。
  5. 订阅者会触发内部的update方法,从而改变vm实例的值,以及每个Watcher里面对应node的nodeValue,即视图上面显示的值。
  6. Watcher里面接收到了消息后,会触发改变对应对象里面的node的视图的value值,而改变视图上面的值。
  7. 至此,视图的值改变了。形成了双向绑定MVVM的效果。

五、后记

至此,我们通过解析vue的绑定原理,实现了一个非常简单的Vue。

我们可以再借鉴此思路的情况下,进行我们需要的定制框架的二次开发。如果开发人数尚可的话,可以实现类似微信小程序自己有的一套框架。

我非常重视技术的原理,只有真正掌握技术的原理,才能在原有的技术上更好地去提高和开发。

ps:此文是较早之前写的,不够规范,后面会修改一个ES6的版本。下方是参考链接,灵感来源于其他博主,我进行了修正优化和代码解释。

参考链接:

  1. Vue.js双向绑定的实现原理
  2. Vue 源码解析:深入响应式原理
  3. 深入响应式原理

原文地址(原创博客):http://www.tangyida.top/detail/150

Vue原理解析——自己写个Vue的更多相关文章

  1. [转] Vue原理解析——自己写个Vue

    一.Vue对比其他框架原理 Vue相对于React,Angular更加综合一点.AngularJS则使用了“脏值检测”. React则采用避免直接操作DOM的虚拟dom树.而Vue则采用的是 Obje ...

  2. vue 脚手架 立即可以写业务 vue + vue-router + less + axios + elementUI + moment

    https://github.com/cynthiawupore/wq-cli

  3. 对Vue中的MVVM原理解析和实现

    对Vue中的MVVM原理解析和实现 首先你对Vue需要有一定的了解,知道MVVM.这样才能更有助于你顺利的完成下面原理的阅读学习和编写 下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章 ...

  4. Vue双向数据绑定原理解析

    基本原理 Vue.采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,数据变动时发布消息给订阅者,触发相应函数的回调 ...

  5. vue.js响应式原理解析与实现

    vue.js响应式原理解析与实现 从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很 ...

  6. vue响应式原理解析

    # Vue响应式原理解析 首先定义了四个核心的js文件 - 1. observer.js 观察者函数,用来设置data的get和set函数,并且把watcher存放在dep中 - 2. watcher ...

  7. 「进阶篇」Vue Router 核心原理解析

    前言 此篇为进阶篇,希望读者有 Vue.js,Vue Router 的使用经验,并对 Vue.js 核心原理有简单了解: 不会大篇幅手撕源码,会贴最核心的源码,对应的官方仓库源码地址会放到超上,可以配 ...

  8. vue 数组遍历方法forEach和map的原理解析和实际应用

    一.前言 forEach和map是数组的两个方法,作用都是遍历数组.在vue项目的处理数据中经常会用到,这里介绍一下两者的区别和具体用法示例. 二.代码 1. 相同点 都是数组的方法 都用来遍历数组 ...

  9. 谈谈我对前端组件化中“组件”的理解,顺带写个Vue与React的demo

    前言 前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的APP的话分工会更细,比如携程: 携程app = 机票频道 + 酒店频道 + 旅游频道 + ..... ...

随机推荐

  1. 阶段3 2.Spring_03.Spring的 IOC 和 DI_2 spring中的Ioc前期准备

    适应配置的方式解决我们刚才的编码操作 -dist结尾的就是spring 的开发包 解压好的 这里面是约束 libs是扎包 三个为一组,实际上只有21个 自己在使用需要导入jar包的时候,选择这种没有任 ...

  2. 十三:jinja2过滤器之default过滤器和or过滤器

    在模板里面有时候需要对传过来的数据进行一些处理,jinja2有一些内置的过滤器可以进行处理.类似于python内置函数,通过 “|” 进行使用,详见jinja2官方文档 使用方法:{{ 变量名|过滤器 ...

  3. Linux 查找当前目录下 包含特定字符串 的所有文件

    使用 Linux 经常会遇到这种情况:只知道文件中包含某些特定的字符串,但是不知道具体的文件名.需要根据“特定的字符串”反向查找文件. 示例(路径文件如下): ./miracle/luna/a.txt ...

  4. Altera DDR2 IP核学习总结3-----------DDR2 IP核的使用

    根据上一篇生成的IP核,例化之后如上图,Local开头的数据是用户侧数据,其他数据暂时不用纠结,不用管. 这些是需要关注的信号,但是初学阶段很难对这些信号形成具体的概念,这里参考明德扬的代码进行二次封 ...

  5. 手写朴素贝叶斯(naive_bayes)分类算法

    朴素贝叶斯假设各属性间相互独立,直接从已有样本中计算各种概率,以贝叶斯方程推导出预测样本的分类. 为了处理预测时样本的(类别,属性值)对未在训练样本出现,从而导致概率为0的情况,使用拉普拉斯修正(假设 ...

  6. SSM004/工作内容

    一.java执行sql脚本 参考博客:java调用SQL脚本执行的方案 1.Service层代码:目的随spring容器启动即执行 //Service层代码 @Component public cla ...

  7. 将Lambda表达式作为参数传递并解析-在构造函数参数列表中使用Lambda表达式

    public class DemoClass { /// <summary> /// 通过Lambda表达式,在构造函数中赋初始值 /// </summary> /// < ...

  8. python 并发编程 协程 gevent模块

    一 gevent模块 gevent应用场景: 单线程下,多个任务,io密集型程序 安装 pip3 install gevent Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步 ...

  9. 【转】MySQL-Utilities,mysql工具包

    原文:https://blog.csdn.net/leshami/article/details/52795777 MySQL Utilities 是一组基于python语言编写的python库的命令 ...

  10. spark 运行报错:java.lang.AbstractMethodError

    报错日志如下: Caused by: java.lang.AbstractMethodError: sparkCore.JavaWordCount$2.call(Ljava/lang/Object;) ...