从JavaScript属性描述器剖析Vue.js响应式视图

特别声明,本文转载@熊建刚的《 从JavaScript属性描述器剖析Vue.js响应式视图
》一文,如需转载,烦请注明原文出处: https://my.oschina.net/u/3451529/blog/918996

学习每一门语言,一般都是从其数据结构开始,JavaScript也是一样,而JavaScript的数据结构中对象( Object
)是最基础也是使用最频繁的概念和语法,坊间有言,JavaScript中,一切皆对象,基本可以描述对象在JavaScript中的地位,而且JavaScript中对象的强大也使其地位名副其实,本篇介绍JavaScript对象属性描述器接口及其在数据视图绑定方向的实践,然后对Vue.js的响应式原理进行剖析。

前言

JavaScript的对象,是一组键值对的集合,可以拥有任意数量的唯一键,键可以是字符串( String
)类型或标记( Symbol
,ES6新增的基本数据类型)类型,每个键对应一个值,值可以是任意类型的任意值。对于对象内的属性,JavaScript提供了一个属性描述器接口 PropertyDescriptor
,大部分开发者并不需要直接使用它,但是很多框架和类库内部实现使用了它,如avalon.js,Vue.js,本篇介绍属性描述器及相关应用。

定义对象属性

在介绍对象属性描述之前,先介绍一下如何定义对象属性。最常用的方式就是使用如下方式:

var a = {
    name: 'jh'
};

// or 
var b = {};
b.name = 'jh';

// or
var c = {};
var key = 'name';
c[key] = 'jh';

本文使用字面量方式创建对象,但是JavaScript还提供其他方式,如, new Object()
Object.create()
,了解更多请查看 对象初始化

Object.defineProperty()

上面通常使用的方式不能实现对属性描述器的操作,我们需要使用 defineProperty()
方法,该方法为一个对象定义新属性或修改一个已定义属性,接受三个参数 Object.defineProperty(obj, prop, descriptor)
,返回值为操作后的对象:

  • obj
    , 待操作对象
  • 属性名
  • 操作属性的属性描述对象

例如:

var x = {}
Object.defineProperty(x, 'count', {})
console.log(x)

由于传入一个空的属性描述对象,所以输出对象属性值为 undefined
,当使用 defineProperty()
方法操作属性时,描述对象默认值为:

  • value
    : undefined
  • set
    : undefined
  • get
    : undefined
  • writable
    : false
  • enumerable
    : false
  • configurable
    : false

不使用该方法定义属性,则属性默认描述为:

  • value
    : undefined
  • set
    : undefined
  • get
    : undefined
  • writable
    : true
  • enumerable
    : true
  • configurable
    : true

默认值均可被明确参数值设置覆盖。

当然还支持批量定义对象属性及描述对象,使用 Object.defineProperties()
方法,如:

var x = {}

Object.defineProperties(x, {
    count: {
        value: 0
    },
    name: {
        value: 'jh'
    }
})

console.log(x)

读取属性描述对象

JavaScript支持我们读取某对象属性的描述对象,使用 Object.getOwnPropertyDescriptor(obj, prop)
方法:

var x = {
    name: 'jh'
}

Object.defineProperty(x, 'count', {})

Object.getOwnPropertyDescriptor(x, 'count')

Object.getOwnPropertyDescriptor(x, 'name')

该实例也印证了上面介绍的以不同方式定义属性时,其默认属性描述对象是不同的。

属性描述对象

PropertyDescriptor
API提供了六大实例属性以描述对象属性,包括: configurable
, enumerable
, get
, set
, value
, writable

value

指定对象属性值:

var x = {}

Object.defineProperty(x, 'count', {
    value: 0
})

console.log(x)

writable

指定对象属性是否可变:

var x = {}

Object.defineProperty(x, 'count', {
    value: 0
})

console.log(x)

x.count = 1 // 静默失败,不会报错

console.log(x)

使用 defineProperty()
方法时,默认有 writable: false
, 需要显示设置 writable: true

存取器函数( getter
/ setter
)

对象属性可以设置存取器函数,使用 get
声明存取器 getter
函数, set
声明存取器 setter
函数;若存在存取器函数,则在访问或设置该属性时,将调用对应的存取器函数:

get

读取该属性值时调用该函数并将该函数返回值赋值给属性值:

var x = {}

Object.defineProperty(x, 'count', {
    get: function () {
        console.log('读取count属性 +1')
        return 0;
    }
})

console.log(x)

x.count = 1

console.log(x.count)

set

当设置函数值时调用该函数,该函数接收设置的属性值作参数:

var x = {}

Object.defineProperty(x, 'count', {
    set: function (val) {
        this.count = val
    }
})

console.log(x)

x.count = 1

console.log(x.count)

执行上面的代码,会发现报错,执行栈溢出:

上述代码在设置 count
属性时,会调用 set
方法,而在该方法内为 count
属性赋值会再次触发 set
方法,所以这样是行不通的,JavaScript使用另一种方式,通常存取器函数得同时声明,代码如下:

var x = {}

Object.defineProperty(x, 'count', {
    get: function () {
        return this._count
    },
    set: function (val) {
        console.log('设置count属性 +1')
        this._count = val
    }
})

console.log(x)

x.count = 1

// 设置count属性 +1
console.log(x.count)

事实上,在使用 defineProperty()
方法设置属性时,通常需要在对象内部维护一个新内部变量(以下划线 _
开头,表示不希望被外部访问),作为存取器函数的中介。

注:当设置了存取器描述时,不能设置 value
writable
描述。

我们发现,设置属性存取器函数后,我们可以实现对该属性的实时监控,这在实践中很有用武之地,后文会印证这一点。

enumerable

指定对象内某属性是否可枚举,即使用 for in
操作是否可遍历:

var x = {
    name: 'jh'
}

Object.defineProperty(x, 'count', {
    value: 0
})

for (var key in x) {
    console.log(`${key} is ${x[key]}`) // => name is jh
}

上面无法遍历 count
属性,因为使用 defineProperty()
方法时,默认有 enumerable: false
,需要显示声明该描述:

var x = {
    name: 'jh'
}

Object.defineProperty(x, 'count', {
    value: 0,
    enumerable: true
})

console.log(x)

for (var key in x) {
    console.log(`${key} is ${x[key]}`)
}

x.propertyIsEnumerable('count')

configurable

该值指定对象属性描述是否可变:

var x = {}

Object.defineProperty(x, 'count', {
    value: 0,
    writable: false
})

Object.defineProperty(x, 'count', {
    value: 0,
    writable: true
})

执行上述代码会报错,因为使用 defineProperty()
方法时默认是 configurable: false
,输出如图:

修改如下,即可:

var x = {}

Object.defineProperty(x, 'count', {
    value: 0,
    writable: false,
    configurable: true
})

x.count = 1

console.log(x.count)

Object.defineProperty(x, 'count', {
    writable: true
})

x.count = 1

console.log(x.count)

属性描述与视图模型绑定

介绍完属性描述对象,我们来看看其在现代JavaScript框架和类库上的应用。目前有很多框架和类库实现数据和DOM视图的单向甚至双向绑定,如React,Angular.js,Avalon.js,Vue.js等,使用它们很容易做到对数据变更进行响应式更新DOM视图,甚至视图和模型可以实现双向绑定,同步更新。当然这些框架、类库内部实现原理主要分为三大阵营。本文以Vue.js为例,Vue.js是当下比较流行的一个响应式的视图层类库,其内部实现响应式原理就是本文介绍的属性描述在技术中的具体应用。

可以 点击此处
,查看一个原生JavaScript实现的简易数据视图单向绑定实例,在该实例中,点击按钮可以实现计数自增,在输入框输入内容会同步更新到展示DOM,甚至在控制台改变 data
对象属性值,DOM会响应更新,如图:

点击查看完整实例代码

数据视图单向绑定

现有如下代码:

var data = {}
var contentEl = document.querySelector('.content')

Object.defineProperty(data, 'text', {
    writable: true,
    configurable: true,
    enumerable: true,

    get: function () {
        return contentEl.innerHTML
    },

    set: function (val) {
        contentEl.innerHTML = val
    }
})

很容易看出,当我们设置 data
对象的 text
属性时,会将该值设置为视图DOM元素的内容,而访问该属性值时,返回的是视图DOM元素的内容,这就简单的实现了数据到视图的单向绑定,即数据变更,视图也会更新。

以上仅是针对一个元素的数据视图绑定,但稍微有经验的开发者便可以根据以上思路,进行封装,很容易的实现一个简易的数据到视图单向绑定的工具类。

抽象封装

接下来对以上实例进行简单抽象封装, 点击查看完整实例代码

首先声明数据结构:

window.data = {
    title: '数据视图单向绑定',
    content: '使用属性描述器实现数据视图绑定',
    count: 0
}

var attr = 'data-on' // => 约定好的语法,声明DOM绑定对象属性

然后封装函数批量处理对象,遍历对象属性,设置描述对象同时为属性注册变更时的回调:

// 为对象中每一个属性设置描述对象,尤其是存取器函数
function defineDescriptors (obj) {
    for (var key in obj) {
        // 遍历属性
        defineDescriptor(obj, key, obj[key])
    }

    // 为特定属性设置描述对象
    function defineDescriptor (obj, key, val) {
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,

            get: function () {
                var value = val
                return value
            },

            set: function (newVal) {
                if (newVal !== val) {
                    // 值发生变更才执行
                    val = newVal
                    Observer.emit(key, newVal) // 触发更新DOM
                }
            }
        })

        Observer.subscribe(key) // 为该属性注册回调
    }
}

管理事件

以发布订阅模式管理属性变更事件及回调:

// 使用发布/订阅模式,集中管理监控和触发回调事件
var Observer = {
    watchers: {},
    subscribe: function (key) {
        var el = document.querySelector('[' + attr + '="' + key + '"]')

        // Demo
        var cb = function react (val) {
            el.innerHTML = val
        }

        if (this.watchers[key]) {
            this.watchers[key].push(cb)
        } else {
            this.watchers[key] = [].concat(cb)
        }
    },
    emit: function (key, val) {
        var len = this.watchers[key] && this.watchers[key].length

        if (len && len > 0) {
            for (var i = 0; i < len; i++) {
                this.watchers[key][i](val)
            }
        }
    }
}

初始化实例

最后初始化实例:

// 初始化DEMO
function init() {
    defineDescriptors(data); // 处理数据对象

    var eles = document.querySelectorAll('[' + attr +']')

    // 初始遍历 DOM 展示数据
    // 其实可以将该操作放到属性描述对象的get方法内
    // 则在初始化时只需要对属性遍历访问即可

    for (var i = 0, len = eles.length; i < len; i++) {
        eles[i].innerHTML = data[eles[i].getAttribute(attr)]
    }

    // 辅助测试实例
    document.querySelector('.add').addEventListener('click', function (e) {
        data.count += 1
    })
}

init()

HTML代码参考如下:

Vue.js的响应式原理

上一节实现了一个简单的数据视图单向绑定实例,现在对Vue.js的响应式单向绑定进行简要分析,主要需要理解其如何追踪数据变更。

依赖追踪

Vue.js支持我们通过 data
参数传递一个JavaScript对象做为组件数据,然后Vue.js将遍历此对象属性,使用 Object.defineProperty
方法设置描述对象,通过存取器函数可以追踪该属性的变更,本质原理和上一节实例差不多,但是不同的是,Vue.js创建了一层 Watcher
层,在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter
被调用时,会通知 Watcher
重新计算,从而使它关联的组件得以更新,如下图:

组件挂载时,实例化 watcher
实例,并把该实例传递给依赖管理类,组件渲染时,使用对象观察接口遍历传入的 data
对象,为每个属性创建一个依赖管理实例并设置属性描述对象,在存取器函数 get
函数中,依赖管理实例添加(记录)该属性为一个依赖,然后当该依赖变更时,触发 set
函数,在该函数内通知依赖管理实例,依赖管理实例分发该变更给其内存储的所有 watcher
实例, watcher
实例重新计算,更新组件。

因此可以总结说Vue.js的响应式原理是 依赖追踪
,通过一个观察对象,为每个属性,设置存取器函数并注册一个依赖管理实例 dep
dep
内为每个组件实例维护一个 watcher
实例,在属性变更时,通过 setter
通知 dep
实例, dep
实例分发该变更给每一个 watcher
实例, watcher
实例各自计算更新组件实例,即 watcher
追踪 dep
添加的依赖, Object.defineProperty()
方法提供这种追踪的技术支持, dep
实例维护这种追踪关系。

源码简单分析

接下来对Vue.js源码进行简单分析,从对JavaScript对象和属性的处理开始:

观察对象(Observer)

首先,Vue.js也提供了一个抽象接口观察对象,为对象属性设置存储器函数,收集属性依赖然后分发依赖更新:

var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();       // 管理对象依赖
    this.vmCount = 0;
    def(value, '__ob__', this); // 缓存处理的对象,标记该对象已处理

    if (Array.isArray(value)) {
        var augment = hasProto
        ? protoAugment
        : copyAugment;
        augment(value, arrayMethods, arrayKeys);
        this.observeArray(value);
    } else {
        this.walk(value);
    }
};

上面代码关注两个节点, this.observeArray(value)
this.walk(value)

若为对象,则调用 walk()
方法,遍历该对象属性,将属性转换为响应式:

Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);

    for (var i = 0; i < keys.length; i++) {
        defineReactive$$1(obj, keys[i], obj[keys[i]]);
    }
};

可以看到,最终设置属性描述对象是通过调用 defineReactive$$1()
方法。

value
为对象数组,则需要额外处理,调用 observeArray()
方法对每一个对象均产生一个 Observer
实例,遍历监听该对象属性:

Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i]);
    }
};

核心是为每个数组项调用 observe
函数:

function observe(value, asRootData) {
    if (!isObject(value)) {
        return // 只需要处理对象
    }

    var ob;

    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__; // 处理过的则直接读取缓存
    } else if (
        observerState.shouldConvert &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue) {
        ob = new Observer(value); // 处理该对象
    }

    if (asRootData && ob) {
        ob.vmCount++;
    }

    return ob
}

调用 ob = new Observer(value);
后就回到第一种情况的结果:调用 defineReactive$$1()
方法生成响应式属性。

生成响应式属性

源码如下:

function defineReactive$$1 (obj,key,val,customSetter) {

    var dep = new Dep(); // 管理属性依赖
    var property = Object.getOwnPropertyDescriptor(obj, key);

    if (property && property.configurable === false) {
        return
    }

    // 之前已经设置了的get/set需要合并调用
    var getter = property && property.get; 
    var setter = property && property.set;

    var childOb = observe(val); // 属性值也可能是对象,需要递归观察处理

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            var value = getter ? getter.call(obj) : val;
            if (Dep.target) { // 管理依赖对象存在指向的watcher实例
                dep.depend(); // 添加依赖(记录)
                if (childOb) { // 属性值为对象
                    childOb.dep.depend(); // 属性值对象也需要添加依赖
                }
                if (Array.isArray(value)) {
                    dependArray(value); // 处理数组
                }
            }
            return value
        },
        set: function reactiveSetter (newVal) {
            var value = getter ? getter.call(obj) : val;

            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return // 未发生变更不需要往后执行
            }

            /* eslint-enable no-self-compare */
            if ("development" !== 'production' && customSetter) {
                customSetter();
            }

            if (setter) {
                setter.call(obj, newVal); // 更新属性值
            } else {
                val = newVal; // 更新属性值
            }

            childOb = observe(newVal); // 每次值变更时需要重新观察,因为可能值为对象
            dep.notify(); // 发布更新事件
        }
    });
}

该方法使用 Object.defineProperty()
方法设置属性描述对象,逻辑集中在属性存取器函数内:

  • get
    : 返回属性值,如果 watcher
    存在,则递归记录依赖;
  • set
    : 属性值发生变更时,更新属性值,并调用 dep.notify()
    方法发布更新事件;

管理依赖

Vue.js需要管理对象的依赖,在属性更新时通知 watcher
更新组件,进而更新视图,Vue.js管理依赖接口采用发布订阅模式实现,源码如下:

var uid$1 = 0;
var Dep = function Dep () {
    this.id = uid$1++; // 依赖管理实例id
    this.subs = []; // 订阅该依赖管理实例的watcher实例数组
};
Dep.prototype.depend = function depend () { // 添加依赖
    if (Dep.target) {
        Dep.target.addDep(this); // 调用watcher实例方法订阅此依赖管理实例
    }
};
Dep.target = null; // watcher实例
var targetStack = []; // 维护watcher实例栈

function pushTarget (_target) {
    if (Dep.target) { targetStack.push(Dep.target); }
    Dep.target = _target; // 初始化Dep指向的watcher实例
}

function popTarget () {
    Dep.target = targetStack.pop();
}

订阅

如之前,生成响应式属性为属性设置存取器函数时, get
函数内调用 dep.depend()
方法添加依赖,该方法内调用 Dep.target.addDep(this)
,即调用指向的 watcher
实例的 addDep
方法,订阅此依赖管理实例:

Watcher.prototype.addDep = function addDep (dep) {

    var id = dep.id;

    if (!this.newDepIds.has(id)) { // 是否已订阅
        this.newDepIds.add(id); // watcher实例维护的依赖管理实例id集合
        this.newDeps.push(dep); // watcher实例维护的依赖管理实例数组

        if (!this.depIds.has(id)) { // watcher实例维护的依赖管理实例id集合
            // 调用传递过来的依赖管理实例方法,添加此watcher实例为订阅者
            dep.addSub(this); 
        }
    }
};

watcher
实例可能同时追踪多个属性(即订阅多个依赖管理实例),所以需要维护一个数组,存储多个订阅的依赖管理实例,同时记录每一个实例的 id
,便于判断是否已订阅,而后调用依赖管理实例的 addSub
方法:

Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub); // 实现watcher到依赖管理实例的订阅关系
};

该方法只是简单的在订阅数组内添加一个订阅该依赖管理实例的 watcher
实例。

发布

属性变更时,在属性的存取器 set
函数内调用了 dep.notify()
方法,发布此属性变更:

Dep.prototype.notify = function notify () {
    // 复制订阅者数组
    var subs = this.subs.slice();
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update(); // 分发变更
    }
};

触发更新

前面提到,Vue.js中由 watcher
层追踪依赖变更,发生变更时,通知组件更新:

Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true;
    } else if (this.sync) { // 同步
        this.run();
    } else { // 异步
        queueWatcher(this); // 最后也是调用run()方法
    }
};

调用 run
方法,通知组件更新:

Watcher.prototype.run = function run () {
    if (this.active) {
        var value = this.get();         // 获取新属性值

        if (value !== this.value ||     // 若值
            isObject(value) || this.deep) {
            var oldValue = this.value;  // 缓存旧值
            this.value = value;         // 设置新值

            if (this.user) {
                try {
                    this.cb.call(this.vm, value, oldValue);
                } catch (e) {
                    handleError(e, this.vm, ("callback for watcher "" + (this.expression) + """));
                }
            } else {
                this.cb.call(this.vm, value, oldValue);
            }
        }
    }
};

调用 this.get()
方法,实际上,后面会看到在该方法内处理了属性值的更新与组件的更新,这里判断当属性变更时调用初始化时传给实例的 cb
回调函数,并且回调函数接受属性新旧值两个参数,此回调通常是对于 watch
声明的监听属性才会存在,否则默认为空函数。

追踪依赖接口实例化

每一个响应式属性都是由一个 Watcher
实例追踪其变更,而针对不同属性( data
, computed
, watch
),Vue.js进行了一些差异处理,如下是接口主要逻辑:

var Watcher = function Watcher (vm,expOrFn,cb,options) {
    this.cb = cb;
    ...
    // parse expression for getter
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = parsePath(expOrFn);
    }
    this.value = this.lazy
        ? undefined
        : this.get();
};

在初始化 Watcher
实例时,会解析 expOrFn
参数(表达式或者函数)成拓展 getterthis.getter
,然后调用 this.get()
方法,返回值作为 this.value
值:

Watcher.prototype.get = function get () {
    pushTarget(this); // 入栈watcher实例

    var value;
    var vm = this.vm;

    if (this.user) {
        try {
            value = this.getter.call(vm, vm); // 通过this.getter获取新值
        } catch (e) {
            handleError(e, vm, ("getter for watcher "" + (this.expression) + """));
        }
    } else {
        value = this.getter.call(vm, vm); // 通过this.getter获取新值
    }

    if (this.deep) { // 深度递归遍历对象追踪依赖
        traverse(value);
    }

    popTarget(); // 出栈watcher实例
    this.cleanupDeps(); // 清空缓存依赖
    return value // 返回新值
};

这里需要注意的是对于 data
属性,而非 computed
属性或 watch
属性,而言,其 watcher
实例的 this.getter
通常就是 updateComponent
函数,即渲染更新组件, get
方法返回 undefined
,而对于 computed
计算属性而言,会传入对应指定函数给 this.getter
,其返回值就是此 get
方法返回值。

data
普通属性

Vue.js的 data
属性是一个对象,需要调用对象观察接口 new Observer(value)

function observe (value, asRootData) {

    if (!isObject(value)) {
        return
    }

    var ob;
    ob = new Observer(value); // 对象观察实例
    return ob;
}

// 初始处理data属性
function initData (vm) {
    // 调用observe函数
    observe(data, true /* asRootData */);
}

计算属性

Vue.js对计算属性处理是有差异的,它是一个变量,可以直接调用 Watcher
接口,把其属性指定的计算规则传递为,属性的拓展 getter
,即:

// 初始处理computed计算属性
function initComputed (vm, computed) {
    for (var key in computed) {
        var userDef = computed[key]; // 对应的计算规则
        // 传递给watcher实例的this.getter -- 拓展getter
        var getter = typeof userDef === 'function' ? userDef : userDef.get; 
        watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions);
    }
}

watch
属性

而对于 watch
属性又有不同,该属性是变量或表达式,而且与计算属性不同的是,它需要指定一个变更事件发生后的回调函数:

function initWatch (vm, watch) {
    for (var key in watch) {
        var handler = watch[key];
        createWatcher(vm, key, handler[i]); // 传递回调
    }
}

function createWatcher (vm, key, handler) {
    vm.$watch(key, handler, options); // 回调
}

Vue.prototype.$watch = function (expOrFn, cb, options) {
    // 实例化watcher,并传递回调
    var watcher = new Watcher(vm, expOrFn, cb, options);
}

初始化 Watcher
与依赖管理接口的连接

无论哪种属性最后都是由 watcher
接口实现追踪依赖,而且组件在挂载时,即会初始化一次 Watcher
实例,绑定到 Dep.target
,也就是将 Watcher
Dep
建立连接,如此在组件渲染时才能对属性依赖进行追踪:

function mountComponent (vm, el, hydrating) {
    ...
    updateComponent = function () {
        vm._update(vm._render(), hydrating);
        ...
    };
    ...
    vm._watcher = new Watcher(vm, updateComponent, noop);
    ...
}

如上,传递 updateComponent
方法给 watcher
实例,该方法内触发组件实例的 vm._render()
渲染方法,触发组件更新,此 mountComponent()
方法会在 $mount()
挂载组件公开方法中调用:

// public mount method
Vue$3.prototype.$mount = function (el, hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};

总结

到此为止,对于JavaScript属性描述器接口的介绍及其应用,还有其在Vue.js中的响应式实践原理基本阐述完了,这次总结从原理到应用,再到实践剖析,花费比较多精力,但是收获是成正比的,不仅对JavaScript基础有更深的理解,还更熟悉了Vue.js响应式的设计原理,对其源码熟悉度也有较大提升,之后在工作和学习过程中,会进行更多的总结分享。

参考

W3CPlus稿源:W3CPlus (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 前端开发 » 从JavaScript属性描述器剖析Vue.js响应式视图

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录