Git Product home page Git Product logo

qvd's Introduction

qvd

简化版Virtual DOM,用于Mobile页面渲染。

Example

var h = require('qvd').h
  , diff = require('qvd').diff;

var a = h('div', { style: { textAlign: 'center' }, [h('text', 'hello')] })
  , b = h('div', { style: { borer: '1px' } });

diff(a, b);

API

/**
 * h(tagName, properties, children)
 * 创建一个节点
 * @param {String} tagName tag名
 * @param {Object} properties 属性对象
 * @param {Array} children 子节点数组
 */
h('div', { style: { border: '1px' } }, [h('p')]);
/**
 * h('text', text)
 * 创建文字节点
 * @param {String} text 文字节点内容
 */
h('text', 'hello world');
/**
 * render(vd)
 * 得到生成的html片段
 * @param {VD} vd
 * @returns {String}
 */
// <div style="border: 1px"><p></p></div>
render(h('div', { style: { border: '1px' } }, [h('p')]))
/**
 * diff(a, b)
 * @param {VD} a 目前状态的a虚拟节点
 * @param {VD} b 要变成状态的b虚拟节点
 * @returns {Array} patches
 */
diff(a, b);
/**
 * patch(patches, container)
 * @param {Array} patches diff方法返回的值
 * @param {Node} container a 虚拟节点的容器
 */
// 节点变换,从a到b
patch(patches, container);

License

Copyright (c) 2014 Matt-Esch.

Copyright (c) 2015 Daniel Yang.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

qvd's People

Contributors

matt-esch avatar raynos avatar neonstalwart avatar uberesch avatar miniflycn avatar mtyaka avatar rgbboy avatar jesseditson avatar mattlummus avatar zolmeister avatar staltz avatar cogell avatar eculver avatar jameshfisher avatar mmckegg avatar shinnn avatar teropa avatar

Stargazers

行旅書讀 avatar vivi avatar 王仔 avatar Sun-son avatar  avatar vanessayin avatar tianzhen avatar raul avatar Amos avatar  avatar Dapeng Gong avatar hary avatar (๑•̀ㅂ•́)و✧ avatar  avatar hanxie avatar o avatar 凹凸 avatar Timothy avatar kingler avatar paranoidjk avatar xiaobin avatar Thinking80s avatar void avatar Anderlu avatar 安迪帆 avatar  avatar Keal avatar  avatar 书越 avatar zhou avatar 叶伟伟 avatar taotao avatar 张翊超 avatar wangyue avatar Commy avatar MaoLin avatar 衣不如新 avatar zk_9 avatar Undefined avatar Jarvan Wayne avatar TangPin avatar lemoncolaz avatar xyz avatar 许峥 avatar  avatar litten avatar Desen Meng avatar Janry avatar

Watchers

James Cloos avatar  avatar Desen Meng avatar  avatar  avatar vanessayin avatar johonz avatar

qvd's Issues

React直出实现与原理

前一篇文章我们介绍了虚拟DOM的实现与原理,这篇文章我们来讲讲React的直出。
比起MVVM,React比较容易实现直出,那么React的直出是如何实现,有什么值得我们学习的呢?

为什么MVVM不能做直出?

对于MVVM,HTML片段即为配置,而直出后的HTML无法还原配置,所以问题不是MVVM能否直出,而是在于直出后的片段能否还原原来的配置。下面是一个简单的例子:

<sapn>Hello {name}!</span>

上面这段HTML配置和数据在一起,直出后会变成:

<span>Hello world!</span>

这时候当我们失去了name的值改变的时候会导致页面渲染这个细节。当然,如果为了实现MVVM直出我们可能有另外的方法来解决,例如直出结果变成这样:

<span>Hello <span q-text="name">world</span>!</span>

这时候我们是可以把丢失的信息找回来的,当然结构可能和我们想象的有些差别。当然还有其他问题,例如直出HTML不一定能反向还原数据,由于篇幅问题,这里不展开讨论。

React如何直出?

2

如图:

  • React的虚拟DOM的生成是可以在任何支持Javascript的环境生成的,所以可以在NodeJS或Iojs环境生成
  • 虚拟DOM可以直接转成String
  • 然后插入到html文件中输出给浏览器便可

具体例子可以参考,https://github.com/DavidWells/isomorphic-react-example/ ,下面是其渲染路由的写法:

// https://github.com/DavidWells/isomorphic-react-example/blob/master/app/routes/coreRoutes.js

var React = require('react/addons');
var ReactApp = React.createFactory(require('../components/ReactApp').ReactApp);

module.exports = function(app) {

    app.get('/', function(req, res){
        // React.renderToString takes your component
        // and generates the markup
        var reactHtml = React.renderToString(ReactApp({}));
        // Output html rendered by react
        // console.log(myAppHtml);
        res.render('index.ejs', {reactOutput: reactHtml});
    });

};

OK,我们现在知道如何利用React实现直出,以及如何前后端代码复用。

但还有下面几个问题有待解决:

  • 如何渲染文字节点,每个虚拟DOM节点是需要对应实际的节点,但无法通过html文件生成相邻的Text Node,例如下面例子应当如何渲染:
React.createClass({
    render: function () {
        return (
            <p>
                Hello {name}!           
            </p>
        );
    }
})
  • 如何避免直出的页面被React重新渲染一遍?或者直出的页面和前端的数据是不对应的怎么办?

相邻的Text Node,想多了相邻的span而已

1

通过一个简单的例子,我们可以发现,实际上React根本没用Text Node,而是使用span来代替Text Node,这样就可以实现虚拟DOM和直出DOM的一一映射关系。

重复渲染?没门

刚刚的例子,如果我们通过React.renderToString拿到<Test />可以发现是:

<p data-reactid=".0" data-react-checksum="-793171045"><span data-reactid=".0.0">Hello </span><span data-reactid=".0.1">world</span><span data-reactid=".0.2">!</span></p>

我们可以发现一个有趣的属性data-react-checksum,这是啥?实际上这是上面这段HTML片段的adler32算法值。实际上调用React.render(<MyComponent />, container);时候做了下面一些事情:

  • 看看container是否为空,不为空则认为有可能是直出了结果。
  • 接下来第一个元素是否有data-react-checksum属性,如果有则通过React.renderToString拿到前端的,通过adler32算法得到的值和data-react-checksum对比,如果一致则表示,无需渲染,否则重新渲染,下面是adler32算法实现:
var MOD = 65521;

// This is a clean-room implementation of adler32 designed for detecting
// if markup is not what we expect it to be. It does not need to be
// cryptographically strong, only reasonably good at detecting if markup
// generated on the server is different than that on the client.
function adler32(data) {
  var a = 1;
  var b = 0;
  for (var i = 0; i < data.length; i++) {
    a = (a + data.charCodeAt(i)) % MOD;
    b = (b + a) % MOD;
  }
  return a | (b << 16);
}
  • 如果需要重新渲染,先通过下面简单的差异算法找到差异在哪里,打印出错误:
/**
 * Finds the index of the first character
 * that's not common between the two given strings.
 *
 * @return {number} the index of the character where the strings diverge
 */
function firstDifferenceIndex(string1, string2) {
  var minLen = Math.min(string1.length, string2.length);
  for (var i = 0; i < minLen; i++) {
    if (string1.charAt(i) !== string2.charAt(i)) {
      return i;
    }
  }
  return string1.length === string2.length ? -1 : minLen;
}

下面是首屏渲染时的主要逻辑,可以发现React对首屏实际上也是通过innerHTML来渲染的:

_mountImageIntoNode: function(markup, container, shouldReuseMarkup) {
    ("production" !== process.env.NODE_ENV ? invariant(
      container && (
        (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE)
      ),
      'mountComponentIntoNode(...): Target container is not valid.'
    ) : invariant(container && (
      (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE)
    )));

    if (shouldReuseMarkup) {
      var rootElement = getReactRootElementInContainer(container);
      if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) {
        return;
      } else {
        var checksum = rootElement.getAttribute(
          ReactMarkupChecksum.CHECKSUM_ATTR_NAME
        );
        rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME);

        var rootMarkup = rootElement.outerHTML;
        rootElement.setAttribute(
          ReactMarkupChecksum.CHECKSUM_ATTR_NAME,
          checksum
        );

        var diffIndex = firstDifferenceIndex(markup, rootMarkup);
        var difference = ' (client) ' +
          markup.substring(diffIndex - 20, diffIndex + 20) +
          '\n (server) ' + rootMarkup.substring(diffIndex - 20, diffIndex + 20);

        ("production" !== process.env.NODE_ENV ? invariant(
          container.nodeType !== DOC_NODE_TYPE,
          'You\'re trying to render a component to the document using ' +
          'server rendering but the checksum was invalid. This usually ' +
          'means you rendered a different component type or props on ' +
          'the client from the one on the server, or your render() ' +
          'methods are impure. React cannot handle this case due to ' +
          'cross-browser quirks by rendering at the document root. You ' +
          'should look for environment dependent code in your components ' +
          'and ensure the props are the same client and server side:\n%s',
          difference
        ) : invariant(container.nodeType !== DOC_NODE_TYPE));

        if ("production" !== process.env.NODE_ENV) {
          ("production" !== process.env.NODE_ENV ? warning(
            false,
            'React attempted to reuse markup in a container but the ' +
            'checksum was invalid. This generally means that you are ' +
            'using server rendering and the markup generated on the ' +
            'server was not what the client was expecting. React injected ' +
            'new markup to compensate which works but you have lost many ' +
            'of the benefits of server rendering. Instead, figure out ' +
            'why the markup being generated is different on the client ' +
            'or server:\n%s',
            difference
          ) : null);
        }
      }
    }

    ("production" !== process.env.NODE_ENV ? invariant(
      container.nodeType !== DOC_NODE_TYPE,
      'You\'re trying to render a component to the document but ' +
        'you didn\'t use server rendering. We can\'t do this ' +
        'without using server rendering due to cross-browser quirks. ' +
        'See React.renderToString() for server rendering.'
    ) : invariant(container.nodeType !== DOC_NODE_TYPE));

    setInnerHTML(container, markup);
  }

最后

尝试一下下面的代码,想想React为啥认为这是错误的?

var Test = React.createClass({
  getInitialState: function() {
    return {name: 'world'};
  },
  render: function() {
    return (
        <p>Hello</p>
        <p>
            Hello {this.state.name}!
        </p>
    );
  }
});

React.render(
  <Test />,
  document.getElementById('content')
);

[工地] React中的Transaction模型

Transaction创建出一个黑盒,用于包裹任意方法,使得该方法的包裹在任意时候都正确触发,哪怕该方法在执行过程中抛错,如下图:

                       wrappers (injected at creation time)
                                      +        +
                                      |        |
                    +-----------------|--------|--------------+
                    |                 v        |              |
                    |      +---------------+   |              |
                    |   +--|    wrapper1   |---|----+         |
                    |   |  +---------------+   v    |         |
                    |   |          +-------------+  |         |
                    |   |     +----|   wrapper2  |--------+   |
                    |   |     |    +-------------+  |     |   |
                    |   |     |                     |     |   |
                    |   v     v                     v     v   | wrapper
                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
                    | |   | |   |   |         |   |   | |   | |
                    | |   | |   |   |         |   |   | |   | |
                    | |   | |   |   |         |   |   | |   | |
                    | +---+ +---+   +---------+   +---+ +---+ |
                    |  initialize                    close    |
                    +-----------------------------------------+

How to use?

// ES6 Object.assign,有点像jQuery.extend,具体参见:http://es6.ruanyifeng.com/#docs/object
var assign = require('react/lib/Object.assign'),
    Transaction = require("react/lib/Transaction");

// 要包裹的方法
function dosth() {
    console.log('dosth');
}

// 创建一个Transaction的子类
function MyTransaction() {
    this.reinitializeTransaction();
}

// mixin
assign(
    MyTransaction.prototype,
    Transaction.Mixin,
    {
        // 需要实现一个getTransactionWrappers方法,获取需要的包裹
        getTransactionWrappers: function() {
            return [{
                // 第一个包裹的初始化方法
                initialize: function (e) {
                    console.log('init first');
                },
                // 第一个包裹的完成方法
                close: function () {
                    console.log('close first');
                }
            }, {
                // 第二个包裹的初始化方法
                initialize: function (e) {
                    console.log('init second');
                },
                // 第二个包裹的完成方法
                close: function () {
                    console.log('close second');
                }
            }];
        }
    }
);

var myTransaction = new MyTransaction();

// 包裹dosth方法,scope是this
myTransaction.perform(dosth, this);
// init first
// init second
// dosth
// close first
// close second

从上面的例子,我们可以发现,在被包裹的函数运行前,会先运行wrappers的初始化方法,然后运行被包裹函数,最后运行wrappers结束方法。

// 前面省略
assign(
    MyTransaction.prototype,
    Transaction.Mixin,
    {
        getTransactionWrappers: function() {
            return [{
                initialize: function (e) {
                    // 让这里抛出错误
                    dosthundefined();
                    console.log('init first');
                },
                close: function () {
                    console.log('close first');
                }
            }, {
                initialize: function (e) {
                    console.log('init second');
                },
                close: function () {
                    console.log('close second');
                }
            }];
        }
    }
);

var myTransaction = new MyTransaction();

myTransaction.perform(dosth, this);
// init second
// close second
// Uncaught ReferenceError: dosthundefined is not defined

从上面的例子,我们可以发现,如果wrapper出现错误,则忽略该wrapper以及被包裹函数。

// 前面省略
function dosth() {
    dosthundefined();
    console.log('dosth');
}
// 中间省略
myTransaction.perform(dosth, this);
// init first
// init second
// close first
// close second
// Uncaught ReferenceError: dosthundefined is not defined

这个例子我们可以发现,如果被包裹函数出错,不会影响wrappers的运行。

具体实现请参考:https://github.com/facebook/react/blob/master/src/utils/Transaction.js ,比较简单。

Why we need Transaction?

由于虚拟DOMDOM两者是分别更新的,而两者是需要保持强一致性的,否则虚拟DOM中用户已经付款了,但页面却不显示用户付款了这就悲剧了!!!

那么哪里最有可能不可预知的错误,导致两者不一致呢?

回想一下同步逻辑:

JSX + 数据 -> 虚拟DOM -> 算出差异 -> 根据差异更新

错误最后可能出现在:

  1. 不可预知的数据,或者数据就是错误的,导致虚拟DOM生成时抛错
  2. 根据差异更新时,节点无法找到,或者DOM不符合预期

而Transaction正是为了解决虚拟DOMDOM同步的一致性而生的。

前沿技术解密——Virtual DOM

作为React的核心技术之一Virtual DOM,一直披着神秘的面纱。

实际上,Virtual DOM包含:

  1. Javascript DOM模型树(VTree),类似文档节点树(DOM)
  2. DOM模型树转节点树方法(VTree -> DOM)
  3. 两个DOM模型树的差异算法(diff(VTree, VTree) -> PatchObject)
  4. 根据差异操作节点方法(patch(DOMNode, PatchObject) -> DOMNode)

接下来我们分别探讨这几个部分:

VTree

VTree模型非常简单,基本结构如下:

{
    // tag的名字
    tagName: 'p',
    // 节点包含属性
    properties: {
        style: {
            color: '#fff'
        }
    },
    // 子节点
    children: [],
    // 该节点的唯一表示,后面会讲有啥用
    key: 1
}

所以我们很容易写一个方法来创建这种树状结构,例如React是这么创建的:

// 创建一个div
react.createElement('div', null, [
    // 子节点img
    react.createElement('img', { src: "avatar.png", class: "profile" }),
    // 子节点h3
    react.createElement('h3', null, [[user.firstName, user.lastName].join(' ')])
]);

VTree -> DOM

这方法也不太难,我们实现一个简单的:

function create(vds, parent) {
  // 首先看看是不是数组,如果不是数组统一成数组
  !Array.isArray(vds) && (vds = [vds]);
  //  如果没有父元素则创建个fragment来当父元素
  parent = parent || document.createDocumentFragment();
  var node;
  // 遍历所有VNode
  vds.forEach(function (vd) {
    // 如果VNode是文字节点
    if (isText(vd)) {
      // 创建文字节点
      node = document.createTextNode(vd.text);
    // 否则是元素
    } else {
      // 创建元素
      node = document.createElement(vd.tag);
    }
    // 将元素塞入父容器
    parent.appendChild(node);
    // 看看有没有子VNode,有孩子则处理孩子VNode
    vd.children && vd.children.length &&
      create(vd.children, node);

    // 看看有没有属性,有则处理属性
    vd.properties &&
      setProps({ style: {} }, vd.properties, node);
  });
  return parent;
}

diff(VTree, VTree) -> PatchObject

差异算法是Virtual DOM的核心,实际上该差异算法是个取巧算法(当然你不能指望用O(n^3)的复杂度来解决两个树的差异问题吧),不过能解决Web的大部分问题。

那么React是如何取巧的呢?

  • 分层对比

如图,React仅仅对同一层的节点尝试匹配,因为实际上,Web中不太可能把一个Component在不同层中移动。

  • 基于key来匹配

还记得之前在VTree中的属性有一个叫key的东东么?这个是一个VNode的唯一识别,用于对两个不同的VTree中的VNode做匹配的。

这也很好理解,因为我们经常会在Web遇到拥有唯一识别的Component(例如课程卡片、用户卡片等等)的不同排列问题。

  • 基于自定义元素做优化

React提供自定义元素,所以匹配更加简单。

patch(DOMNode, PatchObject) -> DOMNode

由于diff操作已经找出两个VTree不同的地方,只要根据计算出来的结果,我们就可以对DOM的进行差异渲染。

扩展阅读

具体可参考下面两份代码实现:

  1. @Matt-Esch实现的:virtual-dom
  2. 我们自己做的简版实现,用于Mobile页面渲染的:qvd

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.