我的项目地址:
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 批量更新的操作
好了咱们上面正式进入正题,先说一下咱们的开发思路:
- 创立 createElement 办法来转换 jsx 为 vnode
- 创立 Component 组件类, 所有的组件都是继承自该类
-
将 vnode 转换为实在 dom 来插入父级节点(这里间接应用 diff 办法比照了, 该办法承载了新旧 vnode 的比照, 并且判断了是否是首次挂载 vnode),diff 首次挂载是会调用 mountElement 办法, 会辨别是否是组件
- 不是组件得时候, 那就是 html 节点或者文本节点, 那就会应用 mountNativeElement 办法间接把以后这个 vnode 递归 (递归过程中会继续应用 mountElement 办法来判断以后这级是组件还是间接的 html 标签) 来转换为 html 片段,最初插入到指定的容器中(例如 id=”root” 的节点)
- 是组件的时候调用 mountComponent 办法,该办法会判断是类组件还是函数组件, 而后来运行他们获取他们返回的 vnode, 类组件时会把组件实例贮存在返回的 vnode 上,不便后续调用组件生命周期, 而后它们解析进去的 vnode, 会查看是不是还是组件,例如例如 function App(){ return <Demo / >} 这种,如果是的话持续调用 mountComponent 解析, 否则调用 mountNativeElement 办法把 vnode 转换成实在 dom, 而后执行组件生命周期 componentDidMount, 给 props.ref 传递组件实例当援用。
-
到这里首次加载就实现了, 而后就是 diff 办法中不是首次加载而是比照新旧 vnode 更新操作了, 这里次要分几步如下:
- 新 vnode 不是组件(组件要独自解决)并且新旧 vnode 标签不同,这会不须要比照间接用新的 vnode 生成实在 dom 而后把老的 dom 替换就能够了
- 新 vnode 是组件调用 diffComponent 办法来比照组件更新,判断两个是否是同一个组件,如果旧的 vnode 不是组件或者和新 vnode 不是同一个组件间接用 mountElement 办法渲染新的 dom,把旧的 dom 替换掉就能够,是同一个组件旧调用 updateComponent 来更新组件
- 新旧 vnode 是同一个节点,标签一样,文本节点的话间接更新内容,元素节点的话更新元素上的属性,也就是更新 props 中的属性
-
到这里同级的比对实现之后就是,就该进行子集的比对了
- 子集的比对要应用到 key 属性,首先获取以后实在 dom html 中的子节点的所有元素节点的 key,存到一个对象 keyedElements 中,值就是以后的实在 dom 的援用,而后开始比对
- 如果从实在 dom 中获取的子节点就没 key 的话,间接一一节点 一个一个一次调用 diff 更新比照就能够了. 这会是同级比对,不会去查找 key
-
而后从实在 dom html 中获取子节点有 key 时,循环新 vnode 的子节点,获取它的 key,而后从 keyedElements 对象中查找看能不能找到 key 一样的获取它的值放到 domElement 变量中(实在 dom 援用)
- 如果找到了就该查看地位对不对了,因为咱们是循环这会用这个 i 获取实在 dom 子集中 i 的地位的元素看和 domElement 是不是同一个,不是的话证实地位不对, 那就把 domElement 插入到以后这个实在 dom 子集中 i 的地位之前就能够了.
- 而后就是如果从 keyedElements 没有找到 key 雷同的,证实实在 dom 就短少这个节点,这会间接应用 mountElement 新建就能够了。
-
子集比照完了之后,就该删除多余节点了
- 判断旧节点的数量比新的长,没有 key,间接删除,没有 key 时证实, 到从结尾到 virtualDOM.children 的结尾都曾经被更新过了,所以咱们从后往前删除,删除到 virtualDOM.children 的结尾处就能够了
- 有 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 架构的繁难实现。
本文内容借鉴于拉钩大前端训练营