preface

Vue2.0's responsiveness needs to rely on Object.defineProperty, while Vue3.0 uses Es6 proxies. It was really hard, Vue2.0 finally got a bit of a primer, then came 3.0...Copy the code

Know the Object. DefineProperty

Object. DefineProperty needs to take three parameters (obj,key,descriptor). Obj: target Object; key: attribute name of the target Object to be defined or modified; 3. This parameter is an Object Object. DefineProperty (obj, newKey, {configurable: true | false, / / said whether the properties can be configured enumerable: true | False, said / / this property can be enumerated value: any type of value, / / to this attribute assignment writable: true | false,}); **getter/setter** These two configuration items are key to implementing reactive let obj={}; let flag="hello"; Object.defineproperty (obj,"newKey",{get(){console.log(' value '); return flag; }, set(newValue){console.log(' assign ') flag=newValue; }}) console.log(obj.newkey); // When assigning a new value to the property, the callback function can get the new value}}) console.log(obj.newkey); // Value //hello obj.newKey="hello2"; console.log(obj.newKey); // value // assign //hello2Copy the code

Implement the constructor of Vue

Function Vue(options){//options are parameters we pass in the new Vue instance, including el,data, etc. If (! This instanceof Vue){throw new Error(" this constructor must use the new keyword ")} this._data=options.data; // This._el=options.el; This._templatedom =document.querySelector(this._el); DOM this._parentdom = this._templatedom.parentNode; // Get the parent element this.initData(); // Initialize the data object to be responsive. This function has not been created yet.Copy the code

So inside the body, you can create an empty div with the id app

<div id="app"></div>
Copy the code

Implementation of initData function, initialization of data, data response

In this initData is this.initdata () in the constructor above. In this function, we use the object.defineProperty we talked about earlier, so let’s create it.

Vue.prototype.initData=function(){ observe(this._data,this); // Observer passes two arguments, the first is the object to be responded to, the second is this, which is used to change the this of our scope} // Observe the action of the function loop through the data object, Function observe(data,vm){if (array.isarray (data)) {for (let I = 0; function observe(data,vm){if (array.isarray (data)) {for (let I = 0; i < data.length; i++) { observe(data[i], vm); } }else{ let keys=Object.keys(data); for(let i=0; i<keys.length; Definereactive. call(vm, data, keys[I], data[keys[I]], true); }} //defineReactive uses object.defineProperty to listen on attributes. Function defineReactive(target, key, value, enumerable) {let that = this; // If the value of the data attribute is also an object, when assigning a value to an attribute of the secondary object, the set cannot be listened on, because we only added a listener to the data attribute. Typeof value === 'object' && value! Typeof value === 'object' && value! == null) { observe(value, that); } else { Object.defineProperty(target, key, { configurable: true, enumerable: !! enumerable, get() { return value; }, set(newValue) {// If (value === newValue) {return; If (typeof newValue === 'object' && newValue!) typeof newValue === 'object' && newValue! = null) { observe(newValue, that); } console.log(" here print a sentence test "+key+" change to "+newValue); value = newValue; }}}})Copy the code

So far we have almost done with the data initialization part, so let’s try it out and go directly to new Vue().

Let the app = new Vue ({el: "# app," data: {name: 'hello', age: 22, a: {b: 'ABC'}}})Copy the code

Then we can test under the control of changing the value of data.name

The next chapter will explain why there is a._data attribute in this instance.

Proxy Object proxy function implementation

Following up on the previous chapter, there is actually a proxy object proxy function in vue source code. Let’s take a look at this function in Vue source codeAt first glance, it doesn’t seem to make sense, because there’s a lot more to it than is shown here. So what this proxy does is it adds all the properties of the _data object to the instance itself, and what it ends up doing is accessing the properties of the instance, essentially accessing the properties of the _data object, so I’ve written a simple version of proxy here.

Function proxy(target, key) {// let keys = object.keys (target[key]); for (let i = 0; i < keys.length; i++) { Object.defineProperty(target, keys[i], { configurable: true, enumerable: true, get() { return target[key][keys[i]]; }, set(newValue) { target[key][keys[i]] = newValue; }}}})Copy the code

So the question is, when is this function called? Since we want to access the property on the instance to be equivalent to accessing _data, we definitely need to call the _data object after it has been responsified, so we need to modify our initData function slightly

Vue.prototype.initData=function(){ observer(this._data,this); proxy(this,"_data"); // Delegate the _data attribute from this instance to the instance}Copy the code

Now let’s control the print test again

Now you don’t have to access _data first to change the value, and you can also listen for the property value to change.

So far, we have only completed the data initialization part, or does not involve the page display, DOM operation, many friends, see this is probably already confused, next we enter the template compilation part.

What is the virtual DOM?

Virtual DOM is generally the kind of DOM tree nodes nested in this layer of nodes, nodes nested in this layer of nodes tree structure, with Javascript object structure to describe.

So we need to create a virtual DOM Vnode class here

Class Vnode {constructor(tag, data, value, nodeType) {this.tag = tag; // nodeName of the real DOM this.data = data; // The real DOM attribute this.value = value; // The actual DOM text this.nodeType = nodeType; This.childrennode = []; this.childrenNode = []; } appendChildNode(vNode) {this.childrenNode.push(vnode); }}Copy the code

Convert the real DOM to the virtual DOM

Function getVnode(node){// let vnode=null; let nodeType=node.nodeType; . / / the node type let nodeName = node nodeName. ToLowerCase (); // If (nodeType==3){// If (nodeType==3){ We can be introduced to undefined vnode = new vnode (nodeName, undefined, node. The nodeValue, nodeType); }else if(nodeType==1){ let atts=node.attributes; // Array of attributes let attsData={}; ForEach (item=>{attsData[item.nodename]=item.nodeValue; }) / / because there is an element node, so the child node and element nodes, nodeValue can be introduced to undefined vnode = new vnode (nodeName, attsData, undefined, nodeType); let childNodes=node.childNodes; For (let I =0; i<childNodes.length; I ++){// loop over the child to the childrenNode array of the current vNode, and the child may still be the element node, so recursive vNode.appendChildNode (getVnode(childNodes[I])); } } return vnode; }Copy the code

Ok, so that’s it, so I’m going to add a dot element to this div whose ID is equal to app

< div id = "app" > < p > my name is: {{name}} < / p > < div > < p > my age is: {{age}} < / p > < p > this is a secondary property {{a.}} < / p > < / div > < / div >Copy the code

You see this is not very familiar with it! But we still can’t render the data in data to the {{}} on the page, don’t worry! Let’s test getVnode for now

	getVnode(document.querySelector("#app"));
Copy the code

The Address symbol seen here is actually a line break, which also belongs to a node; Then we see that {{name}} does not render the data in our data, so we implement the data in the data to replace {{name}}.

Change the virtual DOM with {{}} to the virtual DOM with data

Function combine(vnode, data) {// The first argument is virtual DOM, the second object is target data object let _vnode = null; let nodeType = vnode.nodeType; let tag = vnode.tag; let value = vnode.value; let attsData = vnode.data; If (nodeType == 3) {value = value.replace(/\{\{(.+?)) \}\}/g, function (a, b) { return data[b.trim()]; }) _vnode = new Vnode(tag, attsData, value, nodeType); } else { let childrenNodes = vnode.childrenNode; _vnode = new Vnode(tag, attsData, value, nodeType); childrenNodes.forEach(item => { _vnode.appendChildNode(combine(item, data)); }) } return _vnode; }Copy the code

Testing:

< div id = "app" > < p > my name is: {{name}} < / p > < div > < p > my age is: {{age}} < / p > < p > this is a secondary property {{a.}} < / p > < / div > < / div >Copy the code
let data = {
        name: 'hello',
        age: 22,
        a: {
            b: 'abc'
        }
   }

    let vnode=getVnode(document.querySelector("#app"));
    console.log(combine(vnode,data));
Copy the code

Results:Magically, {{}} disappears and is replaced by the value of the property on data. {{a.b}} is undefined. In the normal development process using VUE, we will often put the attributes of an object attribute in data into our template for compilation. In fact, the truth here is very simple, we just take the property of data to replace, but the data object does not have the property of “a.b”, that is of course undefined, so we need an intermediate function to carry on the transition, to get the value of the object and modify.

Function getByPathObj(obj, path) {// let list = path.split('.'); let result = obj; list.forEach(item => { result = result[item] }) return result; }Copy the code

At the same time we need to modify the Combine function

Function combine(vnode, data) {// The first argument is virtual DOM, the second object is target data object let _vnode = null; let nodeType = vnode.nodeType; let tag = vnode.tag; let value = vnode.value; let attsData = vnode.data; If (vnode.nodeType == 3) {value = value.replace(/\{\{(.+?)) \}\}/g, function (a, b) { return getByPathObj(data,b.trim()); }) _vnode = new Vnode(tag, attsData, value, nodeType); } else { let childrenNodes = vnode.childrenNode; _vnode = new Vnode(tag, attsData, value, nodeType); childrenNodes.forEach(item => { _vnode.appendChildNode(combine(item, data)); }) } return _vnode; }Copy the code

Check the test results again:

Turn the virtual DOM into our real DOM

The template compilation process in this article is a very simplified version that does not consider performance optimization. In Vue source code, in fact, there is a process of the Diff algorithm. This algorithm is mainly to create a new Vnode according to the new data when we modify the value of data, compare the old Vnode with the new Vnode, and delete the DOM violently when the key attribute or tag changes. Create a new DOM. If neither of the two changes, a recursive traversal is performed and refined comparison is performed, and if a difference is found, changes are made directly to the real DOM, or just the DOM order is changed. Ok, so that’s it, I just implemented the virtual DOM into our real DOM, heh heh heh.

Function parseNode(vnode, data) {let nodeName = vnode.tag; let nodeType = vnode.nodeType; let node = null; if (nodeType == 1) { let atts = Object.keys(vnode.data); node = document.createElement(nodeName); atts.forEach(item => { node.setAttribute(item, vnode.data[item]); }) } else { node = document.createTextNode(vnode.value); } return node; }Copy the code

Add the template compilation and rendering process to Vue

Here are two important concepts to understand: Dep and Watcher:

Dep: Dependency collection. In data, one key corresponds to one Dep instance, and one Dep corresponds to multiple Watchers. When data changes on data, dep.notify () is executed to notify all the corresponding Watcher.

Watcher: Observers who perform their own changes watcher.update().

class Dep { constructor() { this.watcherList = []; //watcher } addWatcher(watcher) { this.watcherList.push(watcher); } removeW(watcher) { for (let i = this.watcherList.length - 1; i >= 0; i--) { if (watcher == this.watcherList[i]) { this.watcherList.splice(i, 1); }} depend() {if (dep.target) {dep.target.adddep (this); this.watcherList.push(Dep.target); } } notify() { this.watcherList.forEach(item => { item.update() }) } }Copy the code
Constructor (vm, expFn) {this.vm = vm; this.expFn = expFn; // This. Deps = []; / / this dep array. The get (); } get() { pushStack(this); this.expFn.call(this.vm); popStack(); } update() {this.get(); } clearDep() {// clearDep this.deps = []; } addDep(dep) {// addDep this.deps.push(dep); }}Copy the code
Dep.target = null;
    let targetStack = [];

    function pushStack(target) {
        targetStack.push(target);
        Dep.target = target;
    }

    function popStack() {
        Dep.target = targetStack.pop();
    }
Copy the code

Complete our Vue constructor

function Vue2(options) { if (! This instanceof Vue2) {throw new Error(" you did not use the new keyword "); } this._data = options.data; this._el = options.el; this._templateDom = document.querySelector(this._el); this._parentDom = this._templateDom.parentNode; this.initData(); // Convert our data to responsive this.mount(); } vue2.prototype. initData = function () {observe(this._data, this); proxy(this, '_data'); } Vue2. Prototype. Mount = function () {this.render = this.createrender (); // Start rendering the template this.mountComponent(); } Vue2.prototype.createRender = function () { let that = this; let ast = getVnode(this._templateDom); Return function render() {return combine(ast, tha._data); // Combine (ast, tha._data); } } Vue2.prototype.mountComponent = function () { let mount = () => { this.update(this.render()); } // New Watcher(this, mount); } Vue2.prototype.update = function (vnode) { let realDom = parseNode(vnode, this._data); This._parentdom.replacechild (realDom, document.querySelector(this._el)); }Copy the code

You also need to modify the defineReactive function

function defineReactive(target, key, value, enumerable) { let that = this; if (typeof value === 'object' && value ! == null) { observe(value, that); } else {// create collector instance let dep = new dep (); Object.defineProperty(target, key, { configurable: true, enumerable: !! Enumerable, get() {// Dep collects dep.depend(); return value; }, set(newValue) { if (value === newValue) { return; } if (typeof newValue === 'object' && newValue ! = null) { observe(newValue, that); } value = newValue; // Notify the watcher to update the dep.notify() page when the data attribute values change; }}}})Copy the code

And finally (a little bit about array listening)

So far, such a Vue constructor is almost complete. Many people may ask how to listen for array changes. In how to listen to the array this problem, in vUE source code is actually transformed on the array prototype of 7 methods, not only can let these methods to achieve the function of the method itself, but also can add the function we want, I wrote a basic function on the array method transformation.

function changeArrayMethods(arr){ let arrMethods=["push","pop","shift","unshift","sort","splice","reverse"]; let arrProto=Array.prototype; let arrProtoObj=Object.create(arrProto); ForEach (item=>{object.defineProperty (arrProtoObj,item,{value:function(){console.log(" Add new content here "); return arrProto[item].apply(this,arguments); SetPrototypeOf (arr,arrProtoObj)}Copy the code

Thank you !!!!!!!!!