乐趣区

关于javascript:minireact新版本stack架构

我的项目地址:
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 架构的繁难实现。

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

退出移动版