深入浅出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 

Hello Moon!

} });

可以看出,m(...)将会和在HTML里直接写

Hello Moon!

造成一样的效果

m函数的实现

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

深入浅出MV*框架源码(二):Moon的code->html实现_第1张图片
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函数:


深入浅出MV*框架源码(二):Moon的code->html实现_第2张图片
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钩子。


深入浅出MV*框架源码(二):Moon的code->html实现_第3张图片
render-mount.jpg

在build函数里调用render和m

到build里就是进入正片了:


深入浅出MV*框架源码(二):Moon的code->html实现_第4张图片
render-build.png

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


深入浅出MV*框架源码(二):Moon的code->html实现_第5张图片
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元素。


深入浅出MV*框架源码(二):Moon的code->html实现_第6张图片
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函数的执行流程非常复杂,所以我画了个流程图:


深入浅出MV*框架源码(二):Moon的code->html实现_第7张图片
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:


    深入浅出MV*框架源码(二):Moon的code->html实现_第8张图片
    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是如何设置属性的:

    深入浅出MV*框架源码(二):Moon的code->html实现_第9张图片
    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。

    深入浅出MV*框架源码(二):Moon的code->html实现_第10张图片
    replaceChild.jpg

    它会先销毁当前moon的实例(componentInstance),然后用newNode替换oldNode,最后通过createComponentFromVNode对里面存在的组件进行转化(如果有的话)。
    3.1 destroy
    顺便看下销毁实例的函数:
    深入浅出MV*框架源码(二):Moon的code->html实现_第11张图片
    destroy.jpg

    里面非常简洁:移除事件监听->移除dom引用->$destroy标志位置为真->调用destroy钩子
    3.2 off
    再来看看怎么移除事件监听的:
    深入浅出MV*框架源码(二):Moon的code->html实现_第12张图片
    off.jpg

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

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


    深入浅出MV*框架源码(二):Moon的code->html实现_第13张图片
    callHook.jpg

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

  5. extractAttrs


    深入浅出MV*框架源码(二):Moon的code->html实现_第14张图片
    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") }] }

    深入浅出MV*框架源码(二):Moon的code->html实现_第15张图片
    addEventListeners.jpg

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

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


    深入浅出MV*框架源码(二):Moon的code->html实现_第16张图片
    removeChild.jpg

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


    深入浅出MV*框架源码(二):Moon的code->html实现_第17张图片
    hydrate-child.jpg

总结

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

你可能感兴趣的:(深入浅出MV*框架源码(二):Moon的code->html实现)