New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.生命周期和 diff 算法 #26

Open
MuYunyun opened this Issue Jul 27, 2018 · 0 comments

Comments

Projects
None yet
1 participant
@MuYunyun
Owner

MuYunyun commented Jul 27, 2018

该系列文章在实现 cpreact 的同时理顺 React 框架的核心内容

项目地址

生命周期

先来回顾 React 的生命周期,用流程图表示如下:

该流程图比较清晰地呈现了 react 的生命周期。其分为 3 个阶段 —— 生成期,存在期,销毁期。

因为生命周期钩子函数存在于自定义组件中,将之前 _render 函数作些调整如下:

// 原来的 _render 函数,为了将职责拆分得更细,将 virtual dom 转为 real dom 的函数单独抽离出来
function vdomToDom(vdom) {
  if (_.isFunction(vdom.nodeName)) {        // 为了更加方便地书写生命周期逻辑,将解析自定义组件逻辑和一般 html 标签的逻辑分离开
    const component = createComponent(vdom) // 构造组件
    setProps(component)                     // 更改组件 props
    renderComponent(component)              // 渲染组件,将 dom 节点赋值到 component
    return component.base                   // 返回真实 dom
  }
  ...
}

我们可以在 setProps 函数内(渲染前)加入 componentWillMountcomponentWillReceiveProps 方法,setProps 函数如下:

function setProps(component) {
  if (component && component.componentWillMount) {
    component.componentWillMount()
  } else if (component.base && component.componentWillReceiveProps) {
    component.componentWillReceiveProps(component.props) // 后面待实现
  }
}

而后我们在 renderComponent 函数内加入 componentDidMountshouldComponentUpdatecomponentWillUpdatecomponentDidUpdate 方法

function renderComponent(component) {
  if (component.base && component.shouldComponentUpdate) {
    const bool = component.shouldComponentUpdate(component.props, component.state)
    if (!bool && bool !== undefined) {
      return false // shouldComponentUpdate() 返回 false,则生命周期终止
    }
  }
  if (component.base && component.componentWillUpdate) {
    component.componentWillUpdate()
  }

  const rendered = component.render()
  const base = vdomToDom(rendered)

  if (component.base && component.componentDidUpdate) {
    component.componentDidUpdate()
  } else if (component && component.componentDidMount) {
    component.componentDidMount()
  }

  if (component.base && component.base.parentNode) { // setState 进入此逻辑
    component.base.parentNode.replaceChild(base, component.base)
  }

  component.base = base  // 标志符
}

测试生命周期

测试如下用例:

class A extends Component {
  componentWillReceiveProps(props) {
    console.log('componentWillReceiveProps')
  }

  render() {
    return (
      <div>{this.props.count}</div>
    )
  }
}

class B extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 1
    }
  }

  componentWillMount() {
    console.log('componentWillMount')
  }

  componentDidMount() {
    console.log('componentDidMount')
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log('shouldComponentUpdate', nextProps, nextState)
    return true
  }

  componentWillUpdate() {
    console.log('componentWillUpdate')
  }

  componentDidUpdate() {
    console.log('componentDidUpdate')
  }

  click() {
    this.setState({
      count: ++this.state.count
    })
  }

  render() {
    console.log('render')
    return (
      <div>
        <button onClick={this.click.bind(this)}>Click Me!</button>
        <A count={this.state.count} />
      </div>
    )
  }
}

ReactDOM.render(
  <B />,
  document.getElementById('root')
)

页面加载时输出结果如下:

componentWillMount
render
componentDidMount

点击按钮时输出结果如下:

shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate

diff 的实现

在 react 中,diff 实现的思路是将新老 virtual dom 进行比较,将比较后的 patch(补丁)渲染到页面上,从而实现局部刷新;本文借鉴了 preactsimple-react 中的 diff 实现,总体思路是将旧的 dom 节点和新的 virtual dom 节点进行了比较,根据不同的比较类型(文本节点、非文本节点、自定义组件)调用相应的逻辑,从而实现页面的局部渲染。代码总体结构如下:

/**
 * 比较旧的 dom 节点和新的 virtual dom 节点:
 * @param {*} oldDom  旧的 dom 节点
 * @param {*} newVdom 新的 virtual dom 节点
 */
function diff(oldDom, newVdom) {
  ...
  if (_.isString(newVdom)) {
    return diffTextDom(oldDom, newVdom)   // 对比文本 dom 节点
  }

  if (oldDom.nodeName.toLowerCase() !== newVdom.nodeName) {
    diffNotTextDom(oldDom, newVdom)       // 对比非文本 dom 节点
  }

  if (_.isFunction(newVdom.nodeName)) {
    return diffComponent(oldDom, newVdom) // 对比自定义组件
  }

  diffAttribute(oldDom, newVdom)          // 对比属性

  if (newVdom.children.length > 0) {
    diffChild(oldDom, newVdom)            // 遍历对比子节点
  }

  return oldDom
}

下面根据不同比较类型实现相应逻辑。

对比文本节点

首先进行较为简单的文本节点的比较,代码如下:

// 对比文本节点
function diffTextDom(oldDom, newVdom) {
  let dom = oldDom
  if (oldDom && oldDom.nodeType === 3) {  // 如果老节点是文本节点
    if (oldDom.textContent !== newVdom) { // 这里一个细节:textContent/innerHTML/innerText 的区别
      oldDom.textContent = newVdom
    }
  } else {                                // 如果旧 dom 元素不为文本节点
    dom = document.createTextNode(newVdom)
    if (oldDom && oldDom.parentNode) {
      oldDom.parentNode.replaceChild(dom, oldDom)
    }
  }
  return dom
}

对比非文本节点

对比非文本节点,其思路为将同层级的旧节点替换为新节点,代码如下:

// 对比非文本节点
function diffNotTextDom(oldDom, newVdom) {
  const newDom = document.createElement(newVdom.nodeName);
  [...oldDom.childNodes].map(newDom.appendChild) // 将旧节点下的元素添加到新节点下
  if (oldDom && oldDom.parentNode) {
    oldDom.parentNode.replaceChild(oldDom, newDom)
  }
}

对比自定义组件

对比自定义组件的思路为:如果新老组件不同,则直接将新组件替换老组件;如果新老组件相同,则将新组件的 props 赋到老组件上,然后再对获得新 props 前后的老组件做 diff 比较。代码如下:

// 对比自定义组件
function diffComponent(oldDom, newVdom) {
  if (oldDom._component && (oldDom._component.constructor !== newVdom.nodeName)) { // 如果新老组件不同,则直接将新组件替换老组件
    const newDom = vdomToDom(newVdom)
    oldDom._component.parentNode.insertBefore(newDom, oldDom._component)
    oldDom._component.parentNode.removeChild(oldDom._component)
  } else {
    setProps(oldDom._component, newVdom.attributes) // 如果新老组件相同,则将新组件的 props 赋到老组件上
    renderComponent(oldDom._component)              // 对获得新 props 前后的老组件做 diff 比较(renderComponent 中调用了 diff)
  }
}

遍历对比子节点

遍历对比子节点的策略如下:

  1. 比较新旧 dom 元素相同层级相同位置的节点类型,若节点类型不相等,则直接将新节点替换掉旧节点;
  2. 给节点加上 key 属性;

在 cpreact 的代码实现中,1 的目的降低了空间复杂度(避免了更深层次的遍历);2 的目的目前看来是少了一次新老类型的判断消耗。

代码如下:

// 对比子节点
function diffChild(oldDom, newVdom) {
  const keyed = {}
  const children = []
  const oldChildNodes = oldDom.childNodes
  for (let i = 0; i < oldChildNodes.length; i++) {
    if (oldChildNodes[i].key) {
      keyed[oldChildNodes[i].key] = oldChildNodes[i]
    } else { // 如果不存在 key,则优先找到节点类型相同的元素
      children.push(oldChildNodes[i])
    }
  }

  let newChildNodes = newVdom.children
  if (isArray(newVdom.children[0])) { // https://github.com/MuYunyun/cpreact/issues/9
    newChildNodes = newVdom.children[0]
  }

  for (let i = 0; i < newChildNodes.length; i++) {
    let child = null
    if (keyed[newChildNodes[i].key]) {
      child = keyed[newChildNodes[i].key]
      keyed[newChildNodes[i].key] = undefined
    } else { // 对应上面不存在 key 的情形
      // 在新老节点相同位置上寻找相同类型的节点进行比较;如果不满足上述条件则直接将新节点插入;
      if (children[i] && isSameNodeType(children[i], newChildNodes[i])) {
        child = children[i]
        children[i] = undefined
      } else if (children[i] && !isSameNodeType(children[i], newChildNodes[i])) { // 不是相同类型,直接替代掉
        children[i].replaceWith(newChildNodes[i])
        continue
      }
    }

    const result = diff(child, newChildNodes[i])
    // 如果 child 为 null
    if (result === newChildNodes[i]) {
      oldDom.appendChild(vdomToDom(result))
    }
  }
}

测试

在生命周期的小节中,componentWillReceiveProps 方法还未跑通,稍加修改 setProps 函数即可:

/**
 * 更改属性,componentWillMount 和 componentWillReceiveProps 方法
 */
function setProps(component, attributes) {
  if (attributes) {
    component.props = attributes // 这段逻辑对应上文自定义组件比较中新老组件相同时 setProps 的逻辑
  }

  if (component && component.base && component.componentWillReceiveProps) {
    component.componentWillReceiveProps(component.props)
  } else if (component && component.componentWillMount) {
    component.componentWillMount()
  }
}

来测试下生命周期小节中最后的测试用例:

  • 生命周期测试

  • diff 测试

鸣谢

Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react

@MuYunyun MuYunyun changed the title from 从 0 到 1 实现 React 系列 —— 生命周期和 diff 算法 to 从 0 到 1 实现 React 系列 —— 3.生命周期和 diff 算法 Aug 5, 2018

@MuYunyun MuYunyun changed the title from 从 0 到 1 实现 React 系列 —— 3.生命周期和 diff 算法 to 3.生命周期和 diff 算法 Sep 18, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment