内容简介:在工作做我们一直使用react及react相关框架(antd/antd-mobile)但是对于react的深层的了解不够:JSX语法与虚拟DOM的关系?高阶组件是如何实现的?dom diff的原理?通过写一篇react小册来查缺补漏。
在工作做我们一直使用react及react相关框架(antd/antd-mobile)
但是对于react的深层的了解不够:JSX语法与虚拟DOM的关系?高阶组件是如何实现的?dom diff的原理?
通过写一篇react小册来查缺补漏。
JSX和虚拟DOM
import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render( <label className="test" htmlFor='hello'> hello<span>world</span> </label>, document.getElementById('root') ); 复制代码
使用 ReactDOM.render
,第一个参数传入JSX语法糖,第二个参数传入container,能简单实现在document上创建h1 dom节点。
其实,内部的执行方式如下:
import React from 'react'; import {render} from 'react-dom'; render( React.createElement( 'h1', {name:'yy',className:'test'}, 'hello', React.createElement( 'span', null, 'world' ) ), document.getElementById('root') ); 复制代码
所以ReactDOM.render的时候,看似引入的React没有用,但必须引入。
生产的HTML:
<label for="hello" class="test">hello<span>world</span></label> 复制代码
debug一下react createElement源码了解流程:
var React = { ... createElement: createElementWithValidation, ... } function createElementWithValidation(type, props, children) { var element = createElement.apply(this, arguments); ...//校验迭代器数组是否存在唯一key ...//校验fragment片段props ...//props type校验 return element } function createElement(type, config, children) { var propName = void 0; // Reserved names are extracted var props = {}; var key = null; var ref = null; var self = null; var source = null; ... return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props); } 复制代码
从 React.createElement
开始执行,完成了参数校验, 迭代
展开childrens的参数:type, props, key, ref, self, source。返回了一个类似于babel语法树结构的嵌套对象(只是个人认为...),如图下:
简化版:保留了返回对象中最关键的属性(type,props)
function ReactElement(type,props) { this.type = type; this.props = props; } let React = { createElement(type,props={},...childrens){ childrens.length===1?childrens = childrens[0]:void 0 return new ReactElement(type,{...props,children:childrens}) } }; 复制代码
通过上面的梳理,React.createElement返回的是一个含有type(标签),和它标签属性和内部对象(children)的Object
{ props:{ childrens:['text',{type:'xx',props:{}}] name:'xx' className:'xx' } type:'xx' } 复制代码
我们可以根据ReactDom.render()的入参,简写出它的实现方法。
let render = (vNode,container)=>{ let {type,props} = vNode; let elementNode = document.createElement(type); // 创建第一个元素 for(let attr in props){ // 循环所有属性 if(attr === 'children'){ // 如果是children表示有嵌套关系 if(typeof props[attr] == 'object'){ // 看是否是只有一个文本节点 props[attr].forEach(item=>{ // 多个的话循环判断 如果是对象再次调用render方法 if(typeof item === 'object'){ render(item,elementNode) }else{ //是文本节点 直接创建即可 elementNode.appendChild(document.createTextNode(item)); } }) }else{ // 只有一个文本节点直接创建即可 elementNode.appendChild(document.createTextNode(props[attr])); } }else{ elementNode = setAttribute(elementNode,attr,props[attr]) } } container.appendChild(elementNode) }; function setAttribute(dom,name,value) { if(name === 'className') name = 'class' if(/on\w+/.test(name)){ name = name.toLowerCase(); dom[ name ] = value || ''; }else if ( name === 'style' ) { if ( !value || typeof value === 'string' ) { dom.style.cssText = value || ''; } else if ( value && typeof value === 'object' ) { for ( let name in value ) { dom.style[ name ] = typeof value[ name ] === 'number' ? value[ name ] + 'px' : value[ name ]; } } }else{ if ( name in dom ) { dom[ name ] = value || ''; } if ( value ) { dom.setAttribute( name, value ); } else { dom.removeAttribute( name ); } } return dom } 复制代码
dom diff
Ract作为数据渲染DOM的框架,如果用传统的删除整个节点并新建节点的方式会很消耗性能。
React渲染页面的方法时比较对比虚拟DOM前后的变化,再生产新的DOM。
检查一个节点是否变化,要比较节点自身及它的父子节点,所以查找任意两棵树之间最少修改数的时间复杂度是O(n^3)。
React比较只比较当前层(同一颜色层),将比较步骤优化到了接近O(n)。
一、创建dom
优化JSX和虚拟DOM中, createElement
方法:
element.js
let utils = require('./utils') class Element { constructor(tagName, attrs, key, children) { this.tagName = tagName; this.attrs = attrs; this.key = key; this.children = children || []; } render() { let element = document.createElement(this.tagName); for (let attr in this.attrs) { utils.setAttribute(element, attr, this.attrs[attr]); element.setAttribute('key', this.key) } let children = this.children || []; //先序深度遍历 children.forEach(child => { let childElement = (child instanceof Element) ? child.render() : document.createTextNode(child); element.appendChild(childElement); }); return element; } } 复制代码
引申一下(先序遍历)
class Tree { constructor(v, children) { this.v = v this.children = children || null } } const tree = new Tree(10, [ new Tree(5), new Tree(3, [new Tree(7), new Tree(11)]), new Tree(2) ]) module.exports = tree 复制代码
const tree = require('./1.Tree') function tree_transverse(tree) { console.log(tree.v)//10 5 3 7 11 2 tree.children && tree.children.forEach(tree_transverse) } tree_transverse(tree) 复制代码
创建原始dom dom1,插入到页面。
let ul1 = createElement('ul', {class: 'list'}, 'A', [ createElement('li', {class: 'list1'}, 'B', ['1']), createElement('li', {class: 'list2'}, 'C', ['2']), createElement('li', {class: 'list3'}, 'D', ['3']) ]); let root = dom1.render(); document.body.appendChild(root); 复制代码
创建节点变化的DOM树 dom2,修改了dom2的父节点ul的属性class,新增并修改了子节点的位置
let ul2 = createElement('ul', {class: 'list2'}, 'A', [ createElement('li', {class: 'list4'}, 'E', ['6']), createElement('li', {class: 'list1'}, 'B', ['1']), createElement('li', {class: 'list3'}, 'D', ['3']), createElement('li', {class: 'list5'}, 'F', ['5']), ]); 复制代码
我们不能生硬得去直接销毁dom1,新建dom2。而是应该比较新旧两个dom,在原始dom上增删改。
let patches = diff(dom1, dom2,root) 复制代码
- 首先对两个节点进行
文本节点
比较
function diff(oldTree, newTree, root) { let patches = {}; let index = 0; walk(oldTree, newTree, index, patches, root); return patches; } function walk(oldNode, newNode, index, patches, root) { let currentPatch = []; if (utils.isString(oldNode) && utils.isString(newNode)) { if (oldNode != newNode) { currentPatch.push({type: utils.TEXT, content: newNode}); } } } 复制代码
如果文本不同,我们 打补丁
,记录修改的类型和文本内容
- 标签比较:如果标签一致,进行属性比较。不一致说明节点被替换,记录替换补丁
·· else if (oldNode.tagName == newNode.tagName) { let attrsPatch = diffAttrs(oldNode, newNode); if (Object.keys(attrsPatch).length > 0) { currentPatch.push({type: utils.ATTRS, node: attrsPatch}); } } else { currentPatch.push({type: utils.REPLACE, node: newNode}); } ··· 复制代码
- 根据补丁,修改原始dom节点
let keyIndex = 0; let utils = require('./utils'); let allPatches;//这里就是完整的补丁包 let {Element} = require('./element') function patch(root, patches) { allPatches = patches; walk(root); } function walk(node) { let currentPatches = allPatches[keyIndex++]; (node.childNodes || []).forEach(child => walk(child)); if (currentPatches) { doPatch(node, currentPatches); } } function doPatch(node, currentPatches) { currentPatches.forEach(patch=> { switch (patch.type) { case utils.ATTRS: for (let attr in patch.node) { let value = patch.node[attr]; if (value) { utils.setAttribute(node, attr, value); } else { node.removeAttribute(attr); } } break; case utils.TEXT: node.textContent = patch.content; break; case utils.REPLACE: let newNode = (patch.node instanceof Element) ? patch.node.render() : document.createTextNode(patch.node); node.parentNode.replaceChild(newNode, node); break; case utils.REMOVE: node.parentNode.removeChild(node); break; } }) } module.exports = patch 复制代码
进行到这里,我们已经完成了父节点的修补。
对于ul的子节点,我们可以使用同样的方法进行迭代一次。但是我们推荐用子节点的key来更快速得去判断是否删除、新增、顺序变换。
在oldTree中,有三个子元素 B、C、D 在newTree中,有四个子元素 E、B、C、D
- 在oldTree中去除newTree中没有的元素
function childDiff(oldChildren, newChildren) { let patches = [] let newKeys = newChildren.map(item=>item.key) let oldIndex = 0; while (oldIndex < oldChildren.length) { let oldKey = oldChildren[oldIndex].key;//A if (!newKeys.includes(oldKey)) { remove(oldIndex); oldChildren.splice(oldIndex, 1); } else { oldIndex++; } } } //标记去除的index function remove(index) { patches.push({type: utils.REMOVE, index}) } 复制代码
- 接下来将newTree数组合并到oldTree中,我的口诀是:新向旧合并,相等旧位移,记录新位标(O(∩_∩)O哈哈哈~)
function childDiff(oldChildren, newChildren) { ... oldIndex = 0; newIndex = 0; while (newIndex < newChildren.length) { let newKey = (newChildren[newIndex] || {}).key; let oldKey = (oldChildren[oldIndex] || {}).key; if (!oldKey) { insert(newIndex,newChildren[newIndex]); newIndex++; } else if (oldKey != newKey) { let nextOldKey = (oldChildren[oldIndex + 1] || {}).key; if (nextOldKey == newKey) { remove(newIndex); oldChildren.splice(oldIndex, 1); } else { insert(newIndex, newChildren[newIndex]); newIndex++; } } else { oldIndex++; newIndex++; } } function remove(index) { patches.push({type: utils.REMOVE, index}) } ... 复制代码
- 删除多余节点
while (oldIndex++ < oldChildren.length) { remove(newIndex) } 复制代码
- 根据补丁修改节点
function childPatch(root, patches = []) { let nodeMap = {}; (Array.from(root.childNodes)).forEach(node => { nodeMap[node.getAttribute('key')] = node }); patches.forEach(path=> { let oldNode switch (path.type) { case utils.INSERT: let newNode = nodeMap[path.node.key] || path.node.render() oldNode = root.childNodes[path.index] if (oldNode) { root.insertBefore(newNode, oldNode) } else { root.appendChild(newNode) } break; case utils.REMOVE: oldNode = root.childNodes[path.index] if (oldNode) { root.removeChild(oldNode) } break; default: throw new Error('没有这种补丁类型') } }) } 复制代码
记录补丁修改节点结果:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- HashMap 查漏补缺
- Java 查漏补缺之 jvm
- 前端面试查漏补缺--(八) 前端加密
- 前端面试查漏补缺--(一) 防抖和节流
- 前端面试查漏补缺--(九) HTTP与HTTPS
- 前端面试查漏补缺--(七) XSS攻击与CSRF攻击
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。