深入浅出MV*框架源码(二):Moon的code->html实现

前言

可以说,MV*框架最核心的三个点就是

  1. 模板怎么转化成代码的?
  2. 代码又是怎么转化成模板的?
  3. 模板的依赖和代码中的数据是怎么响应式关联起来的?

这篇文章我们一起来探究一下第二个点:code->html,因为这个步骤Moon提供了现成的api:render。

render的使用

我们先来看看官方的用例:

new Moon({
  render: function(m) {
    return m('h1', {attrs: {}}, {shouldRender: false}, [m("#text", {shouldRender: false}, "Hello Moon!")]);
    // same as <h1>Hello Moon!</h1>
  }
});

可以看出,m(...)将会和在HTML里直接写<h1>Hello Moon!</h1>造成一样的效果

m函数的实现

Yeah,我们现在知道关键就在于这个m函数了,其实它和Vue中的h函数也是类似的。
由于JS运算符优先级的规则,会先调用m("#text", {shouldRender: false}, "Render Moon!"):

render-m.jpg

m函数传入tag, attrs, meta, children四个参数,可以看出'#text'对应type、{shouldRender: false}对应attrs、"Render Mooin"对应meta、undefined对应children,返回一个createElement函数调用的结果,我们再看注释,发现这个createElement函数调用完后返回的其实就是一个VNode。

同理我们可以推测出在内层m函数执行完之后,外层m函数调用时'h1'对应type、attrs: {}对应attrs、{shouldRender: false}对应meta、[m("#text", {shouldRender: false}, "Render Moon!")]对应children。

所以,这个m函数负责把code转成VNode。

createElement

让我们再进入createElement函数:


render-createElement.jpg

它很简单有木有~就是返回了只有五个键的一个对象,而这个对象就是我们俗称的VNode!

从这个过程中,我们发现m函数其实还有一个解析组件的流程,它的判断条件很耐人寻味:

(component = components[tag]) !== undefined
//等价于
(component = components[tag]) && component !== undefined

这里涉及到一个全局变量:components,它是在index.js里声明的,用来存储当前实例的组件,所以其实这里就是判断render函数渲染的标签是不是我们自己定义的。

如果是就会创建一个函数式的组件或者让meta的component属性引用这个组件,至于细节,等我们以后看到组件部分再分析。

总之,m函数非常关键,作者也给了注释:无论怎么样m函数都会返回一个VNode的数据:

{
       type: 'h1', <= nodename
       props: {
         attrs: {'id': 'someId'}, <= regular attributes
         dom: {'textContent': 'some text content'} <= only for DOM properties added by directives,
         directives: {'m-mask': ''} <= any directives
       },
       meta: {}, <= metadata used internally
       children: [], <= any child nodes
}

调用m的前奏

了解完m函数的实现细节后,我们执行代码,程序会先进入Moon里定义$render:

//defineProperty其实就是this[$render]=options.render
defineProperty(this, "$render", options.render, noop);

继续走下去,我们就进入了init和mount函数,可以看出它设置了当前实例的$el、$destroyed、$template、$render属性(可选),然后调用build函数和触发mounted钩子。


render-mount.jpg

在build函数里调用render和m

到build里就是进入正片了:


render-build.png

render实际上是调用$render(也就是用户传进去的render):


render-VNode.jpg

dom变量存储了render生成的(实际上是m生成的)新VNode、old存储当前的Node(可能是原生DOM节点也有可能是VNode),接着调用patch函数对两个node做一些事情。

patch:vnode->html

进入patch函数,可以发现old对应#app4这个DOM节点,vnode对应render生成的VNode节点,parent是body元素。


render-patch.jpg

因为old没有meta属性,所以不是VNode。(VNode有meta属性)于是接着判断old是不是Node类型。

很明显,是hydrate函数把VNode变成一个原生DOM的newNode的。

之后还对比了newNode和old,从例子中可以看出显然h1和app4不一样,这时当前实例的$el就被替换成了vnode中的元素,当前moon实例也被替换掉了。原先#app4的位置就变成了h1元素。

再看看没进入的那个流程:即old有meta属性时,也就是old也是一个VNode时(DOM树上的node没有meta属性),这个时候会直接使用一个createNodeFromVNode和replaceChild方法更改html元素,关于这两个方法,我们后面会提到。

hydrate:真正的执行者

hydrate函数的执行流程非常复杂,所以我画了个流程图:


render-hydrate.png

这里的node即之前patch函数中的old节点,vnode即render函数生成的节点,parent是实例的根元素。

可以看出它是先获取了node的nodeName,然后进入一个非常复杂的判断:这一系列的判断目的只有一个,那就是把vnode的内容转化到浏览器DOM上去。

在我们的例子中,node是原生的#app4DOM节点,vnode是render生成的h1虚拟节点,所以需要调用createNodeFromVNode函数。

node和vnode的hydrate

这个过程会涉及到createNodeFromVNode、diffProps、replaceChild、Moon.destroy、Moon.off、callHook、extractAttrs、addEventListeners、removeChild等方法

  1. createNodeFromVNode根据vnode对象创建一个真实node:


    createNodeFromVNode.jpg

    先根据vnode的类型创建一个元素,类型是文本或SVG的话就创建文本或SVG元素。
    如果只有一个子元素的时候,直接混入。有多个子元素的时候需要迭代调用appendChild函数并把子VNode也通过createNodeFromVNode转化成DOM的Node。
    最后根据vnode.meta.eventListeners给这个Node添加事件监听逻辑、通过diffProps设置属性(包括attr、prop、directive)、Hydrate(也就是把node作为vnode.meta的一个属性,方便两者同步更新)。
    可以说这个hydrate让vnode和node你中有我,我中有你了。

  2. diffProps
    接下来让我们看看diffProps是如何设置属性的:

    diffProps.jpg

    进入以后我们发现其实是对attrs、props进行diff并且执行相关的directive,关于attr和props的区别不明白的读者可以自行搜索一下,简单来说就是attr存在于html元素上,props存在于DOM元素上。
    2.1. 对于attr(作者注释为Node Props),vnode只是一个参照物,始终操纵node,如果node有vnode没有就remove这个attr,如果node没有vnode有就add这个attr。
    2.2. 对于prop(作者注释为DOM Props),会从vnode.props.dom中拿出prop去匹配node,如果没有匹配到就给node加上。
    2.3. 对于directive,会从vnode.props.directives拿出directive去匹配directives,如果匹配到了就执行相关指令。

  3. replaceChild
    这个函数会替换一个子元素,其实就是封装了dom原生提供的replaceChild

    replaceChild.jpg

    它会先销毁当前moon的实例(componentInstance),然后用newNode替换oldNode,最后通过createComponentFromVNode对里面存在的组件进行转化(如果有的话)。
    3.1 destroy
    顺便看下销毁实例的函数:
    destroy.jpg

    里面非常简洁:移除事件监听->移除dom引用->$destroy标志位置为真->调用destroy钩子
    3.2 off
    再来看看怎么移除事件监听的:
    off.jpg

    产生了三条分支:事件名参数为空->移除所有事件;回调名参数为空->移除所有事件回调;参数都存在->移除这个特定的事件回调

  4. callHook
    顺理成章地会走调用钩子的函数


    callHook.jpg

    两步搞定:获取这个钩子->当前实例调用这个钩子方法

  5. extractAttrs


    extractAttrs.jpg

    把当前node的attrs抽离出来,返回一个attrs对象。

  6. addEventListeners
    这个需要在render函数的meta参数里传入一个eventListeners对象才可以触发:
    eventListeners: { "click": [function() { console.log("click") },function() { console.log("click2") } ], "dblclick": [function() { console.log("double") }] }

    addEventListeners.jpg

    它首先会先声明一个addHandler函数,这是个闭包函数,然后遍历eventListeners逐个调用这个addHandler函数。
    在addHandler里面又声明了一个handle函数,它的目的就是把某个具体事件相关的回调全部调用一遍。至于这些回调,都存储在handle.handlers里。
    这些原来声明的回调数组就转化成了一个handle函数,最后把这个handle函数才是node的实际事件回调。

  7. removeChild
    这个函数和replaceChild类似,都是封装了原生的同名api:


    removeChild.jpg

    因为原来的node有子元素,所以就需要移除。hydrate的后半部分也是围绕子元素展开的:首先把vnode的child和node的child获取到,然后广度遍历node的child和vchild,逐个和vchild进行hydrate


    hydrate-child.jpg

总结

之后,调用栈就沿着hydrate->patch->build->mount->init一步步回退,结束了整个过程。也就是说,code->html转开就是code->vnode->node->html的过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容