This section is divided into two parts: the first part will not add new code, mainly discusses the component data flow problem. The second part resolves some remaining bugs and implements Mounted hooks

Let’s start with part one

React and Vue both advise us not to modify the props of the child component directly, but to notify the parent component by triggering a callback, which then causes the parent component to change the corresponding data, thus causing the parent component to update the child component

But why? In other words, why does it have to be a one-way data flow? I’m sure some of you might think, “Hey, I used to change the props of the component, and it looks like it’s ok, too. Why bother with an event notification for the parent component?”

We will continue to use the same my-alert method we used in the previous two sections to discuss why we can’t change the props directly. However, we need to change the props of the my-alert method.

We used the render function of the parent App to control whether the my-Alert component is rendered by judging the value of alertVisible. In fact, there is another way to control whether the component is rendered in many component libraries:

render: function () { let children = [ this.createElement('button', { class: 'click-show', on: { click: CreateElement ('my-alert', {content: this.alertinfo, visible: {props: this. this.alertVisible }, on: { close: this.hideAlert } }) if (o) { children.push(o) } return this.createElement('div', { class: 'alert-wrap' }, children) }Copy the code

Unlike the render of the App in the last section, this time we pass a visible property to the my-Alert component, expecting to control the render result based on the visible (i.e., alertVisible on data) that is passed in. When alertVisible is true, Return the DOM after my-Alert is rendered. When alertVisible is false, return null and store the result in variable O. In this way, according to the return value O, it can determine whether my-Alert is pushed into children, and then it can determine whether it is rendered

In the my-Alert component, our definition is as follows:

let MyAlert = { props: { content: { default: '' }, visible: { default: false }, }, data: function () { return { } }, methods: { close: function () { this.notifyParent('close') } }, render: function () { let children = [ this.createElement('div', { class: 'text' }, this.content), this.createElement('button', { class: 'close', on: { click: This. Close}}, 'Close ')] return this.visible? this.createElement('div', { class: 'alert' }, children) : null } }Copy the code

As you can see, we added the visible property in props to take over the values passed by the parent component, and we made the corresponding changes in render

NotifyParent (‘close’) notifies the parent to change alertVisible. NotifyParent (‘close’) notifies the parent to change alertVisible. NotifyParent (‘close’) notifies the parent to change alertVisible. Render the correct result

This is where the big question is: why can’t we just do this.visible = false?

This. Visible = false (); / / this. Visible = false (); / / this. Visible = false (); / / this. This triggers render to get null, which makes my-alert disappear, and everything looks fine

However, it should be noted that through the analysis of the above process, I don’t know if you have any sense that the whole process is completely changed in the internal data of the child component. After the change, the child component renders itself internally, which has nothing to do with its parent component

But don’t forget that there is a value in the parent component that determines whether my-alert is rendered or not. This is the value passed to the child component this.alertVisible

In our example, this. AlertVisible in the parent component is still true, although we changed the value of this.visible in my-alert to false

That’s the crux of the problem. Our page is rendered layer by layer from the topmost component to the child components below the component tree. This time, after changing the props of the child component, it looks fine. The parent component will pass the error alertVisible to the child component this time, and the child component that has disappeared will be rendered again

If you don’t understand the above paragraph, it doesn’t matter. Let’s use a case to illustrate the problem. Let’s change the example of my-alert again and add a function

Add a button that can be clicked to change the notification to the left of the my-Alert, something like this:

The parent component’s Render looks like this:

render: function () { let children = [ this.createElement('button', { class: 'click-show', on: { click: This.showalert}}, this.createElement('button', {class: 'click-show', on: {click: This. ChangeAlertInfo}}, 'tap to change alert')] let o = this. CreateElement ('my-alert', {props: {content: this.alertInfo, visible: this.alertVisible }, on: { close: this.hideAlert } }) if (o) { children.push(o) } return this.createElement('div', { class: 'alert-wrap' }, children) }Copy the code

And you can see that on the new button, we’ve tied an event, changeAlertInfo, and what the event does is very simple, is change the value of alertInfo

In the my-Alert child component, we change the hidden callback event to:

  methods: {
    close: function () {
      // this.notifyParent('close')
      this.visible = false
    }
  },
Copy the code

Next we do the following:

  1. When the page is rendered, there are only two buttons
  2. Click on the “Click show” button and the My-Alert is rendered
  3. Click the “Close” button and my-Alert disappears
  4. Click “Click to change the alert in the alert”, what should be the effect?

If nothing happens, after step 4 is done, there should be no rendering of my-Alert, that is, there should be no reaction on the page, and if you click “Show” again, the my-Alert with new content will be displayed

Since in step 4 I changed this.alertInfo instead of this.alertVisible, my-alert should only be rendered if this.alertVisible becomes true

After clicking “change alert alert” in step 4, my-Alert is rendered with our changed this.alertinfo

Why is this the case?

Let’s go through the steps

  1. After rendering the page for the first time, alertVisible in the App is false, as is visible in my-Alert
  2. Click “Click show” button, change alertVisible value to true, trigger App update, render my-Alert, visible in sub-components is also true
  3. Click the “Off” button to change Visible in my-Alert to False, but notice that alertVisible in the App is still true
  4. This. AlertInfo in the App is changed to the new content by clicking on “Change alert”, which triggers the update -> render method of the App component. Again, alertVisible is set to true, so my-Alert is rendered. The alertInfo content has also been updated

Having said so much, I really want to express a point: Child components by props renders the state, in fact is ultimately determined by the parent component, child components of props is just made a data transfer, and if modify props, father and son can cause components to record the value of the state are not synchronized, so after the father when the component rendering subcomponents again, will take old data rendering, Results in an error

That’s the end of part ONE

The complete code

Next we see a bug in our existing code, the initial rendering, collect renderAndCollectDependencies dependent method, we put the current rendering component of the update method is assigned to the variable curExecUpdate

After the initial rendering is complete and dependencies are collected, curExecUpdate is set to null, and the global variable curExecUpdate is assigned to the component when the next component instantiates

The problem with this is that curExecUpdate will be overwritten if there are multiple components

What does that mean?

We can start by looking at the flow of a component:

Please note the changes to curExecUpdate in red:

A component may have multiple elements in it, so it is possible for a component to render multiple calls to createElement to create a DOM object, and for each call to createElement to fetch data. When it reads, it fires the get hook for that data, putting curExecUpdate into its own updateFns array

If a new component is created in createElement, it will become:

Notice the black box in the figure, which represents the execution of a child component

As you can see from the above execution flow, the parent component may encounter a custom tag during the creation of the element, and then create the child component. After the child component is created, the parent component may create the DOM under the createElement, and then the problem arises

CurExecUpdate is a global variable that is set to null when the child component is created. If the DOM is created using data from the parent component and curExecUpdate is set to null when the child component is created, What’s the point of triggering the GET hook to collect a null curExecUpdate value into the updateFns array?

So, I indicated in the diagram that the createElement does not collect dependencies

The main reason we didn’t expose this problem before was that we didn’t create the DOM in the parent component after the child component was created

Update (); update (); update ();

let updateStack = []
function pushCurExecUpdateToStack (fn) {
  updateStack.push(fn)
  curExecUpdate = fn
}

function popCurExecUpdateFromStack () {
  updateStack.pop()
  curExecUpdate = updateStack[updateStack.length - 1]
}
Copy the code

Initialize each component, the execution of renderAndCollectDependencies it, take this. Update a push into stack, enclosing the update after execution, it pop out

If there are nested child components, the parent component’s this.update pops out at the top of the stack

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

A mounted hook is used when the DOM of a component is created. In Vue, this hook supports arrays, functions, and so on

ViewComponent.prototype.renderAndCollectDependencies = function () { this.update = this.update.bind(this) pushCurExecUpdateToStack(this.update) this.update() popCurExecUpdateFromStack() let fn = this.$options.mounted if (fn &&  isFunction(fn)) { fn.call(this) } }Copy the code

Mounted hook: Mounted hook / / Mounted initializes some parameters or identifier variables, which involves accessing methods and data. / / Mounted assigns values to data, which triggers the set hook. The methods in updateFns (which are essentially updates to the component) are taken out and executed one by one

Note that this is the second execution of this.update

When is this. Update first executed?

The first time is when the dependencies are collected during initialization

There are also Created hooks in Vue. Assigning values to data in a Created hook does not trigger the component render before the DOM is rendered

The complete code