本文来自网易云社区

前瞻

当前前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Angular、Regular、Vue、React等等,它们最大的优点就是可以实现数据绑定,再也不需要手动进行DOM操作了,它们实现的原理也基本上是脏检查或数据劫持。那么本文就以Vue框架出发,探索作者运用Object.defineProperty来实现数据劫持的奥秘(本文所选取的相关代码源自于Vue v2.0.3版本的源码)。

回顾一下Object.defineProperty

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

  • 参数
     obj:目标对象
     prop:需要定义的属性或方法的名称
     descriptor:目标属性所拥有的特性

  • 可供定义的特性列表
     value:属性的值
     writable:如果为false,属性的值就不能被重写。
     get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
     set:一旦目标属性被赋值,就会调回此方法。
     configurable:如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable,    configurable, enumerable)的行为将被无效化。
     enumerable:是否能在for...in循环中遍历出来或在Object.keys中列举出来。

什么是数据劫持

通过上面对Object.defineProperty的介绍,我们不难发现,当我们访问或设置对象的属性的时候,都会触发相对应的函数,然后在这个函数里返回或设置属性的值。既然如此,我们当然可以在触发函数的时候动一些手脚做点我们自己想做的事情,这也就是“劫持”操作。在Vue中其实就是通过Object.defineProperty来劫持对象属性的setter和getter操作,并“种下”一个监听器,当数据发生变化的时候发出通知。先简单的举个例子:

var data = {
    name:'lhl'}
Object.keys(data).forEach(function(key){
    Object.defineProperty(data,key,{
        enumerable:true,
        configurable:true,
        get:function(){
            console.log('get');
        },
        set:function(){
            console.log('监听到数据发生了变化');
        }
    })
});
data.name //控制台会打印出 “get”data.name = 'hxx' //控制台会打印出 "监听到数据发生了变化"

上面的这个例子可以看出,我们完全可以控制对象属性的设置和读取。在Vue中,作者在很多地方都非常巧妙的运用了Object.defineProperty这个方法,具体用在哪里并且它又解决了哪些问题,下面就做详细的介绍:

监听对象属性的变化

这个应该是Vue敲开数据绑定的前大门,它通过observe每个对象的属性,添加到订阅器dep中,当数据发生变化的时候发出一个notice。 相关源代码如下:(作者采用的是ES6+flow写的,代码在src/core/observer/index.js模块里面)

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep()//创建订阅对象
  const property = Object.getOwnPropertyDescriptor(obj, key)//获取obj对象的key属性的描述  //属性的描述特性里面如果configurable为false则属性的任何修改将无效  if (property && property.configurable === false) {
    return  }   // cater for pre-defined getter/setters  const getter = property && property.get  const setter = property && property.set
  let childOb = observe(val)//创建一个观察者对象  Object.defineProperty(obj, key, {
    enumerable: true,//可枚举    configurable: true,//可修改    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val//先调用默认的get方法取值      //这里就劫持了get方法,也是作者一个巧妙设计,在创建watcher实例的时候,通过调用对象的get方法往订阅器dep上添加这个创建的watcher实例      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value//返回属性值    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val//先取旧值      if (newVal === value) {
        return      }
      //这个是用来判断生产环境的,可以无视      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)//继续监听新的属性值      dep.notify()//这个是真正劫持的目的,要对订阅者发通知了    }
  })
}

以上是Vue监听对象属性的变化,那么问题来了,我们经常在传递数据的时候往往不是一个对象,很有可能是一个数组,那是不是就没有办法了呢,答案显然是否则的。那么下面就看看作者是如何监听数组的变化:

监听数组的变化

我们还看先看这段源码:

const arrayProto = Array.prototype//原生Array的原型export const arrayMethods = Object.create(arrayProto)

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse']
.forEach(function (method) {
  const original = arrayProto[method]//缓存元素数组原型  //这里重写了数组的几个原型方法  def(arrayMethods, method, function mutator () {
    //这里备份一份参数应该是从性能方面的考虑    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    const result = original.apply(this, args)//原始方法求值    const ob = this.__ob__//这里this.__ob__指向的是数据的Observer    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break      case 'unshift':
        inserted = args
        break      case 'splice':
        inserted = args.slice(2)
        break    }
    if (inserted) ob.observeArray(inserted)
    // notify change    ob.dep.notify()
    return result
  })
}) ...//定义属性function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true  });
}

上面的代码主要是继承了Array本身的原型方法,然后又做了劫持修改,可以发出通知。Vue在observer数据阶段会判断如果是数组的话,则修改数组的原型,这样的话,后面对数组的任何操作都可以在劫持的过程中控制。结合Vue的思想,我简单的写个小demo方便更好的理解:

var arrayMethod = Object.create(Array.prototype);
['push','shift'].forEach(function(method){
    Object.defineProperty(arrayMethod,method,{
        value:function(){
            var i = arguments.length
            var args = new Array(i)
            while (i--) {
              args[i] = arguments[i]
            }
            var original = Array.prototype[method];
            var result = original.apply(this,args);
            console.log("已经控制了,哈哈");
            return result;
        },
        enumerable: true,
        writable: true,
        configurable: true    })
})var bar = [1,2];
bar.__proto__ = arrayMethod;
bar.push(3);//控制台会打印出 “已经控制了,哈哈”;并且bar里面已经成功的添加了成员 ‘3’

整个过程看起来好像没有什么问题,似乎Vue已经做到了完美,其实不然,Vue还是不能检测到数据项和数组长度改变的变化,例如下面的调用:

vm.items[index] = "xxx";
vm.items.length = 100;

我们尽量避免这样的调用方式,如果确实需要,作者也帮我们实现了一个$set操作,这里就不做介绍了。

实现对象属性代理

正常情况下我们是这样实例化一个Vue对象:

var VM = new Vue({    data:{        name:'lhl'    },    el:'#id'})

按理说我们操作数据的时候应该是VM.data.name = ‘hxx’才对,但是作者觉得这样不够简洁,所以又通过代理的方式实现了VM.name = ‘hxx’的可能。 相关代码如下:

function proxy (vm, key) {
  if (!isReserved(key)) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return vm._data[key]
      },
      set: function proxySetter (val) {
        vm._data[key] = val;
      }
    });
  }
}

表面上看起来我们是在操作VM.name,实际上还是通过Object.defineProperty()中的get和set方法劫持实现的。

总结

Vue框架很好的利用了Object.defineProperty()这个方法来实现了数据的双向绑定,同时也达到了很好的模块间解耦,在日常开发中,你也可以用好这个方法来优化对象获取和修改属性方式,或者自己实现一个MVVM的双向数据绑定等。

本篇文章是我对Vue的浅薄之悟,如有理解不足之处,还请大家批评指正,Thank you ~

本文来自网易云社区,经作者黎浩梁授权发布。

原文:Vue框架核心之数据劫持

新用户大礼包:https://www.163yun.com/gift

更多网易研发、产品、运营经验分享请访问网易云社区

Vue框架核心之数据劫持的更多相关文章

  1. [Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅

    有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫 ...

  2. Vue 核心之数据劫持

    前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Angular.Regular.Vue.React等等,它们最大的优点就是可以实现数据绑定,再也不需要手动进行DOM操作了,它们实现 ...

  3. Vue核心之数据劫持

    前瞻 当前前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Anglar,Regular,Vue,React等等,它们最大的优点就是可以实现数据绑定,再也不需要手动进行DOM操作了, ...

  4. 搞懂:MVVM模型以及VUE中的数据绑定数据劫持发布订阅模式

    搞懂:MVVM模式和Vue中的MVVM模式 MVVM MVVM : model - view - viewmodel的缩写,说都能直接说出来 model:模型,view:视图,view-Model:视 ...

  5. vue双向绑定(数据劫持+发布者-订阅者模式)

    参考文献:https://www.cnblogs.com/libin-1/p/6893712.html 实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据. 关键点在于data如何更新v ...

  6. 从实体框架核心开始:构建一个ASP。NET Core应用程序与Web API和代码优先开发

    下载StudentApplication.Web.zip - 599.5 KB 下载StudentApplication.API.zip - 11.5 KB 介绍 在上一篇文章中,我们了解了实体框架的 ...

  7. 直播课(1)如何通过数据劫持实现Vue(mvvm)框架

    19.6.28更新: 这篇博客比较完善:将每一部分都分装在单独的js文件中: 剖析Vue原理&实现双向绑定MVVM 半个月前看的直播课,现在才自己敲了一遍,罪过罪过 预览: 思路: 简单实现V ...

  8. Vue之九数据劫持实现MVVM的数据双向绑定

    vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的. 如果不熟悉defineProperty,猛 ...

  9. Vue框架(一)——Vue导读、Vue实例(挂载点el、数据data、过滤器filters)、Vue指令(文本指令v-text、事件指令v-on、属性指令v-bind、表单指令v-model)

    Vue导读 1.Vue框架 vue是可以独立完成前后端分离式web项目的js框架 三大主流框架之一:Angular.React.Vue vue:结合其他框架优点.轻量级.中文API.数据驱动.双向绑定 ...

随机推荐

  1. Python机器学习神器:sklearn&numpy

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDE0MDMzOA==/font/5a6L5L2T/fontsize/400/fill/I0JBQk ...

  2. 七.部署war包到Tomcat(基于Centos安装)

    1.把war包上传至tomcat的webapps目录下面 2.启动Tomcat,在Tomcat的bin目录下面,运行startup.sh 3.访问项目,如下成功打开项目

  3. BZOJ3940:[USACO]Censoring(AC自动机,栈)

    Description Farmer John has purchased a subscription to Good Hooveskeeping magazine for his cows, so ...

  4. PHP-----JSOM类型数据

    JS里的数据类型 JS里的一种数据类型,JSOM类型数据 JSOM这种数据类型,在使用JS和jquery时经常使用的到,比较重要.用起来比较简单. <title>无标题文档</tit ...

  5. HDU 1006 Tick and Tick(时钟,分钟,秒钟角度问题)

    传送门: http://acm.hdu.edu.cn/showproblem.php?pid=1006 Tick and Tick Time Limit: 2000/1000 MS (Java/Oth ...

  6. Entity Framework code first设置不在数据库中生成外键

    你现在用的EF是什么版本?我用EF6,你可以重写SqlServerMigrationSqlGenerator的生成外键和更新外键的方法,把不需要的表都过滤掉不就ok了? public class Ex ...

  7. Python嵌套列表去重

    raw_list = [ [ 'CS_SUPP_INFO', 'A', '1'], [ 'CS_SUPP_INFO', '1', 'A'], [ 'CS_SUPP_INFO', '1', 'A'], ...

  8. Spring Boot集成Hazelcast实现集群与分布式内存缓存

    Hazelcast是Hazelcast公司开源的一款分布式内存数据库产品,提供弹性可扩展.高性能的分布式内存计算.并通过提供诸如Map,Queue,ExecutorService,Lock和JCach ...

  9. [Medium翻译]RESTful API权威设计指南-设计更好的API

    本文为授权译文.希望查看原文的同学请戳链接:https://hackernoon.com/restful-api-design-step-by-step-guide-2f2c9f9fcdbf 对于我们 ...

  10. mysql数据库迁移到oracle数据库后 如何删除相同的数据

    mysql数据库迁移到oracle数据库后 如何删除相同的数据 首先搞清楚有多少数据是重复的 select pid from product group by pid having count(pid ...