react組件化原理及優化實踐

前言

本文篇幅有點長,希望看完后能給你帶去一些收獲;主要針對react組件化原理、與vue開發感知上的對比以及一些基礎優化進行敘述。

React組件化基本原理

react是近來十分火熱的一個前端框架,它把注意力放在UI層,將頁面分成一些細塊,這些塊就是組件,組件之間的組合嵌套就形成最后的網頁界面。和其他很多前端框架一樣,它們主要解決了前端組件化的問題。下面通過一個例子逐步探尋react的組件化原理。

實例分析

假設我們需要實現一個收藏按鈕的功能,你應該能很快的寫出以下html結構:

<div id="starBtn">
    <button>
        <span class="star">☆</span>
    </button>
</div>

這時候的按鈕如上,它只是一個靜態結構沒有綁定任何點擊事件。如果我們需要點擊收藏,再次點擊取消,那么也很容易寫出下面的js代碼:

const starBtn = document.getElementById('starBtn')
const starStyle = document.getElementsByClassName('star')[0]
let isLike = false
document.getElementById('starBtn').addEventListener('click', function() {
    isLike = !isLike
    if (isLike) {
        starStyle.innerHTML = '★'
    } else {
        starStyle.innerHTML = '☆'
    }
}, false)

這時我們基本實現了收藏按鈕功能,可突然你的同事過來說想用你這個按鈕,你得不得拷貝整段html和js給他,還得考慮各種命名沖突兼容性,可以說這個按鈕毫無復用性可言。

結構復用

下面我們換一種方式來實現它,讓它具備一定的復用性。

class StarBtn {
    render() {
        return `
        <button>
            <span class="star">☆</span>
        </button>`
    }
}

上面實現了一個收藏按鈕類,用一個render函數返回了它的html結構。然后按照下面的用法,基本實現了結構的復用:

let starBtn = document.getElementById('starBtn')
let starbtn1 = new StarBtn()
starBtn.innerHTML = starbtn1.render()

但是有一個很明顯的問題,按鈕的結構都是通過innerHTML將一堆字符串塞進去的,這樣只是一個死的按鈕,不能綁定任何事件,下面我們進行優化。

簡單的組件化

設想如果有一個這樣的函數,它可以把一段html字符串輸出成一個dom元素,再把這個元素通過dom操作插入頁面里,我們就可以在這個dom元素上綁定事件來完成邏輯了:

function createDomFromString(str) {
    let _div = document.createElement('div')
    _div.innerHTML = str
    return _div
}

通過這個函數,我們就可以修改上面的按鈕父類代碼:

class StarBtn {
    constructor() {
        this.state = {
            isLike: false
        }
    }
    
    changeState() {
        const starStyle = document.getElementsByClassName('star')[0]
        this.state.isLike = !this.state.isLike
        starStyle.innerHTML = !this.state.isLike ? '☆' : '★'
    }
    
    render() {
        this._el = createDomFromString(`
            <button>
                <span class="star">☆</span>
            </button>`)
        this._el.addEventListener('click', this.changeState.bind(this), false)
        return this._el
    }
}

如上修改我們在構造函數里添加了一個state屬性用來保存每個實例的收藏狀態。然后在render函數中利用createDomFromString來返回一個dom節點,并在節點上綁定了changeState點擊事件處理函數來修改收藏狀態。

減少DOM操作

上面的代碼乍一看很好的實現了一個可復用的按鈕,這是因為該按鈕比較簡單,只有一個isLike狀態和一次修改文本的DOM操作,如果是一個有很多狀態的組件,那么將會充滿DOM操作。那么怎么才能從手動管理這些DOM操作中抽出來呢?我們可以使用一個中介函數來修改狀態并統一重繪這個組件:

class StarBtn {
    constructor() {
        this.state = {
            isLike: false
        }
    }
    setState(state) {
        this.state = state
        this.render()
    }
    changeState() {
        this.setState({
            isLike: !this.state.isLike
        })
    }
    render() {
        this._el = createDomFromString(`
            <button>
                <span class="star">${this.state.isLike ? '★' : '☆'}</span>
            </button>`)
        this._el.addEventListener('click', this.changeState.bind(this), false)
        return this._el
    }
}

我們使用了一個setState函數來統一修改state狀態,并在修改后調用render函數來重繪整個節點。也就是說你只要調用 setState,組件就會重新渲染,成功的去除了DOM操作。但其實這時候點擊按鈕并不會有任何反應,因為你只是重新構建了DOM但是并沒有把新的DOM插入頁面中。

替換新的DOM節點

這時候我們需要在組件外去監聽state的變化,然后插入新DOM,刪除老DOM來實現更新,我們需要添加一個onStateChange函數來做這些。

...
setState(state) {
    const _oldEl = this._el
    this.state = state
    this.render()
    if (this.onStateChange) this.onStateChange(_oldEl, this._el)
}
...
let starBtn = document.getElementById('starBtn')
let starbtn1 = new StarBtn()
starbtn1.onStateChange = (oldEl, newEl) => {
    starBtn.insertBefore(newEl, oldEl) // 插入新的元素
    starBtn.removeChild(oldEl) // 刪除舊的元素
}
starBtn.appendChild(starbtn1.render())

給每個實例添加一個onStateChange函數來監聽state的變化,然后渲染新的組件。還需要在setState函數里添加處理onStateChange函數的代碼。但是你可能會發現一個問題,這樣每次修改state都進行刪除、插入DOM操作不會十分影響性能嗎?這個問題在React中會用一個叫Virtual-DOM的策略規避掉,該策略具體內容不在本文所述范圍內。

這個收藏按鈕組件現在看起來還不錯,可以自由地添加功能,簡單的復用,也不需要DOM操作。但是如果這時想寫一個別的組件,我們可以進一步抽離出一個父類以供所有組件使用。

抽象公共組件類

我們可以抽象出一個Component類作為公共組件類:

class Component {
    setState(state) {
        const _oldEl = this._el
        this.state = state
        this._renderDom()
        if (this.onStateChange) this.onStateChange(_oldEl, this._el)
    }
    _renderDom() {
        this._el = createDomFromString(this.render())
        if (this.onClick) {
            this._el.addEventListener('click', this.onClick.bind(this), false)
        }
        return this._el
    }
}

這個公共類里包含了setState和renderDom兩個方法,用來修改狀態和輸出組件DOM,綁定事件。但是外部還需要將DOM插入頁面以及監聽state改變替換DOM的函數,如下mount函數:

const mount = (component, wrap) => {
    wrap.appendChild(component._renderDom())
    component.onStateChange = (oldEl, newEl) => {
        starBtn.insertBefore(newEl, oldEl) // 插入新的元素
        starBtn.removeChild(oldEl) // 刪除舊的元素
    }
}

我們就可以這樣改寫剛才的收藏按鈕組件的代碼:

class StarBtn extends Component {
    constructor() {
        super()
        this.state = {
            isLike: false
        }
    }
    onClick() {
        this.setState({
            isLike: !this.state.isLike
        })
    }
    render() {
        return `
            <button>
                <span class="star">${this.state.isLike ? '★' : '☆'}</span>
            </button>`
    }
}
let starBtn = document.getElementById('starBtn')
let  starbtn1 = new StarBtn()
mount(starbtn1, starBtn)

該類繼承了Component類,并且在render函數中返回html字符串,綁定onClick函數調用父類的setState來修改自己的狀態。然后將實例和父節點傳入mount函數完成state的監聽和DOM更新。

至此我們基本從頭到尾實現了一次組件化,實際上這個Component類和React源碼中的Component類使用方式很類似。了解了這些就很容易明白React組件化的基本原理。

React組件開發經驗

區分容器組件和可視化組件

Presentational稱為可視化組件,也就是我們用來渲染到頁面上可以被看見的組件,它只負責根據父組件傳來的props渲染視圖;
Container稱為容器組件,它總是作為可視化組件的父級組件出現,通常作用是給可視化組件準備數據,充當支架。

//BAD
class UserList extends React.Component {
    constructor() {
        this.state = {
            userArr: []
        }
    }
    componentWillMount() {
        //ajax獲取用戶列表數據
        this.setState({
            userArr: users
        })
    }
    render() {
        return (
            <ul>
            {
                this.props.userArr.map((user) => {
                    <li key={user.id}>
                        <img src={user.pic} />
                        <a href={user.link}>user detail info</a>
                    </li>
                })
            }
            </ul>
        )
    }
}
//NICE,使用純組件
const User = (user) => {    //這個User組件就是可視化組件,只負責渲染相關
    <li>
        <img src={user.pic} />
        <a href={user.link}>user detail info</a>
    </li>
}
class UserList extends React.Component {    //UserList組件為容器組件,ajax請求數據
    constructor() {
        this.state = {
            userArr: []
        }
    }
    componentWillMount() {
        //ajax獲取用戶列表數據
        this.setState({
            userArr: users
        })
    }
    render() {
        return (
            <ul>
            {
                this.props.userArr.map((user) => {
                    return <User user={user} key={user.id}></User>
                })
            }
            </ul>
        )
    }
}

使用setState需要注意的問題

setState()方法是react中用來修改狀態的內置方法,而且是必須的方式。也就是說不能使用this.state來修改組件的狀態。this.state是不可變的。文檔中提到setState執行總是會觸發一次重繪,所以使用過程中有一些需要注意的地方。

setState是異步的,沒有任何同步性的保證

在開發過程中,我們可能會在 setState 之后立即去拿 this.state 中的某個屬性,但是發現該值并沒有修改過來。官方說法是,為了性能上的優化,采用了 Batch 思想,會收集“一波” state 的變化,統一進行處理。為了解決這個問題 setState 可以有以下兩種寫法:

//第一種
this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});
//第二種
this.setState({
    counter: newVal
}, () => {
    //callback,會在state修改完成后執行
})

或者在生命周期 componentDidUpdate 函數中處理state修改后的邏輯。

setState會造成不必要的渲染

由于setState每執行一次都會出發一次重繪,但實際上很多渲染都是浪費的。我們可能只需要更新一個子組件,但卻會導致很大面積的組件重新render。這個問題會在后面的性能優化中提及。

setState只管理渲染相關的數據

與渲染無關的數據,例如id號、臨時變量等數據并不會影響ui渲染,可能只是用來做標記或與后端交互,那么這些值的修改不應該用setState去重繪。可以使用 localStorage 或者第三方狀態管理庫,如MobX。

綁定this的問題

this指向、執行上下文一直都是js中不可避免會遇到的問題。在react中,對this綁定的處理有很多種方案,總結大致有以下這些:

  • 使用ES5 createClass 方法創建組件自動綁定this
  • 在渲染時使用bind方法綁定
  • 在渲染時使用箭頭函數綁定
  • 在 constructor 構造函數中綁定
  • 使用 等號 加 箭頭函數 綁定

React.createClass方法

在v13版本之前,react都是使用該方式創建組件,但之后的版本開始推薦使用ES6的class屬性來創建,或者使用無狀態組件的方式。文檔中說明了使用React.createClass方法會自動完成對this的綁定,也就是this會自動指向當前react組件。不過框架和語言發展的大勢所趨,這個方式會被放棄。

渲染時綁定

render() {
    return (
        <User onClick = {this.selectUser.bind(this)}></User>
    )
}

這種方式很常規,并且能夠很好的解決this綁定問題;但同時帶來了另一個問題:每次重新渲染組件時都會創建一個新的函數。雖然聽起來好像很嚴重,但實際上對性能的損耗并不明顯。

渲染時使用箭頭函數

受益于ES6的箭頭函數,可以將this的作用域傳入函數內部。所以可以這樣去綁定this:

render() {
    return (
        <User onClick = {e => this.selectUser(e)}></User>
    )
}

該方式和上一種本質是相同的,都會有潛在的性能問題。

在constructor中綁定

class User extends Component {
    constructor(props) {
        super(props);
        this.selectUser = this.selectUser.bind(this);
    }
    ...
}

因為構造函數只會在組件第一次掛載時執行,所以整個生命周期中只會執行一次。在構造函數中對方法進行this綁定,就不會像前面的方法那樣重復創建新函數而造成性能問題。但是如果方法很多的時候,這個構造函數的可讀性就很差了。

一種實驗性特性的寫法

如果你的代碼配置了 babel 的 transform-class-properties 插件,那么可以使用下面的方法完美解決上面的所有問題。既不會造成性能問題,也不會導致代碼冗長難于閱讀。

//babel配置
{
  "plugins": [
    "transform-class-properties"
  ]
}
class User extends Component {
    selectUser = (e) => {
        ...
    }
}

設定 propTypes 和 defaultProps

這兩個都是react組件的靜態屬性,所有組件都應該有這兩個屬性; propTypes 用來規定每個 props 的類型,而 defaultProps 則是給 props 填充默認值。在15.3版本之前,propTypes 在 React 對象中定義了,但之后的版本則需要使用第三方庫 prop-types 來替代。配置了這兩個屬性后,如果傳遞的 props 類型錯誤會在瀏覽器報錯,可以讓使用組件的人清晰的看到問題。同時它們也起到了文檔的作用。

class Dialog extends Component {
    static propTypes = {
        buttons: PropTypes.array,
        show: PropTypes.bool,
        title: PropTypes.string,
        type: PropTypes.string,
    }
    static defaultProps = {
        buttons: [],
        show: false,
        title: '',
        type: '',
    }
    ...
}

這是一個對話框組件,共設置了四個 props ,并規定了他的類型和默認值。

利用解構的便利

這一點其實就是ES6提供的便利,因為react中有一些組件可能會有很多的props。如果將每個屬性都從props中解構出來,可以很好的提高代碼的可閱讀性。還是剛才那個對話框組件:

render() {
    const {title, show, className, children, buttons, type, autoDectect, ...others} = this.props //解構props
    const styleType = type ? type : 'ios'
    const cls = classNames('tui-dialog', {
        'tui-skin_android': styleType === 'android',
        [className]: className
    })
    return (
        <div style={{display: show ? 'block' : 'none'}}>
            <Mask/>
            <div className={cls} {...others}>
                { title ?
                <div className="tui-dialog__hd">
                    <strong className="weui-dialog__title">{title}</strong>
                </div> : false }
                <div className="tui-dialog__bd">
                    {children}
                </div>
                <div className="tui-dialog__ft">
                    {this.renderButtons()}
                </div>
            </div>
        </div>
    )
}

JSX中的判斷

在實際項目中,我們難免會需要在JSX中進行一些判斷,來分別處理不同情況下的渲染。但這是一件不太容易的事情,因為JSX并不像js一樣擁有判斷語法。我們一般的做法是使用三目運算來處理。比如下面這樣:

return (
    { title ?
        <div className="tui-dialog__hd">
            <strong className="weui-dialog__title">{title}</strong>
        </div> : null
    }
)

但是這并不是最好的寫法,&&運算會比三目運算性能更好,如果可以最好寫成這樣:

return (
    { title &&
        <div className="tui-dialog__hd">
            <strong className="weui-dialog__title">{title}</strong>
        </div>
    }
)

React與Vue使用中的對比

組件寫法

React

在react中,組件的寫法是JSX + inline style,也就是說“all in js”,把結構和樣式都寫進js里;

class AlertBox extends React.Component {
    render() {
        var styleObj = {
            color:"blue",
            fontSize:40,
            fontWeight:"normal"
        } 
        return <h1 style={styleObj} className="alert-text">Hello {this.props.name} {this.props.title}</h1>;
    }
}

Vue

而vue中,推薦的寫法是利用webpack+vue-loader來處理.vue單文件組件,js、css、html在一個文件的不同區域。所以對于用模板構建的應用,vue是更合適的選擇。

<template>
    <div class="alert-box"></div>
</template>
<script>
export default {
    name: 'alertbox',
    data() {
        return {}
    },
    methods: {
    },
    ...
}
</script>
<style>
.alert-box {
    width: 100px;
}
</style>

Virtual DOM處理

Vue

在vue中,它會分析跟蹤每一個組件內部和相互之間的依賴關系,當實例的data值改變時,不需要重新渲染整個組件樹。

React

在react中,每當應用的 state 被改變,該組件包括其后代組件都會重新渲染,所以react中會需要用到 shouldComponentUpdate 這個生命周期函數來進行控制是否重新渲染。

數據綁定

Vue

在vue中,實現了數據的雙向綁定,即對于input這類表單元素可以使用v-model指令來將輸入數據與data中的變量進行雙向綁定。內部原理是監聽表單元素的change事件,來通知model層進行更新。

React

在react中,數據只能單向的綁定,即從model到view;如果需要將表單元素的輸入綁定到model上,需要手動實現:

handleChange(e) {
    this.setState({inputValue: e.target.value})
}
render() {
    return (
        <input ref="input" 
        value={this.state.inputValue} 
        onChange="this.handleChange.bind(this)" />
    )
}

修改狀態

Vue

在vue中,組件的狀態保存在組件實例的data屬性中,當需要修改時,直接用 this.property = xxx 就可以完成,并且這個操作是立即生效的。

React

在React中,組件的狀態保存在state當中,但是要修改state不能直接用this.state.property = xxx,而要使用內置的setState函數來修改。并且該方法不能保證修改的同步性。

監聽數據變化

Vue

在Vue中,官方提供了一個 watch 方法來供我們去監聽組件某個屬性的變化:

watch: {
    count (curVal, oldVal) {
        this.step = curVal - oldVal
    },
    userArr(curVal, oldVal) {
        //這里不會執行
    },
    userArr: {
        handler(curVal, oldVal) {
            //這里會執行
        },
        deep: true
    }
}

初次之外,vue中還提供了 computed 計算屬性來監聽某個屬性從而計算出另一個屬性的值。

React

在React中則沒有提供類似 watch 這樣的api。我們需要使用react組件的某些生命周期函數來間接的做到這一點。

componentWillReceiveProps(object nextProps) {
    //當props改變時,組件會執行獲取新props的鉤子函數,處理一些邏輯
}
componentWillUpdate(object nextProps, object nextState) {
    //當state改變時,組件會重新渲染,更新前的鉤子函數中,可以拿到新的state和props處理一些邏輯
}

React v16版本新特性

內核改變 react-fiber

react-fiber 是為了增強動畫、布局、移動端手勢領域的適用性,最重要的特性是對頁面渲染的優化: 允許將渲染方面的工作拆分為多段進行。

react-fiber 可以為我們提供如下幾個功能:

  • 設置渲染任務的優先
  • 采用新的Diff算法
  • 采用虛擬棧設計允許當優先級更高的渲染任務和較低優先的任務之間來回切換

介紹視頻

render函數可return數組,處理多緯渲染

如下有兩個節點的渲染,由于JSX返回時必須嵌套在一個標簽內,所以必須外層套一個div

render() {
    return(
        <div>
            <div className="box1">
                <p>box1</p>
            </div>
            <div className="box2">
                <p>box2</p>
            </div>
        </div>
    )
}

新版本可直接如下處理,以一個數組的形式渲染多個節點

render() {
    return([
        <div className="box1">
            <p>box1</p>
        </div>,
        <div className="box2">
            <p>box2</p>
        </div>
    ])
}

異常降級處理

var MyGoodView = React.createClass({
    render: function () {
        return <p>Cool</p>;
    }
});
var MyBadView = React.createClass({
    render: function () {
        throw new Error('crap');
    }
});
try {
    // 希望拋出錯誤
    React.render(<MyBadView/>, document.body);
} catch (e) {
    // 進行錯誤降級處理
    React.render(<MyGoodView/>, document.body);
}

這段代碼在之前是無法進入到catch內的,而v16版本允許對錯誤進行降級處理,從而來提高組件的可用性。

重寫 SSR api

依賴 Map、Set數據類型

v16版本開始依賴 Map 和 Set 數據類型,所以需要注意在低版本瀏覽器中使用時需要添加polyfill墊片庫;

v16開始有一個ReactElementValidator.js對元素工廠進行包裝,主要對傳遞給組件的props進行驗證,該文件中的 new Map([['children', true], ['key', true]]) 這句話,在ios8中會報錯:TypeError: Map constructor does not accept arguments ,即Map函數不能接收參數。為了解決該問題需要將react及react-dom降級至v15.x.x。

React性能優化實例

在React中,組件的狀態(state)或屬性(props)改變都會導致整個組件的重新渲染,具體的更新流程如下圖所示:


pic1.png

在首次渲染完成后,組件的屬性/狀態改變,會觸發組件一系列的生命周期函數。從圖中可以看出,SCU(shouldComponentUpdate)鉤子十分關鍵,它決定了組件是否進行re-render去生成虛擬DOM。默認情況下,SCU都會返回true。看下面的例子:

如果我們有圖中結構的一個組件樹,其中綠色的組件是我們需要更新的組件,但是它依賴于從祖父節點傳下來的props來更新。


pic2.png

那么我們期望的更新方式是如下綠色的三個節點。祖父節點的某個state改變,然后將它以props的方式傳遞到孫子節點去更新它。


pic3.png

但是事實情況并不如我們所想,而是整個組件樹都會進行re-render,然后生成虛擬DOM,再對虛擬DOM進行diff操作,若改變則更新,否則不更新。這樣就會造成很多冗余的render和diff操作,圖中黃色節點。


pic4.png

這時候我們就需要利用SCU來規避這種情況,但是SCU需要慎重使用,方式不當可能會造成數據改變但UI不變的bug,也可能導致更嚴重的性能問題。

// 接收兩個參數,分別為待更新的屬性及狀態值
shouldComponentUpdate(nextProps, nextState) {
    // 如果當前的value值與待更新不相等,才執行更新
    return this.props.value !== nextProps.value;
    // return this.state.user !== nextState.user;
}

上面是常用的方式,通過判斷新的某個狀態/屬性與之前的值是否相等,來決定是否重新渲染。同時,React v15.3開始提供了一個叫做 PureComponent 的組件來替代 Component 來封裝 shouldComponentUpdate 以達到上述目的。

import React, { PureComponent } from 'react'
class Example extends PureComponent {
  render() {
    // ...
  }
}

但是這樣實際上有一個問題,它們進行的都是淺比較,也就是說如果對比的值是一個對象或者數組,那么它的引用會一直相等,從而造成誤判。
解決這個問題有兩種方式:

  1. 深比較 該方式十分損耗性能,試想一下如果每次修改 state 都要對該深層嵌套的對象進行一次徹底的遍歷,可能還不如多 render 幾次來得快;
  2. Immutable.js 用來替代淺拷貝和深拷貝的方式,用來創建不可變的數據。
// 原來的寫法
let foo = {a: {b: 1}};
let bar = foo;
bar.a.b = 2;
console.log(foo.a.b);  // 打印 2
console.log(foo === bar);  //  打印 true
// 使用 immutable.js 后
import Immutable from 'immutable';
foo = Immutable.fromJS({a: {b: 1}});
bar = foo.setIn(['a', 'b'], 2);   // 使用 setIn 賦值
console.log(foo.getIn(['a', 'b']));  // 使用 getIn 取值,打印 1
console.log(foo === bar);  //  打印 false

下面是項目中遇到的一個實際情況:

this.state = {
    startTime: new Date().getTime(),
    chartDatas: []
}
shouldComponentUpdate(nextProps, nextState) {
    return this.state.chartDatas.length !== nextState.chartDatas.length
}
render() {
    return (
        <Button 
            onClick={e => {
                this.setState({
                    startTime: _today   //今天開始的數據
                })
            }
        }>
        <Button 
            onClick={e => {
                this.setState({
                    startTime: _yesterday   //昨天開始的數據
                })
            }
        }>
        ...
        {
            this.state.chartDatas.map((item, i) => {
                return(
                    <div className="chartItem" key={i}>
                        <h1 className="chartTitle">{item.name}<span>(單位:{item.sum !== undefined ? '次)' : '毫秒)'}</span></h1>
                        <p className="chartOverall">
                            <span>最小值:<b>{item.sum !== undefined ? formatCurrency(item.min) : item.min.toFixed(2)}</b></span>
                            <span>最大值:<b>{item.sum !== undefined ? formatCurrency(item.max) : item.max.toFixed(2)}</b></span>
                            {
                                item.sum !== undefined ? 
                                <span>總值:<b>{formatCurrency(item.sum)}</b></span> :
                                <span>平均值:<b>{item.average.toFixed(2)}</b></span>
                            }
                        </p>
                        <DetailCharts isReflow={this.state.isReflow} data={[{type: 'area', name: item.name, data: item.data}]}></DetailCharts>
                    </div>
                )
            })
        }
    )
}

在我沒有加上 SCU 之前,我每點擊一次切換時間的按鈕,這時只是修改了 startTime 屬性并沒有改圖表數據,但是依然會觸發組件的重新渲染。所有圖標重新畫了一遍,性能差一點的手機上體驗極差。通過 SCU 改善后,避免了絕大部分的不必要渲染。判斷組件是否渲染的方法很簡單,只要在render函數里加console.log()就可以看到渲染了多少次,從而監控到不必要的渲染。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370