从零写一个 Vue(四)虚拟 DOM
写在前面
本篇是从零实现 vue2 系列第四篇,为 YourVue 添加虚拟 dom。
之前在第一篇实现 vue 流程的时候,将模版解析成 ast,直接生成了真实 dom。这并不是 vue 的实现方式,真正的实现方式是将 parse(template)
生成的 ast 通过 gencode 生成 render 函数,然后执行 render 函数生成 VNode,构建虚拟 dom 树,然后通过虚拟 dom 树创建真实 dom。
代码仓库:https://github.com/buppt/YourVue
正文
VNode
首先我们先定义虚拟 dom 的节点 VNode,存储的内容无非就是 tag、属性、子节点、文本内容等。
1export class VNode{
2 constructor(tag, data={}, children=[], text='', elm, context){
3 this.tag=tag;
4 this.props=data ;
5 this.children=children;
6 this.text=text
7 this.key = data && data.key
8 var count = 0;
9 children.forEach(child => {
10 if(child instanceof VNode){
11 count += child.count;
12 }
13 count++;
14 });
15 this.count = count;
16 }
17}
render
再定义四个基本的 render 函数,使用和 vue 相同的 _c、_s 命名方式,每一个都很简单,就是分别创建不同的 VNode。
_c 创建正常的带有 tag 的 VNode
_v 创建文本节点对应的 VNode
_s 就是将变量转成字符串的 toString 函数
_e 用来创建一个空的 VNode 节点
1export default class YourVue{
2 _init(options){
3 initRender(this)
4 }
5}
6export function initRender(vm){
7 vm._c = createElement
8 vm._v = createTextVNode
9 vm._s = toString
10 vm._e = createEmptyVNode
11}
12function createElement (tag, data={}, children=[]){
13 children = simpleNormalizeChildren(children)
14 return new VNode(tag, data, children, undefined, undefined)
15}
16
17export function createTextVNode (val) {
18 return new VNode(undefined, undefined, undefined, String(val))
19}
20
21export function toString (val) {
22 return val == null
23 ? ''
24 : Array.isArray(val)
25 ? JSON.stringify(val, null, 2)
26 : String(val)
27}
28export function createEmptyVNode (text) {
29 const node = new VNode()
30 node.text = text
31 node.isComment = true
32 return node
33}
34
35export function simpleNormalizeChildren (children) {
36 for (let i = 0; i < children.length; i++) {
37 if (Array.isArray(children[i])) {
38 return Array.prototype.concat.apply([], children)
39 }
40 }
41 return children
42}
这其中 createElement 时需要把 children 展开一层,如果 children 中某个子元素是数组,就把子元素数组中的元素 concat 到 children 上。
因为 children 中都应该是 VNode,像后面会实现的 v-for 和 slot 都向 children 中添加了数组元素,其实这些数组元素中的 VNode 和 children 中的 VNode 是并列的 dom 元素。
gencode
gencode 的作用就是生成将 parse 生成的 ast,通过上面 render 函数生成 VNode 的字符串代码。
通过 gencode 生成的代码需要结合前面的 render 函数的参数来阅读,比如 _c 第一个参数是 tag,第二个参数是元素属性,第三个是子节点。
1_c('h4',{attrs:{"style":"color: red"}},[
2 _v(_s(message))]),
3 _v(" "),
4 _c('button',{on:{click:decCount}},[_v("decCount")
5])
生成的代码也是类似 dom 树的嵌套结构,最外层是一个node.type === 1的元素节点,使用 _c 函数,将元素属性通过第二个参数传入 VNode,其余元素按结构保存到第三个 children 参数中。
如果 node.type === 3
说明是纯文本节点,直接使用JSON.stringify(node.text)
。
如果 node.type === 2
说明是带有变量的文本节点,使用 parse 生成的 node.expression
。
1export function templateToCode(template){
2 const ast = parse(template, {})
3 return generate(ast)
4}
5export function generate(ast){
6 const code = ast ? genElement(ast) : '_c("div")'
7 return `with(this){return ${code}}`
8}
9
10function genElement(el){
11 let code
12 let data = genData(el)
13 const children = el.inlineTemplate ? null : genChildren(el, true)
14 code = `_c('${el.tag}'${
15 data ? `,${data}` : '' // data
16 }${
17 children ? `,${children}` : '' // children
18 })`
19 return code
20}
21
22export function genChildren (el){
23 const children = el.children
24 if (children.length) {
25 const el = children[0]
26 return `[${children.map(c => genNode(c)).join(',')}]`
27 }
28}
29function genNode (node) {
30 if (node.type === 1) {
31 return genElement(node)
32 } else if (node.type === 3 && node.isComment) {
33 return `_e(${JSON.stringify(node.text)})`
34 } else {
35 return `_v(${node.type === 2
36 ? node.expression
37 :JSON.stringify(node.text)
38 })`
39 }
40}
41
42function genData(el){
43 let data = '{'
44 if (el.attrs) {
45 data += `attrs:${genProps(el.attrs)},`
46 }
47 if (el.props) {
48 data += `domProps:${genProps(el.props)},`
49 }
50 if (el.events) {
51 data += `on:${genHandlers(el.events)},`
52 }
53 data = data.replace(/,$/, '') + '}'
54 return data
55}
56
57function genHandlers(events){
58 let res = '{'
59 for(let key in events){
60 res += key + ':' + events[key].value
61 }
62 res += '}'
63 return res
64}
转成可执行函数
可以看到最后生成的字符串代码是 with(this){return ${code}}
,那么怎样将它变成可执行的函数呢?就是通过 new Function
来实现啦。
1export default class YourVue{
2 $mount(){
3 const options = this.$options
4 if (!options.render) {
5 let template = options.template
6 if (template) {
7 const code = templateToCode(template)
8 const render = new Function(code).bind(this)
9 options.render = render
10 }
11 }
12 const vm = this
13 new Watcher(vm, vm.update.bind(vm), noop)
14 }
15}
这样我们就把 template 转换成可以生成 VNode 的 render 函数挂到 YourVue 实例的 options 属性上了,这篇内容信息量已经非常多了,如何将 render 函数转换成真实 dom 呢?请看下篇文章。