前言

最近准备把5月份面试的时候被问到的问题整理一下,本来是想写在面试总结篇里面的,但是有些知识点还是值得我单独写一篇来记录的。于是便有了 XXX面试官:小勾同学来说说XXX 系列文章。这个系列文章涉及的内容含有本人经历的面试笔试题,也有网上看的面经,觉得可以深挖的问题,便记录了下来。参考文章

正文

  1. 说一说Vue 如何实现响应式?

    答:Vue 通过发布者订阅者的设计模式来实现对数据的绑定,当数据更新时,会触发视图的变化。Vue 实现响应式具体流程如下。

    1. Observer:对数据对象进行遍历,包括子属性对象的属性,利用 object.defineProperty() 对属性绑定 gettersetter 当数据改变时,会触发 setter ,那么就能监听到数据变化。
    2. Compile:可以监听到数据的变化了,那么如何对视图进行更新呢?这便是 Vue 巧妙之处了,Vue 中有很多模板指令,通过解析模板指令,将模板中的变量都替换为数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知调用更新进行数据更新。
    3. Watcher:数据如何更新知道了,视图如何渲染也知道了。那么数据更新如何让页面也渲染呢?这边运用到订阅者模式了。Vue 通过在 ObserverCompile 之间添加一个桥梁——Watcher ,它的主要功能就是订阅 Observer 中的属性值的变化消息,当收到属性值变化的消息时,触发解析器 Compile中对应的更新函数。
    4. Dep:订阅器采用发布-订阅设计模式 ,用来收集订阅者 Watcher ,对监听器 Observer 和订阅者 Watcher 进行统一管理。

    从第一步可以看出,我们只对对象进行监听了,那么数组如何监听呢?

    通过遍历数组,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。

    下面是一个简单的 Vue 设计原理代码:

    //触发更新视图
    function updateView() {
        console.log('视图更新')
    }
    
    //监听数组
    //重新定义数组原型
    const OldArrayPropetry = Array.prototype
    //创建新对象,原型指向OldArrayPropetry,这样再扩展新的方法不会影响原型
    const arrProto = Object.create(OldArrayPropetry);
    ['push', 'pop'].forEach(item => {
        arrProto[item] = function () {
            updateView()
            OldArrayPropetry[item].call(this, ...arguments)
        }
    })
    
    //重新定义属性,监听起来
    function defineReactvie(target, key, value) {
        //深度监听
        observer(value)
        //核心API
        Object.defineProperty(target, key, {
            get() {
                return value
            },
            set(newValue) {
                if (newValue !== value) {
                    //深度监听
                    observer(newValue)
                    value = newValue
                    updateView()
                }
            }
        })
    }
    
    //监听对象属性
    function observer(target) {
        if (typeof target !== 'object' || target === null) {
            return target
        }
        if (Array.isArray(target)) {
            target.__proto__ = arrProto
        }
        for (let key in target) {
            defineReactvie(target, key, target[key])
        }
    }
    
    //数据
    const data = {
        name: "kweku",
        age: 21,
        info: {
            city: '成都'
        },
        nums: []
    }
    
    observer(data)
    // data.name = 'tom'
    // data.info.city = 'city'
    data.nums.push(1)
  1. 观察 Vue2.x 的实现原理,发现有一个属性在里面体现得十分关键——Object.defineProperty,也是本文将深挖的重点。

    1. 对象的定义与赋值

      经常使用的定义方法与赋值方法 obj.prop=value 或者 obj['prop']=value

    2. Object.defineProperty() 语法

      Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性

      Object.defineProperty(obj, prop, desc)
      • obj 需要定义属性的当前对象
      • prop 当前需要定义的属性名
      • desc 属性描述符

      一般通过为对象的属性赋值的情况下,对象的属性可以修改也可以删除,但是通过Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性。

    3. 属性的特性以及内部属性

      javacript 有三种类型的属性:

      • 命名数据属性:拥有一个确定的值的属性。这也是最常见的属性
      • 命名访问器属性:通过gettersetter进行读取和赋值的属性
      • 内部属性:由JavaScript引擎内部使用的属性,不能通过JavaScript代码直接访问到,不过可以通过一些方法间接的读取和设置。比如,每个对象都有一个内部属性[[Prototype]],你不能直接访问这个属性,但可以通过Object.getPrototypeOf()方法间接的读取到它的值。虽然内部属性通常用一个双吕括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可见的,根本没有上面两种属性有的那种字符串类型的属性
    4. 属性描述符

      通过 Object.defineProperty() 为对象定义属性,有两种形式,且不能混合使用,分别为数据描述符,存取描述符,下面分别描述下两者的区别:

      数据描述符:特有的两个属性:1、value ;2、writable

      let Person = {}
      Object.defineProperty(Person, 'name', {
         value: 'jack',
         writable: true // 是否可以改变
      })

      image-20200708223917100

      image-20200708223935095

      如果描述符中的某些属性被省略,会使用一下默认规则:

      image-20200708224112375

    5. 存取描述符:是由一对getter、setter函数功能来描述的属性

      get:一个给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为 undefined

      set:一个给属性提供setter的方法,如果没有setter则为undefine。该方法接收唯一参数,并将该参数的新值分配给该属性,默认值为undefined。

      let Person = {}
      let temp = null
      Object.defineProperty(Person, 'name', {
        get: function () {
          return temp
        },
        set: function (val) {
          temp = val
        }
      })

      image-20200708224436217

    6. 存取描述符和数据描述符均有以下描述符。

      • configrable 描述属性是否配置,以及可否删除
      • enumerable 描述属性是否会出现在for in 或者 Object.keys()的遍历中

      image-20200708224613759

      image-20200708224640215

      image-20200708224658665

      image-20200708224708216

      image-20200708224716472

      image-20200708224725384

      image-20200708224733848

      从以上代码运行结果分析总结可知:

      1. configurable: false 时,不能删除当前属性,且不能重新配置当前属性的描述符(有一个小小的意外:可以把writable的状态由true改为false,但是无法由false改为true),但是在writable: true的情况下,可以改变value的值

      2. configurable: true时,可以删除当前属性,可以配置当前属性所有描述符。

    7. 不变性

      1. 对象常量:结合writable: false 和 configurable: false 就可以创建一个真正的常量属性(不可修改,不可重新定义或者删除)
        image-20200708230630315

      2. 禁止扩展:如果你想禁止一个对象添加新属性并且保留已有属性,就可以使用 Object.preventExtensions(...)

        image-20200708230642081

        image-20200708230651643

      3. 密封

        Object.seal()会创建一个密封的对象,这个方法实际上会在一个现有对象上调用object.preventExtensions(...)并把所有现有属性标记为configurable:false

        image-20200708230846458

        所以, 密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以改属性的值)

      4. Object.freeze()会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(),并把所有现有属性标记为writable: false,这样就无法修改它们的值。

        image-20200708231203138

总结

Object.defineProperty()Vue2.x 中使用的,但是在3.x 版本已经替换为了 Object.proxy() 方法。后面我再新开一篇文章说说这两种方法有什么优劣。

最后更新: 2020年09月21日 15:46

原始链接: http://www.kweku.top/2020/07/08/07.defineProperty深入浅出/