In the last video, one of the major issues that we left behind was that renderRule wasn’t flexible enough so let’s think about how do we fix that

In fact, one of the key reasons it is not flexible enough is that the data structure is already defined when the New ViewComponent is called, and thus the DOM structure is already defined. When the data changes, the data structure cannot be updated again, thus the DOM structure cannot be updated

Therefore, it is natural to wonder if renderRule could be changed to function form, as follows:

new ViewComponent({ renderRule: function () { return { tag: 'div', class: 'good-detail', children: [ { tag: 'div', class: 'item', content: 'name: {{good.name}}'}, {tag: 'div', class: 'item', content: 'CPU: {{good. CpuNum}}'}, {tag: 'div', class: 'item' content: 'memory: {{good. The memory}}'}, {tag: 'div', class: 'item' content: 'brand: {{good. Brand}}'}, {tag: 'div', class: 'item' content: 'classification: {{good. The category}}'}, {tag: 'div' class: 'item' content: 'color: {{good. Color}}'}}}}])Copy the code

This function will be reexecuted each time the data is updated, retrieving the changed good and fetching the new value

In addition, the last time we also said that the question, is the json format is too not intuitive, so we to make some changes to this renderRule again next Everyone looked at me after changes, of course, will be disappointed, because of changed after the results, seems not intuitive, even worse, this is not intuitive way, We’ll need to work on this for a while until the other important modules are implemented, which I think will make it clearer why we have Template in Vue, JSX in React, and a lot of trouble compiling them into render functions:

new ViewComponent({ render: function () { return this.createElement('div', { class: 'good-detail' }, [ this.createElement('div', { class: 'item'}, 'name: {{good.name}}'), this.createElement('div', {class: 'item'}, 'CPU: {{good.cpunum}}'), this.createElement('div', {class: 'item'}, 'memory: {{good.memory}}'), this.createElement('div', {class: 'item'}, 'brand: {{good.brand}}'), this.createElement('div', {class: 'item'}, 'classification: {{good. The category}}'), enclosing createElement method (' div '{class:}' item ', 'color: {{good. Color}}')]}})Copy the code

As an aside: In fact, if we replace our createElement with a real HTML tag, it’s no different from JSX, right

As you can see, we keep calling a method called createElement with input parameters param1: the label name of the DOM object to be created param2: some properties of the DOM object Param3: child elements of the DOM object

And we’re going to do that in a minute, but you just want to know what it does, and then you’ll see it more purposefully

One more thing, you can see that I changed renderRule to render, because renderRule means render rule, it’s a noun, and render means render, It’s a verb. What we’re doing here is actually showing the render rules of render with the createElement entry (mainly the third parameter), and also creating the DOM element, so this is actually an action, which is different from the previous version, so I want you to notice

On top of that, we’ll also introduce the effect of creating a different DOM depending on the variable, for which I have prepared an example, as shown below:

Click toggle to expand more content, like the following:If YOU hit Toggle, you can go back to the original state and just show the iPhoneX and iPhone

From the requirements point of view, it is obvious that we need to introduce events

It is also important that we track the state of expansion and contraction through the value of a variable, which will be used as the basis for whether we render the detailed information. Since this value is strongly associated with the commodity details, we also put it in data, which I personally think is also a manifestation of encapsulation

For those of you who have experienced manipulating DOM methods and manually setting div display to block or None, tracking DOM state through variables is an important change in development thinking

Without further ado, our call code is as follows:

new ViewComponent({ el: 'body', data: { isShowDetail: false, good: { id: '8faw8cs4fw9760zt7tnesini4qup5hid', name: 'iPhoneX', cpuNum: 1, memory: 1073741824, brand: 'iPhone', category: 'phone', color: 'black' } }, methods: { switchDetail () { this.data.isShowDetail ! = this.data.isShowDetail } }, render: function () { let children = [ this.createElement('div', { class: 'abstract' }, [ this.createElement('div', { class: 'name' }), this.createElement('div', { class: 'brand' }), this.createElement('span', { class: 'switch', click: this.methods.switchDetail }) ]) ] if (this.isShowDetail) { children.push( this.createElement('div', { class: 'item' }, this.data.good.cpuNum), this.createElement('div', { class: 'item' }, this.data.good.memory), this.createElement('div', { class: 'item' }, this.data.good.category), this.createElement('div', { class: 'item' }, this.data.good.color) ) } return this.createElement('div', { class: 'good-detail' }, this.createElement('div', { class: 'detail' }, children) ) } })Copy the code

The isShowDetail in data indicates whether the item is expanded to display more information, which is accompanied by the switchDetail event

But see this disgusting render, I think everyone want to scold me, the front frame of one of the most important purpose is to get the front from joining together of DOM, but this render things, its degree of trouble comes as stitching the DOM, but again, only we know how to do this trouble, Can we better understand why Template and JSX exist

Now let’s look at the ViewComponent implementation: Before we look at the ViewComponent implementation, we want you to keep this question in mind: In switchDetail we just changed the this.data.isShowDetail data and did not call render again, so VueComponent must have some mechanism inside, It uses this mechanism to know that the data this.data.isShowDetail has changed, and then calls render for us.

The idea is something like this: The Render function will create the DOM we want, and it will create the DOM we want each time we change the data, so we can call Render again after each data change and have the DOM generated by Render replace the previous DOM

Here are a few things to note:

  1. The render passed in should only be responsible for generating the DOM. Removing the old DOM and replacing it with a new ONE should be left to the ViewComponent, since removing and replacing the old DOM are regular and necessary for each component
  2. Naturally, we need a way to: This method removes the old DOM and replaces the generated DOM with the original one. We can call this method update, where we fetch the last element from this.el or this.renderedElement. RenderedElement (renderedElement, renderedElement, renderedElement, renderedElement, renderedElement, renderedElement, renderedElement, renderedElement, renderedElement, renderedElement, renderedElement, renderedElement)
function ViewComponent (options) { this.el = document.querySelector(options.el) this.data = options.data this.methods = options.methods this.render = options.render this.renderedElement = null this.init() } ViewComponent.prototype.init = Function () {enclosing the update ()} ViewComponent) prototype. Update = function () {/ / clean up old DOM let oldRenderedElement = this.renderedElement || this.el this.renderedElement = this.render() let parent = oldRenderedElement.parentNode let sibling = oldRenderedElement.nextElementSibling parent.removeChild(oldRenderedElement) if (sibling) { parent.insertBefore(this.renderedElement, sibling) } else { parent.appendChild(this.renderedElement) } }Copy the code

One more thing: component initialization calls init, so we can create the DOM for the first time in init and perform an update that replaces this.el with the value of render execution

The above code implementation generates the DOM for the first time. The next step is how to trigger subsequent update calls each time the click event callback is executed.

Vue is designed to add get and set hooks to each property in a component’s data. Get collects dependencies and set implements all dependencies.

The render method is called to generate the DOM when the component’s update is executed for the first time. During the render call, a property defined on the component’s data object is used, and the property’s GET hook is executed, using the GET hook to place the update function somewhere. This is where the function will be executed if the property is changed in the future. When the property is changed in the future, its set hook will be executed. From this place, the set hook will fetch the functions that need to be updated and execute them one by one, essentially removing the update and replacing it with the new DOM

Since any property in data can be assigned to change, we call setReactive for each property and add dependencies. That is, each property has a place to store its update, and when it is assigned to change, the update is taken out and executed. In fact, we can imagine that each property under the same component data, their update should be the same, are the component update method

Each call to Update, and hence to render, does not go through the set hook, only the GET hook, because update is only responsible for rendering the DOM generated after render

The first update call is triggered by viewComponent.init and the second and subsequent update calls are triggered by the set hook of the property in data. Why or how is the set hook triggered? These paths usually include event callbacks, Ajax callbacks, timer callbacks, and so on. Each callback corresponds to a “round” DOM update. This “round” concept is very, very important

No matter how many times the update is executed, the data is always changed, so the get and set hooks are added only once. Therefore, it is easy to think of putting the code that adds the GET and set hooks in a place where the code is executed only once. Naturally, we think of the init method, so we can put things that we only need to save once, do once, into the init, so we can do this when the component is initialized:

ViewComponent.prototype.init = function () { setDataReactive(this.data) this.update() } function setDataReactive (data) { let keys = Object.keys(data) for (let i = 0; i < keys.length; i++) { let v = data[keys[i]] if (isObject(v)) { setDataReactive(v) } defineReactive(data, keys[i], v) } } let curExecUpdate = null function defineReactive(data, key, val) { let updateFns = [] Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { if (curExecUpdate && ! updateFns.find(curExecUpdate)) { updateFns.push(curExecUpdate) } return val }, set: function (newVal) { val = newVal for (let i = 0; i < updateFns.length; i++) { updateFns[i]() } } }) }Copy the code

We need to explain the following points in this code:

  1. UpdateFns is a function that will be executed when the key property is changed in the future. Because there will be multiple functions that will be executed when the property is changed, updateFns is an array. Get puts curExecUpdate, the current update, into updateFns. The updateFns is specific to the key attribute, which means that after each call to defineReactive, the updateFns variable is still in memory and will not be reclaimed because it will be used in the future when the attribute is get or set. It is important to understand that each defineReactive execution creates an additional updateFns in memory, so the updateFns variable becomes a closure, and each updateFns corresponds to a property on a data
  2. We can see that val is given the initial value defined in data when we call defineReactive, which is then returned when we get, and changed to the same value when we set. Note that when set is executed, val = newVal is called first. The DOM cannot be updated until the updateFns callback returns the same value as the previous updateFns callback. The DOM cannot be updated until the updateFns callback returns the same value

I don’t know if you’re confused, but we’ve suddenly introduced curExecUpdate, and now we’re going to explain what it is, when it gets assigned, and how it works. CurExecUpdate, as the name implies, is the update that’s currently being executed, and how do we understand this current execution? We can review the flow of our code again:

  • Step 1: Initialize the constructor ViewComponent
  • Step 2: ViewComponent.init executes, and then executes setDataReactive and defineReactive
  • Step 3: Continue this. Update in viewComponent. init
  • Step 4: This.update calls render to generate the DOM structure
  • Step 5: After the user clicks the button to trigger the callback, execute this.update a second time to insert the DOM returned by Render into the HTML

SetDataReactive/defineReactive/setDataReactive/defineReactive/setDataReactive/defineReactive/setDataReactive/defineReactive It is at step 4 that the property on data is actually accessed by our render function, which must have been executed inside the update function, triggering the GET hook (note that the set hook has not been triggered yet)

  • Before the component update executes, we assign the currently executing update to curExecUpdate
  • During component update execution, we trigger the property’s GET hook to put curExecUpdate into the property’s own updateFns
  • CurExecUpdate must be set to NULL after the component update has been executed. Of course, it is not clear why curExecUpdate should be set to NULL until we do the componentization step. Okay
ViewComponent.prototype.init = function () {
  setDataReactive(this.data)

  this.update = this.update.bind(this)
  curExecUpdate = this.update
  this.update()
  curExecUpdate = null
}
Copy the code

With this.update in init done, one round of execution is complete and there’s still one line of code above that doesn’t explain why it exists

this.update = this.update.bind(this)
Copy the code

When the user clicks the button, it will trigger the event callback that we bound to the element in Methods:

switchDetail () { this.data.isShowDetail = ! this.data.isShowDetail }Copy the code

The isShowDetail on data is a reactive property, and assigning to it triggers the set hook for isShowDetail

let curExecUpdate = null function defineReactive(data, key, val) { let updateFns = [] Object.defineProperty(data, key, {... set: function (newVal) { val = newVal for (let i = 0; i < updateFns.length; i++) { updateFns[i]() } } }) }Copy the code

What the set hook does is retrieve the updateFns that were collected and execute them one by one. In this case, the updateFns were this.update that was executed on the first rendering

As you can see, the this.update execution is taken from updateFns one by one

updateFns[i]()
Copy the code

Executing this way also causes the error this points to in updateFns

In other words, the first rendering is done directly through this.update(), which preserves the context of the component, which is lost when the function reference is stored in updateFns and iterated through, which is exactly what this line of code looks like

this.update = this.update.bind(this)
Copy the code

The role of

Render the old DOM and create a new DOM, but this time the value of this.data.isShowDetail is different. This value has already been set to true in switchDetail, so the render part will execute:

render: function () {
  ...
  if (this.data.isShowDetail) {
    children.push(
      this.createElement('div', { class: 'item' }, this.data.good.cpuNum),
      this.createElement('div', { class: 'item' }, this.data.good.memory),
      this.createElement('div', { class: 'item' }, this.data.good.category),
      this.createElement('div', { class: 'item' }, this.data.good.color)
    )
  }
  ...
}
Copy the code

In this way, the item details section is added

When toggle, isShowDetail is set to false, the set hook is triggered again, the update function is triggered, and the render function is triggered. This.data. isShowDetail is false, and the detail part is not generated

After analyzing the whole process, there is another point worth noting: It is easy to see that the rendering of the page and the re-rendering of the user’s actions is the first update, then the final update via events, and each update will trigger the execution of render, and each render will trigger the execution of properties, which is a high-frequency operation. In addition, the same attribute is often accessed repeatedly. Here we have done a relatively simple operation to prevent repeated addition, but there must be room for optimization:

let curExecUpdate = null
function defineReactive(data, key, val) {
  let updateFns = []
  Object.defineProperty(data, key, {
	...
    get: function () {
      if (curExecUpdate && !updateFns.find(curExecUpdate)) {
        updateFns.push(curExecUpdate)
      }
    ...
}
Copy the code

In the end of this section, we’ll do some optimization to the code First of all, for the first time update execution process in fact is essentially depend on to create the DOM were collected for rendering, so we can draw the a method called renderAndCollectDependencies inside

ViewComponent.prototype.init = function () {
  setDataReactive(this.data)

  this.renderAndCollectDependencies()
}
ViewComponent.prototype.renderAndCollectDependencies = function () {
  this.update = this.update.bind(this)
  curExecUpdate = this.update
  this.update()
  curExecUpdate = null
}
Copy the code

Secondly, our own render function, when accessing variables on data or methods on methods, need this.data. XXX to write such a long string, we hope to omit data and methods, simplify writing

After such simplification, its disadvantages are also obvious, that is, a layer of namespace is missing. Variables in data and methods cannot be repeated, which needs to be paid attention to

Vue implements this function in the way of proxy, the specific code is as follows:

  function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }
  
  proxy(vm, "_data", key)
Copy the code

I don’t know why at the beginning of using this method If is me, may be directly on the data, the methods of variables and methods directly copied to this But I guess, may be for the sake of data consistency principle, can imagine, if it is according to my way of thinking, data copy a in the past, if the object type is to say, The reference is the same, but if it is a base type, it means that we are actually modifying different data through this. XXX and this.data. XXX, which will cause data inconsistency and violate the single data principle in software design

In addition, when we add this layer of proxy, the property accessed through this itself has a GET hook, and when we access data via the GET hook, we actually get the property that was collected from the update and placed in the updateFns. As an example, when the code executes to this.isshowdetail, the details are as follows:

  • Triggers hooks for properties on component objects first:
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
Copy the code

So this.isshowdetail gets this._data.isShowDetail, which accesses isShowDetail in data

  • Since isShowDetail on data also adds a GET hook, it will enter the GET hook:
function defineReactive(data, key, val) { ... get: function () { if (curExecUpdate && ! updateFns.find(fn => fn === curExecUpdate)) { updateFns.push(curExecUpdate) } return val },Copy the code

Every access to a property actually goes through these two hooks

Here we also refer to the design of Vue to achieve:

ViewComponent.prototype.init = function () {
  this.initData()
  this.initMethods()

  this.renderAndCollectDependencies()
}
ViewComponent.prototype.initData = function () {
  let keys = Object.keys(this.data)
  for (let i = 0; i < keys.length; i++) {
    setProxy(this, 'data', keys[i])
  }
  setDataReactive(this.data)
}
ViewComponent.prototype.initMethods = function () {
  let keys = Object.keys(this.methods)
  for (let i = 0; i < keys.length; i++) {
    setProxy(this, 'methods', keys[i])
  }
}
function setProxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return target[sourceKey][key]
    },
    set: function (v) {
      target[sourceKey][key] = v
    }
  })
}
Copy the code

As a final note, if we define a property named good on data in our component, and then access the property via this.good in the component’s methods, created hooks, the following happens on a micro level:

  1. Good is an attribute on the component object
  2. Trigger get in the proxy hook added to Good via setProxy
  3. Get returns this.data.good, which accesses good on the object returned by data
  4. Trigger the GET hook added to Good in data via defineReactive
  5. Determine whether curExecUpdate exists to collect dependencies

Finally, when we write a component, we essentially write a large object (we can call it options for the moment). The options object contains data, methods and so on. However, at present, our data is just an object. When we instantiate multiple identical components, we will pass this option to each component, but so many components are changed to the same data data (because the object is a reference type), so we need to give each instance a separate data data, so that they do not affect each other. Data in options when the component is instantiated can be written like this:

new ViewComponent({
  el: '#app',
  data: function () {
    return {
      isShowDetail: false,
      good: {
        id: '8faw8cs4fw9760zt7tnesini4qup5hid',
        name: 'iPhoneX',
        cpuNum: 1,
        memory: 1073741824,
        brand: 'iPhone',
        category: 'phone',
        color: 'black'
      }
    }
  },
Copy the code

Perform the following operations on components:

ViewComponent.prototype.initData = function () {
  let data = isFunction(this.data) ? this.data.call(this) : this.data
  let keys = Object.keys(data)
  this.data = data
  for (let i = 0; i < keys.length; i++) {
    setProxy(this, 'data', keys[i])
  }
  setDataReactive(this.data)
}
Copy the code

In Vue, initData is executed after initProps and initMethods, so in the data function, You can use this to ask about the properties and methods of methods and props. I think it is good to know this. As for my personal habit, I seldom use props or methods directly in data

After writing this section, I have a question: why are setReactive and defineReactive not added to the prototype in the Vue source code? What about independent methods? I hope some god can help explain it

Click here for the complete code for this section