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

@TOChtml

杀生丸.jpg


写在前面

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

了解3.0的进步,咱们得先了解2.0的响应式原理,若是已经知道其优点劣势的大佬自行跳过~~react

vue2.0响应式源码实现

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

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

原理:使用 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函数以下:bash

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()方法;不就可以让对象主动发出通知了吗?app

依赖收集.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 = '男'

复制代码

输出结果以下: ide

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>
复制代码
相关文章
相关标签/搜索