Writing in the front
This is the fourth vuE2 series implementation from scratch, adding the virtual DOM to YourVue.
When implementing the VUE process in the first article, I parsed the template into an AST, directly generating the real DOM. This is not how vUE is implemented. The actual implementation is to use gencode to generate the render function from the AST generated by Parse (Template), and then execute the render function to generate the VNode and build the virtual DOM tree. The real DOM is then created from the virtual DOM tree.
This article will be first updated on our official account: BUPPT. Code repository: github.com/buppt/YourV…
The body of the
VNode
First of all, we define the node of the virtual DOM, VNode, which stores nothing more than tags, attributes, child nodes, text content, etc.
export class VNode{
constructor(tag, data={}, children=[], text=' ', elm, context){
this.tag=tag;
this.props=data ;
this.children=children;
this.text=text
this.key = data && data.key
var count = 0;
children.forEach(child= > {
if(child instanceof VNode){
count += child.count;
}
count++;
});
this.count = count; }}Copy the code
render
Define the four basic render functions using the same _c and _s names as vue, each simply creating a different VNode.
- _c Creates a normal VNode with a tag
- _v Creates a VNode for the text node
- _s is the toString function that converts a variable to a string
- _e creates an empty VNode
export default class YourVue{
_init(options){
initRender(this)}}export function initRender(vm){
vm._c = createElement
vm._v = createTextVNode
vm._s = toString
vm._e = createEmptyVNode
}
function createElement (tag, data={}, children=[]){
children = simpleNormalizeChildren(children)
return new VNode(tag, data, children, undefined.undefined)}export function createTextVNode (val) {
return new VNode(undefined.undefined.undefined.String(val))
}
export function toString (val) {
return val == null
? ' '
: Array.isArray(val)
? JSON.stringify(val, null.2)
: String(val)
}
export function createEmptyVNode (text) {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
export function simpleNormalizeChildren (children) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
Copy the code
If a child element is an array, concat the element in the array onto children.
Because children should be vNodes, v-for and slot add an array element to children, and the vnodes in these array elements are parallel to the vnodes in children. So we expand the child element only one level.
gencode
What Gencode does is generate the AST that will be parsed, using the render function above to generate the VNode string code.
The code generated by Gencode needs to be read in conjunction with the arguments to the previous render function, such as _c with tag as the first argument, element attributes as the second argument, and child nodes as the third.
_c('h4', {attrs: {"style":"color: red"}},[
_v(_s(message))]),
_v(""),
_c('button', {on: {click:decCount}},[_v("decCount")])Copy the code
The generated code is also nested like a DOM tree, with the outermost layer of a Node. type === 1 element node. Using the _c function, the element attributes are passed to VNode as the second argument, and the remaining elements are structurally stored in the third children argument.
If node.type === 3 indicates a plain text node, use json.stringify (Node.text) directly.
If Node. type === 2 indicates a text node with variables, use Node. expression generated by parse.
export function templateToCode(template){
const ast = parse(template, {})
return generate(ast)
}
export function generate(ast){
const code = ast ? genElement(ast) : '_c("div")'
return `with(this){return ${code}} `
}
function genElement(el){
let code
let data = genData(el)
const children = el.inlineTemplate ? null : genChildren(el, true)
code = `_c('${el.tag}'${
data ? `,${data}` : ' ' // data
}${
children ? `,${children}` : ' ' // children
}) `
return code
}
export function genChildren (el){
const children = el.children
if (children.length) {
const el = children[0]
return ` [${children.map(c => genNode(c)).join(', ')}] `}}function genNode (node) {
if (node.type === 1) {
return genElement(node)
} else if (node.type === 3 && node.isComment) {
return `_e(The ${JSON.stringify(node.text)}) `
} else {
return `_v(${node.type === 2
? node.expression
:JSON.stringify(node.text)
}) `}}function genData(el){
let data = '{'
if (el.attrs) {
data += `attrs:${genProps(el.attrs)}, `
}
if (el.props) {
data += `domProps:${genProps(el.props)}, `
}
if (el.events) {
data += `on:${genHandlers(el.events)}, `
}
data = data.replace($/ /,.' ') + '} '
return data
}
function genHandlers(events){
let res = '{'
for(let key in events){
res += key + ':' + events[key].value
}
res += '} '
return res
}
Copy the code
To an executable function
With (this){return ${code}}, how do you turn it into an executable function? New Function.
export default class YourVue{
$mount(){
const options = this.$options
if(! options.render) {let template = options.template
if (template) {
const code = templateToCode(template)
const render = new Function(code).bind(this)
options.render = render
}
}
const vm = this
new Watcher(vm, vm.update.bind(vm), noop)
}
}
Copy the code
We have converted the render function template into a VNode and attached it to the options property of YourVue instance. How do we convert the render function into a real dom? See the next article.