vue响应式原理(lǐ)源码:带你一步精通vue

2020/06/02      29931 文(wén)章来源:CSDN 原作者:爱编程的小(xiǎo)和尚

下面是实现的简易版 vue 的源码地址,一定要先下载下来因為(wèi)文(wén)章中的并非全部的代码。

github 源码地址:https://github.com/young-monk/myvue.git

在开始學(xué)习之前,我们先来了解一下什么是 MVVM ,什么是数据响应式。

我们都知道 vue 是一个典型的 MVVM 思想,由数据驱动视图。

那么什么是 MVVM 思想呢(ne)?

MVVM 是 Model-View-ViewModel,是把一个系统分(fēn)為(wèi)了模型( model )、视图( view )和 view-model 三个部分(fēn)。

vue 在 MVVM 思想下,view 和model 之间没有(yǒu)直接的联系,但是 view 和 view-model 、model 和 view-model之间时交互的,当 view 视图进行 dom 操作等使数据发生变化时,可(kě)以通过 view-model 同步到 model 中,同样的 model 数据变化也会同步到 view 中。

那么实现数据响应式都有(yǒu)什么方法呢(ne)?

1、发布者-订阅者模式:当一个对象(发布者)状态发生改变时,所有(yǒu)依赖它的对象(订阅者)都会得到通知。通俗点来讲,发布者就相当于报纸,而订阅者相当于读报纸的人。

2、脏值检查:通过存储旧的数据,和当前新(xīn)的数据进行对比,观察是否有(yǒu)变更,来决定是否更新(xīn)视图。angular.js就是通过脏值检查的方式。最简单的实现方式就是通过 setInterval() 定时轮询检测数据变动,但这样无疑会增加性能(néng),所以,angular 只有(yǒu)在指定的事件触发时进入脏值检测。

3、数据劫持:通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时触发相应的方法。

vue 是如何实现数据响应式的呢(ne)?

vue.js 则是通过数据劫持结合发布者-订阅者模式的方式。

当执行 new Vue() 时,Vue 就进入了初始化阶段,vue会对指令进行解析(初始化视图,增加订阅者,绑定更新(xīn)函数),同时通过Obserber 会遍历数据并通过Object.defineProperty 的 getter 和 setter 实现对的监听, 当数据发生变化的时候,Observer 中的 setter 方法被触发,setter 会立即调用(yòng)Dep.notify(),Dep 开始遍历所有(yǒu)的订阅者,并调用(yòng)订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新(xīn)。

我来依次介绍一下图中的重要的名词:

Observer :数据监听器,能(néng)够对数据对象的所有(yǒu)属性进行监听,如有(yǒu)变动可(kě)拿(ná)到最新(xīn)值并通知订阅者,内部采用(yòng)Object.defineProperty 的 getter 和 setter 来实现

Compile :指令解析器,它的作用(yòng)对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新(xīn)函数

Dep :订阅者收集器或者叫消息订阅器都可(kě)以,它在内部维护了一个数组,用(yòng)来收集订阅者,当数据改变触发 notify 函数,再调用(yòng)订阅者的 update 方法

Watcher :订阅者,它是连接 Observer 和 Compile 的桥梁,收到消息订阅器的通知,更新(xīn)视图

Updater:视图更新(xīn)

所以我们想要实现一个vue响应式,需要完成 数据劫持、依赖收集、 发布者订阅者模式。

下面我来介绍我模仿源码实现的功能(néng):

数据的响应式、双向绑定,能(néng)够对数据对象的所有(yǒu)属性进行监听,如有(yǒu)变动可(kě)拿(ná)到最新(xīn)值并通知订阅者

解析 vue 常用(yòng)的指令 v-html,v-text,v-bind,v-on,v-model,包括( @ 和 : )

数组变异方法的处理(lǐ)

vue 中使用(yòng) this 访问或改变 data 中的数据

我们想要完成以上的功能(néng),需要实现如下类和方法:

实现Observer类:对所有(yǒu)的数据进行监听

实现 array 工具方法:对变异方法的处理(lǐ)

实现 Dep 类:维护订阅者

实现 Watcher 类:接收 Dep 的更新(xīn)通知,用(yòng)于更新(xīn)视图

实现 Compile 类:用(yòng)于对指令进行解析

实现一个 compileUtils 工具方法,实现通过指令更新(xīn)视图、绑定更新(xīn)函数Watcher

实现this.data代理(lǐ):实现对this.data代理(lǐ):实现对 this.data代理(lǐ):实现对this.data 代理(lǐ),可(kě)以直接在vue中使用(yòng) this 获取当前数据

我是使用(yòng)了webpack作為(wèi)构建工具来协同开发的,所以在我实现的vue响应式中会用(yòng)到ES6模块化,webpack的相关知识。知识链接在文(wén)章最后。

1、实现 Observer 类

我们都知道要用(yòng) Obeject.defineProperty() 来监听属性的数据变化,我们需要对 Observer 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter ,这样的话,当给这个对象的某个值赋值,就会触发 setter,那么就能(néng)监听到了数据变化。当然我们在新(xīn)增加数据的时候,也要对新(xīn)的数据对象进行递归遍历,加上 setter 和 getter 。

但我们要注意数组,在处理(lǐ)数组时并不是把数组中的每一个元素都加上 setter 和 getter ,我们试想一下,一个从后端返回的数组数据是非常庞大的,如果為(wèi)每个属性都加上 setter和getter ,性能(néng)消耗是十分(fēn)巨大的。我们想要得到的效果和所消耗的性能(néng)不成正比,所以在数组方面,我们通过对数组的7 个变异方法来实现数据的响应式。只有(yǒu)通过数组变异方法来修改和删除数组时才会重新(xīn)渲染页面。

那么监听到变化之后是如何通知订阅者来更新(xīn)视图的呢(ne)?我们需要实现一个Dep(消息订阅器),其中有(yǒu)一个 notify()方法,是通知订阅者数据发生了变化,再让订阅者来更新(xīn)视图。

我们怎么添加订阅者呢(ne)?我们可(kě)以通过 new Dep(),通过 Dep 中的 addSaubs() 方法来添加订阅者。我们来看一下具體(tǐ)代码。

我们首先需要声明一个Observer类,在创建类的时候,我们需要创建一个消息订阅器,判断一下是否是数组,如果是数组,我们便改造数组,如果是对象,我们便需要為(wèi)对象的每一个属性都加入 setter和getter 。

import { arrayMethods } from './array' //数组变异方法处理(lǐ)

class Observer {

constructor(data) {

//用(yòng)于对数组进行处理(lǐ),存放数组的观察者watcher

this.dep = new Dep()

if (Array.isArray(data)) {

//如果是数组,使用(yòng)数组的变异方法

data.__proto__ = arrayMethods

//把数组数据添加 __ob__ 一个Observer,当使用(yòng)数组变异方法时,可(kě)以更新(xīn)视图

data.__ob__ = this

//给数组的每一项添加数据劫持(setter/getter处理(lǐ))

this.observerArray(data)

} else {

//非数组数据添加数据劫持(setter/getter处理(lǐ))

this.walk(data)

}

}

}

在上面,我们给 data 的__proto__原型链重新(xīn)赋值,我们来看一下 arrayMethods 是什么,arrayMethods 是 array.js 文(wén)件中,抛出的一个新(xīn)的 Array 原型

// 获取Array的原型链

const arrayProto = Array.prototype;

// 重新(xīn)创建一个含有(yǒu)对应原型的对象,在下面称為(wèi)新(xīn)Array

const arrayMethods = Object.create(arrayProto);

// 处理(lǐ)7个数组变异方法

['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach(ele => {

//修改新(xīn)Array的对应的方法

arrayMethods[ele] = function () {

// 执行数组的原生方法,完成其需要完成的内容

arrayProto[ele].call(this, ...arguments)

// 获取Observer对象

const ob = this.__ob__

// 更新(xīn)视图

ob.dep.notify()

}

})

export {

arrayMethods

}

此时呢(ne),我们就拥有(yǒu)了数组的变异方法,我们还需要通过 observerArray 方法為(wèi)数组的每一项添加 getter 和setter ,注意,此时的每一项只是最外面的一层,并非递归遍历。

//循环遍历数组,為(wèi)数组每一项设置setter/getter

observerArray(items) {

for (let i = 0; i < items.length; i++) {

this.observer(items[i])

}

}

如果是一个对象的话,我们就要对 对象 的每一个属性递归遍历,通过walk() 方法

walk(data) {

//数据劫持

if (data && typeof data === "object") {

for (const key in data) {

//绑定setter和getter

this.defineReactive(data, key, data[key])

}

}

}

在上面的调用(yòng)了 defineReactive() ,我们来看看这个方法是干什么的?这个方法就是设置数据劫持的,每一行都有(yǒu)注释。

//数据劫持,设置 setter/getteer

defineReactive(data, key, value) {

//如果是数组的话,需要接受返回的Observer对象

let arrayOb = this.observer(value)

//创建订阅者/收集依赖

const dep = new Dep()

//setter和getter处理(lǐ)

Object.defineProperty(data, key, {

//可(kě)枚举的

enumerable: true,

//可(kě)修改的

configurable: false,

get() {

//当 Dep 有(yǒu) watcher 时, 添加 watcher

Dep.target && dep.addSubs(Dep.target)

//如果是数组,则添加上数组的观察者

Dep.target && arrayOb && arrayOb.dep.addSubs(Dep.target)

return value

},

set: (newVal) => {

//新(xīn)旧数据不相等时更改

if (value !== newVal) {

//為(wèi)新(xīn)设置的数据添加setter/getter

arrayOb = this.observer(newVal);

value = newVal

//通知 dep 数据发送了变化

dep.notify()

}

}

})

}

}

我们需要注意的是,在上面的图解中,在 Observer 中,如果数据发生变化,会通知消息订阅器,那么在何时绑定消息订阅器呢(ne)?就是在设置 setter 和 getter 的时候,创建一个Dep,并為(wèi)Dep添加订阅者,Dep.target && dep.addSubs(Dep.target),通过调用(yòng) dep 的 addSubs 方法添加订阅者

2、实现 Dep

Dep是消息订阅器,它的作用(yòng)就是维护一个订阅者数组,当数据发送变化是,通知对应的订阅者,Dep中有(yǒu)一个notify()方法,作用(yòng)就是通知订阅者,数据发送了变化

// 订阅者收集器

export default class Dep {

constructor() {

//管理(lǐ)的watcher的数组

this.subs = []

}

addSubs(watcher) {

//添加watcher

this.subs.push(watcher)

}

notify() {

//通知watcher更新(xīn)dom

this.subs.forEach(w => w.update())

}

}

3、实现 watcher

Watcher就是订阅者,watcher是 Observer 和 Compile 之间通信的桥梁,当数据改变时,接收到 Dep 的通知(Dep 的notify()方法),来调用(yòng)自己的update()方法,触发 Compile 中绑定的回调,达到更新(xīn)视图的目的。

import Dep from './dep'

import { complieUtils } from './utils'

export default class Watcher {

constructor(vm, expr, cb) {

//当前的vue实例

this.vm = vm;

//表达式

this.expr = expr;

//回调函数,更新(xīn)dom

this.cb = cb

//获取旧的数据,此时获取旧值的时候,Dep.target会绑定上当前的this

this.oldVal = this.getOldVal()

}

getOldVal() {

//将当前的watcher绑定起来

Dep.target = this

//获取旧数据

const oldVal = complieUtils.getValue(this.expr, this.vm)

//绑定完成后,将绑定的置空,防止多(duō)次绑定

Dep.target = null

return oldVal

}

update() {

//更新(xīn)函数

const newVal = complieUtils.getValue(this.expr, this.vm)

if (newVal !== this.oldVal || Array.isArray(newVal)) {

//条用(yòng)更新(xīn)在compile中创建watcher时传入的回调函数

this.cb(newVal)

}

}

}

上面中用(yòng)到了 complieUtils 中的 getValue() 方法,会在下面讲,主要作用(yòng)是获取到指定表达式的值。

我们把整个流程分(fēn)成两条路線(xiàn)的话:

new Vue() ==> Observer数据劫持 ==> 绑定Dep ==> 通知watcher ==> 更新(xīn)视图

new Vue() ==> Compile解析模板指令 ==> 初始化视图 和 绑定watcher

此时,我们第一条線(xiàn)的内容已经实现了,我们再来实现一下第二条線(xiàn)。

4、实现 Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,初始化渲染页面视图。同时也要绑定更新(xīn)函数,添加订阅者。

因為(wèi)在解析的过程中,会多(duō)次的操作dom,為(wèi)提高性能(néng)和效率,会先将vue实例根节点的 el 转换成文(wén)档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中,文(wén)档碎片知识点在文(wén)章最后面的知识点链接中。

class Complie {

constructor(el, vm) {

this.el = this.isNodeElement(el) ? el : document.querySelector(el);

this.vm = vm;

// 1、将所有(yǒu)的dom对象放到fragement文(wén)档碎片中,防止重复操作dom,消耗性能(néng)

const fragments = this.nodeTofragments(this.el)

// 2、编译模板

this.complie(fragments)

// 3、追加子元素到根元素

this.el.appendChild(fragments)

}

}

我们可(kě)以看到,Complie 中主要进行了三步,第一步 nodeTofragments 是讲所有(yǒu)的dom节点放到文(wén)档碎片中操作,最后一步,是把解析好的dom元素,从文(wén)档碎片重新(xīn)加入到页面中,这两步的具體(tǐ)方法,大家去下载我的源码,看一下就明白了,有(yǒu)注释。我就不再解释 了。

我们来看一下第二步,编译模板:

complie(fragments) {

//获取所有(yǒu)节点

const nodes = fragments.childNodes;

[...nodes].forEach(ele => {

if (this.isNodeElement(ele)) {

//1. 编译元素节点

this.complieElement(ele)

} else {

//编译文(wén)本节点

this.complieText(ele)

}

//如果有(yǒu)子节点,循环遍历,编译指令

if (ele.childNodes && ele.childNodes.length) {

this.complie(ele)

}

})

}

我们要知道,模板可(kě)能(néng)有(yǒu)两种情况,一种是文(wén)本节点(含有(yǒu)双大括号的插值表达式)和元素节点(含有(yǒu)指令)。我们获取所有(yǒu)节点后对每个节点进行判断,如果是元素节点,则用(yòng)解析元素节点的方法,如果是文(wén)本节点,则调用(yòng)解析文(wén)本的方法。

complieElement(node) {

//1.获取所有(yǒu)的属性

const attrs = node.attributes;

//2.筛选出是属性的

[...attrs].forEach(attr => {

//attr是一个对象,name是属性名,value是属性值

const {name,value} = attr

//判断是否含有(yǒu)v-开头 如:v-html

if (name.startsWith("v-")) {

//将指令分(fēn)离  text, html, on:click

const [, directive] = name.split("-")

//处理(lǐ)on:click或bind:name的情况 on,click

const [dirName, paramName] = directive.split(":")

//编译模板

complieUtils[dirName](node, value, this.vm, paramName)

//删除属性,在页面中的dom中不会再显示v-html这种指令的属性

node.removeAttribute(name)

} else if (name.startsWith("@")) {

// 如果是事件处理(lǐ) @click='handleClick'

let [, paramName] = name.split('@');

complieUtils['on'](node, value, this.vm, paramName);

node.removeAttribute(name);

} else if (name.startsWith(":")) {

// 如果是事件处理(lǐ) :href='...'

let [, paramName] = name.split(':');

complieUtils['bind'](node, value, this.vm, paramName);

node.removeAttribute(name);

}

})

}

我们在编译模板中调用(yòng)了 complieUtils[dirName](node, value, this.vm, paramName)方法,这是工具类中的一个方法,用(yòng)于处理(lǐ)指令

我们再来看看文(wén)本节点,文(wén)本节点就相对比较简单,只需要匹配{{}}形式的插值表达式就可(kě)以了,同样的调用(yòng)工具方法,来解析。

complieText(node) {

//1.获取所有(yǒu)的文(wén)本内容

const text = node.textContent

//匹配{{}}

if (/\{\{(.+?)\}\}/.test(text)) {

//编译模板

complieUtils['text'](node, text, this.vm)

}

}

上面用(yòng)来这么多(duō)工具方法,我们来看看到底是什么

5、实现 complieUtils 工具方法

这个方法主要是对指令进行处理(lǐ),获取指令中的值,并在页面中更新(xīn)相应的值,同时我们在这里要绑定watcher的回调函数。

我来以v-text指令来解释,其他(tā)指令都有(yǒu)注释,大家自己看。

import Watcher from './watcher'

export const complieUtils = {

//处理(lǐ)text指令

text(node, expr, vm) {

let value;

if (/\{\{.+?\}\}/.test(expr)) {

//处理(lǐ) {{}}

value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {

//绑定观察者/更新(xīn)函数

new Watcher(vm, args[1], () => {

//第二个参数,传入回调函数

this.updater.updaterText(node, this.getContentVal(expr, vm))

})

return this.getValue(args[1], vm)

})

} else {

//v-text

new Watcher(vm, expr, (newVal) => {

this.updater.updaterText(node, newVal)

})

//获取到value值

value = this.getValue(expr, vm)

}

//调用(yòng)更新(xīn)函数

this.updater.updaterText(node, value)

},

}

text处理(lǐ)函数是对dom元素的textContent进行操作的,所以有(yǒu)两种情况,一种是使用(yòng)v-text指令,会更新(xīn)元素的textContent,另一种情况是{{}} 的插值表达式,也是更新(xīn)元素的 textContent。

在此方法中我们先判断是哪一种情况,如果是v-text指令,那么就绑定一个watcher的回调,获取到textContent的值,调用(yòng)updater.updaterText在下面讲,是更新(xīn)元素的方法。如果是双大括号的话,我们就要对其进行特殊处理(lǐ),首先是将双大括号替换成指定的变量的值,同时為(wèi)其绑定 watcher 的回调。

//通过表达式, vm获取data中的值, person.name

getValue(expr, vm) {

return expr.split(".").reduce((data, currentVal) => {

return data[currentVal]

}, vm.$data)

},

获取 textContent 的值是用(yòng)一个 reduce 函数,用(yòng)法在最后面的链接中,因為(wèi)数据可(kě)能(néng)是 person.name 我们需要获取到最深的对象的值。

//更新(xīn)dom元素的方法

updater: {

//更新(xīn)文(wén)本

updaterText(node, value) {

node.textContent = value

}

}

updater.updaterText更新(xīn)dom的方法,其实就是对 textContent 重新(xīn)赋值。

我们再来将一下v-model指令,实现双向的数据绑定,我们都知道,v-model其实实现的是 input 事件和 value 之间的语法糖。所以我们这里同样的监听一下当前dom元素的 input 事件,当数据改变时,调用(yòng)设置新(xīn)值的方法

//处理(lǐ)model指令

model(node, expr, vm) {

const value = this.getValue(expr, vm)

//绑定watcher

new Watcher(vm, expr, (newVal) => {

this.updater.updaterModel(node, newVal)

})

//双向数据绑定

node.addEventListener("input", (e) => {

//设值方法

this.setVal(expr, vm, e.target.value)

})

this.updater.updaterModel(node, value)

},

这个方法同样是通过 reduce 方法,為(wèi)对应的变量设置成新(xīn)的值,此时数据改变了,会自动调用(yòng)更新(xīn)视图的方法,我们在之前已经实现了。

//通过表达式,vm,输入框的值,实现设置值,input中v-model双向数据绑定

setVal(expr, vm, inputVal) {

expr.split(".").reduce((data, currentVal) => {

data[currentVal] = inputVal

}, vm.$data)

},

6、实现vue

最后呢(ne),我们就要来整合这些类和工具方法,在创建一个vue实例的时候,我们先获取options中的参数,然后对起进行数据劫持和编译模板

class Vue {

constructor(options) {

//获取模板

this.$el = options.el;

//获取data中的数据

this.$data = options.data;

//将对象中的属性存起来,以便后续使用(yòng)

this.$options = options

//1.数据劫持,设置setter/getter

new Observer(this.$data)

//2.编译模板,解析指令

new Complie(this.$el, this)

}

}

此时我们想要使用(yòng) vue 中的数据,比如我们想要在 vm 对象中使用(yòng)person.name, 必须用(yòng) this.$data.person.name 才能(néng)获取到,如果我们想在vm对象中使用(yòng) this.person.name 直接修改数据,就需要代理(lǐ)一下 this.$data 。其实就是将当前的 this.$data 中的数据放到全局中进行监听。

export default class Vue {

constructor(options) {

//...

//1.数据劫持,设置setter/getter

//2.编译模板,解析指令

if (this.$el) { //如果有(yǒu)模板

//代理(lǐ)this

this.proxyData(this.$data)

}

}

proxyData(data) {

for (const key in data) {

//将当前的数据放到全局指向中

Object.defineProperty(this, key, {

get() {

return data[key];

},

set(newVal) {

data[key] = newVal

}

})

}

}

}

文(wén)章到了这里,就实现了一个简易版的vue,建议大家反复學(xué)习,仔细體(tǐ)验,细细品味。

在文(wén)章的最后,我通过问、答(dá)的形式,来解答(dá)一些常见的面试题:

问:什么时候页面会重新(xīn)渲染?

答(dá):数据发生改变,页面就会重新(xīn)渲染,但数据驱动视图,数据必须先存在,然后才能(néng)实现数据绑定,改变数据,页面才会重新(xīn)渲染。

问:什么时候页面不会重新(xīn)渲染?

答(dá):有(yǒu)3种情况不会重新(xīn)渲染

未经声明和未使用(yòng)的变量,修改他(tā)们,都不会重新(xīn)渲染页面

通过索引的方式和更改長(cháng)度的方式更改数组,都不会重新(xīn)渲染页面

增加和删除对象的属性,不会重新(xīn)渲染页面

问:如何使 未声明/未使用(yòng)的变量、增加/删除对象属性可(kě)以使页面重新(xīn)渲染?

答(dá):添加利用(yòng)vm.$set/Vue.set,删除利用(yòng)vm.$delete/Vue.delete方法

问:如何更改数组可(kě)以使页面重新(xīn)渲染?

答(dá):可(kě)以使用(yòng)数组的变异方法(共 7 个):push、pop、unshift、shift、splice、sort、reverse

问:数据更新(xīn)后,页面会立刻重新(xīn)渲染么?

答(dá):更改数据后,页面不会立刻重新(xīn)渲染,页面渲染的操作是异步执行的,执行完同步任務(wù)后,才会执行异步的

同步队列,异步队列(宏任務(wù)、微任務(wù)

问:如果更改了数据,想要在页面重新(xīn)渲染后再做操作,怎么办?

答(dá):可(kě)以使用(yòng) vm.$nextTick 或 Vue.nextTick

问:来介绍一下vm.$nextTick 和 Vue.nextTick 吧

答(dá):我们来看个小(xiǎo)例子就明白啦

{{ name }}



问: vm.$nextTick 和 Vue.nextTick 有(yǒu)什么區(qū)别呢(ne) ?

答(dá):Vue.nextTick内部函数的this指向window,vm.$nextTick内部函数的this指向Vue实例对象。

Vue.nextTick(function () {

console.log(this); // window

})

vm.$nextTick(function () {

console.log(this); // vm实例

})

问: vm.$nextTick 和 Vue.nextTick 是通过什么实现的呢(ne) ?

答(dá):二者都是等页面渲染后执行的任務(wù),都是使用(yòng)微任務(wù)。

if(typeof Promise !== 'undefined') {

// 微任務(wù)

// 首先看一下浏览器中有(yǒu)没有(yǒu)promise

// 因為(wèi)IE浏览器中不能(néng)执行Promise

const p = Promise.resolve();

} else if(typeof MutationObserver !== 'undefined') {

// 微任務(wù)

// 突变观察

// 监听文(wén)档中文(wén)字的变化,如果文(wén)字有(yǒu)变化,就会执行回调

// vue的具體(tǐ)做法是:创建一个假节点,然后让这个假节点稍微改动一下,就会执行对应的函数

} else if(typeof setImmediate !== 'undefined') {

// 宏任務(wù)

// 只在IE下有(yǒu)

} else {

// 宏任務(wù)

// 如果上面都不能(néng)执行,那么则会调用(yòng)setTimeout

}

同样的这也是vue的一个小(xiǎo)缺点:vue一直是等主線(xiàn)程执行完以后再执行渲染任務(wù),如果主線(xiàn)程卡死,则永遠(yuǎn)渲染不出来。

问:利用(yòng) Object.defineProperty 实现响应式有(yǒu)什么缺点?

答(dá):

1.天生就需要进行递归

2.监听不到数组不存在的索引的改变

3.监听不到数组長(cháng)度的改变

4.监听不到对象的增删