react源代码重点难点分析
网上已经有不少react源码分析文档,但都是分析主流程和主要功能函数,没有一个是从reactDOM.render()入口开始分析源码把流程逻辑走通尤其是把重点难点走通直到把组件template编译插入网页生效结束这样一个从头到尾的完整过程。本文从ReactDom.Render()入口开始追踪分析源码直到网页显示hello world(ajax从后台获取数据之后被替换为字符串)的整个处理流程,主要针对整个流程中重点难点细节环节分析源码是如何从头走到尾直到输出显示hello world的,本文忽略所有的功能段详细代码,有兴趣可以自己再行研究,不是本文重点,每个功能段的深入研究属于纵向研究,比如判断是否要update具体如何update(diff算法)等功能段,都已经有高手研究发表过文章。
从入口开始到最后显示hello world的处理流程逻辑过程非常复杂,有无数层封装,有一个程序要递归执行,有几段程序要被反复调用,要调用执行100多个函数分布在10几个文件中,尤其是react增加了transaction机制就更复杂了,跟天书一样。
react源码设计太复杂,reac和angular都是专业团队开发的,编程技术都是顶级的,但它们都不是简单高明的设计,有些复杂设计华而不实。比如angular内部模块机制和依赖对象注入机制搞太复杂,其实依赖对象就是全局共享对象,在各组件用import引用即可,没必要搞那么复杂,毫无意义,用webpack开发之后,angular内部的模块机制和依赖对象注入机制就毫无意义,所以angular 2.0也不再搞这些巨复杂又没有用的东西。再比如vue用compile递归编译所有的元素节点,而react把元素分成几种类型,每种类型有不同的mountComponent编译程序,还有performUpdateIfNecessary也按类型分好几种,源码中执行instance.mountComponent()时不知道是哪一种mountComponent,要debug看才能知道,所以debug跟踪react源码很困难。实际上元素类型除了html元素就是自定义组件标签,没必要分类搞那么多mountComponent,个人认为vue的设计是简单高明的,相比之下vue用简单的编程方法也能实现同样的功能甚至更多的功能,给广大程序员又多了一种选择。顺便说到,vue的数据响应设计方法也是最简单实用的,它没有直接使用redux再connect,而是开发了一套挺简单的vuex代码集成到vue就把难题给完美解决了,使用非常简单自然,几乎把专业顶级的高大上的react和angular给颠覆了。
react源码分布在几十个文件中,找函数代码要在这批文件中搜索出来,这无所谓,就算是写在一个文件中有几万行,也是要搜索找函数的,其中最重要的源码文件如下:
React.js
ReactClass.js
ReactElement.js
ReactDOM.js
ReactMount.js
instantiateReactComponent.js
ReactComponent.js
ReactDOMComponent.js
ReactCompositeComponent.js
ReactReconciler.js
ReactUpdates.js
Transaction.js
还有react-redux两个文件:
connect.js
provider.js
首先,有必要啰嗦一下有关js编程的核心技术方法,在源码中,凡是var xxx={},这是要干嘛?一般来说就是定义属性,要合并到一个类中去。js用函数和prototype属性模拟类的实现,用new 函数()实例化类。在源码中凡是有: function xxx(){} xxx.prototype=yyy 这是要干嘛?这就是定义一个类,这个类被使用的方法有两种: 一种是new xxx()实例化。 一种是被其它类继承合并,js没有类也没有类继承方法,都是用js对象复制或引用来实现的,js的核心对象技术就是Object.xxx()方法。
js编程玩来玩去其实就是这点技术,但代码写复杂了会看晕,因为开发框架源码封装太多太复杂,流程逻辑太复杂,细节处理太多太复杂,再加上js有call/apply进行作用域变换,每段代码中的this在执行的时候到底代表哪个对象实例就可能晕菜了。
首先描述一下测试项目基本环境和细节,测试项目采用minimum项目,只有一个/路由组件,显示<div>hello world</div>,如果ajax从后台获取到数据,则显示从后台获取的一个字符串,组件使用redux的store数据。
react项目入口代码:
ReactDOM.render(
<Provider store={store}>
<Router history={history} children={routes} />
</Provider>,
MOUNT_NODE
路由routes.js代码:
import { injectReducer } from 'REDUCER'
import createContainer from 'UTIL/createContainer'
const connectComponent = createContainer(
({ userData, msg }) => ({ userData, msg }),
require('ACTION/msg').default
)
export default {
path: '/',
getComponent (nextState, cb) {
require.ensure([], (require) => { //异步加载组件
injectReducer('msg', require('REDUCER/msg/').default) // 建立store['msg']=reduce方法
cb(null, connectComponent(require('COMPONENT/App').default))
}, 'App')
}
}
/路由组件App.js代码:
import React, { Component } from 'react'
export default class App extends Component {
componentWillMount () {
let _this = this
setTimeout(function() {
_this.props.fetchMsg() // 通过ajax从后台获取字符串保存到store,网页先显示hello world,之后会自动更新为新的字符串
}, 2000)
}
componentWillReceiveProps (nextProps) {
console.log(this)
}
render () {
return (
<div>{ this.props.msg.msgs[0] ? this.props.msg.msgs[0].content : 'hello world' }</div>
)
}
}
react项目入口方法是:
ReactDOM.render(根组件template)
相当于vue 1.0的router.start(app)或vue 2.0的new Vue(app)。
ReactDOM.render方法代码:
function (nextElement, container, callback) {
return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
},
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement); // 构造一个Element,第一次是构造toplevel Element
//React.createElement创建ReactElement对象(vnode),含有type,key,ref,props属性,这个过程中会调用getInitialState()初始化state。
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
return component;
产生的component就是根组件,根组件中第一个节点就是provider组件,里面有层层子节点,有/路由组件,每个element元素/组件有props属性和type(构造函数)。
那么是从这里开始层层递归到/路由组件节点的。
_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
//nextelement就是根元素节点provider组件元素
var componentInstance = instantiateReactComponent(nextElement, false); //element -> instance
function instantiateReactComponent(node, shouldHaveDebugID) { //这个方法是根据Element产生warpper instance,再执行instance.mountComponent开始编译组件节点
//instance = new ReactCompositeComponentWrapper(element); //这句相当于是下面一句
instance = new this.construct(element); // 这是instance的构造函数,就是设置一些属性,很普通
var ReactCompositeComponentMixin = { // 这是instace构造函数代码
construct: function (element) {
this.xxx = yyy;
}
}
return instance;
//instantiateReactComponent根据ReactElement的type分别创建ReactDOMComponent, ReactCompositeComponent,ReactDOMTextComponent等对象,
//不同的对象instance有不同的mountComponent方法,所以react源码文件有无数个mountComponent函数,其实html元素节点可以不当做component处理
//instantiateReactComponent被调用执行多次,每次被调用就处理一个元素节点产生一个instance,在本例minimum项目中,产生如下instance:
_instance:TopLevelWrapper
_instance:Provider // provide节点
_instance:Constructor // router节点
_instance:Constructor
_instance:Connect // /路由组件外层
_instance:App //这是/路由组件实例
ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context); //从根元素provider开始编译处理
至此处理结束,根组件template已经编译插入网页生效,已经从根元素递归处理到最底层元素,所以处理root元素很简单,复杂在于从root元素递归处理子节点再层层返回。
batchedUpdates会调用batchedMountComponentIntoNode,从这个函数开始追踪:
function batchedMountComponentIntoNode(componentInstance, container, shouldReuseMarkup, context) { //从根元素provider开始
//batchedMountComponentIntoNode以transaction事务的形式调用mountComponentIntoNode
transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReu //从根元素provider开始
var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, //mountComponent相当于compile,从根节点开始编译
//Reconciler.mountComponent就是执行instance里面的mountComponent,在执行performInitialMount时会递归调用自己。
//mountComponent的目的是解析得到每个节点的HTML代码(最后插入网页生效),react叫做markup,是类似vue的vnode对象。
Reconciler.mountComponent: function (internalInstance, transaction,
var markup = internalInstance.mountComponent(transaction, hostParent, hostContainerInfo, context, parentDebugID);
return markup;
// internalInstance是不同的component实例,最典型的component节点类型是html元素节点比如<div>和组件元素节点比如<App>
Reconciler.mountComponent会递归调用自己完成从根元素递归到最底层元素<div>,是react源码的最核心最关键最牛的代码,因为前端代码要递归html元素tree,这是与后台代码不同的,也是非常复杂的,一旦递归元素tree,就要开始晕菜了,但代码效率巨高,一个递归就完事了,对于元素tree也只有用递归,除非定死了只有两三层可以不用递归,就可以楞写三层。
不同的instance有不同的mountComponent方法,我们先来看组件元素的mountComponent编译方法:
mountComponent: function (transaction, hostParent, hostContainerInfo, context) { //mountComponent其实就是编译节点的意思,react把一切节点视为component
var publicProps = this._currentElement.props;
var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue);
//inst就是new组件实例,那么是在mountComponent阶段初始化组件实例的,new组件实例之后,执行其render()方法就又产生Element,就又需要递归循环element -> instance -> instance.mountComponent -> inst(组件实例) -> render() -> element 如此递归到子节点
_constructComponent: function (doConstruct, publicProps, publicContext, updateQueue) {
return this._constructComponentWithoutOwner(doConstruct, publicProps, publicContext, updateQueue);
_constructComponentWithoutOwner: function (doConstruct, publicProps, publicContext, updateQueue) {
var Component = this._currentElement.type; //type就是组件构造函数(组件定义代码)
return new Component(publicProps, publicContext, updateQueue); //App组件外套connect组件,这就是new Connect()组件实例的位置,找到这个位置在分析react源码的道路上就前进了一大步,因为组件定义代码无外乎就是定义一些属性,框架肯定准备了一个组件基类,到时一合并,再new实例,这是js唯一的机制,不可能有其它方法,找到这个位置,再前后去追踪,就有较大的可能能看懂框架到底是如何初始化组件实例的,我们定义的组件代码到底到底是何时如何被执行的。
inst.props = publicProps; // double set this.props
this._instance = inst; //new组件实例保存在instance中,只要执行instance的方法,就可以从this取回inst实例,再执行inst实例里面的render()方法产生一个Element递归下去
markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
//inst保存在this实例中,调用performInitialMount时无需传递inst,renderedElement是空的
return markup; //markup就是编译产生的结果,相当于vnode,含html代码
performInitialMount: function (renderedElement, hostParent, hostContainerInfo, transaction, context) {
renderedElement = this._renderValidatedComponent(); //执行inst.render()产生Element,每种组件inst有自己的render方法,provder/router/connect/app组件都有自己的render方法,app的render方法是应用写的,系统组件的render方法都是事先设计好的,比如connect的render方法,还有一个router-context组件
//app的render方法里面是jsx语法,编译时每个节点已经转换为createElement(),所以render方法就是返回一个根元素Element,它里面有多少子元素再递归处理
var child = this._instantiateReactComponent(renderedElement, //根据元素类型生成instance
this._renderedComponent = child;
var markup = ReactReconciler.mountComponent(child, // 在这里递归调用Reconciler.mountComponent,处理下一个子节点child,是前面根据Element生成的
return markup;
这是组件component的mountComponet编译方法,再来看html元素component的mountComponent编译方法:
//ReactDOMComponent是针对html元素,在这个minimum项目中,根组件template中只有一个<div>元素节点
ReactDOMComponent.Mixin = {
mountComponent: function (transaction, hostParent, hostContainerInfo, context) { //只针对<div>执行一次
var props = this._currentElement.props; // 当前元素是div,子节点就是文本内容hello world
if (hostParent != null) {
} else if (hostContainerInfo._tag) {
namespaceURI = hostContainerInfo._namespaceURI;
parentTag = hostContainerInfo._tag;
}
if (transaction.useCreateElement) { //为true
if (namespaceURI === DOMNamespaces.html) {
} else {
el = ownerDocument.createElementNS(namespaceURI, this._currentElement.type); // el就是空的div
}
}
if (!this._hostParent) {
DOMPropertyOperations.setAttributeForRoot(el);
}
this._updateDOMProperties(null, props, transaction);
var lazyTree = DOMLazyTree(el); //子节点hello world文本内容已经插入到tree.node中
this._createInitialChildren(transaction, props, context, lazyTree);
mountImage = lazyTree;
return mountImage; //返回已经插入文本内容的node对象(相当于vue的vnode)
再回到mountComponentIntoNode看Reconciler.mountComponent从根元素provider开始递归编译子节点之后再执行什么:
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReu
var markup = ReactReconciler.mountComponent(wrapperInstance, transaction,
ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction);
//从根元素开始递归编译react内部的元素比如provider/router,一直到app组件到最底层的div元素节点,markup就是从最底层返回的编译结果,就是<div>hello world</div>,但是以对象vnode方式表示的。
但debug看markup是comment,编译到router组件就结束返回了,这是这么回事?这个问题非常复杂,是因为本项目使用了异步加载组件,当递归编译router组件时,返回comment z占位元素插入网页,同时去异步加载组件文件,然后再继续递归编译router组件,继续递归编译到app组件,最后返回编译结果再插入网页替换之前的comment生效。
如果不用异步加载,就没这个问题,从根元素一直递归到div元素结束返回markup=div(含hello world),异步加载改变/干扰了正常的流程逻辑。
顺便提一下,对于异步加载,webpack在编译打包时是用webpackJsonp实现的,#.build.js文件是配合webpackJsonp构造的,打包文件中也在全局空间定义了一个window.webpackJsonp函数方法,因为是用<script src=#.build.js 加载模块文件,必须在全局空间定义webpackJsonp方法,从服务端返回回调此方法时传递模块代码再保存到打包代码build.js中的闭包函数中的modules[]中。为什么要用jsonp这么麻烦是因为它不是要在全局加载执行js文件,而是要加载文件获取模块代码保存到闭包函数中的modules[]中,框架的所有代码都是在闭包中执行,打包文件build.js代码结构是:
(function(modules) { // webpackBootstrap
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) { // server端返回时调用,传入模块数据
modules[moduleId] = moreModules[moduleId]; // 把server端返回的模块保存到闭包中的modules[]中
}
}
)([模块,模块,...]);
组件编译完成之后插入网页有可能替换占位元素,也可能是插入到占位元素之后,或者是插入到占位元素的父元素里面做为父元素的子元素,如果comment占位元素已经被替换/删除了,之后再更新时就找container元素的第一个子节点替换之,这取决于底层插入代码是如何设计的。
mountComponentIntoNode只执行一次,它是编译根组件插入到网页中container元素<div id="app">里面,markup是根组件的编译结果<div>hello world</div>。
根组件编译插入网页之后是不会被切换的,只是会重新编译更新,而<router>标签意味着router组件会切换路由应用组件插入到这个位置显示,那么每次路由切换时从router
组件开始递归编译处理路由应用组件,插入到router元素占位元素位置替换调原来的内容,路由应用组件的插入位置就是router元素的占位元素位置,占位元素可以用comment,也可以用container.firstchild做为插入替换位置,如果只是更新应用组件,则不同于切换组件,因为组件已经在网页显示,只是如何更新的问题,有可能只是更新组件的局部,不需要把组件整个重新插入到网页中,就需要在网页container找组件根节点找子节点插入替换。
在递归过程中当编译处理router组件时会递归编译处理/路由组件也就是app组件及其子节点,会展开另外一个流程,属于路由切换处理流程,由router组件负责处理,不在上面这个逻辑里面,上面这个逻辑只是编译处理根组件根元素。
当递归编译子节点到最底层<div>元素完成之后层层返回时,由于编译处理是套了一层transaction.perform,是在transaction机制里面执行,每次递归编译处理完之后会返回到transaction.perform,这是react设计最复杂深奥的环节:
transaction.perform会被多次执行,method会不断变化,当某一次执行到:
ret = method.call(scope, a, b, c, d, e, f method=runBatchedUpdates(transaction
之后网页显示hello world,说明编译之后是执行runBatchedUpdates插入网页生效的,而不是mountImage。
下面是runBatchedUpdates流程:
function runBatchedUpdates(transaction) {
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcile //执行这句之后网页显示hello world
performUpdateIfNecessary跟mountComponent一样也是不同类型的元素有不同的方法,组件的方法是:
performUpdateIfNecessary: function (transaction) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
updateComponent: function (transaction, prevParentElement, nextParentElement, prevU
this._performComponentUpdate(nextParentElement, nextProps, nextSt
_performComponentUpdate: function (nextElement, nextProps, nextState, nextCont
this._updateRenderedComponent(transaction,
_updateRenderedComponent: function (transaction, context) {
var child = this._instantiateReactComponent(nextRenderedElem
var nextMarkup = ReactReconciler.mountComponent(child, //返回编译好的vnode(含div含hello world)
this._replaceNodeWithMarkup(oldHostNode, nextMarkup, //是在这里调用_replaceNodeWithMarkup
_replaceNodeWithMarkup: function (oldHostNode, nextMarkup, prevInstance) { //p1是comment,p2是div
ReactComponentEnvironment.replaceNodeWithMarkup(oldHostNode, nextMarkup,
var ReactComponentEnvironment = {
injection: {
injectEnvironment: function (environment) {
ReactComponentEnvironment.replaceNodeWithMarkup = environment.replaceNodeWithMarkup;
ReactComponentEnvironment.processChildrenUpdates = environment.processChildrenUpdates;
//debug看传入的environment的两个函数代码是:
dangerouslyReplaceNodeWithMarkup: function (oldChild, markup) { //p1=cmmment,p2=div
if (typeof markup === 'string') {
var newChild = createNodesFromMarkup(markup, emptyFunction)[0];
oldChild.parentNode.replaceChild(newChild, oldChild);
} else {
DOMLazyTree.replaceChildWithTree(oldChild, markup);
dangerouslyProcessChildrenUpdates: function (parentInst, updates) { //p1=div, p2=div的内容
var node = ReactDOMComponentTree.getNodeFromInstance(parentInst); // node是DOM元素div
DOMChildrenOperations.processUpdates(node, updates);
}
//底层操作网页元素的方法是:
node.firstChild.nodeValue = text; 或:node.textContent = text;
上面流程最底层代码才是真正把<div>hello world</div>插入网页替换之前的comment元素,如果之前没有执行_mountImageintoNode没有把comment元素插入网页,comment元素就没有父节点<div id="app">,就没法通过comment元素的父节点用<div>hello world</div>替换comment,因为comment没有在网页中没有父节点。如果没有异步加载,就没有comment问题,就是把最终编译结果插入container,之后再更新时就找container.firstchild替换。
更新时也会调用ReactReconciler.mountComponent再调用相应的编译方法编译元素节点,更新一个组件就是重新编译组件,逻辑是符合的,框架都是如此设计的。
从入口开始编译处理根组件template并且插入网页生效,根组件template的根元素<provider>有tree结构,递归所有子节点,上层子节点是管理组件,最后是connect组件和App组件,最底层Element元素是一个<div>hello world</div>元素节点。
js代码的目的就是动态构造html代码把数据插入html代码再插入网页,框架为了实现语义化和组件机制还有路由切换以及全局数据费了九牛二虎之力,深入到框架内部庞大复杂的机制里面把几十上百个函数流程都走一遍,把几千几万行代码都执行一遍,有一段核心程序是递归反复执行,有几段核心程序是反复调用多次,最后才回到最终的目标,就是把组件的template编译插入数据再插入网页,编译完之后如何转到这最后一步涉及到transaction机制,设计得非常深奥。
至此还有一个问题就是如何执行到runBatchedUpdates?
react用transaction.perform机制来执行里面的函数方法,先执行wrapper的init方法,再执行函数方法,然后再执行wrapper的close方法,在创建update transaction实例时会构造如下的wrapper和init/close方法:
var NESTED_UPDATES = {
initialize: function () {
this.dirtyComponentsLength = dirtyComponents.length;
},
close: function () {
dirtyComponents.splice(0, this.dirtyComponentsLength);
flushBatchedUpdates(); // 会执行runBatchedUpdates
可以看到当执行完编译相关函数之后会执行wrapper的close方法,就会执行flushBatchedUpdates方法,这个方法会执行runBatchedUpdates方法(以transaction.perform的形式),会一直层层调用到底层方法把html插入网页生效。
transaction机制本质上就是把编译函数放在try中执行,把更新函数放在finally中执行,把整个处理流程给分裂成两个,它不是编译完就直接调用更新函数,分析源代码流程逻辑破解这个点就有很高的难度,搞不好就卡在这个点上没法继续下去,搞不清程序流程逻辑往下是怎么走的。
其实程序执行一旦有错是没法补救的,所以用什么办法都没有意义,只是形式上和报错方面可以更优雅一点而已。
下面是一个简单使用 Transaction 的例子(原文地址https://zhuanlan.zhihu.com/p/20328570):
var Transaction = require('./Transaction');
// 我们自己定义的 Transaction
var MyTransaction = function() {
// do sth.
};
Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function() {
return [{
initialize: function() {
console.log('before method perform');
},
close: function() {
console.log('after method perform');
}
}];
};
});
var transaction = new MyTransaction();
var testMethod = function() {
console.log('test');
}
transaction.perform(testMethod);
// before method perform
// test
// after method perform
transaction机制其实就是类似angular 2.0使用的zone.js,也有点类似promise的作用,都是对异步过程加强控制,promise已经成为js原生功能。
使用异步过程管理增强功能代码之后,写代码和看代码就复杂了,要熟悉增强代码才行,否则看代码没法看,增强代码相当于一个外部插件。
增强代码都是可有可无的,它们的作用其实很微弱,甚至本质上没有什么作用,只是改变了编程形式,使编程形式更高级优雅,看似是异步过程同步似的,其实本质上大都还是顺序执行代码的一个顺序流,并不是像多线程并发那样的真正的异步并发过程同步。
对于一般人设计应用程序来说,使用这些复杂的增强功能代价太大,没有必要,但大公司项目产品使用它们,要学习源码就躲不开它们。
小结一下从根Element开始的处理流程:
Element -> wrapper instance -> instance.mountComponent(compile) -> new instance._currentElement.type()组件实例 -> 组件实例.render()产生Element -> 递归子节点
元素树结构:
provider->router->connect->app
每层元素根据类型创建instance,执行其mountComponent,再new 组件实例inst,再执行组件实例inst的render()方法产生一个element,再递归。
我们在定义组件时并没有定义组件构造函数,组件构造函数是根据组件函数定义或类定义以及ReactComponent基类自动构造出来的,只需要用prototype包含一些属性方法即可,很简单,这样在new 组件构造函数()实例化之后含有属性方法即可,比如render()方法。new实例之后就可以执行实例inst.render()产生组件根元素Element,再根据Element产生wrapper instance含相应的mountComponent编译方法,再通过reconciler.mountComponent调用instance.mountComponent编译组件,递归子节点子组件。
所以其实最根本的是第一个根元素Element含有一个子节点,再递归编译子节点时,这个子节点又含有一个子节点,这都是框架设计好的元素节点,然后就会递归到app组件,
先new App()实例化,再执行inst.render()产生一个根元素,这个根元素就是<div>,这是我们在app组件template写的根元素。然后就再递归编译处理<div>根元素,它没有子节点,只有一个文本内容hello world,那么就到此结束。返回到transaction.perform,执行update流程,把/路由组件编译结果<div>hello world</div>插入到<div id="app">替换之前插入的comment不可见占位元素,此时网页显示hello world。等ajax从后台获取数据更新store之后,app组件会通过connect组件自动更新,网页显示另外一个字符串。
递归编译router组件时要递归两次,第一次时instance.props含/路由,第二次时instance.含connect组件,再递归就是app组件,再递归就是div元素,这次是调用domcomponent.mountcomponent编译方法,如果div里面又有组件节点,则又要调用compositecomponent.mountcomponent递归编译组件。
react用connect连接应用组件和redux,应用组件代码没有定义属性方法,应用组件的完整定义实际上是这样的:
connect(state,action)(component)
在元素树/组件树结构中最上层还有provider/router组件,而且router有两层,有一层是context,应用组件有两个,一个是connect,一个是app。
说到connect有一点要注意,那就是要注意connect的render方法:
Connect.prototype.render = function render() {
this.renderedElement = (0, _react.createElement)(WrappedComponent, this.mergedProps);
当store中的属性变化触发执行connect组件的render方法时,可以看到,它产生的Element是App组件元素,并且传递props,那么递归编译处理就是编译更新App组件,在new App(props,context)组件实例时传入props,组件基类ReactComponet构造函数代码有一句this.props=props就是设置app组件的props,之后组件就具有之前传递给connect(props,action)的属性方法。
所以connect是App的代理组件/接口组件,App组件并没有定义props属性,也没有定义和“注册”更新方法listener,组件定义代码只是一部分,要和connect代码“合并”才是完整的代码,每个应用组件都要复用connect组件代码,debug一个组件的代码也涉及到connect组件代码,Connect接口组件的作用就是绑定store。
关于connect还有一点,Connect组件构造函数代码是:
Connect(props,context){}
这么写是有点迷惑的,会误以为props就是之前传递给connect(state,action)的props,其实不是,组件形式上统一这么写,实际上在new Connect()时,只需传递context,里面有store,而props是没东西的,Connect的属性方法都是在prototype定义的,没有
需要在new实例时再传入的属性,在new Connect()时传入的props即使为空结果也一样,在new App(props,context)实例时才需要传入props,这个props是Connect的listener被触发执行获取props再传递给render方法再传递到new App(props)的。
另外router的render方法也需要注意一下,否则debug看数据根本就看不懂:
render: function render(props) {
return _react2.default.createElement(_RouterContext2.default, props);
}
可以看到router也会创建一个element,props就是路由参数,从根组件开始编译时路由就是/,context就是要传递的router实例,每个应用根组件都需要访问router实例。
框架的设计原理是基于template模板的,框架组件的template是如何被处理的我们一般不太容易能看到,template首先会被编译,编译结果是一个render方法代码,里面含createElement()方法,层层嵌套,是一个节点树。再执行render方法代码则产生一个根元素(vnode),含层层嵌套的子节点,是js对象嵌套,不是html对象嵌套。之后从根元素开始compile编译,递归子节点,最后把编译产生的html内容插入网页生效。所以除了编译template之外,框架只要设计一个compile(element)递归程序就把事情搞定了,只要能处理子节点子组件嵌套即可,react的编译程序就是mountComponent,搞得比较复杂麻烦。
react的模板设计是在应用组件外层套了好几层组件,顶层是provider,再通过context传递store,从根组件template根元素provider开始编译递归直到应用组件的根元素再到最底层的div元素,第一个元素还不是provider,而是toplevelwrapper,router元素有两层,应用组件套一层connect组件结果产生了两个组件实例,头几次递归编译处理的元素都是react内部设计好的元素,从/路由组件元素开始才递归编译处理应用组件,debug递归编译程序会看到几个莫名其妙的元素,确实太复杂了。
相比之下vue的入口就是router.start(App)或new Vue(App)从根组件开始查路由表再执行/路由组件以及缺省子路由组件,从template根元素开始编译用compile递归所有子节点包括子组件节点,最后插入网页生效。在每一个组件代码里面定义所有的属性方法,不管是组件本地属性方法还是绑定到store的属性方法,非常简单纯粹,写组件代码很简单,debug也方便,维护和查找问题也很容易。
再就是provider标签属于内部机制,不应该暴露出来,在html模板写<provider>有点莫名其妙,以演示hello world为例(假定hello world是通过ajax从后台获取并保存在store的),react是最麻烦的。
相比之下vue设计简单高明,vue只有一个<router-view>标签针对路由切换,其它的功能都无需写在template上,都隐藏了,由源码自行解决,router实例自动传递到每一个应用组件,每个应用组件如果要使用全局数据只需写一个vuex:{}配置即可,无需套这套那的套connect什么的,vue用set/get方法简单地解决了数据响应难题,应用编程无需涉及store/state机制,只需要写getUserInfo/setUserInfo这样的方法即可,store/state数据做为vue内置机制而不是外部插件需要连接绑定那么麻烦。
react源码最难的地方在于在多个地方会调用ReactConciler.mountComponent这个方法,这个方法本身会递归(子节点):
var markup = internalInstance.mountComponent(transaction, hostParent, hostContainerInfo, context, parentDebugID);
return markup;
因此每次一执行时在递归编译哪个子元素?产生的markup是什么?返回到哪个调用位置?是无法想清楚的,太复杂了,因为可以无限递归,无数次调用,设计者具有惊人逻辑思维能力,把每一次递归执行时的情况都想清楚了。
至此本文差不多就结束了,其实源码中有些细节也还是很深奥不解的,比如
var tagContent = this._createContentMarkup(transaction, props, context)
这段代码不知道有何作用?react如何处理类似transclude?
还有transaction机制不太清楚有何作用?queue异步调度好像与vue差不多。
还有一些细节也还没有完全搞明白。
文中有错误之处欢迎指正。