Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to implement a responsive system based on defineProperty and Proxy

2025-01-16 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly introduces the defineProperty and Proxy how to achieve the responsive system, has a certain reference value, interested friends can refer to, I hope you can learn a lot after reading this article, the following let the editor with you to understand.

1. Minimalist two-way binding

Start with the simplest two-way binding:

/ / html// jslet input = document.getElementById ('input') let span = document.getElementById (' span') input.addEventListener ('keyup', function (e) {span [XSS _ clean] = e.target.value})

There seems to be nothing wrong with the above, but we want to be data-driven instead of directly manipulating dom:

/ manipulate obj data to drive updates let obj = {} let input = document.getElementById ('input') let span = document.getElementById (' span') Object.defineProperty (obj, 'text', {configurable: true, enumerable: true, get () {console.log (' got data') return obj.text}, set (newVal) {console.log ('data updated') input.value = newVal span [XSS _ clean] = newVal}) input.addEventListener ('keyup' Function (e) {obj.text = e.target.value})

The above is a simple two-way data binding, but it is obviously insufficient, so let's continue with the upgrade.

Second, implement the response system with defineProperty

The data response realized by defineProperty before the advent of the Vue3 version is based on the publish and subscribe model, which mainly consists of three parts: Observer, Dep and Watcher.

1. An example of an idea

/ / data to be hijacked let data = {a: 1, b: {c: 3}} / / hijacked data dataobserver (data) / / property new Watch of listening subscription data data ('a subscription, () = > {alert (1)}) new Watch ('ajar, () = > {alert (2)}) new Watch (' b.centering, () = > {alert (3)})

The above is a simple hijacking and listening process, so how to implement the corresponding observer and Watch?

2. Observer

The function of observer is to hijack data, convert data attributes into accessor attributes, and sort out the implementation ideas:

① Observer needs to convert the data to responsive, so it should be a function (class) that can accept parameters.

② in order to make the data responsive, you need to use Object.defineProperty.

There is more than one type of ③ data, which requires recursive judgment.

/ / define a class for passing in listening data class Observer {constructor (data) {let keys = Object.keys (data) for (let I = 0; I

< keys.length; i++) { defineReactive(data, keys[i], data[keys[i]]) } }}// 使用Object.definePropertyfunction defineReactive (data, key, val) { // 每次设置访问器前都先验证值是否为对象,实现递归每个属性 observer(val) // 劫持数据属性 Object.defineProperty(data, key, { configurable: true, enumerable: true, get () { return val }, set (newVal) { if (newVal === val) { return } else { data[key] = newVal // 新值也要劫持 observer(newVal) } } })}// 递归判断function observer (data) { if (Object.prototype.toString.call(data) === '[object, Object]') { new Observer(data) } else { return }}// 监听objobserver(data) 3. Watcher 根据new Watch('a', () =>

{alert (1)}) We guess that Watch should look like this:

Class Watch {/ / the first argument is an expression, and the second argument is the callback function constructor (exp, cb) {this.exp = exp this.cb = cb}}.

So how do Watch and observer relate? Think about whether there is a connection between them? It seems to be possible to start with exp, which is what they have in common:

Class Watch {/ / the first argument is an expression, and the second argument is the callback function constructor (exp, cb) {this.exp = exp this.cb = cb data [exp] / / think about the effect of this sentence}}

Does the sentence data [exp] mean that you are taking a value? if exp is a, it means data.a. Before that, the property under data has been hijacked by us as an accessor property, which means that we can trigger the get function of the corresponding property, which is related to observer. In that case, can you collect the trigger Watch when triggering the get function? At this point, we need a bridge Dep to help.

4. Dep

The idea should be that each attribute under data has a unique Dep object, collect dependencies for this attribute only in get, and then trigger all collected dependencies in the set method. This is done. See the following code:

Class Dep {constructor () {/ / defines a method to collect the corresponding attribute dependencies container this.subs = []} / / the method addSub () {/ Dep.target is a global variable that stores the current watcher this.subs.push (Dep.target)} / / set method that notifies the dependency notify () {for (let I = 1; I) when triggered.

< this.subs.length; i++) { this.subs[i].cb() } }}Dep.target = nullclass Watch { constructor (exp, cb) { this.exp = exp this.cb = cb // 将Watch实例赋给全局变量Dep.target,这样get中就能拿到它了 Dep.target = this data[exp] }} 此时对应的defineReactive我们也要增加一些代码: function defineReactive (data, key, val) { observer() let dep = new Dep() // 新增:这样每个属性就能对应一个Dep实例了 Object.defineProperty(data, key, { configurable: true, enumerable: true, get () { dep.addSub() // 新增:get触发时会触发addSub来收集当前的Dep.target,即watcher return val }, set (newVal) { if (newVal === val) { return } else { data[key] = newVal observer(newVal) dep.notify() // 新增:通知对应的依赖 } } })} 至此observer、Dep、Watch三者就形成了一个整体,分工明确。但还有一些地方需要处理,比如我们直接对被劫持过的对象添加新的属性是监测不到的,修改数组的元素值也是如此。这里就顺便提一下Vue源码中是如何解决这个问题的: 对于对象:Vue中提供了Vue.set和vm.$set这两个方法供我们添加新的属性,其原理就是先判断该属性是否为响应式的,如果不是,则通过defineReactive方法将其转为响应式。 对于数组:直接使用下标修改值还是无效的,Vue只hack了数组中的七个方法:pop','push','shift','unshift','splice','sort','reverse',使得我们用起来依旧是响应式的。其原理是:在我们调用数组的这七个方法时,Vue会改造这些方法,它内部同样也会执行这些方法原有的逻辑,只是增加了一些逻辑:取到所增加的值,然后将其变成响应式,然后再手动出发dep.notify() 三、以Proxy实现响应系统 Proxy是在目标前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的全方位加强版。 依旧是三大件:Observer、Dep、Watch,我们在之前的基础再完善这三大件。 1. Dep let uid = 0 // 新增:定义一个idclass Dep { constructor () { this.id = uid++ // 新增:给dep添加id,避免Watch重复订阅 this.subs = [] } depend() { // 新增:源码中在触发get时是先触发depend方法再进行依赖收集的,这样能将dep传给Watch Dep.target.addDep(this); } addSub () { this.subs.push(Dep.target) } notify () { for (let i = 1; i < this.subs.length; i++) { this.subs[i].cb() } }} 2. Watch class Watch { constructor (exp, cb) { this.depIds = {} // 新增:储存订阅者的id,避免重复订阅 this.exp = exp this.cb = cb Dep.target = this data[exp] // 新增:判断是否订阅过该dep,没有则存储该id并调用dep.addSub收集当前watcher addDep (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this) this.depIds[dep.id] = dep } } // 新增:将订阅者放入待更新队列等待批量更新 update () { pushQueue(this) } // 新增:触发真正的更新操作 run () { this.cb() } }} 3. Observer 与Object.defineProperty监听属性不同,Proxy可以监听(实际是代理)整个对象,因此就不需要遍历对象的属性依次监听了,但是如果对象的属性依然是个对象,那么Proxy也无法监听,所以依旧使用递归套路即可。 function Observer (data) { let dep = new Dep() return new Proxy(data, { get () { // 如果订阅者存在,进去depend方法 if (Dep.target) { dep.depend() } // Reflect.get了解一下 return Reflect.get(data, key) }, set (data, key, newVal) { // 如果值未变,则直接返回,不触发后续操作 if (Reflect.get(data, key) === newVal) { return } else { // 设置新值的同时对新值判断是否要递归监听 Reflect.set(target, key, observer(newVal)) // 当值被触发更改的时候,触发Dep的通知方法 dep.notify(key) } } })}// 递归监听function observer (data) { // 如果不是对象则直接返回 if (Object.prototype.toString.call(data) !== '[object, Object]') { return data } // 为对象时则递归判断属性值 Object.keys(data).forEach(key =>

{data [key] = observer (data [key])}) return Observer (data)} / / listen objObserver (data)

At this point, we have basically completed three major pieces, and it can listen to the array without hack.

Trigger dependency collection and batch asynchronous updates

Completed the responsive system, and incidentally mentioned how dependency collection and batch asynchronous updates are triggered in the Vue source code.

1. Trigger dependency collection

A piece of code is indirectly triggered when the $mount method is called in the Vue source code:

Vm._watcher = new Watcher (vm, () = > {vm._update (vm._render (), hydrating)}, noop)

This causes new Watcher () to evaluate its passed parameters first, which indirectly triggers vm._render (), which actually triggers access to data, which in turn triggers the get method of the property to achieve dependency collection.

two。 Batch asynchronous updates

Vue is executed asynchronously when updating DOM. Whenever a data change is detected, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered multiple times, it will only be pushed into the queue once. This removal of duplicate data during buffering is important to avoid unnecessary calculations and DOM operations. Then, in the next event loop "tick", Vue refreshes the queue and performs the actual (deduplicated) work. Vue internally tries to use native Promise.then, MutationObserver, and setImmediate for asynchronous queues, and uses setTimeout (fn, 0) instead if the execution environment does not support it.

According to the above official document, this queue is mainly asynchronous and deduplicated. First of all, let's sort out our ideas:

There needs to be a queue to store data changes in an event loop and to de-duplicate it.

Adds data changes in the current event loop to the queue.

All data changes in this queue are performed asynchronously.

/ / create a queue using the Set data structure, which automatically deduplicates let queue = new Set () / / triggers watcher.update when the property starts the set method Then execute the following method function pushQueue (watcher) {/ / add the data change to the queue queue.add (watcher) / / the next tick to execute the data change So nextTick should accept a function nextTick ('a function that can traverse the execution of queue')} / / simulate nextTickfunction nextTick with Promise ('a function that can traverse the execution of queue') {Promise.resolve (). Then ('a function that can traverse the execution of queue')}

Now that we have a general idea, let's complete a function that can traverse and execute queue:

/ / queue is an array, so you can function flushQueue () {queue.forEach (watcher = > {/ / trigger the run method in watcher for real update operation watcher.run ()}) / / clear the queue queue = new Set ()} after execution

Another problem is that nextTick should be triggered only once in the same event loop, not every time a queue is added:

/ / set an identity let waiting = falsefunction pushQueue (watcher) {queue.add (watcher) if (! waiting) {/ / guarantee that nextTick triggers waiting = true nextTick only once ('a function that can traverse and execute queue')}}

The complete code is as follows:

/ / the function function flushQueue () {queue.forEach (watcher = > {watcher.run ()}) queue = new Set ()} / / nextTickfunction nextTick (flushQueue) {Promise.resolve (). Then (flushQueue)} / / to define the queue let queue = new Set () / / to pass in the execution queue in nextTick and call nextTicklet waiting = falsefunction pushQueue (watcher) {queue.add (watcher) if (! waiting) {waiting = true nextTick (flushQueue) )}} Thank you for reading this article carefully. I hope the article "how to realize the responsive system of defineProperty and Proxy" shared by the editor will be helpful to you. At the same time, I also hope that you will support and pay attention to the industry information channel. More related knowledge is waiting for you to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report