React原理一把撸
发布时间 : 2022-08-21 13:49
阅读 :
尝试从零来自己实现撸react代码,从而更好的理解React原理。注意本文基于React 16.X版本
粗浅的理解:先来看看什么是虚拟DOM。虚拟dom就是由真实的html标签生成的一个描述数据结构,里面包含当前层级的props,children,key等等的信息。当触发re-render时候,由react调度机制进行当前虚拟dom节点的diff
操作,也就是比较当前dom节点是否有改变,如果有的话再进行真实dom操作,这样大大节省了dom节点操作的开销。当然虚拟dom的形成没有光依靠react官方提供的基础库,还依赖babel插件对jsx语法进行的转译,并形成解析后的中间产物,后面代码会有体现。先看下如果简单实现一个虚拟dom的大概思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const isArr = Array .isArray const toArray = arr = isArr (arr ?? []) ? arr : [arr]const isText = txt => typeof txt === 'string' || typeof txt === 'number' const flatten = arr => [...arr.map (ar => isArr (ar) ? [...faltten (ar)] : isText (ar) ? createTextVNode (ar) : ar)] function h (type, props, ...kids ) { props = props ?? {} kids = flatten (toArray (props.children ?? kids)).filter (Boolean ) if (keys.length ) props.children = kids.length === 1 ? kids[0 ] : kids const key = props.key ?? null const ref = props.ref ?? null delete props.key delete props.ref return createVNode (type, props, key, ref) } function createTextVNode (text ) { return { type : '' , props : { nodeValue : text + '' } } } function createVNode (type, props, key, ref ) { return { type, props, key, ref } }
看完了上面的部分,可以再实现下HTML的部分,引用上面的函数进行VDOM创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > <script type ="module" > import { h } from './index.js' console .log ( h ('div' , { className : 'container' }, h ('div' , { className : 'empty' }, 'hello world!' ) ) ) </script > </head > <body > <div class ='container' > <div class ='empty' > hello world! </div > </div > </body > </html >
执行后可以看到VDom的信息结果。
调度scheduler 接下来思考一下如何完成调度部分。React实现调度的原理是基于[MessageChannel](https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel)
这个WebAPI来实现零延时宏任务调度的。为什么不用微任务
来做调度呢?因为微任务是优先级是高于宏任务
的,而且触发时机是在render之前。所以并不满足调度的要求。简单看下MessageChannel是如何建立的。
我们假设有一个页面,嵌套一个iframe子页面;在主页面里有输入框,当点击按钮(click
)后,由主页面向子页面传递消息,并在子页面iframe
里面接收消息并输出显示。就这样一个功能。代码实现大概如下:
主页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const input = document .getElementById ('message-input' );const output = document .getElementById ('message-output' );const button = document .querySelector ('button' );const iframe = document .querySelector ('iframe' );const channel = new MessageChannel ();const port1 = channel.port1 ;iframe.addEventListener ("load" , onLoad); function onLoad ( ) { button.addEventListener ('click' , onClick); port1.onmessage = onMessage; iframe.contentWindow .postMessage ('init' , '*' , [channel.port2 ]); } function onClick (e ) { e.preventDefault (); port1.postMessage (input.value ); } function onMessage (e ) { output.innerHTML = e.data ; input.value = '' ; }
其中,iframe.contentWindow.postMessage
参数说明如下:
The message being sent. For this initial port transferring this message could be an empty string but in this example it is set to ‘init’.
The origin the message is to be sent to. * means “any origin”.
An object, the ownership of which is transferred to the receiving browsing context. In this case, we are transferring MessageChannel.port2 to the IFrame, so it can be used to communicate with the main page.
子页面接收:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const list = document .querySelector ('ul' );let port2;window .addEventListener ('message' , initPort);function initPort (e ) { port2 = e.ports [0 ]; port2.onmessage = onMessage; } function onMessage (e ) { const listItem = document .createElement ('li' ); listItem.textContent = e.data ; list.appendChild (listItem); port2.postMessage (`Message received by IFrame: "${e.data} "` ); }
这样就完成了从主页面到子页面相互隔离的环境下的消息通信。
进一步思考,还有个webAPI叫requestIdleCallback
的方法,也可以用作任务调度,而且是利用空闲时间片处理任务。听上去是既能执行任务调度,又能压榨浏览器性能,分片时间就能吧任务执行完了。可是现实很残酷,没有被React团队采用也是有他的理由的。用这个API是有50ms性能优化问题
,按照我的理解,大概为:长任务是指执行耗时在 50ms 以上的任务,而Chrome 浏览器页面渲染和 V8 引擎用的是一个线程,如果 JS 脚本执行耗时太长,就会阻塞渲染线程,进而导致页面卡顿。
但是我们也来理解下这个API,从它的参数开始看:
1 2 3 4 5 6 7 typeRequestIdleCallback = (cb: (deadline: Deadline) => void , options?: Options ) => number typeDeadline = { timeRemaining : ()=> number didTimeout : boolean }
结合上面参数,看个简单的demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const unit = 10_000 ;const onOneUnit = ( )=>{ for (let i = 0 ; i <= 500_000 ; i++) {} } const FREE_TIME = 1 ;let _u = 0 ;function cb (deadline ) { while (_u < unit && deadline.timeRemaining () > FREE_TIME ) { onOneUnit (); _u++; } if (_u >= unit) return ; window .requestIdleCallback (cb) } window .requestIdleCallback (cb)
故事到这里,其实还有几个备选的疑问,我在这里统一罗列下吧:
调度为什么不选用setTimeout
? -> setTimeout有坑,执行会有4ms延迟。
为什么不选用web worker
来做? -> 浏览器底层算法,会引起结构化克隆
。数据量大会有很大的性能问题
消息调度很像generator
的机制,为什么没有采用?用团队原话:**”The generators are stateful. You cannot resume it in the middle of it.”**换句话说,generator不能中断,只能从头再来。
扯的远了,我们再来看下调度如何来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 const queue = []const threshold = 1000 / 60 const transtions = []let deadline = 0 const now = ( ) => performance.now ()const peek = arr => arr[0 ]export function startTranstion (cb ) { transtions.push (cb) && postMessage () } export function schedule (cb) { queue.push ({ cb }) startTranstion (flush) } const postMessage = (() => { const cb = ( ) => transtions.splice (0 , 1 ).forEach (c => c ()) const { port1, port2 } = new MessageChannel () port1.onmessage = cb return () => port2.postMessage (null ) })() export function shouldYield () { return navigator.scheduling .isInputPending () || now () >= deadline } function flush ( ) { deadline = now () + threshold let task = peek (queue) while (task && !shouldYield ()) { const { cb } = task task.cb = null const next = cb () if (next && typeof next === 'function' ) { task.cb = next } else { queue.shift () } task = peek (queue) } task && startTranstion (flush) }
Fragment 很简单的原理
1 2 3 export function Fragment (props ) { return props.children }
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 miaozixiong@gmail.com