DOM下的多个VNode节点,下的一个

日期:2019-05-08编辑作者:互联网

问题:实现 Virtual DOM 下的一个 VNode 节点

什么是VNode


作者:彭凯

ca88手机版登录 1

我们知道,render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。...

实现一个VNode


VNode 归根结底就是一个 JavaScript 对象,只要这个类的一些属性可以正确直观地描述清楚当前节点的信息即可。我们来实现一个简单的 VNode 类,加入一些基本属性,为了便于理解,我们先不考虑复杂的情况。

class VNode {

    constructor (tag, data, children, text, elm) {

        /*当前节点的标签名*ca88手机版登录,/

        this.tag = tag;

        /*当前节点的一些数据信息,比如props、attrs等数据*/

        this.data = data;

        /*当前节点的子节点,是一个数组*/

        this.children = children;

        /*当前节点的文本*/

        this.text = text;

        /*当前虚拟节点对应的真实dom节点*/

        this.elm = elm;

    }

}

用 JavaScript 代码形式就是这样的。

function render () {

    return new VNode(

        'span',

        {

            /* 指令集合数组 */

            directives: [

                {

                    /* v-show指令 */

                    rawName: 'v-show',

                    expression: 'isShow',

                    name: 'show',

                    value: true

                }

            ],

            /* 静态class */

            staticClass: 'demo'

        },

        [ new VNode(undefined, undefined, undefined, 'This is a span.') ]

    );

}

看看转换成 VNode 以后的情况。

{

    tag: 'span',

    data: {

        /* 指令集合数组 */

        directives: [

            {

                /* v-show指令 */

                rawName: 'v-show',

                expression: 'isShow',

                name: 'show',

                value: true

            }

        ],

        /* 静态class */

        staticClass: 'demo'

    },

    text: undefined,

    children: [

        /* 子节点是一个文本VNode节点 */

        {

            tag: undefined,

            data: undefined,

}

未完待续......

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

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

什么是VNode

真实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什么是VNode

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 function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。.实现一个VNode

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在减少渲染所作的一些东西。

下面在各详细的例子把:

ca88手机版登录 2

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

ca88手机版登录 3

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 树上。

VNode 归根结底就是一个 JavaScript 对象,只要这个类的一些属性可以正确直观地描述清楚当前节点的信息即可。我们来实现一个简单的 VNode 类,加入一些基本属性,为了便于理解,我们先不考虑复杂的情况。

virtualdom 比较

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

ca88手机版登录 4

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。

ca88手机版登录 5

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 组件。

ca88手机版登录 6

用 JavaScript 代码形式就是这样的。

ca88手机版登录 7

看看转换成 VNode 以后的情况。

ca88手机版登录 8

然后我们可以将 VNode 进一步封装一下,可以实现一些产生常用 VNode 的方法。创建一个空节点function createEmptyVNode () {

const node = new VNode();

node.text = '';

return node;

}

创建一个文本节点function createTextVNode {

return new VNode(undefined, undefined, undefined, String;

}

克隆一个 VNode 节点function cloneVNode {

const cloneVnode = new VNode(

node.tag,

node.data,

node.children,

node.text,

node.elm

);

return cloneVnode;

}

总的来说,VNode 就是一个 JavaScript 对象,用 JavaScript 对象的属性来描述当前节点的一些状态,用 VNode 节点的形式来模拟一棵 Virtual DOM 树。

本文由ca88手机版会员登录发布于互联网,转载请注明出处:DOM下的多个VNode节点,下的一个

关键词:

边缘计算芯片双轮计谋,议程重磅宣布

七月2二日,由雷锋(Lei Feng)网 AI掘金队(Denver Nuggets)志主办的「第叁届中华人民共和国人造智能安全防卫峰会」在...

详细>>

加强绿色数据中心建设实现持续健康发展,三部

飞象网讯一月一11日新闻,为加快灰绿数据大旨建设,MIIT、国家机关专业管理局和国家财富局提议指点意见,须求构...

详细>>

哈啰出行ca88手机版登录,赶快告诉家人

导语:又一网约车平台正式被确认诞生,阿里投资几十亿,赶快告诉家人 随着元宵节返程小高峰过后,2019 年春运如...

详细>>

Java工程师的年终总计,聊聊201陆年前端学习安排

图片来自网络 从毕业到现在已经码了5年代码了,从前端到后端到数据库再到运维,网站和app都玩过了一轮,不能说自...

详细>>