数据驱动

Vue.js    ⼀个核⼼思想是数据驱动。所谓数据驱动,是指视图是由数据驱动⽣成的,我们对视图的修改, 不会直接操作    DOM,⽽是通过修改数据。它相⽐我们传统的前端开发,如使⽤    jQuery    等前端库直接 修改    DOM,⼤⼤简化了代码量。特别是当交互复杂的时候,只关⼼数据的修改会让代码的逻辑变的⾮ 常清晰,因为    DOM    变成了数据的映射,我们所有的逻辑都是对数据的修改,⽽不⽤碰触    DOM,这样 的代码⾮常利于维护。 在    Vue.js    中我们可以采⽤简洁的模板语法来声明式的将数据渲染为    DOM:

最终它会在⻚⾯上渲染出        Hello    Vue    。接下来,我们会从源码⾓度来分析    Vue    是如何实现的,分析 过程会以主线代码为主,重要的分⽀逻辑会放在之后单独分析。数据驱动还有⼀部分是数据更新驱动 视图变化,这⼀块内容我们也会在之后的章节分析,这⼀章我们的⽬标是弄清楚模板和数据如何渲染 成最终的    DOM。


new    Vue    发⽣了什么 

从⼊⼝代码开始分析,我们先来分析        new    Vue        背后发⽣了哪些事情。我们都知道,    new        关键字在 Javascript    语⾔中代表实例化是⼀个对象,⽽        Vue        实际上是⼀个类,类在    Javascript    中是⽤    Function 来实现的,来看⼀下源码,在    src/core/instance/index.js        中。

可以看到        Vue        只能通过    new    关键字初始化,然后会调⽤        this._init        ⽅法,    该⽅法在    src/core/instance/init.js        中定义。

Vue    初始化主要就⼲了⼏件事情,合并配置,初始化⽣命周期,初始化事件中⼼,初始化渲染,初始 化    data、props、computed、watcher    等等。

总结

Vue    的初始化逻辑写的⾮常清楚,把不同的功能逻辑拆成⼀些单独的函数执⾏,让主线逻辑⼀⽬了 然,这样的编程思想是⾮常值得借鉴和学习的。 由于我们这⼀章的⽬标是弄清楚模板和数据如何渲染成最终的    DOM,所以各种初始化逻辑我们先不 看。在初始化的最后,检测到如果有        el        属性,则调⽤        vm.$mount        ⽅法挂载        vm    ,挂载的⽬标就 是把模板渲染成最终的    DOM,那么接下来我们来分析    Vue    的挂载过程。


Vue    实例挂载的实现 

Vue    中我们是通过        $mount        实例⽅法去挂载        vm        的,    $mount        ⽅法在多个⽂件中都有定义,如    src/platform/web/entry-runtime-withcompiler.js    、    src/platform/web/runtime/index.js    、    src/platform/weex/runtime/index.js    。因为        $mount        这个⽅法的实现是和平台、构建⽅式都相关的。接下来我们重点分析带        compiler    版本的        $monut        实现,因为抛开    webpack    的    vue-loader,我们在纯前端浏览器环境分析    Vue    的⼯作原 理,有助于我们对原理理解的深⼊。    compiler        版本的        $monut        实现⾮常有意思,先来看⼀下        src/platform/web/entry-runtimewith-compiler.js        ⽂件中定义:

这段代码⾸先缓存了原型上的        $mount        ⽅法,再重新定义该⽅法,我们先来分析这段代码。⾸先,它 对        el        做了限制,Vue    不能挂载在        body    、    html        这样的根节点上。接下来的是很关键的逻辑    —— 如果没有定义        render        ⽅法,则会把        el        或者        template        字符串转换成        render        ⽅法。这⾥我们 要牢记,在    Vue    2.0    版本中,所有    Vue    的组件的渲染最终都需要        render        ⽅法,⽆论我们是⽤单⽂件 .vue    ⽅式开发组件,还是写了        el        或者        template        属性,最终都会转换成        render        ⽅法,那么这个 过程是    Vue    的⼀个“在线编译”的过程,它是调⽤        compileToFunctions        ⽅法实现的,编译过程我们之 后会介绍。最后,调⽤原先原型上的        $mount        ⽅法挂载。 原先原型上的        $mount        ⽅法在        src/platform/web/runtime/index.js        中定义,之所以这么设计完 全是为了复⽤,因为它是可以被        runtime    only        版本的    Vue    直接使⽤的。


Vue.prototype.$mount    =    function    (        el?:    string    |    Element,        hydrating?:    boolean ):    Component    {        el    =    el    &&    inBrowser    ?    query(el)    :    undefined        return    mountComponent(this,    el,    hydrating)

 

    $mount        ⽅法⽀持传⼊    2    个参数,第⼀个是        el    ,它表⽰挂载的元素,可以是字符串,也可以是 DOM    对象,如果是字符串在浏览器环境下会调⽤        query        ⽅法转换成    DOM    对象的。第⼆个参数是和 服务端渲染相关,在浏览器环境下我们不需要传第⼆个参数。    $mount        ⽅法实际上会去调⽤        mountComponent        ⽅法,这个⽅法定义在    src/core/instance/lifecycle.js        ⽂件中:

从上⾯的代码可以看到,    mountComponent        核⼼就是先调⽤        vm._render        ⽅法先⽣成虚拟    Node,再 实例化⼀个渲染    Watcher    ,在它的回调函数中会调⽤        updateComponent        ⽅法,最终调⽤    vm._update        更新    DOM。    Watcher        在这⾥起到两个作⽤,⼀个是初始化的时候会执⾏回调函数,另⼀个是当    vm    实例中的监测 的数据发⽣变化的时候执⾏回调函数,这块⼉我们会在之后的章节中介绍。 函数最后判断为根节点的时候设置        vm._isMounted        为        true    ,    表⽰这个实例已经挂载了,同时执⾏    mounted        钩⼦函数。    这⾥注意        vm.$vnode        表⽰    Vue    实例的⽗虚拟    Node,所以它为        Null        则表⽰ 当前是根    Vue    的实例。 

 

总结    mountComponent        ⽅法的逻辑也是⾮常清晰的,它会完成整个渲染⼯作,接下来我们要重点分析其中 的细节,也就是最核⼼的    2    个⽅法:    vm._render        和        vm._update    。
 

render

Vue    的        _render        ⽅法是实例的⼀个私有⽅法,它⽤来把实例渲染成⼀个虚拟    Node。它的定义在    src/core/instance/render.js        ⽂件中:
 

这段代码最关键的是        render        ⽅法的调⽤,我们在平时的开发⼯作中⼿写        render        ⽅法的场景⽐较 少,⽽写的⽐较多的是        template        模板,在之前的        mounted        ⽅法的实现中,会把        template        编译 成        render        ⽅法,但这个编译过程是⾮常复杂的,我们不打算在这⾥展开讲,之后会专门花⼀个章节 来分析    Vue    的编译过程。 在    Vue    的官⽅⽂档中介绍了        render        函数的第⼀个参数是        createElement    ,那么结合之前的例⼦:
<div    id="app">        {{    message    }} </div> 

相当于我们编写如下        render        函数:

 

Virtual    DOM 

Virtual    DOM    这个概念相信⼤部分⼈都不会陌⽣,它产⽣的前提是浏览器中的    DOM    是很“昂贵"的,为 了更直观的感受,我们可以简单的把⼀个简单的    div    元素的属性都打印出来,如图所⽰:

可以看到    Vue.js    中的    Virtual    DOM    的定义还是略微复杂⼀些的,因为它这⾥包含了很多    Vue.js    的特 性。这⾥千万不要被这些茫茫多的属性吓到,实际上    Vue.js    中    Virtual    DOM    是借鉴了⼀个开源库 snabbdom    的实现,然后加⼊了⼀些    Vue.js    特⾊的东⻄。我建议⼤家如果想深⼊了解    Vue.js    的    Virtual DOM    前不妨先阅读这个库的源码,因为它更加简单和纯粹。

总结

其实    VNode    是对真实    DOM    的⼀种抽象描述,它的核⼼定义⽆⾮就⼏个关键属性,标签名、数据、⼦ 节点、键值等,其它属性都是都是⽤来扩展    VNode    的灵活性以及实现⼀些特殊    feature    的。由于    VNode 只是⽤来映射到真实    DOM    的渲染,不需要包含操作    DOM    的⽅法,因此它是⾮常轻量和简单的。 Virtual    DOM    除了它的数据结构的定义,映射到真实的    DOM    实际上要经历    VNode    的    create、diff、 patch    等过程。那么在    Vue.js    中,VNode    的    create    是通过之前提到的        createElement        ⽅法创建的,我 们接下来分析这部分的实现。
 

 

createElement

children    的规范化

由于    Virtual    DOM    实际上是⼀个树状结构,每⼀个    VNode    可能会有若⼲个⼦节点,这些⼦节点应该也 是    VNode    的类型。    _createElement        接收的第    4    个参数    children    是任意类型的,因此我们需要把它们 规范成    VNode    类型。 这⾥根据        normalizationType        的不同,调⽤了        normalizeChildren(children)        和    simpleNormalizeChildren(children)        ⽅法,它们的定义都在    src/core/vdom/helpers/normalzie-children.js        中:

    simpleNormalizeChildren        ⽅法调⽤场景是        render        函数当函数是编译⽣成的。理论上编译⽣成的    children        都已经是    VNode    类型的,但这⾥有⼀个例外,就是        functional    component        函数式组件 返回的是⼀个数组⽽不是⼀个根节点,所以会通过        Array.prototype.concat        ⽅法把整个    children        数组打平,让它的深度只有⼀层。    normalizeChildren        ⽅法的调⽤场景有    2    种,⼀个场景是        render        函数是⽤户⼿写的,当    children        只有⼀个节点的时候,Vue.js    从接⼝层⾯允许⽤户把        children        写成基础类型⽤来创建单 个简单的⽂本节点,这种情况会调⽤        createTextVNode        创建⼀个⽂本节点的    VNode;另⼀个场景是 当编译        slot    、    v-for        的时候会产⽣嵌套数组的情况,会调⽤        normalizeArrayChildren        ⽅法, 接下来看⼀下它的实现:

    normalizeArrayChildren        接收    2    个参数,    children        表⽰要规范的⼦节点,    nestedIndex        表⽰ 嵌套的索引,因为单个        child        可能是⼀个数组类型。        normalizeArrayChildren        主要的逻辑就是 遍历        children    ,获得单个节点        c    ,然后对        c        的类型判断,如果是⼀个数组类型,则递归调⽤    normalizeArrayChildren    ;    如果是基础类型,则通过        createTextVNode        ⽅法转换成    VNode    类型; 否则就已经是    VNode    类型了,如果        children        是⼀个列表并且列表还存在嵌套的情况,则根据    nestedIndex        去更新它的    key。这⾥需要注意⼀点,在遍历的过程中,对这    3    种情况都做了如下处 理:如果存在两个连续的        text        节点,会把它们合并成⼀个        text        节点。 经过对        children        的规范化,    children        变成了⼀个类型为    VNode    的    Array。

 VNode    的创建 

 

总结

那么⾄此,我们⼤致了解了        createElement        创建    VNode    的过程,每个    VNode    有    children    ,    children        每个元素也是⼀个    VNode,这样就形成了⼀个    VNode    Tree,它很好的描述 了我们的    DOM    Tree。 回到        mountComponent        函数的过程,我们已经知道        vm._render        是如何创建了⼀个    VNode,接下来 就是要把这个    VNode    渲染成⼀个真实的    DOM    并渲染出来,这个过程是通过        vm._update        完成的, 接下来分析⼀下这个过程。
 

update

    createPatchFunction        内部定义了⼀系列的辅助⽅法,最终返回了⼀个        patch        ⽅法,这个⽅法就 赋值给了        vm._update        函数⾥调⽤的        vm.__patch__    。 在介绍        patch        的⽅法实现之前,我们可以思考⼀下为何    Vue.js    源码绕了这么⼀⼤圈,把相关代码分 散到各个⽬录。因为前⾯介绍过,    patch        是平台相关的,在    Web    和    Weex    环境,它们把虚拟    DOM    映 射到    “平台    DOM”    的⽅法是不同的,并且对    “DOM”    包括的属性模块创建和更新也不尽相同。因此每个
update
47
平台都有各⾃的        nodeOps        和        modules    ,它们的代码需要托管在        src/platforms        这个⼤⽬录下。 ⽽不同平台的        patch        的主要逻辑部分是相同的,所以这部分公共的部分托管在        core        这个⼤⽬录 下。差异化部分只需要通过参数来区别,这⾥⽤到了⼀个函数柯⾥化的技巧,通过    createPatchFunction        把差异化参数提前固化,这样不⽤每次调⽤        patch        的时候都传递    nodeOps        和        modules        了,这种编程技巧也⾮常值得学习。 在这⾥,    nodeOps        表⽰对    “平台    DOM”    的⼀些操作⽅法,    modules        表⽰平台的⼀些模块,它们会在 整个        patch        过程的不同阶段执⾏相应的钩⼦函数。这些代码的具体实现会在之后的章节介绍。 回到        patch        ⽅法本⾝,它接收    4个参数,    oldVnode        表⽰旧的    VNode    节点,它也可以不存在或者是 ⼀个    DOM    对象;    vnode        表⽰执⾏        _render        后返回的    VNode    的节点;    hydrating        表⽰是否是服 务端渲染;    removeOnly        是给        transition-group        ⽤的,之后会介绍。    patch        的逻辑看上去相对复杂,因为它有着⾮常多的分⽀逻辑,为了⽅便理解,我们并不会在这⾥ 介绍所有的逻辑,仅会针对我们之前的例⼦分析它的执⾏逻辑。之后我们对其它场景做源码分析的时 候会再次回顾        patch        ⽅法。 先来回顾我们的例⼦:

 

createComponent

 

构造⼦类构造函数


安装组件钩⼦函数

实例化    VNode

patch

createComponent

    createComponent        函数中,⾸先对        vnode.data        做了⼀些判断:


  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值