【JS】mini-react新版本stack架构

mini-react新版本stack架构

Charon发布于 今天 09:04

项目地址:
https://github.com/544076724/...

之前写过一般react-mini版本的实现就是会有点问题,这里进行了一次改版,这个结构是stack版的后续会出一版filber架构的.
准备工作是和之前一样的,参考我之前写的
https://segmentfault.com/a/11...

打包工具

npm install -g parcel-bundler

.babelrc

{

"presets": ["env"],

"plugins": [

["transform-react-jsx", {

"pragma": "React.createElement"

}]

]

}

最后parcel index.html 启动项目

jsx代码我们还是通过babel设定使用React.createElement方法来转换生成虚拟dom.

关于虚拟dom,我原来那一篇讲过虚拟dom,这里用一句话来总结就是:

在 React 中,每个 DOM 对象都有一个对应的 Virtual DOM 对象,它是 DOM 对象的 JavaScript 对象表现形式,其实就是使用 JavaScript 对象来描述 DOM 对象信息,比如 DOM 对象的类型是什么,它身上有哪些属性,它拥有哪些子元素。

可以把 Virtual DOM 对象理解为 DOM 对象的副本,但是它不能直接显示在屏幕上。
虚拟dom是如何提升效率的总结一下就是:
在 React 第一次创建 DOM 对象后,会为每个 DOM 对象创建其对应的 Virtual DOM 对象,在 DOM 对象发生更新之前,React 会先更新所有的 Virtual DOM 对象,然后 React 会将更新后的 Virtual DOM 和 更新前的 Virtual DOM 进行比较,从而找出发生变化的部分,React 会将发生变化的部分更新到真实的 DOM 对象中,React 仅更新必要更新的部分。

Virtual DOM 对象的更新和比较仅发生在内存中,不会在视图中渲染任何内容,所以这一部分的性能损耗成本是微不足道的。

但是首次渲染的时候因为要生成虚拟dom来做映射,所以首次渲染的时候是没有原来那种方式快的,因为要做一些其他的额外操作.

这里标注一下我们用到的方法以及作用

  • render.js :入口文件调用diff方法挂载或比对新旧vnode
  • diff.js :该方法用来做新旧vnode的比对,以及首次挂载的处理,setState时会调用,或者render(<div>sss</div) 更改render(<span>lll</span>)会用到.
  • Component.js : 类组件的父类,所有类组件继承自它,存储props,内部有setState方法,调用 diff来对比新旧vnode更新,注册一系列生命周期函数.
  • mountElement.js : 该方法用来区分是组件还是 普通元素,普通元素就生成dom挂载到界面,组件时获取组件的虚拟dom vnode 然后再挂载
  • createDOMElement.js :根据虚拟dom生成真实dom,并处理props属性
  • diffComponent.js :该方法用来 对比更新组件
  • updateTextNode.js : 更新textNode 节点
  • updateNodeElement.js :该方法设置或更新真实dom上的属性
  • unmountNode.js :该方法用来删除真实dom上的节点(删除节点时假如该节点是由组件生产的,需要把对应的ref和绑定的事件函数清空掉,防止内存泄露)
  • createElement.js :该函数用来生成虚拟dom,其他类react框架中也会叫h函数
  • index.js :做react入口文件,负责统一导出
  • isFunction.js :判断当前tag是不是一个组件
  • isFunctionComponent.js :该方法用来判断是函数组件还是类组件,组件原型有render方法就认为是类组件
  • mountComponent.js :该方法 用来 获取组件的虚拟dom vnode,并且把组件实例挂载到vnode上,方便后续调用生命周期
  • mountNativeElement.js :该方法根据虚拟vnode 来获取真实dom然后在父节点中进行 更新或添加
  • updateComponent.js :同一个组件更新操作
  • enqueueSetState.js :处理setState批量更新的操作

好了我们下面正式进入正题,先说一下我们的开发思路:

  1. 创建createElement方法来转换jsx为vnode
  2. 创建Component组件类,所有的组件都是继承自该类
  3. 将vnode转换为真实dom来插入父级节点(这里直接使用diff方法对比了,该方法承载了新旧vnode的对比,并且判断了是否是首次挂载vnode),diff首次挂载是会调用mountElement方法,会区分是否是组件

    1. 不是组件得时候,那就是html节点或者文本节点,那就会使用mountNativeElement方法直接把当前这个vnode递归(递归过程中会持续使用mountElement方法来判断当前这级是组件还是直接的html标签)来转换为html片段,最后插入到指定的容器中(例如id="root"的节点)
    2. 是组件的时候调用mountComponent方法,该方法会判断是类组件还是函数组件,然后来运行他们获取他们返回的vnode,类组件时会把组件实例储存在返回的vnode上,方便后续调用组件生命周期,然后它们解析出来的vnode,会查看是不是还是组件,例如例如function App(){ return <Demo / >} 这种,如果是的话继续调用mountComponent解析,否则调用mountNativeElement方法把vnode转换成真实dom,然后执行组件生命周期componentDidMount,给props.ref传递组件实例当引用。

  4. 到这里首次加载就完成了,然后就是diff方法中不是首次加载而是对比新旧vnode更新操作了,这里主要分几步如下:

    1. 新vnode不是组件(组件要单独处理)并且新旧vnode标签不同,这会不需要对比直接用新的vnode生成真实dom然后把老的dom替换就可以了
    2. 新vnode是组件调用diffComponent方法来对比组件更新,判断两个是否是同一个组件,如果旧的vnode不是组件或者和新vnode不是同一个组件直接用mountElement方法渲染新的dom,把旧的dom替换掉就可以,是同一个组件旧调用updateComponent来更新组件
    3. 新旧vnode是同一个节点,标签一样,文本节点的话直接更新内容,元素节点的话更新元素上的属性,也就是更新props中的属性
    4. 到这里同级的比对完成之后就是,就该进行子集的比对了

      1. 子集的比对要使用到key属性,首先获取当前真实dom html中的子节点的所有元素节点的key,存到一个对象keyedElements中,值就是当前的真实dom的引用,然后开始比对
      2. 如果从真实dom中获取的子节点就没key的话,直接逐个节点 一个一个一次调用diff更新对比就可以了.这会是同级比对,不会去查找key
      3. 然后从真实dom html中获取子节点有key时,循环新vnode的子节点,获取它的key,然后从keyedElements对象中查找看能不能找到key一样的获取它的值放到domElement变量中(真实dom引用)

        1. 假如找到了就该查看位置对不对了,因为我们是循环这会用这个i获取真实dom子集中i的位置的元素看和domElement 是不是同一个,不是的话证明位置不对,那就把domElement插入到当前这个真实dom子集中i的位置之前就可以了.
        2. 然后就是如果从keyedElements没有找到key相同的,证明真实dom就缺少这个节点,这会直接使用mountElement新建就可以了。

    5. 子集对比完了之后,就该删除多余节点了

      1. 判断旧节点的数量比新的长,没有key,直接删除,没有key时证明,到从开头到virtualDOM.children的结尾都已经被更新过了,所以我们从后往前删除,删除到virtualDOM.children 的结尾处就可以了
      2. 有key时,通过key属性删除节点,把老的节点里的key从新的vnode.children里查找,如果找不到就删除

到此为止我们的所有新建和更新就完成了。

最后的流程就是贴代码了

Component.js

import {enqueueSetState} from "./enqueueSetState";

export default class Component { //类组件的父类,所有类组件继承自它

constructor(props) {

this.props = props //储存props

}

setState(state) { //获取state

enqueueSetState(state,this)

}

setDOM(dom) { //设置 当前组件对应的真实dom

this._dom = dom

}

getDOM() {//获取

return this._dom

}

updateProps(props) { //更新props

this.props = props

}

// 生命周期函数

componentWillMount() {}

componentDidMount() {}

componentWillReceiveProps(nextProps) {}

shouldComponentUpdate(nextProps, nextState) {

return nextProps != this.props || nextState != this.state

}

componentWillUpdate(nextProps, nextState) {}

componentDidUpdate(prevProps, preState) {}

componentWillUnmount() {}

}

createDOMElement.js

import mountElement from "./mountElement"

import updateNodeElement from "./updateNodeElement"

/**

*

* @param {*} virtualDOM 根据虚拟dom生成真实dom

* 这个函数,当前项 virtualDOM.tag 不会是一个方法,也就是说,函数组件不会直接走到这里

* 这里处理ref 不是组件上的ref而是 <div ref={方法()}></div> 这种,在这里ref的回调传入的不会是组件的引用

*/

export default function createDOMElement (virtualDOM) {

let newElement = null

if (virtualDOM.tag === "text") {

//文本节点

newElement = document.createTextNode(virtualDOM.props.textContent) //取出我们赋值到props里的内容

} else {

newElement = document.createElement(virtualDOM.tag)

updateNodeElement(newElement, virtualDOM) //设置props属性 到真实dom

}

newElement._virtualDOM = virtualDOM //把当前的vnode对象,挂载到真实dom上,用来做新旧vnode对比使用,下次更新它就是旧的vnode了

// 这回我们才创建了第一层节点,要递归创建子节点

virtualDOM.children.forEach(child => {

mountElement(child, newElement) //该方法会区分 当前节点是 组件还是元素

})

//处理ref属性,如果要是 有ref的话,调用ref传入的函数,把当前创建的newElement dom传递给它

if (virtualDOM.props && virtualDOM.props.ref) {

virtualDOM.props.ref(newElement)

}

return newElement //最后返回新建的 dom元素

}

createElement.js

/**

* 该函数用来生成虚拟dom,其他类react框架中也会叫h函数,我们配置了babel 会把jsx转换成React.createElement("div", null, "123")这种形式

* @param {*} tag 标签类型标记是 元素标签还是文本元素text 还是组件 为组件是 tag 是一个函数

* @param {*} props 标签 props 属性

* @param {...any} children 当前元素的下级元素 前两个之后的都是 子元素

*/

export default function createElement (tag, props, ...children) {

//在这里把 所有的 布尔值和null去掉,这是不需要在界面展示的

// children 里会有如下 children 这种格式 也就是子元素有是文本元素的, 这样会类型不统一

//一会是字符串一会是对象所以在这里给它统一一下,子节点都转换成对象

// { tag: "div", props: null, children: Array(4) }

// children: Array(4)

// 0: "hello"

// 1: { tag: "span", props: null, children: Array(1) }

// 2: "react"

// 3: { tag: "span", props: null, children: Array(1) }

// length: 4

// __proto__: Array(0)

// props: null

// tag: "div"

const childElement = [].concat(...children).reduce((result, child) => {

// 通过reduce处理

if (child !== false && child !== true && child !== null) {

if (child instanceof Object) {

result.push(child);

} else {

result.push(createElement("text", { textContent: child }));

}

}

return result;

},[])

//通过这样我们就做了一个类型统一

return {

tag,

props:Object.assign({ children: childElement }, props), //react中props属性也有childern属性这里我们也加上

children:childElement

}

}

diff.js

import mountElement from "./mountElement"

import createDOMElement from "./createDOMElement"

import diffComponent from "./diffComponent"

import updateTextNode from "./updateTextNode"

import updateNodeElement from "./updateNodeElement"

import unmountNode from "./unmountNode"

/**

* 该方法用来做新旧vnode的比对,以及首次挂载的处理

* setState时会调用,或者render(<div>sss</div) 更改render(<span>lll</span>)

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 虚拟dom对应的真实dom,更新时才会有,初始创建时 是没有的, 更新时它也是老的真实dom

*/

export default function diff (virtualDOM, container, oldDOM) {

const oldVirtualDOM = oldDOM && oldDOM._virtualDOM //首次创建时是不存在的

const oldComponent = oldVirtualDOM && oldVirtualDOM.component //查看该节点是否是 组件

if (!oldDOM) { //如果它不存在是首次挂载 直接执行mountElement方法

mountElement(virtualDOM, container)

}

else if (//老的存在,要对比更新

// 如果要比对的两个节点类型不相同

virtualDOM.tag !== oldVirtualDOM.tag &&

// 并且节点的类型不是组件 因为组件要单独处理

typeof virtualDOM.tag !== "function"

) {

// 新旧的标签不同,不需要对比

// 直接使用新的 virtualDOM 对象生成真实 DOM 对象

const newElement = createDOMElement(virtualDOM)

// 使用新的 DOM 对象替换旧的 DOM 对象

oldDOM.parentNode.replaceChild(newElement, oldDOM)

} else if (typeof virtualDOM.tag === "function") {

// 是组件

diffComponent(virtualDOM, oldComponent, oldDOM, container)

} else if (oldVirtualDOM && virtualDOM.tag === oldVirtualDOM.tag) {

//如果旧的vnode存在 并且 两个标签一样 进行节点更新

if (virtualDOM.tag === "text") {//文本节点

// 更新内容

updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)

} else {

// 更新元素节点属性

updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)

}

//最后是key的对比

// 1. 将拥有key属性的子元素放置在一个单独的对象中

let keyedElements = {}

for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {

let domElement = oldDOM.childNodes[i]

if (domElement.nodeType === 1) {

let key = domElement.getAttribute("key")

if (key) {

keyedElements[key] = domElement

}

}

}

let hasNoKey = Object.keys(keyedElements).length === 0

if (hasNoKey) {//无key

// 没有key,直接逐级比对,一个一个更新

virtualDOM.children.forEach((child, i) => {

diff(child, oldDOM, oldDOM.childNodes[i]) //更新比对

})

} else {//html有key

// 2. 循环 virtualDOM 的子元素 获取子元素的 key 属性

virtualDOM.children.forEach((child, i) => {

let key = child.props.key

if (key) {

let domElement = keyedElements[key] //获取对应的真实dom

if (domElement) {

// 3. 看看当前位置的元素是不是我们期望的元素

//如果当前坐标的元素在 上一次操作就存在,然后查看两个是不是相等

//不相等的话,证明当前位置的元素不是 我们要的这个domElement元素,那就直接把它插入到这个位置

if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {

oldDOM.insertBefore(domElement, oldDOM.childNodes[i])

}

} else {

// domElement不存在证明 原来就少这个元素 直接进行新增元素操作

mountElement(child, oldDOM, oldDOM.childNodes[i])

}

}

})

}

//子集对比完了,查看旧的节点是不是比新的长,长的话证明新的没有,需要删除操作

// 获取旧节点

let oldChildNodes = oldDOM.childNodes

// 判断旧节点的数量比新的长

if (oldChildNodes.length > virtualDOM.children.length) {

if (hasNoKey) { //没有key,直接删除

// 有节点需要被删除

for ( //没有key时证明,到从开头到virtualDOM.children的结尾都已经被更新过了,所以我们从后往前删除

//删除到virtualDOM.children 的结尾处就可以了,代码如下

let i = oldChildNodes.length - 1;

i > virtualDOM.children.length - 1;

i--

) {

unmountNode(oldChildNodes[i]) //删除新的vnode上面不存在的节点

}

} else {//有key

// 通过key属性删除节点,把老的节点里的key从新的vnode.children里查找,如果找不到就删除

for (let i = 0; i < oldChildNodes.length; i++) {

let oldChild = oldChildNodes[i]

let oldChildKey = oldChild._virtualDOM.props.key

let found = false

for (let n = 0; n < virtualDOM.children.length; n++) {

if (oldChildKey === virtualDOM.children[n].props.key) { //找到了,退出本次循环

found = true

break

}

}

if (!found) { //没找到,新的vnode.children不存在,删除

unmountNode(oldChild)

}

}

}

}

}

}

diffComponent.js

import mountElement from "./mountElement"

import updateComponent from "./updateComponent"

/**

* 该方法用来 对比更新组件

* @param {*} virtualDOM //虚拟dom

* @param {*} oldComponent //旧的组件

* @param {*} oldDOM //旧真实dom

* @param {*} container //要渲染到的父级

*/

export default function diffComponent(

virtualDOM,

oldComponent,

oldDOM,

container

) {

if (isSameComponent(virtualDOM, oldComponent)) {

// 同一个组件 做组件更新操作

updateComponent(virtualDOM, oldComponent, oldDOM, container)

} else {

// 不是同一个组件,直接用现在vnode 来渲染

mountElement(virtualDOM, container, oldDOM)

}

}

// 判断是否是同一个组件

function isSameComponent(virtualDOM, oldComponent) {

return oldComponent && virtualDOM.type === oldComponent.constructor

}

enqueueSetState.js

import diff from "./diff"//对比新旧vnode更新

/**

* 队列 先进先出 后进后出 ~

* @param {Array:Object} setStateQueue 抽象队列 每个元素都是一个key-value对象 key:对应的stateChange value:对应的组件

* @param {Array:Component} renderQueue 抽象需要更新的组件队列 每个元素都是Component

*/

const setStateQueue = [];

const renderQueue = [];

function defer (fn) {

//requestIdleCallback的兼容性不好,对于用户交互频繁多次合并更新来说,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理可以延迟渲染的任务~

// if (window.requestIdleCallback) {

// console.log('requestIdleCallback');

// return requestIdleCallback(fn);

// }

//高优先级任务 异步的 先挂起

//告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

return requestAnimationFrame(fn);

}

export function enqueueSetState (stateChange, component) {

//第一次进来肯定会先调用defer函数

if (setStateQueue.length === 0) {

//清空队列的办法是异步执行,下面都是同步执行的一些计算

defer(flush);

}

// setStateQueue:[{state:{a:1},component:app},{state:{a:2},component:test},{state:{a:3},component:app}]

//向队列中添加对象 key:stateChange value:component

setStateQueue.push({

stateChange,

component

});

//如果渲染队列中没有这个组件 那么添加进去

if (!renderQueue.some(item => item === component)) {

renderQueue.push(component);

}

}

function flush () {//下次重绘之前调用,合并state

let item, component;

//依次取出对象,执行

while ((item = setStateQueue.shift())) {

const { stateChange, component } = item;

let newState;

// 如果stateChange是一个方法,也就是setState的第二种形式

if (typeof stateChange === 'function') {

newState = Object.assign(

component.state,

stateChange(component.prevState, component.props)

);

} else {

// 如果stateChange是一个对象,则直接合并到setState中

newState = Object.assign(component.state, stateChange);

}

// 如果没有prevState,则将当前的state作为初始的prevState

if (!component.prevState) {

component.prevState = Object.assign({}, newState);

}

component.state = newState;

}

//先做一个处理合并state的队列,然后把state挂载到component下面 这样下面的队列,遍历时候,能也拿到state属性

//依次取出组件,执行更新逻辑,渲染

while ((component = renderQueue.shift())) {

// 获取最新的要渲染的 virtualDOM 对象

let virtualDOM = component.render()

// 获取旧的 virtualDOM 对象 进行比对

let oldDOM = component.getDOM()

// 获取容器

let container = oldDOM.parentNode

// 实现对象

diff(virtualDOM, container, oldDOM)

}

}

index.js

import createElement from "./createElement"

import render from "./render"

import Component from "./Component"

export default {

createElement,

render,

Component

}

isFunction.js

export default function isFunction(virtualDOM) { //判断当前tag是不是一个组件

return virtualDOM && typeof virtualDOM.tag === "function"

}

isFunctionComponent.js

import isFunction from "./isFunction"

/**

* 该方法用来判断是函数组件还是类组件,组件原型有render方法就认为是类组件

* @param {*} virtualDOM 虚拟dom

*/

export default function isFunctionComponent(virtualDOM) {

const type = virtualDOM.tag

return (

type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)

)

}

mountComponent.js

import isFunctionComponent from "./isFunctionComponent"

import mountNativeElement from "./mountNativeElement"

import isFunction from "./isFunction"

/**

* 该方法 用来 获取组件的虚拟domvnode

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 真实dom

*/

export default function mountComponent (virtualDOM, container, oldDOM) {

let nextVirtualDOM = null

let component = null

// 判断组件是类组件还是函数组件

if (isFunctionComponent(virtualDOM)) {

// 函数组件

nextVirtualDOM = buildFunctionComponent(virtualDOM)

} else {

// 类组件

nextVirtualDOM = buildClassComponent(virtualDOM)

component = nextVirtualDOM.component

}

if (isFunction(nextVirtualDOM)) { //如果组件调用解析以后返回的 还是一个 组件的话,就继续解析它

//例如function App(){ return <Demo / >} 这种

mountComponent(nextVirtualDOM, container, oldDOM)

} else {

//否则 当前节点已经解析成 元素或者文本节点了,调用mountNativeElement真实dom,挂载或更新

mountNativeElement(nextVirtualDOM, container, oldDOM)

}

if (component) { //走到这里时 组件已经加载完成了,执行它的生命周期函数

component.componentDidMount()

if (component.props && component.props.ref) {

component.props.ref(component) //传递组件实例给ref引用

}

}

}

function buildFunctionComponent(virtualDOM) {//函数组件返回虚拟dom

return virtualDOM.tag(virtualDOM.props || {})

}

function buildClassComponent(virtualDOM) {//class组件调用render 返回虚拟dom

const component = new virtualDOM.tag(virtualDOM.props || {})

component.prevState = component.state;

const nextVirtualDOM = component.render()

nextVirtualDOM.component = component //把当前组件实例挂载到虚拟dom上,方便后续执行 生命周期函数

return nextVirtualDOM

}

mountElement.js

import mountNativeElement from "./mountNativeElement"

import isFunction from "./isFunction"

import mountComponent from "./mountComponent"

/**

* 该方法用来区分是组件还是 普通元素,普通元素就生成dom挂载到界面

* @param {*} virtualDOM 虚拟dom,当前的vnode

* @param {*} container 要放入的容器元素

* @param {*} oldDOM 旧的 真实dom,上面储存了老的vnode,参数可选,首次渲染界面时不存在

*/

export default function mountElement(virtualDOM, container, oldDOM) {

if (isFunction(virtualDOM)) { //是组件

// Component

mountComponent(virtualDOM, container, oldDOM)

} else {//不是组件,根据虚拟dom生成真实dom挂载

// NativeElement

mountNativeElement(virtualDOM, container, oldDOM)

}

}

mountNativeElement.js

import createDOMElement from "./createDOMElement"

import unmountNode from "./unmountNode"

/**

* 该方法根据虚拟vnode 来获取真实dom然后在父节点中进行 更新或添加

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 老的真实dom

*/

export default function mountNativeElement(virtualDOM, container, oldDOM) {

let newElement = createDOMElement(virtualDOM) //获取真实dom

// 将转换之后的DOM对象放置在页面中

if (oldDOM) { //如果老的dom存在

container.insertBefore(newElement, oldDOM) //插入它之前

} else {

container.appendChild(newElement) //直接添加

}

// 判断旧的DOM对象是否存在 如果存在 删除

if (oldDOM) {

unmountNode(oldDOM)

}

// 获取类组件实例对象

let component = virtualDOM.component

// 如果类组件实例对象存在

if (component) {

// 将DOM对象存储在类组件实例对象中,setState时要获取 真实dom,然后获取对应信息更新

component.setDOM(newElement)

}

}

render.js

import diff from "./diff"

/**

*

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 虚拟dom对应的真实dom,更新时才会有,初始创建时是没有的, 我们在创建完 真实dom之后 会把当前用的vnode

* 挂载到真实dom一个属性上 方便后续做新旧vnode对比

*/

export default function render (

virtualDOM,

container,

oldDOM = container.firstChild

) {

diff(virtualDOM, container, oldDOM) // 虚拟dom diff算法,该方法 初始会生成dom新建, 之后会做新旧vnode对比

}

unmountNode.js

/**

* 该方法用来删除真实dom上的节点

* @param {*} node 要删除的节点

*/

export default function unmountNode(node) {

// 获取节点的 _virtualDOM 对象

const virtualDOM = node._virtualDOM

// 1. 文本节点可以直接删除

if (virtualDOM.type === "text") {

// 删除直接

node.remove()

// 阻止程序向下执行

return

}

// 2. 看一下节点是否是由组件生成的

let component = virtualDOM.component

// 如果 component 存在 就说明节点是由组件生成的

if (component) {

component.componentWillUnmount()

}

// 3. 看一下节点身上是否有ref属性,在这里要置成null,防止后续还有引用该组件实例,导致无法释放

if (virtualDOM.props && virtualDOM.props.ref) {

virtualDOM.props.ref(null)

}

// 4. 看一下节点的属性中是否有事件属性,防止 事件没有删除,导致内存泄漏

Object.keys(virtualDOM.props).forEach(propName => {

if (propName.slice(0, 2) === "on") {

const eventName = propName.toLowerCase().slice(0, 2)

const eventHandler = virtualDOM.props[propName]

node.removeEventListener(eventName, eventHandler)

}

})

// 5. 递归删除子节点,如果子节点是组件的话,把对他的的引用和事件方法都删掉

if (node.childNodes.length > 0) {

for (let i = 0; i < node.childNodes.length; i++) {

unmountNode(node.childNodes[i])

i--

}

}

// 最后处理完了,删除该节点

node.remove()

}

updateComponent.js

import diff from "./diff"

/**

* 同一个组件更新操作

* @param {*} virtualDOM

* @param {*} oldComponent 组件实例

* @param {*} oldDOM 真实dom

* @param {*} container

*/

export default function updateComponent(

virtualDOM,

oldComponent,

oldDOM,

container

) {

oldComponent.componentWillReceiveProps(virtualDOM.props) //生命周期函数,props变化

if (oldComponent.shouldComponentUpdate(virtualDOM.props,oldComponent.prevState)) { //查看是否要更新

// 未更新前的props

let prevProps = oldComponent.props

oldComponent.componentWillUpdate(virtualDOM.props)//即将更新

// 组件更新props

oldComponent.updateProps(virtualDOM.props)

oldComponent.prevState = oldComponent.state //更新prevState

// 获取组件返回的最新的 virtualDOM,都更新了获取最新vnode

let nextVirtualDOM = oldComponent.render()

// 更新 component 组件实例对象,给最新的vnode来赋值 component

nextVirtualDOM.component = oldComponent

// 比对 更新

diff(nextVirtualDOM, container, oldDOM)

oldComponent.componentDidUpdate(prevProps)

}

}

updateNodeElement.js

/**

* 该方法设置或更新真实dom上的属性

* @param {*} newElement 真实dom对象

* @param {*} virtualDOM 新的vnode

* @param {*} oldVirtualDOM 老的vnode

*

*/

export default function updateNodeElement(

newElement,

virtualDOM,

oldVirtualDOM = {}

) {

// 获取节点对应的属性对象

const newProps = virtualDOM.props || {} //获取新的props属性

const oldProps = oldVirtualDOM.props || {} //旧的vnode上props属性

Object.keys(newProps).forEach(propName => {

// 获取属性值

const newPropsValue = newProps[propName]

const oldPropsValue = oldProps[propName]

if (newPropsValue !== oldPropsValue) {

// 判断属性是否是否事件属性 onClick -> click

if (propName.slice(0, 2) === "on") {

// 事件名称

const eventName = propName.toLowerCase().slice(2)

// 为元素添加事件

newElement.addEventListener(eventName, newPropsValue)

//已经挂载过新的处理函数了要删除原有的事件的事件处理函数

if (oldPropsValue) {

newElement.removeEventListener(eventName, oldPropsValue)

}

} else if (propName === "value" || propName === "checked") {//如果要是input属性

newElement[propName] = newPropsValue //直接设置值

} else if (propName !== "children") { //排除子集 属性

// 其他属性都通过setAttribute处理

if (propName === "className") {

newElement.setAttribute("class", newPropsValue)

} else {

newElement.setAttribute(propName, newPropsValue)

}

}

}

})

// 判断属性被删除的情况,遍历旧的属性在新的props里查找,要是找不到证明被删除了,就也要对应删除

Object.keys(oldProps).forEach(propName => {

const newPropsValue = newProps[propName]

const oldPropsValue = oldProps[propName]

if (!newPropsValue) { //没找到,删除了

// 属性被删除了

if (propName.slice(0, 2) === "on") { //删除事件

const eventName = propName.toLowerCase().slice(2)

newElement.removeEventListener(eventName, oldPropsValue)

} else if (propName !== "children") { //排除children,其他都用removeAttribute方法处理

newElement.removeAttribute(propName)

}

}

})

}

updateTextNode.js

export default function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {//更新textNode节点

if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {

oldDOM.textContent = virtualDOM.props.textContent

oldDOM._virtualDOM = virtualDOM //更新完了之后因为用了新的vnode,所以要更新一下

}

}

大家可以把可以去我的仓库里把代码下载下来看一下,整体还是不太复杂的,里面每个方法都有注释以及jsDOC的说明.后续会发一篇filber架构的简易实现。

本文内容借鉴于拉钩大前端训练营

javascript源码react.jsstack虚拟DOM

阅读 45更新于 今天 09:35

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


记录大前端成长路线

世界核平

avatar

Charon

世界核平

14 声望

4 粉丝

0 条评论

得票时间

avatar

Charon

世界核平

14 声望

4 粉丝

宣传栏

项目地址:
https://github.com/544076724/...

之前写过一般react-mini版本的实现就是会有点问题,这里进行了一次改版,这个结构是stack版的后续会出一版filber架构的.
准备工作是和之前一样的,参考我之前写的
https://segmentfault.com/a/11...

打包工具

npm install -g parcel-bundler

.babelrc

{

"presets": ["env"],

"plugins": [

["transform-react-jsx", {

"pragma": "React.createElement"

}]

]

}

最后parcel index.html 启动项目

jsx代码我们还是通过babel设定使用React.createElement方法来转换生成虚拟dom.

关于虚拟dom,我原来那一篇讲过虚拟dom,这里用一句话来总结就是:

在 React 中,每个 DOM 对象都有一个对应的 Virtual DOM 对象,它是 DOM 对象的 JavaScript 对象表现形式,其实就是使用 JavaScript 对象来描述 DOM 对象信息,比如 DOM 对象的类型是什么,它身上有哪些属性,它拥有哪些子元素。

可以把 Virtual DOM 对象理解为 DOM 对象的副本,但是它不能直接显示在屏幕上。
虚拟dom是如何提升效率的总结一下就是:
在 React 第一次创建 DOM 对象后,会为每个 DOM 对象创建其对应的 Virtual DOM 对象,在 DOM 对象发生更新之前,React 会先更新所有的 Virtual DOM 对象,然后 React 会将更新后的 Virtual DOM 和 更新前的 Virtual DOM 进行比较,从而找出发生变化的部分,React 会将发生变化的部分更新到真实的 DOM 对象中,React 仅更新必要更新的部分。

Virtual DOM 对象的更新和比较仅发生在内存中,不会在视图中渲染任何内容,所以这一部分的性能损耗成本是微不足道的。

但是首次渲染的时候因为要生成虚拟dom来做映射,所以首次渲染的时候是没有原来那种方式快的,因为要做一些其他的额外操作.

这里标注一下我们用到的方法以及作用

  • render.js :入口文件调用diff方法挂载或比对新旧vnode
  • diff.js :该方法用来做新旧vnode的比对,以及首次挂载的处理,setState时会调用,或者render(<div>sss</div) 更改render(<span>lll</span>)会用到.
  • Component.js : 类组件的父类,所有类组件继承自它,存储props,内部有setState方法,调用 diff来对比新旧vnode更新,注册一系列生命周期函数.
  • mountElement.js : 该方法用来区分是组件还是 普通元素,普通元素就生成dom挂载到界面,组件时获取组件的虚拟dom vnode 然后再挂载
  • createDOMElement.js :根据虚拟dom生成真实dom,并处理props属性
  • diffComponent.js :该方法用来 对比更新组件
  • updateTextNode.js : 更新textNode 节点
  • updateNodeElement.js :该方法设置或更新真实dom上的属性
  • unmountNode.js :该方法用来删除真实dom上的节点(删除节点时假如该节点是由组件生产的,需要把对应的ref和绑定的事件函数清空掉,防止内存泄露)
  • createElement.js :该函数用来生成虚拟dom,其他类react框架中也会叫h函数
  • index.js :做react入口文件,负责统一导出
  • isFunction.js :判断当前tag是不是一个组件
  • isFunctionComponent.js :该方法用来判断是函数组件还是类组件,组件原型有render方法就认为是类组件
  • mountComponent.js :该方法 用来 获取组件的虚拟dom vnode,并且把组件实例挂载到vnode上,方便后续调用生命周期
  • mountNativeElement.js :该方法根据虚拟vnode 来获取真实dom然后在父节点中进行 更新或添加
  • updateComponent.js :同一个组件更新操作
  • enqueueSetState.js :处理setState批量更新的操作

好了我们下面正式进入正题,先说一下我们的开发思路:

  1. 创建createElement方法来转换jsx为vnode
  2. 创建Component组件类,所有的组件都是继承自该类
  3. 将vnode转换为真实dom来插入父级节点(这里直接使用diff方法对比了,该方法承载了新旧vnode的对比,并且判断了是否是首次挂载vnode),diff首次挂载是会调用mountElement方法,会区分是否是组件

    1. 不是组件得时候,那就是html节点或者文本节点,那就会使用mountNativeElement方法直接把当前这个vnode递归(递归过程中会持续使用mountElement方法来判断当前这级是组件还是直接的html标签)来转换为html片段,最后插入到指定的容器中(例如id="root"的节点)
    2. 是组件的时候调用mountComponent方法,该方法会判断是类组件还是函数组件,然后来运行他们获取他们返回的vnode,类组件时会把组件实例储存在返回的vnode上,方便后续调用组件生命周期,然后它们解析出来的vnode,会查看是不是还是组件,例如例如function App(){ return <Demo / >} 这种,如果是的话继续调用mountComponent解析,否则调用mountNativeElement方法把vnode转换成真实dom,然后执行组件生命周期componentDidMount,给props.ref传递组件实例当引用。

  4. 到这里首次加载就完成了,然后就是diff方法中不是首次加载而是对比新旧vnode更新操作了,这里主要分几步如下:

    1. 新vnode不是组件(组件要单独处理)并且新旧vnode标签不同,这会不需要对比直接用新的vnode生成真实dom然后把老的dom替换就可以了
    2. 新vnode是组件调用diffComponent方法来对比组件更新,判断两个是否是同一个组件,如果旧的vnode不是组件或者和新vnode不是同一个组件直接用mountElement方法渲染新的dom,把旧的dom替换掉就可以,是同一个组件旧调用updateComponent来更新组件
    3. 新旧vnode是同一个节点,标签一样,文本节点的话直接更新内容,元素节点的话更新元素上的属性,也就是更新props中的属性
    4. 到这里同级的比对完成之后就是,就该进行子集的比对了

      1. 子集的比对要使用到key属性,首先获取当前真实dom html中的子节点的所有元素节点的key,存到一个对象keyedElements中,值就是当前的真实dom的引用,然后开始比对
      2. 如果从真实dom中获取的子节点就没key的话,直接逐个节点 一个一个一次调用diff更新对比就可以了.这会是同级比对,不会去查找key
      3. 然后从真实dom html中获取子节点有key时,循环新vnode的子节点,获取它的key,然后从keyedElements对象中查找看能不能找到key一样的获取它的值放到domElement变量中(真实dom引用)

        1. 假如找到了就该查看位置对不对了,因为我们是循环这会用这个i获取真实dom子集中i的位置的元素看和domElement 是不是同一个,不是的话证明位置不对,那就把domElement插入到当前这个真实dom子集中i的位置之前就可以了.
        2. 然后就是如果从keyedElements没有找到key相同的,证明真实dom就缺少这个节点,这会直接使用mountElement新建就可以了。

    5. 子集对比完了之后,就该删除多余节点了

      1. 判断旧节点的数量比新的长,没有key,直接删除,没有key时证明,到从开头到virtualDOM.children的结尾都已经被更新过了,所以我们从后往前删除,删除到virtualDOM.children 的结尾处就可以了
      2. 有key时,通过key属性删除节点,把老的节点里的key从新的vnode.children里查找,如果找不到就删除

到此为止我们的所有新建和更新就完成了。

最后的流程就是贴代码了

Component.js

import {enqueueSetState} from "./enqueueSetState";

export default class Component { //类组件的父类,所有类组件继承自它

constructor(props) {

this.props = props //储存props

}

setState(state) { //获取state

enqueueSetState(state,this)

}

setDOM(dom) { //设置 当前组件对应的真实dom

this._dom = dom

}

getDOM() {//获取

return this._dom

}

updateProps(props) { //更新props

this.props = props

}

// 生命周期函数

componentWillMount() {}

componentDidMount() {}

componentWillReceiveProps(nextProps) {}

shouldComponentUpdate(nextProps, nextState) {

return nextProps != this.props || nextState != this.state

}

componentWillUpdate(nextProps, nextState) {}

componentDidUpdate(prevProps, preState) {}

componentWillUnmount() {}

}

createDOMElement.js

import mountElement from "./mountElement"

import updateNodeElement from "./updateNodeElement"

/**

*

* @param {*} virtualDOM 根据虚拟dom生成真实dom

* 这个函数,当前项 virtualDOM.tag 不会是一个方法,也就是说,函数组件不会直接走到这里

* 这里处理ref 不是组件上的ref而是 <div ref={方法()}></div> 这种,在这里ref的回调传入的不会是组件的引用

*/

export default function createDOMElement (virtualDOM) {

let newElement = null

if (virtualDOM.tag === "text") {

//文本节点

newElement = document.createTextNode(virtualDOM.props.textContent) //取出我们赋值到props里的内容

} else {

newElement = document.createElement(virtualDOM.tag)

updateNodeElement(newElement, virtualDOM) //设置props属性 到真实dom

}

newElement._virtualDOM = virtualDOM //把当前的vnode对象,挂载到真实dom上,用来做新旧vnode对比使用,下次更新它就是旧的vnode了

// 这回我们才创建了第一层节点,要递归创建子节点

virtualDOM.children.forEach(child => {

mountElement(child, newElement) //该方法会区分 当前节点是 组件还是元素

})

//处理ref属性,如果要是 有ref的话,调用ref传入的函数,把当前创建的newElement dom传递给它

if (virtualDOM.props && virtualDOM.props.ref) {

virtualDOM.props.ref(newElement)

}

return newElement //最后返回新建的 dom元素

}

createElement.js

/**

* 该函数用来生成虚拟dom,其他类react框架中也会叫h函数,我们配置了babel 会把jsx转换成React.createElement("div", null, "123")这种形式

* @param {*} tag 标签类型标记是 元素标签还是文本元素text 还是组件 为组件是 tag 是一个函数

* @param {*} props 标签 props 属性

* @param {...any} children 当前元素的下级元素 前两个之后的都是 子元素

*/

export default function createElement (tag, props, ...children) {

//在这里把 所有的 布尔值和null去掉,这是不需要在界面展示的

// children 里会有如下 children 这种格式 也就是子元素有是文本元素的, 这样会类型不统一

//一会是字符串一会是对象所以在这里给它统一一下,子节点都转换成对象

// { tag: "div", props: null, children: Array(4) }

// children: Array(4)

// 0: "hello"

// 1: { tag: "span", props: null, children: Array(1) }

// 2: "react"

// 3: { tag: "span", props: null, children: Array(1) }

// length: 4

// __proto__: Array(0)

// props: null

// tag: "div"

const childElement = [].concat(...children).reduce((result, child) => {

// 通过reduce处理

if (child !== false && child !== true && child !== null) {

if (child instanceof Object) {

result.push(child);

} else {

result.push(createElement("text", { textContent: child }));

}

}

return result;

},[])

//通过这样我们就做了一个类型统一

return {

tag,

props:Object.assign({ children: childElement }, props), //react中props属性也有childern属性这里我们也加上

children:childElement

}

}

diff.js

import mountElement from "./mountElement"

import createDOMElement from "./createDOMElement"

import diffComponent from "./diffComponent"

import updateTextNode from "./updateTextNode"

import updateNodeElement from "./updateNodeElement"

import unmountNode from "./unmountNode"

/**

* 该方法用来做新旧vnode的比对,以及首次挂载的处理

* setState时会调用,或者render(<div>sss</div) 更改render(<span>lll</span>)

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 虚拟dom对应的真实dom,更新时才会有,初始创建时 是没有的, 更新时它也是老的真实dom

*/

export default function diff (virtualDOM, container, oldDOM) {

const oldVirtualDOM = oldDOM && oldDOM._virtualDOM //首次创建时是不存在的

const oldComponent = oldVirtualDOM && oldVirtualDOM.component //查看该节点是否是 组件

if (!oldDOM) { //如果它不存在是首次挂载 直接执行mountElement方法

mountElement(virtualDOM, container)

}

else if (//老的存在,要对比更新

// 如果要比对的两个节点类型不相同

virtualDOM.tag !== oldVirtualDOM.tag &&

// 并且节点的类型不是组件 因为组件要单独处理

typeof virtualDOM.tag !== "function"

) {

// 新旧的标签不同,不需要对比

// 直接使用新的 virtualDOM 对象生成真实 DOM 对象

const newElement = createDOMElement(virtualDOM)

// 使用新的 DOM 对象替换旧的 DOM 对象

oldDOM.parentNode.replaceChild(newElement, oldDOM)

} else if (typeof virtualDOM.tag === "function") {

// 是组件

diffComponent(virtualDOM, oldComponent, oldDOM, container)

} else if (oldVirtualDOM && virtualDOM.tag === oldVirtualDOM.tag) {

//如果旧的vnode存在 并且 两个标签一样 进行节点更新

if (virtualDOM.tag === "text") {//文本节点

// 更新内容

updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)

} else {

// 更新元素节点属性

updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)

}

//最后是key的对比

// 1. 将拥有key属性的子元素放置在一个单独的对象中

let keyedElements = {}

for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {

let domElement = oldDOM.childNodes[i]

if (domElement.nodeType === 1) {

let key = domElement.getAttribute("key")

if (key) {

keyedElements[key] = domElement

}

}

}

let hasNoKey = Object.keys(keyedElements).length === 0

if (hasNoKey) {//无key

// 没有key,直接逐级比对,一个一个更新

virtualDOM.children.forEach((child, i) => {

diff(child, oldDOM, oldDOM.childNodes[i]) //更新比对

})

} else {//html有key

// 2. 循环 virtualDOM 的子元素 获取子元素的 key 属性

virtualDOM.children.forEach((child, i) => {

let key = child.props.key

if (key) {

let domElement = keyedElements[key] //获取对应的真实dom

if (domElement) {

// 3. 看看当前位置的元素是不是我们期望的元素

//如果当前坐标的元素在 上一次操作就存在,然后查看两个是不是相等

//不相等的话,证明当前位置的元素不是 我们要的这个domElement元素,那就直接把它插入到这个位置

if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {

oldDOM.insertBefore(domElement, oldDOM.childNodes[i])

}

} else {

// domElement不存在证明 原来就少这个元素 直接进行新增元素操作

mountElement(child, oldDOM, oldDOM.childNodes[i])

}

}

})

}

//子集对比完了,查看旧的节点是不是比新的长,长的话证明新的没有,需要删除操作

// 获取旧节点

let oldChildNodes = oldDOM.childNodes

// 判断旧节点的数量比新的长

if (oldChildNodes.length > virtualDOM.children.length) {

if (hasNoKey) { //没有key,直接删除

// 有节点需要被删除

for ( //没有key时证明,到从开头到virtualDOM.children的结尾都已经被更新过了,所以我们从后往前删除

//删除到virtualDOM.children 的结尾处就可以了,代码如下

let i = oldChildNodes.length - 1;

i > virtualDOM.children.length - 1;

i--

) {

unmountNode(oldChildNodes[i]) //删除新的vnode上面不存在的节点

}

} else {//有key

// 通过key属性删除节点,把老的节点里的key从新的vnode.children里查找,如果找不到就删除

for (let i = 0; i < oldChildNodes.length; i++) {

let oldChild = oldChildNodes[i]

let oldChildKey = oldChild._virtualDOM.props.key

let found = false

for (let n = 0; n < virtualDOM.children.length; n++) {

if (oldChildKey === virtualDOM.children[n].props.key) { //找到了,退出本次循环

found = true

break

}

}

if (!found) { //没找到,新的vnode.children不存在,删除

unmountNode(oldChild)

}

}

}

}

}

}

diffComponent.js

import mountElement from "./mountElement"

import updateComponent from "./updateComponent"

/**

* 该方法用来 对比更新组件

* @param {*} virtualDOM //虚拟dom

* @param {*} oldComponent //旧的组件

* @param {*} oldDOM //旧真实dom

* @param {*} container //要渲染到的父级

*/

export default function diffComponent(

virtualDOM,

oldComponent,

oldDOM,

container

) {

if (isSameComponent(virtualDOM, oldComponent)) {

// 同一个组件 做组件更新操作

updateComponent(virtualDOM, oldComponent, oldDOM, container)

} else {

// 不是同一个组件,直接用现在vnode 来渲染

mountElement(virtualDOM, container, oldDOM)

}

}

// 判断是否是同一个组件

function isSameComponent(virtualDOM, oldComponent) {

return oldComponent && virtualDOM.type === oldComponent.constructor

}

enqueueSetState.js

import diff from "./diff"//对比新旧vnode更新

/**

* 队列 先进先出 后进后出 ~

* @param {Array:Object} setStateQueue 抽象队列 每个元素都是一个key-value对象 key:对应的stateChange value:对应的组件

* @param {Array:Component} renderQueue 抽象需要更新的组件队列 每个元素都是Component

*/

const setStateQueue = [];

const renderQueue = [];

function defer (fn) {

//requestIdleCallback的兼容性不好,对于用户交互频繁多次合并更新来说,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理可以延迟渲染的任务~

// if (window.requestIdleCallback) {

// console.log('requestIdleCallback');

// return requestIdleCallback(fn);

// }

//高优先级任务 异步的 先挂起

//告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

return requestAnimationFrame(fn);

}

export function enqueueSetState (stateChange, component) {

//第一次进来肯定会先调用defer函数

if (setStateQueue.length === 0) {

//清空队列的办法是异步执行,下面都是同步执行的一些计算

defer(flush);

}

// setStateQueue:[{state:{a:1},component:app},{state:{a:2},component:test},{state:{a:3},component:app}]

//向队列中添加对象 key:stateChange value:component

setStateQueue.push({

stateChange,

component

});

//如果渲染队列中没有这个组件 那么添加进去

if (!renderQueue.some(item => item === component)) {

renderQueue.push(component);

}

}

function flush () {//下次重绘之前调用,合并state

let item, component;

//依次取出对象,执行

while ((item = setStateQueue.shift())) {

const { stateChange, component } = item;

let newState;

// 如果stateChange是一个方法,也就是setState的第二种形式

if (typeof stateChange === 'function') {

newState = Object.assign(

component.state,

stateChange(component.prevState, component.props)

);

} else {

// 如果stateChange是一个对象,则直接合并到setState中

newState = Object.assign(component.state, stateChange);

}

// 如果没有prevState,则将当前的state作为初始的prevState

if (!component.prevState) {

component.prevState = Object.assign({}, newState);

}

component.state = newState;

}

//先做一个处理合并state的队列,然后把state挂载到component下面 这样下面的队列,遍历时候,能也拿到state属性

//依次取出组件,执行更新逻辑,渲染

while ((component = renderQueue.shift())) {

// 获取最新的要渲染的 virtualDOM 对象

let virtualDOM = component.render()

// 获取旧的 virtualDOM 对象 进行比对

let oldDOM = component.getDOM()

// 获取容器

let container = oldDOM.parentNode

// 实现对象

diff(virtualDOM, container, oldDOM)

}

}

index.js

import createElement from "./createElement"

import render from "./render"

import Component from "./Component"

export default {

createElement,

render,

Component

}

isFunction.js

export default function isFunction(virtualDOM) { //判断当前tag是不是一个组件

return virtualDOM && typeof virtualDOM.tag === "function"

}

isFunctionComponent.js

import isFunction from "./isFunction"

/**

* 该方法用来判断是函数组件还是类组件,组件原型有render方法就认为是类组件

* @param {*} virtualDOM 虚拟dom

*/

export default function isFunctionComponent(virtualDOM) {

const type = virtualDOM.tag

return (

type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)

)

}

mountComponent.js

import isFunctionComponent from "./isFunctionComponent"

import mountNativeElement from "./mountNativeElement"

import isFunction from "./isFunction"

/**

* 该方法 用来 获取组件的虚拟domvnode

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 真实dom

*/

export default function mountComponent (virtualDOM, container, oldDOM) {

let nextVirtualDOM = null

let component = null

// 判断组件是类组件还是函数组件

if (isFunctionComponent(virtualDOM)) {

// 函数组件

nextVirtualDOM = buildFunctionComponent(virtualDOM)

} else {

// 类组件

nextVirtualDOM = buildClassComponent(virtualDOM)

component = nextVirtualDOM.component

}

if (isFunction(nextVirtualDOM)) { //如果组件调用解析以后返回的 还是一个 组件的话,就继续解析它

//例如function App(){ return <Demo / >} 这种

mountComponent(nextVirtualDOM, container, oldDOM)

} else {

//否则 当前节点已经解析成 元素或者文本节点了,调用mountNativeElement真实dom,挂载或更新

mountNativeElement(nextVirtualDOM, container, oldDOM)

}

if (component) { //走到这里时 组件已经加载完成了,执行它的生命周期函数

component.componentDidMount()

if (component.props && component.props.ref) {

component.props.ref(component) //传递组件实例给ref引用

}

}

}

function buildFunctionComponent(virtualDOM) {//函数组件返回虚拟dom

return virtualDOM.tag(virtualDOM.props || {})

}

function buildClassComponent(virtualDOM) {//class组件调用render 返回虚拟dom

const component = new virtualDOM.tag(virtualDOM.props || {})

component.prevState = component.state;

const nextVirtualDOM = component.render()

nextVirtualDOM.component = component //把当前组件实例挂载到虚拟dom上,方便后续执行 生命周期函数

return nextVirtualDOM

}

mountElement.js

import mountNativeElement from "./mountNativeElement"

import isFunction from "./isFunction"

import mountComponent from "./mountComponent"

/**

* 该方法用来区分是组件还是 普通元素,普通元素就生成dom挂载到界面

* @param {*} virtualDOM 虚拟dom,当前的vnode

* @param {*} container 要放入的容器元素

* @param {*} oldDOM 旧的 真实dom,上面储存了老的vnode,参数可选,首次渲染界面时不存在

*/

export default function mountElement(virtualDOM, container, oldDOM) {

if (isFunction(virtualDOM)) { //是组件

// Component

mountComponent(virtualDOM, container, oldDOM)

} else {//不是组件,根据虚拟dom生成真实dom挂载

// NativeElement

mountNativeElement(virtualDOM, container, oldDOM)

}

}

mountNativeElement.js

import createDOMElement from "./createDOMElement"

import unmountNode from "./unmountNode"

/**

* 该方法根据虚拟vnode 来获取真实dom然后在父节点中进行 更新或添加

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 老的真实dom

*/

export default function mountNativeElement(virtualDOM, container, oldDOM) {

let newElement = createDOMElement(virtualDOM) //获取真实dom

// 将转换之后的DOM对象放置在页面中

if (oldDOM) { //如果老的dom存在

container.insertBefore(newElement, oldDOM) //插入它之前

} else {

container.appendChild(newElement) //直接添加

}

// 判断旧的DOM对象是否存在 如果存在 删除

if (oldDOM) {

unmountNode(oldDOM)

}

// 获取类组件实例对象

let component = virtualDOM.component

// 如果类组件实例对象存在

if (component) {

// 将DOM对象存储在类组件实例对象中,setState时要获取 真实dom,然后获取对应信息更新

component.setDOM(newElement)

}

}

render.js

import diff from "./diff"

/**

*

* @param {*} virtualDOM 虚拟dom

* @param {*} container 挂载的父级节点

* @param {*} oldDOM 虚拟dom对应的真实dom,更新时才会有,初始创建时是没有的, 我们在创建完 真实dom之后 会把当前用的vnode

* 挂载到真实dom一个属性上 方便后续做新旧vnode对比

*/

export default function render (

virtualDOM,

container,

oldDOM = container.firstChild

) {

diff(virtualDOM, container, oldDOM) // 虚拟dom diff算法,该方法 初始会生成dom新建, 之后会做新旧vnode对比

}

unmountNode.js

/**

* 该方法用来删除真实dom上的节点

* @param {*} node 要删除的节点

*/

export default function unmountNode(node) {

// 获取节点的 _virtualDOM 对象

const virtualDOM = node._virtualDOM

// 1. 文本节点可以直接删除

if (virtualDOM.type === "text") {

// 删除直接

node.remove()

// 阻止程序向下执行

return

}

// 2. 看一下节点是否是由组件生成的

let component = virtualDOM.component

// 如果 component 存在 就说明节点是由组件生成的

if (component) {

component.componentWillUnmount()

}

// 3. 看一下节点身上是否有ref属性,在这里要置成null,防止后续还有引用该组件实例,导致无法释放

if (virtualDOM.props && virtualDOM.props.ref) {

virtualDOM.props.ref(null)

}

// 4. 看一下节点的属性中是否有事件属性,防止 事件没有删除,导致内存泄漏

Object.keys(virtualDOM.props).forEach(propName => {

if (propName.slice(0, 2) === "on") {

const eventName = propName.toLowerCase().slice(0, 2)

const eventHandler = virtualDOM.props[propName]

node.removeEventListener(eventName, eventHandler)

}

})

// 5. 递归删除子节点,如果子节点是组件的话,把对他的的引用和事件方法都删掉

if (node.childNodes.length > 0) {

for (let i = 0; i < node.childNodes.length; i++) {

unmountNode(node.childNodes[i])

i--

}

}

// 最后处理完了,删除该节点

node.remove()

}

updateComponent.js

import diff from "./diff"

/**

* 同一个组件更新操作

* @param {*} virtualDOM

* @param {*} oldComponent 组件实例

* @param {*} oldDOM 真实dom

* @param {*} container

*/

export default function updateComponent(

virtualDOM,

oldComponent,

oldDOM,

container

) {

oldComponent.componentWillReceiveProps(virtualDOM.props) //生命周期函数,props变化

if (oldComponent.shouldComponentUpdate(virtualDOM.props,oldComponent.prevState)) { //查看是否要更新

// 未更新前的props

let prevProps = oldComponent.props

oldComponent.componentWillUpdate(virtualDOM.props)//即将更新

// 组件更新props

oldComponent.updateProps(virtualDOM.props)

oldComponent.prevState = oldComponent.state //更新prevState

// 获取组件返回的最新的 virtualDOM,都更新了获取最新vnode

let nextVirtualDOM = oldComponent.render()

// 更新 component 组件实例对象,给最新的vnode来赋值 component

nextVirtualDOM.component = oldComponent

// 比对 更新

diff(nextVirtualDOM, container, oldDOM)

oldComponent.componentDidUpdate(prevProps)

}

}

updateNodeElement.js

/**

* 该方法设置或更新真实dom上的属性

* @param {*} newElement 真实dom对象

* @param {*} virtualDOM 新的vnode

* @param {*} oldVirtualDOM 老的vnode

*

*/

export default function updateNodeElement(

newElement,

virtualDOM,

oldVirtualDOM = {}

) {

// 获取节点对应的属性对象

const newProps = virtualDOM.props || {} //获取新的props属性

const oldProps = oldVirtualDOM.props || {} //旧的vnode上props属性

Object.keys(newProps).forEach(propName => {

// 获取属性值

const newPropsValue = newProps[propName]

const oldPropsValue = oldProps[propName]

if (newPropsValue !== oldPropsValue) {

// 判断属性是否是否事件属性 onClick -> click

if (propName.slice(0, 2) === "on") {

// 事件名称

const eventName = propName.toLowerCase().slice(2)

// 为元素添加事件

newElement.addEventListener(eventName, newPropsValue)

//已经挂载过新的处理函数了要删除原有的事件的事件处理函数

if (oldPropsValue) {

newElement.removeEventListener(eventName, oldPropsValue)

}

} else if (propName === "value" || propName === "checked") {//如果要是input属性

newElement[propName] = newPropsValue //直接设置值

} else if (propName !== "children") { //排除子集 属性

// 其他属性都通过setAttribute处理

if (propName === "className") {

newElement.setAttribute("class", newPropsValue)

} else {

newElement.setAttribute(propName, newPropsValue)

}

}

}

})

// 判断属性被删除的情况,遍历旧的属性在新的props里查找,要是找不到证明被删除了,就也要对应删除

Object.keys(oldProps).forEach(propName => {

const newPropsValue = newProps[propName]

const oldPropsValue = oldProps[propName]

if (!newPropsValue) { //没找到,删除了

// 属性被删除了

if (propName.slice(0, 2) === "on") { //删除事件

const eventName = propName.toLowerCase().slice(2)

newElement.removeEventListener(eventName, oldPropsValue)

} else if (propName !== "children") { //排除children,其他都用removeAttribute方法处理

newElement.removeAttribute(propName)

}

}

})

}

updateTextNode.js

export default function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {//更新textNode节点

if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {

oldDOM.textContent = virtualDOM.props.textContent

oldDOM._virtualDOM = virtualDOM //更新完了之后因为用了新的vnode,所以要更新一下

}

}

大家可以把可以去我的仓库里把代码下载下来看一下,整体还是不太复杂的,里面每个方法都有注释以及jsDOC的说明.后续会发一篇filber架构的简易实现。

本文内容借鉴于拉钩大前端训练营

以上是 【JS】mini-react新版本stack架构 的全部内容, 来源链接: www.h5w3.com/114191.html

回到顶部