前言
Vue的解析过程: 读取template => 解析生成AST => 拼接成render字符串 => new Function执行render字符串生成vnode => patch生成真实DOM
今天我们就从“render字符串 ”阶段开始,化繁为简,从Vue源码中抽取核心代码,然后进行手写,实现从render字符串到真实DOM的渲染
注:
当前都不涉及动态属性,因为动态属性就是使用了with函数改变作用域从而实现数据访问
下面全文所说的vnode在Vue里面其实也就是一个存数据的class类,这里使用一个对象表示vnode
本人由于表达能力不好,因为会尽量多到少说话,多做事的...
手写实现
1.解析DOM
解析成vnode,render函数实现
模板:
<div id="app">hello world</div>
解析后的render字符串
_c('div',{attrs:{"id":"app"}},[_v(" hello world ")])
实现 _v 方法
function _v(val){
return { text: val }
}
实现 _c 方法
function _c(tag, data, children){
//规范数据格式,如果当前标签不存在attributes时
//生成的函数data里面的value就是其children
if (Array.isArray(data)) {
children = data
data = undefined
}
return { tag: tag, data: data, children}
}
没看错,就这样简单,这样子下来将render字符串执行后得到的结果为
let vnode = _c('div',{attrs:{"id":"app"}},[_v(" hello world ")])
//vnode:
{
tag: "div",
data: { "id":"app" },
children: [ { text: 'hello world' } ]
}
vnode渲染,patch函数实现
我们知道,
获取当前DOM元素的父级,以及当前DOM元素是父级的第几个元素
function insert(parent, elm, ref) {
if (ref !== undefeind) {
parent.insertBefore(elm, ref)
} else {
parent.appendChild(elm)
}
}
function createElm(vnode, parentElm, refElm) {
if (vnode.tag !== undefined) {
//DOM节点
vnode.elm = document.createElement(elm.tag)
if (Array.isArray(vnode.children)) {
for (child of vnode.children) {
createElm(child, vnode.elm, null)
}
}
insert(parentElm, vnode.elm, refElm)
} else {
//文本节点
vnode.elm = document.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
function patch(oldvnode,vnode){
if(oldvnode.nodeType !== undefined){
oldvnode = { tag: oldvnode.tagName.toLowerCase(), elm: oldvnode }
}else{
//diff比较差异阶段
}
let parentElm = document.getElementById('app').parentNode
//挂载真实DOM
createElm(
vnode,
parentElm,
document.getElementById('app').nextSibling
)
//删除模板
if(parentElm !== null){
parentElm.removeChild(oldvnode.elm)
}
}
测试
let vnode = _c('div', { attrs: { "id": "app" } }, [_c('p', [_v("hello world")])])
let realDom = patch(document.getElementById('app'), vnode) console.log(realDom);
//<div><p>hello world</p></div>
小节
_c函数的作用其实就是识别标签类型然后创建对应的vnode对象,作用仅此而已,不用被render字符串的复杂性搞乱头像,
patch函数是对vnode进行递归遍历然后生成真是DOM保存刀vnode.elm属性里面并且根据关系挂载到页面上
2.解析组件
解析成vnode,render函数实现
模板
<div id="app"><children></children> </div>
//children组件
children: { template: '<p>children hello world</p>'}
解析后的render字符串
_c('div',{attrs:{"id":"app"}},[_c('children')])
组件是作为特殊的一个载体,内容都在内部,就好像一个大球套着一个小球,当前解析只是解析到了div和children标签,组件children内的内容并没有被解析到,那children里面的内容何时解析呢?这里我们知道“children”它不是内置的html标签,因此我们需要定义一个数组来区分内置html标签和自定义标签,修改_c函数
function _c(tag, data, children){
//规范数据格式,如果当前标签不存在attributes时
//生成的函数data里面的value就是其children
if (Array.isArray(data)) {
children = data
data = undefined
}
+ let vnode = {}
+ let isHTML = ['div','p','span','ul','li']
+ if(isHTML.some(tag => tag)){
+ //内置html标签
+ vnode = { tag: tag, data: data, children}
+ }else if(/** 这里需要判断配置项components里面是否存在有与tag对应的组件 */){
+ //自定义组件
+ vnode = {
+ tag: 'vue-component-'+ 全局递增id +tag,
+ data,
+ children,
+ componentOptions: {
+ tag,
+ //Ctor就是从component里面得到的组件配置项children,然后通过Vue.extend生成的一个构造器
+ Ctor,
+ componentInstance: null //Ctor组件被new后返回的实例化对象
+ }
+ }
+ }else{
+ //既不是html标签也不是组件
+ vnode = { tag, data, children }
+ }
+ return vnode
- return { tag: tag, data: data, children }
}
Vue的每个组件其实都是通过Vue.extend方法继承得到的,在生成vnode阶段,如果解析到了某个组件的标签,那么就去配置项component里面找到对应的组件,然后获取组件的对应的配置项data, method, template这些属性,然后以这个配置项继承得到一个组件构造器保存到vnode里面
let vnode = _c('div',{attrs:{"id":"app"}},[_c('children')])
//vnode:
{
tag: "div",
data: { attr: { "id": "app" } }
[ { tag: "vue-component-1-children", data: null, children: null componentOptions: { tag: "children", Ctor, componentInstance: null } } ]
}
vnode渲染,patch函数实现
人狠话不多,直接对createElm函数进行改造
function createElm(vnode, parentElm, refElm) {
+ if(vnode.componentOptions !== null){
+ let child = new vnode.componentOptions.Ctor(vnode)
+ //开始进入child的template解析阶段
+ child.$mount(vnode.elm)
+ }
if (vnode.tag !== undefined) {
//DOM节点
vnode.elm = document.createElement(elm.tag)
if (Array.isArray(vnode.children)) {
for (child of vnode.children) {
createElm(child, vnode.elm, null)
}
}
insert(parentElm, vnode.elm, refElm)
} else {
//文本节点
vnode.elm = document.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
就这样简单,因为createElm是递归解析vnode的,如果遇到组件之间new它的构造器,然后调用其mount方法是Vue.extend继承的时候得到的,这时候就开始进入了组件的解析环境,一直到解析到最深度的一个组件,因此这就是Vue的生活钩子函数为什么是:
created(父) created(子1) created(子2) mounted(子2) mounted(子1) mounted(父)的原因
用一个脑图来表示解析过程就是:
总结:
当前是省略了大量的细节,简单的介绍了一下Vue里面的组件是如何渲染成DOM元素,这里只是简单的介绍了一下组件如何渲染,并没有包括组件的传值,插槽等,这些只是附加内容,因为创建vnode时有父子关系,实现传值,插槽这些只需要通过父子关系就可以得到对应的数据了,而DOM的动态属性,事件,以及生命钩子这些因为vnode里面保存了最完整的属性,因此只需要在对应阶段解析其属性即可