Open buxuku opened 3 years ago
React有一条思想:一切皆组件,上面的方法虽然是功能上实现了,但createContext
返回的Provider
和Consumer
并不是一个组件.对于React.createContext({color: 'red'});
这个返回值 ,我们先看看原版返回的是什么.
可以看到,它返回的类似于之前的虚拟Dom,通过$$typeof
标识类型,Provider
和Connsumer
上面同时挂载了一个_context
的循环引用.最外层的_currentValue
和_currentValue2
挂载了对应的value
值,挂载两个主要用于主渲染和辅助渲染使用,这里我们关心一个即可.其它更多属性是在开发环境下增加了,也可以无须关注.
首先在src/constants/index.js
里面新增两个节点类型来做标识
+export const REACT_CONTEXT = Symbol('REACT_CONTEXT');
+export const REACT_PROVIDER = Symbol('REACT_PROVIDER');
按照官方生成的对象修改createContext
方法
-import {REACT_FORWARD_COMPONENT} from "../constants";
+import {REACT_FORWARD_COMPONENT, REACT_CONTEXT, REACT_PROVIDER} from "../constants";
-function createContext(value){
- let context = {
- _value: value,
- Provider,
- Consumer,
- };
- function Provider({value, children}){ // Provider接收一个value的props
- context._value = value;
- return children;
- }
- function Consumer({children}){ // Consumer的children是一个函数
- return children(context._value)
- }
- return context;
-}
+function createContext(value){
+ const context = {$$typeof: REACT_CONTEXT, _currentValue: null};
+ context.Provider = {
+ $$typeof: REACT_PROVIDER,
+ _context: context,
+ };
+ context.Consumer = {
+ $$typeof: REACT_CONTEXT,
+ _context: context,
+ }
+ return context;
+}
这样就可以返回一个基本上和官方主体内容一致的对象了.
这样改造之后,就需要在渲染的时候,增加这两种类型组件的判断.
修改src/react-dom/index.js
,增加mountProviderComponent
和mountContextComponent
方法, 在createDom
里面增加这两种组件类型的判断,同时需要注意这一句vdom.oldVdom = renderVdom;
,我们同样需要把它们子元素生成的虚拟Dom挂载在上面,以便后续更新的时候进行dom-diff;
-import {REACT_FORWARD_COMPONENT, REACT_TEXT, MOVE, REMOVE, INSERT} from '../constants';
+import {REACT_FORWARD_COMPONENT, REACT_TEXT, REACT_CONTEXT, REACT_PROVIDER, MOVE, REMOVE, INSERT} from '../constants';
function createDom(vdom) {
if (isNotNeedRender(vdom)) return
let dom;
const {type, props, ref} = vdom;
if (type && type.$$typeof === REACT_FORWARD_COMPONENT) {
return mountForwardComponent(vdom);
}
if (type === REACT_TEXT) {
dom = document.createTextNode(props.content);
}
+ if (type && type.$$typeof === REACT_PROVIDER){
+ return mountProviderComponent(vdom);
+ }
+ if (type && type.$$typeof === REACT_CONTEXT){
+ return mountContextComponent(vdom);
+ }
if (typeof type === 'string') {
dom = document.createElement(type);
renderAttributes(dom, props);
}
if (typeof type === 'function') {
if (type.isReactComponent) { // 是一个类组件
return mountClassComponent(vdom);
}
let renderVdom = wrapToVdom(type(props));
vdom.oldVdom = renderVdom;
// 让type执行,返回虚拟DOM,继续处理返回的虚拟DOM
return createDom(renderVdom);
}
if (props) {
const {children} = props;
if (Array.isArray(children)) {
reconcileChildren(children, dom);
} else {
reconcileChildren([children], dom);
}
}
vdom.dom = dom;
if (ref) {
ref.current = dom;
}
return dom;
}
+/**
+ * Provider组件的渲染,先更新Provider上面传递的value属性,然后渲染它的children元素
+ * @param vdom
+ * @returns {Text|*|HTMLElement|Text|HTMLElement}
+ */
+function mountProviderComponent(vdom){
+ const {type, props} = vdom;
+ type._context._currentValue = props.value;
+ const renderVdom = props.children;
+ vdom.oldVdom = renderVdom;
+ return createDom(renderVdom);
+}
+
+/**
+ * Consumer组件的渲染,获取到context上面的value,作为children函数的入参来获取返回的虚拟Dom
+ * @param vdom
+ * @returns {Text|*|HTMLElement|Text|HTMLElement}
+ */
+function mountContextComponent(vdom){
+ const {type, props} = vdom;
+ const renderVdom = props.children(type._context._currentValue);
+ vdom.oldVdom = renderVdom;
+ return createDom(renderVdom);
+}
同样的,在更新时,也需要增加这两种类型组件的判断
function updateElement(oldVdom, newVdom) {
if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT ) {
const dom = newVdom.dom = findDom(oldVdom);
if(oldVdom.props.content !== newVdom.props.content){ // 当文本内容有变化才进行更新
dom.textContent = newVdom.props.content;
}
}
+ if (oldVdom.type && oldVdom.type.$$typeof === REACT_PROVIDER){
+ updateProviderComponent(oldVdom, newVdom);
+ }
+ if (oldVdom.type && oldVdom.type.$$typeof === REACT_CONTEXT){
+ updateConsumerComponent(oldVdom, newVdom);
+ }
if (typeof oldVdom.type === 'string') { // 原生的HTML元素
const currentDom = newVdom.dom = findDom(oldVdom); // 把老的Dom节点直接复制过来
renderAttributes(currentDom, newVdom.props, oldVdom.props); // 更新节点属性
updateChildren(currentDom, oldVdom.props.children, newVdom.props.children);
} else if(typeof oldVdom.type === 'function'){
if (oldVdom.type.isReactComponent) {
updateClassComponent(oldVdom, newVdom)
}else{
updateFunctionComponent(oldVdom, newVdom);
}
}
}
+/**
+ * 更新Provider组件,先更新value值,然后把children的虚拟dom和之前挂载的老的虚拟dom进行对比更新
+ * @param oldVdom
+ * @param newVdom
+ */
+function updateProviderComponent(oldVdom, newVdom){
+ const parentDom = findDom(oldVdom);
+ const {type, props} = newVdom;
+ type._context._currentValue = props.value;
+ compareTwoVdoms(oldVdom.oldVdom, newVdom.props.children, parentDom);
+ newVdom.oldVdom = newVdom.props.children;
+}
+
+/**
+ * 更新Consumer组件,通过新的context上面的value值生成新的虚拟dom,和老的虚拟dom进行对比更新
+ * @param oldVdom
+ * @param newVdom
+ */
+function updateConsumerComponent(oldVdom, newVdom){
+ const parentDom = findDom(oldVdom);
+ const {type, props} = newVdom;
+ const renderVdom = props.children(type._context._currentValue);
+ compareTwoVdoms(oldVdom.oldVdom, renderVdom, parentDom);
+ newVdom.oldVdom = renderVdom;
+}
+
注意上面用到的findDom
方法,之前只涉及到原生的HTML元素,类组件,函数组件的判断.这里也需要进行修改一下
function findDom(vdom) {
if (!vdom || typeof vdom !== 'object') return;
const {type} = vdom;
let dom;
/**
* 比如render(){return <Demo />}这里面render出来的还不是最终的虚拟dom;
*/
- if (typeof type === 'function') {
+ if (typeof type === 'string' || type === REACT_TEXT) {
+ dom = vdom.dom;
+ } else {
if(vdom.classInstance){
dom = findDom(vdom.classInstance.oldVdom);
}else{
// vdom可能是一个函数或者类组件,需要继续递归查找真实的DOM节点.
dom = findDom(vdom.oldVdom);
}
- } else {
- dom = vdom.dom;
}
return dom;
}
修改src/react-dom/index.js
里面mountClassComponent
方法里面的contextType
变量名
if(type.contextType){
- classInstance.context = type.contextType._value;
+ classInstance.context = type.contextType._currentValue;
}
以及src/react/Component.js
里面的forceUpdate
方法里面的
if(this.constructor.contextType){
- this.context = this.constructor.contextType._value;
+ this.context = this.constructor.contextType._currentValue;
}
再来测试一下前面的CreatContext
组件,不出意外应该也是能够正常运行的.
在React里,对于数据的管理,除了
props
和state
,还有一个content
的API,它提供了跨组件的通信能力.这在一些小的项目里面,它也可以充当一个redux
的角色.React提供了一个
createContext
的方法,它接受一个默认值的入参,返回一个Provider
和Consumer
的组件,Provider
组件接受一个value
的props
属性,同时在Consumer
组件的children
为一个函数,它的入参便是该value
值.基于此,在
src/react/index.js
里面实现这个createContext
方法.在类组件中,我们一般是通过声明一个静态属性
contextType
来使用,当声明了该属性之后,该类的实例上面就会挂载一个context
的属性,其值便是Provider
的上面的value
值.修改
src/react-dom/index.js
里面的mountClassComponent
方法,如果类组件上面存在该静态方法,便将value
值挂载到context
上面.同时修改
src/react/Component.js
里面的forceUpdate
方法,因为在更新阶段,也要考虑Provider
上面的value
有更新了,所以同时也需要更新组件实例上面的context
值.新建
src/components/CreateContext.js
文件,写一个通过context
传递数据和方法的组件,包括类组件和函数组件里面的使用.可以看到,父组件上面的
color
属性和changeColor
方法,都可以在子组件,子子组件中通过context
来获取到,而无须通过props
的层层传递.