react 遍历对象_React 源码系列 | React Children 详解

02f2022e450e1426781b04b3722d518f.png

本文基于 React V16.8.6,本文代码地址

  • 测试代码
  • 源码讲解

React 中一个元素可能有 0 个、1 个或者多个直接子元素,React 导出的 Children 中包含 5 个处理子元素的方法。

  • map 类似 array.map
  • forEach 类似 array.forEach
  • count 类似 array.length
  • toArray
  • only

React 内部处理 Children 的几个重要函数包括

  • mapChildren
  • traverseAllChildrenImpl
  • mapIntoWithKeyPrefixInternal
  • mapSingleChildIntoContext
  • getPooledTraverseContext
  • releaseTraverseContext

源码都在 packages/react/src/ReactChildren.js 中。

导出的语句

export {forEachChildren as forEach,mapChildren as map,countChildren as count,onlyChild as only,toArray,
};

Children API

map

类似 array.map,但有一下几个不同点:

  • 返回的结果一定是一个一维数组,多维数组会被自动摊平
  • 对返回的每个节点,如果 isValidElement(el) === true ,则会给它加上一个 key,如果元素本来就有 key,则会重新生成一个新的 key

map 的用法:第一个参数是要遍历的 children,第二个参数是遍历的函数,第三个是 context,执行遍历函数时的 this

如果 children == null,则直接返回了。

mapChildren

/*** Maps children that are typically specified as `props.children`.* 用来遍历 `props.children`** @param {?*} children Children tree container.* @param {function(*, int)} func The map function.* @param {*} context Context for mapFunction.* @return {object} Object containing the ordered map of results.*/
function mapChildren(children, func, context) {if (children == null) {return children;}// 遍历出来的元素会丢到 result 中最后返回出去const result = [];mapIntoWithKeyPrefixInternal(children, result, null, func, context);return result;
}

mapIntoWithKeyPrefixInternal

将 children 完全遍历,遍历的节点最终全部存到 array 中,是 ReactElement 的节点会更改 key 之后再放到 array 中。

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {// 这里是处理 key,不关心也没事let escapedPrefix = '';if (prefix != null) {escapedPrefix = escapeUserProvidedKey(prefix) + '/';}// getPooledTraverseContext 和 releaseTraverseContext 是配套的函数// 用处其实很简单,就是维护一个大小为 10 的对象重用池// 每次从这个池子里取一个对象去赋值,用完了就将对象上的属性置空然后丢回池子// 维护这个池子的用意就是提高性能,毕竟频繁创建销毁一个有很多属性的对象消耗性能const traverseContext = getPooledTraverseContext(array, // result escapedPrefix, // ''func, // mapFunccontext, // context);// 最核心的一句traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);releaseTraverseContext(traverseContext);
}

getPooledTraverseContext

getPooledTraverseContextreleaseTraverseContext这两个函数是用来维护一个对象池,池子最大为10。Children 需要频繁的创建对象会导致性能问题,所以维护一个固定数量的对象池,每次从对象池拿一个对象进行复制,使用完将各个属性 reset。

bfa92747e0a535584debf4e39915536a.png
const POOL_SIZE = 10;
const traverseContextPool = [];
// 返回一个传入参数构成的对象
// traverseContextPool 长度为 0 则自己构造一个对象出来,否则从 traverseContextPool pop 一个对象
// 再对这个对象的各个属性进行赋值
function getPooledTraverseContext(mapResult,keyPrefix,mapFunction,mapContext,
) {if (traverseContextPool.length) {const traverseContext = traverseContextPool.pop();traverseContext.result = mapResult;traverseContext.keyPrefix = keyPrefix;traverseContext.func = mapFunction;traverseContext.context = mapContext;traverseContext.count = 0;return traverseContext;} else {return { result: mapResult,keyPrefix: keyPrefix,func: mapFunction,context: mapContext,count: 0,};}
}

releaseTraverseContext

c19e764c822d29b0f4e1841a3d0eeaac.png

getPooledTraverseContext 产生的对象加入数组中,对象池 >= 10 则不用管

function releaseTraverseContext(traverseContext) {traverseContext.result = null;traverseContext.keyPrefix = null;traverseContext.func = null;traverseContext.context = null;traverseContext.count = 0;if (traverseContextPool.length < POOL_SIZE) {traverseContextPool.push(traverseContext);}
}

traverseAllChildren

没太多好说的

function traverseAllChildren(children, callback, traverseContext) {if (children == null) {return 0;}return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

traverseAllChildrenImpl

它的作用可以理解为

  • children 是可渲染节点,则调用 mapSingleChildIntoContext 把 children 推入 result 数组中
  • children 是数组,则再次对数组中的每个元素调用 traverseAllChildrenImpl,传入的 key 是最新拼接好的
  • children 是对象,则通过 children[Symbol.iterator] 获取到对象的迭代器 iterator, 将迭代的结果放到 traverseAllChildrenImpl 处理

函数核心作用就是通过把传入的 children 数组通过遍历摊平成单个节点,然后去执行 mapSingleChildIntoContext

这个函数比较复杂,函数签名是这样的

  • children 要处理的 children
  • nameSoFar 父级 key,会一层一层拼接传递,用 : 分隔
  • callback 如果当前层级是可渲染节点,undefinedboolean 会变成 nullstringnumber$$typeofREACT_ELEMENT_TYPE 或者 REACT_PORTAL_TYPE,会调用 mapSingleChildIntoContext 处理
  • traverseContext 对象池中拿出来的一个对象
/*** @param {?*} children Children tree container. `Children.map` 的第一个参数,要处理的 children* @param {!string} nameSoFar Name of the key path so far.* @param {!function} callback Callback to invoke with each child found. map 时 callback 是* `mapSingleChildIntoContext`* @param {?*} traverseContext Used to pass information throughout the traversal* process. 对象池的一个对象* @return {!number} The number of children in this subtree.*/
function traverseAllChildrenImpl(children,nameSoFar,callback,traverseContext,
) {// 这个函数核心作用就是通过把传入的 children 数组通过遍历摊平成单个节点// 然后去执行 mapSingleChildIntoContext// 开始判断 children 的类型const type = typeof children;if (type === 'undefined' || type === 'boolean') {// All of the above are perceived as null.children = null;}// 决定是否调用 callback// 是可渲染的节点则为 truelet invokeCallback = false;// 判断是否调用,children === null、type 为可渲染的节点则 invokeCallback 为 trueif (children === null) {invokeCallback = true;} else {switch (type) {case 'string':case 'number':invokeCallback = true;break;case 'object':switch (children.$$typeof) {case REACT_ELEMENT_TYPE:case REACT_PORTAL_TYPE:invokeCallback = true;}}}// 如果 children 是可以渲染的节点的话,就直接调用 callback// callback 是 mapSingleChildIntoContext// 我们先去阅读下 mapSingleChildIntoContext 函数的源码if (invokeCallback) {callback(traverseContext,children,// If it's the only child, treat the name as if it was wrapped in an array// so that it's consistent if the number of children grows.// const SEPARATOR = '.';nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,);return 1;}// nextName 和 nextNamePrefix 都是在处理 key 的命名let child;let nextName;let subtreeCount = 0; // Count of children found in the current subtree.// const SUBSEPARATOR = ':';const nextNamePrefix =nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;// 节点是数组的话,就开始遍历数组,并且把数组中的每个元素再递归执行 traverseAllChildrenImpl// 这一步操作也用来摊平数组的// React.Children.map(this.props.children, c => [[c, c]])// c => [[c, c]] 会被摊平为 [c, c, c, c]// 这里如果看不明白的话过会在 mapSingleChildIntoContext 中肯定能看明白if (Array.isArray(children)) {for (let i = 0; i < children.length; i++) {child = children[i];nextName = nextNamePrefix + getComponentKey(child, i); // .$dasdsa:subtreeCount += traverseAllChildrenImpl(child,nextName, // 不同点是 nameSoFar 变了,它会在每一层不断拼接,用 : 分隔callback,traverseContext,);}}  else {// 不是数组的话,就看看 children 是否可以支持迭代// 通过 obj[Symbol.iterator] 的方式去取const iteratorFn = getIteratorFn(children);// ... 中间有部分 __DEV__ 下检测使用正确性的代码// 只有取出来对象是个函数类型才是正确的// 然后就是执行迭代器,重复上面 if 中的逻辑const iterator = iteratorFn.call(children);let step;let ii = 0;while (!(step = iterator.next()).done) {child = step.value;nextName = nextNamePrefix + getComponentKey(child, ii++);subtreeCount += traverseAllChildrenImpl(child,nextName,callback,traverseContext,);}}return subtreeCount;
}

mapSingleChildIntoContext

child 推入 traverseContext 的 result 数组中,child 如果是 ReactElement,则更改 key 了再推入。

只有当传入的 child 是可渲染节点才会调用。如果执行了 mapFunc 返回的是一个数组,则会将数组放到 mapIntoWithKeyPrefixInternal 继续处理。

/*** @param bookKeeping 就是我们从对象池子里取出来的东西,`traverseContext`* @param child 传入的节点,`children`* @param childKey 节点的 key,`nameSoFar`*/
function mapSingleChildIntoContext(bookKeeping, child, childKey) {const {result, keyPrefix, func, context} = bookKeeping; // traverseContext// func 就是我们在 React.Children.map(this.props.children, c => c)// 中传入的第二个函数参数let mappedChild = func.call(context, child, bookKeeping.count++);// 判断函数返回值是否为数组// 因为可能会出现这种情况// React.Children.map(this.props.children, c => [c, c])// 对于 c => [c, c] 这种情况来说,每个子元素都会被返回出去两次// 也就是说假如有 2 个子元素 c1 c2,那么通过调用 React.Children.map(this.props.children, c => [c, c]) 后// 返回的应该是 4 个子元素,c1 c1 c2 c2if (Array.isArray(mappedChild)) {// 是数组的话就回到最先调用的函数中// 然后回到之前 traverseAllChildrenImpl 摊平数组的问题// 假如 c => [[c, c]],当执行这个函数时,返回值应该是 [c, c]// 然后 [c, c] 会被当成 children 传入// traverseAllChildrenImpl 内部逻辑判断是数组又会重新递归执行// 所以说即使你的函数是 c => [[[[c, c]]]]// 最后也会被递归摊平到 [c, c, c, c]mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);} else if (mappedChild != null) {// 不是数组且返回值不为空,判断返回值是否为有效的 Element// 是的话就把这个元素 clone 一遍并且替换掉 keyif (isValidElement(mappedChild)) {mappedChild = cloneAndReplaceKey(mappedChild,// Keep both the (mapped) and old keys if they differ, just as // traverseAllChildren used to do for objects as childrenkeyPrefix +(mappedChild.key && (!child || child.key !== mappedChild.key)? escapeUserProvidedKey(mappedChild.key) + '/': '') +childKey,);}result.push(mappedChild);}
}

map 测试

map 代码就是上面这些,写一个 demo 看看执行过程。

class Child extends Component {render() {console.log('this.props.children', this.props.children)const c = React.Children.map(this.props.children, c => {debuggerreturn c})console.log('mappedChildren', c)return <div>{c}</div>}
}export default class Children extends Component {render() {// return 的代码包含 2 种情况:children 是和不是数组return (<Child><div>childrendasddadas<div>childrendasddadas</div><div>childrendasddadas</div></div><div key="key2">childrendasddadas</div><div key="key3">childrendasddadas</div>{[<div key="key4">childrendasddadas</div>,<div key="key5=">childrendasddadas</div>,<div key="key6:">childrendasddadas</div>,]}</Child>)}
}

打印的结果如下

a14b0ef4f8a4503a5f7de3cce4dbabed.png

React.Children.map 就是把传进去的 this.props.children 全部摊平,最后返回的一定是一维数组,数组中的对象都会添加上 key 属性。对 mappedChildren key 的生成做分析如下。

this.props.children 自身是一个数组,在第一次调用 traverseAllChildrenImpl 时,nextName.0,第一个 child 执行 traverseAllChildrenImpl 时,invokeCallback 为 true,nameSoFar.0,再执行 mapSingleChildIntoContext 走到 cloneAndReplaceKey ,新 key 生成为 .0(因为 (mappedChild.key && (!child || child.key !== mappedChild.key) 为 false,keyPrefix 为空字符串)。

第二个和第三个 child 的 key 加上了 .$,在 traverseAllChildrenImpl 中,遍历到第二个和第三个下标时 nextName = nextNamePrefix + getComponentKey(child, i);nextNamePrefix.i 是 2、3,getComponentKey 执行,由于它有自己的 key,所以 escape 后变成 . + $key2 => .$key2.$key3 同理。

function escape(key) {const escapeRegex = /[=:]/g;const escaperLookup = {'=': '=0', // 替换 =':': '=2', // 替换 :};const escapedString = ('' + key).replace(escapeRegex, function(match) {return escaperLookup[match];});return '$' + escapedString; // 返回的字符串前面加上 $
}

第四、五、六个是嵌套在数组里面的,同上面,this.props.children 遍历到这个数组的时候索引为 3。传给下一轮 traverseAllChildrenImplnameSoFar.3child 为数组,下一 轮traverseAllChildrenImpl ,children 是一个数组,对其进行遍历,nextNamePrefix.3:,由下面这句计算出来。

const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

getComponentKey(child, i),由于数组中的每个元素有自己的 key,所以返回的是 $key4$key5=0$key6=2,拼接出来就是 .3:$key4.3:$key5=0.3:$key6=2,这里第五、六个的 =: 被 escape 处理成了 =0=2

上面例子代码 debugger 时的调用栈:

58fab3b90116ac026d5a9a8864330cfa.png

下面贴一张 map 的流程图。

6e333a3219d4d5ecce300379c1de07d0.png

forEach

类似 array.forEach

map 的不同之处是传给 getPooledTraverseContext 的参数 result 为 null,因为 forEach 只需要遍历,不需要返回一个数组。另外 traverseAllChildren 它的第二个参数变成了 forEachSingleChild

它没有 map 那么复杂。

forEachChildren

调用 traverseAllChildren 让每个 child 都被放到 forEachSingleChild 中执行

/*** Iterates through children that are typically specified as `props.children`.* The provided forEachFunc(child, index) will be called for each* leaf child.** @param {?*} children Children tree container. `this.props.children`* @param {function(*, int)} forEachFunc 遍历函数* @param {*} forEachContext Context for forEachContext. 遍历函数的上下文*/
function forEachChildren(children, forEachFunc, forEachContext) {if (children == null) {return children;}const traverseContext = getPooledTraverseContext(null,null,forEachFunc,forEachContext,);traverseAllChildren(children, forEachSingleChild, traverseContext);releaseTraverseContext(traverseContext);
}

forEachSingleChild

children 中的每个元素放到 func 中执行

/*** 把 `children` 中的每个元素放到 `func` 中执行** @param bookKeeping traverseContext* @param child 单个可 render child* @param name 这里没有用到*/
function forEachSingleChild(bookKeeping, child, name) {const {func, context} = bookKeeping;func.call(context, child, bookKeeping.count++);
}

count

计算 children 的个数,计算的是摊平后数组元素的个数

countChildren

traverseAllChildren 有一个返回值 subtreeCount,表示子节点的个数,traverseAllChildren 遍历所有 child 之后,subtreeCount 会统计出结果。

/*** 计算 children 的个数,计算的是摊平后数组元素的个数* Count the number of children that are typically specified as* `props.children`.** @param {?*} children Children tree container.* @return {number} The number of children.*/
function countChildren(children) {return traverseAllChildren(children, () => null, null);
}

toArray

mapChildren(children, child => child, context)

/*** 是 `mapChildren(children, child => child, context)` 版本* Flatten a children object (typically specified as `props.children`) and* return an array with appropriately re-keyed children.*/
function toArray(children) {const result = [];mapIntoWithKeyPrefixInternal(children, result, null, child => child);return result;
}

only

如果参数是一个 ReactElement,则直接返回它,否则报错,用在测试中,正式代码没什么用。

/*** Returns the first child in a collection of children and verifies that there* is only one child in the collection.* The current implementation of this function assumes that a single child gets* passed without a wrapper, but the purpose of this helper function is to* abstract away the particular structure of children.** @param {?object} children Child collection structure.* @return {ReactElement} The first and only `ReactElement` contained in the* structure.*/
function onlyChild(children) {invariant(isValidElement(children),'React.Children.only expected to receive a single React element child.',);return children;
}function isValidElement(object) {return (typeof object === 'object' &&object !== null &&object.$$typeof === REACT_ELEMENT_TYPE);
}

导出的函数中,map 是最复杂的,把每个函数的意义和签名都读懂之后我对整体有了比较深的认识。看一看流程图,整个过程就清楚了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/476803.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

程序员面试金典 - 面试题 16.19. 水域大小(BFS/DFS)

1. 题目 你有一个用于表示一片土地的整数矩阵 land&#xff0c;该矩阵中每个点的值代表对应地点的海拔高度。 若值为0则表示水域。由垂直、水平或对角连接的水域为池塘。 池塘的大小是指相连接的水域的个数。 编写一个方法来计算矩阵中所有池塘的大小&#xff0c;返回值需要从…

自定义组件--创建mxml组件

创建简单的mxml组件 Example components/CountryComboBox.mxml <?xml version"1.0" encoding"utf-8"?> <mx:ComboBox xmlns:mx"http://www.adobe.com/2006/mxml"> <mx:dataProvider> <mx:String>United States</mx…

19位算法工程师总结:机器学习项目成功落地的三条秘诀

文 | 天于刀刀又是一年金九银十&#xff0c;前几天小编刀刀在一次电话面试过程中被HR 的一个问题问得差点闪了腰。当时情况是这样的&#xff0c;在流利地介绍完之前的几个项目后&#xff0c;招聘小姐姐打断了我后续的&#xff08;忽悠&#xff09;节奏&#xff0c;郑重其事地反…

nodejs 获取cpu核心数量_用 NodeJS 充分利用多核 CPU 的资源[每日前端夜话0xCB]

每日前端夜话0xCA每日前端夜话&#xff0c;陪你聊前端。每天晚上18:00准时推送。正文共&#xff1a;1558 字预计阅读时间&#xff1a;7 分钟作者&#xff1a;Nick Major翻译&#xff1a;疯狂的技术宅来源&#xff1a;coderrocketfuel介绍单个 Node.js 程序的实例仅在一个线程上…

程序员面试金典 - 面试题 16.20. T9键盘(数组)

1. 题目 在老式手机上&#xff0c;用户通过数字键盘输入&#xff0c;手机将提供与这些数字相匹配的单词列表。 每个数字映射到0至4个字母。给定一个数字序列&#xff0c;实现一个算法来返回匹配单词的列表。 你会得到一张含有有效单词的列表。映射如下图所示&#xff1a; 示…

胃部不适,原来好辛苦!

这几天胃部都感觉不舒服&#xff0c;那种感觉很难形容&#xff0c;总之就非常辛苦&#xff0c;无胃口&#xff0c;浑身都不舒服&#xff0c;可能是之前几天早餐没吃饱&#xff0c;有几天很晚才入睡空着肚子的缘故吧&#xff0c;原来胃部不适是如此辛苦的&#xff0c;怕怕&#…

重磅!顶级一区期刊官宣:明年起将不再拒稿!

源 | 青塔学术、量子位等颠覆科学出版的“游戏规则”?10月20日&#xff0c;国际著名生物学综合期刊eLife官方宣布了一个重大决定&#xff1a;从2023年1月31日起&#xff0c;所有经过同行评审的文章&#xff0c;eLife都不会作出接受/拒绝的决定&#xff0c;而是直接发布在其网站…

EM(期望极大化)算法及其推广

文章目录1. EM算法2. EM算法收敛3. EM算法应用4. EM算法的推广5. sklearn.mixture.GaussianMixture概率模型有时既有观测变量&#xff08;observable variable&#xff09;&#xff0c;又有隐变量或潜在变量&#xff08;latent variable&#xff09;如果概率模型的变量都是观测…

GridView 中添加删除确认提示框

在GridView中我们可以直接添加一个CommandField删除列来删除某行信息。但为了避免误操作引起的误删除&#xff0c;在删除操作者让操作者再确认下&#xff0c;完后再进行删除。 首先我们给我们的GridView 添加一个模板列&#xff0c;如下&#xff1a; <asp:TemplateField He…

终于有人喊出来:论文一稿多投是作者的合法权利!

文 | 马建平&#xff0c;三峡大学学报编辑源 | 现代出版一稿多投目前已演变为许多作者一种常态化的投稿方式。针对一稿多投现象&#xff0c;舆论似乎是一边倒的反对和谴责之声&#xff0c;认为它浪费了极为稀缺的出版资源&#xff0c;扰乱了报刊社正常的出版秩序&#xff0c;是…

python中pop用法_Python dict pop()用法及代码示例

Python语言为几乎所有容器(无论是列表容器还是集合容器)指定了pop()。这篇特别的文章着重说明Python词典提供的pop()方法。这种方法对于经常处理字典的程序员很有用。 用法&#xff1a;dict.pop(key, def) 参数&#xff1a; key:必须返回并删除其键值对的键。 def:如果指定的键…

桩训日记

2007年2月7日 弄了一天的模拟练习&#xff0c;挺累的&#xff0c;明天估计是练习 起步停车2007年2月8日 上午起步停车&#xff08;年龄挺大的李教练&#xff09;&#xff0c;临近中午开始倒库&#xff08;挺帅的尹教练&#xff09;&#xff0c;下午继续倒库&#xff0c;基本茫然…

LeetCode 912. 排序数组(10种排序)

文章目录1. 题目2. 解题2.1 插入排序2.2 冒泡排序2.3 选择排序2.4 希尔排序2.5 归并排序2.6 快速排序2.7 堆排序2.8 计数排序2.9 桶排序2.10 基数排序3. 复杂度表1. 题目 给你一个整数数组 nums&#xff0c;将该数组升序排列。 示例 1&#xff1a; 输入&#xff1a;nums [5,…

顶会审稿人谈论文中稿“潜规则”

科研论文&#xff0c;不同于毕业论文之处在于——科研论文是根据有价值的生产实践或科研课题写作的&#xff0c;具有原创性和独到性的论文。在学术界&#xff0c;有人写论文是为了升硕士&#xff0c;升博士或者研究生博士顺利毕业。毕竟在学术界论文是工作和科研水平的直观体现…

python asyncio_如何使用Python中的asyncio?

【51CTO.com快译】Python的异步编程功能(简称async)让你可以编写不必等待独立任务完成就可以完成更多工作的程序。Python附带的asyncio库为你提供了使用async处理磁盘或网络I/O、无需其他方面等待的工具。 asyncio提供了两种处理异步操作的API&#xff1a;高级和低级。高级API用…

程序员面试金典 - 面试题 08.05. 递归乘法(位运算)

1. 题目 递归乘法。 写一个递归函数&#xff0c;不使用 * 运算符&#xff0c; 实现两个正整数的相乘。 可以使用加号、减号、位移&#xff0c;但要吝啬一些。 示例1:输入&#xff1a;A 1, B 10输出&#xff1a;10示例2:输入&#xff1a;A 3, B 4输出&#xff1a;12提示: …

最新整理完成

终于把主站做完了&#xff0c;用了一个小型的cms系统&#xff0c;因为买不起asp.net的空间&#xff0c;而且因为买的时候没问清楚&#xff0c;后来才知道限制cpu4%以内&#xff0c;才知道频繁的server unavaliable是多么的痛苦。我现在把修正后的新闻系统重新提供下载&#xff…

PromptCLUE:大规模多任务Prompt预训练中文开源模型

简介PromptCLUE&#xff1a;大规模多任务Prompt预训练中文开源模型。中文上的三大统一&#xff1a;统一模型框架&#xff0c;统一任务形式&#xff0c;统一应用方式。支持几十个不同类型的任务&#xff0c;具有较好的零样本学习能力和少样本学习能力。针对理解类任务&#xff0…

JS基础知识总结

前几天在网上看到的一个总结&#xff0c;觉得挺好的&#xff0c;所以记录了下来&#xff0c;以备查用。1 创建脚本块 1: <script language”JavaScript”>2: JavaScript code goes here3: </script> 2 隐藏脚本代码 1: <script language”JavaScript”>2: &…