virtual-dom 分析梳理【一】

目前前端的三个主流的框架都使用virtual-dom来处理dom的渲染,每一个框架都会在virtual-dom的核心原理上进行了一些特点的扩展,这篇文章主要是经过github.com/Matt-Esch/v…源码来分析最基础核心的原理。html

virtual-dom简介

virtual-dom也就是使用js的数据结构来表示dom元素的结构,由于不是真是的dom节点,也就称为虚拟DOM。它最大的特色是将页面进行抽象成JS对象形式,配合不一样的工具使跨平台成为可能,能够根据不一样的平台渲染出相应的真实“DOM”。除此以外,在页面进行更新的时候,能够将DOM元素的变更放在内存比较,再结合一些框架的机制,将屡次的渲染合并成一次渲染更新。前端

开始

先看看virtual-dom的库是怎么使用的,平时接触的都是框架或者webpack转换以前的代码,好比jsxvue的模板等。如下将virtual-dom简称为VDvue

var h = require('virtual-dom/h');
var diff = require('virtual-dom/diff');
var patch = require('virtual-dom/patch');
var createElement = require('virtual-dom/create-element');

// 1: Create a function that declares what the DOM should look like
function render(count) {
    return h('div', {
        style: {
            textAlign: 'center',
            lineHeight: (100 + count) + 'px',
            border: '1px solid red',
            width: (100 + count) + 'px',
            height: (100 + count) + 'px'
        }
    }, [String(count)]);
}

// 2: Initialise the document
var count = 0;      // We need some app data. Here we just store a count.

var tree = render(count);               // We need an initial tree
var rootNode = createElement(tree);     // Create an initial root DOM node ...
document.body.appendChild(rootNode);    // ... and it should be in the document

// 3: Wire up the update logic
setInterval(function () {
      count++;

      var newTree = render(count);
      var patches = diff(tree, newTree);
      rootNode = patch(rootNode, patches);
      tree = newTree;
}, 1000);
复制代码

以上代码为我上面说的那个github上库的代码的例子。其实从这上面咱们就能够看到VD主要分为四大部分或者说是功能:node

  1. h(能够叫其余名字,好比 React.creatElement) 函数,用来建立VD对象,好比jsx书写的代码就会被转换成React.creatElement(tag, props, ...children)这样的。
  2. diff 函数,用来比较两个VD对象的具体不一样,造成一个描述(描述两个VD的不一样点)对象。
  3. patch 函数,用来经过对比后产生的描述对象对前一个DOM树进行更新或者成为打补丁(我的理解)。
  4. createElement 函数,根据VD对象产生真实的DOM树。

以后内容就会根据这四个部分进行梳理,可能顺序不一样,会先梳理h函数和createElement函数这两个比较简单 :joy: 。该篇就是讲这两点。webpack

建立 VD 对象

看一下h函数的实现git

function h(tagName, properties, children) {
   var childNodes = [];
    var tag, props, key, namespace;
    // 若是第三参数为空,就先看看第二个参数是否为子节点,
    // 也就是能够这样写 h('div', ['children']) ,若是没有props就能够不用穿,
    // 能够不须要这样 h('div', null, ['children'])
    if (!children && isChildren(properties)) {   
        children = properties;
        props = {};
    }

    props = props || properties || {};  // props 赋值
    // 作一下检查和解析,好比,若是tagName参数为空,则返回 div 做为兜底
    tag = parseTag(tagName, props); 
    ...
    if (children !== undefined && children !== null) {
        // 为子元素作一些判断,若是是字符串就转成 VText 对象,数值就先 String 化再转,这些
        // 最终目的是将传入的 children 进行标准话,作一下边界处理,
        // 使得符合 VNode 构造须要的几个参数
        addChild(children, childNodes, tag, props);
    }
   ... 
   return new VNode(tag, props, childNodes, key, namespace);
}
复制代码

简单的先看一下函数的结构。h接受三个参数,如最开始的代码中:github

h('div', {
        style: {
            textAlign: 'center',
            lineHeight: (100 + count) + 'px',
            border: '1px solid red',
            width: (100 + count) + 'px',
            height: (100 + count) + 'px'
        }
    }, [String(count)]);
复制代码

这里就是对于的个参数,其实转换成DOM就是这样的(jsx的伪代码):web

<div style={{...}}>count</div>
复制代码

因此h函数的三个参数是比较好理解的。分别是 标签名称、属性和子节点。算法

该函数最终返回了一个 VNode 对象,除了传入重要的标签名属性子节点以外,还传入了两个其余属性,其实在ReactVue中都会本身对VNode这个对象进行扩展,因此这里也是该库的优化可扩展,暂时先不看,主要是梳理过程。数据结构

VNode

经过h函数能够new一个VNode对象,那么这个VNode对象,本质上也就是一个描述Dom的字面量对象。

function VirtualNode(tagName, properties, children, key, namespace) {
    this.tagName = tagName
    this.properties = properties || noProperties
    this.children = children || noChildren
    this.key = key != null ? String(key) : undefined
    this.namespace = (typeof namespace === "string") ? namespace : null
    ...
    this.count = count + descendants
    this.hasWidgets = hasWidgets
    this.hasThunks = hasThunks
    this.hooks = hooks
    this.descendantHooks = descendantHooks
}
复制代码

省略的代码主要是来计算和根据传入参数产生一些附加信息,这些我以为属性扩展吧,因此先不说吧,仍是关注流程。经过h函数,咱们就能够获得这样的一个字面对象。h函数嵌套使用就能够获得一颗这样的字面对象的“树”。

h('div', {}, [
  h('p', {}, ['demo']),
  ....
])
复制代码

根据 VNode 子面量对象,建立真实 DOM

当咱们使用 h 函数获得整颗VD树以后,咱们就须要经过createElement函数建立真实的DOM,最后插入到某个节点里面,页面就这样生成了(页面更新的过程,旧 VD Tree 和 新 VD Tree 还须要进行比较)。

function createElement(vnode, opts) {
    var doc = opts ? opts.document || document : document
    var warn = opts ? opts.warn : null

    vnode = handleThunk(vnode).a

    if (isWidget(vnode)) {
         // 不是很清楚 Widget
         // 我好像没有试过用,你们要是知道,能够指点指点,谢谢了。
         // 应该是用来初始化一些能够复用的小部件
        return vnode.init() 
    } else if (isVText(vnode)) {
        // 若是是文本,就建立一个简单的文本节点
        return doc.createTextNode(vnode.text)
    } else if (!isVNode(vnode)) {
        // 若是都不是VD就警告
        if (warn) {
            warn("Item is not a valid virtual dom node", vnode)
        }
        return null
    }
    ...
    var props = vnode.properties
    // 这个方法就是给节点赋值属性,好比: node.setAttribute('key', props[key])
    applyProperties(node, props)

    var children = vnode.children
    
    // 递归建立子节点。
    for (var i = 0; i < children.length; i++) {
        var childNode = createElement(children[i], opts)
        if (childNode) {
            node.appendChild(childNode)
        }
    }

    return node
}
复制代码

代码上加了一些注释,其实createElement是相对简单的,核心就是判断 VD 是个什么类型的节点,建立相应的 DOM ,而后再低柜建立子节点。

建立好 DOM 后就能够如官方给出的示例,将这份 DOM 树插入到指点的页面元素下面了。VD 到真实的 DOM就是这样一个过程。内容比较基础,梳理为主。

小结

经过上面的梳理,能够知道咱们日常写的jsx就是先经过Babel这样的工具,先将jsx转出h('div', {}, [...])这样的函数,而后执行获得 VD Tree,再经过createElement建立对象的DOM节点,而后插入到页面某个节点下面。

下一章就来梳理一下。diff 算法的流程和基础原理。

原文地址

相关文章
相关标签/搜索