Implementation of Vue.js data two-way binding

foreword

When we use vue, when the data changes, the interface will also be updated, but this is not a matter of course. When we modify the data, how does vue monitor the data change and how does vue refresh the interface when the data changes? of?

When we modify the data, vue monitors the data changes through the Object.definePropertymethod in es5. When the data changes 发布订阅模式, it refreshes through the statistical subscriber interface. This is a design pattern

As shown in the figure below, starting from new Vue to create a Vue instance, el and data will be passed in, and data will be passed into an observer object, and Object.definpropertythe data in data will be converted into getter/setter for data hijacking, and each attribute in data will be created A Dep instance is used to save the watcher instance

And el is passed to compile, and the instruction is parsed in compile. When the data in data used in el is parsed, our getter will be triggered, so that our watcher will be added to the dependency. When the data changes, it will trigger our setter to issue a dependency notification, notify the watcher, and the watcher will send a notification to the view after receiving the notification, and let the view update

data hijacking

The html part creates a div tag with an id of app, which contains span and input tags. The span tag uses interpolation expressions, and the input tag uses v-model

<div class="container" id="app"><span>内容:{
   
   {content}}</span><input type="text" v-model="content">
</div> 

The js part introduces a vue.js file, where the code to realize two-way binding of data is written, and then creates a Vue instance vm, and mounts the data to the div tag

const vm=new Vue({el:'#app',data:{content:'请输入开机密码'}
}) 

A new Vue instance obviously needs to use a constructor. The definition class in the source code of Vue is defined by function . Here I use ES6 class to create this Vue instance.

Then set constructor, the formal parameter is set to obj_instance, as the object passed in when a new Vue instance is passed in, and the data in the passed object is assigned to the $data attribute in the instance

The properties of the object in javascript have changed, we need to tell us, we can update the changed properties to the dom node, so when initializing the instance, define a listening function as a call, and pass in the data to be monitored when calling

class Vue{//创建Vue实例constructor(obj_instance){this.$data=obj_instance.dataObserver(this.$data)}
}
function Observer(data_instance){//监听函数} 

Print this instance vm

The instance has been created, but it still needs to monitor each property in $data. It is used to implement data monitoring Object.defineProperty. Object.definePropertyYou can modify the existing properties of the object. The syntax format is Object.defineProperty(obj, prop, descriptor)

  • obj: the object whose properties are to be defined
  • prop: the name of the property to define or modify
  • descriptor: The attribute descriptor to be defined or modified

To monitor each property in the object, we use Object.keys and foreach to traverse each property in the object and use Object.defineProperty for data monitoring for each property

function Observer(data_instance){Object.keys(data_instance).forEach(key=>{Object.defineProperty(data_instance,key,{enumerable:true,//设置为true表示属性可以枚举configurable:true,//设置为true表示属性描述符可以被改变get(){},//访问该属性的时候触发,get和set函数就是数据监听的核心set(){},//修改该属性的时候触发})})
} 

BeforeObject.defineProperty that, you need to save the value corresponding to the attribute and return it in the get function, otherwise the value of the attribute will be gone after the get function, and the value returned to the attribute will become undefined

let value=data_instance[key]
Object.defineProperty(data_instance,key,{enumerable:true,configurable:true,get(){console.log(key,value);return value},set(){}
}) 

Clicking on the attribute name in $data will trigger the get function

Then set the set function and set the formal parameter for the set. This formal parameter represents the newly passed attribute value, and then assign the new attribute value to the variable value. There is no need to return anything, just modify it, and the return is to access the get. Returned at time, get will also access the latest value variable value after modification

set(newValue){console.log(key,value,newValue);value = newValue
} 

But currently only get and set are set for the first layer attribute of $data, if there is a deeper layer such as

obj:{a:'a',b:'b'
} 

This kind of one does not set get and set. We need to hijack data layer by layer into the attribute, so we use recursion to monitor ourselves again, and make conditional judgment before traversing. If there is no sub-attribute or no object is detected, it will terminate recursion

function Observer(data_instance){//递归出口if(!data_instance || typeof data_instance != 'object') returnObject.keys(data_instance).forEach(key=>{let value=data_instance[key]Observer(value)//递归-子属性的劫持Object.defineProperty(data_instance,key,{enumerable:true,configurable:true,get(){console.log(key,value);return value},set(newValue){console.log(key,value,newValue);value = newValue}})})
} 

There is another detail, if we rewrite the content property of $data from a string to an object, this new object does not have get and set

Because we did not set get and set at all when modifying, we need to call the listening function in set

set(newValue){console.log(key,value,newValue);value = newValueObserver(newValue)
} 

template parsing

After hijacking the data, the data application in the Vue instance must be brought to the page, and a temporary memory area must be added to update all the data before rendering the page to reduce DOM operations

Create a parsing function, set two parameters, one is the element mounted in the Vue instance, and the other is the Vue instance, get the element in the function and save it in $el of the instance, and put it into the temporary memory after getting the element. Required to [createDocumentFragment]create a new blank document fragment

Then add the child nodes of $el to the fragment variable one by one, the page has no content, and the content is temporarily stored in the fragment

class Vue{constructor(obj_instance){this.$data=obj_instance.dataObserver(this.$data)Compile(obj_instance.el,this)}
}
function Compile(ele,vm){vm.$el=document.querySelector(ele)const fragment=document.createDocumentFragment()let child;while (child=vm.$el.firstChild){fragment.append(child)}console.log(fragment);console.log(fragment.childNodes);
} 

Now directly apply the content that needs to be modified to the document fragment, and re-render after application. You only need to modify the text node of the childNodes child node of the fragment. The type of the text node is 3. You can create a function and call it to modify the content in the fragment

There may be nodes in the node, so it is determined whether the node type is 3, if not, call this parsing function recursively

If the node type is 3, modify the operation, but it is not possible to modify the text of the entire node. You only need to modify the content of the interpolation expression, so you need to use regular expression matching and save the matching result in a variable. The matching result is An array, and the element with index 1 is the element we need to extract. This element is the string obtained by removing { {}} and spaces, and then you can directly use the Vue instance to access the value of the corresponding attribute. After modification After return, go out and end the recursion

function Compile(ele,vm){vm.$el=document.querySelector(ele) //获取元素保存在实例了的$el里const fragment=document.createDocumentFragment() //创建文档碎片let child;while (child=vm.$el.firstChild){//循环将子节点添加到文档碎片里fragment.append(child)}fragment_compile(fragment)function fragment_compile(node){ //修改文本节点内容const pattern = /\{\{\s*(\S*)\s*\}\}/ //检索字符串中正则表达式的匹配,用于匹配插值表达式if(node.nodeType===3){const result = pattern.exec(node.nodeValue)if(result){console.log('result[1]')const value=result[1].split('.').reduce(//split将对象里的属性分布在数组里,链式地进行排列;reduce进行累加,层层递进获取$data的值(total,current)=>total[current],vm.$data)node.nodeValue=node.nodeValue.replace(pattern,value) //replace函数将插值表达式替换成$data里的属性的值}return }node.childNodes.forEach(child=>fragment_compile(child))}vm.$el.appendChild(fragment) //将文档碎片应用到对应的dom元素里面
} 

The content of the page comes out again, and the interpolation expression is replaced by the data in the vm instance

Subscribe to Publisher Pattern

Although data hijacking has been carried out and data is applied to the page, the data cannot be updated in time when the data changes, and the subscription publisher mode needs to be implemented

First create a class to collect and notify subscribers. When generating an instance, you need an array to store subscriber information, a method to add subscribers to this array and a method to notify subscribers. Call this method and return Traverse the array of subscribers and let the subscribers call their own update method to update

class Dependency{constructor(){this.subscribers=[] //存放订阅者的信息}addSub(sub){this.subscribers.push(sub) //将订阅者添加到这个数组里}notify(){this.subscribers.forEach(sub=>sub.update()) //遍历订阅者的数组,调用自身的update函数进行更新}
} 

To set the subscriber class, you need to use the properties on the Vue instance, you need the Vue instance and the corresponding properties of the Vue instance and a callback function as parameters, and assign the parameters to the instance

Then you can create the update function of the subscriber, and call the passed callback function in the function

class Watcher{constructor(vm,key,callback){//将参数都赋值给Watcher实例this.vm=vmthis.key=keythis.callback=callback}update(){this.callback() }
} 

When replacing the document fragment content, you need to tell the subscriber how to update it, so the subscriber instance is created when the template parses the node value to replace the content, and the vm instance is passed in. The index value 1 and the callback function after the successful exec match will replace the text The execution statement is copied to the callback function, and the callback function is called when the subscriber is notified of the update

The nodeValue in the callback function should be saved in advance, otherwise the replaced content is not an interpolation expression but replaced content

Then we have to find a way to store the subscribers in the array of Dependency instances. We can save the instances in the subscribers array when constructing the Watcher instance.

Dependency.temp=this //设置一个临时属性temp 

Add a new subscriber to the subscriber array and perform the same operation on all subscribers, then you can add the subscriber to the subscriber array when triggering get, in order to correctly trigger the corresponding property get , you need to use the reduce method to perform the same operation on the key

You can see that the console prints Wathcer instances, and each instance is different, corresponding to different attribute values

The Dependency class has not yet created an instance, and the subscriber array inside does not exist, so create an instance first and then add the subscriber to the subscriber array

When modifying data, notify the subscriber to update, call the notification method of dependency in the set, the notification method will go through the array, and the subscriber executes its own update method to update the data

However, the update call callback function lacks setting formal parameters, and the split and reduce methods are still used to obtain attribute values

update(){const value =this.key.split('.').reduce((total,current)=>total[current],this.vm.$data)this.callback(value)
} 

Modifying the attribute value in the console has been successfully modified, and the page is automatically updated

After completing the binding of the text, you can bind the input box. In vue, you can bind through the v-model, so you need to determine which node has a v-model. The type of the element node is 1. You can use nodeName to match the input element. Make a new judgment directly under the judgment text node

if(node.nodeType===1&&node.nodeName==='INPUT'){const attr=Array.from(node.attributes)console.log(attr);
} 

The node name nodeName is v-model, nodeValue is name, which is the attribute name in the data

Therefore, the array is traversed, and the v-model is matched to find the corresponding attribute value according to the nodeValue, and the attribute value is assigned to the node. At the same time, in order for the subscriber to know to update itself after the data is updated, a new Watcher instance is also added to the INPUT node.

attr.forEach(i=>{if(i.nodeName==='v-model'){const value=i.nodeValue.split('.').reduce((total,current)=>total[current],vm.$data)node.value=valuenew Watcher(vm,i.nodeValue,newValue=>{node.value=newValue})}
}) 

Modify the attribute value, and the page is also modified

The last thing left is to use the view to change the data, just use addEventListener to add the input listening event on the v-model node

node.addEventListener('input',e=>{const arr1=i.nodeValue.split('.')const arr2=arr1.slice(0,arr1.length - 1)const final=arr2.reduce((total,current)=>total[current],vm.$data)final[arr1[arr1.length - 1]]=e.target.value
}) 

At last

Organized 75 JS high-frequency interview questions, and gave answers and analysis, which can basically guarantee that you can cope with the interviewer's questions about JS.



Friends in need, you can click the card below to receive and share for free

Guess you like

Origin blog.csdn.net/web22050702/article/details/128705627