1. Renderer
Renderers are used to complete rendering operations, such as on browser platforms, the renderer can convert virtual DOM to real DOM.
2. A simple example to understand the working process of the renderer in Vue
const { effect, ref } = VueReactivity function renderer(domString, container) { = domString } const count = ref(1) effect(() => { renderer(`<h1>${}</h1>`, ('app')) }) ++
We passed@vue/reactivity
Refer to vue's responsive API, usingeffect
(Side Effects Function) andref
(generate responsive data) makescount
andrenderer
Build connections, like thiscount
When changes are made, it will be calledrenderer
Process the rendering andh1
Tag mounted to idapp
on the element. This is a simple renderer implementation.
3. Several operations involved in the renderer: mount, update, and uninstall
Suppose we already have a rendererrenderer
:
const renderer = createRenderer()
There is a data that describes the nodes of the elementvnode
: Similar to the following structure
// type is type, children is child nodeconst vnode = { type: 'h1', children: 'hello' }
You can see that this is a hello text inside<h1>
node
Then there will be several situations in the actual working process of the renderer:
(vnode, ('#app'))
The operation involved at this time isMount, just change the vnode to the DOM element and put it inside the app element
(newVNode, ('#app'))
becauseThere are already old nodes, so it cannot be simply mounted directly, it needs to beComparison of old and new nodes, find the part that needs to be updated and then change it. The operation at this time isrenew, In order to handle updates, there needs to be corresponding logic in the renderer.
(null, ('#app'))
The new node is nullThis means that the renderer needs to be clearedapp
The contents in the element, the operation involved at this time isuninstall
According to the processing of the above three situations, the renderer can be written as:
function createRenderer() { function patch(n1, n2, container) { } function render(vnode, container) { if (vnode) { // The new vnode exists, pass it to the patch function together with the old vnode for patching patch(container._vnode, vnode, container) } else { if (container._vnode) { // The old vnode exists, and the new vnode does not exist, which means it is an unmount operation // Just clear the DOM in the container = '' } } //Storing vnode to container._vnode, that is, the old vnode in subsequent rendering container._vnode = vnode } return { render } }
Use createRenderer to repeat the above three renderings and analyze the process:
// First rendering(vnode, ('#app')) // The second render(newVNode, ('#app')) //The third render(null, ('#app'))
- First rendering:
vnode
Being rendered and existscontainer._vnode
middle; - Second rendering: old and new
vnode
All exist, need to bepatch
Updates in the function; - The third rendering: new
vnode
fornull
Judge old nodescontainer._vnode
Does it exist, if the old onevnode
If present, the processing is uninstall;
4. How to implement a platform-independent renderer:
A general renderer should be platform-independent.That is, the rendering function of the renderer cannot be only effective in a certain platform.. To achieve this goal, we first implement a browser-based renderer and observe in which steps the browser-related API is used. Then we can consider it.Extract these APIs as configuration items, this allows you to implement a general renderer.
1. Implement a renderer that depends on the browser API
From the previous onevnode
As an example:
const vnode = { type: 'h1', children: 'hello' }
-
type
for label type, -
children
is a child element.
Based on the above implementation, we first improvepatch
Functions used to handle node mounts and updates
patch(container._vnode, vnode, container)
Through the previous code, you can see thatpatch
The function will accept three parameters, namelyOld nodes, new nodes, and containers,We judge the old node parameters in it, so as to handle different operations: At present, only the mount situation is analyzed:
function patch(n1, n2, container) { if (!n1) { mountElement(n2, container) } else { // } }
As shown in the above code:n1
That is, when the old node does not exist, it is proved to be a mount operation, then it is called directlymountElement
Mounting the new node
function mountElement(vnode, container) { const el = createElement() if (typeof === 'string') { = } (el) }
existmountElement
We have simply processed the mount logic:
1. UsecreateElement
According to the vnodetype
to create the corresponding DOM type:like='h1'
Then el is<h1></h1>
2. Make judgments:ifIt is a string type to prove that the child node is a text node, and the element is directly used.
textContent
Attribute setting element.
3. Call appendChild to add the processed elements to the target container. This mount is completed.
2. Implement a general renderer
The renderer implemented aboveThere is a dependency on the browser's API, among themappendChild
、createElement
、textContent
These are APIs that require a browser environment to execute. If you want the renderer to be universal, you need to remove dependencies on these APIs.
Solution: Pass these APIs as configuration items into the renderer to create functions, so we can define it ourselvescreateRenderer
Which APIs are used to perform rendering operations so that the renderer's work no longer depends on a certain platform.
const renderer2 = createRenderer({ //Create an element createElement(tag) { return { tag } }, //Set the element text content setElementText(el, text) { (`set up ${(el)} Text content:${text}`) = text }, //Insert the element into the target node insert(el, parent, anchor = null) { = el } })
We put the logic and API of creating elements intocreateElement
In, put the API to set text content insetElementText
In, put the API procedure of mounting the element to the containerinsert
middle. Then use the incoming configuration to callmountElement
function mountElement(vnode, container) { const el = createElement() if (typeof === 'string') { setElementText(el, ) } insert(el, container) }
This way we can pass increateRenderer
Functionaloptions
Configuration, what methods are used in different platforms to perform elements creation, processing of element content, and mounting of elements.
The entire rendering function is as follows:
function createRenderer(options) { const { createElement, insert, setElementText } = options function mountElement(vnode, container) { const el = createElement() if (typeof === 'string') { setElementText(el, ) } insert(el, container) } function patch(n1, n2, container) { if (!n1) { mountElement(n2, container) } else { // } } function render(vnode, container) { if (vnode) { // The new vnode exists, pass it to the patch function together with the old vnode for patching patch(container._vnode, vnode, container) } else { if (container._vnode) { // The old vnode exists, and the new vnode does not exist, which means it is an unmount operation // Just clear the DOM in the container = '' } } //Storing vnode to container._vnode, that is, the old vnode in subsequent rendering container._vnode = vnode } return { render } }
Therefore, a simple renderer has been implemented, but in actual cases, the mount and update of elementsThere are more details that need to be processed, such as attributes, classes, and how to correctly mount events, how to improve the efficiency of renderer updates (diff algorithm), etc., these will be discussed in subsequent articles.
The above is a detailed explanation of the simple implementation of the renderer in Vue. For more information about Vue implementing the renderer, please pay attention to my other related articles!