美文网首页
vue综合讲解

vue综合讲解

作者: 我是上帝可爱多 | 来源:发表于2017-08-10 18:07 被阅读116次

    vue2.0和1.0模板渲染的区别

    Vue 2.0 中模板渲染与 Vue 1.0 完全不同,1.0 中采用的 DocumentFragment (想了解可以观看这篇文章),而 2.0 中借鉴 React 的 Virtual DOM。基于 Virtual DOM,2.0 还可以支持服务端渲染(SSR),也支持 JSX 语法。

    真实DOM存在什么问题,为什么要用虚拟DOM

    我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是 document.createElement 这个方法创建的真实 DOM 元素会带来性能上的损失。我们来看一个 document.createElement 方法的例子

    let div = document.createElement('div');
    for(let k in div) {
      console.log(k);
    }
    

    打开 console 运行一下上面的代码,会发现打印出来的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,关联着真实的dom,比如属性elm,只包括我们需要的属性,并新增了一些在 diff 过程中需要使用的属性,例如 isStatic。

    DOM的操作很慢,但是JS确很快的,DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来,既然我们可以用JS对象表示DOM结构,那么当数据状态发生变化而需要改变DOM结构时,我们先通过JS对象表示的虚拟DOM计算出实际DOM需要做的最小变动,反过来,就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树,操作实际DOM更新了, 从而避免了粗放式的DOM操作带来的性能问题。

    Virtual DOM算法,简单总结下包括几个步骤:

    • 1用JS对象描述出DOM树的结构,然后在初始化构建中,用这个描述树去构建真正的DOM,并实际展现到页面中

    • 2当有数据状态变更时,重新构建一个新的JS的DOM树,通过新旧对比DOM数的变化diff,并记录两棵树差异

    • 3把步骤2中对应的差异通过步骤1重新构建真正的DOM,并重新渲染到页面中,这样整个虚拟DOM的操作就完成了,视图也就更新了

    我们看一下 Vue 2.0 源码中 AST 数据结构(其实就是构建vnode的标准) 的定义:

    declare type ASTNode = ASTElement | ASTText | ASTExpression
    declare type ASTElement = { // 有关元素的一些定义
      type: 1;
      tag: string;
      attrsList: Array{ name: string; value: string }>;
      attrsMap: { [key: string]: string | null };
      parent: ASTElement | void;
      children: ArrayASTNode>;
      //......
    }
    declare type ASTExpression = {
      type: 2;
      expression: string;
      text: string;
      static?: boolean;
    }
    declare type ASTText = {
      type: 3;
      text: string;
      static?: boolean;
    }
    

    我们看到 ASTNode 有三种形式:ASTElement,ASTText,ASTExpression。用属性 type 区分。

    VNode数据结构

    下面是 Vue 2.0 源码中 VNode 数据结构 的定义 (带注释的跟下面介绍的内容有关):

    constructor {
      this.tag = tag   //元素标签
      this.data = data  //属性
      this.children = children  //子元素列表
      this.text = text
      this.elm = elm  //对应的真实 DOM 元素
      this.ns = undefined
      this.context = context 
      this.functionalContext = undefined
      this.key = data && data.key
      this.componentOptions = componentOptions
      this.componentInstance = undefined
      this.parent = undefined
      this.raw = false
      this.isStatic = false //是否被标记为静态节点
      this.isRootInsert = true
      this.isComment = false
      this.isCloned = false
      this.isOnce = false
    }
    

    isStatic是否被标记为静态节点很重要下面会讲到。

    render函数

    这个函数是通过编译模板文件得到的,其运行结果是 VNode。render 函数 与 JSX 类似,Vue 2.0 中除了 Template 也支持 JSX 的写法。大家可以使用 Vue.compile(template)方法编译下面这段模板。

    div id="app">
      header>
        h1>I am a template!/h1>
      /header>
      p v-if="message">
        {{ message }}
      /p>
      p v-else>
        No message.
      /p>
    /div>
    

    方法会返回一个对象,对象中有 render 和 staticRenderFns 两个值。看一下生成的 render函数

    (function() {
      with(this){
        return _c('div',{   //创建一个 div 元素
          attrs:{"id":"app"}  //div 添加属性 id
          },[
            _m(0),  //静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
            _v(" "), //空的文本节点
            (message) //三元表达式,判断 message 是否存在
             //如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
            ?_c('p',[_v("\n    "+_s(message)+"\n  ")])
            //如果不存在,创建 p 元素,元素里面有文本,值为 No message. 
            :_c('p',[_v("\n    No message.\n  ")])
          ]
        )
      }
    })
    

    我们可以看到,通过上面的函数我们将一段html通过函数生成了,类似jsx语法。
    _m(0)是啥意思,可能不好理解,我们稍后会讲解。
    要看懂上面的 render函数,只需要了解 _c,_m,_v,_s 这几个函数的定义,其中 _c 是 createElement(创建元素),_m 是 renderStatic(渲染静态节点),_v 是 createTextVNode(创建文本dom),_s 是 toString (转换为字符串)

    header是静态节点,与vue渲染无关,通过_m(renderStatic)渲染的节点不会进入diff计算。

    除了 render 函数,还有一个 staticRenderFns 数组,这个数组中的函数与 VDOM 中的 diff 算法优化相关,我们会在编译阶段给后面不会发生变化的 VNode 节点打上 static 为 true 的标签,那些被标记为静态节点的 VNode 就会单独生成 staticRenderFns 函数

    (function() { //上面 render 函数 中的 _m(0) 会调用这个方法
      with(this){
        return _c('header',[_c('h1',[_v("I'm a template!")])])
      }
    })
    

    其实到现在我们已经很清楚,给我们任意一个模板template都可以将它构建成vnode的形式,这样就很好区分,通过render字符串的就是有vue指令属性的html,而staticFns的则是静态节点。

    compile 函数就是将 template 编译成 render 函数的字符串形式。

    import { parse } from './parser/index'
    import { optimize } from './optimizer'
    import { generate } from './codegen/index'
    
    /**
     * Compile a template.
     */
    export function compile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      const ast = parse(template.trim(), options)   // 把template转化为抽象语法树
      optimize(ast, options)
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    }
    

    parse方法位于src/compiler/parser/index.js,大家可以自己去学习。

    我们来看一下generate函数如何写的。

    export function generate (
      ast: ASTElement | void,
      options: CompilerOptions
    ): {
      render: string,
      staticRenderFns: Array<string>
    } {
      // save previous staticRenderFns so generate calls can be nested
      const prevStaticRenderFns: Array<string> = staticRenderFns
      const currentStaticRenderFns: Array<string> = staticRenderFns = []
      const prevOnceCount = onceCount
      onceCount = 0
      currentOptions = options
      warn = options.warn || baseWarn
      transforms = pluckModuleFunction(options.modules, 'transformCode')
      dataGenFns = pluckModuleFunction(options.modules, 'genData')
      platformDirectives = options.directives || {}
      isPlatformReservedTag = options.isReservedTag || no
      const code = ast ? genElement(ast) : '_c("div")'
      staticRenderFns = prevStaticRenderFns
      onceCount = prevOnceCount
      return {
        render: `with(this){return ${code}}`,
        staticRenderFns: currentStaticRenderFns
      }
    }
    

    现在一个前端也要逐渐习惯ts语法了,我们通过genElement(ast)实现模板渲染的code。

    这个函数主要有三个步骤组成:parse,optimize 和 generate,分别输出一个包含 AST,staticRenderFns 的对象和 render函数 的字符串。

    其中 genElement 函数(src/compiler/codegen/index.js)是会根据 AST 的属性调用不同的方法生成字符串返回。

    function genElement (el: ASTElement): string {
    if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el)
    } else if (el.once && !el.onceProcessed) {
    return genOnce(el)
    } else if (el.for && !el.forProcessed) {
    return genFor(el)
    } else if (el.if && !el.ifProcessed) {
    return genIf(el)
    } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el) || 'void 0'
    } else if (el.tag === 'slot') {
    }
    return code
    }
    }

    • parse 函数,主要功能是将 -template字符串解析成 AST。前面定义了ASTElement的数据结构,parse 函数就是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后解析生成AST。
    • optimize 函数(src/compiler/optimizer.js)主要功能就是标记静态节点,为后面 patch 过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较。
    • generate 函数(src/compiler/codegen/index.js)主要功能就是根据 AST 结构拼接生成 render 函数的字符串。

    讲到这里,大概也知道了vue在减少渲染所作的一些东西。

    下面在各详细的例子把:

    对应的结构是这样的,这个可以其实就是真实DOM树的一个结构映射了:

    image.png
    _v(_s(answer)): {{answer}} 模板语法自制文本。
    domProps对应的是:value = 'input'
    on:{'input',update}  input促发事件update
    

    数据发现变化后,会执行 Watcher 中的 _update 函数(src/core/instance/lifecycle.js),_update 函数会执行这个渲染函数,输出一个新的 VNode 树形结构的数据。然后在调用 patch 函数,拿这个新的 VNode 与旧的 VNode 进行对比,只有发生了变化的节点才会被更新到真实 DOM 树上。

    virtualdom 比较

    react的diff其实和vue的diff大同小异。所以这张图能很好的解释过程。比较只会在同层级进行, 不会跨层级比较。

    diff的过程就是调用patch函数,就像打补丁一样修改真实dom。

    function patch (oldVnode, vnode) {
        if (sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode)
        } else {
            const oEl = oldVnode.el
            let parentEle = api.parentNode(oEl)
            createEle(vnode)
            if (parentEle !== null) {
                api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
                api.removeChild(parentEle, oldVnode.el)
                oldVnode = null
            }
        }
        return vnode
    }
    

    patch函数有两个参数,vnode和oldVnode,也就是新旧两个虚拟节点。在这之前,我们先了解完整的vnode都有什么属性,举个一个简单的例子:

    // body下的 <div id="v" class="classA"><div> 对应的 oldVnode 就是
    
    {
      el:  div  //对真实的节点的引用,本例中就是document.querySelector('#id.classA')
      tagName: 'DIV',   //节点的标签
      sel: 'div#v.classA'  //节点的选择器
      data: null,       // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style
      children: [], //存储子节点的数组,每个子节点也是vnode结构
      text: null,    //如果是文本节点,对应文本节点的textContent,否则为null
    }
    

    sameVnode函数就是看这两个节点是否值得比较,代码相当简单:

    function sameVnode(oldVnode, vnode){
        return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
    }
    

    两个vnode的key和sel相同才去比较它们,比如p和span,div.classA和div.classB都被认为是不同结构而不去比较它们。

    当节点不值得比较,进入else中

        else {
            const oEl = oldVnode.el
            let parentEle = api.parentNode(oEl)
            createEle(vnode)
            if (parentEle !== null) {
                api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
                api.removeChild(parentEle, oldVnode.el)
                oldVnode = null
            }
        }
    

    过程如下:

    • 取得oldvnode.el的父节点,parentEle是真实dom
    • createEle(vnode)会为vnode创建它的真实dom,令vnode.el =真实dom
    • parentEle将新的dom插入,移除旧的dom当不值得比较时,新节点直接把老节点整个替换了

    最后return node

    patch最后会返回vnode,vnode和进入patch之前的不同在哪?
    没错,就是vnode.el,唯一的改变就是之前vnode.el = null, 而现在它引用的是对应的真实dom。

    patchVnode

    两个节点值得比较时,会调用patchVnode函数
    patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
    api.setTextContent(el, vnode.text)
    }else {
    updateEle(el, vnode, oldVnode)
    if (oldCh && ch && oldCh !== ch) {
    updateChildren(el, oldCh, ch)
    }else if (ch){
    createEle(vnode) //create el's children dom
    }else if (oldCh){
    api.removeChildren(el)
    }
    }
    }
    1.const el = vnode.el = oldVnode.el 这是很重要的一步,让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化。
    节点的比较有5种情况
    if (oldVnode === vnode),他们的引用一致,可以认为没有变化。

    2.if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本节点的比较,需要修改,则会调用Node.textContent = vnode.text。

    3.if( oldCh && ch && oldCh !== ch ), 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren函数比较子节点,这是diff的核心,后边会讲到。

    4.else if (ch),只有新的节点有子节点,调用createEle(vnode),vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点。

    5.else if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。

    今天的讲解就到这,相信大家对vue的模板渲染机制和vnode diff计算有了一定了解。

    相关文章

      网友评论

          本文标题:vue综合讲解

          本文链接:https://www.haomeiwen.com/subject/zyrhrxtx.html