你真的了解vue吗?vue2.0响应式源码实践

@TOC

杀生丸.jpg

写在前面

震惊!!! 2019年10月5日,尤小右公开了 Vue 3.0 的源代码。源码地址:vue-next,此次更新的主要内容除了自行查看源码还可以在知乎上进行了解尤小右 3.0 RFC,在这两篇的基础上,接下来我将为大家展示最近学习到3.0的内容解读

了解3.0的进步,我们得先了解2.0的响应式原理,如果已经知道其优势劣势的大佬自行跳过~~

vue2.0响应式源码实现

看过官方文档的同学都知道Vue 响应式系统的解释: 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因

下面我们将大概先实现vue2.0响应

原理:使用 Object.defineProperty 可以重新定义属性,并且给属性增加 getter 和setter;

1. 先创建一个对象

// 我们先创建一个对象,然后通过某个方法去监听这个对象,当对象的值改变时,触发操作
let defalutName = ''
let data = {name:''}
// observer监听函数
observer(data)
console.log(data.name);
// expected output: Magic Eno

// 给data里面的name赋值 = "Eno"
data.name = 'Eno'
console.log(data.name);   // expected output: Eno

console.log(defalutName); // expected output: Eno

2.实现observer方法

observer的效果要求很简单,就是监听data对象,当data里面的属性值改变时,监听到其改变;
下面实现一个简陋的双向数据绑定,即data的name改变时,defaultName也要改变,实现双向数据绑定,即defalutName与data对象的name双向绑定了

let defalutName = ''
let data = {name:''};

function observer (data) {
  //Object.defineProperty直接在对象上定义新属性,或修改对象上的现有属性,然后返回对象。
//不了解的请转MDN文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, 'name', {
    get(){
      return defalutName
    },
    set(newValue){
      defalutName = newValue
    }
  });
}
// 
observer(data)
console.log(data.name);
// expected output: ''

// 给data里面的name赋值 = "Eno"
data.name = 'Eno'
console.log(data.name);   // expected output: Eno

console.log(defalutName); // expected output: Eno

3.接下来我们对observer函数进行改造

上面我们的observer对象并没有对data的所有值进行监听,接下来我们完善oberver函数如下:


function observer(data){
  // 判断是否为对象 如果不是则直接返回,Object.defineProperty是对象上的属性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  for(let key in data){
    defineReactive(data,key,data[key]);
  }
}
function defineReactive(data,key,value){
  //Object.defineProperty直接在对象上定义新属性,或修改对象上的现有属性,然后返回对象。
  //不了解的请转MDN文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
      get(){
         console.log('获取了值') // 在此做依赖收集的操作
          return value 
      },
      set(newValue){
          if(newValue !== value){
              console.log('设置了值')
              value = newValue
          }
      }
  });
}

let data = {name:'',age:18};
observer(data)

// 给data里面的name赋值 = "Eno"
data.name = 'Eno'
console.log(data.name);  
// 设置了值
// 获取了值
// Eno
 data.age = 12
 console.log(data.age); 
// 设置了值
// 获取了值
// 12
  • 输入如图


    image.png

    此时更新data里面的所有值都触发了defineProperty 的get和set方法

补充:什么是依赖收集? 我们都知道,当一个可观测对象的属性被读写时,会触发它的getter/setter方法。如果我们可以在可观测对象的getter/setter里面,执行监听器里面的update()方法;不就能够让对象主动发出通知了吗?

依赖收集.png

4. 假如给data添加不存在key会如何呢?

// ...
let data = {name:'',age:18};
observer(data)

// 给data里面的name赋值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
data.gender = '男'

输出结果如下:


image.png

由图可知,并没有触发set和get,这个因为,在我们对data进行监测的时候是没有gender这个属性值的,因此我们如果想要对新增的属性进行监听的话,需要在赋值后再进行一次监听,即vm.$set的效果;我们可以创建一个reactiveSet函数如下:

function reactiveSet (data,key,value) {
  data[key] = value
  observer(data)
}


let data = {name:'',age:18};
observer(data)

// 给data里面的name赋值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// 通过reactiveSet添加属性
reactiveSet(data,'gender','男')
console.log(data.gender)

执行结果如下:


image.png

此时是可以响应的,不过vue并不是这样做的,下面可以看vue的源码
vuejs in github
里面是这样判断的,如果这个key目前没有存在于对象中,那么会进行赋值并监听。但是这里省略了ob的判断;

补充: ob是什么呢? vue初始化的数据(如data中的数据)在页面初始化的时候都会被监听,而被监听的属性都会被绑定ob属性,下图就是判断这个数据有没有被监听的。如果这个数据没有被监听,那么就默认你不想监听这个数据,所以直接赋值并返回

image.png

5. 假如data里面的数据是多层嵌套对象呢?

目前,我们是对data一个简单对象进行监听,思考🤔一下假如是多层对象该如何调整及修改observer呢?

  • 其实很简单, 我们只需要在observer调用的defineReactive函数里边对value值进行递归监听就可以实现,但这种方式,会有一定性能问题;defineReactive修改如下:
// ...
function defineReactive(data,key,value){
  observer(value); // 递归 继续对当前value进行拦截
  
  //Object.defineProperty直接在对象上定义新属性,或修改对象上的现有属性,然后返回对象。
  //不了解的请转MDN文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
      get(){
          console.log('获取了值') // 在此做依赖收集的操作
          return value 
      },
      set(newValue){
          if(newValue !== value){
              // 对于新增的值也需要监听
              observer(newValue)
              console.log('设置了值')
              value = newValue
          }
      }
  });
}
// ...

2019年10月21日更新

6. 假如data里面的数据是多层嵌套数组呢?

假如data里面的对象里面有数组,那么需要对数组进行拦截,如果数组里面是多维数组,还需和5.嵌套对象的做法一致,还需要进行递归监听,observer修改如下:

function observer(data){
  // 判断是否为对象 如果不是则直接返回,Object.defineProperty是对象上的属性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  if(Array.isArray(data)){ // 如果是数组,则对数据进行遍历并对其value进行递归监听
    for(let i = 0; i< data.length ;i++){
        observer(data[i]);
    }
  } else {
    for(let key in data){
      defineReactive(data,key,data[key]);
    }
  }
}
// ...
// ...

let data = {name:'',age:18};
observer(data)

// 给data里面的name赋值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// reactiveSet(data,'gender','男')
// console.log(data.gender)


reactiveSet(data,'attr',[1,2,3,4,5,100])
console.log(data.attr)
  • 执行输出结果如下:


    image.png

从上图可以看出,在reactiveSet的情况下,即使给data设置了不存在的数组,也能够得到监听,接下来尝试对数组进行修改测试;

// ...
let data = {name:'',attr:[1,2,3,4,5,100]};
observer(data)

// 给data里面的name赋值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// reactiveSet(data,'gender','男')
// console.log(data.gender)
// console.log(data.attr)
data.attr.push(10000); 
data.attr.splice(0,1); 
  • 结果输出如下: 此时对数据push或者删除其中某个元素,很明显observer并未监测到其变化:
    image.png

究其原因:由于数组的方法在对数组的增删查改过程中,vue并没其操作更新视图的操作,故此时是不能响应式的,因此如果需要对此类方法的调用时,通过视图更新,则需要对数组方法重写,查看vue/array.js源码可知:

image.png
  • 其中:def的源码如下: 即对obj的属性进行了重写或者称之为元素的属性重新定义


    image.png
  • 接下来我们写一个简陋版的数组重写:

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(method=>{
  arrayMethods[method] = function(){ 
    // 函数劫持 把函数进行重写 
    // 而内部实际上继续调用原来的方法但在这里我们可以去调用更新视图的方法
     console.log('数组 更新啦...')
      arrayProto[method].call(this,...arguments)
  }
});
  • 并且在observer中设置新的数组方法;

function observer(data){
  // 判断是否为对象 如果不是则直接返回,Object.defineProperty是对象上的属性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  if(Array.isArray(data)){ // 如果是数组,则对数据进行遍历并对其value进行递归监听
    // 在这里对数组方法进行重写 即函数劫持
    Object.setPrototypeOf(data, arrayMethods); 

    for(let i = 0; i< data.length ;i++){
        observer(data[i]);
    }
  } else {
    for(let key in data){
      defineReactive(data,key,data[key]);
    }
  }
}
  • 此时,所有代码如下:
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(method=>{
  arrayMethods[method] = function(){ 
    //函数劫持 把函数进行重写 
    // 而内部实际上继续调用原来的方法但在这里我们可以去调用更新视图的方法
     console.log('数组 更新啦...')
      arrayProto[method].call(this,...arguments)
  }
});


function observer(data){
  // 判断是否为对象 如果不是则直接返回,Object.defineProperty是对象上的属性
  if(typeof data !== 'object' || data == null){
    return data;
  }
  if(Array.isArray(data)){ // 如果是数组,则对数据进行遍历并对其value进行递归监听
    // 在这里对数组方法进行重写 即函数劫持
    Object.setPrototypeOf(data, arrayMethods); 
    for(let i = 0; i< data.length ;i++){
        observer(data[i]);
    }
  } else {
    for(let key in data){
      defineReactive(data,key,data[key]);
    }
  }
}
function defineReactive(data,key,value){
  observer(value); // 递归 继续对当前value进行拦截

  //Object.defineProperty直接在对象上定义新属性,或修改对象上的现有属性,然后返回对象。
  //不了解的请转MDN文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
      get(){
          console.log('获取了值') // 在此做依赖收集的操作
          return value 
      },
      set(newValue){
          if(newValue !== value){
              // 对于新增的值也需要监听
              observer(newValue)
              console.log('设置了值')
              value = newValue
          }
      }
  });
}

function reactiveSet (data,key,value) {
  data[key] = value
  observer(data)
}


let data = {name:'',attr:[1,2,3,4,5,100]};
observer(data)

// 给data里面的name赋值 = "Eno"
// data.name = 'Eno'
// console.log(data.name);  
//  data.age = 12
//  console.log(data.age); 
// reactiveSet(data,'gender','男')
// console.log(data.gender)
// console.log(data.attr)
data.attr.push(10000); 
data.attr.splice(0,1); 

运行结果如下:


image.png

看到视图真的更新了,不得不佩服尤大大真的厉害,听说2020年第一季度就要出vue3.0了,接下来要写篇vue3.0的初步学习文章,希望各位看官支持,不要忘记点赞喔;

文章源码:github wLove-c


总结: 能力有限,暂时先写这么多,接下来有时间会写一篇vue-next的源码实践和理解, 希望各位看官大人不要忘记点赞哈,写的不好的地方欢迎指正;

vue3.0使用小测

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script src="vue.global.js"></script>
  <script>
    console.log('Vue====',Vue)
    const App = {
        setup() {
          // reactive state
          let count =  Vue.reactive({value:1}) // 知乎上尤大大推荐的是使用 const count = value(0) 但目前这个版本是没有value的 先用reactive做响应
          // computed state
          const plusOne = Vue.computed(() => count.value * 2)
          // method
          const increment = () => {
             count.value++ 

            }
          // watch
          Vue.watch(() => count.value * 2, val => {
            console.log(`value * 2 is ${val}`)
          })
          // lifecycle
          Vue.onMounted(() => {
            console.log(`mounted`)
          })
          // expose bindings on render context
          return {
            count,
            plusOne,
            increment
          }
        },
        template: `
          <div>
            <div>count is {{ count.value }}</div>
            <span>plusOne is {{ plusOne }}</span>
            <button @click="increment">count++</button>
          </div>
        `,
      }
    Vue.createApp().mount(App,app)
  </script>
</body>
</html>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269