vue综合讲解

2017-08-10  本文已影响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算法,简单总结下包括几个步骤:

我们看一下 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
}
}

讲到这里,大概也知道了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
        }
    }

过程如下:

最后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计算有了一定了解。

上一篇下一篇

猜你喜欢

热点阅读